feat(audio): Audio.* standard library over a native AVAudioPlayer backend (#22)
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:
parent
d6ca320269
commit
4eef5ebbce
23 changed files with 22294 additions and 21004 deletions
|
|
@ -216,6 +216,21 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "touch_y") { bare = "input_touch_y"; push(labels, "index") }
|
||||
if (meth == "set_touch") { bare = "input_set_touch"; push(labels, "index"); push(labels, "x"); push(labels, "y"); push(labels, "active") }
|
||||
}
|
||||
# Audio.* (#22) — sfx/music playback over the platform audio backend
|
||||
# (runtime/native/audio.ludic + audio.ll). Playback is out-of-band; the
|
||||
# triggers are ordinary frame-driven calls, so a replay fires the same sounds.
|
||||
if (ns == "Audio") {
|
||||
if (meth == "load") { bare = "audio_load"; push(labels, "path") }
|
||||
if (meth == "play") { bare = "audio_play"; push(labels, "id") }
|
||||
if (meth == "play_sound") { bare = "audio_play"; push(labels, "id") }
|
||||
if (meth == "play_music") { bare = "audio_play_music"; push(labels, "id") }
|
||||
if (meth == "stop") { bare = "audio_stop"; push(labels, "id") }
|
||||
if (meth == "stop_music") { bare = "audio_stop_music" }
|
||||
if (meth == "stop_all") { bare = "audio_stop_all" }
|
||||
if (meth == "volume") { bare = "audio_volume"; push(labels, "v") }
|
||||
if (meth == "pitch") { bare = "audio_pitch"; push(labels, "v") }
|
||||
if (meth == "is_playing") { bare = "audio_is_playing"; push(labels, "id") }
|
||||
}
|
||||
# Phase 3: the bare reflection / networking / process builtins, namespaced.
|
||||
# Each is a pure alias — the callee is rewritten to the bare name below.
|
||||
if (ns == "World") {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,12 @@ function emit_header() -> void {
|
|||
emith("declare void @win_mouse(ptr)\n")
|
||||
emith("declare void @win_pad(ptr)\n")
|
||||
emith("declare void @win_touch(ptr)\n")
|
||||
emith("declare ptr @snd_load(ptr)\n") # #22 audio backend (audio.ll)
|
||||
emith("declare void @snd_play(ptr, i32, i32, i32)\n")
|
||||
emith("declare void @snd_stop(ptr)\n")
|
||||
emith("declare i32 @snd_playing(ptr)\n")
|
||||
emith("declare void @snd_set_volume(ptr, i32)\n")
|
||||
emith("declare void @snd_set_rate(ptr, i32)\n")
|
||||
emith("@__stderrp = external global ptr\n")
|
||||
emith("@__stdoutp = external global ptr\n")
|
||||
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ function is_intrinsic2(name: pointer) -> bool {
|
|||
if (name == "win_running") or (name == "win_close") { return true }
|
||||
if (name == "win_held") or (name == "win_mouse") { return true } # #50 device layer
|
||||
if (name == "win_pad") or (name == "win_touch") { return true } # #51 gamepad / touch
|
||||
if (name == "snd_load") or (name == "snd_play") or (name == "snd_stop") { return true } # #22 audio
|
||||
if (name == "snd_playing") or (name == "snd_set_volume") or (name == "snd_set_rate") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -60,5 +62,15 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val {
|
|||
# words/point [active, x, y]. Both windowed-only, DCE'd headless.
|
||||
if (name == "win_pad") { let a = arg_code(e, 0); emit(" call void @win_pad(ptr "); emit(a); emit(")\n"); return val("0", "void") }
|
||||
if (name == "win_touch") { let a = arg_code(e, 0); emit(" call void @win_touch(ptr "); emit(a); emit(")\n"); return val("0", "void") }
|
||||
# #22 audio backend — AVAudioPlayer via audio.ll; windowed-only, DCE'd headless.
|
||||
if (name == "snd_load") { let a = arg_code(e, 0); return val(emit_bind(`call ptr @snd_load(ptr {a})`), "pointer") }
|
||||
if (name == "snd_play") {
|
||||
let a = arg_code(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2); let d = arg_code(e, 3)
|
||||
emit(" call void @snd_play(ptr "); emit(a); emit(", i32 "); emit(b); emit(", i32 "); emit(c); emit(", i32 "); emit(d); emit(")\n"); return val("0", "void")
|
||||
}
|
||||
if (name == "snd_stop") { let a = arg_code(e, 0); emit(" call void @snd_stop(ptr "); emit(a); emit(")\n"); return val("0", "void") }
|
||||
if (name == "snd_playing") { let a = arg_code(e, 0); return val(emit_bind(`call i32 @snd_playing(ptr {a})`), "int") }
|
||||
if (name == "snd_set_volume") { let a = arg_code(e, 0); let b = arg_code(e, 1); emit(" call void @snd_set_volume(ptr "); emit(a); emit(", i32 "); emit(b); emit(")\n"); return val("0", "void") }
|
||||
if (name == "snd_set_rate") { let a = arg_code(e, 0); let b = arg_code(e, 1); emit(" call void @snd_set_rate(ptr "); emit(a); emit(", i32 "); emit(b); emit(")\n"); return val("0", "void") }
|
||||
return val("0", "void")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,6 +192,8 @@ function p_postfix() -> Node {
|
|||
# the engine auto-advance to do the ticking.
|
||||
if e.a.kind == E_ID and e.a.s == "Anim" and (e.s == "play" or e.s == "clip" or e.s == "on_frame" or e.s == "fired") { g_uses_anim_rt = true }
|
||||
if e.a.kind == E_ID and e.a.s == "Motion" and e.s == "to" { g_uses_anim_rt = true }
|
||||
# Audio.* (#22) — any Audio method splices the audio runtime.
|
||||
if e.a.kind == E_ID and e.a.s == "Audio" { g_uses_audio = true }
|
||||
# Tween.to/chain/delay/value/stop/parallel (#48): the fluent stateful handles
|
||||
# live in tween.ludic, advanced by an engine-owned system each Update tick.
|
||||
if e.a.kind == E_ID and e.a.s == "Tween" and (e.s == "to" or e.s == "chain" or e.s == "delay" or e.s == "value" or e.s == "stop" or e.s == "parallel") { g_uses_tween_rt = true }
|
||||
|
|
@ -426,6 +428,7 @@ var g_uses_esys: bool = false # an engine-owned system component (SpriteAnim
|
|||
var g_uses_input: bool = false # a program used Input.bind/down/poll/… (action maps + record/replay) -> splice input.ludic
|
||||
var g_uses_anim_rt: bool = false # Anim.play/clip/on_frame/fired or Motion.to (#48) -> splice systems.ludic + force the reflection ABI
|
||||
var g_uses_tween_rt: bool = false # Tween.to/chain/delay/… (#48) -> splice tween.ludic + run esys_tween each Update
|
||||
var g_uses_audio: bool = false # Audio.* (#22) -> splice audio.ludic; a windowed build also links audio.ll + AVFoundation
|
||||
|
||||
function already_loaded(full: pointer) -> bool {
|
||||
var i = 0
|
||||
|
|
@ -646,6 +649,14 @@ function maybe_splice_runtime() -> void {
|
|||
do_import("runtime/native/tween.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
# Audio.* (#22): splice the Audio runtime. It is self-contained — its native
|
||||
# calls (snd_*) are is_windowed()-guarded, so a headless build carries the API
|
||||
# as no-ops and needs no audio backend — so it works in a plain program too.
|
||||
if g_uses_audio {
|
||||
cur_dir = ""
|
||||
do_import("runtime/native/audio.ludic")
|
||||
cur_dir = saved
|
||||
}
|
||||
# Anim.play/Motion.to sugar (#48): the writes live in systems.ludic and use the
|
||||
# reflection ABI, so splice it and force the world table even when the game does
|
||||
# not otherwise trip uses_engine_systems.
|
||||
|
|
|
|||
42668
selfhost/ludicc.seed.ll
42668
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -165,6 +165,13 @@ entry {
|
|||
# dead-stripped; -needed_framework forces the load command so the class is
|
||||
# registered and #51's gamepad polling can see it.
|
||||
cmd = `{cmd} {cocoa} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-rpath,@loader_path`
|
||||
# Audio.* (#22) pulls in the AVAudioPlayer backend and AVFoundation, but only
|
||||
# when the program actually uses it — the snd_* calls are is_windowed()-guarded
|
||||
# yet still live in a windowed build, so the backend must link.
|
||||
if g_uses_audio {
|
||||
let audio = path_join(home, "runtime/native/audio.ll")
|
||||
cmd = `{cmd} {audio} -Wl,-needed_framework,AVFoundation`
|
||||
}
|
||||
}
|
||||
cmd = `{cmd} -o {out}`
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue