Files
KeySelector/public/app.js
T

147 lines
5.5 KiB
JavaScript

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'); }