mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Review fixes: - The audio host takes each physics step exactly once (one-step pulses latched, nothing while paused or silenced, at most 4 steps per frame), so pushes and other one-frame events are never dropped or doubled. e2e at 60 fps and above is byte-identical; 30 / 45 fps change only by the latch and the step pair. - Removed the emitter PROFILES fallback, the stray trace dump and the audition script; data-gated tests are #[ignore = "needs the private install data"] and fail loudly when run without it; per-post log lines behind SKATE_AUDIO_TRACE; checked bank parsing, finite output, camera-cut Doppler reset, fewer render allocations. - The 15 verdict-settled A/B switches are removed; their "on" behaviour is the only one (byte-identical). World / NPC audio, hookable by engine systems and mods: - Map-change fix: world and NPC hosts reset when the map's banks are unloaded, so surviving objects sound again. - Engine surface (skate-game::world_audio): TrafficAudio / PedAudio / NpcSkaterAudio components, horn / alarm / speech messages, read-back of who is audible; retail limits (4 cars within 40 m, 15 peds within 50 m with footsteps for the nearest 3, one NPC skater within 30 m), measured in the recompilation. - Mod surface (Lua API 2, `sdk.world_audio.*`); test mod `mods/world-audio-test/` (cars, peds, a ghost skater). - Per-skater audio state builder (local player output identical); remote players take the NPC skater slot; an opt-in non-retail "more audible" setting. - Docs: doc 15 "Hooking up world audio", doc 11 sections, PR plan.
249 lines
11 KiB
Rust
249 lines
11 KiB
Rust
//! Our voice-graph modules against the PoC's replay-verified kernels (oracle vectors written by the
|
|
//! local PoC probe `.claude/skills/aems-port/tools/poc/dsp_vectors.rs` into
|
|
//! `.local/audio-re/golden/dsp/vectors.txt`; skipped when absent). Prints the agreement numbers.
|
|
use std::path::PathBuf;
|
|
|
|
use skate_audio::dsp::biquad::{Coefficients, Kind, coefficients, kernel, omega};
|
|
use skate_audio::dsp::gain::ramp;
|
|
use skate_audio::dsp::resample::Resampler;
|
|
|
|
fn vectors() -> Option<String> {
|
|
let path = std::env::var_os("SKATE_DSP_VECTORS")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.local/audio-re/golden/dsp/vectors.txt"));
|
|
std::fs::read_to_string(path).ok()
|
|
}
|
|
|
|
/// The probe's test signal (same formula, same f32 libm).
|
|
fn signal(n: usize, seed: u32) -> Vec<f32> {
|
|
let mut x = seed;
|
|
(0..n)
|
|
.map(|i| {
|
|
x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
|
|
let noise = (x >> 8) as f32 / (1u32 << 24) as f32 - 0.5;
|
|
(i as f32 * 0.130_899_7).sin() * 0.6 + noise * 0.3
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn floats(words: &[&str]) -> Vec<f32> {
|
|
words.iter().map(|w| f32::from_bits(u32::from_str_radix(w, 16).unwrap())).collect()
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs the private install data"]
|
|
fn resample_is_bit_exact_with_the_retail_kernel() {
|
|
let Some(text) = vectors() else {
|
|
panic!("missing private data: no oracle vectors");
|
|
};
|
|
let src = signal(16384, 7);
|
|
let (mut cases, mut samples, mut exact) = (0, 0, 0);
|
|
for line in text.lines().filter(|l| l.starts_with("resample ")) {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
let step: u32 = parts[1].parse().unwrap();
|
|
let want = floats(&parts[2..]);
|
|
let mut r = Resampler::default();
|
|
r.step = step;
|
|
let mut got = Vec::new();
|
|
for _ in 0..8 {
|
|
let mut out = vec![0.0f32; 256];
|
|
r.render(&mut out, |i| src.get(i as usize).copied().unwrap_or(0.0));
|
|
r.advance(256);
|
|
got.extend(out);
|
|
}
|
|
cases += 1;
|
|
samples += want.len();
|
|
exact += got.iter().zip(&want).filter(|(a, b)| a.to_bits() == b.to_bits()).count();
|
|
}
|
|
eprintln!("resample: {cases} steps, {exact}/{samples} samples bit-exact");
|
|
assert!(cases > 0);
|
|
assert_eq!(exact, samples);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs the private install data"]
|
|
fn lowpass_coefficients_and_kernel_agree() {
|
|
let Some(text) = vectors() else {
|
|
panic!("missing private data: no oracle vectors");
|
|
};
|
|
let mut poc_coeffs = std::collections::HashMap::new();
|
|
let (mut words, mut exact_words, mut max_ulps) = (0, 0, 0u32);
|
|
for line in text.lines().filter(|l| l.starts_with("lpfcoef ")) {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
let fc: f32 = parts[1].parse().unwrap();
|
|
let c = floats(&parts[2..]);
|
|
let want = Coefficients { a1: c[0], a2: c[1], b0: c[2], b1: c[3], b2: c[4] };
|
|
let got = coefficients(Kind::LowPass, omega(fc, 48000.0));
|
|
for (a, b) in [(got.a1, want.a1), (got.a2, want.a2), (got.b0, want.b0), (got.b1, want.b1), (got.b2, want.b2)] {
|
|
words += 1;
|
|
let ulps = (a.to_bits() as i64 - b.to_bits() as i64).unsigned_abs() as u32;
|
|
exact_words += usize::from(ulps == 0);
|
|
max_ulps = max_ulps.max(ulps);
|
|
}
|
|
poc_coeffs.insert(parts[1].to_string(), want);
|
|
}
|
|
eprintln!("lpf coefficients: {exact_words}/{words} words bit-exact, max {max_ulps} ulp");
|
|
assert!(words > 0);
|
|
assert!(max_ulps <= 1, "coefficients differ by {max_ulps} ulp");
|
|
|
|
let input = signal(2048, 11);
|
|
let (mut samples, mut exact, mut worst) = (0, 0, 0.0f32);
|
|
for line in text.lines().filter(|l| l.starts_with("biquad ")) {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
let want = floats(&parts[2..]);
|
|
let k = poc_coeffs[parts[1]];
|
|
let mut history = [0.0f32; 4];
|
|
let mut got = input.clone();
|
|
for block in got.chunks_mut(256) {
|
|
kernel(&k, &mut history, block);
|
|
}
|
|
let peak = want.iter().fold(0.0f32, |m, v| m.max(v.abs())).max(1e-9);
|
|
let (mut case_exact, mut case_worst, mut first) = (0, 0.0f32, None);
|
|
for (i, (a, b)) in got.iter().zip(&want).enumerate() {
|
|
samples += 1;
|
|
case_exact += usize::from(a.to_bits() == b.to_bits());
|
|
if a.to_bits() != b.to_bits() && first.is_none() {
|
|
first = Some(i);
|
|
}
|
|
case_worst = case_worst.max((a - b).abs() / peak);
|
|
}
|
|
eprintln!(" biquad {} Hz: {case_exact}/{} bit-exact, first diff at {first:?}, max {case_worst:.3e} of peak", parts[1], want.len());
|
|
exact += case_exact;
|
|
worst = worst.max(case_worst);
|
|
}
|
|
eprintln!("biquad kernel: {exact}/{samples} samples bit-exact, max error {worst:.3e} of peak");
|
|
assert_eq!(exact, samples, "biquad not bit-exact (max error {worst} of peak)");
|
|
}
|
|
|
|
/// Fit the feed-forward association per sample position (n mod 8) against the oracle, black box:
|
|
/// each sample is computed from the oracle's own previous outputs (teacher forcing), and every
|
|
/// candidate order of the fused multiply-adds is scored. `cargo test … fit_biquad -- --ignored
|
|
/// --nocapture`. Its result is what `biquad::kernel` implements.
|
|
#[test]
|
|
#[ignore]
|
|
fn fit_biquad_association() {
|
|
let Some(text) = vectors() else { return };
|
|
let input = signal(2048, 11);
|
|
let mut coeffs = std::collections::HashMap::new();
|
|
for line in text.lines().filter(|l| l.starts_with("lpfcoef ")) {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
let c = floats(&parts[2..]);
|
|
coeffs.insert(parts[1].to_string(), Coefficients { a1: c[0], a2: c[1], b0: c[2], b1: c[3], b2: c[4] });
|
|
}
|
|
// Feed-forward candidates: (order of the three products, bias placement).
|
|
// bias 0: innermost addend; 1: plain add after the sum; 2: plain mul innermost, bias after.
|
|
let perms: [[usize; 3]; 6] = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]];
|
|
let ff = |k: &Coefficients, x: [f32; 3], perm: [usize; 3], bias: u8| -> f32 {
|
|
let b = [k.b0, k.b1, k.b2];
|
|
let (o, m, i) = (perm[0], perm[1], perm[2]);
|
|
match bias {
|
|
0 => b[o].mul_add(x[o], b[m].mul_add(x[m], b[i].mul_add(x[i], 1e-18))),
|
|
1 => b[o].mul_add(x[o], b[m].mul_add(x[m], b[i] * x[i])) + 1e-18,
|
|
_ => b[o].mul_add(x[o], b[m].mul_add(x[m], b[i] * x[i] + 1e-18)),
|
|
}
|
|
};
|
|
let fb = |k: &Coefficients, t: f32, y1: f32, y2: f32, order: u8| -> f32 {
|
|
match order {
|
|
0 => (-k.a2).mul_add(y2, (-k.a1).mul_add(y1, t)),
|
|
1 => (-k.a1).mul_add(y1, (-k.a2).mul_add(y2, t)),
|
|
_ => t - k.a1.mul_add(y1, k.a2 * y2),
|
|
}
|
|
};
|
|
let mut score = vec![vec![0usize; 6 * 3 * 3]; 8];
|
|
let mut total = [0usize; 8];
|
|
for line in text.lines().filter(|l| l.starts_with("biquad ")) {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
let want = floats(&parts[2..]);
|
|
let k = coeffs[parts[1]];
|
|
for n in 2..want.len() {
|
|
let x = [input[n], input[n - 1], input[n - 2]];
|
|
total[n % 8] += 1;
|
|
for (pi, perm) in perms.iter().enumerate() {
|
|
for bias in 0..3u8 {
|
|
for order in 0..3u8 {
|
|
let y = fb(&k, ff(&k, x, *perm, bias), want[n - 1], want[n - 2], order);
|
|
if y.to_bits() == want[n].to_bits() {
|
|
score[n % 8][(pi * 3 + bias as usize) * 3 + order as usize] += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for pos in 0..8 {
|
|
let best = (0..score[pos].len()).max_by_key(|&c| score[pos][c]).unwrap();
|
|
let (pi, rest) = (best / 9, best % 9);
|
|
eprintln!(
|
|
"position {pos}: products {:?} bias {} feedback {} → {}/{} exact",
|
|
perms[pi].map(|i| ["b0·x", "b1·x1", "b2·x2"][i]),
|
|
rest / 3,
|
|
rest % 3,
|
|
score[pos][best],
|
|
total[pos]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs the private install data"]
|
|
fn gain_ramp_is_bit_exact_with_the_retail_kernel() {
|
|
let Some(text) = vectors() else {
|
|
panic!("missing private data: no oracle vectors");
|
|
};
|
|
let input = signal(256, 23);
|
|
let (mut samples, mut exact, mut max_ulps) = (0, 0, 0u64);
|
|
for line in text.lines().filter(|l| l.starts_with("gainramp ")) {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
let (g, s) = parts[1].split_once(':').unwrap();
|
|
let start = f32::from_bits(u32::from_str_radix(g, 16).unwrap());
|
|
let step = f32::from_bits(u32::from_str_radix(s, 16).unwrap());
|
|
let want = floats(&parts[2..]);
|
|
let mut got = input.clone();
|
|
ramp(&mut got, start, step);
|
|
samples += want.len();
|
|
exact += got.iter().zip(&want).filter(|(a, b)| a.to_bits() == b.to_bits()).count();
|
|
for (a, b) in got.iter().zip(&want) {
|
|
max_ulps = max_ulps.max((a.to_bits() as i64 - b.to_bits() as i64).unsigned_abs());
|
|
}
|
|
}
|
|
eprintln!("gain ramp: {exact}/{samples} samples bit-exact, max {max_ulps} ulp");
|
|
assert!(samples > 0);
|
|
// The lane arithmetic of irregular steps is not fully recovered (fit_gain_ramp): ≤ 1 ulp.
|
|
assert!(max_ulps <= 1, "gain ramp off by {max_ulps} ulp");
|
|
}
|
|
|
|
/// Fit the gain-ramp lane arithmetic black-box against the oracle (`--ignored --nocapture`).
|
|
/// Candidates map (src, start, step, k) to the output sample; k ≥ 64 uses k = 64.
|
|
#[test]
|
|
#[ignore]
|
|
fn fit_gain_ramp() {
|
|
let Some(text) = vectors() else { return };
|
|
let input = signal(256, 23);
|
|
type F = fn(f32, f32, f32, usize) -> f32;
|
|
let candidates: [(&str, F); 8] = [
|
|
("src·(start + k·step)", |x, s, d, k| x * (s + k as f32 * d)),
|
|
("src·((start + 8g·step) + m·step)", |x, s, d, k| x * ((s + (8 * (k / 8)) as f32 * d) + (k % 8) as f32 * d)),
|
|
("fma(src, k·step, src·start)", |x, s, d, k| x.mul_add(k as f32 * d, x * s)),
|
|
("fma(src·step, k, src·start)", |x, s, d, k| (x * d).mul_add(k as f32, x * s)),
|
|
("src·start + src·(k·step)", |x, s, d, k| x * s + x * (k as f32 * d)),
|
|
("src·(f64 start + k·step)", |x, s, d, k| x * (f64::from(s) + k as f64 * f64::from(d)) as f32),
|
|
("f64 src·(start + k·step)", |x, s, d, k| (f64::from(x) * (f64::from(s) + k as f64 * f64::from(d))) as f32),
|
|
("src·fma(k, step, start) f64", |x, s, d, k| x * ((k as f64).mul_add(f64::from(d), f64::from(s)) as f32)),
|
|
];
|
|
for (name, f) in candidates {
|
|
let (mut n, mut exact) = (0, 0);
|
|
for line in text.lines().filter(|l| l.starts_with("gainramp ")) {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
let (g, s) = parts[1].split_once(':').unwrap();
|
|
let start = f32::from_bits(u32::from_str_radix(g, 16).unwrap());
|
|
let step = f32::from_bits(u32::from_str_radix(s, 16).unwrap());
|
|
let want = floats(&parts[2..]);
|
|
for k in 0..256 {
|
|
n += 1;
|
|
exact += usize::from(f(input[k], start, step, k.min(64)).to_bits() == want[k].to_bits());
|
|
}
|
|
}
|
|
eprintln!("{name}: {exact}/{n}");
|
|
}
|
|
}
|