124 lines
4.5 KiB
Rust
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)
|
|
}
|