Move a sound left or right with a Pan node (#319)

A sound FX had no way to place a sound in the stereo field. The new
Pan node does that on a voice, a bus and a track alike.

- One input, Pan from -1 (left) to 1 (right), 0 by default. Behind it
  is a StereoPannerNode. A typed-in 0 leaves the node out, a pan fed
  from a parameter stays at any value.
- It moves with Set sound FX parameters, and an Envelope can move it
  in an instrument.
- As the issue decided, no pan from the position in Play sound and no
  width control for stereo sounds.

Finding: the issue and the plan said a stereo sound gets a balance.
Web Audio pans stereo differently: the side it goes to stays and the
other side moves over into it. The node description and the wiki say
so, the e2e check measures it.

Tests:
- Unit soundfx 46 (1 new), sound-library 97 (1 new), green.
- Counter-check: a pan from a parameter at 0 left out, 1 test red.
- e2e 34 (melektaus-atx): ALL PASSED, 77 checks (1 new). Chrome
  offline, a mono sine at 0.5 gives 0.5 / 0 at -1 and 0.354 / 0.354
  at 0. A stereo one at 0.5 panned 0.5 gives 0.354 / 0.854.
- 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:47:53 +02:00
1 parent e1ca6a5f5a
commit c99bda6489
5 files changed
+94 -2

No files matched your search

+11
View File
@@ -2036,6 +2036,16 @@ export function stageNodes(
if (stage.kind === 'crush') return crushNodes(context, stage, follow);
if (stage.kind === 'phaser') return phaserNodes(context, stage, follow);
if (stage.kind === 'flanger') return flangerNodes(context, stage, at, follow);
if (stage.kind === 'pan') {
const panner = context.createStereoPanner();
panner.pan.value = stage.mod ? 0 : stage.pan;
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;
@@ -2417,6 +2427,7 @@ function levelsOf(chain: FxChain, into: number[] = []): number[] {
into.push(stage.frequency, stage.rate, stage.depth, stage.feedback, stage.mix);
else if (stage.kind === 'flanger')
into.push(stage.offset, stage.sweep, stage.rate, stage.depth, stage.feedback, stage.mix);
else if (stage.kind === 'pan') into.push(stage.pan);
else if (stage.kind !== 'envelope') into.push(stage.gain);
return into;
}