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:
adminandClaude Opus 5.5 committed 2026-10-07 20:43:49 +02:00
1 parent daeffe0f9d
commit e1ca6a5f5a
9 files changed
+282 -98

No files matched your search

+7 -32
View File
@@ -360,42 +360,17 @@ function throughFxWith(fx: FxPlan, sound: FxPlan, bind: (amount: FxAmount) => Fx
return withStages(pitched, stages);
}
/** Every number of a stage, bound; an Envelope's own numbers are the instrument's and stay. */
function bindStage(stage: FxStagePlan, bind: (amount: FxAmount) => FxAmount): FxStagePlan {
if (stage.kind === 'gain') return { ...stage, db: bind(stage.db) };
if (stage.kind === 'filter') return { ...stage, gain: bind(stage.gain) };
if (stage.kind === 'pass')
return { ...stage, cutoff: bind(stage.cutoff), resonance: bind(stage.resonance) };
if (stage.kind === 'envelope') return stage;
if (stage.kind === 'crush')
return { ...stage, bits: bind(stage.bits), rate: bind(stage.rate), mix: bind(stage.mix) };
if (stage.kind === 'phaser')
return {
...stage,
frequency: bind(stage.frequency),
rate: bind(stage.rate),
depth: bind(stage.depth),
feedback: bind(stage.feedback),
mix: bind(stage.mix),
};
if (stage.kind === 'flanger')
return {
...stage,
offset: bind(stage.offset),
sweep: bind(stage.sweep),
rate: bind(stage.rate),
depth: bind(stage.depth),
feedback: bind(stage.feedback),
mix: bind(stage.mix),
};
return {
...stage,
time: bind(stage.time),
feedback: bind(stage.feedback),
damping: bind(stage.damping),
mix: bind(stage.mix),
};
const bound: Record<string, unknown> = {};
for (const [key, value] of Object.entries(stage)) bound[key] = isAmount(value) ? bind(value) : value;
return bound as FxStagePlan;
}
const isAmount = (value: unknown): value is FxAmount =>
typeof value === 'object' && value !== null && typeof (value as FxAmount).value === 'number';
const builds = new Map<string, { fingerprint: string; notes: Map<number, InstrumentBuild> }>();
/**