Shape a sound with every filter type, a Q and a slope (#318)
The Filter node from #335 only knew low-pass, high-pass and band-pass and had a Resonance from 0 to 1. It now has every type Web Audio offers, a Q as a plain factor and a slope up to 48 dB per octave. - Types: low-pass, high-pass, band-pass, notch, low shelf, high shelf, peaking and all-pass. The cutoff row is now labelled Frequency, its key stays `cutoff`. - Q from 0.1 to 30, 0.707 by default, replaces Resonance. Web Audio takes the Q of a low- or high-pass in dB, so it is converted when the voice is wired. No project used the node. - Gain in dB is an input only for the shelves and the peak. It is a derived port, so the sound FX compiler now resolves derived ports. - Slope 12, 24 or 48 dB/oct runs 1, 2 or 4 filters in a row. They share one cutoff carried by a ConstantSource, only the last has the Q. Shelves, peak and all-pass hide the row. - The Chip Presets get the Q their resonance gave before. Mutate moves Q in octaves and gain by up to a dB. - bindStage in the instrument compiler binds every amount of a stage generically, so new stages need no case of their own there. Finding: FLAT_Q was in dB and the delay damping used it. It is a factor now, the damping takes it through passQ and is unchanged. Tests: - Unit envelope 13, sound-library 96 (2 new), chip-presets 14, soundfx 45, all green. - Counter-checks: the Q on every filter in a row, the gain row for every type. Each turns one test red. - e2e 34 (melektaus-atx): ALL PASSED, 76 checks (1 new). Chrome offline, 2 kHz through a low-pass at 1 kHz: -12.37 dB at 12 dB/oct, -49.5 dB at 48. A +6 dB peak gives 6.00 dB, a notch -124 dB. - The gate runs once on the last commit of this series. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
daeffe0f9d
commit
e1ca6a5f5a
9 files changed
+282
-98
No files matched your search
@@ -2,6 +2,7 @@ import {
|
||||
DELAY_MAX,
|
||||
DRY_VOICE,
|
||||
FLAT_Q,
|
||||
passQ,
|
||||
fxChainOf,
|
||||
type FxChain,
|
||||
type FxChip,
|
||||
@@ -2048,22 +2049,48 @@ export function stageNodes(
|
||||
};
|
||||
}
|
||||
|
||||
/** A Filter (#335): a cutoff that moves in octaves, and a Q – a band-pass's as a factor, so in ratios too. */
|
||||
/**
|
||||
* A Filter (#335, #318): a cutoff that moves in octaves, a Q – as a factor in ratios too, in dB for a
|
||||
* low- or high-pass – and a gain in dB. Several in a row share one cutoff, carried by a source of its
|
||||
* own into each of them; only the last has the Q, the others none.
|
||||
*/
|
||||
function passNodes(context: BaseAudioContext, stage: FxPass, follow?: Follow): StageNodes {
|
||||
const filter = context.createBiquadFilter();
|
||||
filter.type = stage.filter;
|
||||
filter.frequency.value = stage.cutoffMod ? 0 : stage.cutoff;
|
||||
filter.Q.value = stage.qMod ? 0 : stage.q;
|
||||
const qEven = stage.filter === 'bandpass' ? 'ratio' : 'value';
|
||||
if (stage.cutoffMod) follow?.(stage.cutoffMod, filter.frequency, true);
|
||||
if (stage.qMod) follow?.(stage.qMod, filter.Q, qEven === 'ratio');
|
||||
const qEven = stage.filter === 'lowpass' || stage.filter === 'highpass' ? 'value' : 'ratio';
|
||||
const filters = Array.from({ length: Math.max(1, stage.stages) }, () => {
|
||||
const filter = context.createBiquadFilter();
|
||||
filter.type = stage.filter;
|
||||
filter.Q.value = passQ(stage.filter, FLAT_Q);
|
||||
return filter;
|
||||
});
|
||||
filters.slice(1).forEach((filter, i) => filters[i]!.connect(filter));
|
||||
const last = filters.at(-1)!;
|
||||
last.Q.value = stage.qMod ? 0 : stage.q;
|
||||
last.gain.value = stage.gainMod ? 0 : stage.gain;
|
||||
if (stage.qMod) follow?.(stage.qMod, last.Q, qEven === 'ratio');
|
||||
if (stage.gainMod) follow?.(stage.gainMod, last.gain, false);
|
||||
let cutoff = filters[0]!.frequency;
|
||||
let stop: (() => void) | undefined;
|
||||
if (filters.length > 1) {
|
||||
const driver = context.createConstantSource();
|
||||
for (const filter of filters) {
|
||||
filter.frequency.value = 0;
|
||||
driver.connect(filter.frequency);
|
||||
}
|
||||
driver.start();
|
||||
cutoff = driver.offset;
|
||||
stop = () => driver.stop();
|
||||
}
|
||||
cutoff.value = stage.cutoffMod ? 0 : stage.cutoff;
|
||||
if (stage.cutoffMod) follow?.(stage.cutoffMod, cutoff, true);
|
||||
return {
|
||||
input: filter,
|
||||
output: filter,
|
||||
input: filters[0]!,
|
||||
output: last,
|
||||
knobs: [
|
||||
{ param: stage.cutoffMod ? null : filter.frequency, target: stage.cutoff, even: 'ratio' },
|
||||
{ param: stage.qMod ? null : filter.Q, target: stage.q, even: qEven },
|
||||
{ param: stage.cutoffMod ? null : cutoff, target: stage.cutoff, even: 'ratio' },
|
||||
{ param: stage.qMod ? null : last.Q, target: stage.q, even: qEven },
|
||||
{ param: stage.gainMod ? null : last.gain, target: stage.gain, even: 'value' },
|
||||
],
|
||||
...(stop && { stop }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2129,7 +2156,7 @@ function delayNodes(context: BaseAudioContext, stage: FxDelay): StageNodes {
|
||||
const damp = context.createBiquadFilter();
|
||||
damp.type = 'lowpass';
|
||||
damp.frequency.value = stage.damping;
|
||||
damp.Q.value = FLAT_Q;
|
||||
damp.Q.value = passQ('lowpass', FLAT_Q);
|
||||
const feedback = gainNode(context, stage.feedback);
|
||||
const wet = gainNode(context, stage.mix);
|
||||
line.connect(damp);
|
||||
@@ -2384,7 +2411,7 @@ function levelsOf(chain: FxChain, into: number[] = []): number[] {
|
||||
}
|
||||
for (const stage of chain.stages)
|
||||
if (stage.kind === 'delay') into.push(stage.time, stage.feedback, stage.damping, stage.mix);
|
||||
else if (stage.kind === 'pass') into.push(stage.cutoff, stage.q);
|
||||
else if (stage.kind === 'pass') into.push(stage.cutoff, stage.q, stage.gain);
|
||||
else if (stage.kind === 'crush') into.push(stage.bits, stage.rate, stage.mix);
|
||||
else if (stage.kind === 'phaser')
|
||||
into.push(stage.frequency, stage.rate, stage.depth, stage.feedback, stage.mix);
|
||||
|
||||
Reference in new issue
Block a user