|
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}; |
|
} |
|
|