Keep both sounds full at the middle of a Mix with a curve #331
game-maker / gate (push) Successful in 3m16s
game-maker / suite (push) Successful in 16m2s

A Mix crossfaded: at 0.5 each sound was at half. Layering two sounds
in an instrument now goes through a Mix (076c564), so the user asked
for a second curve where each side stays full up to the middle and
only fades on the other half. At 0.25 a is full and b half, at 0.5
both are full, at 0.75 a is half and b full.

- The Mix has a setting Curve with Crossfade (default) and Both at
  center. A plan and a chain only say `curve` where it is `both`, so
  old projects and their compiled plans stay as they were.
- A share is not a straight line of `mix` with that curve. Two ramps
  of their own would dip to half each when `mix` glides across the
  middle. So a ConstantSourceNode carries `mix` on the audio thread,
  and two WaveShaperNodes make the shares from it. The knob of such a
  Mix is that one source.
- The source is started with the voice and stopped when it is cut or
  ends by itself, so it does not run on after the voice.
- A sound FX node in an instrument keeps the curve of a Mix inside it.
- At 0.5 both sounds are added in full, up to +6 dB where both are
  loud. That is what several edges into the Output did before. The
  wiki says so.

Found on the way: the suite of 23bdc3f (task 529) failed the Step
check in e2e 34. It timed the jump from the start of the voice, but a
timed change runs from when it arrives. In CI that was 80 ms later,
and the pitch rightly held to 2.08 s.

- stats() says `tunedAt`, the second of the audio clock the last
  change moved a voice. A change that moves nothing, as On update
  sends every frame, leaves it.
- The Step check times its readings from there.

Tests:
- sound-library.test.ts: both shares shaped from one source, a glide
  moves only the source, the source stops with a cut and with an end
  by itself (50 tests, 2 new)
- soundfx.test.ts: curve only where set, one sound at 0 (41 tests)
- instrument.test.ts: the curve through a sound FX node (34 tests)
- Counter-checks: dropping the curve in throughFx and not stopping
  the source in done() each turn their new test red
- e2e 34 on melektaus-atx: ALL PASSED, 48 checks (+1). Rendered
  offline in Chrome, a and b come to 1/0, 1/0.5, 1/1, 0.5/1 and 0/1
  at mix 0 to 1, and a glide from 0 to 1 reads the same at 0.25, 0.5
  and 0.75 s. Step jumped 2.04 s after the change.
- Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3793 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-06 13:12:56 +02:00
1 parent 076c564415
commit fb4d65a2ab
8 files changed
+261 -25

No files matched your search

+9
View File
@@ -230,6 +230,15 @@ describe('a Mix in a sound FX graph (#316)', () => {
expect(b).toMatchObject({ pitch: 0, stages: [{ kind: 'gain' }, { kind: 'filter' }] });
});
it('says its curve only where it is "Both at center", and is still one sound at 0 or 1', () => {
expect(fxChainOf(compileSoundFx(forked().fx.graph).compiled!)).not.toHaveProperty('curve');
const f = forked();
f.mix.params.curve = 'both';
expect(fxChainOf(compileSoundFx(f.fx.graph).compiled!)).toMatchObject({ mix: 0.5, curve: 'both' });
f.mix.params.mix = 0;
expect(fxChainOf(compileSoundFx(f.fx.graph).compiled!)).toMatchObject({ pitch: 12 });
});
it('takes its share from a parameter, held between 0 and 1, and keeps both ways for it (#315)', () => {
const f = forked();
f.input.ports = [{ key: 'blend', kind: 'number', default: 0.25 }];