runtime/audio (macOS): voices on one AVAudioEngine instead of an AVAudioPlayer per sound

AVFAudio keeps a 64-byte AudioQueueOwner for good on every AVAudioPlayer play
after the clip has finished - measured one a play, whatever is called around
it (prepareToPlay, pause, no rewind) and still there after the player is
released; a stop before the play made one every time. The windowed walk showed
it as AudioQueueOwner 73 -> 87 in a minute.

A sound is now decoded once into a PCM buffer and played by a voice of its own
on one shared engine: a player node (the buffer scheduled again on each play,
looping for -1), a varispeed (the rate) and a small mixer (volume and pan).
snd_playing compares the uptime clock with the end worked out when the clip was
played (asking the node where it is made two AVAudioTime objects a call), and
snd_play drains an autorelease pool of its own. The C interface is unchanged.

A harness driving snd_* directly (400 plays past the end, the playing flag
checked during and after each, a loop and a stop, the setters) holds the heap
flat to a block and gets the flag right 400 of 400; the windowed valley
compiles and links against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 23:00:59 +03:00
parent 34421b8015
commit a5f6c02a09
3 changed files with 295 additions and 96 deletions

View file

@ -2,7 +2,7 @@
# audio.ludic — the Audio.* standard library (#22).
#
# A small, game-shaped sound API over the platform audio backend (audio.ll,
# AVAudioPlayer on macOS). A sound is loaded once into a handle; sfx are fired
# AVAudioEngine on macOS). A sound is loaded once into a handle; sfx are fired
# one-shot, music plays on a single looping channel. Playback itself is not part
# of the deterministic simulation — the device output is real-time and
# non-deterministic — but every *trigger* here is an ordinary function call the
@ -21,7 +21,7 @@ const AUDIO_CAP: int = 64 # max simultaneously-loaded sounds
export state RtAudioState {
snd_ready: bool = false
snd_tab: pointers = null # handle (1-based) -> AVAudioPlayer pointer
snd_tab: pointers = null # handle (1-based) -> the backend's voice pointer
snd_master: fixed = 1.0 # master volume, applied to every play (and to audio_play_at's gain)
snd_rate: fixed = 1.0 # playback rate / pitch (1.0 = normal)
snd_music: int = 0 # the handle currently playing as music (0 = none)
@ -124,7 +124,7 @@ function audio_play_at(rt_audio_st: mut RtAudioState, id: int, gain: fixed, pitc
if pn < -1.0 { pn = -1.0 }
if pn > 1.0 { pn = 1.0 }
var pt = pitch
if pt < 0.25 { pt = 0.25 } # AVAudioPlayer's own rate range
if pt < 0.25 { pt = 0.25 } # the varispeed's own rate range
if pt > 4.0 { pt = 4.0 }
snd_set_pan(p, pn)
snd_play(p, 0, pt, g * rt_audio_st.snd_master)