open-standards/wai/wai-rs/tests/cli_generate_score.rs
Transaction Science 56ff513aec
Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 17s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Failing after 54s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Failing after 2m33s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 3m8s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
wai-audit collector and player integration / does this change touch it (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
WAI operation catalogue / does this change touch it (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-quantum for JavaScript / does this change touch it (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum studio and desktop / does this change touch it (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-android native sink library / does this change touch it (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-sink-swift native sink package / does this change touch it (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
WAI Studio / does this change touch it (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-ledger service / does this change touch it (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
wai-app and Create / wai-app · does this change touch it (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
wai-fleet service / does this change touch it (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
wai.audio.score: band-limited timbres, a sine and noise, a voice per note, and two channels
The reference synthesiser gains what a short channel sting needs, all in the
wai.det.fixed64 floor and trig-free, every committed score rendering byte for byte as
before (the three first-revision vectors keep their mixdown hash, PCM SHA-256 and score
hash):

- score §2.2, §3.1: waveforms 4 to 6, the saw, square and 25 %-pulse with each step
  corrected by a polynomial (PolyBLEP); 7, a sine by a refined parabola; 8, noise held
  from a 32-bit LCG between wraps of the phase.
- score §2.1, §3.4: a contract of two channels mixes down to interleaved frames; a reader
  refuses any count but 1 and 2; a score with no voices centres each note.
- score §2.4, §3.3: an optional third section, one 73-byte voice per note: a
  constant-power pan by the profile's integer square root, curved attack, decay and
  release (the release from the level at note-off), up to four one-pole low-pass sections
  that follow the envelope, a linear glide, the noise seed, and up to eight echoes at
  alternating pans. The 65-byte note record is unchanged; the score hash binds the
  voices when present.
- wai score build|render: channels 2, waveforms to 8, each note's optional voice; render
  prints frames and channels. Receipt blocks are 4096 frames.
- score-conformance: five vectors at 48 kHz (band-limited, two channels, voiced in one,
  a two-second sting, a voiced scale); multi_verify.py's independent synth agrees with
  all eight.
- packaging §5.8: a score of one or two channels packs beside the lead with its voices,
  its floor the FLAC of the mixdown in its channels; verify holds the floor to the
  base's channels and rate. pack-conformance/multi/multi-sting-01 is a voiced
  two-channel score at 48 kHz.
- The composing sink synthesises and plays a score in its contract's channels; its
  reference package's score is now two channels at 48 kHz, voiced.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 14:21:46 -05:00

568 lines
30 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// wai-conformance: int-kernels
//! `wai generate` and `wai score build|render`, run as a user runs them: the native
//! pins a page checks its own output against. Each output is checked against a
//! committed vector — `tests/vectors/generate/` for pictures, `score-conformance/`
//! for scores — and every refusal is checked to write nothing.
//!
//! Cargo.toml declares this target with required-features = ["generate", "score"], so a
//! run without them stops with an error instead of reporting an empty binary as passed.
//! The two larger listed sizes of `instr-v0.json` take seconds in a debug build and run
//! in the release step.
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use serde_json::Value;
use wai::container::Wai;
fn root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn gen_dir() -> PathBuf {
root().join("tests/vectors/generate")
}
fn score_dir() -> PathBuf {
root().join("../score-conformance")
}
fn model() -> PathBuf {
gen_dir().join("generate_spectral_model.json")
}
fn jload(p: &Path) -> Value {
serde_json::from_slice(&std::fs::read(p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))).unwrap()
}
fn sha256_hex(b: &[u8]) -> String {
wai::pin::sha256(b).iter().map(|x| format!("{x:02x}")).collect()
}
fn scratch(name: &str) -> PathBuf {
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_generate_score");
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join(name);
let _ = std::fs::remove_file(&p);
p
}
fn wai(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_wai")).args(args).output().expect("run wai")
}
fn ok(run: &Output) -> String {
assert!(run.status.success(), "wai failed: {}", String::from_utf8_lossy(&run.stderr));
String::from_utf8(run.stdout.clone()).unwrap()
}
fn refused(run: &Output, out: &Path, what: &str) -> String {
let stderr = String::from_utf8_lossy(&run.stderr).into_owned();
assert!(!run.status.success(), "{what}: must be refused; stdout: {}", String::from_utf8_lossy(&run.stdout));
assert!(!out.exists(), "{what}: a refusal writes nothing");
stderr
}
/// The `data` of the JSON envelope (envelope/1) that `wai ... --json` prints.
fn data(stdout: &str) -> Value {
let env: Value = serde_json::from_str(stdout.trim()).expect("one JSON envelope");
assert_eq!((env["wai"].as_str(), env["ok"].as_bool()), (Some("envelope/1"), Some(true)), "{env}");
env["data"].clone()
}
/// The `--json` data `wai generate` prints, checked against the file it wrote.
fn made(stdout: &str, out: &Path, w: u64, h: u64) -> String {
let v: Value = data(stdout);
let rgb = std::fs::read(out).unwrap();
assert_eq!((v["w"].as_u64(), v["h"].as_u64()), (Some(w), Some(h)));
assert_eq!(v["bytes"].as_u64(), Some(w * h * 3));
assert_eq!(rgb.len() as u64, w * h * 3, "RGB8, w·h·3 bytes");
let sha = v["sha256"].as_str().unwrap().to_string();
assert_eq!(sha256_hex(&rgb), sha, "the printed SHA-256 is the file's");
sha
}
fn m() -> String {
model().to_string_lossy().into_owned()
}
fn p(path: &Path) -> &str {
path.to_str().unwrap()
}
#[test]
fn generate_reproduces_the_committed_vectors() {
// 96×96: the committed vector and its SHA-256 sidecar.
let pin = jload(&gen_dir().join("generate.json"));
let side = jload(&gen_dir().join("sha256.json"));
let out = scratch("p96.rgb");
let seed = pin["seed"].to_string();
let steps = pin["steps"].to_string();
let run = wai(&["generate", "--model", &m(), "--prompt", pin["prompt"].as_str().unwrap(), "--seed", &seed,
"--steps", &steps, "--size", "96x96", "-o", p(&out), "--json"]);
assert_eq!(made(&ok(&run), &out, 96, 96), side["generate"]["sha256"].as_str().unwrap());
// sizes.json: the same prompt, seed and steps at its non-square sizes.
let sizes = jload(&gen_dir().join("sizes.json"));
for s in sizes["sizes"].as_array().unwrap() {
let (w, h) = (s["w"].as_u64().unwrap(), s["h"].as_u64().unwrap());
let out = scratch(&format!("p{w}x{h}.rgb"));
let run = wai(&["generate", "--model", &m(), "--prompt", sizes["prompt"].as_str().unwrap(), "--seed", &seed,
"--steps", &steps, "--size", &format!("{w}x{h}"), "-o", p(&out), "--json"]);
assert_eq!(made(&ok(&run), &out, w, h), s["sha256"].as_str().unwrap(), "{w}x{h}");
}
}
#[test]
fn generate_refuses_flags_outside_form_v0() {
let base = ["generate", "--model", &m(), "--prompt", "calm ocean", "--seed", "3", "--steps", "4"];
for (extra, what) in [
(&["--size", "0x128"][..], "a side of 0"),
(&["--size", "4097x1"][..], "a side past 4096"),
(&["--size", "4096x4096"][..], "more than 2^23 pixels"),
(&["--size", "256"][..], "not WxH"),
] {
let out = scratch("flags.rgb");
let mut args: Vec<&str> = base.to_vec();
args.extend_from_slice(extra);
args.extend_from_slice(&["-o", p(&out)]);
refused(&wai(&args), &out, what);
}
for (flag, value, what) in [
("--seed", "4294967296", "a seed past 2^32 - 1"),
("--steps", "0", "steps below 1"),
("--steps", "65", "steps past 64"),
("--prompt", "a \"quoted\" prompt", "a quotation mark in the prompt"),
] {
let out = scratch("flags.rgb");
let mut args: Vec<String> = base.iter().map(|s| s.to_string()).collect();
let i = args.iter().position(|a| a == flag).unwrap();
args[i + 1] = value.into();
args.extend(["--size", "64x32", "-o", p(&out)].map(String::from));
let args: Vec<&str> = args.iter().map(String::as_str).collect();
refused(&wai(&args), &out, what);
}
}
/// The vector's object, written as `--write-instr` writes it from the same flags.
#[test]
fn write_instr_writes_the_vectors_object() {
let v = jload(&gen_dir().join("instr-v0.json"));
let object = v["object"].as_str().unwrap();
let o: Value = serde_json::from_str(object).unwrap();
let sizes: Vec<String> = o["sizes"].as_array().unwrap().iter().map(|s| format!("{}x{}", s[0], s[1])).collect();
let out = scratch("written.instr");
ok(&wai(&["generate", "--model", &m(), "--prompt", o["prompt"].as_str().unwrap(), "--seed", &o["seed"].to_string(),
"--steps", &o["steps"].to_string(), "--sizes", &sizes.join(","), "--write-instr", p(&out)]));
assert_eq!(std::fs::read(&out).unwrap(), object.as_bytes());
}
fn instr_sizes(min_pixels: u64, max_pixels: u64) -> usize {
let v = jload(&gen_dir().join("instr-v0.json"));
// A file of its own per caller: the two tests that call this run at once, and each
// removes and rewrites its object before its `wai` children read it.
let object = scratch(&format!("object-{min_pixels}.instr"));
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
let mut n = 0;
for e in v["outputs"].as_array().unwrap() {
let (w, h) = (e["w"].as_u64().unwrap(), e["h"].as_u64().unwrap());
if !(min_pixels..=max_pixels).contains(&(w * h)) {
continue;
}
let out = scratch(&format!("instr{w}x{h}.rgb"));
let run = wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", &format!("{w}x{h}"),
"-o", p(&out), "--json"]);
assert_eq!(made(&ok(&run), &out, w, h), e["sha256"].as_str().unwrap(), "{w}x{h}");
n += 1;
}
n
}
#[test]
fn instr_makes_the_listed_small_sizes() {
assert_eq!(instr_sizes(0, 640 * 320), 2);
}
#[test]
#[cfg_attr(debug_assertions, ignore = "1280x720 and 1920x1080 take seconds in a debug build; the release step runs them")]
fn instr_makes_the_listed_large_sizes() {
assert_eq!(instr_sizes(640 * 320 + 1, u64::MAX), 2);
}
/// Every object the vector lists as refused is refused by `wai generate --instr` and
/// by `wai wrap`, each for the reason the vector states (the first rule the reader
/// finds broken), and neither writes anything. Holding the reason, not only the
/// refusal, notices a rule that stops refusing while another rule still refuses the
/// same object: a quotation mark in the prompt, say, which the canonical-bytes check
/// would refuse if the prompt rule let it through. The vector's own object is accepted
/// by both, and the envelope carries it byte for byte with intent `create`.
#[test]
fn the_vectors_refused_objects_are_refused() {
let v = jload(&gen_dir().join("instr-v0.json"));
let cases = v["refused"].as_array().unwrap();
assert!(cases.len() >= 30, "the vector lists its refusals");
let object = scratch("refused.instr");
for c in cases {
let why = c["why"].as_str().unwrap();
let reason = c["reason"].as_str().unwrap_or_else(|| panic!("{why}: the vector states no reason"));
std::fs::write(&object, c["object"].as_str().unwrap()).unwrap();
let out = scratch("refused.rgb");
let stderr = refused(&wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", "256x128",
"-o", p(&out)]), &out, why);
assert!(stderr.contains(&format!("refused: {reason}: ")), "{why}: refused, but not as {reason}: {stderr}");
let env = scratch("refused.wai");
let stderr = refused(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]),
&env, why);
assert!(stderr.contains("instruction object of form v0") && stderr.contains(&format!(": {reason}: ")),
"{why}: refused, but not as {reason}: {stderr}");
}
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
let env = scratch("accepted.wai");
ok(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]));
let e = Wai::read(&env).unwrap();
assert_eq!((e.capability(), e.intent()), ("wai.generate.spectral", "create"));
assert_eq!(e.payload, v["object"].as_str().unwrap().as_bytes());
}
/// Every object the vector lists as accepted — one at each inclusive bound of form v0 —
/// is accepted by `wai generate --instr`, which makes its first listed size with the
/// SHA-256 the vector states, and by `wai wrap`, whose envelope carries it byte for
/// byte. A reader stricter than form v0 by one refuses one of them.
#[test]
fn the_vectors_accepted_objects_are_accepted() {
let v = jload(&gen_dir().join("instr-v0.json"));
let cases = v["accepted"].as_array().unwrap();
assert!(cases.len() >= 12, "the vector lists an object at each bound");
let object = scratch("accepted.instr");
for c in cases {
let why = c["why"].as_str().unwrap();
std::fs::write(&object, c["object"].as_str().unwrap()).unwrap();
for e in c["outputs"].as_array().unwrap() {
let (w, h) = (e["w"].as_u64().unwrap(), e["h"].as_u64().unwrap());
let out = scratch("accepted.rgb");
let run = wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", &format!("{w}x{h}"),
"-o", p(&out), "--json"]);
assert!(run.status.success(), "{why}: refused: {}", String::from_utf8_lossy(&run.stderr));
assert_eq!(made(&ok(&run), &out, w, h), e["sha256"].as_str().unwrap(), "{why}: {w}x{h}");
}
let env = scratch("accepted-bound.wai");
let run = wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]);
assert!(run.status.success(), "{why}: wrap refused it: {}", String::from_utf8_lossy(&run.stderr));
assert_eq!(Wai::read(&env).unwrap().payload, c["object"].as_str().unwrap().as_bytes(), "{why}");
}
}
#[test]
fn instr_refuses_another_model_and_an_unlisted_size() {
let v = jload(&gen_dir().join("instr-v0.json"));
let object = scratch("model.instr");
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
// The same model with one more byte (a line feed): the same parameters, another digest.
let other = scratch("other-model.json");
let mut bytes = std::fs::read(model()).unwrap();
bytes.push(b'\n');
std::fs::write(&other, bytes).unwrap();
let out = scratch("model.rgb");
let stderr = refused(&wai(&["generate", "--model", p(&other), "--instr", p(&object), "--size", "256x128",
"-o", p(&out)]), &out, "another model file");
assert!(stderr.contains("the object's model is sha256:"), "{stderr}");
let stderr = refused(&wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", "96x96", "-o", p(&out)]),
&out, "an unlisted size");
assert!(stderr.contains("not a listed size"), "{stderr}");
}
#[test]
fn wrap_refuses_replicate_over_the_generator() {
let v = jload(&gen_dir().join("instr-v0.json"));
let object = scratch("replicate.instr");
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
for extra in [&[][..], &["--intent", "replicate"][..]] {
let env = scratch("replicate.wai");
let mut args = vec!["wrap", "wai.generate.spectral", p(&object), "-o", p(&env)];
args.extend_from_slice(extra);
let stderr = refused(&wai(&args), &env, "replicate over a generator");
assert!(stderr.contains("GenerativeDeterministic"), "{stderr}");
}
}
/// SPEC §5: an envelope that names the generator carries the intent `create`. `wai wrap`
/// writes no other: not `improve`, not a string the manifest schema leaves open, and not
/// with the generator as the fallback of another capability.
#[test]
fn wrap_writes_the_generator_only_with_intent_create() {
let v = jload(&gen_dir().join("instr-v0.json"));
let object = scratch("intent.instr");
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
for intent in ["improve", "banana", "Create", ""] {
let env = scratch("intent.wai");
let stderr = refused(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", intent, "-o", p(&env)]),
&env, &format!("intent {intent:?}"));
assert!(stderr.contains("carries the intent `create`"), "{intent:?}: {stderr}");
}
let env = scratch("intent.wai");
let stderr = refused(&wai(&["wrap", "wai.image.png", p(&object), "--fallback", "wai.generate.spectral",
"--intent", "improve", "-o", p(&env)]), &env, "the generator as the fallback");
assert!(stderr.contains("carries the intent `create`"), "{stderr}");
ok(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]));
}
/// SPEC §5: `wai inspect` and `wai unwrap` report an envelope that names the generator —
/// its payload, its intent and any pin — ✓ when it conforms, ✗ with the reason when it
/// does not. Each bad `WAI1` differs from a good one in one respect: one prompt byte
/// changed to 0x01 in place (the envelope still parses; the payload is no longer JSON,
/// since a string holds no raw control character), the intent `improve`, or a pin
/// over the very model file `model` names. A `WAI2` component of the generator is
/// reported the same way, with and without a pin.
#[test]
fn inspect_and_unwrap_report_the_generators_envelope() {
use wai::container::{ManifestV2, Prior, RenditionMeta, WaiMulti};
let v = jload(&gen_dir().join("instr-v0.json"));
let object = v["object"].as_str().unwrap();
let input = scratch("report.instr");
std::fs::write(&input, object).unwrap();
let good = scratch("report-good.wai");
ok(&wai(&["wrap", "wai.generate.spectral", p(&input), "--intent", "create", "-o", p(&good)]));
let report = |env: &Path| (ok(&wai(&["unwrap", p(env)])), ok(&wai(&["inspect", p(env)])));
let (u, i) = report(&good);
assert!(u.contains("conformance \u{2713} an instruction object of form v0, intent create, no pin"), "{u}");
assert!(i.contains("instruction: \u{2713}") && !i.contains("does not conform"), "{i}");
let mut bytes = std::fs::read(&good).unwrap();
let at = bytes.windows(4).position(|w| w == b"calm").unwrap();
bytes[at] = 0x01;
let bad_payload = scratch("report-payload.wai");
std::fs::write(&bad_payload, &bytes).unwrap();
let mut e = Wai::read(&good).unwrap();
e.manifest.intent = "improve".into();
let bad_intent = scratch("report-intent.wai");
e.write(&bad_intent).unwrap();
let model_sha = sha256_hex(&std::fs::read(model()).unwrap());
let pin = Prior::new("the spectral model", model_sha, "integer generator");
let mut e = Wai::read(&good).unwrap();
e.manifest.conditioning.prior = Some(pin.clone());
let bad_pin = scratch("report-pin.wai");
e.write(&bad_pin).unwrap();
for (env, said) in [(&bad_payload, "not_json"), (&bad_intent, "the intent is `improve`"), (&bad_pin, "carries a pin")] {
let (u, i) = report(env);
assert!(u.contains("conformance \u{2717}") && u.contains(said), "{said}: {u}");
assert!(i.contains("instruction: \u{2717}") && i.contains(said) && i.contains("does not conform"),
"{said}: {i}");
}
for (pinned, name) in [(false, "report-two.wai"), (true, "report-two-pin.wai")] {
let mut meta = RenditionMeta::new("wai.generate.spectral", "bytes");
if pinned {
meta = meta.with_prior(pin.clone());
}
let two = WaiMulti::from_parts(ManifestV2::new("image", "create", vec![meta]), vec![object.as_bytes().to_vec()]).unwrap();
let path = scratch(name);
two.write(&path).unwrap();
let u = ok(&wai(&["unwrap", p(&path), "--component", "0"]));
let i = ok(&wai(&["inspect", p(&path)]));
let mark = if pinned { "\u{2717}" } else { "\u{2713}" };
assert!(u.contains(&format!("conformance [0] {mark}")), "{name}: {u}");
assert!(i.contains(&format!("[0] instruction: {mark}")), "{name}: {i}");
assert_eq!(pinned, u.contains("carries a pin") && i.contains("carries a pin"), "{name}");
}
}
/// Every score of the corpus: `build` from its authoring form writes the committed
/// `WSCR` byte for byte, and `render` prints, and `--pcm` writes, the digests
/// `expected_hashes.json` states.
#[test]
fn score_build_and_render_reproduce_the_corpus() {
let expected = jload(&score_dir().join("expected_hashes.json"));
let entries = expected.as_object().unwrap();
assert_eq!(entries.len(), 8);
for (file, spec) in entries {
let name = file.strip_suffix(".wscr").unwrap();
let committed = std::fs::read(score_dir().join("vectors").join(file)).unwrap();
let built = scratch(&format!("{name}.wscr"));
let notes = score_dir().join("notes").join(format!("{name}.json"));
let stdout = ok(&wai(&["score", "build", p(&notes), "-o", p(&built), "--json"]));
assert_eq!(std::fs::read(&built).unwrap(), committed, "{name}: built byte for byte");
let b: Value = data(&stdout);
assert_eq!(b["bytes"].as_u64(), Some(committed.len() as u64));
assert_eq!(b["score_hash"], spec["score_hash"], "{name}");
let pcm = scratch(&format!("{name}.s16"));
let stdout = ok(&wai(&["score", "render", p(&built), "--pcm", p(&pcm), "--json"]));
let r: Value = data(&stdout);
let duration = spec["duration"].as_u64().unwrap();
let channels = spec["channels"].as_u64().unwrap();
assert_eq!(r["samples"].as_u64(), Some(duration), "{name}: the contract's duration, whole");
assert_eq!(r["sample_rate"], spec["sample_rate"], "{name}");
assert_eq!(r["channels"].as_u64(), Some(channels), "{name}");
assert_eq!(r["notes"], spec["notes"]);
assert_eq!(r["pcm_sha256"], spec["pcm_sha256"], "{name}");
assert_eq!(r["mixdown_blake3"], spec["mixdown_hash"], "{name}");
assert_eq!(r["score_hash"], spec["score_hash"], "{name}");
let samples = std::fs::read(&pcm).unwrap();
assert_eq!(samples.len() as u64, duration * channels * 2, "{name}: i16 samples of every channel");
assert_eq!(Some(sha256_hex(&samples).as_str()), spec["pcm_sha256"].as_str(), "{name}: the written PCM");
// The score travels under intent `replicate`: its class is StandardDefined.
let env = scratch(&format!("{name}.wai"));
ok(&wai(&["wrap", "wai.audio.score", p(&built), "--intent", "replicate", "-o", p(&env)]));
let e = Wai::read(&env).unwrap();
assert_eq!((e.capability(), e.intent()), ("wai.audio.score", "replicate"));
assert_eq!(e.payload, committed);
}
}
/// The voiced two-channel example of score §2.3, as the extension prints it, builds and
/// renders: 72 000 frames of two channels at 48 kHz, interleaved, the note panned left
/// (its left louder than its right) and its echoes crossing to the right.
#[test]
fn the_extension_s_two_channel_example_builds_and_renders() {
let md = std::fs::read_to_string(root().join("../extensions/score.md")).unwrap();
let doc = md
.split("```json")
.skip(1)
.map(|b| b.split("```").next().unwrap())
.find(|b| b.contains("\"channels\": 2"))
.expect("score §2.3's two-channel example");
let input = scratch("example.json");
std::fs::write(&input, doc).unwrap();
let built = scratch("example.wscr");
ok(&wai(&["score", "build", p(&input), "-o", p(&built)]));
let pcm = scratch("example.s16");
let r: Value = data(&ok(&wai(&["score", "render", p(&built), "--pcm", p(&pcm), "--json"])));
assert_eq!((r["samples"].as_u64(), r["channels"].as_u64(), r["sample_rate"].as_u64()), (Some(72000), Some(2), Some(48000)), "{r}");
let b = std::fs::read(&pcm).unwrap();
assert_eq!(b.len(), 72000 * 2 * 2);
assert_eq!(Some(sha256_hex(&b).as_str()), r["pcm_sha256"].as_str());
let samples = |b: &[u8]| b.chunks(2).map(|c| i16::from_le_bytes([c[0], c[1]]) as i64).collect::<Vec<i64>>();
let s = samples(&b);
// The same note with no echoes: what the first echo adds before the second starts.
std::fs::write(&input, doc.replace("\"echo_taps\": 3", "\"echo_taps\": 0")).unwrap();
let dry_built = scratch("example-dry.wscr");
ok(&wai(&["score", "build", p(&input), "-o", p(&dry_built)]));
let dry_pcm = scratch("example-dry.s16");
ok(&wai(&["score", "render", p(&dry_built), "--pcm", p(&dry_pcm)]));
let dry = samples(&std::fs::read(&dry_pcm).unwrap());
let energy = |x: &[i64], from: usize, to: usize, ch: usize| (from..to).map(|f| x[2 * f + ch].pow(2)).sum::<i64>();
assert!(energy(&s, 0, 12000, 0) > 2 * energy(&s, 0, 12000, 1), "the note sits left");
assert_eq!(s[..2 * 12000], dry[..2 * 12000], "nothing echoes before the first echo");
let echo: Vec<i64> = s.iter().zip(&dry).map(|(a, b)| a - b).collect();
assert!(energy(&echo, 12000, 24000, 1) > 2 * energy(&echo, 12000, 24000, 0), "its first echo crosses right");
}
#[test]
fn score_build_refuses_what_is_not_integer_notes_rendered_whole() {
let note = r#"{"start":0,"dur":100,"phase_inc":50960238,"gain":1717986918,"waveform":2,"attack":10,"decay":10,"sustain":2147483648,"release":50}"#;
let score = |note: &str, top: &str| format!(r#"{{"sample_rate":22050,"channels":1,"duration":200{top},"notes":[{note}]}}"#);
// The note with a voice of the members given.
let voiced = |members: &str| note.replace(",\"release\":50}", &format!(",\"release\":50,\"voice\":{{{members}}}}}"));
// The same in two channels at 48 kHz.
let stereo = |members: &str| score(&voiced(members), "").replace("\"channels\":1", "\"channels\":2").replace("22050", "48000");
// The base case builds, and so does each form of it with a voice.
let input = scratch("base.json");
std::fs::write(&input, score(note, "")).unwrap();
let out = scratch("base.wscr");
ok(&wai(&["score", "build", p(&input), "-o", p(&out)]));
for doc in [
score(&voiced(""), ""),
score(&voiced(r#""decay_curve":8,"lp_poles":4,"lp_a":0,"lp_env":4294967296,"glide_to":0,"glide":200,"seed":4294967295,"echo_taps":1,"echo_delay":50,"echo_gain":4294967296"#), ""),
stereo(r#""pan":-4294967296,"echo_pan":4294967296,"echo_taps":8,"echo_delay":6,"echo_gain":2147483648"#),
] {
std::fs::write(&input, &doc).unwrap();
ok(&wai(&["score", "build", p(&input), "-o", p(&out)]));
let s = wai::score::Score::from_bytes(&std::fs::read(&out).unwrap()).unwrap();
assert!(s.voices.is_some(), "{doc}: a voiced score");
}
for (doc, what) in [
(score(&note.replace("1717986918", "0.4"), ""), "a gain with a fraction"),
(score(&note.replace("1717986918", "4294967297"), ""), "a gain past 1.0"),
(score(&note.replace("50960238", "4294967296"), ""), "a phase_inc of a whole cycle"),
(score(&note.replace("\"waveform\":2", "\"waveform\":9"), ""), "a waveform past 8"),
(score(&note.replace("\"start\":0", "\"start\":-1"), ""), "a negative start"),
(score(&note.replace("\"release\":50", "\"release\":101"), ""), "a release the duration would cut"),
(score(&note.replace("\"dur\":100,", "\"dur\":100,\"pitch\":60,"), ""), "a member that is not a note field"),
(score(&note.replace(",\"release\":50", ""), ""), "a missing note field"),
(score(&note.replace("\"start\":0", "\"start\":0,\"start\":5"), ""), "a note field given twice"),
(score(note, ",\"duration\":300"), "a document member given twice"),
(score(note, ",\"numeric\":\"wai.det.float\""), "another numeric profile"),
(score(note, "").replace("\"channels\":1", "\"channels\":3"), "three channels"),
(score(&voiced(r#""pan":1073741824"#), ""), "a pan in one channel"),
(score(&voiced(r#""decay_curve":0"#), ""), "a curve of 0"),
(score(&voiced(r#""release_curve":9"#), ""), "a curve past 8"),
(score(&voiced(r#""lp_poles":5"#), ""), "five low-pass sections"),
(score(&voiced(r#""lp_a":4294967297"#), ""), "a low-pass coefficient past 1.0"),
(score(&voiced(r#""glide_to":4294967296"#), ""), "a glide to a whole cycle"),
(score(&voiced(r#""glide":10"#), ""), "a glide to no pitch"),
(score(&voiced(r#""echo_taps":9,"echo_delay":1"#), ""), "nine echoes"),
(score(&voiced(r#""echo_taps":1"#), ""), "an echo of no delay"),
(score(&voiced(r#""echo_taps":1,"echo_delay":51"#), ""), "an echo the duration would cut"),
(score(&voiced(r#""echo_gain":4294967297"#), ""), "an echo louder than its note"),
(score(&voiced(r#""seed":4294967296"#), ""), "a seed past 32 bits"),
(score(&voiced(r#""cutoff":1"#), ""), "a member that is not a voice field"),
(score(&voiced(r#""seed":1,"seed":2"#), ""), "a voice field given twice"),
(score(&note.replace(",\"release\":50}", ",\"release\":50,\"voice\":[]}"), ""), "a voice that is not an object"),
(stereo(r#""pan":4294967297"#), "a pan past 1.0"),
(stereo(r#""echo_pan":-4294967297"#), "an echo pan past -1.0"),
(stereo(r#""pan":0.5"#), "a pan with a fraction"),
(score(note, "").replace("\"channels\":1", "\"channels\":2").replace("\"duration\":200", "\"duration\":33554433"), "two channels past 2^26 samples"),
(score(note, "").replace("\"duration\":200", "\"duration\":67108865"), "a duration past 2^26 samples"),
("[]".to_string(), "not an object"),
] {
std::fs::write(&input, &doc).unwrap();
let out = scratch("refused.wscr");
let stderr = refused(&wai(&["score", "build", p(&input), "-o", p(&out)]), &out, what);
assert!(stderr.contains("refused"), "{what}: {stderr}");
}
}
/// `render` refuses a contract it would misreport: the reference mixdown is one channel
/// or two in `wai.det.fixed64`, so a contract stating three channels or another numeric
/// profile is refused, as `build` refuses to write one. Each case is the committed
/// scale.wscr with its contract patched in place.
#[test]
fn score_render_refuses_a_contract_it_does_not_render() {
let committed = std::fs::read(score_dir().join("vectors/scale.wscr")).unwrap();
let patch = |from: &str, to: &str| {
assert_eq!(from.len(), to.len(), "a patch in place keeps every offset");
let at = committed.windows(from.len()).position(|w| w == from.as_bytes()).unwrap();
let mut b = committed.clone();
b[at..at + to.len()].copy_from_slice(to.as_bytes());
b
};
for (bytes, what, said) in [
(patch("\"channels\":1", "\"channels\":3"), "three channels", "`channels` is 3"),
(patch("wai.det.fixed64", "wai.det.float64"), "another numeric profile", "wai.det.float64"),
] {
let input = scratch("contract.wscr");
std::fs::write(&input, bytes).unwrap();
let pcm = scratch("contract.s16");
let stderr = refused(&wai(&["score", "render", p(&input), "--pcm", p(&pcm), "--json"]), &pcm, what);
assert!(stderr.contains("refused") && stderr.contains(said), "{what}: {stderr}");
}
}
/// A `WSCR` that declares more notes than its notes section holds is refused before the
/// reader reserves room for them: a file of a few bytes can declare 2^32 − 1. The file
/// carries a well-formed contract (section 0x01) and a notes section (0x02) holding only
/// the count, so without the check the reader would pass every other rule and reach the
/// reservation; the check, not a missing section, is what refuses it.
#[test]
fn score_render_refuses_a_note_count_its_bytes_cannot_hold() {
let contract = wai::score::ScoreContract {
sample_rate: 22050, numeric: wai::world::NUMERIC_FIXED64.into(), channels: 1, duration: 100,
}
.to_canonical_json();
let mut b = b"WSCR".to_vec();
b.extend_from_slice(&2u16.to_le_bytes());
for (kind, off, len) in [(0x01u8, 0u32, contract.len() as u32), (0x02, contract.len() as u32, 4)] {
b.push(kind);
b.extend_from_slice(&off.to_le_bytes());
b.extend_from_slice(&len.to_le_bytes());
}
b.extend_from_slice(&contract);
b.extend_from_slice(&u32::MAX.to_le_bytes());
// The same file declaring no notes renders: the contract and the layout are good.
let mut empty = b.clone();
let n = empty.len();
empty[n - 4..].copy_from_slice(&0u32.to_le_bytes());
let input = scratch("empty.wscr");
std::fs::write(&input, empty).unwrap();
let r: Value = data(&ok(&wai(&["score", "render", p(&input), "--json"])));
assert_eq!((r["samples"].as_u64(), r["notes"].as_u64()), (Some(100), Some(0)), "{r}");
let input = scratch("huge.wscr");
std::fs::write(&input, b).unwrap();
let pcm = scratch("huge.s16");
let stderr = refused(&wai(&["score", "render", p(&input), "--pcm", p(&pcm)]), &pcm, "2^32 - 1 notes in 4 bytes");
assert!(stderr.contains("declares 4294967295 notes"), "{stderr}");
}