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

+30
View File
@@ -975,6 +975,36 @@ export async function sound(t) {
filtered.notch < -40,
JSON.stringify(filtered),
);
// #319: Pan rendered offline. A mono sine at 0.5 all to the left leaves the right silent, in the
// middle each side gets 0.707 of it. A stereo sound panned half right keeps its right side
// and moves part of its left into it, as Web Audio pans stereo: left 0.707, right 1 + 0.707.
const panned = await evaluate(`
(async () => {
const RATE = 48000;
const peaks = async (pan, channels) => {
const context = new OfflineAudioContext(2, RATE / 10, RATE);
const sound = context.createBuffer(channels, RATE / 10, RATE);
for (let c = 0; c < channels; c++) {
const data = sound.getChannelData(c);
for (let i = 0; i < data.length; i++) data[i] = Math.sin((2 * Math.PI * 1000 * i) / RATE) * 0.5;
}
const source = context.createBufferSource();
source.buffer = sound;
const nodes = __gm.sound.stageNodes(context, { kind: 'pan', pan });
source.connect(nodes.input);
nodes.output.connect(context.destination);
source.start();
const out = await context.startRendering();
return [0, 1].map((c) => +Math.max(...out.getChannelData(c).map(Math.abs)).toFixed(3));
};
return { left: await peaks(-1, 1), middle: await peaks(0, 1), balance: await peaks(0.5, 2) };
})()`);
check(
'Pan moves a mono sound between the sides, and a stereo one towards the side it goes to',
JSON.stringify(panned) ===
JSON.stringify({ left: [0.5, 0], middle: [0.354, 0.354], balance: [0.354, 0.854] }),
JSON.stringify(panned),
);
// #329: the Chip on the worklet, rendered offline. A square at 220 Hz with a pitch of 12 and a duty
// of 0.25, starting at 0.25 s: silence until then, 440 periods a second after it, up a
// quarter of each. Sliding two octaves a second down from 440 Hz with a lowest frequency of