Fix and display metronome
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user