mod fixtures;
use fixtures::graph_of;
use sva_ast::Graph;
use sva_engine::{RenderConfig, Silent, Stream, StreamConfig, render, render_until_silent};
const RATE: u32 = 44_100;
const PIANO3: &str = "0.0014822 * chaigne_askenfelt(f0, vel=vel, b=max(1.4e-4, \
4.1e-4*pow(f0/262, 1.9)), strike_pos=0.12, hammer_mass=0.009, hammer_k=2e10*max(1, \
pow(f0/523, 0.6)), hammer_p=3, damp_dc=1.327*exp(0.394*log(f0/262) - \
0.12*log(f0/262)*log(f0/262)), damp_freq=0.00044109413838472024, unison_count=3, \
detune=1, bridge_coupling=97.2*pow(f0/262, 0.677), bridge_mass=1.04*exp(0.466*log(f0/262) \
- 0.506*log(f0/262)*log(f0/262)), string1_cents=-0.2, string2_cents=0, string3_cents=0.25, \
string1_hammer_k_ratio=1, string2_hammer_k_ratio=0.8, string3_hammer_k_ratio=0.6)\n";
const VOICE: &str = "lowpass(sample(0.3*vel*(saw(f0*8ct) + saw(f0/8ct))*(crop(min(t/0.005, 1)*(0.6 + \
0.4*exp(-t/0.25)), 0s, release) + crop(min(release/0.005, 1)*(0.6 + \
0.4*exp(-release/0.25))*exp(-(t - release)/0.3), release, 3600s))), cutoff=min(f0*(2 + \
10*vel), 18000), q=0.9)\n";
const ECHO: &str = "feedback = 0.35\nx + feedback*self(t - 0.25s)\n";
fn composition() -> Graph {
graph_of(
"stream",
&[
("piano3", PIANO3),
("echo", ECHO),
("note", "@piano3(t, f0=261.63, vel=4.5)\n"),
("echoed", "@echo(t, x=@note)\n"),
(
"chain",
"lowpass(highpass(@note, cutoff=180, q=0.7), cutoff=2400, q=1.3)\n",
),
("saw", "0.3*saw(220*t)\n"),
("sawed", "lowpass(sample(@saw), cutoff=900, q=0.8)\n"),
("spectrum", "exp(0 - pow(f/300, 2))\n"),
("ahead", "@note(t + 0.01s)\n"),
("string", "chaigne_askenfelt(f0, release=release)\n"),
("damped", "@string(t, f0=523.25, release=0.05)\n"),
("held", "sin(2*pi*220*t)\n"),
("bar", "chaigne_doutaut(440)\n"),
("slow", "sin(2*pi*220*t)*exp(0 - 0.5*t)\n"),
(
"keyed",
"lowpass(sample(@saw*(crop(1, 0s, release) + crop(exp(0 - (t - release)/0.01), \
release, 60s))), cutoff=900, q=0.8)\n",
),
("keyed_up", "@keyed(t, release=0.05)\n"),
("voice", VOICE),
("low", "@voice(t, f0=65.406, vel=0.8, release=0.1)\n"),
("high", "@voice(t, f0=1046.502, vel=0.8, release=0.1)\n"),
(
"released",
"@voice(t, f0=1046.502, vel=0.8, release=0.05)\n",
),
("tone", "0.3*saw(220)\n"),
("clipped", "crop(0.3*saw(220), 0s, 0.1s)\n"),
],
)
}
fn streamed(g: &Graph, target: &str, block: usize, samples: usize) -> Vec<f64> {
let config = StreamConfig {
rate: RATE,
block,
silent: None,
};
let mut stream = Stream::open(g, target, &[], config).unwrap_or_else(|e| panic!("{e}"));
let mut out = Vec::with_capacity(samples + block);
while out.len() < samples {
let block = stream.next_block().unwrap_or_else(|e| panic!("{e}"));
out.extend_from_slice(block.expect("a stream with no end").plane(0));
}
out.truncate(samples);
out
}
fn whole(g: &Graph, target: &str, samples: usize) -> Vec<f64> {
let secs = samples as f64 / f64::from(RATE);
let held = render(g, target, RenderConfig::seconds(RATE, secs), None)
.unwrap_or_else(|e| panic!("{target}: {e}"));
let id = held.id(target).expect("the root");
held.buffer(id).expect("a buffer").plane(0).to_vec()
}
fn sounds(samples: &[f64]) {
assert!(samples.iter().any(|v| *v != 0.0), "silence tests nothing");
}
#[test]
fn a_streamed_piano_note_is_the_whole_render_bit_for_bit() {
let g = composition();
let samples = 9_000;
let want = whole(&g, "note", samples);
sounds(&want);
for block in [128, 1_000] {
assert_eq!(
streamed(&g, "note", block, samples),
want,
"blocks of {block}"
);
}
}
#[test]
fn an_echo_over_a_streamed_note_is_the_whole_render_bit_for_bit() {
let g = composition();
let samples = 16_000;
let want = whole(&g, "echoed", samples);
assert_ne!(want[11_100..], whole(&g, "note", samples)[11_100..]);
assert_eq!(streamed(&g, "echoed", 1_024, samples), want);
}
#[test]
fn a_biquad_chain_over_a_streamed_note_is_the_whole_render_bit_for_bit() {
let g = composition();
let samples = 6_000;
let want = whole(&g, "chain", samples);
sounds(&want);
assert_eq!(streamed(&g, "chain", 777, samples), want);
}
#[test]
fn a_streamed_closed_form_is_the_same_in_blocks_of_any_size() {
let g = composition();
let samples = 5_000;
for target in ["saw", "sawed", "spectrum"] {
let one = streamed(&g, target, samples, samples);
sounds(&one);
for block in [1, 64, 1_023] {
assert_eq!(
streamed(&g, target, block, samples),
one,
"{target} in {block}"
);
}
}
}
#[test]
fn a_streamed_synth_voice_is_the_whole_render_bit_for_bit_in_blocks_of_any_size() {
let g = composition();
let samples = 10_000;
for target in ["low", "high"] {
let want = whole(&g, target, samples);
sounds(&want);
for block in [1, 64, 441, 5_000] {
assert_eq!(
streamed(&g, target, block, samples),
want,
"{target} in blocks of {block}"
);
}
}
}
#[test]
fn a_released_voice_or_a_clipped_saw_ends_before_any_sample_brute_force_hears() {
for target in ["released", "clipped"] {
ends_before_brute_force_hears(target);
}
}
fn ends_before_brute_force_hears(target: &str) {
let g = composition();
let silent = Silent {
bits: 24,
max_secs: 10.0,
};
let config = StreamConfig {
rate: RATE,
block: 441,
silent: Some(silent),
};
let mut stream = Stream::open(&g, target, &[], config).expect("a stream opens");
let mut heard = Vec::new();
while let Some(block) = stream.next_block().expect("a block") {
heard.extend_from_slice(block.plane(0));
}
let end = stream.end().expect("a proven end");
assert_eq!(heard.len(), end);
let brute = whole(&g, target, 2 * end);
assert_eq!(heard[..], brute[..end], "{target}");
let last = brute.iter().rposition(|v| v.abs() >= silent.threshold());
assert!(
last.is_some_and(|at| at < end),
"{target} heard at {last:?}, past {end}"
);
}
#[test]
fn a_bound_name_reaches_the_target_as_a_named_argument() {
let g = composition();
let config = StreamConfig {
rate: RATE,
block: 2_000,
silent: None,
};
let mut stream = Stream::open(
&g,
"piano3",
&[("f0".into(), 261.63), ("vel".into(), 4.5)],
config,
)
.expect("a bound stream");
let first = stream.next_block().expect("a block").expect("no end");
assert_eq!(first.plane(0), &whole(&g, "note", 2_000)[..]);
}
#[test]
fn a_node_no_block_reads_alone_refuses_the_stream() {
let g = composition();
let config = StreamConfig {
rate: RATE,
block: 256,
silent: None,
};
let refused = Stream::open(&g, "ahead", &[], config)
.err()
.expect("a read ahead refuses");
assert_eq!(refused.code(), "engine.no_stream", "{refused}");
for name in ["f0) + (1", "2x"] {
let refused = Stream::open(&g, "piano3", &[(name.into(), 1.0)], config)
.err()
.expect("a name that is no word refuses");
assert_eq!(refused.code(), "engine.no_stream", "{refused}");
}
let refused = Stream::open(&g, "piano3", &[("f0".into(), f64::NAN)], config)
.err()
.expect("a value that is no number refuses");
assert_eq!(refused.code(), "engine.no_stream", "{refused}");
}
const SILENT: Silent = Silent {
bits: 16,
max_secs: 10.0,
};
#[test]
fn a_stream_until_silent_ends_where_the_bound_proves_it() {
let g = composition();
let config = StreamConfig {
rate: RATE,
block: 4_096,
silent: Some(SILENT),
};
let mut stream = Stream::open(&g, "damped", &[], config).expect("a proven end");
let mut heard = Vec::new();
while let Some(block) = stream.next_block().expect("a block") {
heard.extend_from_slice(block.plane(0));
}
assert_eq!(Some(heard.len()), stream.end());
let whole = render_until_silent(
&g,
"damped",
RenderConfig::seconds(RATE, SILENT.max_secs),
SILENT,
None,
)
.expect("a silent render");
let want = whole.buffer(whole.root).expect("a buffer").plane(0);
sounds(want);
assert_eq!(heard[..want.len()], *want);
assert!(
heard[want.len()..]
.iter()
.all(|v| v.abs() < SILENT.threshold())
);
}
#[test]
fn a_stream_whose_silence_is_never_proven_refuses_at_its_first_block() {
let g = composition();
let config = StreamConfig {
rate: RATE,
block: 256,
silent: Some(SILENT),
};
for (target, code) in [
("held", "engine.never_silent"),
("tone", "engine.never_silent"),
("bar", "engine.no_tail_bound"),
] {
let mut stream = Stream::open(&g, target, &[], config).expect("no proof at the opening");
let refused = stream.next_block().err().expect("no proof");
assert_eq!(refused.code(), code, "{target}: {refused}");
assert_eq!(stream.position(), config.block, "{target}");
}
}
#[test]
fn a_filtered_stream_until_silent_is_proven_from_its_blocks() {
let g = composition();
let config = StreamConfig {
rate: RATE,
block: 441,
silent: Some(SILENT),
};
let mut stream = Stream::open(&g, "keyed_up", &[], config).expect("a filter opens");
let mut heard = Vec::new();
while let Some(block) = stream.next_block().expect("a block") {
heard.extend_from_slice(block.plane(0));
}
assert_eq!(Some(heard.len()), stream.end());
assert!(
heard.len() > 2_205,
"an end before the key-up at {}",
heard.len()
);
let want = whole(&g, "keyed_up", heard.len());
sounds(&want);
assert_eq!(heard, want);
let held = StreamConfig {
silent: Some(Silent {
max_secs: 0.05,
..SILENT
}),
..config
};
let mut stream = Stream::open(&g, "sawed", &[], held).expect("a held filter opens");
let refused = loop {
match stream.next_block() {
Ok(Some(_)) => {}
Ok(None) => panic!("a held saw proven silent"),
Err(e) => break e,
}
};
assert_eq!(refused.code(), "engine.not_silent_by", "{refused}");
}
#[test]
fn a_stream_its_state_does_not_prove_silent_by_max_secs_refuses_there() {
let g = composition();
let silent = Silent {
bits: 24,
max_secs: 0.1,
};
let config = StreamConfig {
rate: RATE,
block: 256,
silent: Some(silent),
};
let mut stream = Stream::open(&g, "slow", &[], config).expect("a decay opens");
let limit = (silent.max_secs * f64::from(RATE)).ceil() as usize;
let refused = loop {
let at = stream.position();
match stream.next_block() {
Ok(Some(_)) => assert!(at < limit, "a block from {at}, past {limit}"),
Ok(None) => panic!("a slow decay proven silent by {}", stream.position()),
Err(e) => break e,
}
};
assert_eq!(refused.code(), "engine.not_silent_by", "{refused}");
assert!(stream.position() >= limit && stream.end().is_none());
}
#[test]
fn a_stream_asked_silent_by_its_opening_refuses_before_any_block() {
let g = composition();
let config = StreamConfig {
rate: RATE,
block: 256,
silent: Some(Silent {
bits: 24,
max_secs: 0.0,
}),
};
let mut stream = Stream::open(&g, "slow", &[], config).expect("a decay opens");
let refused = stream.next_block().err().expect("no block before a proof");
assert_eq!(refused.code(), "engine.not_silent_by", "{refused}");
let text = refused.to_string();
assert!(text.contains("no block has ended by 0s"), "{text}");
assert_eq!(stream.position(), 0);
}