Load the sounds of a project ahead of their first play

A sound was fetched and decoded on its first Play sound. That play
started late, its beginning was not heard, and a graph that tuned the
voice in the meantime had moved on by the time it started. Images and
fonts were already loaded when a project opens. Sounds now are too.

- projectFiles lists the sounds of a project, and preloadProjectFiles
  hands them to the new soundLibrary.preload. That covers the editor
  and the published game.
- Preloading opens no audio device. It decodes with an
  OfflineAudioContext at 48 kHz. The real AudioContext is still made
  on the first play, and a source resamples where the device runs at
  another rate.
- A run started right after opening a project can still find a sound
  loading. It then starts late as before.

Tests:
- sound-library: a preload makes only the offline context, and a
  play after it starts at once, opening the device only then.
- sound-library + project-files: 18 tests, green.
- e2e 34 on melektaus-atx: ALL PASSED, 30 checks.
- Counter-check: decoding with the real AudioContext again fails the
  new test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-05 21:47:00 +02:00
1 parent ded0022903
commit ed2b58b3c0
5 files changed
+72 -8

No files matched your search

+13 -4
View File
@@ -1,6 +1,7 @@
/**
* What the editor loads ahead of the first draw: every stored file a project's assets name, so an
* animation or an animated tile does not blink out the first time it reaches a frame of its own.
* animation or an animated tile does not blink out the first time it reaches a frame of its own,
* and the first play of a sound is not late.
*/
import { describe, expect, it } from 'vitest';
import type { Asset } from '../../src/core/model/Asset';
@@ -12,6 +13,7 @@ const PNG_B = `${'b'.repeat(64)}.webp`;
const FONT = `${'c'.repeat(64)}.woff2`;
const CEL_A = `${'d'.repeat(64)}.cel`;
const CEL_B = `${'e'.repeat(64)}.cel`;
const SOUND = `${'f'.repeat(64)}.ogg`;
function projectWith(assets: unknown[]) {
const project = createEmptyProject('p', 'P');
@@ -20,7 +22,7 @@ function projectWith(assets: unknown[]) {
}
describe('the files a project names', () => {
it('lists every texture, font and cel, drawn somewhere or not', () => {
it('lists every texture, font, cel and sound, used somewhere or not', () => {
const project = projectWith([
{
id: 't1',
@@ -57,15 +59,22 @@ describe('the files a project names', () => {
{ id: 'f3', cels: { l: CEL_A } },
],
},
{ id: 's', type: 'sound', name: 'Blip', props: {}, file: SOUND },
]);
expect(projectFiles(project)).toEqual({ textures: [PNG_A, PNG_B], fonts: [FONT], cels: [CEL_A, CEL_B] });
expect(projectFiles(project)).toEqual({
textures: [PNG_A, PNG_B],
fonts: [FONT],
cels: [CEL_A, CEL_B],
sounds: [SOUND],
});
});
it('leaves out a name that is no stored file', () => {
const project = projectWith([
{ id: 't', type: 'texture', name: 'Failed', props: {}, file: '', width: 0, height: 0, frames: [] },
{ id: 'f', type: 'font', name: 'Failed', props: {}, file: 'x.woff2' },
{ id: 's', type: 'sound', name: 'Failed', props: {}, file: '' },
]);
expect(projectFiles(project)).toEqual({ textures: [], fonts: [], cels: [] });
expect(projectFiles(project)).toEqual({ textures: [], fonts: [], cels: [], sounds: [] });
});
});