All files / src ddnoise.js

74.28% Statements 52/70
60.71% Branches 17/28
33.33% Functions 6/18
73.68% Lines 42/57

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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135    17x 17x 17x 17x           17x 17x 17x 17x 17x   17x       12x 12x 12x 12x 12x                                                                                           20x 20x 19x 19x 19x 7x 7x   12x 12x 12x 230x 230x 230x           230x   12x       15x 15x                 6x 6x 3x 68x 3x         19x 4x 4x 4x 55x                              
import { SamplePlayer } from "./sample-player.js";
 
const Idle = 0;
const SpinUp = 1;
const Spinning = 2;
const Volume = 0.25;
/**
 * seek3.wav is a drive stepping at the DFS's 24 ms a step. Where its first click begins and
 * how far apart they come were fitted across the file's clicks, so a grain cut on that grid
 * holds one click; a run is such grains at the controller's own step rate.
 */
const RunFirstClickSeconds = 0.0085;
const RunClickSeconds = 0.024209;
const RunClicks = 73;
const GrainLeadSeconds = 0.002;
const GrainFadeSeconds = 0.002;
/** Where step.wav's burst gives way to its ring: what a run's last click leaves behind. */
const SettleOffsetSeconds = 0.024;
 
export class DdNoise extends SamplePlayer {
    constructor(context, destination) {
        super(context, destination, Volume);
        this.state = Idle;
        this.motor = null;
        this.run = null;
        this.clickEnds = 0;
    }
 
    async initialise() {
        await this.loadSounds({
            motorOn: "sounds/disc525/motoron.wav",
            motorOff: "sounds/disc525/motoroff.wav",
            motor: "sounds/disc525/motor.wav",
            step: "sounds/disc525/step.wav",
            seek3: "sounds/disc525/seek3.wav",
        });
    }
 
    spinUp() {
        if (this.state === Spinning || this.state === SpinUp) return;
        this.state = SpinUp;
        this.play(this.sounds.motorOn).then(
            () => {
                // Handle race: we may have had spinDown() called on us before the
                // spinUp() initial sound finished playing.
                if (this.state === Idle) return;
                this.play(this.sounds.motor, true).then((source) => {
                    this.motor = source;
                    this.state = Spinning;
                });
            },
            () => {},
        );
    }
 
    spinDown() {
        if (this.state === Idle) return;
        this.state = Idle;
        if (this.motor) {
            this.motor.stop();
            this.motor = null;
            this.oneShot(this.sounds.motorOff);
        }
    }
 
    /**
     * The head is about to take `steps` steps, one every `stepMs`: a click for one step, held
     * off while the last click still sounds, otherwise a run of clicks scheduled on the audio
     * clock at that rate, with the click's ring as the settle after the last.
     */
    seekStart(steps, stepMs) {
        if (steps < 0) steps = -steps;
        if (steps === 0) return;
        this.cancelRun();
        const now = this.context.currentTime;
        if (steps === 1) {
            if (now >= this.clickEnds) this.clickEnds = now + this.oneShot(this.sounds.step);
            return;
        }
        const stepSeconds = stepMs / 1000;
        const grains = [];
        for (let step = 0; step < steps; ++step) {
            const at = now + step * stepSeconds;
            const click = Math.floor(Math.random() * RunClicks);
            const source = this.startSound(this.sounds.seek3, {
                when: at,
                offset: RunFirstClickSeconds + click * RunClickSeconds - GrainLeadSeconds,
                duration: RunClickSeconds,
                fadeSeconds: GrainFadeSeconds,
            });
            grains.push({ source, at });
        }
        this.run = { grains, settle: this.scheduleSettle(now + steps * stepSeconds), start: now, stepSeconds };
    }
 
    scheduleSettle(at) {
        const source = this.startSound(this.sounds.step, { when: at, offset: SettleOffsetSeconds });
        return { source, at };
    }
 
    /**
     * The head has stopped after `steps` steps. Fewer than announced drops the clicks past
     * that and brings the ring forward to where the last of them was to sound. The run stays
     * until a new movement, so that a settle still to come can give way to it.
     */
    seekEnd(steps) {
        const run = this.run;
        if (!run || steps >= run.grains.length) return;
        const now = this.context.currentTime;
        for (const grain of [...run.grains.splice(steps), run.settle]) Eif (grain.at > now) grain.source?.stop();
        run.settle = this.scheduleSettle(run.start + steps * run.stepSeconds);
    }
 
    /** Stops the parts of a run that have not yet sounded. */
    cancelRun() {
        if (!this.run) return;
        const { grains, settle } = this.run;
        this.run = null;
        const now = this.context.currentTime;
        for (const grain of [...grains, settle]) if (grain.at > now) grain.source?.stop();
    }
}
 
export class FakeDdNoise {
    seekStart() {}
    seekEnd() {}
    initialise() {
        return Promise.resolve();
    }
    spinUp() {}
    spinDown() {}
    mute() {}
    unmute() {}
}