// Elements const recordToggle = document.getElementById('record-toggle'); const playBtn = document.getElementById('play-btn'); const statusText = document.getElementById('status-text'); const indicator = document.getElementById('recording-indicator'); const micIcon = document.getElementById('mic-icon'); const stopIcon = document.getElementById('stop-icon'); const playIcon = document.getElementById('play-icon'); const pauseIcon = document.getElementById('pause-icon'); const audioPlayback = document.getElementById('audio-playback'); const transcriptDiv = document.getElementById('id_transcript'); const submitButton = document.getElementById('submit_button'); const SAMPLE_RATE = 44100; let audioContext; let processor; let source; let recordedChunks = []; let isRecording = false; let hasRecording = false; // Request mic with all browser processing disabled (important for pro mics) navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, sampleRate: SAMPLE_RATE, echoCancellation: false, noiseSuppression: false, autoGainControl: false, }, video: false, }).then(stream => { audioContext = new AudioContext({ sampleRate: SAMPLE_RATE }); source = audioContext.createMediaStreamSource(stream); }).catch(err => { console.error('Mic error:', err); statusText.textContent = 'Mic access denied'; recordToggle.disabled = true; }); // Record toggle recordToggle.addEventListener('click', () => { if (!isRecording) startRecording(); else stopRecording(); }); function startRecording() { recordedChunks = []; isRecording = true; processor = audioContext.createScriptProcessor(4096, 1, 1); processor.onaudioprocess = (e) => { if (isRecording) { recordedChunks.push(new Float32Array(e.inputBuffer.getChannelData(0))); } }; source.connect(processor); processor.connect(audioContext.destination); // Stop any active playback if (!audioPlayback.paused) { audioPlayback.pause(); audioPlayback.currentTime = 0; showPlayIcon(); } statusText.textContent = 'Recording...'; indicator.classList.add('active'); indicator.classList.remove('has-recording'); micIcon.style.display = 'none'; stopIcon.style.display = 'block'; recordToggle.classList.add('recording'); playBtn.disabled = true; transcriptDiv.textContent = ''; } function stopRecording() { isRecording = false; source.disconnect(processor); processor.disconnect(); processor = null; const wavBuffer = encodeWAV(recordedChunks, SAMPLE_RATE); const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' }); audioPlayback.src = URL.createObjectURL(audioBlob); hasRecording = true; statusText.textContent = 'Uploading...'; indicator.classList.remove('active'); indicator.classList.add('has-recording'); micIcon.style.display = 'block'; stopIcon.style.display = 'none'; recordToggle.classList.remove('recording'); playBtn.disabled = false; const reader = new FileReader(); reader.onloadend = () => { liveSend({ text: reader.result.split(',')[1] }); }; reader.readAsDataURL(audioBlob); } // WAV encoder — 16-bit PCM, mono function encodeWAV(chunks, sampleRate) { let totalLength = 0; for (const c of chunks) totalLength += c.length; const samples = new Float32Array(totalLength); let offset = 0; for (const c of chunks) { samples.set(c, offset); offset += c.length; } const buffer = new ArrayBuffer(44 + samples.length * 2); const view = new DataView(buffer); function str(off, s) { for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i)); } str(0, 'RIFF'); view.setUint32( 4, 36 + samples.length * 2, true); str(8, 'WAVE'); str(12, 'fmt '); view.setUint32(16, 16, true); // chunk size view.setUint16(20, 1, true); // PCM view.setUint16(22, 1, true); // mono view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * 2, true); // byte rate view.setUint16(32, 2, true); // block align view.setUint16(34, 16, true); // bits per sample str(36, 'data'); view.setUint32(40, samples.length * 2, true); let off = 44; for (let i = 0; i < samples.length; i++) { const s = Math.max(-1, Math.min(1, samples[i])); view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7FFF, true); off += 2; } return buffer; } // Play / Pause playBtn.addEventListener('click', () => { if (audioPlayback.paused) { audioPlayback.play(); showPauseIcon(); } else { audioPlayback.pause(); showPlayIcon(); } }); audioPlayback.addEventListener('ended', showPlayIcon); function showPlayIcon() { playIcon.style.display = 'block'; pauseIcon.style.display = 'none'; } function showPauseIcon() { playIcon.style.display = 'none'; pauseIcon.style.display = 'block'; } function liveRecv(data) { statusText.textContent = 'Saved'; submitButton.disabled = false; } document.addEventListener('DOMContentLoaded', () => liveSend({}));