diff --git a/packages/core/src/audio/audioFxGraph.test.ts b/packages/core/src/audio/audioFxGraph.test.ts index 9c54f12bc9..4d85eca285 100644 --- a/packages/core/src/audio/audioFxGraph.test.ts +++ b/packages/core/src/audio/audioFxGraph.test.ts @@ -438,3 +438,26 @@ describe("automatable parameters", () => { expect(onePole.automation?.frequency).toBeUndefined(); }); }); + +describe("phaser automation targets", () => { + /** + * `in_gain` and `out_gain` trim the signal entering and leaving the effect, + * which the builder drives through inTrim/outTrim while pinning wet and dry + * to 1. The automation map used to aim both lanes at wet/dry — so an envelope + * modulated a constant, the trim it was supposed to move stayed frozen, and + * "fade the phaser out" left the dry leg playing at full level. + * + * Asserted by VALUE rather than by node identity: the trims are internal, and + * the only honest question is whether the param a lane would drive is the one + * the knob sets. + */ + it("drives the trims a lane is named for, not the pinned wet/dry pair", () => { + const handle = buildFxNode(ctx() as unknown as BaseAudioContext, "phaser", { + ...defaultAudioFxParams("phaser"), + in_gain: 0.25, + out_gain: 0.5, + }); + expect(handle.automation?.in_gain?.[0]?.param.value).toBeCloseTo(0.25, 6); + expect(handle.automation?.out_gain?.[0]?.param.value).toBeCloseTo(0.5, 6); + }); +}); diff --git a/packages/core/src/audio/audioFxGraph.ts b/packages/core/src/audio/audioFxGraph.ts index a366b823e7..abb1f2e5f0 100644 --- a/packages/core/src/audio/audioFxGraph.ts +++ b/packages/core/src/audio/audioFxGraph.ts @@ -360,8 +360,13 @@ const allpassPhaser: Builder = (ctx, p) => { // frequency at once — not one knob, one param — so they stay unautomated. automation: { speed: [{ param: lfo.frequency }], - in_gain: [{ param: dry.gain }], - out_gain: [{ param: wet.gain }], + // The trims, not wet/dry. apply() drives inTrim/outTrim from these knobs + // and pins wet and dry to 1 — so a lane aimed at wet/dry modulated a + // constant and left the trim frozen, and the next values-only edit slammed + // it back over the running envelope. The comment above records that this + // wiring was already moved once; the automation map was missed. + in_gain: [{ param: inTrim.gain }], + out_gain: [{ param: outTrim.gain }], }, dispose: () => { try {