feat(audio): Audio.* standard library over a native AVAudioPlayer backend (#22)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m25s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s

Adds the Audio.* namespace and its platform backend, the audio subsystem #22 was
blocked on.

- runtime/native/audio.ll: the macOS backend, AVAudioPlayer driven through the
  objc runtime C ABI (no ObjC/C source), same style as cocoa.ll — snd_load /
  play / stop / playing / set_volume / set_rate. Spliced and linked with
  AVFoundation only when a windowed build actually uses Audio.* (needed_framework,
  since AVAudioPlayer is reached by name).
- runtime/native/audio.ludic: the Audio.* runtime — a handle table, master
  volume/pitch, a single music channel. load/play/play_sound/play_music/stop/
  stop_music/stop_all/volume/pitch/is_playing. Every native call is
  is_windowed()-guarded, so a headless build carries the API as no-ops (load
  returns 0, is_playing false) and needs no audio device.
- compiler: Audio.* namespace dispatch, g_uses_audio splice, snd_* intrinsics +
  declarations, and the conditional AVFoundation link in both the canonical
  (main.ludic) and dev-runner (x app) paths.
- docs: a full docs/language/audio section (10 method pages); check-impl green.
- test: examples/library/audio.ludic self-asserts the headless no-op path.

Playback is out-of-band and never feeds the deterministic sim, but triggers are
frame-driven so replays fire the same sounds. Reseeded; suites green (80 + 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 17:52:31 +03:00
parent d6ca320269
commit 4eef5ebbce
23 changed files with 22294 additions and 21004 deletions

View file

@ -0,0 +1,7 @@
---
id: audio
title: Audio
order: 7
---
Sound effects and music. A sound is loaded once with <a href="audio-load.html"><code>Audio.load</code></a> into a handle; <a href="audio-play.html"><code>Audio.play</code></a> fires it one-shot and <a href="audio-play_music.html"><code>Audio.play_music</code></a> loops it on a single music channel. Playback is <strong>out-of-band</strong> — the audio device is real-time, not part of the deterministic simulation — but every trigger here is an ordinary frame-driven call, so a recorded run replays the same sounds at the same frames. Headless builds carry the whole API as no-ops (no audio device needed), so the same game code runs under the test harness.

View file

@ -0,0 +1,23 @@
---
id: audio-is_playing
name: Audio.is_playing
category: audio
kind: namespace-method
tokens: Audio.is_playing
sig: Audio.is_playing(id) -> bool
tip: Is this sound playing?
order: 9
ns: Audio
member: is_playing
---
Returns whether the sound <code>id</code> is currently playing. Always <code>false</code> in a headless build.
```ludic
program Demo {
entry {
let sfx = Audio.load("blip.wav")
if Audio.is_playing(sfx) { print(1) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: audio-load
name: Audio.load
category: audio
kind: namespace-method
tokens: Audio.load
sig: Audio.load(path) -> int
tip: Load a sound file into a handle.
order: 0
ns: Audio
member: load
---
Loads a sound file and returns a handle (>= 1), or <code>0</code> on failure or in a headless build. Load each sound once (e.g. during the <code>Start</code> phase) and reuse the handle.
```ludic
program Demo {
entry {
let sfx = Audio.load("blip.wav")
print(sfx)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: audio-pitch
name: Audio.pitch
category: audio
kind: namespace-method
tokens: Audio.pitch
sig: Audio.pitch(v) -> void
tip: Set the playback rate / pitch.
order: 8
ns: Audio
member: pitch
---
Sets the playback rate / pitch (<code>1.0</code> normal, <code>0.5</code> an octave down, <code>2.0</code> up), applied to every loaded sound.
```ludic
program Demo {
entry {
Audio.pitch(1.5)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: audio-play
name: Audio.play
category: audio
kind: namespace-method
tokens: Audio.play
sig: Audio.play(id) -> void
tip: Fire a one-shot sound.
order: 1
ns: Audio
member: play
---
Plays the sound <code>id</code> from the start as a one-shot effect, at the current master volume and pitch.
```ludic
program Demo {
entry {
let sfx = Audio.load("blip.wav")
Audio.play(sfx)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: audio-play_music
name: Audio.play_music
category: audio
kind: namespace-method
tokens: Audio.play_music
sig: Audio.play_music(id) -> void
tip: Loop a sound as background music.
order: 3
ns: Audio
member: play_music
---
Plays the sound <code>id</code> as looping background music on the single music channel; any music already playing is stopped first.
```ludic
program Demo {
entry {
let theme = Audio.load("theme.ogg")
Audio.play_music(theme)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: audio-play_sound
name: Audio.play_sound
category: audio
kind: namespace-method
tokens: Audio.play_sound
sig: Audio.play_sound(id) -> void
tip: Fire a one-shot sound (alias of play).
order: 2
ns: Audio
member: play_sound
---
An alias of <a href="audio-play.html"><code>Audio.play</code></a> — plays the sound <code>id</code> once from the start.
```ludic
program Demo {
entry {
let sfx = Audio.load("blip.wav")
Audio.play_sound(sfx)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: audio-stop
name: Audio.stop
category: audio
kind: namespace-method
tokens: Audio.stop
sig: Audio.stop(id) -> void
tip: Stop one sound.
order: 4
ns: Audio
member: stop
---
Stops the sound <code>id</code> and rewinds it to the start. If it was the current music, the music channel is cleared.
```ludic
program Demo {
entry {
let sfx = Audio.load("blip.wav")
Audio.play(sfx)
Audio.stop(sfx)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: audio-stop_all
name: Audio.stop_all
category: audio
kind: namespace-method
tokens: Audio.stop_all
sig: Audio.stop_all() -> void
tip: Stop every sound.
order: 6
ns: Audio
member: stop_all
---
Stops every loaded sound, including the music channel.
```ludic
program Demo {
entry {
Audio.stop_all()
}
}
```

View file

@ -0,0 +1,22 @@
---
id: audio-stop_music
name: Audio.stop_music
category: audio
kind: namespace-method
tokens: Audio.stop_music
sig: Audio.stop_music() -> void
tip: Stop the music channel.
order: 5
ns: Audio
member: stop_music
---
Stops whatever is playing on the music channel (started by <a href="audio-play_music.html"><code>Audio.play_music</code></a>).
```ludic
program Demo {
entry {
Audio.stop_music()
}
}
```

View file

@ -0,0 +1,22 @@
---
id: audio-volume
name: Audio.volume
category: audio
kind: namespace-method
tokens: Audio.volume
sig: Audio.volume(v) -> void
tip: Set the master volume.
order: 7
ns: Audio
member: volume
---
Sets the master volume (<code>0.0</code> .. <code>1.0</code>), applied to every loaded sound now and to every future play.
```ludic
program Demo {
entry {
Audio.volume(0.8)
}
}
```