//! Host tests for the `Player` controls added for live tempo/volume on the Daisy Pod //! (`position` / `seek` / `set_volume` / `last_level`). These run on the host (the no_std lib links //! against the test harness's std allocator, same as `synthrender`), so the device behavior is //! exercised without hardware. use pm_synth::{Player, SPIKE_BARS, SPIKE_PROGRAM}; fn spike_player() -> Player { let track = track_format::parse(SPIKE_PROGRAM); Player::new(&track, SPIKE_BARS) } #[test] fn fresh_player_starts_at_zero() { let p = spike_player(); assert_eq!(p.position(), 0.0); assert_eq!(p.last_level(), 0, "no click has fired yet"); } #[test] fn position_advances_with_playback() { let mut p = spike_player(); for _ in 0..48_000 { p.next_sample(); } let pos = p.position(); assert!(pos > 0.0 && pos < 1.0, "position should be mid-loop, got {pos}"); } #[test] fn seek_sets_position_and_is_idempotent_with_position() { let mut p = spike_player(); p.seek(0.5); let pos = p.position(); assert!((pos - 0.5).abs() < 0.01, "seek(0.5) → position {pos}, want ~0.5"); } #[test] fn seek_clamps_to_loop() { let mut p = spike_player(); p.seek(2.0); // out of range assert!(p.position() < 1.0, "seek past the end must clamp inside the loop"); p.seek(-1.0); assert_eq!(p.position(), 0.0, "negative seek clamps to start"); } #[test] fn seek_cursor_lands_on_next_event() { // The spike program has a kick on every beat (level 1/X). After seeking just before the loop // end, advancing should still fire the wrap-around downbeat — i.e. the cursor was repositioned, // not left dangling past the end. let mut p = spike_player(); let before = p.fired(); p.seek(0.999); // Run through the loop seam (a chunk longer than the remaining tail). for _ in 0..48_000 { p.next_sample(); } assert!(p.fired() > before, "clicks should fire after seeking near the end and wrapping"); } #[test] fn last_level_reports_accent_priority() { // kick909 is level 1 (X with no explicit accent map → normal), clap on beats 2&4 is X (accent). // Run a full loop; by the end last_level must have seen at least a normal-level hit. let mut p = spike_player(); let track = track_format::parse(SPIKE_PROGRAM); let mbar = track_format::schedule::master_bar_ns(&track) * SPIKE_BARS; let samples = (mbar as f64 / 1e9 * pm_synth::SR as f64) as usize; for _ in 0..samples { p.next_sample(); } assert!(p.last_level() >= 1 && p.last_level() <= 3, "last_level must be a valid click level, got {}", p.last_level()); } #[test] fn set_volume_does_not_panic_and_silences_at_zero() { let mut p = spike_player(); p.set_volume(0.0); // Drive past the first downbeat; at zero master the output must be silent. let mut peak = 0.0f32; for _ in 0..24_000 { peak = peak.max(p.next_sample().abs()); } assert_eq!(peak, 0.0, "volume 0 should produce silence, peaked at {peak}"); }