feat: enhance media handling and audio processing logic

This commit is contained in:
MythEclipse committed 2026-05-15 22:23:29 +07:00
1 parent 6ac4a5c11a
commit e32e092596
10 files changed
+63 -148

No files matched your search

+12 -10
View File
@@ -69,6 +69,7 @@
isListening: false,
localStreaming: false,
localListening: false,
mediaAutoListening: false,
audioContextTransmit: null,
audioContextListen: null,
processor: null,
@@ -101,7 +102,7 @@
async function disconnectVoice() { await apiRequest('/api/disconnect', { method: 'POST' }); await refreshStatus(); }
function connectWebSocket() { const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; state.socket = new WebSocket(`${protocol}//${location.host}/ws`); state.socket.binaryType = 'arraybuffer'; state.socket.onopen = () => { el.wsDot.classList.add('on'); el.wsStatusText.textContent = 'Connected'; }; state.socket.onclose = () => { el.wsDot.classList.remove('on'); el.wsStatusText.textContent = 'Reconnecting'; setTimeout(connectWebSocket, 2500); }; state.socket.onerror = () => { el.wsDLine truncated
function handleJsonEvent(raw) { const message = JSON.parse(raw); if (message.type === 'ui_state') return applyServerState(message.state); if (message.type === 'user_state') return renderUsers(message.users || []); if (message.type === 'message_created') { state.text.unshift(message.data); renderText(); } if (message.type === 'message_updated') { const item = state.text.find((entry) => entry.id === message.data.id); if (item) Object.assign(item, { edited_content: message.data.edited_content,Line truncated
function handleJsonEvent(raw) { const message = JSON.parse(raw); if (message.type === 'ui_state') return applyServerState(message.state); if (message.type === 'user_state') return renderUsers(message.users || []); if (message.type === 'message_created') { state.text.unshift(message.data); renderText(); } if (message.type === 'message_updated') { const item = state.text.find((entry) => entry.id === message.data.id); if (item) Object.assign(item, { edited_content: message.data.edited_content,Line truncated
async function applyServerState(next) {
if (!next || state.applyingServerState) return;
@@ -134,14 +135,14 @@
if (textChanged || textGuildChanged || state.activeTab === 'text') {
fetchText().catch((error) => showError(error.message));
}
await reconcileListenState();
await reconcileStreamingState();
await reconcileDynamicAudio();
state.applyingServerState = false;
}
function applyActiveTab(tab) { document.querySelectorAll('.tab-btn').forEach((item) => item.classList.toggle('active', item.dataset.tab === tab)); document.querySelectorAll('.tab-content').forEach((item) => item.classList.toggle('active', item.id === tab)); el.activeTabLabel.textContent = tab === 'text' ? 'Text' : 'Voice'; }
async function reconcileListenState() { if (state.isListening && !state.localListening) { try { await startListeningLocal(); } catch (error) { showError(`Speaker error: ${error.message}`); state.isListening = false; stopListeningLocal(); apiRequest('/api/ui-state', { method: 'POST', body: JSON.stringify({ isListening: false }) }).catch((postError) => showError(postError.message)); } } else if (!state.isListening && state.localListening) { stopListeningLocal(); } }
async function reconcileStreamingState() { if (state.isStreaming && !state.localStreaming) { try { await startStreamingLocal(); } catch (error) { showError(`Microphone error: ${error.message}`); state.isStreaming = false; stopStreamingLocal(); apiRequest('/api/ui-state', { method: 'POST', body: JSON.stringify({ isStreaming: false }) }).catch((postError) => showError(postError.message)); } } else if (!state.isStreaming && state.localStreaming) { stopStreamingLocal(); } }
async function reconcileDynamicAudio() { await reconcileStreamingState(); await reconcileListenState(); }
async function reconcileListenState() { const shouldListen = state.isListening || !!state.media.current; if (shouldListen && !state.localListening) { try { await startListeningLocal(!!state.media.current && !state.isListening); } catch (error) { showError(`Speaker error: ${error.message}`); state.isListening = false; state.mediaAutoListening = false; stopListeningLocal(); apiRequest('/api/ui-state', { method: 'POST', body: JSON.stringify({ isListening: false }) }).catch((postError) => showELine truncated
async function reconcileStreamingState() { if (state.media.current && state.isStreaming) { state.isStreaming = false; apiRequest('/api/ui-state', { method: 'POST', body: JSON.stringify({ isStreaming: false }) }).catch((postError) => showError(postError.message)); } if (state.isStreaming && !state.localStreaming) { try { await startStreamingLocal(); } catch (error) { showError(`Microphone error: ${error.message}`); state.isStreaming = false; stopStreamingLocal(); apiRequest('/api/ui-state', Line truncated
function renderUsers(users) { el.userList.replaceChildren(); if (users.length === 0) return appendEmpty(el.userList, 'No active speakers'); for (const user of users) { const row = document.createElement('div'); row.className = `user-item${user.speaking ? ' speaking' : ''}`; const img = document.createElement('img'); img.src = user.avatar || ''; img.alt = ''; const name = document.createElement('span'); name.textContent = user.username; row.append(img, name); el.userList.appendChild(row); } Line truncated
async function fetchText() { if (!state.selectedTextChannel) return renderText(); const result = await apiRequest(`/api/messages?channel=${encodeURIComponent(state.selectedTextChannel)}&type=text&limit=80`); state.text = result.data || []; renderText(); }
@@ -152,8 +153,9 @@
function handleIncomingPCM(data) { if (!state.localListening || !state.audioContextListen) return; const headerView = new DataView(data, 0, 4); const userIdHash = headerView.getInt32(0, true); const audioData = data.slice(4); const int16Array = new Int16Array(audioData); const float32Array = new Float32Array(int16Array.length); for (let i = 0; i < int16Array.length; i++) float32Array[i] = int16Array[i] / 32768; const audioBuffer = state.audioContextListen.createBuffer(CHANNELS, float32ArrayLine truncated
async function startStreamingLocal() { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); state.localStreaming = true; el.toggleBtn.textContent = 'Stop Transmitting'; state.audioContextTransmit = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE }); const source = state.audioContextTransmit.createMediaStreamSource(stream); const analyser = state.audioContextTransmit.createAnalyser(); analyser.fftSize = 64; source.connect(analyser); consLine truncated
function stopStreamingLocal() { state.localStreaming = false; if (state.processor) state.processor.disconnect(); if (state.audioContextTransmit) state.audioContextTransmit.close(); state.processor = null; state.audioContextTransmit = null; el.toggleBtn.textContent = 'Start Transmitting'; bars.forEach((bar) => { bar.style.height = '2px'; }); }
async function startListeningLocal() { if (!state.audioContextListen) state.audioContextListen = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE }); await state.audioContextListen.resume(); state.localListening = true; el.listenBtn.textContent = 'Stop Listening'; el.listenStatus.textContent = 'Listening Live...'; }
function stopListeningLocal() { state.audioContextListen?.suspend(); state.userTimelines.clear(); state.localListening = false; el.listenBtn.textContent = 'Join Listen Channel'; el.listenStatus.textContent = 'Speaker Off'; }
async function startListeningLocal(auto = false) { if (!state.audioContextListen) state.audioContextListen = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE }); await state.audioContextListen.resume(); state.localListening = true; state.mediaAutoListening = auto; renderListenStatus(); }
function stopListeningLocal() { state.audioContextListen?.suspend(); state.userTimelines.clear(); state.localListening = false; state.mediaAutoListening = false; renderListenStatus(); }
function renderListenStatus() { el.listenBtn.textContent = state.isListening ? 'Stop Listening' : 'Join Listen Channel'; el.listenStatus.textContent = state.localListening ? (state.media.current && state.mediaAutoListening ? 'Media Monitor On' : 'Listening Live...') : 'Speaker Off'; }
function updateVisualizer(level) { bars.forEach((bar, index) => { const wave = Math.sin(index * 0.55 + Date.now() / 140) * 0.35 + 0.65; bar.style.height = `${Math.max(3, level * 190 * wave)}px`; }); }
document.querySelectorAll('.tab-btn').forEach((button) => { button.addEventListener('click', () => postUIState({ activeTab: button.dataset.tab }).catch((error) => showError(error.message))); });
@@ -167,9 +169,9 @@
el.channelFilter.addEventListener('change', () => { const selectedTextChannel = el.channelFilter.value; const url = new URL(location.href); if (selectedTextChannel) url.searchParams.set('channel', selectedTextChannel); else url.searchParams.delete('channel'); if (el.textGuildSelect.value) url.searchParams.set('guild', el.textGuildSelect.value); history.replaceState({}, '', url); postUIState({ selectedTextChannel }).catch((error) => showError(error.message)); });
async function fetchMediaStatus() { state.media = await apiRequest('/api/media/status'); renderMedia(); }
async function queueMedia() { const source = el.mediaSourceInput.value.trim(); if (!source) return showError('Enter a music URL or local file path'); state.media = await apiRequest('/api/media/queue', { method: 'POST', body: JSON.stringify({ source }) }); el.mediaSourceInput.value = ''; renderMedia(); }
async function skipMedia() { state.media = await apiRequest('/api/media/skip', { method: 'POST' }); renderMedia(); }
async function stopMedia() { state.media = await apiRequest('/api/media/stop', { method: 'POST' }); renderMedia(); }
async function queueMedia() { const source = el.mediaSourceInput.value.trim(); if (!source) return showError('Enter a music URL or local file path'); if (state.isStreaming || state.localStreaming) await postUIState({ isStreaming: false }); state.media = await apiRequest('/api/media/queue', { method: 'POST', body: JSON.stringify({ source }) }); el.mediaSourceInput.value = ''; await reconcileDynamicAudio(); renderMedia(); }
async function skipMedia() { state.media = await apiRequest('/api/media/skip', { method: 'POST' }); await reconcileDynamicAudio(); renderMedia(); }
async function stopMedia() { state.media = await apiRequest('/api/media/stop', { method: 'POST' }); await reconcileDynamicAudio(); renderMedia(); }
function renderMedia() { el.mediaQueueList.replaceChildren(); const current = state.media.current; el.mediaStatus.textContent = current ? `Playing ${current.title}` : 'Idle'; if (current) { const item = document.createElement('div'); item.className = 'event-card'; item.textContent = `Now: ${current.title}`; el.mediaQueueList.appendChild(item); } for (const queued of state.media.queue || []) { const item = document.createElement('div'); item.className = 'event-card'; item.textContent = queueLine truncated
el.queueMediaBtn.addEventListener('click', () => queueMedia().catch((error) => showError(error.message)));