let isListening = false; let currentTrack = null; let currentSource = 'system'; // 'system', 'mic', 'device' let currentStream = null; let mediaRecorder = null; let audioChunks = []; let audioContext = null; let analyser = null; let animFrameId = null; let isProcessingAudio = false; 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 ? 'Dark' : 'Light'; if (currentTrack && currentTrack.key) { window.updateWheel(currentTrack.key); } else { window.updateWheel(null); } } // Audio Source Management async function changeAudioSource(source) { currentSource = source; const deviceSelect = document.getElementById('audio-device-select'); if (source === 'device') { deviceSelect.style.display = 'inline-block'; await populateAudioDevices(); } else { deviceSelect.style.display = 'none'; } // If already listening, restart stream with new source if (isListening) { stopAudioStream(); await initAudioStream(); } } async function populateAudioDevices() { const deviceSelect = document.getElementById('audio-device-select'); deviceSelect.innerHTML = ''; try { // Request permission once to get labels await navigator.mediaDevices.getUserMedia({ audio: true }).then(s => s.getTracks().forEach(t => t.stop())).catch(() => {}); const devices = await navigator.mediaDevices.enumerateDevices(); const audioInputs = devices.filter(d => d.kind === 'audioinput'); audioInputs.forEach((d, i) => { const opt = document.createElement('option'); opt.value = d.deviceId; opt.textContent = d.label || `Audioeingang ${i + 1}`; deviceSelect.appendChild(opt); }); } catch (err) { console.warn('[Audio] Could not enumerate devices:', err); } } async function selectSpecificDevice(deviceId) { if (!deviceId) return; currentSource = 'device'; if (isListening) { stopAudioStream(); await initAudioStream(deviceId); } } // Stream Initialization async function initAudioStream(specificDeviceId = null) { try { stopAudioStream(); if (currentSource === 'system') { updateStatus('Wähle Tab/Bildschirm mit Audio...', 'waiting'); // System audio capture (YouTube, VLC, DJ software, browser tabs) const displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false, systemAudio: 'include' } }); const audioTracks = displayStream.getAudioTracks(); if (audioTracks.length === 0) { alert('Kein Audiotrack geteilt! Bitte setze im Teilen-Dialog das Häkchen bei "Audio teilen" (Systemaudio teilen).'); displayStream.getTracks().forEach(t => t.stop()); toggleListening(false); return false; } // Stop video track to save CPU and battery displayStream.getVideoTracks().forEach(t => t.stop()); currentStream = new MediaStream(audioTracks); } else if (currentSource === 'device' && specificDeviceId) { currentStream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: specificDeviceId } } }); } else { // Microphone currentStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false } }); } // When the user stops sharing via the browser bar currentStream.getAudioTracks()[0].onended = () => { console.log('[Audio] Stream ended by user.'); toggleListening(false); }; // Setup Visualizer setupVisualizer(currentStream); return true; } catch (err) { console.error('[Audio] Stream init failed:', err); if (err.name !== 'NotAllowedError') { alert('Audio-Zugriff fehlgeschlagen: ' + err.message); } toggleListening(false); return false; } } function stopAudioStream() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { try { mediaRecorder.stop(); } catch (e) {} } if (currentStream) { currentStream.getTracks().forEach(t => t.stop()); currentStream = null; } if (animFrameId) { cancelAnimationFrame(animFrameId); animFrameId = null; } const meter = document.getElementById('audio-meter-fill'); if (meter) meter.style.width = '0%'; } // Live Visualizer (Level Meter) function setupVisualizer(stream) { try { if (!audioContext || audioContext.state === 'closed') { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } if (audioContext.state === 'suspended') { audioContext.resume(); } const source = audioContext.createMediaStreamSource(stream); analyser = audioContext.createAnalyser(); analyser.fftSize = 256; source.connect(analyser); const dataArray = new Uint8Array(analyser.frequencyBinCount); const meter = document.getElementById('audio-meter-fill'); function updateMeter() { if (!isListening) { meter.style.width = '0%'; return; } analyser.getByteFrequencyData(dataArray); let sum = 0; for (let i = 0; i < dataArray.length; i++) { sum += dataArray[i]; } const avg = sum / dataArray.length; const pct = Math.min(100, Math.round((avg / 128) * 100)); meter.style.width = `${pct}%`; animFrameId = requestAnimationFrame(updateMeter); } updateMeter(); } catch (err) { console.warn('[Visualizer] Error:', err); } } // Listening Loop & Recognition async function toggleListening(forceState = null) { isListening = forceState !== null ? forceState : !isListening; const statusEl = document.getElementById('listening-status'); const icon = statusEl.querySelector('i'); const text = document.getElementById('listening-text'); if (isListening) { statusEl.classList.remove('paused'); statusEl.classList.add('active'); icon.className = 'fa-solid fa-microphone-lines'; const sourceLabel = currentSource === 'system' ? 'PC-Audio' : 'Mikrofon'; text.textContent = `Höre ${sourceLabel}...`; const success = await initAudioStream(); if (success) { startRecordingChunk(); } } else { statusEl.classList.remove('active'); statusEl.classList.add('paused'); icon.className = 'fa-solid fa-pause'; text.textContent = 'Pausiert'; stopAudioStream(); } } function startRecordingChunk() { if (!isListening || !currentStream) return; audioChunks = []; try { mediaRecorder = new MediaRecorder(currentStream, { mimeType: MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : undefined }); mediaRecorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) { audioChunks.push(e.data); } }; mediaRecorder.onstop = async () => { if (!isListening) return; const blob = new Blob(audioChunks, { type: 'audio/webm' }); audioChunks = []; // Check if sound was loud enough before querying recognition if (!isProcessingAudio && blob.size > 1000) { await processAudio(blob); } if (isListening) { setTimeout(startRecordingChunk, 1500); // 1.5s pause between checks } }; // Record for 7 seconds (optimal for ACRCloud / AudD) mediaRecorder.start(); setTimeout(() => { if (mediaRecorder && mediaRecorder.state === 'recording') { mediaRecorder.stop(); } }, 7000); } catch (e) { console.error('[Recorder] Error:', e); } } async function processAudio(blob) { isProcessingAudio = true; updateStatus('Analysiere Track...', 'analyzing'); const formData = new FormData(); formData.append('audio', blob, 'sample.webm'); try { const res = await fetch('/api/recognize', { method: 'POST', body: formData }); const json = await res.json(); if (json.status === 'success' && json.data) { const { title, artist, matchScore, key, bpm, cover_url, isCorrected, preview_url, duration_sec, energy, danceability, happiness } = json.data; // Avoid refetching if same track is still playing if (currentTrack && currentTrack.title.toLowerCase() === title.toLowerCase() && currentTrack.artist.toLowerCase() === artist.toLowerCase()) { updateStatus(`Laufend: ${title}`, 'active'); isProcessingAudio = false; return; } updateStatus(`Erkannt: ${title}`, 'active'); // Select track in UI and load details/suggestions window.selectTrack({ title, artist, cover_url, preview_url, duration_sec, energy, danceability, happiness, camelot_key: key, bpm: bpm, is_corrected: isCorrected, is_local: !!key }); } else if (json.status === 'not_configured') { updateStatus(json.message, 'warning'); } else { const sourceLabel = currentSource === 'system' ? 'PC-Audio' : 'Mikrofon'; updateStatus(`Höre ${sourceLabel}...`, 'active'); } } catch (err) { console.error('[Audio] Recognition network error:', err); updateStatus('Erkennungsfehler', 'warning'); } finally { isProcessingAudio = false; } } function updateStatus(text, type = 'normal') { const statusEl = document.getElementById('listening-status'); const textEl = document.getElementById('listening-text'); if (!textEl) return; textEl.textContent = text; statusEl.className = 'status ' + type; } // Hero View Toggles 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'); } // ---------------------------------------------------- // Global 30-Second Audio Preview Player (Tunebat Style) // ---------------------------------------------------- let activeAudio = null; let activePreviewUrl = null; let activePlayBtn = null; function stopAllPreviews() { if (activeAudio) { activeAudio.pause(); activeAudio.currentTime = 0; activeAudio = null; } activePreviewUrl = null; if (activePlayBtn) { activePlayBtn.classList.remove('playing'); const icon = activePlayBtn.querySelector('i'); if (icon) icon.className = 'fa-solid fa-play'; activePlayBtn = null; } // Also reset hero preview button if needed const heroBtn = document.getElementById('hero-preview-btn'); if (heroBtn) { heroBtn.classList.remove('playing'); const heroIcon = heroBtn.querySelector('i'); if (heroIcon) heroIcon.className = 'fa-solid fa-play'; } } async function togglePreviewTrack(track, buttonEl) { if (!track) return; let previewUrl = track.preview_url; // If clicking on already playing preview -> Pause & Stop if (activeAudio && activePreviewUrl === previewUrl) { stopAllPreviews(); return; } // If another track was playing, stop it first stopAllPreviews(); // If track doesn't have preview_url yet, try to search Deezer dynamically if (!previewUrl) { if (buttonEl) { const icon = buttonEl.querySelector('i'); if (icon) icon.className = 'fa-solid fa-spinner fa-spin'; } try { const sRes = await fetch(`/api/search?q=${encodeURIComponent(track.artist + ' ' + track.title)}`); const sJson = await sRes.json(); if (sJson.status === 'success' && sJson.data && sJson.data.length > 0 && sJson.data[0].preview_url) { previewUrl = sJson.data[0].preview_url; track.preview_url = previewUrl; } } catch (e) { console.warn('[Preview] Could not find preview URL:', e); } } if (!previewUrl) { alert(`Keine 30s-Vorschau für "${track.title}" verfügbar.`); if (buttonEl) { const icon = buttonEl.querySelector('i'); if (icon) icon.className = 'fa-solid fa-play'; } return; } try { activeAudio = new Audio(previewUrl); activePreviewUrl = previewUrl; activePlayBtn = buttonEl; if (buttonEl) { buttonEl.classList.add('playing'); const icon = buttonEl.querySelector('i'); if (icon) icon.className = 'fa-solid fa-pause'; } activeAudio.onended = () => { stopAllPreviews(); }; activeAudio.onerror = (e) => { console.warn('[Preview] Audio playback error:', e); stopAllPreviews(); }; await activeAudio.play(); } catch (err) { console.warn('[Preview] Play failed (user interaction needed):', err); stopAllPreviews(); } } function toggleHeroPreview() { const heroBtn = document.getElementById('hero-preview-btn'); togglePreviewTrack(window.currentTrack, heroBtn); } // Expose globals window.toggleTheme = toggleTheme; window.changeAudioSource = changeAudioSource; window.selectSpecificDevice = selectSpecificDevice; window.toggleListening = toggleListening; window.toggleWheel = toggleWheel; window.toggleCover = toggleCover; window.resetHero = resetHero; window.stopAllPreviews = stopAllPreviews; window.togglePreviewTrack = togglePreviewTrack; window.toggleHeroPreview = toggleHeroPreview; // Initial state document.addEventListener('DOMContentLoaded', () => { window.updateWheel(null); });