use std::cell::RefCell;
use crate::fixtures::{Now, added, edited, graph_of, next, removed, replaced};
use sva_ast::Graph;
use sva_engine::{Change, Changed, Placed, change};
use sva_engine::{Range, RenderConfig, Stream, StreamConfig, Tier, render};
const RATE: u32 = 8_000;
const BLOCK: usize = 256;
const PIANO: &str = "release = inf\n0.0014822 * chaigne_askenfelt(f0, vel=vel, \
damper_r=0.1*pow(262/f0, 2)*crop(min(1, (t - release)/0.03), release, inf), \
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 PLUCK: &str = "release = inf\nchaigne_askenfelt(f0, damper_r=0.1*pow(262/f0, 2)\
*crop(min(1, (t - release)/0.03), release, inf))\n";
const ECHO: &str = "feedback = 0.35\nsample(x) + feedback*self[idx(t - 0.25s)]\n";
fn composition(released: f64) -> Graph {
graph_of(
"edit",
&[
("piano", PIANO),
("echo", ECHO),
("pluck", PLUCK),
(
"key",
"release = inf\n@echo(t, x=@piano(t, f0=261.63, vel=4.5, release=release))\n",
),
("released", &format!("@key(t, release={released})\n")),
(
"note",
"release = inf\n@piano(t, f0=261.63, vel=4.5, release=release)\n",
),
("played", &format!("@note(t, release={released})\n")),
(
"toned",
"release = inf\nlowpass(highpass(@note(t, release=release), cutoff=180, q=0.7), cutoff=2400, \
q=1.3)\n",
),
("toned_up", &format!("@toned(t, release={released})\n")),
(
"string",
"release = inf\n@pluck(t, f0=523.25, release=release)\n",
),
("struck", &format!("@string(t, release={released})\n")),
(
"doubled",
"release = inf\n@string(t, release=release) + 0.5*@string(t - 0.3s, release=release)\n",
),
("doubled_up", &format!("@doubled(t, release={released})\n")),
("burst", "crop(sin(2*pi*440*t), 0s, 0.05s)\n"),
(
"blip",
"crop(lowpass(sample(sin(2*pi*f0*t)), cutoff=2000, q=0.7), 0s, 0.4s)\n",
),
],
)
}
fn config_at(rate: u32, end: Option<i64>) -> StreamConfig {
StreamConfig {
block: BLOCK,
channels: None,
render: RenderConfig {
range: Range {
start: Some(0),
end,
},
..RenderConfig::at(rate)
},
}
}
fn expr(text: &str) -> sva_ast::Expr {
sva_ast::parse_expr(text).unwrap_or_else(|e| panic!("`{text}`: {}", e.message))
}
fn opened(g: &Graph, text: &str, cache: &Tier) -> RefCell<Stream> {
opened_at(RATE, g, text, cache)
}
fn opened_at(rate: u32, g: &Graph, text: &str, cache: &Tier) -> RefCell<Stream> {
let end = Some(4 * i64::from(rate));
let opened = Stream::open(g, &expr(text), config_at(rate, end), cache).now();
RefCell::new(opened.unwrap_or_else(|e| panic!("{e}")))
}
fn edit(stream: &RefCell<Stream>, g: &Graph, text: &str) {
edited(stream, g, &expr(text), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("`{text}`: {e}"));
}
fn blocks(stream: &RefCell<Stream>, count: usize) -> Vec<f64> {
let mut out = Vec::with_capacity(count * BLOCK);
for _ in 0..count {
let block = next(&mut stream.borrow_mut()).unwrap_or_else(|e| panic!("{e}"));
out.extend_from_slice(block.expect("a stream with no end").plane(0));
}
out
}
fn whole(g: &Graph, target: &str, samples: usize) -> Vec<f64> {
whole_at(RATE, g, target, samples)
}
fn whole_at(rate: u32, 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),
&Tier::default(),
)
.unwrap_or_else(|e| panic!("{target}: {e}"));
let id = held.id(target).expect("the root");
held.output(id).expect("a buffer").plane(0).to_vec()
}
fn released_at(target: &str, whole_target: &str, k: usize) {
released_at_rate(RATE, target, whole_target, k);
}
fn released_at_rate(rate: u32, target: &str, whole_target: &str, k: usize) {
let release = k as f64 / f64::from(rate);
let g = composition(release);
let held = opened_at(rate, &g, &format!("@{target}(t)"), &Tier::default());
let mut heard = blocks(&held, k / BLOCK);
let released = opened_at(rate, &g, &format!("@{target}(t)"), &Tier::default());
blocks(&released, k / BLOCK);
edit(&released, &g, &format!("@{target}(t, release={release})"));
heard.extend(blocks(&released, 8));
let want = whole_at(rate, &g, whole_target, heard.len());
assert_ne!(
want[k + 2 * BLOCK..],
blocks(&held, 8)[2 * BLOCK..],
"{target}: the damper did nothing"
);
assert_eq!(heard, want, "{target} at {rate}");
}
#[test]
fn key_up_at_another_rate_is_the_whole_render_released_there() {
released_at_rate(11_025, "string", "struck", 3 * BLOCK);
}
#[test]
fn key_up_over_an_echo_is_the_whole_render_released_there() {
released_at("key", "released", 12 * BLOCK);
}
#[test]
fn key_up_of_a_unison_under_filters_is_the_whole_render_released_there() {
released_at("note", "played", 22 * BLOCK);
released_at("toned", "toned_up", 22 * BLOCK);
}
#[test]
fn key_up_of_a_string_read_again_later_is_the_whole_render_released_there() {
released_at("string", "struck", 3 * BLOCK);
released_at("doubled", "doubled_up", 3 * BLOCK);
}
#[test]
fn a_note_pressed_and_released_mid_stream_is_the_whole_render_of_the_last_edit() {
let (t0, t1, t2) = (5 * BLOCK, 9 * BLOCK, 12 * BLOCK);
let release = (t1 - t0) as f64 / f64::from(RATE);
let first = "@pluck(t, f0=440)";
let pressed = format!("{first} + @pluck(t - {t0}sp, f0=660)");
let released = format!("{first} + @pluck(t - {t0}sp, f0=660, release={release})");
let last = format!("{released} + @pluck(t - {t2}sp, f0=440)");
let g = composition(1.0);
let echoed = |x: &str| format!("@echo(t, x={x})");
let stream = opened(&g, &echoed(first), &Tier::default());
let mut heard = blocks(&stream, t0 / BLOCK);
edit(&stream, &g, &echoed(&pressed));
heard.extend(blocks(&stream, (t1 - t0) / BLOCK));
edit(&stream, &g, &echoed(&released));
heard.extend(blocks(&stream, (t2 - t1) / BLOCK));
edit(&stream, &g, &echoed(&last));
heard.extend(blocks(&stream, 16));
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr(&echoed(&last))));
let want = whole(&whole_g, "final", heard.len());
assert_ne!(
want,
whole(&g, "string", heard.len()),
"the press was heard"
);
assert_eq!(heard, want);
}
#[test]
fn a_note_past_its_end_leaves_the_sum_and_its_echo_rings_on() {
let g = composition(1.0);
let stream = opened(&g, "@echo(t, x=@notes)", &Tier::default());
let a = format!("@blip(t - {BLOCK}sp, f0=200)");
let b = format!("@blip(t - {}sp, f0=300)", 8 * BLOCK);
let add = |term: &str| {
added(&stream, &g, &expr(term), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("`{term}`: {e}"))
};
let (held_a, held_b) = (add(&a), add(&b));
let mut heard = blocks(&stream, 20);
assert_eq!(
removed(&stream, held_a, &Tier::default()).now().ok(),
Some(false),
"the first note ended"
);
heard.extend(blocks(&stream, 10));
let c = format!("@blip(t - {}sp, f0=250)", 30 * BLOCK);
add(&c);
assert_eq!(
removed(&stream, held_b, &Tier::default()).now().ok(),
Some(false),
"the second note ended"
);
heard.extend(blocks(&stream, 30));
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr(&format!("@echo(t, x={a} + {b} + {c})"))));
let want = whole(&whole_g, "final", heard.len());
let gap = 8 * BLOCK + (0.4 * f64::from(RATE)) as usize..30 * BLOCK;
assert!(
heard[gap].iter().any(|v| *v != 0.0),
"the echo rings between notes"
);
assert_eq!(heard, want);
}
#[test]
fn a_term_removed_while_it_sounds_is_cut_where_the_stream_stands() {
let g = composition(1.0);
let stream = opened(&g, "@echo(t, x=@notes)", &Tier::default());
let add = |term: &str| {
added(&stream, &g, &expr(term), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("`{term}`: {e}"))
};
let (a, b) = (
format!("@blip(t - {BLOCK}sp, f0=200)"),
format!("@blip(t - {}sp, f0=300)", 4 * BLOCK),
);
let first = add(&a);
add(&b);
let mut heard = blocks(&stream, 6);
assert_eq!(
removed(&stream, first, &Tier::default()).now().ok(),
Some(true)
);
assert_eq!(
removed(&stream, first, &Tier::default()).now().ok(),
Some(false),
"a removed handle"
);
assert_eq!(
stream.borrow().counts().terms,
1,
"the cut term left the sum"
);
heard.extend(blocks(&stream, 30));
let cut = (6 * BLOCK) as f64 / f64::from(RATE);
let mut whole_g = g.clone();
let last = format!("@echo(t, x=crop({a}, -inf, {cut}s) + {b})");
assert!(whole_g.define("final", expr(&last)));
assert_eq!(heard, whole(&whole_g, "final", heard.len()), "{last}");
}
#[test]
fn a_live_edit_starts_a_node_with_no_state_silent_and_names_it() {
let g = composition(1.0);
let cache = Tier::default();
cache.set_mark_every(BLOCK);
render(&g, "string", RenderConfig::seconds(RATE, 0.5), &cache).expect("a render");
let held = "@echo(t, x=@pluck(t, f0=523.25))";
let released = "@echo(t, x=@pluck(t, f0=523.25, release=0.1))";
let (exact, live) = (opened(&g, held, &cache), opened(&g, held, &cache));
live.borrow_mut().go_live();
let before = blocks(&live, 8);
assert_eq!(before, blocks(&exact, 8));
edit(&exact, &g, released);
edit(&live, &g, released);
assert!(exact.borrow().dropped().is_empty());
assert_eq!(
live.borrow().dropped().len(),
1,
"{:?}",
live.borrow().dropped()
);
assert!(
live.borrow().dropped()[0].starts_with("pluck("),
"{:?}",
live.borrow().dropped()
);
assert_ne!(blocks(&live, 4), blocks(&exact, 4));
}
#[test]
fn a_constant_moved_inside_a_loop_keeps_its_tail_ringing() {
let g = composition(1.0);
let stream = opened(
&g,
"@echo(t, x=sample(@burst), feedback=0.35)",
&Tier::default(),
);
let k = 13 * BLOCK;
let mut heard = blocks(&stream, k / BLOCK);
edit(&stream, &g, "@echo(t, x=sample(@burst), feedback=0.5)");
heard.extend(blocks(&stream, 12));
let delay = RATE as usize / 4;
assert!(heard[k..].iter().any(|v| *v != 0.0), "the tail rings on");
for n in k..heard.len() {
assert_eq!(heard[n], 0.5 * heard[n - delay], "sample {n}");
}
}
#[test]
fn a_stateful_target_edited_in_place_keeps_its_tail_ringing() {
let g = composition(1.0);
let loop_at = |feedback: f64| format!("sample(@burst) + {feedback}*self[idx(t - 0.25s)]");
let stream = opened(&g, &loop_at(0.35), &Tier::default());
let k = 13 * BLOCK;
let mut heard = blocks(&stream, k / BLOCK);
edit(&stream, &g, &loop_at(0.5));
heard.extend(blocks(&stream, 12));
let delay = RATE as usize / 4;
assert!(heard[k..].iter().any(|v| *v != 0.0), "the tail rings on");
for n in k..heard.len() {
assert_eq!(heard[n], 0.5 * heard[n - delay], "sample {n}");
}
}
#[test]
fn a_long_session_holds_no_more_than_its_first_seconds() {
let rate = RATE;
let g = composition(1.0);
let config = StreamConfig {
block: 256,
channels: None,
render: RenderConfig {
range: Range {
start: Some(0),
end: Some(3_600 * i64::from(rate)),
},
..RenderConfig::at(rate)
},
};
let stream = Stream::open(&g, &expr("@echo(t, x=0)"), config, &Tier::default()).now();
let stream = RefCell::new(stream.expect("opens"));
let (every, kept) = (8 * 256, 2 * i64::from(rate));
let mut onsets: Vec<i64> = Vec::new();
let mut held = Vec::new();
while stream.borrow().position() < 60 * i64::from(rate) {
let now = stream.borrow().position();
if now % every == 0 {
onsets.retain(|onset| now - onset < kept);
onsets.push(now);
let notes: Vec<String> = onsets
.iter()
.map(|at| format!("@blip(t - {at}sp, f0={})", 200 + at % 7 * 50))
.collect();
edit(&stream, &g, &format!("@echo(t, x={})", notes.join(" + ")));
}
let block = next(&mut stream.borrow_mut()).expect("a block");
block.expect("no end");
held.push(stream.borrow().held_bytes());
}
let seconds = |s: usize| s * rate as usize / 256;
let first = held[..seconds(10)].iter().max().expect("ten seconds");
let last = held[seconds(50)..].iter().max().expect("ten more");
assert!(last <= first, "{last} bytes held late, {first} early");
}
#[test]
fn a_shifted_term_reads_the_note_a_render_left_in_memory() {
let g = composition(1.0);
let cache = Tier::default();
render(&g, "string", RenderConfig::seconds(RATE, 0.2), &cache).expect("a render");
let answered = |stream: &RefCell<Stream>| {
let stats = stream.borrow().stats();
let lookups = stats.lookups.iter();
lookups
.filter(|l| l.node.starts_with("pluck") && l.store == Some(true))
.count()
};
let later = "@echo(t, x=@pluck(t - 2000sp, f0=523.25))";
let (cold, warm) = (
opened(&g, later, &Tier::default()),
opened(&g, later, &cache),
);
assert_eq!(blocks(&warm, 20), blocks(&cold, 20));
assert_eq!(answered(&warm), 1, "{:?}", warm.borrow().stats());
let twice = "@pluck(t - 1000sp, f0=523.25) + @pluck(t - 3000sp, f0=523.25)";
let (cold, warm) = (
opened(&g, twice, &Tier::default()),
opened(&g, twice, &cache),
);
assert_eq!(blocks(&warm, 16), blocks(&cold, 16));
assert_eq!(
answered(&warm),
1,
"memory answers the note once for both reads: {:?}",
warm.borrow().stats()
);
}
const COMB: &str = "delay = 0.0297\ng = 0.8\nx + g*self[idx(t - delay)]\n";
const ALLPASS: &str = "delay = 1/300\ng = 0.7\n@comb(t - delay, x=x, delay=delay, g=g) - g*@comb(t, x=x, delay=delay, g=g)\n";
const HALL: &str = "size = 1\ndecay = 0.8\n@allpass(t, x=@allpass(t, x=0.25*(@comb(t, x=x, \
delay=0.0297*size, g=decay) + @comb(t, x=x, delay=0.0371*size, g=decay)), delay=2/300, g=0.7), \
delay=1/300, g=0.7)\n";
const SPACE: &str = "mix = 0.3\ndry = x\n(1 - mix)*x + mix*@hall(t, x=dry, size=1, decay=0.8)\n";
fn reverb() -> Graph {
graph_of(
"reverb",
&[
("comb", COMB),
("allpass", ALLPASS),
("hall", HALL),
("space", SPACE),
(
"blip",
"crop(lowpass(sample(sin(2*pi*f0*t)), cutoff=2000, q=0.7), 0s, 0.4s)\n",
),
],
)
}
fn added_under(master: &dyn Fn(&str) -> String) {
let g = reverb();
let stream = opened(&g, &master("@notes"), &Tier::default());
stream.borrow_mut().go_live();
let a = format!("@blip(t - {BLOCK}sp, f0=200)");
let b = format!("@blip(t - {}sp, f0=300)", 8 * BLOCK);
added(&stream, &g, &expr(&a), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
let mut heard = blocks(&stream, 8);
added(&stream, &g, &expr(&b), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
heard.extend(blocks(&stream, 12));
assert!(
stream.borrow().dropped().is_empty(),
"{:?}",
stream.borrow().dropped()
);
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr(&master(&format!("{a} + {b}")))));
assert_eq!(heard, whole(&whole_g, "final", heard.len()));
}
#[test]
fn a_note_added_under_a_reverb_keeps_its_tail_ringing() {
added_under(&|x| format!("@space(t, x=sample({x}), dry=sample({x}), mix=0.3)"));
}
#[test]
fn a_note_added_under_parallel_combs_reads_each_comb_s_own_past() {
added_under(&|x| format!("@hall(t, x=sample({x}))"));
}
#[test]
fn a_loop_edited_to_read_further_back_than_it_kept_steps_again_or_starts_silent() {
let g = reverb();
let comb = |delay: f64| format!("@comb(t, x=sample(@blip(t, f0=200)), delay={delay})");
let k = 4 * BLOCK;
let exact = opened(&g, &comb(0.0297), &Tier::default());
let mut heard = blocks(&exact, k / BLOCK);
edit(&exact, &g, &comb(0.0371));
heard.extend(blocks(&exact, 8));
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr(&comb(0.0371))));
let want = whole(&whole_g, "final", heard.len());
assert_eq!(heard[k..], want[k..]);
let live = opened(&g, &comb(0.0297), &Tier::default());
live.borrow_mut().go_live();
blocks(&live, k / BLOCK);
edit(&live, &g, &comb(0.0371));
blocks(&live, 8);
assert_eq!(
live.borrow().dropped().len(),
1,
"{:?}",
live.borrow().dropped()
);
}
#[test]
fn a_reverb_over_the_bare_note_sum_opens_with_no_notes_and_plays_them() {
added_under(&|x| format!("@space(t, x={x}, dry={x}, mix=0.3)"));
}
const PIANO4: &str = "release = inf\nvel = 0.5\n0.0014822 * chaigne_askenfelt(f0, vel=1 + 5*vel, \
damper_r=0.1*pow(262/f0, 2)*min(1, max(0, 1 + 17.312340490667562*log(1318.5102276514797/f0)))\
*crop(max(0, min(1, (t - release)/0.03)), release, inf))\n";
#[test]
fn key_up_at_a_whole_sample_opens_a_clamped_damper_ramp_at_zero() {
let rate = 48_000;
let (k, after) = (12 * BLOCK, 4 * BLOCK);
let g = graph_of("key-up-sp", &[("piano4", PIANO4)]);
let held = "@piano4(t, f0=293.6648, vel=0.7087)";
let released = format!("@piano4(t, f0=293.6648, vel=0.7087, release={k}sp)");
let mut whole_g = g.clone();
assert!(whole_g.define("released", expr(&released)));
let want = whole_at(rate, &whole_g, "released", k + after);
let stream = opened_at(rate, &g, &released, &Tier::default());
assert_eq!(blocks(&stream, (k + after) / BLOCK), want, "streamed");
let live = opened_at(rate, &g, "@notes", &Tier::default());
let note = added(&live, &g, &expr(held), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
let mut heard = blocks(&live, k / BLOCK);
let replaced = replaced(&live, &g, (note, &expr(&released)), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
assert!(replaced, "the held note is still sounding");
heard.extend(blocks(&live, after / BLOCK));
assert_eq!(heard, want, "replaced live");
}
const PAD: &str = "release = inf\ncrop(sin(2*pi*f0*t)*exp(-t/0.25), 0s, release) + \
crop(exp(-release/0.25)*sin(2*pi*f0*t)*exp(-(t - release)/0.05), release, 3600s)\n";
#[test]
fn a_removed_term_leaves_the_sum_once_the_stream_passes_its_cut() {
let g = graph_of("pads", &[("pad", PAD)]);
let config = StreamConfig {
block: BLOCK,
channels: None,
render: RenderConfig {
range: Range {
start: Some(0),
end: Some(3_600 * i64::from(RATE)),
},
..RenderConfig::at(RATE)
},
};
let stream = Stream::open(&g, &expr("@notes"), config, &Tier::default()).now();
let stream = RefCell::new(stream.expect("opens"));
let fade = (0.2 * f64::from(RATE)) as i64;
let mut sounding: Vec<(sva_engine::Handle, i64)> = Vec::new();
for i in 0..300 {
let onset = stream.borrow().position();
let pad = format!("@pad(t - {onset}sp, f0={})", 200 + i % 4 * 50);
let note = added(&stream, &g, &expr(&pad), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
blocks(&stream, 2);
let up = stream.borrow().position();
let released = format!(
"@pad(t - {onset}sp, f0={}, release={}sp)",
200 + i % 4 * 50,
up - onset
);
let held = replaced(&stream, &g, (note, &expr(&released)), &Tier::default()).now();
assert_eq!(held.ok(), Some(true), "note {i} is held");
sounding.push((note, up + fade));
blocks(&stream, 2);
let now = stream.borrow().position();
while sounding.first().is_some_and(|(_, ends)| *ends <= now) {
let (gone, _) = sounding.remove(0);
let removed = removed(&stream, gone, &Tier::default()).now().ok();
assert_eq!(removed, Some(true), "note {i}'s predecessor");
}
let terms = stream.borrow().counts().terms;
assert!(
terms <= sounding.len(),
"{terms} terms after note {i}, {} sounding",
sounding.len()
);
}
}
#[test]
fn a_note_whose_crop_the_stream_passed_leaves_the_sum_unremoved() {
let g = graph_of(
"cropped",
&[
("pad", PAD),
("tone", "crop(sin(2*pi*f0*t), 0s, 0.05s)\n"),
(
"blip",
"crop(lowpass(sample(sin(2*pi*f0*t)), cutoff=2000, q=0.7), 0s, 0.05s)\n",
),
],
);
let stream = opened(&g, "@notes", &Tier::default());
let add = |term: &str| {
added(&stream, &g, &expr(term), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("`{term}`: {e}"))
};
add("@pad(t, f0=200)");
blocks(&stream, 2);
let terms = || stream.borrow().counts().terms;
for node in ["tone", "blip"] {
let at = stream.borrow().position();
add(&format!("@{node}(t - {at}sp, f0=300)"));
assert_eq!(terms(), 2, "{node} sounds");
blocks(&stream, 1);
assert_eq!(terms(), 2, "{node} sounds on");
blocks(&stream, 1);
assert_eq!(terms(), 1, "{node} ended");
}
let at = stream.borrow().position();
add(&format!("crop(@pad(t, f0=300), 0s, {}sp)", at - 1));
assert_eq!(terms(), 1, "a note cropped into the past");
}
#[test]
fn a_term_placed_at_its_landing_plays_from_its_sample_zero_there() {
let g = graph_of("landing", &[("pad", PAD), ("note", "@pad(t, f0=300)\n")]);
let stream = opened(&g, "@notes", &Tier::default());
let mut heard = blocks(&stream, 5);
let held = expr("@pad(t, f0=300)");
let build = |_: &Stream| {
Ok::<_, sva_engine::EngineError>(Change::Add(g.clone(), held.clone(), Placed::Landing))
};
let Ok(Changed::Added(note)) = change(&stream, build, &Tier::default()).now() else {
panic!("an add answers its handle");
};
let landed = 5 * BLOCK as i64;
assert_eq!(stream.borrow().landed(note), Some(landed));
heard.extend(blocks(&stream, 2));
let up = expr(&format!("@pad(t, f0=300, release={}sp)", 2 * BLOCK));
let build = |_: &Stream| {
Ok::<_, sva_engine::EngineError>(Change::Replace(
note,
g.clone(),
up.clone(),
Placed::Landing,
))
};
assert_eq!(
change(&stream, build, &Tier::default()).now().ok(),
Some(Changed::Held(true))
);
heard.extend(blocks(&stream, 8));
let alone = whole(&g, "note", 2 * BLOCK);
assert!(alone.iter().any(|v| *v != 0.0));
assert_eq!(
heard[5 * BLOCK..7 * BLOCK],
alone[..],
"the attack is whole"
);
let mut whole_g = g.clone();
let last = format!("@pad(t - {landed}sp, f0=300, release={}sp)", 2 * BLOCK);
assert!(whole_g.define("final", expr(&last)));
assert_eq!(heard, whole(&whole_g, "final", heard.len()), "{last}");
}
#[test]
fn every_faded_note_leaves_the_sum() {
let g = composition(1.0);
let stream = opened(&g, "@echo(t, x=@notes)", &Tier::default());
stream.borrow_mut().go_live();
let a = format!("@blip(t - {BLOCK}sp, f0=200)");
added(&stream, &g, &expr(&a), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
let mut heard = blocks(&stream, 20);
assert_eq!(stream.borrow().counts().terms, 0, "the note faded");
edit(&stream, &g, "@echo(t, x=@notes)");
heard.extend(blocks(&stream, 4));
let b = format!("@blip(t - {}sp, f0=300)", 24 * BLOCK);
added(&stream, &g, &expr(&b), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
heard.extend(blocks(&stream, 20));
assert_eq!(stream.borrow().counts().terms, 0, "both notes faded");
assert!(
stream.borrow().dropped().is_empty(),
"{:?}",
stream.borrow().dropped()
);
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr(&format!("@echo(t, x={a} + {b})"))));
assert_eq!(heard, whole(&whole_g, "final", heard.len()));
}
#[test]
fn a_note_added_under_a_silent_master_lands() {
let g = composition(1.0);
let stream = opened(
&g,
&format!("crop(@notes, 0s, {BLOCK}sp)"),
&Tier::default(),
);
blocks(&stream, 2);
let build = |_: &Stream| {
let term = expr("@blip(t, f0=300)");
Ok::<_, sva_engine::EngineError>(Change::Add(g.clone(), term, Placed::Landing))
};
let Ok(Changed::Added(note)) = change(&stream, build, &Tier::default()).now() else {
panic!("an add answers its handle");
};
let at = 2 * BLOCK as i64;
assert_eq!(stream.borrow().landed(note), Some(at), "the note landed");
blocks(&stream, 12);
assert_eq!(stream.borrow().landed(note), Some(at), "the note sounds on");
blocks(&stream, 1);
assert_eq!(stream.borrow().counts().terms, 0, "the note ended");
}
#[test]
fn a_live_term_reading_a_stateful_value_at_a_moving_index_computes_it_whole() {
let g = graph_of(
"moving",
&[(
"looped",
"crop(lowpass(sample(saw(110*t)), cutoff=900, q=0.7), 0s, 0.1s)\n",
)],
);
let term = format!(
"crop(@looped[idx((t - {0}sp) % 800sp)], {0}sp, inf)",
8 * BLOCK
);
let stream = opened(&g, "@notes", &Tier::default());
stream.borrow_mut().go_live();
let mut heard = blocks(&stream, 4);
added(&stream, &g, &expr(&term), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
heard.extend(blocks(&stream, 16));
assert!(
stream.borrow().dropped().is_empty(),
"{:?}",
stream.borrow().dropped()
);
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr(&term)));
assert_eq!(heard, whole(&whole_g, "final", heard.len()));
}
#[test]
fn a_live_term_reading_a_value_twice_starts_it_where_the_earliest_read_stands() {
let g = composition(1.0);
let stream = opened(&g, "@notes", &Tier::default());
stream.borrow_mut().go_live();
let mut heard = blocks(&stream, 4);
let term = "@blip(t - 1000sp, f0=300) + @blip(t - 1500sp, f0=300)";
added(&stream, &g, &expr(term), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
heard.extend(blocks(&stream, 16));
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr(term)));
let whole = whole(&whole_g, "final", heard.len());
let now = 4 * BLOCK;
assert!(
whole[now..].iter().any(|v| *v != 0.0),
"silence tests nothing"
);
assert_eq!(heard[now..], whole[now..]);
}
fn widths() -> Graph {
graph_of(
"widths",
&[
("mono", "crop(sin(2*pi*200*t), 0s, 0.1s)\n"),
(
"pair",
"crop(join(sin(2*pi*300*t), sin(2*pi*301*t)), 0s, 0.1s)\n",
),
(
"three",
"crop(join(sin(2*pi*300*t), sin(2*pi*301*t), sin(2*pi*302*t)), 0s, 0.1s)\n",
),
],
)
}
fn opened_with(
g: &Graph,
text: &str,
channels: Option<usize>,
) -> Result<RefCell<Stream>, sva_engine::EngineError> {
let config = StreamConfig {
channels,
..config_at(RATE, Some(4 * i64::from(RATE)))
};
let mut stream = Stream::open(g, &expr(text), config, &Tier::default()).now()?;
stream.go_live();
Ok(RefCell::new(stream))
}
fn planes(stream: &RefCell<Stream>, count: usize) -> Vec<Vec<f64>> {
let mut out = vec![Vec::new(); stream.borrow().width()];
for _ in 0..count {
let block = next(&mut stream.borrow_mut()).unwrap_or_else(|e| panic!("{e}"));
let block = block.expect("a stream with no end");
assert_eq!(block.width(), out.len(), "a block of the stream's channels");
for (c, plane) in out.iter_mut().enumerate() {
plane.extend_from_slice(block.plane(c));
}
}
out
}
#[test]
fn what_is_wider_than_the_stream_is_refused() {
let g = widths();
let add = |stream: &RefCell<Stream>, term: &str| {
added(stream, &g, &expr(term), &Tier::default()).now()
};
let mono = opened_with(&g, "@notes", None).unwrap_or_else(|e| panic!("{e}"));
add(&mono, "@mono(t - 512sp)").unwrap_or_else(|e| panic!("{e}"));
let widened = add(&mono, "@pair(t - 1024sp)").expect_err("a stereo term widens it");
assert_eq!(widened.code(), "engine.stream_width", "{widened}");
let edit = edited(&mono, &g, &expr("join(@notes, @notes)"), &Tier::default()).now();
let widened = edit.expect_err("a stereo target widens it");
assert_eq!(widened.code(), "engine.stream_width", "{widened}");
assert_eq!(
(mono.borrow().width(), mono.borrow().counts().terms),
(1, 1)
);
let heard = blocks(&mono, 8);
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr("@mono(t - 512sp)")));
assert_eq!(heard, whole(&whole_g, "final", heard.len()), "plays on");
let stereo = opened_with(&g, "@notes", Some(2)).unwrap_or_else(|e| panic!("{e}"));
let widened = add(&stereo, "@three(t - 512sp)").expect_err("three channels widen it");
assert_eq!(widened.code(), "engine.stream_width", "{widened}");
let narrow = opened_with(&g, "@pair", Some(1))
.err()
.expect("a stereo target in mono");
assert_eq!(narrow.code(), "engine.stream_width", "{narrow}");
}
#[test]
fn a_stereo_stream_plays_a_mono_term_in_both_channels() {
let g = widths();
let stream = opened_with(&g, "@notes", Some(2)).unwrap_or_else(|e| panic!("{e}"));
for term in ["@mono(t - 512sp)", "@pair(t - 1024sp)"] {
added(&stream, &g, &expr(term), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("`{term}`: {e}"));
}
let heard = planes(&stream, 8);
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr("@mono(t - 512sp) + @pair(t - 1024sp)")));
let samples = heard[0].len() as f64 / f64::from(RATE);
let whole = render(
&whole_g,
"final",
RenderConfig::seconds(RATE, samples),
&Tier::default(),
)
.unwrap_or_else(|e| panic!("{e}"));
let whole = whole
.output(whole.id("final").expect("the root"))
.expect("a buffer");
for (c, plane) in heard.iter().enumerate() {
assert_eq!(plane[..], whole.plane(c)[..], "channel {c}");
}
}
#[test]
fn a_stereo_stream_edited_to_the_bare_sum_takes_a_mono_term_once_its_stereo_one_went() {
let g = widths();
let stream = opened_with(&g, "@notes + join(0, 0)", None).unwrap_or_else(|e| panic!("{e}"));
added(&stream, &g, &expr("@pair(t - 256sp)"), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
edit(&stream, &g, "@notes");
planes(&stream, 8);
assert_eq!(stream.borrow().counts().terms, 0, "the stereo term went");
let at = stream.borrow().position();
let mono = format!("@mono(t - {at}sp)");
added(&stream, &g, &expr(&mono), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("`{mono}`: {e}"));
let heard = planes(&stream, 4);
let mut whole_g = g.clone();
assert!(whole_g.define("final", expr("@mono(t)")));
let alone = whole(&whole_g, "final", heard[0].len());
assert!(alone.iter().any(|v| *v != 0.0), "silence tests nothing");
assert_eq!((&heard[0], &heard[1]), (&alone, &alone));
}
#[test]
fn an_edit_plays_a_node_as_its_own_graph_holds_it() {
let graph = |level: &str| {
graph_of(
"renewed",
&[
("bed", &format!("{level}*sin(2*pi*110*t)\n")),
("tone", "0.1*sin(2*pi*440*t)\n"),
],
)
};
let config = StreamConfig {
block: BLOCK,
channels: None,
render: RenderConfig::at(RATE),
};
let expr = |text: &str| sva_ast::parse_expr(text).expect("an expression");
let (loud, soft) = (graph("0.5"), graph("0.25"));
let target = expr("@bed + @tone");
let stream = Stream::open(&loud, &target, config, &Tier::default()).now();
let stream = RefCell::new(stream.expect("a stream"));
next(&mut stream.borrow_mut()).expect("a block");
edited(&stream, &soft, &expr("@bed(t) + @tone"), &Tier::default())
.now()
.expect("edited");
let heard = next(&mut stream.borrow_mut())
.expect("a block")
.expect("a block");
let whole = render(
&soft,
"bed",
RenderConfig::seconds(RATE, 0.1),
&Tier::default(),
)
.expect("a render");
let bed = whole.output(whole.root).expect("the bed").plane(0).to_vec();
let tone = |n: usize| 0.1 * (std::f64::consts::TAU * 440.0 * n as f64 / f64::from(RATE)).sin();
for (k, v) in heard.plane(0).iter().enumerate() {
let n = BLOCK + k;
assert!((v - (bed[n] + tone(n))).abs() < 1e-9, "sample {n}: {v}");
}
}