use proptest::prelude::*;
use tono_core::dsl::{TempoPoint, tempo_map_beat_at_seconds, tempo_map_seconds_at};
use tono_core::program::ProgramMeta;
use tono_core::runtime::Transport;
use tono_core::units::{
Beat, Frames, MeterPoint, SampleRate, Tempo, bar_count_at_beat, beat_at_bar, beat_to_frames,
};
fn config() -> ProptestConfig {
ProptestConfig {
cases: 128,
failure_persistence: None,
..ProptestConfig::default()
}
}
fn arb_meter_map() -> BoxedStrategy<Vec<MeterPoint>> {
proptest::option::of(proptest::collection::vec(
(
1..=12u32,
proptest::sample::select(vec![2u32, 4, 8, 16]),
1..=4u32,
),
1..=4,
))
.prop_map(|opt| {
let mut bar = 0u32;
opt.unwrap_or_default()
.into_iter()
.map(|(numerator, denominator, gap)| {
let point = MeterPoint {
bar,
numerator,
denominator,
};
bar += gap;
point
})
.collect()
})
.boxed()
}
fn arb_tempo_map() -> BoxedStrategy<Vec<TempoPoint>> {
(
20.0f32..=300.0,
proptest::collection::vec((1..=8i64, 20.0f32..=300.0), 0..=4),
)
.prop_map(|(bpm0, tail)| {
let mut beat = 0i64;
let mut map = vec![TempoPoint {
at: Beat::zero(),
bpm: bpm0,
}];
for (gap, bpm) in tail {
beat += gap;
map.push(TempoPoint {
at: Beat::from_int(beat),
bpm,
});
}
map
})
.boxed()
}
fn meta(tempo_bpm: f32, sample_rate: u32) -> ProgramMeta {
ProgramMeta {
name: "fuzz-time".into(),
tempo_bpm,
beats_per_bar: 4,
steps_per_beat: 4,
tempo_map: vec![],
meter_map: vec![],
pickup: None,
sections: vec![],
markers: vec![],
length_bars: 0,
duration_secs: 0.0,
duration_frames: 0,
sample_rate,
tracks: vec![],
}
}
proptest! {
#![proptest_config(config())]
#[test]
fn beat_at_bar_is_non_decreasing(
map in arb_meter_map(),
default_numerator in 1..=8u32,
pickup in proptest::option::of((0..=3i64, 1..=4u32).prop_map(|(n, d)| Beat::new(n, d))),
bars in proptest::collection::vec(0..=64u32, 2..=12),
) {
let mut bars = bars;
bars.sort_unstable();
let beats: Vec<Beat> = bars
.iter()
.map(|&bar| beat_at_bar(&map, default_numerator, pickup, bar))
.collect();
for (i, pair) in beats.windows(2).enumerate() {
prop_assert!(
pair[0] <= pair[1],
"bar {} starts after bar {}",
bars[i],
bars[i + 1]
);
}
}
#[test]
fn transport_and_units_agree_on_constant_tempo(
bpm in 0.0f32..=300.0,
rate in proptest::sample::select(vec![8_000u32, 44_100, 48_000]),
beat in (-64i64..=512, 1..=16u32).prop_map(|(n, d)| Beat::new(n, d)),
) {
let transport = Transport::for_program(&meta(bpm, rate));
prop_assert_eq!(
Frames(transport.frame_at_beat(beat.to_f64())),
beat_to_frames(beat, Tempo(bpm), SampleRate(rate)),
"Transport::frame_at_beat and units::beat_to_frames disagree"
);
}
#[test]
fn tempo_map_walk_is_monotone_and_invertible(
map in arb_tempo_map(),
beats in proptest::collection::vec(0.0f64..=64.0, 2..=10),
) {
let mut beats = beats;
beats.sort_by(f64::total_cmp);
let seconds: Vec<f64> = beats
.iter()
.map(|&b| tempo_map_seconds_at(&map, b))
.collect();
for pair in seconds.windows(2) {
prop_assert!(pair[0] <= pair[1], "seconds went backwards along the beat axis");
}
for (&beat, &secs) in beats.iter().zip(&seconds) {
let back = tempo_map_beat_at_seconds(&map, secs);
prop_assert!(
(back - beat).abs() <= 1e-9,
"inversion failed: beat {beat} -> {secs} s -> {back}"
);
}
}
#[test]
fn bar_count_at_beat_matches_the_legacy_ceil(
note_end in 0..=512u32,
spb in 1..=8u32,
bpb in 1..=8u32,
) {
prop_assert_eq!(
bar_count_at_beat(&[], bpb, None, Beat::new(note_end as i64, spb)),
note_end.div_ceil(bpb * spb),
"bar_count_at_beat diverged from length_bars' ceil semantics"
);
}
}