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

@ -1,5 +1,5 @@
# load.ludic - a clip the runtime can actually open. Audio.load takes a real FILESYSTEM path (it # load.ludic - a clip the runtime can actually open. Audio.load takes a real FILESYSTEM path (it
# bottoms out in AVAudioPlayer), not the pack-aware file_open every other asset arrives through: in a # bottoms out in AVAudioFile), not the pack-aware file_open every other asset arrives through: in a
# bundle every clip loads as 0 while Fs.exists, which IS pack-aware, says it is there. So try the # bundle every clip loads as 0 while Fs.exists, which IS pack-aware, says it is there. So try the
# runtime's path, and on 0 lift the bytes out of the pack into the cache directory and load that. # runtime's path, and on 0 lift the bytes out of the pack into the cache directory and load that.

View file

@ -1,168 +1,367 @@
; ============================================================================ ; ============================================================================
; audio.ll — the macOS audio backend, written in LLVM IR. ; audio.ll — the macOS audio backend, written in LLVM IR.
; ;
; Like cocoa.ll, this compiles no Objective-C and no C: it drives AVAudioPlayer ; Like cocoa.ll, this compiles no Objective-C and no C: it drives AVAudioEngine
; through the Objective-C runtime's C ABI (objc_getClass / sel_registerName / ; through the Objective-C runtime's C ABI (objc_getClass / sel_registerName /
; objc_msgSend). ludicc splices it in — alongside cocoa.ll — only when a ; objc_msgSend). ludicc splices it in — alongside cocoa.ll — only when a
; windowed build actually uses Audio.* (g_uses_audio), and links AVFoundation ; windowed build actually uses Audio.* (g_uses_audio), and links AVFoundation
; then. A sound is loaded once into an AVAudioPlayer and the opaque player ; then. A sound is decoded once into a PCM buffer and given a voice of its own
; pointer is handed back to the Ludic side (audio.ludic), which keeps the handle ; on one shared engine; the opaque voice pointer is handed back to the Ludic
; table; these functions just operate on one player at a time. ; side (audio.ludic), which keeps the handle table.
;
; A voice is a player node (the buffer, scheduled again on every play), a
; varispeed (the rate) and a small mixer (the volume and the pan), in that order
; into the engine's main mixer. It was an AVAudioPlayer until AVFAudio was found
; to keep a 64-byte AudioQueueOwner for good on every play after the clip had
; finished, whatever was called around it and even after the player was
; released: a valley's footsteps added one a play for as long as it ran. An
; engine's voice replays with nothing kept.
; ;
; Exposed to Ludic (through the snd_* intrinsics, all is_windowed()-guarded so ; Exposed to Ludic (through the snd_* intrinsics, all is_windowed()-guarded so
; they vanish from a headless build): ; they vanish from a headless build):
; snd_load(path) -> player snd_play(p, loops, rate, vol) ; snd_load(path) -> voice snd_play(v, loops, rate, vol)
; snd_stop(p) snd_playing(p) -> bool ; snd_stop(v) snd_playing(v) -> bool
; snd_set_volume(p, vol) snd_set_rate(p, rate) snd_set_pan(p, pan) ; snd_set_volume(v, vol) snd_set_rate(v, rate) snd_set_pan(v, pan)
; loops: 0 = once, -1 = forever. rate / vol are 16.16 fixed (value/65536). ; loops: 0 = once, -1 = forever. rate / vol / pan are 16.16 fixed (value/65536).
; ============================================================================ ; ============================================================================
declare ptr @objc_getClass(ptr) declare ptr @objc_getClass(ptr)
declare ptr @sel_registerName(ptr) declare ptr @sel_registerName(ptr)
declare ptr @objc_msgSend(ptr, ptr, ...) declare ptr @objc_msgSend(ptr, ptr, ...)
declare ptr @objc_autoreleasePoolPush()
declare void @objc_autoreleasePoolPop(ptr)
declare ptr @malloc(i64)
declare i64 @clock_gettime_nsec_np(i32)
; the one engine every voice plays through, made with the first sound loaded
@snd_engine = internal global ptr null
; a voice is 64 bytes: node, varispeed, mixer, buffer (pointers at 0, 8, 16, 24), the buffer's
; frames (i64 at 32), whether it loops (i32 at 40), its length in seconds (double at 48) and when a
; one-shot ends on the uptime clock (i64 nanoseconds at 56)
@CLOCK_UPTIME_RAW = internal constant i32 8
@.a_avp = private unnamed_addr constant [14 x i8] c"AVAudioPlayer\00"
@.a_nsstr = private unnamed_addr constant [9 x i8] c"NSString\00" @.a_nsstr = private unnamed_addr constant [9 x i8] c"NSString\00"
@.a_nsurl = private unnamed_addr constant [6 x i8] c"NSURL\00" @.a_nsurl = private unnamed_addr constant [6 x i8] c"NSURL\00"
@.a_utf8 = private unnamed_addr constant [22 x i8] c"stringWithUTF8String:\00" @.a_utf8 = private unnamed_addr constant [22 x i8] c"stringWithUTF8String:\00"
@.a_furl = private unnamed_addr constant [17 x i8] c"fileURLWithPath:\00" @.a_furl = private unnamed_addr constant [17 x i8] c"fileURLWithPath:\00"
@.a_alloc = private unnamed_addr constant [6 x i8] c"alloc\00" @.a_alloc = private unnamed_addr constant [6 x i8] c"alloc\00"
@.a_init = private unnamed_addr constant [29 x i8] c"initWithContentsOfURL:error:\00" @.a_initv = private unnamed_addr constant [5 x i8] c"init\00"
@.a_enrat = private unnamed_addr constant [15 x i8] c"setEnableRate:\00" @.a_rel = private unnamed_addr constant [8 x i8] c"release\00"
@.a_prep = private unnamed_addr constant [14 x i8] c"prepareToPlay\00" @.c_file = private unnamed_addr constant [12 x i8] c"AVAudioFile\00"
@.a_svol = private unnamed_addr constant [11 x i8] c"setVolume:\00" @.c_pcm = private unnamed_addr constant [17 x i8] c"AVAudioPCMBuffer\00"
@.a_srate = private unnamed_addr constant [9 x i8] c"setRate:\00" @.c_eng = private unnamed_addr constant [14 x i8] c"AVAudioEngine\00"
@.a_sloop = private unnamed_addr constant [18 x i8] c"setNumberOfLoops:\00" @.c_node = private unnamed_addr constant [18 x i8] c"AVAudioPlayerNode\00"
@.a_sctime= private unnamed_addr constant [16 x i8] c"setCurrentTime:\00" @.c_vs = private unnamed_addr constant [21 x i8] c"AVAudioUnitVarispeed\00"
@.c_mix = private unnamed_addr constant [17 x i8] c"AVAudioMixerNode\00"
@.a_finit = private unnamed_addr constant [22 x i8] c"initForReading:error:\00"
@.a_pfmt = private unnamed_addr constant [17 x i8] c"processingFormat\00"
@.a_len = private unnamed_addr constant [7 x i8] c"length\00"
@.a_binit = private unnamed_addr constant [33 x i8] c"initWithPCMFormat:frameCapacity:\00"
@.a_read = private unnamed_addr constant [22 x i8] c"readIntoBuffer:error:\00"
@.a_mmix = private unnamed_addr constant [14 x i8] c"mainMixerNode\00"
@.a_attach= private unnamed_addr constant [12 x i8] c"attachNode:\00"
@.a_conn = private unnamed_addr constant [19 x i8] c"connect:to:format:\00"
@.a_start = private unnamed_addr constant [21 x i8] c"startAndReturnError:\00"
@.a_isrun = private unnamed_addr constant [10 x i8] c"isRunning\00"
@.a_sched = private unnamed_addr constant [49 x i8] c"scheduleBuffer:atTime:options:completionHandler:\00"
@.a_play = private unnamed_addr constant [5 x i8] c"play\00" @.a_play = private unnamed_addr constant [5 x i8] c"play\00"
@.a_stop = private unnamed_addr constant [5 x i8] c"stop\00" @.a_stop = private unnamed_addr constant [5 x i8] c"stop\00"
@.a_isply = private unnamed_addr constant [10 x i8] c"isPlaying\00" @.a_isply = private unnamed_addr constant [10 x i8] c"isPlaying\00"
@.a_svol = private unnamed_addr constant [11 x i8] c"setVolume:\00"
@.a_srate = private unnamed_addr constant [9 x i8] c"setRate:\00"
@.a_span = private unnamed_addr constant [8 x i8] c"setPan:\00" @.a_span = private unnamed_addr constant [8 x i8] c"setPan:\00"
@.a_srt = private unnamed_addr constant [11 x i8] c"sampleRate\00"
; Load a sound file into an AVAudioPlayer; returns the player (or null). ; [[cls alloc] init]
define internal ptr @snd_new(ptr %clsname) {
entry:
%cls = call ptr @objc_getClass(ptr %clsname)
%sa = call ptr @sel_registerName(ptr @.a_alloc)
%o0 = call ptr (ptr, ptr) @objc_msgSend(ptr %cls, ptr %sa)
%si = call ptr @sel_registerName(ptr @.a_initv)
%o = call ptr (ptr, ptr) @objc_msgSend(ptr %o0, ptr %si)
ret ptr %o
}
; the engine, made the first time a sound is loaded
define internal ptr @snd_engine_get() {
entry:
%e = load ptr, ptr @snd_engine
%have = icmp ne ptr %e, null
br i1 %have, label %done, label %make
make:
%n = call ptr @snd_new(ptr @.c_eng)
store ptr %n, ptr @snd_engine
br label %done
done:
%r = load ptr, ptr @snd_engine
ret ptr %r
}
; the engine running: after a start, and again after the output device changed (which stops it)
define internal void @snd_engine_run(ptr %e) {
entry:
%nil = icmp eq ptr %e, null
br i1 %nil, label %out, label %ask
ask:
%sr = call ptr @sel_registerName(ptr @.a_isrun)
%run = call i8 (ptr, ptr) @objc_msgSend(ptr %e, ptr %sr)
%on = icmp ne i8 %run, 0
br i1 %on, label %out, label %start
start:
%ss = call ptr @sel_registerName(ptr @.a_start)
%ok = call i8 (ptr, ptr, ptr) @objc_msgSend(ptr %e, ptr %ss, ptr null)
br label %out
out:
ret void
}
; Load a sound file into a voice; returns the voice (or null).
define ptr @snd_load(ptr %path) { define ptr @snd_load(ptr %path) {
entry: entry:
%pool = call ptr @objc_autoreleasePoolPush()
%strcls = call ptr @objc_getClass(ptr @.a_nsstr) %strcls = call ptr @objc_getClass(ptr @.a_nsstr)
%sel_u = call ptr @sel_registerName(ptr @.a_utf8) %sel_u = call ptr @sel_registerName(ptr @.a_utf8)
%s = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %path) %s = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %path)
%urlcls = call ptr @objc_getClass(ptr @.a_nsurl) %urlcls = call ptr @objc_getClass(ptr @.a_nsurl)
%sel_f = call ptr @sel_registerName(ptr @.a_furl) %sel_f = call ptr @sel_registerName(ptr @.a_furl)
%url = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %urlcls, ptr %sel_f, ptr %s) %url = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %urlcls, ptr %sel_f, ptr %s)
%avpcls = call ptr @objc_getClass(ptr @.a_avp) %srel = call ptr @sel_registerName(ptr @.a_rel)
%nocls = icmp eq ptr %avpcls, null %sa = call ptr @sel_registerName(ptr @.a_alloc)
br i1 %nocls, label %fail, label %alloc %fcls = call ptr @objc_getClass(ptr @.c_file)
alloc: %nocls = icmp eq ptr %fcls, null
%sel_a = call ptr @sel_registerName(ptr @.a_alloc) br i1 %nocls, label %fail, label %open
%p0 = call ptr (ptr, ptr) @objc_msgSend(ptr %avpcls, ptr %sel_a) open:
%sel_i = call ptr @sel_registerName(ptr @.a_init) %f0 = call ptr (ptr, ptr) @objc_msgSend(ptr %fcls, ptr %sa)
; initWithContentsOfURL:error: — pass NULL for the NSError** out-parameter. %sfi = call ptr @sel_registerName(ptr @.a_finit)
%p = call ptr (ptr, ptr, ptr, ptr) @objc_msgSend(ptr %p0, ptr %sel_i, ptr %url, ptr null) %file = call ptr (ptr, ptr, ptr, ptr) @objc_msgSend(ptr %f0, ptr %sfi, ptr %url, ptr null)
%nop = icmp eq ptr %p, null %nofile = icmp eq ptr %file, null
br i1 %nop, label %fail, label %ready br i1 %nofile, label %fail, label %read
ready: read:
; enable rate control so setRate: takes effect, and pre-buffer. %spf = call ptr @sel_registerName(ptr @.a_pfmt)
%sel_er = call ptr @sel_registerName(ptr @.a_enrat) %fmt = call ptr (ptr, ptr) @objc_msgSend(ptr %file, ptr %spf)
%e = call ptr (ptr, ptr, i8) @objc_msgSend(ptr %p, ptr %sel_er, i8 1) %sl = call ptr @sel_registerName(ptr @.a_len)
%sel_pr = call ptr @sel_registerName(ptr @.a_prep) %frames = call i64 (ptr, ptr) @objc_msgSend(ptr %file, ptr %sl)
%pr = call ptr (ptr, ptr) @objc_msgSend(ptr %p, ptr %sel_pr) %fr32 = trunc i64 %frames to i32
ret ptr %p %pcmcls = call ptr @objc_getClass(ptr @.c_pcm)
%b0 = call ptr (ptr, ptr) @objc_msgSend(ptr %pcmcls, ptr %sa)
%sbi = call ptr @sel_registerName(ptr @.a_binit)
%buf = call ptr (ptr, ptr, ptr, i32) @objc_msgSend(ptr %b0, ptr %sbi, ptr %fmt, i32 %fr32)
%nobuf = icmp eq ptr %buf, null
br i1 %nobuf, label %fail_file, label %fill
fill:
%srd = call ptr @sel_registerName(ptr @.a_read)
%rok = call i8 (ptr, ptr, ptr, ptr) @objc_msgSend(ptr %file, ptr %srd, ptr %buf, ptr null)
%good = icmp ne i8 %rok, 0
br i1 %good, label %voice, label %fail_buf
voice:
%e = call ptr @snd_engine_get()
%smm = call ptr @sel_registerName(ptr @.a_mmix)
%main = call ptr (ptr, ptr) @objc_msgSend(ptr %e, ptr %smm)
%node = call ptr @snd_new(ptr @.c_node)
%vs = call ptr @snd_new(ptr @.c_vs)
%mix = call ptr @snd_new(ptr @.c_mix)
%sat = call ptr @sel_registerName(ptr @.a_attach)
%a1 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %e, ptr %sat, ptr %node)
%a2 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %e, ptr %sat, ptr %vs)
%a3 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %e, ptr %sat, ptr %mix)
%sc = call ptr @sel_registerName(ptr @.a_conn)
%c1 = call ptr (ptr, ptr, ptr, ptr, ptr) @objc_msgSend(ptr %e, ptr %sc, ptr %node, ptr %vs, ptr %fmt)
%c2 = call ptr (ptr, ptr, ptr, ptr, ptr) @objc_msgSend(ptr %e, ptr %sc, ptr %vs, ptr %mix, ptr %fmt)
%c3 = call ptr (ptr, ptr, ptr, ptr, ptr) @objc_msgSend(ptr %e, ptr %sc, ptr %mix, ptr %main, ptr %fmt)
call void @snd_engine_run(ptr %e)
%rf = call ptr (ptr, ptr) @objc_msgSend(ptr %file, ptr %srel)
%ssr = call ptr @sel_registerName(ptr @.a_srt)
%rate = call double (ptr, ptr) @objc_msgSend(ptr %fmt, ptr %ssr)
%frd = sitofp i64 %frames to double
%ratepos = fcmp ogt double %rate, 0.0
%rate1 = select i1 %ratepos, double %rate, double 4.8e+04
%secs = fdiv double %frd, %rate1
%v = call ptr @malloc(i64 64)
store ptr %node, ptr %v
%v8 = getelementptr i8, ptr %v, i64 8
store ptr %vs, ptr %v8
%v16 = getelementptr i8, ptr %v, i64 16
store ptr %mix, ptr %v16
%v24 = getelementptr i8, ptr %v, i64 24
store ptr %buf, ptr %v24
%v32 = getelementptr i8, ptr %v, i64 32
store i64 %frames, ptr %v32
%v40 = getelementptr i8, ptr %v, i64 40
store i32 0, ptr %v40
%v48 = getelementptr i8, ptr %v, i64 48
store double %secs, ptr %v48
%v56 = getelementptr i8, ptr %v, i64 56
store i64 0, ptr %v56
call void @objc_autoreleasePoolPop(ptr %pool)
ret ptr %v
fail_buf:
%rb = call ptr (ptr, ptr) @objc_msgSend(ptr %buf, ptr %srel)
br label %fail_file
fail_file:
%rf2 = call ptr (ptr, ptr) @objc_msgSend(ptr %file, ptr %srel)
br label %fail
fail: fail:
call void @objc_autoreleasePoolPop(ptr %pool)
ret ptr null ret ptr null
} }
; Rewind to the start and play, applying loops / rate / volume. define internal ptr @snd_part(ptr %v, i64 %off) {
define void @snd_play(ptr %p, i32 %loops, i32 %rate, i32 %vol) {
entry: entry:
%nil = icmp eq ptr %p, null %a = getelementptr i8, ptr %v, i64 %off
%p = load ptr, ptr %a
ret ptr %p
}
; a 16.16 value as a float
define internal float @snd_fx(i32 %x) {
entry:
%f = sitofp i32 %x to float
%r = fdiv float %f, 6.5536e+04
ret float %r
}
; Rewind to the start and play, applying loops / rate / volume: the buffer scheduled again over
; whatever was playing, looping when loops is -1.
define void @snd_play(ptr %v, i32 %loops, i32 %rate, i32 %vol) {
entry:
%nil = icmp eq ptr %v, null
br i1 %nil, label %out, label %go br i1 %nil, label %out, label %go
go: go:
%vf = sitofp i32 %vol to float ; play autoreleases an AVAudioTime: its own pool, so a sound played before a frame's pool keeps nothing
%v = fdiv float %vf, 6.5536e+04 %pool = call ptr @objc_autoreleasePoolPush()
%sel_v = call ptr @sel_registerName(ptr @.a_svol) %e = load ptr, ptr @snd_engine
%rv = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_v, float %v) call void @snd_engine_run(ptr %e)
%rf = sitofp i32 %rate to float %node = call ptr @snd_part(ptr %v, i64 0)
%r = fdiv float %rf, 6.5536e+04 %vs = call ptr @snd_part(ptr %v, i64 8)
%sel_r = call ptr @sel_registerName(ptr @.a_srate) %mix = call ptr @snd_part(ptr %v, i64 16)
%rr = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_r, float %r) %buf = call ptr @snd_part(ptr %v, i64 24)
%l64 = sext i32 %loops to i64 %sst = call ptr @sel_registerName(ptr @.a_stop)
%sel_l = call ptr @sel_registerName(ptr @.a_sloop) %r0 = call ptr (ptr, ptr) @objc_msgSend(ptr %node, ptr %sst)
%rl = call ptr (ptr, ptr, i64) @objc_msgSend(ptr %p, ptr %sel_l, i64 %l64) %sv = call ptr @sel_registerName(ptr @.a_svol)
%sel_ct = call ptr @sel_registerName(ptr @.a_sctime) %vf = call float @snd_fx(i32 %vol)
%rc = call ptr (ptr, ptr, double) @objc_msgSend(ptr %p, ptr %sel_ct, double 0.0) %r1 = call ptr (ptr, ptr, float) @objc_msgSend(ptr %mix, ptr %sv, float %vf)
%sel_p = call ptr @sel_registerName(ptr @.a_play) %sr = call ptr @sel_registerName(ptr @.a_srate)
%rp = call ptr (ptr, ptr) @objc_msgSend(ptr %p, ptr %sel_p) %rf = call float @snd_fx(i32 %rate)
%r2 = call ptr (ptr, ptr, float) @objc_msgSend(ptr %vs, ptr %sr, float %rf)
%loop = icmp eq i32 %loops, -1
%opt = select i1 %loop, i64 3, i64 2 ; AVAudioPlayerNodeBufferLoops (1) | Interrupts (2)
%lp = zext i1 %loop to i32
%v40 = getelementptr i8, ptr %v, i64 40
store i32 %lp, ptr %v40
%ssc = call ptr @sel_registerName(ptr @.a_sched)
%r3 = call ptr (ptr, ptr, ptr, ptr, i64, ptr) @objc_msgSend(ptr %node, ptr %ssc, ptr %buf, ptr null, i64 %opt, ptr null)
%sp = call ptr @sel_registerName(ptr @.a_play)
%r4 = call ptr (ptr, ptr) @objc_msgSend(ptr %node, ptr %sp)
; a one-shot ends its length (at this rate) from now
%v48 = getelementptr i8, ptr %v, i64 48
%secs = load double, ptr %v48
%rpos = fcmp ogt float %rf, 0.0
%rf1 = select i1 %rpos, float %rf, float 1.0
%rd = fpext float %rf1 to double
%dur = fdiv double %secs, %rd
%durns = fmul double %dur, 1.0e+09
%dns = fptosi double %durns to i64
%clk = load i32, ptr @CLOCK_UPTIME_RAW
%now = call i64 @clock_gettime_nsec_np(i32 %clk)
%end = add i64 %now, %dns
%v56 = getelementptr i8, ptr %v, i64 56
store i64 %end, ptr %v56
call void @objc_autoreleasePoolPop(ptr %pool)
br label %out br label %out
out: out:
ret void ret void
} }
define void @snd_stop(ptr %p) { define void @snd_stop(ptr %v) {
entry: entry:
%nil = icmp eq ptr %p, null %nil = icmp eq ptr %v, null
br i1 %nil, label %out, label %go br i1 %nil, label %out, label %go
go: go:
%sel_s = call ptr @sel_registerName(ptr @.a_stop) %node = call ptr @snd_part(ptr %v, i64 0)
%rs = call ptr (ptr, ptr) @objc_msgSend(ptr %p, ptr %sel_s) %sst = call ptr @sel_registerName(ptr @.a_stop)
%sel_ct = call ptr @sel_registerName(ptr @.a_sctime) %r = call ptr (ptr, ptr) @objc_msgSend(ptr %node, ptr %sst)
%rc = call ptr (ptr, ptr, double) @objc_msgSend(ptr %p, ptr %sel_ct, double 0.0) %v40 = getelementptr i8, ptr %v, i64 40
store i32 0, ptr %v40
%v56 = getelementptr i8, ptr %v, i64 56
store i64 0, ptr %v56
br label %out br label %out
out: out:
ret void ret void
} }
define i32 @snd_playing(ptr %p) { ; Playing while the node plays and, for a one-shot, until the time its buffer ends: a player node
; goes on "playing" silence after its buffer, and asking it where it is made two AVAudioTime objects a
; call that nothing gave back, so the end is worked out when it is played and only compared here.
define i32 @snd_playing(ptr %v) {
entry: entry:
%nil = icmp eq ptr %p, null %nil = icmp eq ptr %v, null
br i1 %nil, label %zero, label %ask br i1 %nil, label %zero, label %ask
ask: ask:
%sel_ip = call ptr @sel_registerName(ptr @.a_isply) %node = call ptr @snd_part(ptr %v, i64 0)
%b = call i8 (ptr, ptr) @objc_msgSend(ptr %p, ptr %sel_ip) %sip = call ptr @sel_registerName(ptr @.a_isply)
%bi = zext i8 %b to i32 %b = call i8 (ptr, ptr) @objc_msgSend(ptr %node, ptr %sip)
%r = and i32 %bi, 1 %on = icmp ne i8 %b, 0
ret i32 %r br i1 %on, label %loopq, label %zero
loopq:
%v40 = getelementptr i8, ptr %v, i64 40
%lp = load i32, ptr %v40
%looping = icmp ne i32 %lp, 0
br i1 %looping, label %one, label %clock
clock:
%v56 = getelementptr i8, ptr %v, i64 56
%end = load i64, ptr %v56
%clk = load i32, ptr @CLOCK_UPTIME_RAW
%now = call i64 @clock_gettime_nsec_np(i32 %clk)
%left = icmp slt i64 %now, %end
%li = zext i1 %left to i32
ret i32 %li
one:
ret i32 1
zero: zero:
ret i32 0 ret i32 0
} }
define void @snd_set_volume(ptr %p, i32 %vol) { define void @snd_set_volume(ptr %v, i32 %vol) {
entry: entry:
%nil = icmp eq ptr %p, null %nil = icmp eq ptr %v, null
br i1 %nil, label %out, label %go br i1 %nil, label %out, label %go
go: go:
%vf = sitofp i32 %vol to float %mix = call ptr @snd_part(ptr %v, i64 16)
%v = fdiv float %vf, 6.5536e+04 %sv = call ptr @sel_registerName(ptr @.a_svol)
%sel_v = call ptr @sel_registerName(ptr @.a_svol) %f = call float @snd_fx(i32 %vol)
%rv = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_v, float %v) %r = call ptr (ptr, ptr, float) @objc_msgSend(ptr %mix, ptr %sv, float %f)
br label %out br label %out
out: out:
ret void ret void
} }
; Stereo position, -1.0 (hard left) .. 1.0 (hard right). AVAudioPlayer's own pan, so it ; Stereo position, -1.0 (hard left) .. 1.0 (hard right): the voice's mixer's pan into the main
; costs nothing and needs no mixer: a one-shot can be placed before it is fired. ; mixer, so a one-shot can be placed before it is fired.
define void @snd_set_pan(ptr %p, i32 %pan) { define void @snd_set_pan(ptr %v, i32 %pan) {
entry: entry:
%nil = icmp eq ptr %p, null %nil = icmp eq ptr %v, null
br i1 %nil, label %out, label %go br i1 %nil, label %out, label %go
go: go:
%pf = sitofp i32 %pan to float %mix = call ptr @snd_part(ptr %v, i64 16)
%v = fdiv float %pf, 6.5536e+04 %sp = call ptr @sel_registerName(ptr @.a_span)
%sel_p = call ptr @sel_registerName(ptr @.a_span) %f = call float @snd_fx(i32 %pan)
%rp = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_p, float %v) %r = call ptr (ptr, ptr, float) @objc_msgSend(ptr %mix, ptr %sp, float %f)
br label %out br label %out
out: out:
ret void ret void
} }
define void @snd_set_rate(ptr %p, i32 %rate) { define void @snd_set_rate(ptr %v, i32 %rate) {
entry: entry:
%nil = icmp eq ptr %p, null %nil = icmp eq ptr %v, null
br i1 %nil, label %out, label %go br i1 %nil, label %out, label %go
go: go:
%rf = sitofp i32 %rate to float %vs = call ptr @snd_part(ptr %v, i64 8)
%r = fdiv float %rf, 6.5536e+04 %sr = call ptr @sel_registerName(ptr @.a_srate)
%sel_r = call ptr @sel_registerName(ptr @.a_srate) %f = call float @snd_fx(i32 %rate)
%rr = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_r, float %r) %r = call ptr (ptr, ptr, float) @objc_msgSend(ptr %vs, ptr %sr, float %f)
br label %out br label %out
out: out:
ret void ret void

View file

@ -2,7 +2,7 @@
# audio.ludic — the Audio.* standard library (#22). # audio.ludic — the Audio.* standard library (#22).
# #
# A small, game-shaped sound API over the platform audio backend (audio.ll, # 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 # 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 # of the deterministic simulation — the device output is real-time and
# non-deterministic — but every *trigger* here is an ordinary function call the # 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 { export state RtAudioState {
snd_ready: bool = false 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_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_rate: fixed = 1.0 # playback rate / pitch (1.0 = normal)
snd_music: int = 0 # the handle currently playing as music (0 = none) 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 }
if pn > 1.0 { pn = 1.0 } if pn > 1.0 { pn = 1.0 }
var pt = pitch 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 } if pt > 4.0 { pt = 4.0 }
snd_set_pan(p, pn) snd_set_pan(p, pn)
snd_play(p, 0, pt, g * rt_audio_st.snd_master) snd_play(p, 0, pt, g * rt_audio_st.snd_master)