use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
use rill_core::time::SystemClock;
pub trait MidiClockStrategy: Send + Sync {
fn on_bpm(&mut self, clock: &SystemClock, bpm: f64);
fn on_start(&mut self, clock: &SystemClock);
fn on_stop(&mut self, clock: &SystemClock);
fn on_continue(&mut self, clock: &SystemClock);
}
pub struct ResetOnStart;
impl MidiClockStrategy for ResetOnStart {
fn on_bpm(&mut self, clock: &SystemClock, bpm: f64) {
clock.set_bpm(bpm);
}
fn on_start(&mut self, clock: &SystemClock) {
clock.reset();
}
fn on_stop(&mut self, _clock: &SystemClock) {}
fn on_continue(&mut self, _clock: &SystemClock) {}
}
pub struct FreeRunning;
impl MidiClockStrategy for FreeRunning {
fn on_bpm(&mut self, clock: &SystemClock, bpm: f64) {
clock.set_bpm(bpm);
}
fn on_start(&mut self, _clock: &SystemClock) {}
fn on_stop(&mut self, _clock: &SystemClock) {}
fn on_continue(&mut self, _clock: &SystemClock) {}
}
pub struct SongPosition {
is_playing: bool,
}
impl SongPosition {
pub fn new() -> Self {
Self { is_playing: false }
}
pub fn is_playing(&self) -> bool {
self.is_playing
}
}
impl Default for SongPosition {
fn default() -> Self {
Self::new()
}
}
impl MidiClockStrategy for SongPosition {
fn on_bpm(&mut self, clock: &SystemClock, bpm: f64) {
clock.set_bpm(bpm);
}
fn on_start(&mut self, clock: &SystemClock) {
clock.reset();
self.is_playing = true;
}
fn on_stop(&mut self, _clock: &SystemClock) {
self.is_playing = false;
}
fn on_continue(&mut self, _clock: &SystemClock) {
self.is_playing = true;
}
}
const TICKS_PER_BEAT: usize = 24;
pub struct MidiClockTracker {
clock: Arc<SystemClock>,
strategy: Box<dyn MidiClockStrategy>,
last_tick: Option<Instant>,
intervals: VecDeque<f64>,
tick_count: u64,
ticks_since_bpm: usize,
bpm: Option<f64>,
is_playing: Arc<AtomicBool>,
}
impl MidiClockTracker {
pub fn new(clock: Arc<SystemClock>, strategy: Box<dyn MidiClockStrategy>) -> Self {
let bpm = clock.bpm();
Self {
clock,
strategy,
last_tick: None,
intervals: VecDeque::with_capacity(TICKS_PER_BEAT),
tick_count: 0,
ticks_since_bpm: 0,
bpm: Some(bpm),
is_playing: Arc::new(AtomicBool::new(false)),
}
}
pub fn shared_clock(&self) -> Arc<SystemClock> {
Arc::clone(&self.clock)
}
pub fn playing_flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.is_playing)
}
pub fn bpm(&self) -> Option<f64> {
self.bpm
}
pub fn tick_count(&self) -> u64 {
self.tick_count
}
pub fn process_status(&mut self, status: u8) {
match status {
0xF8 => self.tick(),
0xFA => {
self.is_playing.store(true, Ordering::Release);
self.strategy.on_start(&self.clock);
self.reset_tracking();
}
0xFC => {
self.is_playing.store(false, Ordering::Release);
self.strategy.on_stop(&self.clock);
}
0xFB => {
self.is_playing.store(true, Ordering::Release);
self.strategy.on_continue(&self.clock);
}
_ => {}
}
}
fn tick(&mut self) {
let now = Instant::now();
self.tick_count += 1;
self.ticks_since_bpm += 1;
if let Some(prev) = self.last_tick.replace(now) {
let dt = now.duration_since(prev).as_secs_f64();
if dt > 0.0 && dt < 2.0 {
self.intervals.push_back(dt);
if self.intervals.len() > TICKS_PER_BEAT {
self.intervals.pop_front();
}
}
}
if self.ticks_since_bpm >= TICKS_PER_BEAT {
self.ticks_since_bpm = 0;
if !self.intervals.is_empty() {
let avg: f64 = self.intervals.iter().sum::<f64>() / self.intervals.len() as f64;
let bpm = 60.0 / (avg * TICKS_PER_BEAT as f64);
let bpm = bpm.clamp(20.0, 300.0);
self.bpm = Some(bpm);
self.strategy.on_bpm(&self.clock, bpm);
}
}
}
fn reset_tracking(&mut self) {
self.tick_count = 0;
self.ticks_since_bpm = 0;
self.intervals.clear();
self.last_tick = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_tracker_initial_bpm() {
let clock = Arc::new(SystemClock::new(44100.0, 120.0));
let tracker = MidiClockTracker::new(clock, Box::new(FreeRunning));
assert_eq!(tracker.bpm(), Some(120.0));
}
#[test]
fn test_strategy_free_running() {
let mut strategy = FreeRunning;
let clock = SystemClock::new(44100.0, 120.0);
strategy.on_bpm(&clock, 100.0);
assert!((clock.bpm() - 100.0).abs() < 1e-10);
}
#[test]
fn test_strategy_reset_on_start() {
let mut strategy = ResetOnStart;
let clock = SystemClock::new(44100.0, 120.0);
strategy.on_start(&clock);
assert_eq!(clock.position(), 0);
}
#[test]
fn test_strategy_song_position() {
let mut strategy = SongPosition::new();
assert!(!strategy.is_playing());
let clock = SystemClock::new(44100.0, 120.0);
strategy.on_start(&clock);
assert!(strategy.is_playing());
strategy.on_stop(&clock);
assert!(!strategy.is_playing());
strategy.on_continue(&clock);
assert!(strategy.is_playing());
}
#[test]
fn test_bpm_derivation_from_pulses() {
let clock = Arc::new(SystemClock::new(44100.0, 120.0));
let mut tracker = MidiClockTracker::new(clock, Box::new(FreeRunning));
for _ in 0..96 {
tracker.process_status(0xF8);
thread::sleep(std::time::Duration::from_micros(20833));
}
let bpm = tracker.bpm().unwrap();
assert!(
bpm > 110.0 && bpm < 130.0,
"expected ~120 BPM, got {:.1}",
bpm
);
}
#[test]
fn test_transport_start_resets_ticks() {
let clock = Arc::new(SystemClock::new(44100.0, 120.0));
let mut tracker = MidiClockTracker::new(clock, Box::new(FreeRunning));
tracker.process_status(0xF8);
tracker.process_status(0xF8);
assert_eq!(tracker.tick_count(), 2);
tracker.process_status(0xFA); assert_eq!(tracker.tick_count(), 0);
}
#[test]
fn test_clamps_outlier_bpm() {
let clock = Arc::new(SystemClock::new(44100.0, 120.0));
let mut tracker = MidiClockTracker::new(clock, Box::new(FreeRunning));
for _ in 0..TICKS_PER_BEAT {
tracker.process_status(0xF8);
}
let bpm = tracker.bpm().unwrap();
assert!(
bpm <= 300.0 && bpm >= 20.0,
"BPM {:.1} out of clamp range",
bpm
);
}
}