diff --git a/packages/ludic.audio/load.ludic b/packages/ludic.audio/load.ludic index 934d13a0..ec2082c3 100644 --- a/packages/ludic.audio/load.ludic +++ b/packages/ludic.audio/load.ludic @@ -1,5 +1,5 @@ # 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 # runtime's path, and on 0 lift the bytes out of the pack into the cache directory and load that. diff --git a/runtime/native/audio.ll b/runtime/native/audio.ll index b7904266..23c323e4 100644 --- a/runtime/native/audio.ll +++ b/runtime/native/audio.ll @@ -1,168 +1,367 @@ ; ============================================================================ ; 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 / ; objc_msgSend). ludicc splices it in — alongside cocoa.ll — only when a ; windowed build actually uses Audio.* (g_uses_audio), and links AVFoundation -; then. A sound is loaded once into an AVAudioPlayer and the opaque player -; pointer is handed back to the Ludic side (audio.ludic), which keeps the handle -; table; these functions just operate on one player at a time. +; then. A sound is decoded once into a PCM buffer and given a voice of its own +; on one shared engine; the opaque voice pointer is handed back to the Ludic +; 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 ; they vanish from a headless build): -; snd_load(path) -> player snd_play(p, loops, rate, vol) -; snd_stop(p) snd_playing(p) -> bool -; snd_set_volume(p, vol) snd_set_rate(p, rate) snd_set_pan(p, pan) -; loops: 0 = once, -1 = forever. rate / vol are 16.16 fixed (value/65536). +; snd_load(path) -> voice snd_play(v, loops, rate, vol) +; snd_stop(v) snd_playing(v) -> bool +; snd_set_volume(v, vol) snd_set_rate(v, rate) snd_set_pan(v, pan) +; loops: 0 = once, -1 = forever. rate / vol / pan are 16.16 fixed (value/65536). ; ============================================================================ declare ptr @objc_getClass(ptr) declare ptr @sel_registerName(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) -@.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_nsurl = private unnamed_addr constant [6 x i8] c"NSURL\00" +; 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_nsstr = private unnamed_addr constant [9 x i8] c"NSString\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_furl = private unnamed_addr constant [17 x i8] c"fileURLWithPath:\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_enrat = private unnamed_addr constant [15 x i8] c"setEnableRate:\00" -@.a_prep = private unnamed_addr constant [14 x i8] c"prepareToPlay\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_sloop = private unnamed_addr constant [18 x i8] c"setNumberOfLoops:\00" -@.a_sctime= private unnamed_addr constant [16 x i8] c"setCurrentTime:\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_alloc = private unnamed_addr constant [6 x i8] c"alloc\00" +@.a_initv = private unnamed_addr constant [5 x i8] c"init\00" +@.a_rel = private unnamed_addr constant [8 x i8] c"release\00" +@.c_file = private unnamed_addr constant [12 x i8] c"AVAudioFile\00" +@.c_pcm = private unnamed_addr constant [17 x i8] c"AVAudioPCMBuffer\00" +@.c_eng = private unnamed_addr constant [14 x i8] c"AVAudioEngine\00" +@.c_node = private unnamed_addr constant [18 x i8] c"AVAudioPlayerNode\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_stop = private unnamed_addr constant [5 x i8] c"stop\00" @.a_isply = private unnamed_addr constant [10 x i8] c"isPlaying\00" -@.a_span = private unnamed_addr constant [8 x i8] c"setPan:\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_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) { entry: + %pool = call ptr @objc_autoreleasePoolPush() %strcls = call ptr @objc_getClass(ptr @.a_nsstr) %sel_u = call ptr @sel_registerName(ptr @.a_utf8) %s = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %path) %urlcls = call ptr @objc_getClass(ptr @.a_nsurl) %sel_f = call ptr @sel_registerName(ptr @.a_furl) %url = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %urlcls, ptr %sel_f, ptr %s) - %avpcls = call ptr @objc_getClass(ptr @.a_avp) - %nocls = icmp eq ptr %avpcls, null - br i1 %nocls, label %fail, label %alloc -alloc: - %sel_a = call ptr @sel_registerName(ptr @.a_alloc) - %p0 = call ptr (ptr, ptr) @objc_msgSend(ptr %avpcls, ptr %sel_a) - %sel_i = call ptr @sel_registerName(ptr @.a_init) - ; initWithContentsOfURL:error: — pass NULL for the NSError** out-parameter. - %p = call ptr (ptr, ptr, ptr, ptr) @objc_msgSend(ptr %p0, ptr %sel_i, ptr %url, ptr null) - %nop = icmp eq ptr %p, null - br i1 %nop, label %fail, label %ready -ready: - ; enable rate control so setRate: takes effect, and pre-buffer. - %sel_er = call ptr @sel_registerName(ptr @.a_enrat) - %e = call ptr (ptr, ptr, i8) @objc_msgSend(ptr %p, ptr %sel_er, i8 1) - %sel_pr = call ptr @sel_registerName(ptr @.a_prep) - %pr = call ptr (ptr, ptr) @objc_msgSend(ptr %p, ptr %sel_pr) - ret ptr %p + %srel = call ptr @sel_registerName(ptr @.a_rel) + %sa = call ptr @sel_registerName(ptr @.a_alloc) + %fcls = call ptr @objc_getClass(ptr @.c_file) + %nocls = icmp eq ptr %fcls, null + br i1 %nocls, label %fail, label %open +open: + %f0 = call ptr (ptr, ptr) @objc_msgSend(ptr %fcls, ptr %sa) + %sfi = call ptr @sel_registerName(ptr @.a_finit) + %file = call ptr (ptr, ptr, ptr, ptr) @objc_msgSend(ptr %f0, ptr %sfi, ptr %url, ptr null) + %nofile = icmp eq ptr %file, null + br i1 %nofile, label %fail, label %read +read: + %spf = call ptr @sel_registerName(ptr @.a_pfmt) + %fmt = call ptr (ptr, ptr) @objc_msgSend(ptr %file, ptr %spf) + %sl = call ptr @sel_registerName(ptr @.a_len) + %frames = call i64 (ptr, ptr) @objc_msgSend(ptr %file, ptr %sl) + %fr32 = trunc i64 %frames to i32 + %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: + call void @objc_autoreleasePoolPop(ptr %pool) ret ptr null } -; Rewind to the start and play, applying loops / rate / volume. -define void @snd_play(ptr %p, i32 %loops, i32 %rate, i32 %vol) { +define internal ptr @snd_part(ptr %v, i64 %off) { 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 go: - %vf = sitofp i32 %vol to float - %v = fdiv float %vf, 6.5536e+04 - %sel_v = call ptr @sel_registerName(ptr @.a_svol) - %rv = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_v, float %v) - %rf = sitofp i32 %rate to float - %r = fdiv float %rf, 6.5536e+04 - %sel_r = call ptr @sel_registerName(ptr @.a_srate) - %rr = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_r, float %r) - %l64 = sext i32 %loops to i64 - %sel_l = call ptr @sel_registerName(ptr @.a_sloop) - %rl = call ptr (ptr, ptr, i64) @objc_msgSend(ptr %p, ptr %sel_l, i64 %l64) - %sel_ct = call ptr @sel_registerName(ptr @.a_sctime) - %rc = call ptr (ptr, ptr, double) @objc_msgSend(ptr %p, ptr %sel_ct, double 0.0) - %sel_p = call ptr @sel_registerName(ptr @.a_play) - %rp = call ptr (ptr, ptr) @objc_msgSend(ptr %p, ptr %sel_p) + ; play autoreleases an AVAudioTime: its own pool, so a sound played before a frame's pool keeps nothing + %pool = call ptr @objc_autoreleasePoolPush() + %e = load ptr, ptr @snd_engine + call void @snd_engine_run(ptr %e) + %node = call ptr @snd_part(ptr %v, i64 0) + %vs = call ptr @snd_part(ptr %v, i64 8) + %mix = call ptr @snd_part(ptr %v, i64 16) + %buf = call ptr @snd_part(ptr %v, i64 24) + %sst = call ptr @sel_registerName(ptr @.a_stop) + %r0 = call ptr (ptr, ptr) @objc_msgSend(ptr %node, ptr %sst) + %sv = call ptr @sel_registerName(ptr @.a_svol) + %vf = call float @snd_fx(i32 %vol) + %r1 = call ptr (ptr, ptr, float) @objc_msgSend(ptr %mix, ptr %sv, float %vf) + %sr = call ptr @sel_registerName(ptr @.a_srate) + %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 out: ret void } -define void @snd_stop(ptr %p) { +define void @snd_stop(ptr %v) { entry: - %nil = icmp eq ptr %p, null + %nil = icmp eq ptr %v, null br i1 %nil, label %out, label %go go: - %sel_s = call ptr @sel_registerName(ptr @.a_stop) - %rs = call ptr (ptr, ptr) @objc_msgSend(ptr %p, ptr %sel_s) - %sel_ct = call ptr @sel_registerName(ptr @.a_sctime) - %rc = call ptr (ptr, ptr, double) @objc_msgSend(ptr %p, ptr %sel_ct, double 0.0) + %node = call ptr @snd_part(ptr %v, i64 0) + %sst = call ptr @sel_registerName(ptr @.a_stop) + %r = call ptr (ptr, ptr) @objc_msgSend(ptr %node, ptr %sst) + %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 out: 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: - %nil = icmp eq ptr %p, null + %nil = icmp eq ptr %v, null br i1 %nil, label %zero, label %ask ask: - %sel_ip = call ptr @sel_registerName(ptr @.a_isply) - %b = call i8 (ptr, ptr) @objc_msgSend(ptr %p, ptr %sel_ip) - %bi = zext i8 %b to i32 - %r = and i32 %bi, 1 - ret i32 %r + %node = call ptr @snd_part(ptr %v, i64 0) + %sip = call ptr @sel_registerName(ptr @.a_isply) + %b = call i8 (ptr, ptr) @objc_msgSend(ptr %node, ptr %sip) + %on = icmp ne i8 %b, 0 + 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: ret i32 0 } -define void @snd_set_volume(ptr %p, i32 %vol) { +define void @snd_set_volume(ptr %v, i32 %vol) { entry: - %nil = icmp eq ptr %p, null + %nil = icmp eq ptr %v, null br i1 %nil, label %out, label %go go: - %vf = sitofp i32 %vol to float - %v = fdiv float %vf, 6.5536e+04 - %sel_v = call ptr @sel_registerName(ptr @.a_svol) - %rv = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_v, float %v) + %mix = call ptr @snd_part(ptr %v, i64 16) + %sv = call ptr @sel_registerName(ptr @.a_svol) + %f = call float @snd_fx(i32 %vol) + %r = call ptr (ptr, ptr, float) @objc_msgSend(ptr %mix, ptr %sv, float %f) br label %out out: ret void } -; Stereo position, -1.0 (hard left) .. 1.0 (hard right). AVAudioPlayer's own pan, so it -; costs nothing and needs no mixer: a one-shot can be placed before it is fired. -define void @snd_set_pan(ptr %p, i32 %pan) { +; Stereo position, -1.0 (hard left) .. 1.0 (hard right): the voice's mixer's pan into the main +; mixer, so a one-shot can be placed before it is fired. +define void @snd_set_pan(ptr %v, i32 %pan) { entry: - %nil = icmp eq ptr %p, null + %nil = icmp eq ptr %v, null br i1 %nil, label %out, label %go go: - %pf = sitofp i32 %pan to float - %v = fdiv float %pf, 6.5536e+04 - %sel_p = call ptr @sel_registerName(ptr @.a_span) - %rp = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_p, float %v) + %mix = call ptr @snd_part(ptr %v, i64 16) + %sp = call ptr @sel_registerName(ptr @.a_span) + %f = call float @snd_fx(i32 %pan) + %r = call ptr (ptr, ptr, float) @objc_msgSend(ptr %mix, ptr %sp, float %f) br label %out out: ret void } -define void @snd_set_rate(ptr %p, i32 %rate) { +define void @snd_set_rate(ptr %v, i32 %rate) { entry: - %nil = icmp eq ptr %p, null + %nil = icmp eq ptr %v, null br i1 %nil, label %out, label %go go: - %rf = sitofp i32 %rate to float - %r = fdiv float %rf, 6.5536e+04 - %sel_r = call ptr @sel_registerName(ptr @.a_srate) - %rr = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_r, float %r) + %vs = call ptr @snd_part(ptr %v, i64 8) + %sr = call ptr @sel_registerName(ptr @.a_srate) + %f = call float @snd_fx(i32 %rate) + %r = call ptr (ptr, ptr, float) @objc_msgSend(ptr %vs, ptr %sr, float %f) br label %out out: ret void diff --git a/runtime/native/audio.ludic b/runtime/native/audio.ludic index d894969c..f439ace3 100644 --- a/runtime/native/audio.ludic +++ b/runtime/native/audio.ludic @@ -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)