Three things a game could not say, each of which had been worked around. Audio.play_at(id, gain:, pitch:, pan:) fires a one-shot with its own gain, pitch and stereo position. Audio.volume and Audio.pitch are global - they are the options screen - so a game placing a sound in the world was fighting them, and distance attenuation was simply not expressible. The backend already took volume and rate per call; this adds setPan: alongside them and stops routing through the master state. ludic.render3d gains outline_model(model, mat, width, r, g, b): a rim around something that is not an Actor. The outline pass walked the actor list and stopped, so instanced scatter - a forest - could not be highlighted at all. It is a queue flushed by the same pass, which is what gets the depth test right when the caller does not control pass order. And terrain_coast(cx, cz, margin, fall), the other way to make an island: the sea around the survey's own edge rather than cut out of the middle of it. terrain_island measures a radius from a centre, which drowns two thirds of a real survey to make an island of the rest; this measures inward from the boundary, so everything the data covers stays land and the coast is where the data runs out. Both modes gained a strand - the last few metres of height either side of the water line compressed, which stretches a cliff plunge out into beach and shallows. 132 regression tests and the self-host fixpoints pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
169 lines
6.7 KiB
LLVM
169 lines
6.7 KiB
LLVM
; ============================================================================
|
|
; audio.ll — the macOS audio backend, written in LLVM IR.
|
|
;
|
|
; Like cocoa.ll, this compiles no Objective-C and no C: it drives AVAudioPlayer
|
|
; 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.
|
|
;
|
|
; 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).
|
|
; ============================================================================
|
|
|
|
declare ptr @objc_getClass(ptr)
|
|
declare ptr @sel_registerName(ptr)
|
|
declare ptr @objc_msgSend(ptr, ptr, ...)
|
|
|
|
@.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"
|
|
@.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_isply = private unnamed_addr constant [10 x i8] c"isPlaying\00"
|
|
@.a_span = private unnamed_addr constant [8 x i8] c"setPan:\00"
|
|
|
|
; Load a sound file into an AVAudioPlayer; returns the player (or null).
|
|
define ptr @snd_load(ptr %path) {
|
|
entry:
|
|
%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
|
|
fail:
|
|
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) {
|
|
entry:
|
|
%nil = icmp eq ptr %p, 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)
|
|
br label %out
|
|
out:
|
|
ret void
|
|
}
|
|
|
|
define void @snd_stop(ptr %p) {
|
|
entry:
|
|
%nil = icmp eq ptr %p, 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)
|
|
br label %out
|
|
out:
|
|
ret void
|
|
}
|
|
|
|
define i32 @snd_playing(ptr %p) {
|
|
entry:
|
|
%nil = icmp eq ptr %p, 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
|
|
zero:
|
|
ret i32 0
|
|
}
|
|
|
|
define void @snd_set_volume(ptr %p, i32 %vol) {
|
|
entry:
|
|
%nil = icmp eq ptr %p, 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)
|
|
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) {
|
|
entry:
|
|
%nil = icmp eq ptr %p, 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)
|
|
br label %out
|
|
out:
|
|
ret void
|
|
}
|
|
|
|
define void @snd_set_rate(ptr %p, i32 %rate) {
|
|
entry:
|
|
%nil = icmp eq ptr %p, 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)
|
|
br label %out
|
|
out:
|
|
ret void
|
|
}
|