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 | 6x 6x 6x 6x 6x 6x 6x 6x 128x 6x 15x 15x 30x 15x 2400x 30000x 6x 6x 6x 30x 30x 75x 45x 15x 15x 75x 6000x 15x 50x 50x 43x 58x 50x 150x 50x 15x 58x | // The stages between the sound chip and the device, shared by the worklet and
// by tools that replay the same path headlessly. Values and their sources are
// in docs/audio-path.md.
import { Biquad } from "./biquad.js";
// The board's output filter is an equal-component Sallen-Key (Service Manual
// section 3.8: 10K and 2n2 twice, gain K = 1 + 22/39): f0 = 1/(2*pi*RC) = 7234 Hz,
// Q = 1/(3 - K) = 0.696, below 1/sqrt(2), so no resonant peak. Run at the chip
// rate, ahead of the resampler.
export const OutputFilterHz = 7234;
export const OutputFilterQ = 0.696;
// The coupling capacitors after the filter, each a first-order high-pass:
// C10 100nF into the LM386's 50K input, C79 330nF into 4K7 + 1K (Master only),
// and C18 47uF into the 8 ohm speaker.
export const BoardHighPassHz = [32, 85, 423];
// The internal speaker and case, fitted to microphone captures of a Master 128
// (the driver's resonance, a presence bump, and the roll-off above 6 kHz).
export const SpeakerStages = [
{ type: "highPass", frequency: 550, q: 1.56 },
{ type: "peaking", frequency: 460, q: 3.5, gainDb: 16.5 },
{ type: "peaking", frequency: 2750, q: 0.5, gainDb: 13.9 },
{ type: "highShelf", frequency: 5600, q: 1.3, gainDb: -16.7 },
{ type: "lowPass", frequency: 11700, q: 1.7 },
];
// The resampler's sinc is cut off below the output Nyquist so that its
// transition band has finished before anything folds; sampled sound rides on
// a 31 kHz or higher carrier that would otherwise land in the audible band.
export const ResamplerCutoffOfOutputRate = 0.4;
export const ResamplerTaps = 201;
export const AudioOutputs = Object.freeze({
speaker: "speaker",
board: "board",
off: "off",
});
export const DefaultAudioOutput = AudioOutputs.speaker;
export function isAudioOutput(value) {
return Object.values(AudioOutputs).includes(value);
}
const stageFactories = {
lowPass: (rate, s) => Biquad.lowPass(rate, s.frequency, s.q),
highPass: (rate, s) => Biquad.highPass(rate, s.frequency, s.q),
peaking: (rate, s) => Biquad.peaking(rate, s.frequency, s.q, s.gainDb),
highShelf: (rate, s) => Biquad.highShelf(rate, s.frequency, s.q, s.gainDb),
};
const GainProbeHz = [...Array(400).keys()].map((i) => 20 * 2 ** (i / 40));
function chainMagnitude(stages, sampleRate, hz) {
return stages.reduce((gain, stage) => gain * stage.magnitudeAt(sampleRate, hz), 1);
}
const FlatQ = Math.SQRT1_2;
const FlatHighPassHz = 20;
const FlatLowPassHz = 20000;
const lerp = (from, to, t) => from + (to - from) * t;
const lerpLog = (from, to, t) => from * (to / from) ** t;
// The fit scaled toward flat: gains shrink, the resonant Q falls to
// Butterworth, and the band edges move out of the way.
function scaledStage(stage, amount) {
switch (stage.type) {
case "peaking":
case "highShelf":
return { ...stage, gainDb: stage.gainDb * amount };
case "highPass":
return {
...stage,
frequency: lerpLog(FlatHighPassHz, stage.frequency, amount),
q: lerp(FlatQ, stage.q, amount),
};
case "lowPass":
return {
...stage,
frequency: lerpLog(FlatLowPassHz, stage.frequency, amount),
q: lerp(FlatQ, stage.q, amount),
};
default:
throw new Error(`Unknown stage type ${stage.type}`);
}
}
// The speaker's presence bump would push loud chords past full scale, so the
// chain is scaled to peak at unity across the band.
function speakerStages(sampleRate, amount) {
const stages = SpeakerStages.map((stage) => stageFactories[stage.type](sampleRate, scaledStage(stage, amount)));
const peak = Math.max(...GainProbeHz.map((hz) => chainMagnitude(stages, sampleRate, hz)));
return [...stages, Biquad.gain(1 / peak)];
}
// Zero turns the whole path off; a setting the biquad cannot realise
// (non-finite, at or above Nyquist, non-positive Q) falls back to the board's
// own values.
function boardFilter(sampleRate, frequency = OutputFilterHz, q = OutputFilterQ) {
const usable = frequency < sampleRate / 2 && q > 0;
if (!usable) return Biquad.lowPass(sampleRate, OutputFilterHz, OutputFilterQ);
return Biquad.lowPass(sampleRate, frequency, q);
}
/**
* The filters to run on the chip's output, in order, for the chosen output.
*
* @param {number} sampleRate the chip's rate
* @param {string} output one of AudioOutputs
* @param {{filterHz?: number, filterQ?: number, speakerAmount?: number}} options overrides for the
* board's low-pass, and how much of the speaker fit to apply (1 is the fit, 0 is flat)
* @returns {Biquad[]}
*/
export function outputStages(sampleRate, output, { filterHz, filterQ, speakerAmount = 1 } = {}) {
if (output === AudioOutputs.off || filterHz <= 0) return [];
const board = [
boardFilter(sampleRate, filterHz, filterQ),
...BoardHighPassHz.map((hz) => Biquad.firstOrderHighPass(sampleRate, hz)),
];
if (output === AudioOutputs.board) return board;
const amount = Number.isFinite(speakerAmount) ? Math.min(Math.max(speakerAmount, 0), 1) : 1;
return [...board, ...speakerStages(sampleRate, amount)];
}
|