Replace the 5-Band-EQ with the Parametric EQ and remove it (#395)
game-maker / gate (push) Successful in 4m13s
game-maker / suite (push) Successful in 16m21s

The Parametric EQ does all the 5-Band-EQ did and more, so the editor
keeps only one EQ. The old node was used in one project only, with
nothing linked to its band inputs.

- "Garden of Sacrifices" was converted once on the project file, not
  by a load migration (#137). Its 5 nodes are Parametric EQs now with
  the same filters: low shelf 80 Hz, peaks at 250 Hz, 1 kHz and 4 kHz
  with Q 1, high shelf 12 kHz. A band at 0 became no band.
- Two values below -24 dB were held to -24, as the Parametric EQ goes
  no further. Both are in the asset "Delay" (high -60, low -36). The
  user will rework them by ear.
- The write went through the API's own Store (lock, revision 20181 to
  20182), so a tab with the old revision gets a 409 instead of
  overwriting it.
- Removed with the node: its stage kind "filter" and the type
  FxFilter, which nothing else made, and the player's branch for it.
  A Pan is now the last branch of fxNodes, a Flanger that of stagesOf.
- Tests that took the 5-Band-EQ as "some effect" use the Pan or the
  Parametric EQ now. The test for a parameter feeding a band now
  feeds the cutoff of a Filter, which had no such test yet.
- A project that still names the type, such as an old snapshot of
  "Garden of Sacrifices", loads without those nodes. Unknown types
  were always left out on load.

Tests:
- Unit (stein2): soundfx 67, instrument 36, envelope 13, sound-library
  127, all green
- Counter-check: with the cutoff not taken from the parameter, only
  the new Filter test goes red
- The converted project loads and compiles: Delay, Vocals and Drums
  play their bands (3+1, 4 and 5)
- e2e 34 (atx): ALL PASSED, 136 checks, the palette check now looks
  for the Parametric EQ
- Gate (atx): CHECK EXIT 0, 288 files, 4417 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 19:46:03 +02:00
1 parent cb3183227b
commit 8c53ce0cbe
7 files changed
+93 -129

No files matched your search

+14 -64
View File
@@ -20,21 +20,17 @@ import { eqBandsOf, eqStagesOf, hasGain, isEqBandHeard, MAX_EQ_BANDS, type PassB
export { SOUND_FX_INPUT, SOUND_FX_OUTPUT }; export { SOUND_FX_INPUT, SOUND_FX_OUTPUT };
/** The filter shapes the EQ uses – Web Audio's names, so the player hands them on as they are. */
export type FxFilter = 'lowshelf' | 'peaking' | 'highshelf';
/** The shapes of a Filter (#335, #318), Web Audio's names. */ /** The shapes of a Filter (#335, #318), Web Audio's names. */
export type FxPassFilter = export type FxPassFilter =
'lowpass' | 'highpass' | 'bandpass' | 'notch' | 'lowshelf' | 'highshelf' | 'peaking' | 'allpass'; 'lowpass' | 'highpass' | 'bandpass' | 'notch' | 'lowshelf' | 'highshelf' | 'peaking' | 'allpass';
/** /**
* One step a voice passes on its way out: a volume, one band of an equalizer, a filter, the volume an * One step a voice passes on its way out: a volume, a filter or a band of an EQ, the volume an
* Envelope gives a note (#335), or an echo (#327). An echo's `damping` is already the cutoff of the * Envelope gives a note (#335), or an echo (#327). An echo's `damping` is already the cutoff of the
* low-pass in its feedback, in Hz. A `mod` is an Envelope moving that number while the note plays. * low-pass in its feedback, in Hz. A `mod` is an Envelope moving that number while the note plays.
*/ */
export type FxStage = export type FxStage =
| { kind: 'gain'; gain: number; mod?: FxMod } | { kind: 'gain'; gain: number; mod?: FxMod }
| { kind: 'filter'; filter: FxFilter; frequency: number; gain: number; q: number; mod?: FxMod }
| FxPass | FxPass
| { kind: 'envelope'; envelope: FxEnvelope } | { kind: 'envelope'; envelope: FxEnvelope }
| FxDelay | FxDelay
@@ -298,7 +294,6 @@ export interface FxAmount {
export type FxStagePlan = export type FxStagePlan =
| { kind: 'gain'; db: FxAmount } | { kind: 'gain'; db: FxAmount }
| { kind: 'filter'; filter: FxFilter; frequency: number; gain: FxAmount; q: number }
| { kind: 'pass'; filter: FxPassFilter; stages: number; cutoff: FxAmount; q: FxAmount; gain: FxAmount } | { kind: 'pass'; filter: FxPassFilter; stages: number; cutoff: FxAmount; q: FxAmount; gain: FxAmount }
| { kind: 'envelope'; envelope: FxEnvelopePlan } | { kind: 'envelope'; envelope: FxEnvelopePlan }
| { | {
@@ -588,43 +583,6 @@ export const SoundFxPitch: SoundFxNodeDef = effect(
'own', 'own',
); );
/** The five bands of the EQ: a shelf at either end and three peaks between them. */
const EQ_BANDS: readonly { key: string; label: string; filter: FxFilter; frequency: number }[] = [
{ key: 'low', label: '80 Hz', filter: 'lowshelf', frequency: 80 },
{ key: 'lowMid', label: '250 Hz', filter: 'peaking', frequency: 250 },
{ key: 'mid', label: '1 kHz', filter: 'peaking', frequency: 1000 },
{ key: 'highMid', label: '4 kHz', filter: 'peaking', frequency: 4000 },
{ key: 'high', label: '12 kHz', filter: 'highshelf', frequency: 12000 },
];
/** About an octave and a half wide: neighbouring peaks overlap, so no frequency falls between them. */
const PEAK_Q = 1;
const EQ_ROWS = EQ_BANDS.map((band) => numberRow(band.key, band.label, -60, 24, 0.5));
export const SoundFxEq: SoundFxNodeDef = effect(
{
type: 'SoundFxEq',
label: '5-Band-EQ',
category: 'sound',
description:
'A five-band equalizer: raises or lowers the lows (80 Hz), low mids (250 Hz), mids (1 kHz), high mids (4 kHz) and highs (12 kHz), each in decibels. A band at 0 does nothing.' +
BLENDS,
inputs: [audioIn, ...EQ_ROWS.map(followingIn)],
outputs: [audioOut],
params: [],
emit: (sounds, amounts) => {
const stages: FxStagePlan[] = [];
EQ_BANDS.forEach((band, i) => {
const gain = held(amounts[band.key], EQ_ROWS[i]!);
if (!isNone(gain))
stages.push({ kind: 'filter', filter: band.filter, frequency: band.frequency, gain, q: PEAK_Q });
});
return withStages(arriving(sounds), stages);
},
},
'blend',
);
export const SoundFxParametricEq: SoundFxNodeDef = effect( export const SoundFxParametricEq: SoundFxNodeDef = effect(
{ {
type: 'SoundFxParametricEq', type: 'SoundFxParametricEq',
@@ -1326,7 +1284,6 @@ export const SOUND_FX_NODES: readonly SoundFxNodeDef[] = [
SoundFxOutput, SoundFxOutput,
SoundFxGain, SoundFxGain,
SoundFxPitch, SoundFxPitch,
SoundFxEq,
SoundFxParametricEq, SoundFxParametricEq,
SoundFxFilter, SoundFxFilter,
SoundFxMix, SoundFxMix,
@@ -1542,26 +1499,19 @@ function stagesOf(plan: readonly FxStagePlan[], params: FxParams, tempo: number)
stages.push({ kind: 'pan', pan, ...(mod && { mod }) }); stages.push({ kind: 'pan', pan, ...(mod && { mod }) });
continue; continue;
} }
if (stage.kind === 'flanger') { const offsetMod = modOf(stage.offset, params);
const offsetMod = modOf(stage.offset, params); const mixMod = modOf(stage.mix, params);
const mixMod = modOf(stage.mix, params); stages.push({
stages.push({ kind: 'flanger',
kind: 'flanger', offset: amountOf(stage.offset, params),
offset: amountOf(stage.offset, params), sweep: amountOf(stage.sweep, params),
sweep: amountOf(stage.sweep, params), rate: amountOf(stage.rate, params),
rate: amountOf(stage.rate, params), depth: amountOf(stage.depth, params),
depth: amountOf(stage.depth, params), feedback: amountOf(stage.feedback, params),
feedback: amountOf(stage.feedback, params), mix: amountOf(stage.mix, params),
mix: amountOf(stage.mix, params), ...(offsetMod && { offsetMod }),
...(offsetMod && { offsetMod }), ...(mixMod && { mixMod }),
...(mixMod && { mixMod }), });
});
continue;
}
const gain = amountOf(stage.gain, params);
const mod = modOf(stage.gain, params);
if (gain !== 0 || stage.gain.param !== undefined || mod)
stages.push({ ...stage, gain, ...(mod && { mod }) });
} }
return stages; return stages;
} }
+6 -19
View File
@@ -133,26 +133,13 @@ export function stageNodes(
{ name: 'release', value: stage.release, even: 'ratio' }, { name: 'release', value: stage.release, even: 'ratio' },
{ name: 'autoGain', value: stage.autoGain, even: 'value' }, { name: 'autoGain', value: stage.autoGain, even: 'value' },
]); ]);
if (stage.kind === 'pan') { const panner = context.createStereoPanner();
const panner = context.createStereoPanner(); panner.pan.value = stage.mod ? 0 : stage.pan;
panner.pan.value = stage.mod ? 0 : stage.pan; if (stage.mod) follow?.(stage.mod, panner.pan, false);
if (stage.mod) follow?.(stage.mod, panner.pan, false);
return {
input: panner,
output: panner,
knobs: [{ param: stage.mod ? null : panner.pan, target: stage.pan, even: 'value' }],
};
}
const filter = context.createBiquadFilter();
filter.type = stage.filter;
filter.frequency.value = stage.frequency;
filter.gain.value = stage.mod ? 0 : stage.gain;
filter.Q.value = stage.q;
if (stage.mod) follow?.(stage.mod, filter.gain, false);
return { return {
input: filter, input: panner,
output: filter, output: panner,
knobs: [{ param: stage.mod ? null : filter.gain, target: stage.gain, even: 'value' }], knobs: [{ param: stage.mod ? null : panner.pan, target: stage.pan, even: 'value' }],
}; };
} }
+7 -6
View File
@@ -198,21 +198,21 @@ describe('an Envelope in an instrument', () => {
}); });
}); });
it('moves a Pitch on top of the one the way has, a Gain in its factor, a band and a Mix', () => { it('moves a Pitch on top of the one the way has, a Gain in its factor, a Pan and a Mix', () => {
const f = makeInstrument(); const f = makeInstrument();
const envelope = f.edit.addNode('InstrumentEnvelope', { from: -12, to: 0 }); const envelope = f.edit.addNode('InstrumentEnvelope', { from: -12, to: 0 });
const pitch = f.edit.addNode('SoundFxPitch'); const pitch = f.edit.addNode('SoundFxPitch');
const gain = f.edit.addNode('SoundFxGain'); const gain = f.edit.addNode('SoundFxGain');
const eq = f.edit.addNode('SoundFxEq'); const pan = f.edit.addNode('SoundFxPan');
const mix = f.edit.addNode('SoundFxMix'); const mix = f.edit.addNode('SoundFxMix');
f.edit.wire([ f.edit.wire([
[f.sample, 'out', pitch, 'in'], [f.sample, 'out', pitch, 'in'],
[envelope, 'value', pitch, 'semitones'], [envelope, 'value', pitch, 'semitones'],
[pitch, 'out', gain, 'in'], [pitch, 'out', gain, 'in'],
[envelope, 'value', gain, 'gain'], [envelope, 'value', gain, 'gain'],
[gain, 'out', eq, 'in'], [gain, 'out', pan, 'in'],
[envelope, 'value', eq, 'mid'], [envelope, 'value', pan, 'pan'],
[eq, 'out', mix, 'a'], [pan, 'out', mix, 'a'],
[f.sample, 'out', mix, 'b'], [f.sample, 'out', mix, 'b'],
[envelope, 'value', mix, 'mix'], [envelope, 'value', mix, 'mix'],
[mix, 'out', f.output, 'in'], [mix, 'out', f.output, 'in'],
@@ -227,7 +227,8 @@ describe('an Envelope in an instrument', () => {
expect(chain.a.pitchMods).toMatchObject([{ from: -12, to: 0 }]); expect(chain.a.pitchMods).toMatchObject([{ from: -12, to: 0 }]);
expect(chain.a.stages).toMatchObject([ expect(chain.a.stages).toMatchObject([
{ kind: 'gain', mod: { from: 10 ** (-12 / 20), to: 1 } }, { kind: 'gain', mod: { from: 10 ** (-12 / 20), to: 1 } },
{ kind: 'filter', frequency: 1000, mod: { from: -12, to: 0 } }, // The Pan rests at "from" as well, held to -1..1.
{ kind: 'pan', mod: { from: -1, to: 0 } },
]); ]);
}); });
+6 -6
View File
@@ -307,7 +307,7 @@ describe('compiling a note', () => {
f.instrument.graph.edges = f.instrument.graph.edges.filter((e) => e.to.node !== output.id); f.instrument.graph.edges = f.instrument.graph.edges.filter((e) => e.to.node !== output.id);
const byVelocity = f.edit.addNode('SoundFxGain'); const byVelocity = f.edit.addNode('SoundFxGain');
const byParam = f.edit.addNode('SoundFxGain'); const byParam = f.edit.addNode('SoundFxGain');
const byNote = f.edit.addNode('SoundFxEq'); const byNote = f.edit.addNode('SoundFxPan');
f.stack.execute( f.stack.execute(
new ConnectCommand( new ConnectCommand(
f.instrument.id, f.instrument.id,
@@ -320,21 +320,21 @@ describe('compiling a note', () => {
[byVelocity, 'out', byParam, 'in'], [byVelocity, 'out', byParam, 'in'],
[params, 'level', byParam, 'gain'], [params, 'level', byParam, 'gain'],
[byParam, 'out', byNote, 'in'], [byParam, 'out', byNote, 'in'],
[notes, 'note', byNote, 'high'], [notes, 'note', byNote, 'pan'],
[byNote, 'out', output, 'in'], [byNote, 'out', output, 'in'],
]); ]);
const plan = compile(f, 62).layers[0]!; const plan = compile(f, 62).layers[0]!;
expect(plan.stages).toEqual([ expect(plan.stages).toEqual([
{ kind: 'gain', db: { value: 1, param: VELOCITY_PARAM, min: -120, max: 24 } }, { kind: 'gain', db: { value: 1, param: VELOCITY_PARAM, min: -120, max: 24 } },
{ kind: 'gain', db: { value: -3, param: 'level', min: -120, max: 24 } }, { kind: 'gain', db: { value: -3, param: 'level', min: -120, max: 24 } },
// The note is a number by now: 62, held to the band's row. // The note is a number by now: 62, held to the Pan's row.
{ kind: 'filter', filter: 'highshelf', frequency: 12000, gain: { value: 24 }, q: 1 }, { kind: 'pan', pan: { value: 1 } },
]); ]);
const chain = fxChainOf(plan, { [VELOCITY_PARAM]: -6, level: -9 }); const chain = fxChainOf(plan, { [VELOCITY_PARAM]: -6, level: -9 });
expect(chain.stages.map((s) => ('gain' in s ? s.gain : 0))).toEqual([ expect(chain.stages.map((s) => (s.kind === 'pan' ? s.pan : 'gain' in s ? s.gain : 0))).toEqual([
10 ** (-6 / 20), 10 ** (-6 / 20),
10 ** (-9 / 20), 10 ** (-9 / 20),
24, 1,
]); ]);
}); });
+39 -28
View File
@@ -80,6 +80,15 @@ function line(effects: { type: string; params?: PropertyBag }[]) {
return { ...f, input, output }; return { ...f, input, output };
} }
/** A shelf of a Parametric EQ as its node keeps it. */
const shelf = (type: 'lowshelf' | 'highshelf', frequency: number, gain: number) => ({
type,
frequency,
gain,
q: 1,
slope: 12,
});
describe('the sound FX asset', () => { describe('the sound FX asset', () => {
it('is not placeable, starts with an empty graph and is what the fx picker offers', () => { it('is not placeable, starts with an empty graph and is what the fx picker offers', () => {
expect(isPlaceableType('soundfx')).toBe(false); expect(isPlaceableType('soundfx')).toBe(false);
@@ -128,7 +137,6 @@ describe('the sound FX asset', () => {
'SoundFxOutput', 'SoundFxOutput',
'SoundFxGain', 'SoundFxGain',
'SoundFxPitch', 'SoundFxPitch',
'SoundFxEq',
'SoundFxParametricEq', 'SoundFxParametricEq',
'SoundFxFilter', 'SoundFxFilter',
'SoundFxMix', 'SoundFxMix',
@@ -149,7 +157,6 @@ describe('the sound FX asset', () => {
expect(ids()).toEqual([ expect(ids()).toEqual([
'SoundFxGain', 'SoundFxGain',
'SoundFxPitch', 'SoundFxPitch',
'SoundFxEq',
'SoundFxParametricEq', 'SoundFxParametricEq',
'SoundFxFilter', 'SoundFxFilter',
'SoundFxMix', 'SoundFxMix',
@@ -194,24 +201,28 @@ describe('compiling a sound FX graph', () => {
const f = line([ const f = line([
{ type: 'SoundFxGain', params: { gain: -6 } }, { type: 'SoundFxGain', params: { gain: -6 } },
{ type: 'SoundFxPitch', params: { semitones: 12 } }, { type: 'SoundFxPitch', params: { semitones: 12 } },
{ type: 'SoundFxEq', params: { low: 6, high: -3 } }, {
type: 'SoundFxParametricEq',
params: { bands: [shelf('lowshelf', 80, 6), shelf('highshelf', 12000, -3)] },
},
{ type: 'SoundFxPitch', params: { semitones: -5 } }, { type: 'SoundFxPitch', params: { semitones: -5 } },
]); ]);
const build = compileSoundFx(f.fx.graph); const build = compileSoundFx(f.fx.graph);
expect(build.effects).toBe(4); expect(build.effects).toBe(4);
const chain = fxChainOf(build.compiled!); const chain = fxChainOf(build.compiled!);
expect(pitchOf(chain)).toBe(7); expect(pitchOf(chain)).toBe(7);
expect(chain.stages).toHaveLength(3); const stages = chain.stages.filter((s) => s.kind !== 'tap');
expect(chain.stages[0]!.kind).toBe('gain'); expect(stages).toHaveLength(3);
expect(gainOf(chain.stages[0])).toBeCloseTo(0.501, 3); expect(stages[0]!.kind).toBe('gain');
expect(chain.stages.slice(1)).toEqual([ expect(gainOf(stages[0])).toBeCloseTo(0.501, 3);
{ kind: 'filter', filter: 'lowshelf', frequency: 80, gain: 6, q: 1 }, expect(stages.slice(1)).toEqual([
{ kind: 'filter', filter: 'highshelf', frequency: 12000, gain: -3, q: 1 }, { kind: 'pass', filter: 'lowshelf', stages: 1, cutoff: 80, q: 1, gain: 6 },
{ kind: 'pass', filter: 'highshelf', stages: 1, cutoff: 12000, q: 1, gain: -3 },
]); ]);
}); });
it('leaves out what does nothing and holds a value to its row', () => { it('leaves out what does nothing and holds a value to its row', () => {
const quiet = compileSoundFx(line([{ type: 'SoundFxGain' }, { type: 'SoundFxEq' }]).fx.graph); const quiet = compileSoundFx(line([{ type: 'SoundFxGain' }, { type: 'SoundFxPan' }]).fx.graph);
expect(quiet.compiled).toEqual(EMPTY_PLAN); expect(quiet.compiled).toEqual(EMPTY_PLAN);
expect(quiet.effects).toBe(2); expect(quiet.effects).toBe(2);
@@ -232,21 +243,21 @@ describe('compiling a sound FX graph', () => {
}); });
describe('a Mix in a sound FX graph (#316)', () => { describe('a Mix in a sound FX graph (#316)', () => {
/** Input → Pitch +12 → Mix a, Input → Gain -6 → Mix b, Mix → EQ → Output. */ /** Input → Pitch +12 → Mix a, Input → Gain -6 → Mix b, Mix → Pan → Output. */
function forked(mix: Record<string, number> = {}) { function forked(mix: Record<string, number> = {}) {
const f = makeFx(); const f = makeFx();
const input = f.edit.addNode('SoundFxInput'); const input = f.edit.addNode('SoundFxInput');
const pitch = f.edit.addNode('SoundFxPitch', { semitones: 12 }); const pitch = f.edit.addNode('SoundFxPitch', { semitones: 12 });
const gain = f.edit.addNode('SoundFxGain', { gain: -6 }); const gain = f.edit.addNode('SoundFxGain', { gain: -6 });
const node = f.edit.addNode('SoundFxMix', mix); const node = f.edit.addNode('SoundFxMix', mix);
const eq = f.edit.addNode('SoundFxEq', { low: 6 }); const pan = f.edit.addNode('SoundFxPan', { pan: 0.5 });
const output = f.edit.addNode('SoundFxOutput'); const output = f.edit.addNode('SoundFxOutput');
f.edit.connect(input, 'out', pitch, 'in'); f.edit.connect(input, 'out', pitch, 'in');
f.edit.connect(input, 'out', gain, 'in'); f.edit.connect(input, 'out', gain, 'in');
f.edit.connect(pitch, 'out', node, 'a'); f.edit.connect(pitch, 'out', node, 'a');
f.edit.connect(gain, 'out', node, 'b'); f.edit.connect(gain, 'out', node, 'b');
f.edit.connect(node, 'out', eq, 'in'); f.edit.connect(node, 'out', pan, 'in');
f.edit.connect(eq, 'out', output, 'in'); f.edit.connect(pan, 'out', output, 'in');
return { ...f, input, mix: node }; return { ...f, input, mix: node };
} }
@@ -258,17 +269,17 @@ describe('a Mix in a sound FX graph (#316)', () => {
mix: 0.5, mix: 0.5,
a: { pitch: 12, stages: [] }, a: { pitch: 12, stages: [] },
b: { pitch: 0, stages: [{ kind: 'gain' }] }, b: { pitch: 0, stages: [{ kind: 'gain' }] },
stages: [{ kind: 'filter', filter: 'lowshelf', gain: 6 }], stages: [{ kind: 'pan', pan: 0.5 }],
}); });
}); });
it('is only the one sound at 0 or 1, with what follows the Mix', () => { it('is only the one sound at 0 or 1, with what follows the Mix', () => {
expect(fxChainOf(compileSoundFx(forked({ mix: 0 }).fx.graph).compiled!)).toMatchObject({ expect(fxChainOf(compileSoundFx(forked({ mix: 0 }).fx.graph).compiled!)).toMatchObject({
pitch: 12, pitch: 12,
stages: [{ kind: 'filter' }], stages: [{ kind: 'pan' }],
}); });
const b = fxChainOf(compileSoundFx(forked({ mix: 5 }).fx.graph).compiled!); const b = fxChainOf(compileSoundFx(forked({ mix: 5 }).fx.graph).compiled!);
expect(b).toMatchObject({ pitch: 0, stages: [{ kind: 'gain' }, { kind: 'filter' }] }); expect(b).toMatchObject({ pitch: 0, stages: [{ kind: 'gain' }, { kind: 'pan' }] });
}); });
it('says its curve only where it is "Both at center", and is still one sound at 0 or 1', () => { it('says its curve only where it is "Both at center", and is still one sound at 0 or 1', () => {
@@ -294,15 +305,15 @@ describe('a Mix in a sound FX graph (#316)', () => {
it('hands a Pitch behind it to both of its sounds', () => { it('hands a Pitch behind it to both of its sounds', () => {
const f = forked(); const f = forked();
const output = f.fx.graph.nodes.find((n) => n.type === 'SoundFxOutput')!; const output = f.fx.graph.nodes.find((n) => n.type === 'SoundFxOutput')!;
const eq = f.fx.graph.nodes.find((n) => n.type === 'SoundFxEq')!; const pan = f.fx.graph.nodes.find((n) => n.type === 'SoundFxPan')!;
f.fx.graph.edges = f.fx.graph.edges.filter((e) => e.to.node !== output.id); f.fx.graph.edges = f.fx.graph.edges.filter((e) => e.to.node !== output.id);
const after = f.edit.addNode('SoundFxPitch', { semitones: -5 }); const after = f.edit.addNode('SoundFxPitch', { semitones: -5 });
f.edit.connect(eq, 'out', after, 'in'); f.edit.connect(pan, 'out', after, 'in');
f.edit.connect(after, 'out', output, 'in'); f.edit.connect(after, 'out', output, 'in');
expect(fxChainOf(compileSoundFx(f.fx.graph).compiled!)).toMatchObject({ expect(fxChainOf(compileSoundFx(f.fx.graph).compiled!)).toMatchObject({
a: { pitch: 7 }, a: { pitch: 7 },
b: { pitch: -5 }, b: { pitch: -5 },
stages: [{ kind: 'filter' }], stages: [{ kind: 'pan' }],
}); });
}); });
@@ -626,7 +637,6 @@ describe('a Reverb in a sound FX graph (#328)', () => {
describe('Bypass and Wet (#354)', () => { describe('Bypass and Wet (#354)', () => {
/** Every effect, and how it says its wet: a row of its own, or the "mix" it had. */ /** Every effect, and how it says its wet: a row of its own, or the "mix" it had. */
const EFFECTS: Record<string, 'wet' | 'mix'> = { const EFFECTS: Record<string, 'wet' | 'mix'> = {
SoundFxEq: 'wet',
SoundFxBitcrusher: 'mix', SoundFxBitcrusher: 'mix',
SoundFxCompressor: 'wet', SoundFxCompressor: 'wet',
SoundFxDelay: 'mix', SoundFxDelay: 'mix',
@@ -665,12 +675,12 @@ describe('Bypass and Wet (#354)', () => {
def.type === 'SoundFxMix', def.type === 'SoundFxMix',
]); ]);
} }
expect(Object.keys(EFFECTS)).toHaveLength(16); expect(Object.keys(EFFECTS)).toHaveLength(15);
}); });
it('bypassed, an effect is left out as if it were not there, and its node is drawn faded', () => { it('bypassed, an effect is left out as if it were not there, and its node is drawn faded', () => {
for (const type of Object.keys(EFFECTS)) { for (const type of Object.keys(EFFECTS)) {
const on = { gain: -6, semitones: 5, low: 3, pan: 1, depth: 50, bands: [{ gain: 6 }] }; const on = { gain: -6, semitones: 5, pan: 1, depth: 50, bands: [{ gain: 6 }] };
expect([type, stagesOf(type, on).length > 0 || type === 'SoundFxPitch']).toEqual([type, true]); expect([type, stagesOf(type, on).length > 0 || type === 'SoundFxPitch']).toEqual([type, true]);
// The taps a Parametric EQ keeps do nothing to the sound (#374). // The taps a Parametric EQ keeps do nothing to the sound (#374).
const bypassed = compileSoundFx(line([{ type, params: { ...on, bypass: true } }]).fx.graph).compiled!; const bypassed = compileSoundFx(line([{ type, params: { ...on, bypass: true } }]).fx.graph).compiled!;
@@ -691,7 +701,8 @@ describe('Bypass and Wet (#354)', () => {
{ kind: 'wet', wet: { value: 0.25 }, stages: [{ kind: 'gain', db: { value: -6 } }] }, { kind: 'wet', wet: { value: 0.25 }, stages: [{ kind: 'gain', db: { value: -6 } }] },
]); ]);
// Every band of an EQ in one blend, not each band on its own. // Every band of an EQ in one blend, not each band on its own.
const eq = stagesOf('SoundFxEq', { low: 6, high: -3, wet: 0.5 }); const bands = [shelf('lowshelf', 80, 6), shelf('highshelf', 12000, -3)];
const eq = stagesOf('SoundFxParametricEq', { bands, wet: 0.5 }).filter((s) => s.kind !== 'tap');
expect(eq).toMatchObject([{ kind: 'wet', stages: [{ filter: 'lowshelf' }, { filter: 'highshelf' }] }]); expect(eq).toMatchObject([{ kind: 'wet', stages: [{ filter: 'lowshelf' }, { filter: 'highshelf' }] }]);
// What came before the node stays outside the blend. // What came before the node stays outside the blend.
const f = line([ const f = line([
@@ -953,10 +964,10 @@ describe('the parameters of a sound FX', () => {
expect(fxChainOf(soundFxBuildFor(typed.fx).compiled!)).toMatchObject({ pitch: 0, shifted: false }); expect(fxChainOf(soundFxBuildFor(typed.fx).compiled!)).toMatchObject({ pitch: 0, shifted: false });
}); });
it('feed an EQ band as well', () => { it('feed the cutoff of a Filter as well', () => {
const f = withParam('SoundFxEq', 'mid', [{ key: 'body', kind: 'number', default: 3 }]); const f = withParam('SoundFxFilter', 'cutoff', [{ key: 'bright', kind: 'number', default: 500 }]);
const chain = fxChainOf(soundFxBuildFor(f.fx).compiled!, { body: -4 }); const chain = fxChainOf(soundFxBuildFor(f.fx).compiled!, { bright: 2000 });
expect(chain.stages).toEqual([{ kind: 'filter', filter: 'peaking', frequency: 1000, gain: -4, q: 1 }]); expect(chain.stages.filter((s) => s.kind !== 'tap')).toMatchObject([{ kind: 'pass', cutoff: 2000 }]);
}); });
it('are kept with their defaults through a save, a default only on a number', () => { it('are kept with their defaults through a save, a default only on a number', () => {
+20 -5
View File
@@ -422,7 +422,13 @@ afterEach(() => {
function planOf(chain: Chain): FxPlan { function planOf(chain: Chain): FxPlan {
const stages = chain.stages.map((stage): FxStagePlan => { const stages = chain.stages.map((stage): FxStagePlan => {
if (stage.kind === 'gain') return { kind: 'gain', db: { value: 20 * Math.log10(stage.gain) } }; if (stage.kind === 'gain') return { kind: 'gain', db: { value: 20 * Math.log10(stage.gain) } };
if (stage.kind === 'filter') return { ...stage, gain: { value: stage.gain } }; if (stage.kind === 'pass')
return {
...stage,
cutoff: { value: stage.cutoff },
q: { value: stage.q },
gain: { value: stage.gain },
};
if (stage.kind === 'crush') if (stage.kind === 'crush')
return { return {
kind: 'crush', kind: 'crush',
@@ -450,7 +456,16 @@ function open(): FxPlan {
mix: { value: 0.25, param: 'blend', min: 0, max: 1 }, mix: { value: 0.25, param: 'blend', min: 0, max: 1 },
a: { pitch: [{ value: 0, param: 'depth', min: -64, max: 64 }], stages: [] }, a: { pitch: [{ value: 0, param: 'depth', min: -64, max: 64 }], stages: [] },
b: { pitch: [], stages: [{ kind: 'gain', db: { value: 0, param: 'loud', min: -120, max: 24 } }] }, b: { pitch: [], stages: [{ kind: 'gain', db: { value: 0, param: 'loud', min: -120, max: 24 } }] },
stages: [{ kind: 'filter', filter: 'lowshelf', frequency: 80, gain: { value: 6 }, q: 1 }], stages: [
{
kind: 'pass',
filter: 'lowshelf',
stages: 1,
cutoff: { value: 80 },
gain: { value: 6 },
q: { value: 1 },
},
],
}; };
} }
@@ -642,7 +657,7 @@ describe('the sound library', () => {
pitch: 12, pitch: 12,
stages: [ stages: [
{ kind: 'gain', gain: 0.5 }, { kind: 'gain', gain: 0.5 },
{ kind: 'filter', filter: 'lowshelf', frequency: 80, gain: 6, q: 1 }, { kind: 'pass', filter: 'lowshelf', stages: 1, cutoff: 80, gain: 6, q: 1 },
], ],
}; };
await play(library, FILE, {}, fx); await play(library, FILE, {}, fx);
@@ -791,7 +806,7 @@ describe('the sound library', () => {
expect(worklets).toHaveLength(0); expect(worklets).toHaveLength(0);
expect(sources).toHaveLength(2); expect(sources).toHaveLength(2);
expect(library.tune(voice, { depth: 12 })).toBe(true); expect(library.tune(voice, { depth: 12 })).toBe(true);
expect(library.levels(FILE)).toEqual([[12, 0.75, 1, 0.25, 6]]); expect(library.levels(FILE)).toEqual([[12, 0.75, 1, 0.25, 80, 1, 6]]);
expect(console.error).toHaveBeenCalledTimes(1); expect(console.error).toHaveBeenCalledTimes(1);
}); });
@@ -805,7 +820,7 @@ describe('the sound library', () => {
mix: 0.25, mix: 0.25,
a: { pitch: 12, stages: [] }, a: { pitch: 12, stages: [] },
b: { pitch: 0, stages: [{ kind: 'gain', gain: 0.5 }] }, b: { pitch: 0, stages: [{ kind: 'gain', gain: 0.5 }] },
stages: [{ kind: 'filter', filter: 'lowshelf', frequency: 80, gain: 6, q: 1 }], stages: [{ kind: 'pass', filter: 'lowshelf', stages: 1, cutoff: 80, gain: 6, q: 1 }],
}, },
); );
expect(wires).toEqual([ expect(wires).toEqual([
+1 -1
View File
@@ -355,7 +355,7 @@ export async function sound(t) {
const fxPalette = await json('__gm.overlayTexts()'); const fxPalette = await json('__gm.overlayTexts()');
check( check(
'its palette offers the sound effects and no behavior node', 'its palette offers the sound effects and no behavior node',
['Input', 'Output', 'Gain', 'Pitch', '5-Band-EQ', 'Mix', 'Delay'].every((label) => ['Input', 'Output', 'Gain', 'Pitch', 'Parametric EQ', 'Mix', 'Delay'].every((label) =>
fxPalette.includes(label), fxPalette.includes(label),
) && !fxPalette.includes('Branch'), ) && !fxPalette.includes('Branch'),
JSON.stringify(fxPalette.slice(0, 12)), JSON.stringify(fxPalette.slice(0, 12)),