UNPKG

@just-every/ensemble

Version:

LLM provider abstraction layer with unified streaming interface

342 lines 12.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AudioStreamPlayer = void 0; class AudioStreamPlayer { audioContext = null; sourceNodes = []; gainNodes = []; nextStartTime = 0; expectedChunkIndex = 0; receivedFinalChunk = false; pcmParameters = null; pcmDataQueue = []; bufferDurationTarget = 0.2; bytesPerSample = 2; isFirstBuffer = true; currentFormat = null; fallbackAudio = null; fallbackChunks = []; onFirstAudioPlay; constructor(options = {}) { this.onFirstAudioPlay = options.onFirstAudioPlay; } async initAudioContext() { if (this.audioContext && this.audioContext.state === 'running') { return; } try { if (!this.audioContext) { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } if (this.audioContext.state === 'suspended') { await this.audioContext.resume(); } } catch (error) { console.error('Failed to initialize AudioContext:', error); this.audioContext = null; } } startStream(params, format) { this.stopStream(); this.currentFormat = format; if ((format === 'wav' || format.includes('pcm')) && params) { if (!this.audioContext || this.audioContext.state !== 'running') { console.error('AudioContext not ready'); return; } this.pcmParameters = params; this.bytesPerSample = params.bitDepth / 8; this.expectedChunkIndex = 0; this.receivedFinalChunk = false; this.pcmDataQueue = []; this.isFirstBuffer = true; this.nextStartTime = 0; } else { this.fallbackChunks = []; console.log(`Starting ${format} stream - will play when complete`); } } addChunk(base64Chunk, chunkIndex, isFinalChunk) { const format = this.currentFormat; if (!format) { console.error('No format set'); return; } if (this.receivedFinalChunk) { return; } if (format === 'wav' || format.includes('pcm')) { this._addPcmChunk(base64Chunk, chunkIndex, isFinalChunk); } else { this._addFallbackChunk(base64Chunk, chunkIndex, isFinalChunk, format); } } _addPcmChunk(base64Chunk, chunkIndex, isFinalChunk) { if (!this.audioContext || !this.pcmParameters) { console.error('Not initialized for PCM'); return; } if (chunkIndex !== this.expectedChunkIndex) { console.warn(`Out of order chunk: expected ${this.expectedChunkIndex}, got ${chunkIndex}`); return; } this.expectedChunkIndex++; this.receivedFinalChunk = isFinalChunk; try { const binaryString = atob(base64Chunk); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } if (bytes.length > 0) { this.pcmDataQueue.push(bytes.buffer); } this._processPcmQueue(); } catch (error) { console.error('Error processing PCM chunk:', error); } } _processPcmQueue() { if (!this.audioContext || !this.pcmParameters) { return; } if (this.receivedFinalChunk && this.pcmDataQueue.length === 0 && this.sourceNodes.length === 0) { console.log('PCM stream finished'); this._resetState(); return; } if (!this.currentFormat) { return; } if (this.nextStartTime > this.audioContext.currentTime + 1.0) { setTimeout(() => this._processPcmQueue(), 100); return; } const totalBytes = this.pcmDataQueue.reduce((sum, buffer) => sum + buffer.byteLength, 0); const requiredBytes = this.pcmParameters.sampleRate * this.pcmParameters.channels * this.bytesPerSample * this.bufferDurationTarget; if (totalBytes < requiredBytes && !(this.receivedFinalChunk && totalBytes > 0)) { return; } const bytesToProcess = this.receivedFinalChunk ? totalBytes : requiredBytes; let processedBytes = 0; const buffersToProcess = []; while (processedBytes < bytesToProcess && this.pcmDataQueue.length > 0) { const buffer = this.pcmDataQueue.shift(); buffersToProcess.push(buffer); processedBytes += buffer.byteLength; } if (buffersToProcess.length === 0 || processedBytes === 0) return; let skipBytes = 0; if (this.nextStartTime === 0 && buffersToProcess.length > 0) { const firstBuffer = new Uint8Array(buffersToProcess[0]); if (firstBuffer.length >= 4) { const header = String.fromCharCode(...firstBuffer.slice(0, 4)); if (header === 'RIFF') { skipBytes = 44; } } } const totalSamples = (processedBytes - skipBytes) / this.bytesPerSample; const concatenatedPcm = new Int16Array(totalSamples); let offset = 0; let bytesSkipped = 0; for (const buffer of buffersToProcess) { const view = new DataView(buffer); for (let i = 0; i < buffer.byteLength; i += 2) { if (bytesSkipped < skipBytes) { bytesSkipped += 2; continue; } if (offset < concatenatedPcm.length && i + 1 < buffer.byteLength) { concatenatedPcm[offset] = view.getInt16(i, true); offset++; } } } const float32Array = new Float32Array(concatenatedPcm.length); for (let i = 0; i < concatenatedPcm.length; i++) { float32Array[i] = concatenatedPcm[i] / 32768; } const numberOfSamples = float32Array.length / this.pcmParameters.channels; const audioBuffer = this.audioContext.createBuffer(this.pcmParameters.channels, numberOfSamples, this.pcmParameters.sampleRate); audioBuffer.getChannelData(0).set(float32Array); const sourceNode = this.audioContext.createBufferSource(); sourceNode.buffer = audioBuffer; const gainNode = this.audioContext.createGain(); gainNode.gain.value = 1.0; sourceNode.connect(gainNode); gainNode.connect(this.audioContext.destination); const currentTime = this.audioContext.currentTime; const startTime = this.nextStartTime <= currentTime ? currentTime : this.nextStartTime; sourceNode.start(startTime); this.nextStartTime = startTime + audioBuffer.duration; this.sourceNodes.push(sourceNode); this.gainNodes.push(gainNode); if (this.isFirstBuffer && this.onFirstAudioPlay) { this.isFirstBuffer = false; if (startTime === currentTime) { this.onFirstAudioPlay(); } else { const delay = (startTime - currentTime) * 1000; setTimeout(() => this.onFirstAudioPlay?.(), delay); } } sourceNode.onended = () => { const index = this.sourceNodes.indexOf(sourceNode); if (index > -1) { this.sourceNodes.splice(index, 1); const gainNode = this.gainNodes[index]; if (gainNode) { this.gainNodes.splice(index, 1); } } setTimeout(() => this._processPcmQueue(), 20); }; if (this.pcmDataQueue.length > 0 || !this.receivedFinalChunk) { setTimeout(() => this._processPcmQueue(), 50); } } _addFallbackChunk(base64Chunk, chunkIndex, isFinalChunk, format) { const binaryString = atob(base64Chunk); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } this.fallbackChunks.push(bytes); if (isFinalChunk) { this.receivedFinalChunk = true; if (!this.fallbackAudio) { const mimeType = format === 'mp3' ? 'audio/mpeg' : format === 'opus' ? 'audio/opus' : format === 'aac' ? 'audio/aac' : format === 'flac' ? 'audio/flac' : 'audio/mpeg'; const blob = new Blob(this.fallbackChunks, { type: mimeType }); const url = URL.createObjectURL(blob); this.fallbackAudio = new Audio(); this.fallbackAudio.src = url; this.fallbackAudio .play() .then(() => { if (this.onFirstAudioPlay) { this.onFirstAudioPlay(); } }) .catch(err => console.error('Playback failed:', err)); } } } stopStream() { this.sourceNodes.forEach(node => { try { node.onended = null; node.stop(); } catch { } }); this.sourceNodes = []; this.gainNodes = []; if (this.fallbackAudio) { this.fallbackAudio.pause(); this.fallbackAudio = null; } this._resetState(); } fadeOutAndStop(fadeTimeMs = 150) { this.receivedFinalChunk = true; this.pcmDataQueue = []; if (!this.audioContext) { this.stopStream(); return; } const currentTime = this.audioContext.currentTime; const fadeTimeSeconds = fadeTimeMs / 1000; this.gainNodes.forEach((gainNode, index) => { try { gainNode.gain.cancelScheduledValues(currentTime); gainNode.gain.setValueAtTime(gainNode.gain.value, currentTime); gainNode.gain.linearRampToValueAtTime(0, currentTime + fadeTimeSeconds); const sourceNode = this.sourceNodes[index]; if (sourceNode) { sourceNode.stop(currentTime + fadeTimeSeconds); } } catch { } }); if (this.fallbackAudio && !this.fallbackAudio.paused) { const audio = this.fallbackAudio; const initialVolume = audio.volume; const fadeSteps = 20; const stepTime = fadeTimeMs / fadeSteps; let step = 0; const fadeInterval = setInterval(() => { step++; audio.volume = initialVolume * (1 - step / fadeSteps); if (step >= fadeSteps) { clearInterval(fadeInterval); audio.pause(); this.fallbackAudio = null; } }, stepTime); } const tempSourceNodes = [...this.sourceNodes]; const tempGainNodes = [...this.gainNodes]; this.expectedChunkIndex = 0; this.pcmDataQueue = []; this.fallbackChunks = []; this.nextStartTime = 0; this.isFirstBuffer = true; this.currentFormat = null; setTimeout(() => { tempSourceNodes.forEach(node => { try { node.disconnect(); } catch { } }); tempGainNodes.forEach(node => { try { node.disconnect(); } catch { } }); this.sourceNodes = []; this.gainNodes = []; }, fadeTimeMs + 50); } _resetState() { this.expectedChunkIndex = 0; this.receivedFinalChunk = false; this.pcmDataQueue = []; this.fallbackChunks = []; this.nextStartTime = 0; this.isFirstBuffer = true; this.currentFormat = null; this.gainNodes = []; } get isPlaying() { return this.sourceNodes.length > 0 || (this.fallbackAudio !== null && !this.fallbackAudio.paused); } get isStreaming() { return !this.receivedFinalChunk || this.pcmDataQueue.length > 0 || this.sourceNodes.length > 0; } } exports.AudioStreamPlayer = AudioStreamPlayer; //# sourceMappingURL=audio_stream_player.js.map