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]>
66 lines
2.2 KiB
Rust
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()
|
|
}
|
|
}
|