open-standards/wai/wai-web/src/score.rs
Transaction Science 172b7f64e0 wai: music as instructions — wai.audio.score (Phase 6)
The deepest audio lever: a score carries no recording, only note events.
A deterministic trig-free reference synth (phase-accumulator oscillator +
linear ADSR, all in the Fx floor) reconstructs a canonical i16 PCM
mixdown; conformance is mixdown-equivalence, the music counterpart of
spatial audio. A soundfont/neural instrument is the sink's presentation.

- wai-rs `score` feature: WSCR container + reference synth + mixdown/
  score hashes; corpus (scale/chord/arp) via wai_score_vectors; registered
  + tiered Lossless.
- wai-web ScoreSink + the /score browser sink (in-tab verify + Web Audio).
- extensions/score.md, SPEC §5 row, content-taxonomy music → ✅.

Green: wai-rs all features (68 lib + integration) + clippy, wai-web wasm.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 04:29:10 -04:00

66 lines
2.2 KiB
Rust

//! Browser sink for `wai.audio.score` — the music-as-instructions
//! extension. The sink reconstructs the score's canonical PCM mixdown
//! with the same deterministic Rust synth that made the corpus and
//! checks the hash (mixdown-equivalence); the PCM feeds a Web Audio
//! buffer for playback. A richer instrument (soundfont / neural) would
//! be the sink's presentation — the verified object is the reference mix.
use wasm_bindgen::prelude::*;
use wai::score::Score;
#[wasm_bindgen]
pub struct ScoreSink {
score: Score,
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
#[wasm_bindgen]
impl ScoreSink {
#[wasm_bindgen(constructor)]
pub fn new(wscr: &[u8]) -> Result<ScoreSink, JsError> {
Ok(ScoreSink { score: Score::from_bytes(wscr).map_err(|e| JsError::new(&e.to_string()))? })
}
pub fn sample_rate(&self) -> u32 {
self.score.contract.sample_rate
}
pub fn duration(&self) -> u64 {
self.score.contract.duration
}
pub fn note_count(&self) -> u32 {
self.score.notes.len() as u32
}
pub fn mixdown_hash_hex(&self) -> String {
hex(&self.score.mixdown_hash())
}
pub fn score_hash_hex(&self) -> String {
hex(&self.score.score_hash())
}
/// The canonical mono `i16` PCM — the mixdown-equivalence quantity,
/// and the samples a Web Audio buffer plays.
pub fn mixdown(&self) -> Box<[i16]> {
self.score.mixdown().into_boxed_slice()
}
/// Per-note display rows for a piano-roll view: 4 f32 per note —
/// `[start, dur, pitch_norm, gain]` where `pitch_norm` is derived
/// from the phase increment (∝ log frequency) for vertical placement.
pub fn notes(&self) -> Box<[f32]> {
let mut out = Vec::with_capacity(self.score.notes.len() * 4);
for n in &self.score.notes {
// phase_inc = freq/sr → relative pitch via its magnitude.
let inc = n.phase_inc.approx_f64().max(1e-9);
let pitch = (inc.ln() - (-9.0)) / 6.0; // rough [0,1] spread for display
out.push(n.start as f32);
out.push(n.dur as f32);
out.push(pitch as f32);
out.push(n.gain.approx_f64() as f32);
}
out.into_boxed_slice()
}
}