hey-buddy-onnx
Version:
Audio WakeWord Detection
152 lines (141 loc) • 5.69 kB
JavaScript
/** @module audio */
// Minified worklet code
const workletName = "hey-buddy";
const workletBlob = new Blob([`(()=>{class t extends AudioWorkletProcessor{constructor(t){super(t),this.targetSampleRate=t.processorOptions.targetSampleRate,this.inputBuffer=new Float32Array(this.inputFrameSize),this.inputBufferSize=0,this.outputBuffer=new Float32Array(this.targetFrameSize)}get inputFrameSize(){return Math.round(sampleRate/50)}get targetFrameSize(){return Math.round(this.targetSampleRate/50)}async flush(){const t=sampleRate/this.targetSampleRate;this.outputBuffer.fill(0);for(let e=0;e<this.targetFrameSize;e++){const i=e*t,r=Math.floor(i),s=Math.min(r+1,this.targetFrameSize-1),u=i-r;this.outputBuffer[e]=this.inputBuffer[r]*(1-u)+this.inputBuffer[s]*u}await this.port.postMessage(this.outputBuffer)}pushAudio(t){const e=t.length,i=this.inputFrameSize-this.inputBufferSize;if(e<i)return this.inputBuffer.set(t,this.inputBufferSize),void(this.inputBufferSize+=e);this.inputBuffer.set(t.subarray(0,i),this.inputBufferSize),this.flush(),this.inputBufferSize=0,this.pushAudio(t.subarray(i))}process(t,e,i){return this.pushAudio(t[0][0]),!0}}registerProcessor("${workletName}",t)})();`], {type: "application/javascript"});
const workletUrl = URL.createObjectURL(workletBlob);
/**
* A class that batches audio samples and calls a callback with the batch.
*/
export class AudioBatcher {
/**
* @param {number} batchSeconds - The number of seconds to batch.
* @param {number} batchIntervalSeconds - The number of seconds to wait before calling the callback.
* @param {number} targetSampleRate - The target sample rate of the worklet.
*/
constructor(
batchSeconds=2.0,
batchIntervalSeconds=0.05, // 50ms
targetSampleRate=16000,
) {
this.initialized = false;
this.callbacks = [];
this.batchSeconds = batchSeconds;
this.batchIntervalSeconds = batchIntervalSeconds;
this.batchIntervalCount = 0;
this.targetSampleRate = targetSampleRate;
this.buffer = new Float32Array(this.batchSamples);
this.buffer.fill(0);
this.initialize();
}
/**
* The number of samples in a batch.
* @type {number}
*/
get batchSamples() {
return Math.floor(this.batchSeconds * this.targetSampleRate);
}
/**
* The number of samples in a batch interval.
* @type {number}
*/
get batchIntervalSamples() {
return Math.floor(this.batchIntervalSeconds * this.targetSampleRate);
}
/**
* Clears the buffer.
*/
clearBuffer() {
this.buffer.fill(0);
}
/**
* Pushes new audio samples into the buffer.
* @param {Float32Array} data - The new audio samples.
*/
push(data) {
const dataLength = data.length;
// Shift the buffer back by this much
this.buffer.set(this.buffer.subarray(dataLength));
// Append the new data
this.buffer.set(data, this.buffer.length - dataLength);
this.batchIntervalCount += dataLength;
// If we have enough samples, call the callbacks and reset the interval count
if (this.batchIntervalCount >= this.batchIntervalSamples) {
this.callbacks.forEach(callback => callback(this.buffer));
this.batchIntervalCount = 0;
}
}
/**
* Adds a callback to be called with each batch.
* @param {Function} callback - The callback to add.
*/
onBatch(callback) {
this.callbacks.push(callback);
}
/**
* Removes a callback from the list of callbacks.
* @param {Function} callback - The callback to remove.
*/
offBatch(callback) {
this.callbacks = this.callbacks.filter(c => c !== callback);
}
/**
* Initializes the audio batcher.
*/
async initialize() {
if (this.initialized) {
return;
}
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
echoCancellation: true,
autoGainControl: true,
noiseSuppression: true,
}
});
this.audioContext = new AudioContext();
this.sourceNode = new MediaStreamAudioSourceNode(
this.audioContext,
{ mediaStream: this.stream }
);
this.workerNode = await AudioNode.create(
this.audioContext,
this.targetSampleRate,
);
this.sourceNode.connect(this.workerNode.worker);
this.workerNode.worker.port.onmessage = (event) => {
this.push(event.data);
}
this.clearBuffer();
this.initialized = true;
}
}
/**
* A class that wraps an AudioWorkletNode.
*/
export class AudioNode {
/**
* @param {AudioContext} context - The audio context.
* @param {AudioWorkletNode} worker - The audio worklet node.
*/
constructor(context, worker) {
this.context = context;
this.worker = worker;
}
/**
* Creates an AudioNode.
* @param {AudioContext} context - The audio context.
* @param {number} targetSampleRate - The target sample rate of the worklet.
* @returns {Promise<AudioNode>} The created AudioNode.
*/
static async create(context, targetSampleRate) {
await context.audioWorklet.addModule(workletUrl);
const workletOptions = {
processorOptions: {
targetSampleRate: targetSampleRate,
}
};
const worker = new AudioWorkletNode(context, workletName, workletOptions);
return new AudioNode(context, worker);
}
}