147 lines
5.5 KiB
JavaScript
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();
|
|
f |