Reintroduce apps/arcade and the ten game crates that were ripped out of
the fork (assets, audio, blocks, coedit, gen, net, pkg, render, script,
session), from makepad-internal/dev. Reconcile against the fork's layout:
- makepad-game-sim/math come from the fork's existing libs/sim + sim/math;
the copied internal libs/game/{sim,math} are dropped and the kept game
crates plus arcade point at ../../sim and ../../sim/math.
- arcade AI is rewritten off the removed makepad-ai agent API onto the
makepad-ai-hub headless ChatProvider worker (example/cad pattern):
AiWorker{send,cancel,poll}, ai_worker_loop driving
ClaudeApiChatProvider(ClaudeCli), AiWorkerEvent availability/delta/
done/error mapped into the chat feed and authoring land_edit, with
main.rs send_message/cancel_request/event-drain rebuilt around it.
- the game-script sandbox is adapted to the fork's jailed splash storage:
every isolate gets mod.fs = splash_storage jail; splaes splash_storage
set_root_for_heap is now public so the ScriptHost (or Splash) can grant
a per-game jail root. ENT font: build.rs/dispatch.rs entities fill new
fork sim Entity/Part fields via ..Default::default(); world_raycast now
borrows mutably and returns the material id.
cargo check -p makepad-arcade and the game + arcade test suites pass.
158 lines
5.8 KiB
Rust
158 lines
5.8 KiB
Rust
//! Compares the Vorbis decoder against a reference produced by the system
|
|
//! decoder. Skips cleanly where the fixtures or `afconvert` are unavailable,
|
|
//! so a fresh checkout is never blocked by a missing asset.
|
|
use makepad_game_audio as audio;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
const KENNEY: &str = "../../../apps/arcade/resources/audio/kenney";
|
|
|
|
/// Decode `ogg` with the system decoder for comparison. `None` when the tool
|
|
/// or the fixture is missing.
|
|
fn reference(ogg: &Path, tag: &str) -> Option<audio::Pcm> {
|
|
if !ogg.exists() {
|
|
return None;
|
|
}
|
|
let out = std::env::temp_dir().join(format!("mp_vorbis_ref_{tag}.wav"));
|
|
let _ = std::fs::remove_file(&out);
|
|
let ok = Command::new("afconvert")
|
|
.args(["-f", "WAVE", "-d", "LEF32"])
|
|
.arg(ogg)
|
|
.arg(&out)
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false);
|
|
if !ok {
|
|
return None;
|
|
}
|
|
audio::wav::decode(&std::fs::read(&out).ok()?).ok()
|
|
}
|
|
|
|
fn corr(a: &[f32], b: &[f32]) -> f64 {
|
|
let n = a.len().min(b.len());
|
|
let (mut num, mut da, mut db) = (0f64, 0f64, 0f64);
|
|
for i in 0..n {
|
|
let (x, y) = (a[i] as f64, b[i] as f64);
|
|
num += x * y;
|
|
da += x * x;
|
|
db += y * y;
|
|
}
|
|
if da <= 0.0 || db <= 0.0 {
|
|
return 0.0;
|
|
}
|
|
num / (da.sqrt() * db.sqrt())
|
|
}
|
|
|
|
/// Mono decode must be sample-exact: alignment, amplitude and length.
|
|
#[test]
|
|
fn mono_vorbis_matches_the_system_decoder() {
|
|
let p = Path::new(KENNEY).join("interface-sounds/click_001.ogg");
|
|
let Some(want) = reference(&p, "mono") else {
|
|
eprintln!("skip: run apps/arcade/download_assets.sh (or no afconvert)");
|
|
return;
|
|
};
|
|
let got = audio::decode(&std::fs::read(&p).unwrap()).expect("decode ogg");
|
|
assert_eq!(got.channels, want.channels, "channel count");
|
|
assert_eq!(got.sample_rate, want.sample_rate, "sample rate");
|
|
assert!(got.samples.iter().all(|s| s.is_finite()), "non-finite output");
|
|
|
|
let c = corr(&got.samples, &want.samples);
|
|
eprintln!(
|
|
"mono: got {} frames, ref {} frames, corr {c:.6}",
|
|
got.frames(),
|
|
want.frames()
|
|
);
|
|
// Aligned and scaled correctly, not merely "similar".
|
|
assert!(c > 0.999, "correlation {c:.6} — decoder disagrees with reference");
|
|
}
|
|
|
|
/// Stereo must be sample-exact too, and the file that pins it is deliberately
|
|
/// one that opens `[256, 256, 2048, ...]`. An early long block's window starts
|
|
/// before sample zero, and clamping that start to zero instead of dropping the
|
|
/// out-of-stream samples slid the block later — corrupting the head while the
|
|
/// body stayed correct, which reads as "mostly right" on any averaged metric.
|
|
/// Correlation alone hid it at 0.82, so this asserts per-sample agreement.
|
|
#[test]
|
|
fn stereo_vorbis_with_an_early_long_block_matches_sample_for_sample() {
|
|
let p = Path::new(KENNEY).join("impact-sounds/impactSoft_medium_003.ogg");
|
|
let Some(want) = reference(&p, "stereo") else {
|
|
eprintln!("skip: run apps/arcade/download_assets.sh (or no afconvert)");
|
|
return;
|
|
};
|
|
let got = audio::decode(&std::fs::read(&p).unwrap()).expect("decode ogg");
|
|
assert_eq!(got.channels, 2, "fixture must be stereo to pin coupling");
|
|
assert_eq!(got.channels, want.channels, "channel count");
|
|
|
|
let c = corr(&got.samples, &want.samples);
|
|
// afconvert trims a further 128 frames off the front, so compare the
|
|
// overlap rather than the lengths.
|
|
let n = got.samples.len().min(want.samples.len());
|
|
let close = (0..n)
|
|
.filter(|&i| (got.samples[i] - want.samples[i]).abs() < 1.0e-4)
|
|
.count();
|
|
let pct = 100.0 * close as f64 / n as f64;
|
|
eprintln!(
|
|
"stereo: got {} frames, ref {} frames, corr {c:.6}, {pct:.2}% samples equal",
|
|
got.frames(),
|
|
want.frames()
|
|
);
|
|
assert!(c > 0.999, "correlation {c:.6} — stereo decode disagrees");
|
|
assert!(
|
|
pct > 99.0,
|
|
"only {pct:.2}% of samples match — a correct body can hide a wrong head"
|
|
);
|
|
}
|
|
|
|
/// The stream's own granule position is the authority on length; the system
|
|
/// decoder trims a further half-window, so compare against the file, not it.
|
|
#[test]
|
|
fn output_length_follows_the_granule_position() {
|
|
let p = Path::new(KENNEY).join("interface-sounds/click_001.ogg");
|
|
if !p.exists() {
|
|
eprintln!("skip: run apps/arcade/download_assets.sh");
|
|
return;
|
|
}
|
|
let bytes = std::fs::read(&p).unwrap();
|
|
let got = audio::decode(&bytes).expect("decode");
|
|
let pages = audio::ogg::read_packets(&bytes).expect("pages");
|
|
assert_eq!(
|
|
got.frames() as u64,
|
|
pages.last_granule,
|
|
"decoded frames must equal the final granule position"
|
|
);
|
|
}
|
|
|
|
/// Corrupt input must be refused, never panic and never hang.
|
|
#[test]
|
|
fn malformed_vorbis_is_refused_not_fatal() {
|
|
let p = Path::new(KENNEY).join("interface-sounds/click_001.ogg");
|
|
if !p.exists() {
|
|
eprintln!("skip: run apps/arcade/download_assets.sh");
|
|
return;
|
|
}
|
|
let good = std::fs::read(&p).unwrap();
|
|
// Deterministic mutations across the whole file: header fields, segment
|
|
// tables and packet payloads all get hit.
|
|
let mut seed = 0x9E3779B97F4A7C15u64;
|
|
for i in 0..3000 {
|
|
let mut bad = good.clone();
|
|
for _ in 0..(1 + i % 8) {
|
|
seed ^= seed << 13;
|
|
seed ^= seed >> 7;
|
|
seed ^= seed << 17;
|
|
let at = (seed as usize) % bad.len();
|
|
bad[at] = (seed >> 32) as u8;
|
|
}
|
|
// Must return, either way, without panicking.
|
|
if let Ok(p) = audio::decode(&bad) {
|
|
assert!(p.samples.iter().all(|s| s.is_finite()), "non-finite from mutated input");
|
|
}
|
|
}
|
|
// Truncations at every scale.
|
|
for cut in [1usize, 2, 27, 47, 100, 1000] {
|
|
if cut < good.len() {
|
|
let _ = audio::decode(&good[..cut]);
|
|
let _ = audio::decode(&good[cut..]);
|
|
}
|
|
}
|
|
}
|