A pitch sweep counted down in On update or On fixed update runs in
game time, while the sound runs on the clock of the audio device. A
short sound magnifies any offset between the two, so the sweep came
out different on every play and most of all on the first.
- Set sound FX parameters has a new input time in seconds. The values
run evenly to their target in that time, scheduled on the audio
clock with linearRampToValueAtTime or exponentialRampToValueAtTime.
- Evenly in their own unit: a pitch in semitones and a gain in
decibels, an EQ band and a Mix share linearly in their value.
- A time of 0 or below keeps the quick glide of about 0.1 s.
- Each change starts from where the voice is now. Whatever was still
planned is dropped, so a change never queues behind an older one.
Firefox lacks cancelAndHoldAtTime, there the value is held by hand.
- A voice still waiting on its bytes starts a timed change with the
sound, so the whole sweep is heard.
Tests:
- node-catalog: time is handed on, a time below 0 is a quick glide.
- sound-library: semitones and decibels ramp in their own steps, a
timed change on a waiting voice starts with the sound.
- sound-library + node-catalog + soundfx: 419 tests, green.
- e2e 34 on melektaus-atx: ALL PASSED, 31 checks. A 2 s ramp from
depth 7 to -5 read speed 0.979, 0.926 and 0.778 half a second on.
- Counter-check: folding a timed change on a waiting voice into its
start values fails the new test.
- The gate did not run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Until now a value handed to a sound FX counted from the start of the
voice only. An engine whose pitch follows the speed, or a loop that
gets quieter, could not be built. Every play of Play sound now gives
out the number of its voice, and a new node lets the knobs of that one
voice glide to new values while it plays.
- Play sound has a new output voice: the number of this one play, or 0
where nothing was played. Kept in a Number variable it reaches the
voice from another event. Two plays of one sound are two voices.
- New action node Set sound FX parameters (exec, voice, fx params).
It reaches exactly the voice it is given, a detached one too. A
voice that has ended, or voice 0, is not reached.
- AudioSink.play returns the voice number and takes the plan and the
values (FxVoice) instead of a finished chain. AudioSink.tune takes a
voice number and new values. A name the voice's FX does not have is
left out, one not given keeps its value.
- SoundLibrary hands out the number at once, before the bytes are
there, and never twice. A voice still waiting for its bytes starts
with the values handed in meanwhile.
- Every knob of a voice moves with setTargetAtTime (30 ms time
constant). A knob whose value did not change is not sent again, so a
tune every frame costs next to nothing.
- A pitch on a playing voice glides by playbackRate. A new WSOLA copy
in the middle of a sound is not worked out, so the voice gets shorter
or longer while it is moved. On a loop that is not heard.
- fxChainOf now gives a chain whose shape depends on the plan alone.
A stage fed by a parameter stays at 0 dB, and a Mix fed by a
parameter keeps both ways. A typed-in 0 or Mix at 0/1 is still left
out by the compiler.
- Every way of a voice has a source of its own, also at the same
pitch, so each can glide on its own. The pitched copy is shared.
- The tab keeps the numbers of what it plays, so moving a parameter
field is heard at once.
- Test hook __gm.sound.levels(file) reads the knobs of every voice of
a file, oldest first, and stats() counts the voices tuned.
Tests:
- soundfx.test.ts: 29 tests (+2 for the node in a running world: the
voice of the first of two plays of one sound, and a play that played
nothing)
- sound-library.test.ts: 15 tests (+3: glide, reach by number,
pending voice)
- node-catalog.test.ts: 370 tests (+2, Play sound checks its voice)
- sounds + keep-on-scene-load: 31 tests, unchanged
- Counter-test: a tune that reaches every voice of the same file makes
the reach test fail.
- e2e 34 (melektaus-atx): ALL PASSED - 30 checks (+1). In Chrome two
looping voices of one sound start at depth 7. The one whose number
sits in a variable glides to 0.7938 (2^(-4/12) = 0.7937), the other
stays at 1. Both gains stay at 0.5012.
- The gate ran before voices got a number (CHECK EXIT 0, 241 files,
3658 tests), not after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>