All files / src stereo-ring.js

100% Statements 48/48
100% Branches 19/19
100% Functions 7/7
100% Lines 40/40

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        18x 18x 18x 18x 18x 18x 18x 18x       20x 20x       537x 537x 537x 63924x 63924x 63924x   537x 537x       275x 24x   251x   275x 275x 269x 269x 29069x 29069x 29069x   269x 269x   275x 275x           21x 21x 21x       251x 251x 251x       23x 23x      
// Between a bursty producer and an audio worklet's steady reads: an underrun
// plays silence and waits for targetFrames again; a lapping writer drops the oldest.
export class StereoRing {
    constructor(capacityFrames, { targetFrames, excessFrames, persistentExcessFrames }) {
        this.capacityFrames = capacityFrames;
        this.excessFrames = excessFrames;
        this.persistentExcessFrames = persistentExcessFrames;
        this.frames = new Float32Array(capacityFrames * 2);
        this.readFrame = 0;
        this.occupancy = 0;
        this.running = false;
        this.setTargetFrames(targetFrames);
    }
 
    setTargetFrames(targetFrames) {
        this.targetFrames = targetFrames;
        this.excessRunFrames = 0;
    }
 
    write(interleaved, scale = 1) {
        const count = interleaved.length >> 1;
        let writeFrame = (this.readFrame + this.occupancy) % this.capacityFrames;
        for (let i = 0; i < count; ++i) {
            this.frames[2 * writeFrame] = interleaved[2 * i] * scale;
            this.frames[2 * writeFrame + 1] = interleaved[2 * i + 1] * scale;
            if (++writeFrame === this.capacityFrames) writeFrame = 0;
        }
        this.occupancy += count;
        if (this.occupancy > this.capacityFrames) this._discard(this.occupancy - this.capacityFrames);
    }
 
    read(left, right) {
        if (!this.running) {
            if (this.occupancy >= this.targetFrames) this._restart();
        } else {
            this._skipPersistentExcess(left.length);
        }
        let played = 0;
        if (this.running) {
            played = Math.min(left.length, this.occupancy);
            for (let i = 0; i < played; ++i) {
                left[i] = this.frames[2 * this.readFrame];
                right[i] = this.frames[2 * this.readFrame + 1];
                if (++this.readFrame === this.capacityFrames) this.readFrame = 0;
            }
            this.occupancy -= played;
            if (played < left.length) this.running = false;
        }
        left.fill(0, played);
        right.fill(0, played);
    }
 
    // Trims to the target at a point where the output is already
    // discontinuous: after an underrun, or when skipping a persistent excess.
    _restart() {
        this._discard(Math.max(0, this.occupancy - this.targetFrames));
        this.excessRunFrames = 0;
        this.running = true;
    }
 
    _skipPersistentExcess(frames) {
        const excess = this.occupancy > this.targetFrames + this.excessFrames;
        this.excessRunFrames = excess ? this.excessRunFrames + frames : 0;
        if (this.excessRunFrames >= this.persistentExcessFrames) this._restart();
    }
 
    _discard(count) {
        this.readFrame = (this.readFrame + count) % this.capacityFrames;
        this.occupancy -= count;
    }
}