Decode a clip from a file of its own, not its whole sound (#349)
game-maker / gate (push) Successful in 4m3s
game-maker / suite (push) Failing after 14m58s

A clip was cut from its sound's file, so a game decoded the whole file
for it and held it for as long as the page was open. A long recording
played through a few short clips cost the recording. Every clip now has
a copy of its cut as Opus, with Stream on or off, and a game decodes
that instead.

- StreamKeeper makes the copy of each clip whether its sound streams
  or not. Stream only decides whether a sound gets a copy of its own.
- clipCopyTable says which cut decodes from which copy. The sound
  library loads the copy for a clip unless the whole file is decoded
  already. Then the clip is cut from that as before.
- projectFiles decodes the whole file ahead only where something may
  play the sound itself: no clips, a clip without a fitting copy, or
  the sound's id anywhere in an asset that can name one. That last
  test walks the data, not the fields, so no place is missed.
- A copy that cannot be had falls back to the whole file. A library
  that bakes keeps to the whole files, as the bake's fingerprint does.

Tests:
- Gate: CHECK EXIT 0, 280 files, 4319 tests (7 new: streams +4,
  sound-library +3).
- e2e 34: ALL PASSED, 108 checks (2 new). With Stream off a fresh
  library decodes the ten-second reversed clip's copy and not the
  five-minute file. It is the cut made from the original to within
  0.0091 a sample and 0.013 dB in any 20 ms. Once a node plays the
  sound itself, the whole file is decoded ahead again.
- e2e 65: all passed with this change.
- Counter-checks red as they should be: the library ignoring copies,
  the whole file always decoded ahead, clip copies only with Stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-09 20:53:49 +02:00
1 parent 2764919bb8
commit 82e96e680f
10 files changed
+332 -42

No files matched your search

+48
View File
@@ -505,6 +505,54 @@ describe('the sound library', () => {
expect(library.stats().playing).toBe(1);
});
describe('a clip with a copy of its own (#349)', () => {
const cut = `${FILE}#0.5~1~0.005~0.005~linear~linear`;
const copy = `${'a1'.repeat(6)}/${'d4'.repeat(32)}.ogg`;
/** Which files are fetched; one ending in `missing` is not there. */
function fetching(missing = '#'): string[] {
const urls: string[] = [];
vi.stubGlobal('fetch', (url: string) => {
urls.push(url.replace(/^.*\/sounds\//, ''));
if (url.endsWith(missing)) return Promise.resolve({ ok: false, status: 404 });
return Promise.resolve({ ok: true, arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)) });
});
return urls;
}
it('decodes its copy and plays all of it, the whole file never fetched', async () => {
const urls = fetching();
const library = new SoundLibrary(false);
library.setCopies(new Map([[cut, copy]]));
await play(library, cut);
expect(urls).toEqual([copy]);
expect(library.decoded(FILE)).toBeNull();
// The copy is the cut already: played as it is, not cut again.
expect((sources[0]?.buffer as { length: number }).length).toBe(1200);
expect(library.duration(cut)).toBe(1.5);
expect(library.stats()).toMatchObject({ started: 1, decoded: 1 });
});
it('is cut from the whole file as before while that is decoded anyway', async () => {
const urls = fetching();
const library = new SoundLibrary(false);
library.setCopies(new Map([[cut, copy]]));
await play(library);
await play(library, cut);
expect(urls).toEqual([FILE]);
expect((sources[1]?.buffer as { length: number }).length).toBe(400);
});
it('is cut from the whole file when its copy cannot be had', async () => {
const urls = fetching('.ogg');
const library = new SoundLibrary(false);
library.setCopies(new Map([[cut, copy]]));
await play(library, cut);
expect(urls).toEqual([copy, FILE]);
expect((sources[0]?.buffer as { length: number }).length).toBe(400);
});
});
it('sends the master out through a limiter at -1 dBFS once the worklet is there, late by it (#359)', async () => {
let load: () => void = () => {};
loadModule = () => new Promise((resolve) => (load = resolve));