metronome/rust/track-format/tests/schedule.rs
Me Here c1601d9e46 Rust port Stage 2: scheduler timing + no_std (builds for RP2350)
- schedule.rs: ports the firmware's durs/timeline math (app.py tick/_prepare_next).
  render(track, bars) yields the deterministic click timeline; tests/schedule.rs
  asserts quarter-note spacing, subdivisions, swing 2/3:1/3, polymeter 5:4,
  accents/ghosts, mute, and multi-bar looping. All green on the host.
- The crate is now #![no_std] + alloc and builds for thumbv8m.main-none-eabihf,
  so the codec + scheduler are firmware-ready (verified:
  cargo build --lib --target thumbv8m.main-none-eabihf).

./rust/run.sh -> 9 tests pass (2 conformance + 7 schedule). docs/rust-port.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:34:02 -05:00

70 lines
2.6 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Stage 2 scheduler timing tests — deterministic, no hardware. Asserts the click
//! timeline math (subdivision spacing, swing ratio, polymeter, accents, mute).
use track_format::parse;
use track_format::schedule::{lane_durs, master_bar_ns, render};
const S: i64 = 1_000_000_000; // one second in ns
#[test]
fn quarter_notes_at_120() {
let t = parse("t120;kick:4=Xxxx");
let cl = render(&t, 1);
assert_eq!(cl.iter().map(|c| c.time_ns).collect::<Vec<_>>(), vec![0, S / 2, S, 3 * S / 2]);
assert_eq!(cl.iter().map(|c| c.level).collect::<Vec<_>>(), vec![2, 1, 1, 1]); // accent on beat 1
}
#[test]
fn eighth_subdivision_spacing() {
// hats on the quarters only (X.x.x.x.) over an 8th grid → 0, 0.5, 1.0, 1.5 s
let t = parse("t120;hatClosed:4/2=X.x.x.x.");
let cl = render(&t, 1);
assert_eq!(cl.iter().map(|c| c.time_ns).collect::<Vec<_>>(), vec![0, S / 2, S, 3 * S / 2]);
assert_eq!(cl[0].level, 2);
}
#[test]
fn swing_splits_two_thirds_one_third() {
let t = parse("t120;ride:4/2s=Xxxxxxxx"); // all 8ths sounding, swung
let durs = lane_durs(&t.lanes[0], 120, master_bar_ns(&t));
let beat = 500_000_000;
assert_eq!(durs[0], beat * 2 / 3); // on-beat = long
assert_eq!(durs[1], beat / 3); // off-beat = short
assert!(durs[0] > durs[1]);
// a long+short pair is one beat (within integer-division rounding)
assert!((durs[0] + durs[1] - beat).abs() <= 1);
}
#[test]
fn polymeter_five_over_four() {
let t = parse("t100;kick:4=Xxxx;claves:5=Xxxxx~");
let mbar = master_bar_ns(&t);
assert_eq!(mbar, 2_400_000_000); // 4 beats at 100 bpm
// claves (lane 1) is poly: 5 steps spread evenly across the master bar
assert_eq!(lane_durs(&t.lanes[1], 100, mbar), vec![480_000_000; 5]);
let claves: Vec<i64> = render(&t, 1).into_iter().filter(|c| c.lane == 1).map(|c| c.time_ns).collect();
assert_eq!(claves, vec![0, 480_000_000, 960_000_000, 1_440_000_000, 1_920_000_000]);
}
#[test]
fn accents_and_ghosts_carry_through() {
let t = parse("t120;snare:4/3=..gg.gX.gg.g"); // ghosts (3) + an accent (2)
let cl = render(&t, 1);
assert!(cl.iter().any(|c| c.level == 3)); // ghost present
assert!(cl.iter().any(|c| c.level == 2)); // accent present
}
#[test]
fn muted_lane_is_silent() {
let t = parse("t120;kick:4=Xxxx;snare:4=Xxxx!");
let cl = render(&t, 1);
assert!(cl.iter().all(|c| c.lane == 0)); // the muted snare produces no clicks
}
#[test]
fn loops_over_multiple_bars() {
let t = parse("t120;kick:4=Xxxx");
assert_eq!(render(&t, 2).len(), 8); // 4 quarter-note clicks per bar × 2 bars
}