diff --git a/Cargo.lock b/Cargo.lock index 52561c16..20c3d1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4840,6 +4840,10 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "skate-audio" +version = "0.1.0" + [[package]] name = "skate-core" version = "0.1.0" @@ -4878,6 +4882,7 @@ dependencies = [ "sdl3", "serde", "serde_json", + "skate-audio", "skate-core", "skate-data", "skate-dynamics", diff --git a/Cargo.toml b/Cargo.toml index e8a82a77..c9b8ba01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/skate-core", "crates/skate-data", "crates/skate-game", "crates/skate-dynamics", "crates/skate-mods", "crates/skate-net", "crates/skate-steam-relay"] +members = ["crates/skate-core", "crates/skate-audio", "crates/skate-data", "crates/skate-game", "crates/skate-dynamics", "crates/skate-mods", "crates/skate-net", "crates/skate-steam-relay"] resolver = "3" [workspace.package] diff --git a/crates/skate-audio/Cargo.toml b/crates/skate-audio/Cargo.toml new file mode 100644 index 00000000..347e24ab --- /dev/null +++ b/crates/skate-audio/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "skate-audio" +version.workspace = true +edition.workspace = true +publish.workspace = true +description = "Native port of Skate 3's AEMS patch-program evaluator and RenderWare-style voice graph (engine independent)" diff --git a/crates/skate-audio/examples/aems_list.rs b/crates/skate-audio/examples/aems_list.rs new file mode 100644 index 00000000..47bca200 --- /dev/null +++ b/crates/skate-audio/examples/aems_list.rs @@ -0,0 +1,51 @@ +//! List every bank's modules: bound class, capacity, payload words, players and the opcodes used. +//! +//! usage: aems_list [name filter] +use std::path::Path; + +use skate_audio::be::u8_at; +use skate_audio::eval::Evaluator; +use skate_audio::eval::symbols::SymRef; +use skate_audio::formats::{Bank, Project}; + +fn main() { + let args: Vec = std::env::args().collect(); + let dir = Path::new(&args[1]); + let filter = args.get(2).map(|s| s.to_ascii_lowercase()); + let mut eval = Evaluator::new(); + for name in std::fs::read_to_string(dir.join("csi_order.txt")).expect("csi_order.txt").lines() { + eval.install_project(&Project::parse(name, &std::fs::read(dir.join(name)).unwrap()).unwrap()); + } + let mut names: Vec = std::fs::read_dir(dir) + .unwrap() + .filter_map(|e| e.ok()?.file_name().into_string().ok()) + .filter(|n| n.to_ascii_lowercase().ends_with(".abk")) + .filter(|n| filter.as_ref().is_none_or(|f| n.to_ascii_lowercase().contains(f))) + .collect(); + names.sort(); + for name in names { + let bank = Bank::parse(&name, std::fs::read(dir.join(&name)).unwrap()).unwrap(); + for (i, m) in bank.modules.iter().enumerate() { + let class = bank + .exports + .iter() + .find(|e| e.handle_offset == m.offset + 4) + .and_then(|e| eval.registry.lookup(e.kind, e.project, e.name_id, &e.name)) + .and_then(|s| match s { + SymRef::Class(c) => Some(eval.registry.classes[c].name.clone()), + _ => None, + }) + .unwrap_or_else(|| "?".into()); + let payload = m.class_data_state.map(|o| u8_at(m.template(&bank.data), o as usize + 16)); + let mut ops: Vec = m.program.iter().map(|o| o.opcode).collect(); + ops.sort_unstable(); + ops.dedup(); + println!( + "{name} #{i} class={class} capacity={} payload={payload:?} players={} samples={} ops={ops:?}", + m.max_instances, + m.num_players, + bank.samples.len() + ); + } + } +} diff --git a/crates/skate-audio/examples/aems_render.rs b/crates/skate-audio/examples/aems_render.rs new file mode 100644 index 00000000..c8700c9a --- /dev/null +++ b/crates/skate-audio/examples/aems_render.rs @@ -0,0 +1,129 @@ +//! Headless end-to-end render: install the projects, load banks with their decoded WAVs, post one +//! message and render through the voice graph and the output stage's stereo fold. Prints per-second +//! RMS / peak (dBFS), voice counts and the render speed; optionally writes a 48 kHz stereo WAV. +//! +//! usage: aems_render