Sound #45

Closed
opened 2026-09-12 15:03:59 +02:00 by meleki · 1 comment
Owner

Sound: Audio-Assets und ein Node zum Abspielen.

Stand: es gibt keinerlei Audio, weder Asset-Typ noch Node.

Abzuklären:

  • Import wie bei Texturen (Datei hochladen) und welche Formate
  • Node: abspielen, stoppen, Lautstärke, Loop (Musik vs. Effekt)
  • Browser spielen erst nach der ersten Nutzergeste Ton ab

Übernommen aus todo-notes/sound.md beim Umzug auf das Issue-System (2026-09-12).

  • Angelegt: 2026-09-11 · bf41919 — Add todo notes for the next round of features, gaps and upkeep
  • Gemeldet von: Claude (Assistent)
Sound: Audio-Assets und ein Node zum Abspielen. Stand: es gibt keinerlei Audio, weder Asset-Typ noch Node. Abzuklären: - Import wie bei Texturen (Datei hochladen) und welche Formate - Node: abspielen, stoppen, Lautstärke, Loop (Musik vs. Effekt) - Browser spielen erst nach der ersten Nutzergeste Ton ab --- *Übernommen aus `todo-notes/sound.md` beim Umzug auf das Issue-System (2026-09-12).* - **Angelegt:** 2026-09-11 · `bf41919` — Add todo notes for the next round of features, gaps and upkeep - **Gemeldet von:** Claude (Assistent)
meleki added the todo-note label 2026-09-12 15:03:59 +02:00
admin added this to the Development project 2026-09-13 13:30:08 +02:00
admin moved this to Next in Development on 2026-09-15 06:24:53 +02:00
Author
Owner

Implemented in 5671a38, wiki in 452b31d.

Sound is an asset like any other. The 15th type, not placeable (Font is the twin): one binary, content-addressed on the server as data/sounds/{sha256}.{ext}, recognised by the file's own signature rather than the browser's MIME type — the same .m4a arrives as audio/mp4, audio/x-m4a or nothing at all. MP3 has no fixed signature, so it is matched by an ID3 tag or a frame header's eleven set bits. A sound is never re-encoded, exactly like a font.

The asset carries Volume (0-1) and Loop. Because sound is a row in the kind table, the picker, the list items and a Sound literal node come along for free.

Nodes: Play sound, Stop sound, Sound.

  • the node's volume multiplies the asset's;
  • loop on the node is a three-way choice — From the asset (default), On, Off — not a checkbox. A checkbox has to borrow one of its two positions to mean "nothing said", so a looping asset could never be played once. The port stays any, so a boolean arriving over an edge is still read literally;
  • every Play is its own voice: two jumps in a row sound twice, loop or not. Stop sound ends every voice of one file, or all of them with no selection.

The audio layer stops at the layer line. NodeContext.playSound(assetId, ...) takes an asset id, not a file — resolving it and combining asset with node values is the BehaviorSystem's job, because only it holds the project. Below that, AudioSink (runtime/systems/Audio.ts) knows nothing but files and voices, and viewport/SoundLibrary implements it against Web Audio. So core/ stays browser-free, and a run with no sink wired plays silence instead of crashing.

Against the clock: pause suspends the context (the tone resumes mid-note), stop or restart ends every voice including music, and time scale leaves the pitch alone — a game in slow motion should not sound like a tape.

One bug worth naming, because a test caught it: the first abort mechanism remembered "stopped" as a flag, which would have let an earlier Stop swallow the very first Play of the same file while it was still decoding. Stops are counted per file now, and a play compares the count from before with the one from after.

Evidence

npm run check            -> 81 files, 1050 tests, green
CI task 48 (5671a38)     -> === CHECK EXIT 0 ===
                            ALL PASSED - 901 checks
                            smokes 24 / 19 / 21

New: step 34 sound (18 checks) and two unit test files. CI runs the browser muted, so __gm.sound.stats() is the only witness — the same lesson as render-on-change. The harness passes --autoplay-policy=no-user-gesture-required: muted yes, but the voice has to start, otherwise the check measures the browser instead of the editor. Fixture sounds are generated WAVs (tools/lib/audio.mjs); unlike DejaVu there is no audio file the host and the CI image safely share.

Not touched: nginx and php.ini. The two limits that live in this repo are set (MAX_SOUND_BYTES 16 MB, MAX_BODY_BYTES 8 -> 20 MB) and only take effect once client_max_body_size follows.

Deliberately left for when they are actually needed, one file each: Set volume, Is playing.

Implemented in 5671a38, wiki in 452b31d. **Sound is an asset like any other.** The 15th type, not placeable (Font is the twin): one binary, content-addressed on the server as `data/sounds/{sha256}.{ext}`, recognised by the file's own **signature** rather than the browser's MIME type — the same `.m4a` arrives as `audio/mp4`, `audio/x-m4a` or nothing at all. MP3 has no fixed signature, so it is matched by an ID3 tag or a frame header's eleven set bits. A sound is never re-encoded, exactly like a font. The asset carries `Volume` (0-1) and `Loop`. Because `sound` is a row in the kind table, the picker, the list items and a `Sound` literal node come along for free. **Nodes:** `Play sound`, `Stop sound`, `Sound`. - the node's `volume` multiplies the asset's; - `loop` on the node is a **three-way choice** — *From the asset* (default), *On*, *Off* — not a checkbox. A checkbox has to borrow one of its two positions to mean "nothing said", so a looping asset could never be played once. The port stays `any`, so a boolean arriving over an edge is still read literally; - **every Play is its own voice**: two jumps in a row sound twice, loop or not. `Stop sound` ends every voice of one file, or all of them with no selection. **The audio layer stops at the layer line.** `NodeContext.playSound(assetId, ...)` takes an asset id, not a file — resolving it and combining asset with node values is the `BehaviorSystem`'s job, because only it holds the project. Below that, `AudioSink` (`runtime/systems/Audio.ts`) knows nothing but files and voices, and `viewport/SoundLibrary` implements it against Web Audio. So `core/` stays browser-free, and a run with no sink wired plays silence instead of crashing. **Against the clock:** pause suspends the context (the tone resumes mid-note), stop or restart ends every voice including music, and time scale leaves the pitch alone — a game in slow motion should not sound like a tape. **One bug worth naming**, because a test caught it: the first abort mechanism remembered "stopped" as a flag, which would have let an earlier `Stop` swallow the very first `Play` of the same file while it was still decoding. Stops are counted per file now, and a play compares the count from before with the one from after. **Evidence** ``` npm run check -> 81 files, 1050 tests, green CI task 48 (5671a38) -> === CHECK EXIT 0 === ALL PASSED - 901 checks smokes 24 / 19 / 21 ``` New: **step 34 `sound`** (18 checks) and two unit test files. CI runs the browser muted, so `__gm.sound.stats()` is the only witness — the same lesson as render-on-change. The harness passes `--autoplay-policy=no-user-gesture-required`: muted yes, but the voice has to *start*, otherwise the check measures the browser instead of the editor. Fixture sounds are **generated WAVs** (`tools/lib/audio.mjs`); unlike DejaVu there is no audio file the host and the CI image safely share. **Not touched:** nginx and `php.ini`. The two limits that live in this repo are set (`MAX_SOUND_BYTES` 16 MB, `MAX_BODY_BYTES` 8 -> 20 MB) and only take effect once `client_max_body_size` follows. Deliberately left for when they are actually needed, one file each: `Set volume`, `Is playing`.
admin moved this to Done in Development on 2026-09-15 09:40:42 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#45