//! Look-ahead scheduler timing — Stage 2 of the Rust port (see `docs/rust-port.md`). //! //! Deterministic: turns a parsed `Track` + bpm into the click timeline. This is the same //! `durs` math the firmware uses (`app.py` `_prepare_next` / `tick`) and the engine.js //! scheduler primitives marked "PORTS TO FIRMWARE" — the real-time firmware loop just //! plays this timeline against the wall clock. Host-testable because the *timing grid* //! is pure. use crate::{Lane, Track}; use alloc::vec::Vec; const NS_PER_MIN: i64 = 60_000_000_000; #[derive(Debug, Clone, PartialEq)] pub struct Click { pub time_ns: i64, pub lane: usize, pub level: u8, } /// The master bar length in ns — defined by the first lane (beats = steps / sub). pub fn master_bar_ns(track: &Track) -> i64 { let beat = NS_PER_MIN / track.bpm.max(1); let m = &track.lanes[0]; let beats = (m.levels.len().max(1) / m.sub.max(1) as usize) as i64; beat * beats.max(1) } /// Per-step durations (ns) for a lane. Mirrors the firmware exactly: /// - poly → the lane's steps are spread evenly across the master bar (true polymeter) /// - swing → even subdivisions split 2/3 (on-beat) + 1/3 (off-beat) /// - else → uniform beat/sub pub fn lane_durs(lane: &Lane, bpm: i64, master_bar_ns: i64) -> Vec { let beat = NS_PER_MIN / bpm.max(1); let sub = lane.sub.max(1) as i64; let steps = lane.levels.len().max(1) as i64; if lane.poly { let d = master_bar_ns / steps; vec![d; steps as usize] } else if lane.swing && sub % 2 == 0 { let pair = beat / (sub / 2).max(1); let lng = pair * 2 / 3; let sht = pair / 3; (0..steps) .map(|s| if (s % sub) % 2 == 0 { lng } else { sht }) .collect() } else { let d = beat / sub; vec![d; steps as usize] } } /// Render the deterministic click timeline over `bars` master bars. /// Every lane starts together at t=0 and loops; muted lanes and rests (level 0) are dropped. pub fn render(track: &Track, bars: i64) -> Vec { let mbar = master_bar_ns(track); let total = mbar * bars.max(1); let mut out = Vec::new(); for (li, lane) in track.lanes.iter().enumerate() { if lane.mute { continue; } let durs = lane_durs(lane, track.bpm, mbar); if durs.iter().all(|&d| d <= 0) { continue; } let mut t = 0i64; let mut s = 0usize; while t < total { let lvl = lane.levels[s]; if lvl > 0 { out.push(Click { time_ns: t, lane: li, level: lvl }); } t += durs[s].max(1); s = (s + 1) % durs.len(); } } out.sort_by_key(|c| (c.time_ns, c.lane)); out }