use proptest::prelude::*;
use tono_core::dsl::Value;
use tono_core::song::{
Pattern, concat, gate, humanize, layer, probability, quantize, repeat, reverse, rotate, slice,
stretch, transpose, vel,
};
fn config() -> ProptestConfig {
ProptestConfig {
cases: 128,
failure_persistence: None,
..ProptestConfig::default()
}
}
const PITCHES: &[&str] = &[
"C2", "E2", "G2", "C3", "D#3", "F#3", "A3", "C4", "E4", "G4", "B4", "D5", "midi:36", "midi:38",
"midi:60", "midi:69",
];
fn arb_pattern() -> BoxedStrategy<(Pattern, u32)> {
(1..=4u32, proptest::sample::select(vec![4u32, 8, 16]))
.prop_flat_map(|(bars, spb)| {
let total = (bars * spb) as usize;
(Just(bars), Just(spb), 2..=total.min(16))
})
.prop_flat_map(|(bars, spb, count)| {
let total = bars * spb;
(
proptest::sample::subsequence((0..total).collect::<Vec<u32>>(), count),
proptest::collection::vec(1..=8u32, count),
proptest::collection::vec(proptest::sample::select(PITCHES), count),
proptest::collection::vec(0.0f32..=1.0, count),
Just(bars),
Just(spb),
)
})
.prop_map(|(steps, lens, pitches, gains, bars, spb)| {
let total = bars * spb;
let notes = steps
.into_iter()
.zip(lens)
.zip(pitches)
.zip(gains)
.map(|(((step, len), pitch), gain)| {
let len = len.min(total - step);
tono_core::song::note_vel(step, len, pitch, gain)
})
.collect();
(
Pattern {
name: "p".into(),
bars,
notes,
},
spb,
)
})
.boxed()
}
#[test]
fn reverse_saturates_notes_past_the_pattern_end() {
let p = Pattern {
name: "p".into(),
bars: 1,
notes: vec![tono_core::song::note(12, 8, "C2")], };
let once = reverse(&p, 16);
assert_eq!(once.notes[0].step, 0, "the overrun saturates to step 0");
let twice = reverse(&once, 16);
assert_eq!(
twice.notes[0].step, 8,
"the saturated mirror lands at total - len, not the original step"
);
}
fn key(p: &Pattern) -> Vec<(u32, u32, String, u32)> {
p.notes
.iter()
.map(|n| {
let pitch = match &n.pitch {
Value::Note(s) => s.clone(),
Value::Const(hz) => format!("{hz}"),
Value::Modulated(_) => "mod".into(),
};
(n.step, n.len, pitch, n.gain.to_bits())
})
.collect()
}
fn assert_same(p: &Pattern, q: &Pattern) -> Result<(), TestCaseError> {
prop_assert_eq!(key(p), key(q), "notes differ");
prop_assert_eq!(p.bars, q.bars, "bar counts differ");
Ok(())
}
proptest! {
#![proptest_config(config())]
#[test]
fn reverse_is_an_involution((p, spb) in arb_pattern()) {
let twice = reverse(&reverse(&p, spb), spb);
assert_same(&p, &twice)?;
}
#[test]
fn transpose_composes(
(p, _spb) in arb_pattern(),
a in -12i16..=12,
b in -12i16..=12,
) {
let stepwise = transpose(&transpose(&p, a).unwrap(), b).unwrap();
let summed = transpose(&p, a + b).unwrap();
assert_same(&stepwise, &summed)?;
}
#[test]
fn rotate_is_modular(
(p, spb) in arb_pattern(),
h1 in -96i64..=96,
h2 in -96i64..=96,
) {
let total = i64::from(p.bars * spb);
assert_same(&p, &rotate(&p, total, spb))?;
assert_same(
&rotate(&rotate(&p, h1, spb), h2, spb),
&rotate(&p, h1 + h2, spb),
)?;
}
#[test]
fn stretch_round_trips_exactly((p, spb) in arb_pattern()) {
assert_same(&p, &stretch(&p, 1, 1, spb).unwrap())?;
let doubled = stretch(&p, 2, 1, spb).unwrap();
let halved = stretch(&doubled, 1, 2, spb).unwrap();
assert_same(&p, &halved)?;
}
#[test]
fn quantize_is_idempotent((p, _spb) in arb_pattern(), g in 1..=16u32) {
let once = quantize(&p, g);
let twice = quantize(&once, g);
assert_same(&once, &twice)?;
}
#[test]
fn probability_bounds_and_note_identity(
(p, _spb) in arb_pattern(),
keep in 0.0f32..=1.0,
seed in any::<u64>(),
) {
assert_same(&p, &probability(&p, 1.0, seed))?;
prop_assert!(probability(&p, 0.0, seed).notes.is_empty());
assert_same(
&probability(&p, keep, seed),
&probability(&p, keep, seed),
)?;
let mut reversed = p.clone();
reversed.notes.reverse();
let mut a = key(&probability(&p, keep, seed));
let mut b = key(&probability(&reversed, keep, seed));
a.sort();
b.sort();
prop_assert_eq!(a, b, "the drops must follow the notes, not the vec order");
}
#[test]
fn humanize_neutral_and_deterministic(
(p, _spb) in arb_pattern(),
timing in 0.0f32..=1.0,
velocity in 0.0f32..=1.0,
seed in any::<u64>(),
) {
assert_same(&p, &humanize(&p, 0.0, 0.0, seed))?;
assert_same(
&humanize(&p, timing, velocity, seed),
&humanize(&p, timing, velocity, seed),
)?;
}
#[test]
fn vel_and_gate_have_exact_neutral_elements((p, _spb) in arb_pattern()) {
assert_same(&p, &vel(&p, 1.0))?;
assert_same(&p, &gate(&p, 1.0))?;
}
#[test]
fn repeat_concat_layer_arithmetic(
(a, spb) in arb_pattern(),
(b, _) in arb_pattern(),
times in 0..=4u32,
) {
let r = repeat(&a, spb, times);
prop_assert_eq!(r.notes.len(), a.notes.len() * times as usize);
prop_assert_eq!(r.bars, a.bars * times);
let c = concat(&a, &b, spb);
prop_assert_eq!(c.notes.len(), a.notes.len() + b.notes.len());
prop_assert_eq!(c.bars, a.bars + b.bars);
prop_assert_eq!(&key(&c)[..a.notes.len()], &key(&a)[..]);
let l = layer(&a, &a);
prop_assert_eq!(l.notes.len(), 2 * a.notes.len());
prop_assert_eq!(l.bars, a.bars);
}
#[test]
fn slice_over_the_whole_pattern_is_the_identity((p, spb) in arb_pattern()) {
let total = p.bars * spb;
let s = slice(&p, 0, total, spb);
assert_same(&p, &s)?;
let windowed = slice(&p, total / 2, total / 2, spb);
prop_assert!(windowed.notes.len() <= p.notes.len());
}
}