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
15
changes/audio-namespace.md
Normal file
15
changes/audio-namespace.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**Audio standard library (#22).** `Audio.*` — sound effects and music over a new
|
||||||
|
platform audio backend (`runtime/native/audio.ll`, AVAudioPlayer on macOS, spliced
|
||||||
|
and linked with AVFoundation only when a windowed build uses it). `Audio.load`
|
||||||
|
loads a sound file into a handle; `Audio.play` / `play_sound` fire it one-shot,
|
||||||
|
`play_music` loops it on a single music channel; `Audio.stop` / `stop_music` /
|
||||||
|
`stop_all` stop them; `Audio.volume` sets the master volume and `Audio.pitch` the
|
||||||
|
playback rate, both across every loaded sound; `Audio.is_playing` reports state.
|
||||||
|
Playback is out-of-band — the audio device is real-time, explicitly not part of
|
||||||
|
the deterministic simulation — but every trigger is an ordinary frame-driven
|
||||||
|
call, so a recorded/replayed run fires the same sounds at the same frames.
|
||||||
|
Headless builds carry the whole API as no-ops (the native `snd_*` calls are
|
||||||
|
`is_windowed()`-guarded and dead-stripped), so the same game code runs under the
|
||||||
|
test harness with no audio device.
|
||||||
7
docs/language/audio/_section.md
Normal file
7
docs/language/audio/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: audio
|
||||||
|
title: Audio
|
||||||
|
order: 7
|
||||||
|
---
|
||||||
|
|
||||||
|
Sound effects and music. A sound is loaded once with <a href="audio-load.html"><code>Audio.load</code></a> into a handle; <a href="audio-play.html"><code>Audio.play</code></a> fires it one-shot and <a href="audio-play_music.html"><code>Audio.play_music</code></a> loops it on a single music channel. Playback is <strong>out-of-band</strong> — the audio device is real-time, not part of the deterministic simulation — but every trigger here is an ordinary frame-driven call, so a recorded run replays the same sounds at the same frames. Headless builds carry the whole API as no-ops (no audio device needed), so the same game code runs under the test harness.
|
||||||
23
docs/language/audio/audio-is_playing.md
Normal file
23
docs/language/audio/audio-is_playing.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
id: audio-is_playing
|
||||||
|
name: Audio.is_playing
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.is_playing
|
||||||
|
sig: Audio.is_playing(id) -> bool
|
||||||
|
tip: Is this sound playing?
|
||||||
|
order: 9
|
||||||
|
ns: Audio
|
||||||
|
member: is_playing
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns whether the sound <code>id</code> is currently playing. Always <code>false</code> in a headless build.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let sfx = Audio.load("blip.wav")
|
||||||
|
if Audio.is_playing(sfx) { print(1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/language/audio/audio-load.md
Normal file
23
docs/language/audio/audio-load.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
id: audio-load
|
||||||
|
name: Audio.load
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.load
|
||||||
|
sig: Audio.load(path) -> int
|
||||||
|
tip: Load a sound file into a handle.
|
||||||
|
order: 0
|
||||||
|
ns: Audio
|
||||||
|
member: load
|
||||||
|
---
|
||||||
|
|
||||||
|
Loads a sound file and returns a handle (>= 1), or <code>0</code> on failure or in a headless build. Load each sound once (e.g. during the <code>Start</code> phase) and reuse the handle.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let sfx = Audio.load("blip.wav")
|
||||||
|
print(sfx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/audio/audio-pitch.md
Normal file
22
docs/language/audio/audio-pitch.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: audio-pitch
|
||||||
|
name: Audio.pitch
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.pitch
|
||||||
|
sig: Audio.pitch(v) -> void
|
||||||
|
tip: Set the playback rate / pitch.
|
||||||
|
order: 8
|
||||||
|
ns: Audio
|
||||||
|
member: pitch
|
||||||
|
---
|
||||||
|
|
||||||
|
Sets the playback rate / pitch (<code>1.0</code> normal, <code>0.5</code> an octave down, <code>2.0</code> up), applied to every loaded sound.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
Audio.pitch(1.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/language/audio/audio-play.md
Normal file
23
docs/language/audio/audio-play.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
id: audio-play
|
||||||
|
name: Audio.play
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.play
|
||||||
|
sig: Audio.play(id) -> void
|
||||||
|
tip: Fire a one-shot sound.
|
||||||
|
order: 1
|
||||||
|
ns: Audio
|
||||||
|
member: play
|
||||||
|
---
|
||||||
|
|
||||||
|
Plays the sound <code>id</code> from the start as a one-shot effect, at the current master volume and pitch.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let sfx = Audio.load("blip.wav")
|
||||||
|
Audio.play(sfx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/language/audio/audio-play_music.md
Normal file
23
docs/language/audio/audio-play_music.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
id: audio-play_music
|
||||||
|
name: Audio.play_music
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.play_music
|
||||||
|
sig: Audio.play_music(id) -> void
|
||||||
|
tip: Loop a sound as background music.
|
||||||
|
order: 3
|
||||||
|
ns: Audio
|
||||||
|
member: play_music
|
||||||
|
---
|
||||||
|
|
||||||
|
Plays the sound <code>id</code> as looping background music on the single music channel; any music already playing is stopped first.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let theme = Audio.load("theme.ogg")
|
||||||
|
Audio.play_music(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/language/audio/audio-play_sound.md
Normal file
23
docs/language/audio/audio-play_sound.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
id: audio-play_sound
|
||||||
|
name: Audio.play_sound
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.play_sound
|
||||||
|
sig: Audio.play_sound(id) -> void
|
||||||
|
tip: Fire a one-shot sound (alias of play).
|
||||||
|
order: 2
|
||||||
|
ns: Audio
|
||||||
|
member: play_sound
|
||||||
|
---
|
||||||
|
|
||||||
|
An alias of <a href="audio-play.html"><code>Audio.play</code></a> — plays the sound <code>id</code> once from the start.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let sfx = Audio.load("blip.wav")
|
||||||
|
Audio.play_sound(sfx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
24
docs/language/audio/audio-stop.md
Normal file
24
docs/language/audio/audio-stop.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: audio-stop
|
||||||
|
name: Audio.stop
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.stop
|
||||||
|
sig: Audio.stop(id) -> void
|
||||||
|
tip: Stop one sound.
|
||||||
|
order: 4
|
||||||
|
ns: Audio
|
||||||
|
member: stop
|
||||||
|
---
|
||||||
|
|
||||||
|
Stops the sound <code>id</code> and rewinds it to the start. If it was the current music, the music channel is cleared.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let sfx = Audio.load("blip.wav")
|
||||||
|
Audio.play(sfx)
|
||||||
|
Audio.stop(sfx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/audio/audio-stop_all.md
Normal file
22
docs/language/audio/audio-stop_all.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: audio-stop_all
|
||||||
|
name: Audio.stop_all
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.stop_all
|
||||||
|
sig: Audio.stop_all() -> void
|
||||||
|
tip: Stop every sound.
|
||||||
|
order: 6
|
||||||
|
ns: Audio
|
||||||
|
member: stop_all
|
||||||
|
---
|
||||||
|
|
||||||
|
Stops every loaded sound, including the music channel.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
Audio.stop_all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/audio/audio-stop_music.md
Normal file
22
docs/language/audio/audio-stop_music.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: audio-stop_music
|
||||||
|
name: Audio.stop_music
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.stop_music
|
||||||
|
sig: Audio.stop_music() -> void
|
||||||
|
tip: Stop the music channel.
|
||||||
|
order: 5
|
||||||
|
ns: Audio
|
||||||
|
member: stop_music
|
||||||
|
---
|
||||||
|
|
||||||
|
Stops whatever is playing on the music channel (started by <a href="audio-play_music.html"><code>Audio.play_music</code></a>).
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
Audio.stop_music()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
docs/language/audio/audio-volume.md
Normal file
22
docs/language/audio/audio-volume.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
id: audio-volume
|
||||||
|
name: Audio.volume
|
||||||
|
category: audio
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Audio.volume
|
||||||
|
sig: Audio.volume(v) -> void
|
||||||
|
tip: Set the master volume.
|
||||||
|
order: 7
|
||||||
|
ns: Audio
|
||||||
|
member: volume
|
||||||
|
---
|
||||||
|
|
||||||
|
Sets the master volume (<code>0.0</code> .. <code>1.0</code>), applied to every loaded sound now and to every future play.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
Audio.volume(0.8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
33
examples/library/audio.ludic
Normal file
33
examples/library/audio.ludic
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# audio.ludic — the Audio.* standard library (#22). Sound effects and music over
|
||||||
|
# the platform audio backend (AVAudioPlayer on macOS). A sound is loaded once
|
||||||
|
# into a handle; sfx fire one-shot, music loops on a single channel; master
|
||||||
|
# volume and pitch apply across everything.
|
||||||
|
#
|
||||||
|
# Playback is out-of-band — the audio device is real-time, not part of the
|
||||||
|
# deterministic simulation — but every trigger is an ordinary frame-driven call,
|
||||||
|
# so a recorded run replays the same sounds at the same frames. Headless (no
|
||||||
|
# window) every call is a safe no-op: load returns 0 and is_playing returns
|
||||||
|
# false, so the same code runs under the test harness. A full headless run:
|
||||||
|
# bin/ludic examples/library/audio.ludic -> 0 0 0 0 1
|
||||||
|
program AudioDemo {
|
||||||
|
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||||
|
|
||||||
|
entry {
|
||||||
|
let sfx = Audio.load("assets/sfx/blip.wav") # 0 headless (no audio device)
|
||||||
|
let mus = Audio.load("assets/music/theme.ogg") # 0 headless
|
||||||
|
print(sfx) # 0
|
||||||
|
print(mus) # 0
|
||||||
|
|
||||||
|
Audio.volume(0.8) # master volume, 0.0..1.0
|
||||||
|
Audio.pitch(1.25) # playback rate / pitch
|
||||||
|
Audio.play(sfx) # one-shot sound effect
|
||||||
|
Audio.play_music(mus) # looping background music
|
||||||
|
print(bi(Audio.is_playing(sfx))) # 0 headless
|
||||||
|
|
||||||
|
Audio.stop(sfx) # stop one sound
|
||||||
|
Audio.stop_music() # stop the music channel
|
||||||
|
Audio.stop_all() # stop everything
|
||||||
|
print(bi(Audio.is_playing(mus))) # 0
|
||||||
|
print(1) # 1 — every call returned cleanly
|
||||||
|
}
|
||||||
|
}
|
||||||
152
runtime/native/audio.ll
Normal file
152
runtime/native/audio.ll
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
; ============================================================================
|
||||||
|
; 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)
|
||||||
|
; 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"
|
||||||
|
|
||||||
|
; 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
140
runtime/native/audio.ludic
Normal file
140
runtime/native/audio.ludic
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
# ============================================================================
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# game makes from a frame handler, so a recorded/replayed run fires exactly the
|
||||||
|
# same sounds at exactly the same frames. Nothing in this file feeds the
|
||||||
|
# lockstep sim.
|
||||||
|
#
|
||||||
|
# Headless builds carry the whole API as no-ops: every native call is guarded by
|
||||||
|
# is_windowed(), which folds to a compile-time constant, so the snd_* intrinsics
|
||||||
|
# (and AVFoundation) are dead-stripped out of a headless binary. Audio.load then
|
||||||
|
# returns 0 and Audio.is_playing returns false, so the same game code runs in the
|
||||||
|
# test harness without an audio device.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
const AUDIO_CAP: int = 64 # max simultaneously-loaded sounds
|
||||||
|
|
||||||
|
var snd_ready: bool = false
|
||||||
|
var snd_tab: pointers = null # handle (1-based) -> AVAudioPlayer pointer
|
||||||
|
var snd_master: fixed = 1.0 # master volume, applied to every play
|
||||||
|
var snd_rate: fixed = 1.0 # playback rate / pitch (1.0 = normal)
|
||||||
|
var snd_music: int = 0 # the handle currently playing as music (0 = none)
|
||||||
|
|
||||||
|
function audio_init() -> void {
|
||||||
|
if snd_ready { return }
|
||||||
|
snd_tab = bytes(AUDIO_CAP * 8) # one 8-byte pointer slot per handle
|
||||||
|
fill(snd_tab, 0, AUDIO_CAP * 8) # malloc does not zero; empty slots must read null
|
||||||
|
snd_ready = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve a 1-based handle to its player pointer (null if out of range / empty).
|
||||||
|
function audio_get(id: int) -> pointer {
|
||||||
|
audio_init()
|
||||||
|
if (id < 1) or (id > AUDIO_CAP) { return null }
|
||||||
|
return snd_tab[id - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load a sound file and return its handle (>= 1), or 0 on failure / headless.
|
||||||
|
function audio_load(path: pointer) -> int {
|
||||||
|
audio_init()
|
||||||
|
if not is_windowed() { return 0 }
|
||||||
|
var i = 0
|
||||||
|
while i < AUDIO_CAP {
|
||||||
|
if snd_tab[i] == null {
|
||||||
|
let p = snd_load(path)
|
||||||
|
if p == null { return 0 }
|
||||||
|
snd_tab[i] = p
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fire a one-shot sound from the start.
|
||||||
|
function audio_play(id: int) -> void {
|
||||||
|
if not is_windowed() { return }
|
||||||
|
let p = audio_get(id)
|
||||||
|
if p == null { return }
|
||||||
|
snd_play(p, 0, snd_rate, snd_master)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Play a sound as looping background music on the single music channel; any
|
||||||
|
# previous music is stopped first.
|
||||||
|
function audio_play_music(id: int) -> void {
|
||||||
|
if not is_windowed() { return }
|
||||||
|
let p = audio_get(id)
|
||||||
|
if p == null { return }
|
||||||
|
audio_stop_music()
|
||||||
|
snd_music = id
|
||||||
|
snd_play(p, -1, snd_rate, snd_master)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop one sound.
|
||||||
|
function audio_stop(id: int) -> void {
|
||||||
|
if not is_windowed() { return }
|
||||||
|
let p = audio_get(id)
|
||||||
|
if p == null { return }
|
||||||
|
snd_stop(p)
|
||||||
|
if id == snd_music { snd_music = 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop the current music channel.
|
||||||
|
function audio_stop_music() -> void {
|
||||||
|
if not is_windowed() { return }
|
||||||
|
if snd_music != 0 {
|
||||||
|
let p = audio_get(snd_music)
|
||||||
|
if p != null { snd_stop(p) }
|
||||||
|
snd_music = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop every loaded sound.
|
||||||
|
function audio_stop_all() -> void {
|
||||||
|
if not is_windowed() { return }
|
||||||
|
audio_init()
|
||||||
|
var i = 0
|
||||||
|
while i < AUDIO_CAP {
|
||||||
|
if snd_tab[i] != null { snd_stop(snd_tab[i]) }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
snd_music = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Master volume (0.0 .. 1.0) — applied to every currently-loaded sound now and
|
||||||
|
# to every future play.
|
||||||
|
function audio_volume(v: fixed) -> void {
|
||||||
|
snd_master = v
|
||||||
|
if not is_windowed() { return }
|
||||||
|
audio_init()
|
||||||
|
var i = 0
|
||||||
|
while i < AUDIO_CAP {
|
||||||
|
if snd_tab[i] != null { snd_set_volume(snd_tab[i], v) }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Playback rate / pitch (1.0 = normal, 0.5 = an octave down, 2.0 = up).
|
||||||
|
function audio_pitch(v: fixed) -> void {
|
||||||
|
snd_rate = v
|
||||||
|
if not is_windowed() { return }
|
||||||
|
audio_init()
|
||||||
|
var i = 0
|
||||||
|
while i < AUDIO_CAP {
|
||||||
|
if snd_tab[i] != null { snd_set_rate(snd_tab[i], v) }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Is this sound currently playing?
|
||||||
|
function audio_is_playing(id: int) -> bool {
|
||||||
|
if not is_windowed() { return false }
|
||||||
|
let p = audio_get(id)
|
||||||
|
if p == null { return false }
|
||||||
|
return snd_playing(p) != 0
|
||||||
|
}
|
||||||
|
|
@ -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 == "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") }
|
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.
|
# Phase 3: the bare reflection / networking / process builtins, namespaced.
|
||||||
# Each is a pure alias — the callee is rewritten to the bare name below.
|
# Each is a pure alias — the callee is rewritten to the bare name below.
|
||||||
if (ns == "World") {
|
if (ns == "World") {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,12 @@ function emit_header() -> void {
|
||||||
emith("declare void @win_mouse(ptr)\n")
|
emith("declare void @win_mouse(ptr)\n")
|
||||||
emith("declare void @win_pad(ptr)\n")
|
emith("declare void @win_pad(ptr)\n")
|
||||||
emith("declare void @win_touch(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("@__stderrp = external global ptr\n")
|
||||||
emith("@__stdoutp = 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")
|
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_running") or (name == "win_close") { return true }
|
||||||
if (name == "win_held") or (name == "win_mouse") { return true } # #50 device layer
|
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 == "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
|
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.
|
# 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_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") }
|
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")
|
return val("0", "void")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,8 @@ function p_postfix() -> Node {
|
||||||
# the engine auto-advance to do the ticking.
|
# 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 == "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 }
|
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
|
# 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.
|
# 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 }
|
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_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_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_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 {
|
function already_loaded(full: pointer) -> bool {
|
||||||
var i = 0
|
var i = 0
|
||||||
|
|
@ -646,6 +649,14 @@ function maybe_splice_runtime() -> void {
|
||||||
do_import("runtime/native/tween.ludic")
|
do_import("runtime/native/tween.ludic")
|
||||||
cur_dir = saved
|
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
|
# 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
|
# reflection ABI, so splice it and force the world table even when the game does
|
||||||
# not otherwise trip uses_engine_systems.
|
# 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
|
# dead-stripped; -needed_framework forces the load command so the class is
|
||||||
# registered and #51's gamepad polling can see it.
|
# registered and #51's gamepad polling can see it.
|
||||||
cmd = `{cmd} {cocoa} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-rpath,@loader_path`
|
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}`
|
cmd = `{cmd} -o {out}`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,9 @@ function cmd_app() -> int {
|
||||||
let ll = `{outbin}.ll`
|
let ll = `{outbin}.ll`
|
||||||
print(`ludicc: {src} -> {outbin} (Ludic-written compiler -> LLVM IR -> windowed binary)`)
|
print(`ludicc: {src} -> {outbin} (Ludic-written compiler -> LLVM IR -> windowed binary)`)
|
||||||
if not shq(`bin/ludicc --windowed {src} > {ll}`) { return 1 }
|
if not shq(`bin/ludicc --windowed {src} > {ll}`) { return 1 }
|
||||||
if not shq(`{cc()} -O2 {ll} runtime/native/cocoa.ll -framework Cocoa -Wl,-needed_framework,GameController -Wl,-rpath,@loader_path -o {outbin}`) { return 1 }
|
# audio.ll (#22) is always linked here for the dev runner — unused snd_* are
|
||||||
|
# dead-stripped; the canonical `ludicc -o` path links it only when Audio.* is used.
|
||||||
|
if not shq(`{cc()} -O2 {ll} runtime/native/cocoa.ll runtime/native/audio.ll -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path -o {outbin}`) { return 1 }
|
||||||
if not save { run(`rm -f {ll}`) }
|
if not save { run(`rm -f {ll}`) }
|
||||||
print(`done. run: ./{outbin} (from the repo root, so assets/ resolves)`)
|
print(`done. run: ./{outbin} (from the repo root, so assets/ resolves)`)
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,7 @@ function cmd_test() -> int {
|
||||||
# deterministic input record/replay — fed one key per poll from stdin.
|
# deterministic input record/replay — fed one key per poll from stdin.
|
||||||
feat_case("library/input_actions", " xwa", "1 0 1 1 0 1 0", "input_actions.ludic (#7 action maps + rebinding + deterministic replay)")
|
feat_case("library/input_actions", " xwa", "1 0 1 1 0 1 0", "input_actions.ludic (#7 action maps + rebinding + deterministic replay)")
|
||||||
feat_case("library/input_device", "", "1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1", "input_device.ludic (#50 multi-key held state + analog axis/vector + mouse + gamepad/touch + full-state replay)")
|
feat_case("library/input_device", "", "1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1", "input_device.ludic (#50 multi-key held state + analog axis/vector + mouse + gamepad/touch + full-state replay)")
|
||||||
|
feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)")
|
||||||
|
|
||||||
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
|
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
|
||||||
net_case("events/scoped", "2")
|
net_case("events/scoped", "2")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue