let audioContext; let audioBufferSource = null; let savedChunks = []; let isPlaying = false; // Indicates if we're currently playing something let chunkIdCounter = 0; function startAudioContext(givenSampleRate) { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: givenSampleRate, // Set the sample rate }); console.log("play_audio: tartAudioContext: AudioContext created with sampleRate", audioContext.sampleRate); console.log("play_audio: startAudioContext: Native sample rate is:", audioContext.sampleRate); } } /** * pushChunk: Receives PCM or WAV data, sample rate, and format. * - If format==='wav', skip 44 bytes of WAV header. * - If format==='pcm', use the data as-is. * - Then store the chunk in savedChunks and (if not already playing) start playback. * * @param {Uint8Array|ArrayBuffer} data - The raw audio data * @param {number} sampleRate - Desired sample rate * @param {string} format - 'wav' or 'pcm' */ function pushChunk(data, sampleRate, format) { startAudioContext(sampleRate); let arrayBuffer; if (data instanceof Uint8Array) { arrayBuffer = data.buffer; } else if (data instanceof ArrayBuffer) { arrayBuffer = data; } else { console.error('Invalid data type. Expected Uint8Array or ArrayBuffer.'); return; } let dataOffset = 0; if (format === 'wav') { dataOffset = 44; } const pcmData = extractPCMData(arrayBuffer, dataOffset); const chunkId = chunkIdCounter++; savedChunks.push({ pcmData, sampleRate, chunkId }); // console.log(`📥 pushChunk: Added chunk #${chunkId} with ${pcmData.length} samples`); // savedChunks.push({ pcmData, sampleRate }); if (!isPlaying && savedChunks.length >= MIN_BUFFERED_CHUNKS) { playNextChunk(); } } function extractPCMData(buffer, dataOffset = 0) { const view = new DataView(buffer); const length = view.byteLength - dataOffset; const float32Data = new Float32Array(length / 2); for (let i = 0; i < length; i += 2) { float32Data[i / 2] = view.getInt16(dataOffset + i, true) / 32768.0; } return float32Data; } let scheduledTime = null; const MIN_BUFFERED_CHUNKS = 5; /** * playNextChunk: Plays the first chunk in the savedChunks queue, then * on `ended`, removes it and recursively plays the next one. */ function playNextChunk() { if (savedChunks.length === 0) { isPlaying = false; scheduledTime = null; return; } const { pcmData, sampleRate, chunkId } = savedChunks[0]; const duration = pcmData.length / sampleRate; if (scheduledTime === null || scheduledTime < audioContext.currentTime) { scheduledTime = audioContext.currentTime + 0.2 // 200ms delay for buffering network latency } // console.log("Actual AudioContext sampleRate:", audioContext.sampleRate); // console.log(`▶️ Playing chunk #${chunkId} | Samples: ${pcmData.length} | Duration ~${(duration * 1000).toFixed(1)}ms`); const audioBuffer = audioContext.createBuffer(1, pcmData.length, sampleRate); audioBuffer.copyToChannel(pcmData, 0); const source = audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(audioContext.destination); source.start(scheduledTime); scheduledTime += duration; savedChunks.shift(); source.onended = () => { playNextChunk(); }; // source.onended = () => { // reportInDart(); // }; } /** * stopAudio: Stops current playback, if any. */ function stopAudio() { if (audioBufferSource) { audioBufferSource.stop(); audioBufferSource.disconnect(); audioBufferSource = null; } savedChunks = []; isPlaying = false; } /** * reportInDart: Notifies Dart that a chunk has finished playing. */ function reportInDart() { window.parent.postMessage({ type: 'chunkPlayed' }, '*'); } /** * Legacy code for processing WAV files * @param {Uint8Array | ArrayBuffer} data * @param {int} dataOffset * @returns */ function processWavFile(data, dataOffset) { if (dataOffset == 44) { sampleRate = 16000; } else sampleRate = 8000; startAudioContext(sampleRate); if (data instanceof Uint8Array) { arrayBuffer = data.buffer; } else if (data instanceof ArrayBuffer) { arrayBuffer = data; } else { console.error('Invalid data type. Expected Uint8Array or ArrayBuffer.'); return; } const pcmData = extractPCMData(arrayBuffer, dataOffset); savedChunks = [pcmData]; playChunks(sampleRate); }