use crate::*; use std::f32::consts::TAU; pub struct Metronome { // Audio playback beep_samples: Arc, click_volume: f32, // 0.0 to 1.0 // Timing state samples_per_beat: u32, beat_time: u32, } impl Metronome { pub fn new(beep_samples: Arc, state: &State) -> Self { Self { beep_samples, click_volume: state.metronome.click_volume, samples_per_beat: state.metronome.samples_per_beat, beat_time: 0, } } pub fn samples_per_beat(&self) -> u32 { self.samples_per_beat } /// Process audio for current buffer, writing to output slice /// Returns the sample index where a beat occurred, if any pub fn process(&mut self, ps: &jack::ProcessScope, ports: &mut JackPorts) -> Result> { if 0 == self.beat_time { self.beat_time = ps.last_frame_time(); } let time_since_last_beat = ps.last_frame_time() - self.beat_time; if time_since_last_beat >= (2 * self.samples_per_beat) && self.beat_time != 0 { return Err(LooperError::Xrun(std::panic::Location::caller())); } let beat_sample_index = if time_since_last_beat >= self.samples_per_beat { self.beat_time += self.samples_per_beat; Some(self.beat_time - ps.last_frame_time()) } else { None }; // Get output buffer for click track let click_output = ports.click_track_out.as_mut_slice(ps); let buffer_size = click_output.len(); // Calculate current position within the beep (frames since last beat started) let frames_since_beat_start = ps.last_frame_time() - self.beat_time; let beep_length = self.beep_samples.sample_count as u32; if let Some(beat_offset) = beat_sample_index { let beat_offset = beat_offset as usize; // Write silence up to beat boundary let silence_end = beat_offset.min(buffer_size); click_output[0..silence_end].fill(0.0); // Write beep samples from boundary onward if beat_offset < buffer_size { let remaining_buffer = buffer_size - beat_offset; let samples_to_write = remaining_buffer.min(beep_length as usize); // Copy beep samples in bulk let dest = &mut click_output[beat_offset..beat_offset + samples_to_write]; if self.beep_samples.copy_samples(dest, 0).is_ok() { // Apply volume scaling with iterators dest.iter_mut().for_each(|sample| *sample *= self.click_volume); } // Fill remaining buffer with silence click_output[(beat_offset + samples_to_write)..].fill(0.0); } } else if frames_since_beat_start < beep_length { // Continue playing beep from previous beat if still within beep duration let beep_start_offset = frames_since_beat_start as usize; let remaining_beep_samples = beep_length as usize - beep_start_offset; let samples_to_write = buffer_size.min(remaining_beep_samples); // Copy remaining beep samples in bulk let dest = &mut click_output[0..samples_to_write]; if self.beep_samples.copy_samples(dest, beep_start_offset).is_ok() { // Apply volume scaling with iterators dest.iter_mut().for_each(|sample| *sample *= self.click_volume); } // Fill remaining buffer with silence click_output[samples_to_write..].fill(0.0); } else { click_output.fill(0.0); } Ok(beat_sample_index) } } /// Generate a 100ms sine wave beep at 1000Hz pub fn generate_beep( sample_rate: u32, chunk_factory: &mut F, ) -> Result> { const FREQUENCY_HZ: f32 = 1000.0; const DURATION_MS: f32 = 100.0; let sample_count = ((sample_rate as f32) * (DURATION_MS / 1000.0)) as usize; let mut samples = Vec::with_capacity(sample_count); for i in 0..sample_count { let t = i as f32 / sample_rate as f32; let sample = (TAU * FREQUENCY_HZ * t).sin(); samples.push(sample); } // Create AudioChunk and fill it with samples let mut chunk = chunk_factory.create_chunk()?; chunk.append_samples(&samples, chunk_factory)?; Ok(chunk) }