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,33 @@
# audio.ludic — the Audio.* standard library (#22). Sound effects and music over
# the platform audio backend (AVAudioPlayer on macOS). A sound is loaded once
# into a handle; sfx fire one-shot, music loops on a single channel; master
# volume and pitch apply across everything.
#
# Playback is out-of-band — the audio device is real-time, not part of the
# deterministic simulation — but every trigger is an ordinary frame-driven call,
# so a recorded run replays the same sounds at the same frames. Headless (no
# window) every call is a safe no-op: load returns 0 and is_playing returns
# false, so the same code runs under the test harness. A full headless run:
# bin/ludic examples/library/audio.ludic -> 0 0 0 0 1
program AudioDemo {
function bi(b: bool) -> int { if b { return 1 }; return 0 }
entry {
let sfx = Audio.load("assets/sfx/blip.wav") # 0 headless (no audio device)
let mus = Audio.load("assets/music/theme.ogg") # 0 headless
print(sfx) # 0
print(mus) # 0
Audio.volume(0.8) # master volume, 0.0..1.0
Audio.pitch(1.25) # playback rate / pitch
Audio.play(sfx) # one-shot sound effect
Audio.play_music(mus) # looping background music
print(bi(Audio.is_playing(sfx))) # 0 headless
Audio.stop(sfx) # stop one sound
Audio.stop_music() # stop the music channel
Audio.stop_all() # stop everything
print(bi(Audio.is_playing(mus))) # 0
print(1) # 1 — every call returned cleanly
}
}