All files / src resampler.js

100% Statements 59/59
100% Branches 14/14
100% Functions 6/6
100% Lines 51/51

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  574860x 574860x 574860x 9450364x 9450364x 9450364x   574860x                           44x 44x 44x 44x 44x 44x 44x 2860x 2860x 2860x 574860x 574860x 574860x 574860x 574860x 574860x 574860x   574860x   44x 44x         13373x 13373x 34x 34x 34x   13373x       63895x                 13373x 13373x 1705920x 1705920x 1705920x 1705920x 1705920x 1705920x 1705920x 1705920x 1705920x 1705920x 342889920x 342889920x 342889920x   1705920x           13373x      
function besselI0(x) {
    let sum = 1;
    let term = 1;
    for (let k = 1; k < 50; ++k) {
        term *= (x / (2 * k)) ** 2;
        sum += term;
        if (term < sum * 1e-12) break;
    }
    return sum;
}
 
/**
 * Rate conversion by a Kaiser-windowed sinc evaluated only where an output
 * sample falls, so the cost scales with the output rate, not the input rate.
 * The kernel is tabulated at `phases` fractional offsets and interpolated
 * between them. Each quantum the caller reserves room for its input, renders
 * into `buffer` at `inputOffset`, reads the output with `read`, then calls
 * `commit` to carry the last `taps` input samples over as the next quantum's
 * history. Nothing is allocated once the buffer has reached its working size.
 */
export class PolyphaseResampler {
    constructor(inputRate, cutoffHz, { taps = 201, phases = 64, beta = 8.5 } = {}) {
        this.taps = taps;
        this.phases = phases;
        this.half = (taps - 1) / 2;
        this.table = new Float32Array((phases + 1) * taps);
        const scale = (2 * cutoffHz) / inputRate;
        const norm = besselI0(beta);
        for (let p = 0; p <= phases; ++p) {
            const row = p * taps;
            let sum = 0;
            for (let k = 0; k < taps; ++k) {
                const t = k - this.half - p / phases;
                const x = Math.PI * scale * t;
                const sinc = x === 0 ? 1 : Math.sin(x) / x;
                const u = t / (this.half + 1);
                const window = u <= -1 || u >= 1 ? 0 : besselI0(beta * Math.sqrt(1 - u * u)) / norm;
                this.table[row + k] = scale * sinc * window;
                sum += this.table[row + k];
            }
            for (let k = 0; k < taps; ++k) this.table[row + k] /= sum;
        }
        this.buffer = new Float32Array(taps);
        this.newSamples = 0;
    }
 
    /** Make room for `count` new input samples after the history. */
    reserve(count) {
        const needed = this.taps + count + 1;
        if (this.buffer.length < needed) {
            const grown = new Float32Array(needed * 2);
            grown.set(this.buffer.subarray(0, this.taps));
            this.buffer = grown;
        }
        this.newSamples = count;
    }
 
    get inputOffset() {
        return this.taps;
    }
 
    /**
     * Fill `out` with samples taken `ratio` input samples apart, the first at
     * `phase` (0 to 1) past the last sample of the previous quantum. The
     * output lags the input by `half` samples, which the kernel needs ahead.
     */
    read(out, phase, ratio) {
        const { buffer, table, taps, phases } = this;
        for (let i = 0; i < out.length; ++i) {
            const pos = phase + i * ratio;
            const loc = Math.floor(pos);
            const fracPhase = (pos - loc) * phases;
            const p = Math.min(Math.floor(fracPhase), phases - 1);
            const mix = fracPhase - p;
            const rowA = p * taps;
            const rowB = rowA + taps;
            let a = 0;
            let b = 0;
            for (let k = 0; k < taps; ++k) {
                const x = buffer[loc + k];
                a += table[rowA + k] * x;
                b += table[rowB + k] * x;
            }
            out[i] = a + (b - a) * mix;
        }
    }
 
    /** Keep the last `taps` input samples as history for the next quantum. */
    commit() {
        this.buffer.copyWithin(0, this.newSamples, this.newSamples + this.taps);
    }
}