#[derive(Debug)]
pub(crate) struct PassthroughClock {
multiply: u8,
divide: u8,
timeout_ns: u64,
input_count: u64,
output_subdivision: u8,
output_beat: u64,
last_tick_ns: Option<u64>,
is_running: bool,
}
impl PassthroughClock {
pub(crate) fn new(multiply: u8, divide: u8, timeout_ns: u64) -> Self {
Self {
multiply: multiply.max(1),
divide: divide.max(1),
timeout_ns,
input_count: 0,
output_subdivision: 0,
output_beat: 0,
last_tick_ns: None,
is_running: true,
}
}
pub(crate) fn feed_clock(&mut self, timestamp_ns: u64) -> u32 {
self.last_tick_ns = Some(timestamp_ns);
self.input_count += 1;
let prev_total = ((self.input_count - 1) * self.multiply as u64) / self.divide as u64;
let curr_total = (self.input_count * self.multiply as u64) / self.divide as u64;
(curr_total - prev_total) as u32
}
pub(crate) fn advance_output(&mut self) {
self.output_subdivision += 1;
if self.output_subdivision >= 24 {
self.output_subdivision = 0;
self.output_beat += 1;
}
}
pub(crate) fn output_subdivision(&self) -> u8 {
self.output_subdivision
}
pub(crate) fn output_beat(&self) -> u64 {
self.output_beat
}
pub(crate) fn check_dropout(&self, now_ns: u64) -> bool {
if let Some(last) = self.last_tick_ns {
now_ns.saturating_sub(last) > self.timeout_ns
} else {
false
}
}
pub(crate) fn reset(&mut self) {
self.output_subdivision = 0;
self.output_beat = 0;
self.input_count = 0;
}
pub(crate) fn set_running(&mut self, running: bool) {
self.is_running = running;
}
pub(crate) fn is_running(&self) -> bool {
self.is_running
}
pub(crate) fn set_position_from_spp(&mut self, midi_beats: u16) {
self.output_beat = (midi_beats / 4) as u64;
self.output_subdivision = ((midi_beats % 4) * 6) as u8;
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_passthrough_1_to_1() {
let mut clock = PassthroughClock::new(1, 1, 1_000_000_000);
let mut total_output = 0u32;
for i in 0..24 {
let ticks = clock.feed_clock(i * 20_833_333);
total_output += ticks;
}
assert_eq!(
total_output, 24,
"1:1 should produce 24 output ticks from 24 inputs"
);
}
#[test]
fn test_passthrough_multiply_2() {
let mut clock = PassthroughClock::new(2, 1, 1_000_000_000);
let mut total_output = 0u32;
for i in 0..24 {
let ticks = clock.feed_clock(i * 20_833_333);
total_output += ticks;
}
assert_eq!(
total_output, 48,
"multiply=2 should produce 48 output ticks from 24 inputs"
);
}
#[test]
fn test_passthrough_divide_2() {
let mut clock = PassthroughClock::new(1, 2, 1_000_000_000);
let mut total_output = 0u32;
for i in 0..24 {
let ticks = clock.feed_clock(i * 20_833_333);
total_output += ticks;
}
assert_eq!(
total_output, 12,
"divide=2 should produce 12 output ticks from 24 inputs"
);
}
#[test]
fn test_passthrough_multiply_3_divide_2() {
let mut clock = PassthroughClock::new(3, 2, 1_000_000_000);
let mut total_output = 0u32;
for i in 0..24 {
let ticks = clock.feed_clock(i * 20_833_333);
total_output += ticks;
}
assert_eq!(
total_output, 36,
"multiply=3, divide=2 should produce 36 output ticks from 24 inputs"
);
}
#[test]
fn test_passthrough_dropout() {
let timeout_ns = 500_000_000; let mut clock = PassthroughClock::new(1, 1, timeout_ns);
clock.feed_clock(1_000_000);
assert!(
!clock.check_dropout(100_000_000),
"should not be a dropout within the timeout window"
);
assert!(
clock.check_dropout(1_000_000 + timeout_ns + 1),
"should detect dropout after timeout"
);
}
#[test]
fn test_passthrough_no_dropout_before_ticks() {
let clock = PassthroughClock::new(1, 1, 500_000_000);
assert!(
!clock.check_dropout(10_000_000_000),
"should not report dropout when no tick has been received"
);
}
#[test]
fn test_passthrough_reset() {
let mut clock = PassthroughClock::new(1, 1, 1_000_000_000);
for i in 0..30 {
let out = clock.feed_clock(i * 20_833_333);
for _ in 0..out {
clock.advance_output();
}
}
assert!(
clock.output_beat() > 0 || clock.output_subdivision() > 0,
"position should have advanced"
);
clock.reset();
assert_eq!(
clock.output_subdivision(),
0,
"subdivision should be 0 after reset"
);
assert_eq!(clock.output_beat(), 0, "beat should be 0 after reset");
}
#[test]
fn test_passthrough_advance_output_wraps_at_24() {
let mut clock = PassthroughClock::new(1, 1, 1_000_000_000);
for _ in 0..24 {
clock.advance_output();
}
assert_eq!(clock.output_subdivision(), 0);
assert_eq!(clock.output_beat(), 1);
for _ in 0..6 {
clock.advance_output();
}
assert_eq!(clock.output_subdivision(), 6);
assert_eq!(clock.output_beat(), 1);
}
#[test]
fn test_passthrough_running_state() {
let mut clock = PassthroughClock::new(1, 1, 1_000_000_000);
assert!(clock.is_running(), "should be running initially");
clock.set_running(false);
assert!(
!clock.is_running(),
"should be stopped after set_running(false)"
);
clock.set_running(true);
assert!(
clock.is_running(),
"should be running after set_running(true)"
);
}
#[test]
fn test_passthrough_multiply_and_divide_clamped_to_1() {
let mut clock = PassthroughClock::new(0, 0, 1_000_000_000);
let mut total_output = 0u32;
for i in 0..24 {
total_output += clock.feed_clock(i * 20_833_333);
}
assert_eq!(total_output, 24, "clamped (0,0) should behave as (1,1)");
}
#[test]
fn test_passthrough_set_position_from_spp() {
let mut clock = PassthroughClock::new(1, 1, 1_000_000_000);
clock.set_position_from_spp(7);
assert_eq!(clock.output_beat(), 1);
assert_eq!(clock.output_subdivision(), 18);
clock.set_position_from_spp(0);
assert_eq!(clock.output_beat(), 0);
assert_eq!(clock.output_subdivision(), 0);
}
#[test]
fn test_passthrough_divide_produces_even_distribution() {
let mut clock = PassthroughClock::new(1, 3, 1_000_000_000);
let mut outputs = Vec::new();
for i in 0..24 {
outputs.push(clock.feed_clock(i * 20_833_333));
}
let total: u32 = outputs.iter().sum();
assert_eq!(total, 8, "divide=3 from 24 inputs should produce 8 outputs");
for (i, &out) in outputs.iter().enumerate() {
if (i + 1) % 3 == 0 {
assert_eq!(out, 1, "tick {} should produce 1 output", i);
} else {
assert_eq!(out, 0, "tick {} should produce 0 outputs", i);
}
}
}
}