| | import debug from './debug'; |
| |
|
| | type AddAudioToBufferFunction = ( |
| | samples: Array<number>, |
| | sampleRate: number, |
| | ) => void; |
| |
|
| | export type BufferedSpeechPlayer = { |
| | addAudioToBuffer: AddAudioToBufferFunction; |
| | setGain: (gain: number) => void; |
| | start: () => void; |
| | stop: () => void; |
| | }; |
| |
|
| | type Options = { |
| | onEnded?: () => void; |
| | onStarted?: () => void; |
| | }; |
| |
|
| | export default function createBufferedSpeechPlayer({ |
| | onStarted, |
| | onEnded, |
| | }: Options): BufferedSpeechPlayer { |
| | const audioContext = new AudioContext(); |
| | const gainNode = audioContext.createGain(); |
| | gainNode.connect(audioContext.destination); |
| |
|
| | let unplayedAudioBuffers: Array<AudioBuffer> = []; |
| |
|
| | let currentPlayingBufferSource: AudioBufferSourceNode | null = null; |
| |
|
| | let isPlaying = false; |
| |
|
| | |
| | let shouldPlayWhenAudioAvailable = false; |
| |
|
| | const setGain = (gain: number) => { |
| | gainNode.gain.setValueAtTime(gain, audioContext.currentTime); |
| | }; |
| |
|
| | const start = () => { |
| | shouldPlayWhenAudioAvailable = true; |
| | debug()?.start(); |
| | playNextBufferIfNotAlreadyPlaying(); |
| | }; |
| |
|
| | |
| | const stop = () => { |
| | shouldPlayWhenAudioAvailable = false; |
| |
|
| | |
| | currentPlayingBufferSource?.stop(); |
| | currentPlayingBufferSource = null; |
| |
|
| | unplayedAudioBuffers = []; |
| |
|
| | onEnded != null && onEnded(); |
| | isPlaying = false; |
| | return; |
| | }; |
| |
|
| | const playNextBufferIfNotAlreadyPlaying = () => { |
| | if (!isPlaying) { |
| | playNextBuffer(); |
| | } |
| | }; |
| |
|
| | const playNextBuffer = () => { |
| | if (shouldPlayWhenAudioAvailable === false) { |
| | console.debug( |
| | '[BufferedSpeechPlayer][playNextBuffer] Not playing any more audio because shouldPlayWhenAudioAvailable is false.', |
| | ); |
| | |
| | return; |
| | } |
| | if (unplayedAudioBuffers.length === 0) { |
| | console.debug( |
| | '[BufferedSpeechPlayer][playNextBuffer] No buffers to play.', |
| | ); |
| | if (isPlaying) { |
| | isPlaying = false; |
| | onEnded != null && onEnded(); |
| | } |
| | return; |
| | } |
| |
|
| | |
| | if (isPlaying === false) { |
| | isPlaying = true; |
| | onStarted != null && onStarted(); |
| | } |
| |
|
| | const source = audioContext.createBufferSource(); |
| |
|
| | |
| | const buffer = unplayedAudioBuffers.shift() ?? null; |
| | source.buffer = buffer; |
| | console.debug( |
| | `[BufferedSpeechPlayer] Playing buffer with ${source.buffer?.length} samples`, |
| | ); |
| |
|
| | source.connect(gainNode); |
| |
|
| | const startTime = new Date().getTime(); |
| | source.start(); |
| | currentPlayingBufferSource = source; |
| | |
| | isPlaying = true; |
| |
|
| | |
| | const onThisBufferPlaybackEnded = () => { |
| | console.debug( |
| | `[BufferedSpeechPlayer] Buffer with ${source.buffer?.length} samples ended.`, |
| | ); |
| | source.removeEventListener('ended', onThisBufferPlaybackEnded); |
| | const endTime = new Date().getTime(); |
| | debug()?.playedAudio(startTime, endTime, buffer); |
| | currentPlayingBufferSource = null; |
| |
|
| | |
| | playNextBuffer(); |
| | }; |
| |
|
| | source.addEventListener('ended', onThisBufferPlaybackEnded); |
| | }; |
| |
|
| | const addAudioToBuffer: AddAudioToBufferFunction = (samples, sampleRate) => { |
| | const incomingArrayBufferChunk = audioContext.createBuffer( |
| | |
| | 1, |
| | samples.length, |
| | sampleRate, |
| | ); |
| |
|
| | incomingArrayBufferChunk.copyToChannel( |
| | new Float32Array(samples), |
| | |
| | 0, |
| | ); |
| |
|
| | console.debug( |
| | `[addAudioToBufferAndPlay] Adding buffer with ${incomingArrayBufferChunk.length} samples to queue.`, |
| | ); |
| |
|
| | unplayedAudioBuffers.push(incomingArrayBufferChunk); |
| | debug()?.receivedAudio( |
| | incomingArrayBufferChunk.length / incomingArrayBufferChunk.sampleRate, |
| | ); |
| | const audioBuffersTableInfo = unplayedAudioBuffers.map((buffer, i) => { |
| | return { |
| | index: i, |
| | duration: buffer.length / buffer.sampleRate, |
| | samples: buffer.length, |
| | }; |
| | }); |
| | const totalUnplayedDuration = unplayedAudioBuffers.reduce((acc, buffer) => { |
| | return acc + buffer.length / buffer.sampleRate; |
| | }, 0); |
| |
|
| | console.debug( |
| | `[addAudioToBufferAndPlay] Current state of incoming audio buffers (${totalUnplayedDuration.toFixed( |
| | 1, |
| | )}s unplayed):`, |
| | ); |
| | console.table(audioBuffersTableInfo); |
| |
|
| | if (shouldPlayWhenAudioAvailable) { |
| | playNextBufferIfNotAlreadyPlaying(); |
| | } |
| | }; |
| |
|
| | return {addAudioToBuffer, setGain, stop, start}; |
| | } |
| |
|