wip(0.S2): migrate - a var nothing writes becomes a let; a state is keyed by its module, directory or program, so every program's plan agrees; paths normalized; program states named SceneDemoState / scene_demo_st; the LSP reads state, mut and entry/handler parameters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 15:31:34 +03:00
parent 3eda72e8c2
commit 5ffe50ed02
463 changed files with 72045 additions and 67421 deletions

View file

@ -1,37 +1,33 @@
# emit.ludic - the only calls into the runtime's playback. aud_emit is the ONE place a float
# crosses into Audio.play_at's Q16.16: handed over unconverted, 0.94 as a pitch clamped to 4x and
# -0.85 as a pan to hard left, and every sound in a valley played fast out of one speaker
export state AudioState {
last_gain: float = 0.0
last_pitch: float = 0.0
last_pan: float = 0.0
emitted: int = 0
tried: int = 0
live: int = 0
}
var last_gain: float = 0.0
var last_pitch: float = 0.0
var last_pan: float = 0.0
var emitted: int = 0
export function aud_emit(audio_st: mut AudioState, h: int, gain: float, pitch: float, pan: float) -> void {
export function aud_emit(h: int, gain: float, pitch: float, pan: float) -> void {
if h == 0 { return }
audio_st.last_gain = gain
audio_st.last_pitch = pitch
audio_st.last_pan = pan
audio_st.emitted += 1
last_gain = gain
last_pitch = pitch
last_pan = pan
emitted += 1
Audio.play_at(h, fixed(gain), fixed(pitch), fixed(pan))
}
# a sound at a place in the world, heard within `range` metres: false (and nothing played) when the
# listener is out of earshot
export function aud_emit_at(audio_st: mut AudioState, h: int, x: float, z: float, range: float) -> bool {
export function aud_emit_at(h: int, x: float, z: float, range: float) -> bool {
let g = aud_gain_at(x, z, range)
if g == 0.0 { return false }
aud_emit(audio_st, h, g, aud_pitch_at(g), aud_pan_at(x, z))
aud_emit(h, g, aud_pitch_at(g), aud_pan_at(x, z))
return true
}
# a click, a chime, a shutter, a warning: in the player's head, flat
export function aud_ui(audio_st: mut AudioState, h: int) -> void {
export function aud_ui(h: int) -> void {
if h == 0 { return }
audio_st.emitted += 1
emitted += 1
Audio.play(h)
}
@ -41,7 +37,7 @@ export function aud_master(v: float) -> void { Audio.volume(fixed(Math.clamp(v,
# what the last emit handed the runtime, and how many shots so far: Audio.* is a no-op headless, so
# the numbers are the only thing a test can hear
export function aud_last_gain(audio_st: AudioState) -> float { return audio_st.last_gain }
export function aud_last_pitch(audio_st: AudioState) -> float { return audio_st.last_pitch }
export function aud_last_pan(audio_st: AudioState) -> float { return audio_st.last_pan }
export function aud_emitted(audio_st: AudioState) -> int { return audio_st.emitted }
export function aud_last_gain() -> float { return last_gain }
export function aud_last_pitch() -> float { return last_pitch }
export function aud_last_pan() -> float { return last_pan }
export function aud_emitted() -> int { return emitted }

View file

@ -2,23 +2,25 @@
# bottoms out in AVAudioPlayer), 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.
var tried: int = 0
var live: int = 0
export function aud_load(audio_st: mut AudioState, path: string) -> int {
audio_st.tried += 1
export function aud_load(path: string) -> int {
tried += 1
if not is_windowed() { return 0 }
var h = Audio.load(path)
if h == 0 {
let out = aud_unpack(path, base_name(path))
if out != null { h = Audio.load(out) }
}
if h != 0 { audio_st.live += 1 }
if h != 0 { live += 1 }
return h
}
# how many clips were asked for and how many loaded: a silent game looks exactly like a working one
# unless something says so
export function aud_tried(audio_st: AudioState) -> int { return audio_st.tried }
export function aud_live(audio_st: AudioState) -> int { return audio_st.live }
export function aud_tried() -> int { return tried }
export function aud_live() -> int { return live }
# One packed file out to the cache, or null. A cached copy is trusted only if it is as long as the
# packed one: a write interrupted halfway would otherwise play truncated for ever.

View file

@ -4,37 +4,35 @@
import "ludic.audio"
program AudioTest {
numbers float
state AudiotestState {
lx: float = 0.0
lz: float = 0.0
lyaw: float = 0.0
}
function t_x(audiotest_st: AudiotestState) -> float { return audiotest_st.lx }
function t_z(audiotest_st: AudiotestState) -> float { return audiotest_st.lz }
function t_yaw(audiotest_st: AudiotestState) -> float { return audiotest_st.lyaw }
var lx: float = 0.0
var lz: float = 0.0
var lyaw: float = 0.0
function t_x() -> float { return lx }
function t_z() -> float { return lz }
function t_yaw() -> float { return lyaw }
function dir() -> string { return Os.temp_dir() + "/ludic-audio-test" }
function cache() -> string { return dir() + "/cache" }
bind AudioListener { x: fn t_x, z: fn t_z, yaw: fn t_yaw }
bind AudioFiles { cache_dir: fn cache }
test "the gain falls off with distance and is gone past the range" (audiotest_st: mut AudiotestState) {
test "the gain falls off with distance and is gone past the range" {
let near = aud_gain_at(0.0, -10.0, 200.0)
let mid = aud_gain_at(0.0, -100.0, 200.0)
let far = aud_gain_at(0.0, -150.0, 200.0)
expect(near > mid and mid > far and far > 0.0)
expect_eq(aud_gain_at(0.0, -400.0, 200.0), 0.0)
expect_near(aud_gain_at(0.0, 0.0, 50.0), 1.0, 0.0001)
audiotest_st.lx = 1000.0
lx = 1000.0
expect_near(aud_gain_at(1000.0, 0.0, 50.0), 1.0, 0.0001)
}
test "the pan says which side, ahead and behind are centred, and it turns with the listener" (audiotest_st: mut AudiotestState) {
test "the pan says which side, ahead and behind are centred, and it turns with the listener" {
expect(aud_pan_at(-60.0, 0.0) < -0.5)
expect(aud_pan_at(60.0, 0.0) > 0.5)
expect_near(aud_pan_at(0.0, -60.0), 0.0, 0.01)
expect_near(aud_pan_at(0.0, 60.0), 0.0, 0.01)
expect_near(aud_pan_at(0.5, 0.0), 0.0, 0.0001)
audiotest_st.lyaw = 3.14159265
lyaw = 3.14159265
expect(aud_pan_at(-60.0, 0.0) > 0.5)
}
@ -43,18 +41,18 @@ program AudioTest {
expect(aud_pitch_at(0.0) > 0.9 and aud_pitch_at(1.0) < 1.1)
}
test "emit hands the runtime what it was told, and a sound out of earshot is not played" (audio_st: mut AudioState) {
aud_emit(audio_st, 7, 0.5, 0.97, -0.25)
expect_near(aud_last_gain(audio_st), 0.5, 0.0001)
expect_near(aud_last_pan(audio_st), -0.25, 0.0001)
expect_eq(aud_emitted(audio_st), 1)
aud_emit(audio_st, 0, 1.0, 1.0, 0.0)
expect_eq(aud_emitted(audio_st), 1)
expect(not aud_emit_at(audio_st, 7, 0.0, -500.0, 100.0))
expect(aud_emit_at(audio_st, 7, 30.0, 0.0, 100.0))
expect(aud_last_pan(audio_st) > 0.5)
expect(aud_last_gain(audio_st) > 0.4 and aud_last_gain(audio_st) < 0.5)
expect(aud_last_pitch(audio_st) < 1.0)
test "emit hands the runtime what it was told, and a sound out of earshot is not played" {
aud_emit(7, 0.5, 0.97, -0.25)
expect_near(aud_last_gain(), 0.5, 0.0001)
expect_near(aud_last_pan(), -0.25, 0.0001)
expect_eq(aud_emitted(), 1)
aud_emit(0, 1.0, 1.0, 0.0)
expect_eq(aud_emitted(), 1)
expect(not aud_emit_at(7, 0.0, -500.0, 100.0))
expect(aud_emit_at(7, 30.0, 0.0, 100.0))
expect(aud_last_pan() > 0.5)
expect(aud_last_gain() > 0.4 and aud_last_gain() < 0.5)
expect(aud_last_pitch() < 1.0)
}
function wav(path: string, rate: int, frames: int) -> void {
@ -94,9 +92,9 @@ program AudioTest {
expect(aud_unpack(dir() + "/nothing.wav", "nothing.wav") == null)
}
test "nothing loads headless, and the count says so" (audio_st: mut AudioState) {
expect_eq(aud_load(audio_st, dir() + "/click.wav"), 0)
expect_eq(aud_tried(audio_st), 1)
expect_eq(aud_live(audio_st), 0)
test "nothing loads headless, and the count says so" {
expect_eq(aud_load(dir() + "/click.wav"), 0)
expect_eq(aud_tried(), 1)
expect_eq(aud_live(), 0)
}
}