Fix and display metronome

This commit is contained in:
2025-06-21 22:58:49 +02:00
parent 89645db1d9
commit 7e6a539296
19 changed files with 345 additions and 131 deletions

View File

@@ -23,6 +23,16 @@ pub struct BufferTiming {
pub beat_in_missed: Option<u32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PpqnTick {
/// Sample offset within the buffer where this tick occurs
pub sample_offset: usize,
/// PPQN tick index (0-23)
pub tick_index: usize,
/// Position within the beat (0.0 - 1.0)
pub position: f32,
}
impl Metronome {
pub fn new(click_samples: Arc<AudioChunk>, state: &State) -> Self {
Self {
@@ -44,6 +54,9 @@ impl Metronome {
let buffer_size = ps.n_frames();
let current_frame_time = ps.last_frame_time();
// Process PPQN ticks for this buffer
self.process_ppqn_ticks(ps, ports, osc_controller)?;
// Calculate timing for this buffer
let timing = self.calculate_timing(current_frame_time, buffer_size)?;
@@ -52,9 +65,6 @@ impl Metronome {
self.render_click(buffer_size, &timing, click_output);
// Process PPQN ticks for this buffer
self.process_ppqn_ticks(ps, ports, osc_controller)?;
Ok(timing)
}
@@ -66,63 +76,24 @@ impl Metronome {
) -> Result<()> {
let buffer_size = ps.n_frames() as usize;
// Calculate the starting position for this buffer
let start_position = (self.frames_since_last_beat + self.frames_per_beat - buffer_size)
% self.frames_per_beat;
// Use the pure function to find tick (allocation-free)
if let Some(tick) = Self::find_next_ppqn_tick_in_buffer(
self.frames_since_last_beat,
self.frames_per_beat,
buffer_size,
) {
// Send MIDI clock
let midi_clock_message = [0xF8];
let mut midi_writer = ports.midi_out.writer(ps);
midi_writer
.write(&jack::RawMidi {
time: tick.sample_offset as u32,
bytes: &midi_clock_message,
})
.map_err(|_| LooperError::Midi(std::panic::Location::caller()))?;
// Process PPQN ticks in the current buffer only
self.send_buffer_ppqn_ticks(ps, ports, osc_controller, start_position, buffer_size)?;
Ok(())
}
fn send_buffer_ppqn_ticks(
&self,
ps: &jack::ProcessScope,
ports: &mut JackPorts,
osc_controller: &OscController,
buffer_start_position: usize,
buffer_size: usize,
) -> Result<()> {
let frames_per_ppqn_tick = self.frames_per_beat / 24;
let mut current_position = buffer_start_position;
let mut frames_processed = 0;
// Get MIDI output writer
let mut midi_writer = ports.midi_out.writer(ps);
while frames_processed < buffer_size {
let frames_to_next_ppqn =
frames_per_ppqn_tick - (current_position % frames_per_ppqn_tick);
let frames_remaining_in_buffer = buffer_size - frames_processed;
if frames_remaining_in_buffer >= frames_to_next_ppqn {
// We hit a PPQN tick within this buffer
let tick_frame_offset = frames_processed + frames_to_next_ppqn;
// Ensure we don't hit the buffer boundary (JACK expects [0, buffer_size))
if tick_frame_offset >= buffer_size {
break;
}
current_position = (current_position + frames_to_next_ppqn) % self.frames_per_beat;
frames_processed = tick_frame_offset;
// Send sample-accurate MIDI clock
let midi_clock_message = [0xF8]; // MIDI Clock byte
midi_writer
.write(&jack::RawMidi {
time: tick_frame_offset as u32,
bytes: &midi_clock_message,
})
.map_err(|_| LooperError::Midi(std::panic::Location::caller()))?;
// Send OSC position message
let ppqn_position = (current_position as f32) / (self.frames_per_beat as f32);
osc_controller.metronome_position_changed(ppqn_position)?;
} else {
break;
}
// Send OSC position message
osc_controller.metronome_position_changed(tick.position)?;
}
Ok(())
@@ -257,6 +228,40 @@ impl Metronome {
beat_in_missed,
})
}
/// Pure function to find the next PPQN tick in a buffer (allocation-free)
/// Returns Some(tick) if there's a tick in this buffer, None otherwise
pub fn find_next_ppqn_tick_in_buffer(
frames_since_last_beat: usize,
frames_per_beat: usize,
buffer_size: usize,
) -> Option<PpqnTick> {
const TICKS_PER_BEAT: usize = 24;
let frames_per_tick = frames_per_beat / TICKS_PER_BEAT;
// Find distance to next tick boundary
let frames_since_last_tick = frames_since_last_beat % frames_per_tick;
let frames_to_next_tick = if frames_since_last_tick == 0 {
0 // We're exactly on a tick
} else {
frames_per_tick - frames_since_last_tick
};
// Check if this tick occurs within our buffer
if frames_to_next_tick < buffer_size {
let absolute_tick_position = frames_since_last_beat + frames_to_next_tick;
let tick_index = (absolute_tick_position / frames_per_tick) % TICKS_PER_BEAT;
let position = (tick_index as f32) / (TICKS_PER_BEAT as f32);
Some(PpqnTick {
sample_offset: frames_to_next_tick,
tick_index,
position,
})
} else {
None
}
}
}
#[cfg(test)]
@@ -451,4 +456,77 @@ mod tests {
assert_eq!(result.missed_frames, 0);
assert_eq!(metronome.frames_since_last_beat, 256);
}
// Tests for the pure PPQN calculation function
#[test]
fn test_ppqn_no_tick_in_buffer() {
let tick = Metronome::find_next_ppqn_tick_in_buffer(100, 960000, 128);
assert_eq!(tick, None);
}
#[test]
fn test_ppqn_tick_at_buffer_start() {
let frames_per_beat = 960000;
// Position exactly at tick boundary
let tick = Metronome::find_next_ppqn_tick_in_buffer(0, frames_per_beat, 128).unwrap();
assert_eq!(tick.sample_offset, 0);
assert_eq!(tick.tick_index, 0);
assert_eq!(tick.position, 0.0);
}
#[test]
fn test_ppqn_tick_in_middle_of_buffer() {
let frames_per_beat = 960000;
let frames_per_tick = frames_per_beat / 24; // 40000
// Position 64 frames before tick 1
let frames_since_last_beat = frames_per_tick - 64;
let tick =
Metronome::find_next_ppqn_tick_in_buffer(frames_since_last_beat, frames_per_beat, 128)
.unwrap();
assert_eq!(tick.sample_offset, 64);
assert_eq!(tick.tick_index, 1);
assert_eq!(tick.position, 1.0 / 24.0);
}
#[test]
fn test_ppqn_tick_wrapping() {
let frames_per_beat = 960000;
let frames_per_tick = frames_per_beat / 24; // 40000
// Position near end of beat cycle
let frames_since_last_beat = 23 * frames_per_tick - 64; // 64 frames before tick 23
let tick =
Metronome::find_next_ppqn_tick_in_buffer(frames_since_last_beat, frames_per_beat, 128)
.unwrap();
assert_eq!(tick.sample_offset, 64);
assert_eq!(tick.tick_index, 23);
assert_eq!(tick.position, 23.0 / 24.0);
}
#[test]
fn test_ppqn_real_world_scenario() {
// Your actual values
let frames_per_beat = 960000;
let frames_since_last_beat = 39936;
let buffer_size = 128;
let tick = Metronome::find_next_ppqn_tick_in_buffer(
frames_since_last_beat,
frames_per_beat,
buffer_size,
);
if let Some(tick) = tick {
println!(
"Tick {} at offset {} (position {})",
tick.tick_index, tick.sample_offset, tick.position
);
assert_eq!(tick.sample_offset, 64); // 40000 - 39936 = 64
assert_eq!(tick.tick_index, 1);
}
}
}

View File

@@ -76,4 +76,9 @@ impl jack::NotificationHandler for NotificationHandler {
.expect("Could not send port registration notification");
};
}
fn xrun(&mut self, _: &jack::Client) -> jack::Control {
log::error!("XRUN");
jack::Control::Continue
}
}