aolsenjazz/libsamplerate-js

View on GitHub
src/src.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import { toChunks, copyOrWriteArray } from './util';
import { ModuleType } from './module-type';
import { ConverterTypeValue } from './converter-type';

/**
 * The length (in `float`s) of the input and output buffers used to transmit data between
 * JS and WASM. Each buffer is currently set to ~4MB.
 */
const BUFFER_LENGTH = 1008000;

/**
 * Manages communication between WASM code and JS
 */
export class SRC {
    /** WASM module containing C code. Load outside of this class because it must be loaded async */
    module: ModuleType;

    /** Float32Array used to transmit data from JS to WASM */
    sourceArray: Float32Array;

    /** Float32Array used to receive data from WASM */
    targetArray: Float32Array;

    _converterType: ConverterTypeValue;
    _nChannels: number;
    _inputSampleRate: number;
    _outputSampleRate: number;
    ratio: number;
    isDestroyed: boolean;

    /**
     * Run WASM module initialization and retrieves WASM data transmission arrays. Data transmission to WASM
     * code is owned by the WASM module to avoid extra copies
     * @param mod the loaded WASM module
     * @param converterType     ConverterType object. See benchmarks to get a sense of which is best for you.
     * @param nChannels         the number of output channels. 1-8 supported
     * @param inputSampleRate   The sample rate of whatever source audio you want to resample
     * @param outputSampleRate  If playing audio in-browser, this should be equal to AudioContext.sampleRate
     */
    constructor(
        mod: ModuleType,
        converterType: ConverterTypeValue,
        nChannels: number,
        inputSampleRate: number,
        outputSampleRate: number
    ) {
        this.module = mod;
        this._converterType = converterType;
        this._nChannels = nChannels;
        this._inputSampleRate = inputSampleRate;
        this._outputSampleRate = outputSampleRate;
        this.ratio = outputSampleRate / inputSampleRate;
        this.isDestroyed = false;

        // init can cause heap memory to be increased, so call it before we get references to arrays below
        mod.init(nChannels, converterType, inputSampleRate, outputSampleRate);

        this.sourceArray = mod.sourceArray(BUFFER_LENGTH);
        this.targetArray = mod.targetArray(BUFFER_LENGTH);
    }

    /**
     * Calls the libsamplerate `simple` API. This should be used when resampling one individual chunk of audio,
     * and no more calls to are required. If more calls are required, use the `full` API. If the array submitted
     * is > 4MB, audio will be broken up into chunks and the `full` API will be used
     *
     * More (and better) info available at: http://www.mega-nerd.com/SRC/api_simple.html
     *
     * @param dataIn Float32Array containing mono|interleaved audio data where -1 < dataIn[i] < 1
     * @returns The resampled data
     */
    simple(dataIn: Float32Array): Float32Array {
        return this._resample(this.module.simple, dataIn);
    }

    /**
     * Calls the libsamplerate `full` API. This should be used when resampling several chunks of the
     * sample audio, e.g. receiving a live stream from WebRTC/websocket API.
     *
     * More (and better) info available at: http://www.mega-nerd.com/SRC/api_full.html
     *
     * @param dataIn Float32Array containing mono|interleaved audio data where -1 < dataIn[i] < 1
     * @param dataOut Optionally, pass a Float32Array to avoid allocating an extra array for every resampling operation
     * @returns The resampled data. If dataOut != null, dataOut is returned
     */
    full(
        dataIn: Float32Array,
        dataOut: Float32Array | null = null
    ): Float32Array {
        return this._resample(this.module.full, dataIn, dataOut);
    }

    /**
     * Cleans up WASM SRC resources. Once this is called on an instance, that instance must be
     * reinitialized with src.init() before it can be used again.
     *
     * TODO: destroy is a gross name
     */
    destroy(): void {
        if (this.isDestroyed === true) {
            console.warn('destroy() has already been called on this instance');
        } else {
            this.module.destroy();
            this.isDestroyed = true;
        }
    }

    get inputSampleRate(): number {
        return this._inputSampleRate;
    }

    set inputSampleRate(inputSampleRate: number) {
        this._inputSampleRate = inputSampleRate;

        this.module.destroy();
        this.module.init(
            this.nChannels,
            this.converterType,
            this.inputSampleRate,
            this.outputSampleRate
        );
    }

    get outputSampleRate(): number {
        return this._outputSampleRate;
    }

    set outputSampleRate(outputSampleRate: number) {
        this._outputSampleRate = outputSampleRate;

        this.module.destroy();
        this.module.init(
            this.nChannels,
            this.converterType,
            this.inputSampleRate,
            this.outputSampleRate
        );
    }

    get nChannels(): number {
        return this._nChannels;
    }

    set nChannels(nChannels: number) {
        this._nChannels = nChannels;

        this.module.destroy();
        this.module.init(
            this.nChannels,
            this.converterType,
            this.inputSampleRate,
            this.outputSampleRate
        );
    }

    get converterType(): ConverterTypeValue {
        return this._converterType;
    }

    set converterType(converterType: ConverterTypeValue) {
        this._converterType = converterType;

        this.module.destroy();
        this.module.init(
            this.nChannels,
            this.converterType,
            this.inputSampleRate,
            this.outputSampleRate
        );
    }

    /**
     * Splits a large (> 4MB) chunk of audio into many smaller pieces, to be consumed by the SRC `full` api.
     *
     * @param dataIn Float32Array containing mono|interleaved audio data where -1 < dataIn[i] < 1
     * @returns The resampled data
     */
    _chunkAndResample(dataIn: Float32Array): Float32Array {
        let accumulatedSize = 0;
        const resampledChunks: Float32Array[] = [];

        const chunkSize = (this.inputSampleRate / 10) * this.nChannels;
        const chunks = toChunks(dataIn, chunkSize, Float32Array) as Float32Array[];

        for (let i = 0; i < chunks.length; i++) {
            const resampled = this._resample(this.module.full, chunks[i]);
            accumulatedSize += resampled.length;
            resampledChunks.push(resampled);
        }

        const accumulated = new Float32Array(accumulatedSize);
        let accumulatedIndex = 0;
        for (let i = 0; i < resampledChunks.length; i++) {
            for (let j = 0; j < resampledChunks[i].length; j++) {
                accumulated[accumulatedIndex++] = resampledChunks[i][j];
            }
        }

        return accumulated;
    }

    /**
     * Calls libsamplerate `full` or `simple` API after validating data. If dataIn > 4MB,
     * uses _chunkAndResample instead.
     *
     * @param resampleFunc this.module.simple || this.module.full
     * @param dataIn Float32Array containing mono|interleaved audio data where -1 < dataIn[i] < 1
     * @param dataOut if resampleFunc === this.module.full, pass an optional resuable buffer to avoid extra allocations
     * @returns The resampled audio, if any
     */
    _resample(
        resampleFunc: ModuleType['simple'] | ModuleType['full'],
        dataIn: Float32Array,
        dataOut: Float32Array | null = null
    ): Float32Array {
        // if we don't actually need to resample, just copy values
        if (this.inputSampleRate === this.outputSampleRate) return dataIn;

        if (dataOut !== null && dataOut.length < this.ratio * dataIn.length)
            throw 'dataOut must be at least ceil(srcRatio * dataIn.length) samples long';

        // if client is trying to resample a big piece of audio, process in chunks
        const projectedSize = Math.ceil(dataIn.length * this.ratio);
        if (Math.max(projectedSize, dataIn.length) > BUFFER_LENGTH)
            return this._chunkAndResample(dataIn);

        this.sourceArray.set(dataIn);

        // outputFrames are *per channel*
        const outputFrames = resampleFunc(
            dataIn.length,
            this.nChannels, // ignored by module.full()
            this.converterType, // ignored by module.full()
            this.inputSampleRate, // ignored by module.full()
            this.outputSampleRate // ignored by module.full()
        );

        return copyOrWriteArray(
            outputFrames * this.nChannels,
            this.targetArray,
            dataOut
        );
    }
}