(() => { const state = { audioContext: null, destination: null, micStream: null, systemStream: null, mixedStream: null, recorder: null, chunks: [], animationFrame: null, analyzers: {}, downloadUrl: null, }; const els = { connectMic: document.getElementById('connect-mic'), connectSystem: document.getElementById('connect-system'), start: document.getElementById('start-recording'), stop: document.getElementById('stop-recording'), reset: document.getElementById('reset-audio'), micStatus: document.getElementById('mic-status'), systemStatus: document.getElementById('system-status'), mixStatus: document.getElementById('mix-status'), micMeter: document.getElementById('mic-meter'), systemMeter: document.getElementById('system-meter'), mixMeter: document.getElementById('mix-meter'), log: document.getElementById('audio-log'), result: document.getElementById('recording-result'), preview: document.getElementById('recording-preview'), download: document.getElementById('download-recording'), sharedVideo: document.getElementById('shared-video-preview'), }; if (!navigator.mediaDevices || !window.MediaRecorder) { log('This browser does not support the required MediaDevices/MediaRecorder APIs. Try current Chrome or Edge over HTTPS.'); [els.connectMic, els.connectSystem, els.start].forEach((button) => { if (button) button.disabled = true; }); return; } els.connectMic?.addEventListener('click', connectMicrophone); els.connectSystem?.addEventListener('click', connectSystemAudio); els.start?.addEventListener('click', startRecording); els.stop?.addEventListener('click', stopRecording); els.reset?.addEventListener('click', resetAudio); window.addEventListener('beforeunload', cleanupTracks); async function ensureAudioGraph() { if (state.audioContext) return; const AudioContextClass = window.AudioContext || window.webkitAudioContext; state.audioContext = new AudioContextClass(); state.destination = state.audioContext.createMediaStreamDestination(); state.mixedStream = state.destination.stream; state.analyzers.mix = createAnalyser(); const mixSource = state.audioContext.createMediaStreamSource(state.mixedStream); mixSource.connect(state.analyzers.mix); updateStatus(els.mixStatus, 'Waiting', 'muted'); startMeters(); } async function connectMicrophone() { try { await ensureAudioGraph(); await state.audioContext.resume(); const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); replaceStream('micStream', stream); connectStream(stream, 'mic'); updateStatus(els.micStatus, 'Connected', 'active'); log('Microphone connected.'); refreshButtons(); } catch (error) { handleCaptureError('microphone', error); } } async function connectSystemAudio() { try { await ensureAudioGraph(); await state.audioContext.resume(); const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false, }, }); if (!stream.getAudioTracks().length) { stopStream(stream); throw new Error('No audio track was shared. Reopen sharing and enable tab/system audio.'); } replaceStream('systemStream', stream); connectStream(stream, 'system'); els.sharedVideo.srcObject = stream; els.sharedVideo.play().catch(() => {}); updateStatus(els.systemStatus, 'Connected', 'active'); log('Computer/tab audio connected. Keep the shared source open while recording.'); stream.getTracks().forEach((track) => { track.addEventListener('ended', () => { if (state.systemStream === stream) { updateStatus(els.systemStatus, 'Stopped', 'muted'); log('Shared computer/tab audio stopped by the browser.'); state.systemStream = null; refreshButtons(); } }); }); refreshButtons(); } catch (error) { handleCaptureError('computer/tab audio', error); } } function connectStream(stream, sourceName) { const audioTracks = stream.getAudioTracks(); if (!audioTracks.length) return; const audioOnlyStream = new MediaStream(audioTracks); const source = state.audioContext.createMediaStreamSource(audioOnlyStream); const analyser = createAnalyser(); state.analyzers[sourceName] = analyser; source.connect(analyser); source.connect(state.destination); updateStatus(els.mixStatus, 'Mix ready', 'active'); } function createAnalyser() { const analyser = state.audioContext.createAnalyser(); analyser.fftSize = 256; analyser.smoothingTimeConstant = 0.78; return analyser; } function startMeters() { if (state.animationFrame) return; const data = new Uint8Array(128); const draw = () => { drawMeter(state.analyzers.mic, els.micMeter, data); drawMeter(state.analyzers.system, els.systemMeter, data); drawMeter(state.analyzers.mix, els.mixMeter, data); state.animationFrame = requestAnimationFrame(draw); }; draw(); } function drawMeter(analyser, element, data) { if (!element) return; if (!analyser) { element.style.width = '0%'; return; } analyser.getByteTimeDomainData(data); let sum = 0; for (const value of data) { const centered = value - 128; sum += centered * centered; } const rms = Math.sqrt(sum / data.length); const width = Math.min(100, Math.max(3, rms * 4.6)); element.style.width = `${width}%`; } function startRecording() { if (!state.mixedStream || !state.mixedStream.getAudioTracks().length) { log('Connect at least one audio source before recording.'); return; } cleanupRecordingUrl(); state.chunks = []; const options = pickRecorderOptions(); state.recorder = new MediaRecorder(state.mixedStream, options); state.recorder.addEventListener('dataavailable', (event) => { if (event.data && event.data.size > 0) state.chunks.push(event.data); }); state.recorder.addEventListener('stop', () => { const mimeType = state.recorder?.mimeType || 'audio/webm'; const blob = new Blob(state.chunks, { type: mimeType }); state.downloadUrl = URL.createObjectURL(blob); els.preview.src = state.downloadUrl; els.download.href = state.downloadUrl; els.download.download = `reliefsignal-audio-test-${new Date().toISOString().replace(/[:.]/g, '-')}.webm`; els.result.hidden = false; log(`Recording ready (${formatBytes(blob.size)}).`); refreshButtons(); }); state.recorder.start(1000); updateStatus(els.mixStatus, 'Recording', 'recording'); log('Local recording started.'); refreshButtons(); } function stopRecording() { if (state.recorder && state.recorder.state !== 'inactive') { state.recorder.stop(); updateStatus(els.mixStatus, 'Mix ready', hasAnySource() ? 'active' : 'muted'); log('Recording stopped.'); refreshButtons(); } } function resetAudio() { if (state.recorder && state.recorder.state !== 'inactive') { state.recorder.stop(); } cleanupTracks(); cleanupRecordingUrl(); state.micStream = null; state.systemStream = null; state.mixedStream = null; state.destination = null; state.analyzers = {}; state.audioContext?.close().catch(() => {}); state.audioContext = null; state.recorder = null; state.chunks = []; if (state.animationFrame) cancelAnimationFrame(state.animationFrame); state.animationFrame = null; els.preview.removeAttribute('src'); els.download.removeAttribute('href'); els.result.hidden = true; els.sharedVideo.srcObject = null; updateStatus(els.micStatus, 'Not connected', 'muted'); updateStatus(els.systemStatus, 'Not connected', 'muted'); updateStatus(els.mixStatus, 'Waiting', 'muted'); [els.micMeter, els.systemMeter, els.mixMeter].forEach((meter) => { if (meter) meter.style.width = '0%'; }); log('Audio capture reset.'); refreshButtons(); } function refreshButtons() { const recording = state.recorder && state.recorder.state === 'recording'; const hasSource = hasAnySource(); els.start.disabled = !hasSource || recording; els.stop.disabled = !recording; els.reset.disabled = !hasSource && !state.downloadUrl; els.connectMic.disabled = Boolean(state.micStream) || recording; els.connectSystem.disabled = Boolean(state.systemStream) || recording; } function hasAnySource() { return Boolean( state.micStream?.getAudioTracks().some((track) => track.readyState === 'live') || state.systemStream?.getAudioTracks().some((track) => track.readyState === 'live') ); } function pickRecorderOptions() { const candidates = [ 'audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', ]; const mimeType = candidates.find((type) => MediaRecorder.isTypeSupported(type)); return mimeType ? { mimeType } : {}; } function replaceStream(key, stream) { if (state[key]) stopStream(state[key]); state[key] = stream; } function stopStream(stream) { stream?.getTracks().forEach((track) => track.stop()); } function cleanupTracks() { stopStream(state.micStream); stopStream(state.systemStream); } function cleanupRecordingUrl() { if (state.downloadUrl) { URL.revokeObjectURL(state.downloadUrl); state.downloadUrl = null; } } function updateStatus(element, label, mode) { if (!element) return; element.textContent = label; element.classList.remove('active', 'muted', 'recording'); element.classList.add(mode); } function log(message) { if (!els.log) return; const item = document.createElement('li'); item.textContent = `${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} — ${message}`; els.log.prepend(item); } function handleCaptureError(label, error) { const message = error?.name === 'NotAllowedError' ? `Permission was not granted for ${label}.` : error?.message || `Could not connect ${label}.`; log(message); } function formatBytes(bytes) { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); return `${(bytes / Math.pow(1024, index)).toFixed(index ? 1 : 0)} ${units[index]}`; } })();