A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s

Sounds follow the road fonts already paved: the file goes to the server
content-addressed under its sha256, the project keeps a named asset that
points at it, and nothing is ever re-encoded. The format is read from the
file's own signature, not from what the browser claims - the same .m4a
arrives as audio/mp4, audio/x-m4a or as nothing at all.

Playing is two nodes. "Play sound" takes the asset and may raise or lower
its volume; every call is its own voice, so two jumps in a row are two
jumps heard. Its loop input is a choice of three - from the asset, on,
off - because a checkbox has to spend one of its two positions on "not
set" and would lose a case: an asset that loops could then never be
played once.

Below the graph core stays browser-free: NodeContext hands out an asset
id, BehaviorSystem resolves it against the project and folds the asset's
own volume and loop into the call, and only viewport/SoundLibrary knows
about Web Audio. A run without a sink plays nothing instead of throwing.

The CI browser is muted, so the ear is no witness: the debug hook counts
started and stopped voices, and step 34 reads that counter.
This commit is contained in:
admin committed 2026-09-15 07:54:28 +02:00
1 parent 7babf88636
commit 5671a38044
47 files changed
+2066 -42

No files matched your search

+5 -1
View File
@@ -5,6 +5,7 @@ import { hasEditorRun, runsInEditor } from '../core/properties/editorRun';
import { addGameSystems, type BehaviorSystem, behaviorError } from '../runtime/systems/BehaviorSystem';
import { Input } from '../runtime/systems/Input';
import { type ModelWrite, World } from '../runtime/World';
import { soundLibrary } from '../viewport/SoundLibrary';
import type { LiveMachineState } from './graph/GraphCanvas';
import type { EditorState } from './state';
@@ -119,7 +120,7 @@ export class EditorRun {
// Baking a container's content into the scene would be an edit nobody asked for, so it is off
// here: the content runs, but resolved from its template (see `WorldOptions.bakeContainers`).
const fresh = World.instantiate(project, scene.id, { bakeContainers: false });
this.behaviors = addGameSystems(fresh, project, this.input, (e) => runsInEditor(e.props));
this.behaviors = addGameSystems(fresh, project, this.input, (e) => runsInEditor(e.props), soundLibrary);
this.world = fresh;
this.hadFocus = this.hooks.focused();
this.input.attach(this.hooks.focused);
@@ -153,6 +154,9 @@ export class EditorRun {
private stop(): void {
if (!this.world) return;
// Its sounds end with it, for the reason a played run's do – an editor run is restarted by any
// edit, and a loop left behind would pile up one voice per edit.
soundLibrary.stopAll();
this.world.destroy();
this.world = null;
this.behaviors = null;