From dd2096e0df97c0a1b29db11948bc8b0006b9ba94 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Sun, 18 Oct 2026 22:13:48 +0200 Subject: [PATCH] Make a bus of the mix at its level, so a quiet bus starts quiet A bus of the project's mix was made at unity and then glided to its level. Where the audio context comes with the first sound of a run, that sound already plays on the new bus. On a bus at volume 0 it was heard for a moment, loud, while the level glided down. - setMix now sets the level of a bus it makes at once, without a glide. A bus that is there already glides to a new level as before. - Found while building #436: the e2e check with a sound on a bus at volume 0 measured -7.8 dB on that bus. With the fix it is silent. Tests: - New unit test in sound-library: buses made at their level with no glide, a later change glides. Red without the fix. - sound-library 130/130, tsc clean on this change alone. Co-Authored-By: Claude Opus 5.5 --- src/viewport/SoundLibrary.ts | 14 +++++++++++++- tests/viewport/sound-library.test.ts | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/viewport/SoundLibrary.ts b/src/viewport/SoundLibrary.ts index 85c03ae0..45fc5505 100644 --- a/src/viewport/SoundLibrary.ts +++ b/src/viewport/SoundLibrary.ts @@ -909,10 +909,15 @@ export class SoundLibrary implements AudioSink { const names = new Set(this.mix.map((bus) => bus.name)); const waits: Promise[] = []; for (const { name, volume, into, sends, fx } of this.mix) { + const made = !this.buses.has(name); const bus = this.bus(name, into); if (sends) this.setSends(bus, sends); else this.route(name, bus, into); - this.glideLevel(bus, this.mixVolumes.get(name) ?? volume); + const level = this.mixVolumes.get(name) ?? volume; + // A bus made now is at its level at once: made with the context, it carries the first sound of + // a run, which would otherwise be heard through a glide down from unity (found by #436). + if (made) this.setLevel(bus, level); + else this.glideLevel(bus, level); waits.push(this.setFx(bus, fx)); } const gone = [...this.buses.keys()].filter( @@ -923,6 +928,13 @@ export class SoundLibrary implements AudioSink { await Promise.all(waits); } + /** The level of a bus at `gain` from now on, without a glide. */ + private setLevel(bus: Bus, gain: number): void { + const to = Math.max(0, gain); + bus.gain.gain.value = to; + bus.set = { ...bus.set, gain: to }; + } + /** A quick glide of the level of a bus to `gain`. */ private glideLevel(bus: Bus, gain: number, time = 0, ease: EaseShare = 'linear'): void { const to = Math.max(0, gain); diff --git a/tests/viewport/sound-library.test.ts b/tests/viewport/sound-library.test.ts index c122c1ce..f05ee6e2 100644 --- a/tests/viewport/sound-library.test.ts +++ b/tests/viewport/sound-library.test.ts @@ -2539,6 +2539,22 @@ describe('the master and the buses of the project (#325)', () => { expect(wires.filter((wire) => wire.endsWith('> destination'))).toEqual(['panner > destination']); }); + it('makes a bus at its level, so the first sound of a run on a quiet bus is not heard gliding down (#436)', async () => { + const library = new SoundLibrary(false); + await library.setMix(mix); + await play(library); + expect(library.busLevels().map(({ name, level }) => [name, level])).toEqual([ + ['mix', 0.5], + ['mix:a', 1], + ['mix:b', 0.25], + ]); + expect(gains.filter((gain) => gain.glides.length > 0)).toEqual([]); + // One there already glides to a new level, as before. + await library.setMix([bus('mix', 0.25, null), mix[1]!, mix[2]!]); + expect(library.busLevels()[0]).toMatchObject({ gain: 0.25, level: 0.5 }); + expect(gains.some((gain) => gain.glides.includes(0.25))).toBe(true); + }); + it('keeps the mix where the first thing heard is a tab of a song, which names its bus before there is a context', async () => { const library = new SoundLibrary(false); await library.setMix(mix);