All files / src/utils pcm-processor.ts

0% Statements 0/89
0% Branches 0/1
0% Functions 0/1
0% Lines 0/89

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105                                                                                                                                                                                                                 
const VOLUME_THRESHOLD = 0.01;
const MAX_SILENCE_DURATION_SECONDS = 5;
const MAX_BUFFER_SIZE = 10;
 
class PcmProcessor extends AudioWorkletProcessor {
 
    private targetSampleRate = 0;
    private silenceCountDown = 0;
    private readonly audioBuffer: Float32Array[] = [];
 
    constructor() {
        super();
        this.port.onmessage = this.handleMessage.bind(this);
    }
 
    private handleMessage(event: MessageEvent) {
        this.targetSampleRate = event.data;
    }
 
    // noinspection JSUnusedGlobalSymbols
    process(inputs: Float32Array[][]): boolean {
        if (this.targetSampleRate >= 0 && inputs[0].length > 0) {
            const float32Data = inputs[0][0];
            const downSampled = downSampleBuffer(float32Data, sampleRate, this.targetSampleRate);
            const volume = calculateRootMeanSquare(downSampled);
            if (volume >= VOLUME_THRESHOLD) {
                this.sendAudio(downSampled);
                this.silenceCountDown = MAX_SILENCE_DURATION_SECONDS;
            } else if (this.silenceCountDown > 0) {
                this.sendAudio(downSampled);
                const samples = downSampled.length;
                this.silenceCountDown -= calculateChunkDuration(samples, this.targetSampleRate);
                this.silenceCountDown = Math.max(0, this.silenceCountDown);
            }
        }
        return true;
    }
 
    private sendAudio(audio: Float32Array) {
        this.audioBuffer.push(audio);
        if (this.audioBuffer.length === MAX_BUFFER_SIZE) {
            const totalLength = this.audioBuffer.reduce((acc, val) => acc + val.length, 0);
            const combined = new Float32Array(totalLength);
            let offset = 0;
            for (const audio of this.audioBuffer) {
                combined.set(audio, offset);
                offset += audio.length;
            }
            const uint8Buffer = float32ToUint8Pcm(combined);
            this.port.postMessage(uint8Buffer);
            this.audioBuffer.length = 0;
        }
    }
}
 
function downSampleBuffer(buffer: Float32Array, originalSampleRate: number, targetSampleRate: number): Float32Array {
    if (targetSampleRate >= originalSampleRate) {
        return buffer;
    }
    const sampleRateRatio = originalSampleRate / targetSampleRate;
    const newLength = Math.round(buffer.length / sampleRateRatio);
    const result = new Float32Array(newLength);
    let offsetResult = 0;
    let offsetBuffer = 0;
    while (offsetResult < result.length) {
        const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
        // Use average value of skipped samples
        let accumulator = 0;
        let count = 0;
        for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
            accumulator += buffer[i];
            count++;
        }
        result[offsetResult] = accumulator / count;
        // Or you can simply get rid of the skipped samples:
        // result[offsetResult] = buffer[nextOffsetBuffer];
        offsetResult++;
        offsetBuffer = nextOffsetBuffer;
    }
    return result;
}
 
function calculateRootMeanSquare(input: Float32Array): number {
    let sum = 0;
    for (let i = 0; i < input.length; i++) {
        sum += input[i] * input[i];
    }
    return Math.sqrt(sum / input.length);
}
 
function float32ToUint8Pcm(input: Float32Array): ArrayBuffer {
    const output = new DataView(new ArrayBuffer(input.length * 2));
    for (let i = 0; i < input.length; i++) {
        const sample = Math.max(-1, Math.min(1, input[i]));
        output.setInt16(i * 2, sample * 32767, true);
    }
    return output.buffer;
}
 
function calculateChunkDuration(samples: number, sampleRate: number): number {
    return samples / sampleRate;
}
 
registerProcessor("pcm-processor", PcmProcessor);