Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Spectral Analyzer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .spectrum-bg { | |
| background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); | |
| } | |
| .bar { | |
| transition: height 0.1s ease-out; | |
| background: linear-gradient(to top, #3b82f6, #8b5cf6); | |
| } | |
| .frequency-line { | |
| position: absolute; | |
| width: 100%; | |
| height: 1px; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| .frequency-label { | |
| position: absolute; | |
| right: 0; | |
| transform: translateY(50%); | |
| color: rgba(255, 255, 255, 0.5); | |
| font-size: 0.75rem; | |
| } | |
| .visualizer-container { | |
| position: relative; | |
| } | |
| canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-4xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> | |
| Spectral Analyzer | |
| </h1> | |
| <p class="text-gray-400">Real-time audio frequency visualization</p> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <div class="lg:col-span-2"> | |
| <div class="spectrum-bg rounded-xl shadow-xl p-4"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-3 h-3 rounded-full bg-red-500"></div> | |
| <div class="w-3 h-3 rounded-full bg-yellow-500"></div> | |
| <div class="w-3 h-3 rounded-full bg-green-500"></div> | |
| </div> | |
| <div class="text-sm text-gray-400"> | |
| <span id="status">Waiting for input...</span> | |
| </div> | |
| </div> | |
| <div class="visualizer-container relative h-64 w-full" id="visualizer"> | |
| <!-- Frequency labels will be added here --> | |
| <canvas id="canvas" class="rounded-lg"></canvas> | |
| <div id="frequencyLabels" class="absolute top-0 left-0 w-full h-full pointer-events-none"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="bg-gray-800 rounded-xl shadow-xl p-6 h-full"> | |
| <h2 class="text-xl font-semibold mb-4 text-purple-400">Controls</h2> | |
| <button id="toggleMic" class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center transition mb-4"> | |
| <i class="fas fa-microphone mr-2"></i> | |
| <span>Start Analysis</span> | |
| </button> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">FFT Size</label> | |
| <select id="fftSize" class="w-full bg-gray-700 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-purple-500 outline-none"> | |
| <option value="64">64</option> | |
| <option value="128">128</option> | |
| <option value="256" selected>256</option> | |
| <option value="512">512</option> | |
| <option value="1024">1024</option> | |
| <option value="2048">2048</option> | |
| <option value="4096">4096</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Smoothing</label> | |
| <input id="smoothing" type="range" min="0" max="1" step="0.01" value="0.6" class="w-full" /> | |
| <div class="text-xs text-gray-400 flex justify-between"> | |
| <span>Sharp</span> | |
| <span>Smooth</span> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center pt-4 border-t border-gray-700"> | |
| <span class="text-sm text-gray-400">Volume</span> | |
| <div id="volumeMeter" class="flex items-center text-sm text-green-400"> | |
| <i class="fas fa-volume-up mr-1"></i> | |
| <span>0.00</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-8 bg-gray-800 rounded-xl shadow-xl p-6"> | |
| <h2 class="text-xl font-semibold mb-4 text-purple-400">Frequency Details</h2> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <div class="bg-gray-700 p-3 rounded-lg"> | |
| <div class="text-sm text-gray-400">Dominant Freq</div> | |
| <div id="dominantFreq" class="text-xl font-mono">0 Hz</div> | |
| </div> | |
| <div class="bg-gray-700 p-3 rounded-lg"> | |
| <div class="text-sm text-gray-400">Bass (20-250Hz)</div> | |
| <div id="bassLevel" class="text-xl font-mono">0.00</div> | |
| </div> | |
| <div class="bg-gray-700 p-3 rounded-lg"> | |
| <div class="text-sm text-gray-400">Mid (250-4kHz)</div> | |
| <div id="midLevel" class="text-xl font-mono">0.00</div> | |
| </div> | |
| <div class="bg-gray-700 p-3 rounded-lg"> | |
| <div class="text-sm text-gray-400">Treble (4k-20kHz)</div> | |
| <div id="trebleLevel" class="text-xl font-mono">0.00</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Audio context variables | |
| let audioContext; | |
| let analyser; | |
| let microphone; | |
| let dataArray; | |
| let isAnalyzing = false; | |
| let animationId; | |
| // DOM elements | |
| const toggleMicBtn = document.getElementById('toggleMic'); | |
| const statusElement = document.getElementById('status'); | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const fftSizeSelect = document.getElementById('fftSize'); | |
| const smoothingInput = document.getElementById('smoothing'); | |
| const volumeMeter = document.getElementById('volumeMeter'); | |
| const dominantFreqElement = document.getElementById('dominantFreq'); | |
| const bassLevelElement = document.getElementById('bassLevel'); | |
| const midLevelElement = document.getElementById('midLevel'); | |
| const trebleLevelElement = document.getElementById('trebleLevel'); | |
| const frequencyLabels = document.getElementById('frequencyLabels'); | |
| // Initialize visualizer | |
| function initVisualizer() { | |
| // Set canvas to full size of its container | |
| const container = document.getElementById('visualizer'); | |
| canvas.width = container.clientWidth; | |
| canvas.height = container.clientHeight; | |
| // Set visual properties | |
| ctx.fillStyle = 'rgb(0, 0, 0)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Add frequency labels | |
| addFrequencyLabels(); | |
| } | |
| // Add frequency labels to the visualizer | |
| function addFrequencyLabels() { | |
| // Clear existing labels | |
| frequencyLabels.innerHTML = ''; | |
| const frequencies = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000]; | |
| const height = frequencyLabels.clientHeight; | |
| frequencies.forEach(freq => { | |
| // Convert frequency to position (logarithmic scale) | |
| const minFreq = 20; | |
| const maxFreq = 20000; | |
| const minPosition = 0; | |
| const maxPosition = height; | |
| // Logarithmic scaling | |
| const position = height - ( | |
| (Math.log10(freq) - Math.log10(minFreq)) / | |
| (Math.log10(maxFreq) - Math.log10(minFreq)) * maxPosition | |
| ); | |
| // Create line | |
| const line = document.createElement('div'); | |
| line.className = 'frequency-line'; | |
| line.style.top = `${position}px`; | |
| frequencyLabels.appendChild(line); | |
| // Create label | |
| const label = document.createElement('div'); | |
| label.className = 'frequency-label pr-1'; | |
| label.style.top = `${position}px`; | |
| label.textContent = freq >= 1000 ? `${Math.round(freq/1000)}k` : freq; | |
| frequencyLabels.appendChild(label); | |
| }); | |
| } | |
| // Start audio analysis | |
| async function startAnalysis() { | |
| try { | |
| // Initialize audio context | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| // Set FFT size | |
| analyser.fftSize = parseInt(fftSizeSelect.value); | |
| const bufferLength = analyser.frequencyBinCount; | |
| dataArray = new Uint8Array(bufferLength); | |
| // Get microphone input | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| microphone = audioContext.createMediaStreamSource(stream); | |
| microphone.connect(analyser); | |
| // Update status | |
| statusElement.textContent = 'Analyzing audio...'; | |
| toggleMicBtn.innerHTML = '<i class="fas fa-microphone-slash mr-2"></i><span>Stop Analysis</span>'; | |
| toggleMicBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); | |
| toggleMicBtn.classList.add('bg-red-600', 'hover:bg-red-700'); | |
| isAnalyzing = true; | |
| // Start visualization | |
| draw(); | |
| } catch (error) { | |
| console.error('Error accessing microphone:', error); | |
| statusElement.textContent = 'Error: ' + error.message; | |
| } | |
| } | |
| // Stop audio analysis | |
| function stopAnalysis() { | |
| if (microphone && audioContext) { | |
| microphone.disconnect(); | |
| audioContext.close(); | |
| } | |
| if (animationId) { | |
| cancelAnimationFrame(animationId); | |
| } | |
| // Clear canvas | |
| ctx.fillStyle = 'rgb(0, 0, 0)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Update status | |
| statusElement.textContent = 'Analysis stopped'; | |
| toggleMicBtn.innerHTML = '<i class="fas fa-microphone mr-2"></i><span>Start Analysis</span>'; | |
| toggleMicBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); | |
| toggleMicBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); | |
| isAnalyzing = false; | |
| // Reset indicators | |
| volumeMeter.innerHTML = '<i class="fas fa-volume-up mr-1"></i><span>0.00</span>'; | |
| dominantFreqElement.textContent = '0 Hz'; | |
| bassLevelElement.textContent = '0.00'; | |
| midLevelElement.textContent = '0.00'; | |
| trebleLevelElement.textContent = '0.00'; | |
| } | |
| // Draw the frequency visualization | |
| function draw() { | |
| animationId = requestAnimationFrame(draw); | |
| analyser.smoothingTimeConstant = parseFloat(smoothingInput.value); | |
| analyser.getByteFrequencyData(dataArray); | |
| // Clear canvas | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const barWidth = canvas.width / dataArray.length; | |
| let volumeSum = 0; | |
| let maxValue = 0; | |
| let maxIndex = 0; | |
| // Calculate frequency ranges | |
| let bassSum = 0; | |
| let midSum = 0; | |
| let trebleSum = 0; | |
| const bassEnd = Math.floor(250 / (audioContext.sampleRate / 2) * dataArray.length); | |
| const midEnd = Math.floor(4000 / (audioContext.sampleRate / 2) * dataArray.length); | |
| for (let i = 0; i < dataArray.length; i++) { | |
| const value = dataArray[i] / 255; | |
| volumeSum += value; | |
| // Track maximum frequency | |
| if (value > maxValue) { | |
| maxValue = value; | |
| maxIndex = i; | |
| } | |
| // Accumulate frequency ranges | |
| if (i < bassEnd) { | |
| bassSum += value; | |
| } else if (i < midEnd) { | |
| midSum += value; | |
| } else { | |
| trebleSum += value; | |
| } | |
| // Draw frequency bar | |
| const barHeight = value * canvas.height; | |
| const x = i * barWidth; | |
| // Create gradient for the bar | |
| const gradient = ctx.createLinearGradient(x, canvas.height - barHeight, x, canvas.height); | |
| gradient.addColorStop(0, 'rgba(59, 130, 246, 0.8)'); | |
| gradient.addColorStop(1, 'rgba(139, 92, 246, 0.8)'); | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight); | |
| } | |
| // Calculate and display volume | |
| const avgVolume = volumeSum / dataArray.length; | |
| volumeMeter.innerHTML = `<i class="fas fa-volume-${avgVolume > 0.5 ? 'up' : avgVolume > 0.1 ? 'down' : 'mute'} mr-1"></i><span>${avgVolume.toFixed(2)}</span>`; | |
| // Calculate dominant frequency | |
| const dominantFreq = maxIndex * (audioContext.sampleRate / 2) / dataArray.length; | |
| dominantFreqElement.textContent = `${Math.round(dominantFreq)} Hz`; | |
| // Calculate and display frequency ranges | |
| const bassAvg = bassSum / bassEnd; | |
| const midAvg = midSum / (midEnd - bassEnd); | |
| const trebleAvg = trebleSum / (dataArray.length - midEnd); | |
| bassLevelElement.textContent = bassAvg.toFixed(2); | |
| midLevelElement.textContent = midAvg.toFixed(2); | |
| trebleLevelElement.textContent = trebleAvg.toFixed(2); | |
| } | |
| // Event listeners | |
| toggleMicBtn.addEventListener('click', () => { | |
| if (isAnalyzing) { | |
| stopAnalysis(); | |
| } else { | |
| startAnalysis(); | |
| } | |
| }); | |
| fftSizeSelect.addEventListener('change', () => { | |
| if (analyser && isAnalyzing) { | |
| analyser.fftSize = parseInt(fftSizeSelect.value); | |
| const bufferLength = analyser.frequencyBinCount; | |
| dataArray = new Uint8Array(bufferLength); | |
| } | |
| }); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| initVisualizer(); | |
| }); | |
| // Initialize | |
| initVisualizer(); | |
| // Add FFT size explanation | |
| const fftSizeLabel = document.createElement('div'); | |
| fftSizeLabel.className = 'text-xs text-gray-400 mt-1'; | |
| fftSizeLabel.textContent = 'Higher values provide more frequency detail'; | |
| fftSizeSelect.parentNode.appendChild(fftSizeLabel); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> | |
| </html> |