Proposal: Audio standard library (Audio.*) — sfx/music playback, volume, deterministic triggers #22

Closed
opened 2026-08-29 23:39:26 +02:00 by orkun · 1 comment
Owner

Follow-up to #2 (namespaced standard library). The rest of the stdlib shipped on branch stdlib-namespaces, but Audio.* was left unimplemented because the runtime has no audio subsystem yet — it is a new platform feature, not a stdlib addition.

Proposed surface

Audio.play_sound(id), Audio.play_music(id), Audio.stop_music(), Audio.stop_all(), Audio.volume(v), Audio.pitch(v), Audio.is_playing(id) -> bool (PICO-8 sfx/music, LÖVE love.audio).

Work required

A platform audio backend in runtime/native/ (CoreAudio native, WebAudio for the wasm target) plus asset loading. Playback device output is non-deterministic, but trigger points are deterministic (driven by the frame clock), so recording/replay stays exact.

Blocked on: a runtime audio backend.

Follow-up to #2 (namespaced standard library). The rest of the stdlib shipped on branch `stdlib-namespaces`, but `Audio.*` was left unimplemented because the runtime has **no audio subsystem** yet — it is a new platform feature, not a stdlib addition. ## Proposed surface `Audio.play_sound(id)`, `Audio.play_music(id)`, `Audio.stop_music()`, `Audio.stop_all()`, `Audio.volume(v)`, `Audio.pitch(v)`, `Audio.is_playing(id) -> bool` (PICO-8 `sfx`/`music`, LÖVE `love.audio`). ## Work required A platform audio backend in `runtime/native/` (CoreAudio native, WebAudio for the wasm target) plus asset loading. Playback device output is non-deterministic, but **trigger points are deterministic** (driven by the frame clock), so recording/replay stays exact. **Blocked on:** a runtime audio backend.
orkun added the
proposal
priority:medium
area:stdlib
labels 2026-08-29 23:39:26 +02:00
Author
Owner

Shipped in 4eef5eb — Audio.* plus the platform audio backend this was blocked on.

Backend — runtime/native/audio.ll, an AVAudioPlayer client driven through the Objective-C runtime's C ABI, in exactly the same hand-written-IR style as cocoa.ll (no ObjC and no C source in the build). It exposes snd_load / snd_play / snd_stop / snd_playing / snd_set_volume / snd_set_rate. It is spliced in — and AVFoundation linked (-needed_framework, since AVAudioPlayer is reached by name) — only when a windowed program actually uses Audio.*; a game that doesn't touch audio links neither.

Surface — runtime/native/audio.ludic keeps a handle table, a master volume + pitch, and a single music channel:

  • Audio.load(path) -> int — load once, get a handle.
  • Audio.play(id) / Audio.play_sound(id) — one-shot sfx.
  • Audio.play_music(id) — loop on the music channel (replaces current music).
  • Audio.stop(id) / Audio.stop_music() / Audio.stop_all().
  • Audio.volume(v) — master volume 0..1, applied across all sounds.
  • Audio.pitch(v) — playback rate / pitch (AVAudioPlayer's enableRate), 1.0 normal.
  • Audio.is_playing(id) -> bool.

Determinism — playback output is real-time and out-of-band, and nothing here feeds the lockstep/replay sim, exactly as the proposal required. But every trigger is an ordinary call a frame handler makes, so a recorded run fires the same sounds at the same frames.

Headless — every native snd_* call is is_windowed()-guarded, so a headless build dead-strips the whole backend (and AVFoundation): Audio.load returns 0, Audio.is_playing returns false, and the rest no-op. The same game code runs under the test harness with no audio device — examples/library/audio.ludic self-asserts that path in the suite.

Docs: a full docs/language/audio section (10 method pages); check-impl / check-docs green. Full + self-host suites green (80 + 29).

Shipped in 4eef5eb — `Audio.*` plus the platform audio backend this was blocked on. **Backend** — `runtime/native/audio.ll`, an AVAudioPlayer client driven through the Objective-C runtime's C ABI, in exactly the same hand-written-IR style as `cocoa.ll` (no ObjC and no C source in the build). It exposes `snd_load / snd_play / snd_stop / snd_playing / snd_set_volume / snd_set_rate`. It is spliced in — and AVFoundation linked (`-needed_framework`, since `AVAudioPlayer` is reached by name) — **only when a windowed program actually uses `Audio.*`**; a game that doesn't touch audio links neither. **Surface** — `runtime/native/audio.ludic` keeps a handle table, a master volume + pitch, and a single music channel: - `Audio.load(path) -> int` — load once, get a handle. - `Audio.play(id)` / `Audio.play_sound(id)` — one-shot sfx. - `Audio.play_music(id)` — loop on the music channel (replaces current music). - `Audio.stop(id)` / `Audio.stop_music()` / `Audio.stop_all()`. - `Audio.volume(v)` — master volume 0..1, applied across all sounds. - `Audio.pitch(v)` — playback rate / pitch (AVAudioPlayer's `enableRate`), 1.0 normal. - `Audio.is_playing(id) -> bool`. **Determinism** — playback output is real-time and out-of-band, and nothing here feeds the lockstep/replay sim, exactly as the proposal required. But every trigger is an ordinary call a frame handler makes, so a recorded run fires the same sounds at the same frames. **Headless** — every native `snd_*` call is `is_windowed()`-guarded, so a headless build dead-strips the whole backend (and AVFoundation): `Audio.load` returns 0, `Audio.is_playing` returns false, and the rest no-op. The same game code runs under the test harness with no audio device — `examples/library/audio.ludic` self-asserts that path in the suite. Docs: a full `docs/language/audio` section (10 method pages); `check-impl` / `check-docs` green. Full + self-host suites green (80 + 29).
orkun closed this issue 2026-08-31 16:52:49 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#22
No description provided.