Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 19s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m32s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 3m0s
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-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
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-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-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-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
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 breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m44s
The C ABI test read wai_last_error() through CStr::from_ptr after every call. After a decode that succeeds no error is set and the pointer is null, so a C ABI that returned the two-channel capability as mono crashed the test binary (SIGSEGV) instead of failing its assertion. The test now reads the error only when the pointer is not null; with that fault planted on Thor it fails at `assert_eq!(code, WAI_ERR_CODEC)` (left 0, right 2). Changelog: none Co-Authored-By: Claude Opus 5.5 <[email protected]>
166 lines
8.2 KiB
Rust
166 lines
8.2 KiB
Rust
//! The neural audio route over the committed vectors of `tests/vectors/neural_audio/`
|
||
//! (SPEC §5, *Payloads of the neural audio codecs*): one envelope each of
|
||
//! `wai.neural.encodec24`, `wai.neural.encodec48`, `wai.neural.dac16` and
|
||
//! `wai.neural.dac24`, pinned to a tiny decoder with its model's geometry, and the decode
|
||
//! `tools/wai_neural_audio_vectors.py` computes for it on its own (the second
|
||
//! implementation). No model download: every input is committed.
|
||
//!
|
||
//! Each vector decodes through the dispatch to its expected samples, at its model's
|
||
//! sample rate and channel count; a payload outside the model is refused before the
|
||
//! decoder runs; a decoder the pin does not name is refused; the candidate
|
||
//! `wai.neural.soundstream` is never dispatched; and the C ABI, which has no form for
|
||
//! two channels, refuses the stereo capability rather than return it as mono.
|
||
|
||
#![cfg(feature = "neural")]
|
||
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use serde_json::Value;
|
||
use wai::codecs::{is_candidate, is_dispatchable, CAP_NEURAL_SOUNDSTREAM};
|
||
use wai::container::Wai;
|
||
use wai::neural::{decode_envelope, decode_payload, decodes_to_several_channels, DecodeError, Decoded, ModelRegistry};
|
||
|
||
const VECTORS: [&str; 4] = ["dac16", "dac24", "encodec24", "encodec48"];
|
||
|
||
fn dir(name: &str) -> PathBuf {
|
||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/neural_audio").join(name)
|
||
}
|
||
|
||
fn meta(name: &str) -> Value {
|
||
serde_json::from_slice(&std::fs::read(dir(name).join("meta.json")).unwrap()).unwrap()
|
||
}
|
||
|
||
fn envelope(name: &str) -> Wai {
|
||
Wai::from_bytes(&std::fs::read(dir(name).join("vector.wai")).unwrap()).unwrap()
|
||
}
|
||
|
||
fn registry(cap: &str, decoder_of: &str) -> ModelRegistry {
|
||
ModelRegistry::new().register(cap, dir(decoder_of).join("decoder.onnx"))
|
||
}
|
||
|
||
/// The samples `expected.f32` holds: index i where i % every == 0 or i lies in an overlap.
|
||
fn selected(m: &Value) -> Vec<usize> {
|
||
let n = m["n_samples"].as_u64().unwrap() as usize;
|
||
let every = m["every"].as_u64().unwrap() as usize;
|
||
let overlaps: Vec<(usize, usize)> = m["overlaps"].as_array().unwrap().iter()
|
||
.map(|o| (o[0].as_u64().unwrap() as usize, o[1].as_u64().unwrap() as usize)).collect();
|
||
(0..n).filter(|i| i % every == 0 || overlaps.iter().any(|&(a, b)| (a..b).contains(i))).collect()
|
||
}
|
||
|
||
fn invalid(r: Result<wai::neural::Dispatched, DecodeError>) -> String {
|
||
match r {
|
||
Err(DecodeError::InvalidPayload(why)) => why,
|
||
Err(e) => panic!("refused, but not as an invalid payload: {e}"),
|
||
Ok(_) => panic!("decoded"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn each_vector_decodes_to_the_samples_the_second_implementation_gives() {
|
||
for name in VECTORS {
|
||
let m = meta(name);
|
||
let cap = m["capability"].as_str().unwrap();
|
||
let channels = m["channels"].as_u64().unwrap() as usize;
|
||
let n = m["n_samples"].as_u64().unwrap() as usize;
|
||
let env = envelope(name);
|
||
assert_eq!(env.capability(), cap, "{name}");
|
||
// The envelope pins its decoder: the registry verifies the pin before the decode.
|
||
let d = decode_envelope(&env, ®istry(cap, name)).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||
assert_eq!(d.capability, cap);
|
||
let Decoded::Audio(a) = d.output else { panic!("{name}: not audio") };
|
||
assert_eq!(a.sample_rate as u64, m["sample_rate"].as_u64().unwrap(), "{name}");
|
||
assert_eq!(a.channels as usize, channels, "{name}");
|
||
assert_eq!(a.samples.len(), n * channels, "{name}: samples per channel");
|
||
assert_eq!(decodes_to_several_channels(cap), channels > 1, "{name}");
|
||
let raw = std::fs::read(dir(name).join("expected.f32")).unwrap();
|
||
let want: Vec<f32> = raw.chunks(4).map(|b| f32::from_le_bytes(b.try_into().unwrap())).collect();
|
||
let at = selected(&m);
|
||
assert_eq!(want.len(), at.len() * channels, "{name}: expected.f32");
|
||
let mut worst = 0f32;
|
||
for (k, &i) in at.iter().enumerate() {
|
||
for c in 0..channels {
|
||
worst = worst.max((a.samples[i * channels + c] - want[k * channels + c]).abs());
|
||
}
|
||
}
|
||
println!(" {name}: {n} samples × {channels} @ {} Hz, max |Δ| = {worst:.2e} over {} values", a.sample_rate, want.len());
|
||
assert!(worst <= 1e-5, "{name}: the route's decode differs from the second implementation's by {worst}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_payload_outside_its_model_is_refused_before_the_decoder_runs() {
|
||
let reg = registry("wai.neural.encodec24", "encodec24");
|
||
let with = |f: &dyn Fn(&mut Vec<u8>)| {
|
||
let mut e = envelope("encodec24");
|
||
f(&mut e.payload);
|
||
decode_envelope(&e, ®)
|
||
};
|
||
// Three codebooks is no operating point of EnCodec 24 kHz.
|
||
assert!(invalid(with(&|p| p[0..4].copy_from_slice(&3u32.to_le_bytes()))).contains("operating point"));
|
||
// The value that marks a codebook not used is no code.
|
||
assert!(invalid(with(&|p| p[12..14].copy_from_slice(&1024u16.to_le_bytes()))).contains("outside a codebook"));
|
||
// More samples than the frames give.
|
||
assert!(invalid(with(&|p| p[8..12].copy_from_slice(&1601u32.to_le_bytes()))).contains("samples per channel"));
|
||
assert!(invalid(with(&|p| { p.pop(); })).contains("its header gives"));
|
||
// EnCodec 48 kHz: a chunk's scale is finite and above 0.
|
||
let mut e = envelope("encodec48");
|
||
e.payload[16..20].copy_from_slice(&0f32.to_le_bytes());
|
||
assert!(invalid(decode_envelope(&e, ®istry("wai.neural.encodec48", "encodec48"))).contains("scale"));
|
||
// DAC 16 kHz: its decoder gives 8 samples fewer than the frames' 320 each.
|
||
let mut e = envelope("dac16");
|
||
e.payload[8..12].copy_from_slice(&(4u32 * 320 - 7).to_le_bytes());
|
||
assert!(invalid(decode_envelope(&e, ®istry("wai.neural.dac16", "dac16"))).contains("samples per channel"));
|
||
}
|
||
|
||
#[test]
|
||
fn a_decoder_the_pin_does_not_name_is_refused() {
|
||
// DAC 24 kHz's tiny decoder has EnCodec 24 kHz's input shape, but not its digest.
|
||
let r = decode_envelope(&envelope("encodec24"), ®istry("wai.neural.encodec24", "dac24"));
|
||
assert!(matches!(r, Err(DecodeError::PriorMismatch(_))), "{:?}", r.map(|d| d.capability));
|
||
}
|
||
|
||
#[test]
|
||
fn the_candidate_soundstream_is_never_dispatched() {
|
||
assert!(is_candidate(CAP_NEURAL_SOUNDSTREAM));
|
||
assert!(!is_dispatchable(CAP_NEURAL_SOUNDSTREAM));
|
||
// An envelope that names it, at a sink that holds a file for it, is inert there.
|
||
let mut env = envelope("encodec24");
|
||
env.manifest.model_requirement.capability = CAP_NEURAL_SOUNDSTREAM.to_string();
|
||
env.manifest.conditioning.prior = None;
|
||
let reg = registry(CAP_NEURAL_SOUNDSTREAM, "encodec24");
|
||
assert!(matches!(decode_envelope(&env, ®), Err(DecodeError::ModelNotRegistered(_))));
|
||
// And the route has no decoder for it.
|
||
let params = wai::neural::ParamSource::Unpinned(dir("encodec24").join("decoder.onnx"));
|
||
assert!(matches!(decode_payload(CAP_NEURAL_SOUNDSTREAM, &env.payload, ¶ms), Err(DecodeError::UnknownCapability(_))));
|
||
}
|
||
|
||
/// The C ABI has no form for two channels: the stereo capability is refused before
|
||
/// anything is decoded, never returned as mono; a mono one decodes.
|
||
#[cfg(feature = "ffi")]
|
||
#[test]
|
||
fn the_c_abi_refuses_two_channels_and_decodes_one() {
|
||
use std::ffi::{CStr, CString};
|
||
use wai::ffi::*;
|
||
use wai::ffi_neural::*;
|
||
let call = |name: &str| {
|
||
let env = std::fs::read(dir(name).join("vector.wai")).unwrap();
|
||
let path = CString::new(dir(name).join("decoder.onnx").to_str().unwrap()).unwrap();
|
||
let mut out = WaiBuffer { data: std::ptr::null_mut(), len: 0, cap: 0 };
|
||
let mut sr = 0u32;
|
||
let code = unsafe { wai_neural_decode_audio(env.as_ptr(), env.len(), path.as_ptr(), &mut out, &mut sr) };
|
||
// No error is set after a decode that succeeds: the pointer is null then.
|
||
let last = wai_last_error();
|
||
let err = if last.is_null() { String::new() } else { unsafe { CStr::from_ptr(last) }.to_string_lossy().into_owned() };
|
||
let n = out.len / 4;
|
||
if code == WAI_OK {
|
||
unsafe { wai_buffer_free(out) };
|
||
}
|
||
(code, err, n, sr)
|
||
};
|
||
let (code, err, _, _) = call("encodec48");
|
||
assert_eq!(code, WAI_ERR_CODEC, "{err}");
|
||
assert!(err.contains("no wai_decoded_t kind"), "{err}");
|
||
let (code, err, n, sr) = call("encodec24");
|
||
assert_eq!(code, WAI_OK, "{err}");
|
||
assert_eq!((n, sr), (1523, 24_000));
|
||
}
|