A voice used to end the moment its sources did, and Stop sound cut it dead. An echo has to outlive its sound, and a stop must not click. - Delay node: a delay line with a damped feedback loop, added at mix next to the untouched sound. Time 0.01 to 2 s, feedback up to 0.95, damping 20 kHz to 500 Hz evenly in octaves, all fed by parameters and moved by Set sound FX parameters like any other number. - Ping-pong is a setting of the node. It takes the sound into the left of the line and swaps the sides every round. - A voice with an echo rings on for its tail: until the last echo is 60 dB below the sound, from time, feedback and mix (fxTailOf). It stays reachable meanwhile, and a change counts the tail again. - The end of a tail is timed on the audio clock with a silent ConstantSourceNode, so a paused run holds it. Then the voice is disconnected, since an echo is a loop of nodes. - Stop sound fades a voice out in 10 ms instead of cutting it. Let ring fades only its sources and keeps the voice for its echoes. Where the context is suspended it cuts at once. - Stop sound takes a voice. Connected, it stops only that play, and a 0 from a Play sound that played nothing stops nothing. - Play sound hands out tail: length plus the echoes at its start values. - Destroying a behavior and ending a run still cut, echoes included. - No limit on voices. The emit of a sound FX node now also gets the node's settings. Finding: in Chrome every round of a feedback loop comes one render quantum late (128 samples, 2.7 ms at 48 kHz), measured as 0, 128, 256, 384 for the first four echoes. Left so, since a browser without it would then hear the echoes early. Written in the wiki. Tests: - sound-library, soundfx, node-catalog, sounds, keep-on-scene-load: 489 tests green. New: fade on stop, cut while suspended, echo and ping-pong wiring, ringing until the tail, tail counted again, let ring, stop by voice, echo knobs, delay compile, tail sums. - Counter-checks: no tail, a stop without fade, a connected voice 0 read as unconnected. Each fails its tests, files back by cmp. - e2e 34 (melektaus-atx): ALL PASSED, 37 checks. A blip through a ping-pong Delay (0.3 s, 0.5, 0.5) rang 2.97 s, 2.95 s worked out. Let ring kept the voice, a stop after it cut it. Rendered offline with the player's own stageNodes the echoes are 0.25 left, 0.125 right, 0.063 left. 64 stereo voices of it took about 0.3 s for 2 s, so about 400 play at once. - Gate (melektaus-atx): CHECK EXIT 0, 242 test files, 3713 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e70421b92
commit
cf6799a784
16 files changed
+987
-72
No files matched your search
@@ -33,8 +33,18 @@ export interface AudioSink {
|
||||
* its bytes have not arrived.
|
||||
*/
|
||||
length(file: string): number;
|
||||
/** Silences every voice of that file; `''` silences all of them. */
|
||||
stop(file: string): void;
|
||||
/**
|
||||
* How many seconds one voice rings on after its sound has ended (#324), at the values it has now:
|
||||
* the echoes of its FX; 0 where it has none or is not there.
|
||||
*/
|
||||
tail(voice: number): number;
|
||||
/**
|
||||
* Silences every voice of that file; `''` silences all of them. A voice is faded out in a few
|
||||
* milliseconds, its echoes with it – unless `letRing`, which stops the sound and lets them fade.
|
||||
*/
|
||||
stop(file: string, letRing?: boolean): void;
|
||||
/** Silences one voice, by the number `play` gave it, the same way. */
|
||||
stopVoice(voice: number, letRing?: boolean): void;
|
||||
/** Silences every voice started by that owner – what destroying a behavior does. */
|
||||
stopOwner(owner: string): void;
|
||||
}
|
||||
@@ -43,6 +53,8 @@ export const SILENT_AUDIO: AudioSink = {
|
||||
play: () => 0,
|
||||
tune: () => {},
|
||||
length: () => 0,
|
||||
tail: () => 0,
|
||||
stop: () => {},
|
||||
stopVoice: () => {},
|
||||
stopOwner: () => {},
|
||||
};
|
||||
@@ -172,10 +172,12 @@ export function behaviorContext(
|
||||
},
|
||||
// An id nothing resolves stops nothing, rather than silencing the whole run by accident;
|
||||
// only the empty id means "everything", which is what the node promises.
|
||||
stopSound: (assetId) => {
|
||||
soundTail: (voice) => run.audio.tail(voice),
|
||||
stopSound: (assetId, letRing) => {
|
||||
const file = assetId === '' ? '' : soundFileOf(project, assetId);
|
||||
if (assetId === '' || file) run.audio.stop(file);
|
||||
if (assetId === '' || file) run.audio.stop(file, letRing);
|
||||
},
|
||||
stopVoice: (voice, letRing) => run.audio.stopVoice(voice, letRing),
|
||||
tuneSound: (voice, fxParams, time, ease) => run.audio.tune(voice, fxParams, time, ease),
|
||||
spawn: (assetId, position, parent) => world.spawn(assetId, position, resolve(parent) ?? null),
|
||||
addJoint: (assetId, owner, connected, at) => {
|
||||
|
||||
Reference in new issue
Block a user