Keep the audio thread free of ended voices and too many vocoders (#356)
A song that had been played a few times in a long session stuttered at regular intervals until the page was reloaded. A quick run up the keys of a sampled instrument made every sound drop out for a second or two. Both came from work the audio thread should not have had. - Every worklet node of a voice listened on its port. Chrome never collects a port that is listened to, and through its listener not the voice either. Every node of the voice stayed on its bus and was worked on in every block. A way or stage on the worklet now closes its port once it ends or is stopped, and the processor closes its own side when it is told to stop. - A voice without echoes now leaves its bus as soon as its sound and what its stages hold back are through. Before, only a voice with echoes did. - At most 12 ways play through the pitch worklet at once. One more cuts the oldest in 10 ms, a note already let go before one still held. Each runs a phase vocoder, whose first block alone took 2.7 ms of the 2.67 ms a block has on stein2. - Play for the game ends everything the tabs play: the piece holding the transport and every key or preview (`stopPreviews`). - Closing a Sound, Sound FX, Instrument, Pattern or Song tab stops what it plays of that asset (`AssetTabEditor.closed`). - A preset in the Instrument tab plays nothing any more. With Audition on it is heard like any other change. - Test hooks: `__gm.instrument.play`, `__gm.sound.playing`, and `vocoders` in the stats of the sound library. Measured: - A copy of the project, its Song 2 played for 12 s and stopped, six times (Chrome on stein2, CDP WebAudio): before, 746 nodes were left after the first time and 4469 after the sixth, the load at rest grew from 0.01 to 0.06. After, 7 nodes are left every time (the master and one bus) and the load at rest stays at 0.01 to 0.02. - Pitch-player voices live in Chrome on melektaus-atx: 12 take 0.50 of the audio thread, 16 take 0.71, 24 take 0.99. 30 struck 40 ms apart held it at 0.99 from the first second on. Tests: - Unit sound-library: 110 green (3 new), 4 counter-probes each red - e2e 34 (melektaus-atx): ALL PASSED, 86 checks (3 new, 2 changed) - e2e counter-probe with game start and preset undone: their 3 checks red, and the old check of Play note in the game as well, which now counts on the preview being gone - e2e counter-probe with tab close undone: exactly its check red. A first form of it held a key and stayed green, since the keys let go of a held key by themselves once the tab shows no asset - Gate (melektaus-atx): 270 files, 4181 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c0d1cfd3f
commit
25c1d02b06
14 files changed
+269
-88
No files matched your search
@@ -95,7 +95,12 @@ interface FakeWorklet {
|
||||
pitch: FakeParam;
|
||||
parameters: Map<string, FakeParam>;
|
||||
posted: unknown[];
|
||||
port: { postMessage: (message: unknown) => void; onmessage: ((event: { data: unknown }) => void) | null };
|
||||
port: {
|
||||
postMessage: (message: unknown) => void;
|
||||
onmessage: ((event: { data: unknown }) => void) | null;
|
||||
close: () => void;
|
||||
closed: boolean;
|
||||
};
|
||||
disconnected: boolean;
|
||||
connect: (target: unknown) => unknown;
|
||||
disconnect: () => void;
|
||||
@@ -362,6 +367,10 @@ beforeEach(() => {
|
||||
port: {
|
||||
postMessage: (message: unknown) => node.posted.push(message),
|
||||
onmessage: null,
|
||||
close: () => {
|
||||
node.port.closed = true;
|
||||
},
|
||||
closed: false,
|
||||
},
|
||||
disconnected: false,
|
||||
disconnect: () => {
|
||||
@@ -2551,3 +2560,52 @@ describe('Wet (#354)', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('what has ended is let go', () => {
|
||||
it('closes the port of a pitched way once it ends or is stopped, so its voice can be collected', async () => {
|
||||
const library = new SoundLibrary();
|
||||
await play(library, FILE, {}, { pitch: 3, stages: [] });
|
||||
await play(library, FILE, {}, { pitch: 5, stages: [] });
|
||||
const [ended, stopped] = worklets;
|
||||
ended!.port.onmessage!({ data: 'ended' });
|
||||
expect(ended!.port).toMatchObject({ closed: true, onmessage: null });
|
||||
expect(stopped!.port.closed).toBe(false);
|
||||
library.stop(FILE);
|
||||
clockTo();
|
||||
expect(stopped!.posted).toEqual(['stop']);
|
||||
expect(stopped!.port).toMatchObject({ closed: true, onmessage: null });
|
||||
});
|
||||
|
||||
it('takes a voice without echoes off its bus as soon as its sound has ended', async () => {
|
||||
const library = new SoundLibrary();
|
||||
await play(library);
|
||||
const before = disconnects.length;
|
||||
sources[0]!.onended!();
|
||||
expect(library.stats().playing).toBe(0);
|
||||
expect(disconnects.slice(before)).toEqual(['gain']);
|
||||
});
|
||||
|
||||
it('cuts the oldest pitched voices past twelve, one already let go before one still held', async () => {
|
||||
const library = new SoundLibrary();
|
||||
const oneShot: FxPlan = {
|
||||
pitch: [{ value: 3 }],
|
||||
stages: [],
|
||||
source: { file: FILE, volume: 1, loop: false, loopStart: 0, loopEnd: 0, gate: false },
|
||||
};
|
||||
const voices: number[] = [];
|
||||
const strike = async () => {
|
||||
voices.push(library.playNote('instrument:i', [oneShot], {}, 1));
|
||||
await settle();
|
||||
await arrive();
|
||||
};
|
||||
for (let i = 0; i < 12; i++) await strike();
|
||||
expect(voices.every((voice) => library.playing(voice))).toBe(true);
|
||||
library.release(voices[4]!);
|
||||
await strike();
|
||||
expect(voices.map((voice) => library.playing(voice))).toEqual(voices.map((_, i) => i !== 4));
|
||||
await strike();
|
||||
expect(voices.map((voice) => library.playing(voice))).toEqual(voices.map((_, i) => i !== 0 && i !== 4));
|
||||
expect(library.stats().playing).toBe(12);
|
||||
expect(worklets).toHaveLength(14);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user