let isListening = true; let currentTrack = null; function toggleTheme() { const html = document.documentElement; const isLight = html.getAttribute('data-theme') === 'light'; const next = isLight ? 'dark' : 'light'; html.setAttribute('data-theme', next); document.getElementById('theme-btn').textContent = isLight ? 'Light' : 'Dark'; if (currentTrack && currentTrack.key) { window.updateWheel(currentTrack.key); } else { window.updateWheel(null); } } let mediaRecorder; let audioChunks = []; let stream = null; async function startRecordingLoop() { if (!isListening) return; try { if (!stream) { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); } mediaRecorder = new MediaRecorder(stream); mediaRecorder.ondataavailable = event => { if (event.data.size > 0) { audioChunks.push(event.data); } }; mediaRecorder.onstop = async () => { if (!isListening) return; const audioBlob = new Blob(audioChunks); audioChunks = []; await processAudio(audioBlob); // Start next chunk immediately if still listening if (isListening) { startRecordingLoop(); } }; // Record for 12 seconds mediaRecorder.start(); setTimeout(() => { if (mediaRecorder.state === 'recording') { mediaRecorder.stop(); } }, 12000); } catch (err) { console.error("Microphone access denied or error:", err); alert("Microphone access is required to detect songs."); if (isListening) toggleListening(); } } async function processAudio(blob) { const formData = new FormData(); formData.append('audio', blob, 'recording.webm'); try { const recRes = await fetch('/api/recognize', { method: 'POST', body: formData }); const recData = await recRes.json(); if (recData.status === 'success') { const { title, artist, matchScore } = recData.data; // Don't update if it's the exact same track to avoid flicker if (currentTrack && currentTrack.title === title && currentTrack.artist === artist) { return; } document.getElementById('track-title').textContent = title; document.getElementById('track-artist').textContent = artist; document.getElementById('stat-match').textContent = matchScore; // Fetch metadata const metaRes = await fetch(`/api/metadata?track=${encodeURIComponent(title)}&artist=${encodeURIComponent(artist)}`); const metaData = await metaRes.json(); if (metaData.status === 'success') { const { key, bpm, tags } = metaData.data; currentTrack = { title, artist, key, bpm }; document.getElementById('stat-key').textContent = key; document.getElementById('stat-key-sub').textContent = window.camelotToMusicKey ? window.camelotToMusicKey[key] : ''; document.getElementById('stat-bpm').textContent = bpm; const tagsContainer = document.getElementById('tags-row'); tagsContainer.innerHTML = ''; const keyTag = document.createElement('span'); keyTag.className = 'tag tag-key'; keyTag.textContent = key; keyTag.style.backgroundColor = window.colorMatrix ? window.colorMatrix[key] : 'var(--key-current)'; tagsContainer.appendChild(keyTag); tags.forEach(t => { const el = document.createElement('span'); el.className = 'tag tag-genre'; el.textContent = t; tagsContainer.appendChild(el); }); window.updateWheel(key); document.documentElement.style.setProperty('--key-current', window.colorMatrix ? window.colorMatrix[key] : '#4F9D69'); } } } catch (e) { console.error("Error processing audio", e); } } function toggleListening() { isListening = !isListening; const statusEl = document.getElementById('listening-status'); const icon = statusEl.querySelector('i'); const text = document.getElementById('listening-text'); if (isListening) { statusEl.classList.remove('paused'); icon.className = 'fa-solid fa-microphone'; text.textContent = 'Listening'; startRecordingLoop(); } else { statusEl.classList.add('paused'); icon.className = 'fa-solid fa-pause'; text.textContent = 'Paused'; if (mediaRecorder && mediaRecorder.state === 'recording') { mediaRecorder.stop(); } } } // Initial state window.updateWheel(null); startRecordingLoop(); function toggleWheel() { const hero = document.getElementById('hero'); if (hero.classList.contains('wheel-mode')) { resetHero(); } else { hero.classList.remove('cover-mode'); hero.classList.add('wheel-mode'); } } function toggleCover() { const hero = document.getElementById('hero'); if (hero.classList.contains('cover-mode')) { resetHero(); } else { hero.classList.remove('wheel-mode'); hero.classList.add('cover-mode'); } } function resetHero() { document.getElementById('hero').classList.remove('wheel-mode', 'cover-mode'); }