fcb_looper/src/metronome.rs

124 lines
4.5 KiB
Rust

use crate::*;
use std::f32::consts::TAU;
pub struct Metronome {
// Audio playback
beep_samples: Arc<AudioChunk>,
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<AudioChunk>, 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<Option<u32>> {
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<F: ChunkFactory>(
sample_rate: u32,
chunk_factory: &mut F,
) -> Result<Arc<AudioChunk>> {
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)
}