timed metronome beep
This commit is contained in:
parent
77b09b385b
commit
9e6bd37b8f
@ -48,14 +48,21 @@ async fn main() {
|
||||
let beep_samples = generate_beep(jack_client.sample_rate() as u32, &mut allocator)
|
||||
.expect("Could not generate beep samples");
|
||||
|
||||
let process_handler =
|
||||
ProcessHandler::new(ports, allocator, beep_samples).expect("Could not create process handler");
|
||||
|
||||
let notification_handler = NotificationHandler::new();
|
||||
let mut notification_channel = notification_handler.subscribe();
|
||||
|
||||
let (mut state_manager, state_rx) = StateManager::new(notification_handler.subscribe());
|
||||
|
||||
// Load state values for metronome configuration
|
||||
let initial_state = state_rx.borrow().clone();
|
||||
|
||||
let process_handler = ProcessHandler::new(
|
||||
ports,
|
||||
allocator,
|
||||
beep_samples,
|
||||
&initial_state,
|
||||
).expect("Could not create process handler");
|
||||
|
||||
let mut connection_manager = ConnectionManager::new(
|
||||
state_rx,
|
||||
notification_handler.subscribe(),
|
||||
|
||||
@ -46,9 +46,8 @@ impl ConnectionManager {
|
||||
let result = self
|
||||
.jack_client
|
||||
.connect_ports_by_name(external_port, &our_port);
|
||||
match result {
|
||||
Ok(_) => log::info!("Connected {} -> {}", external_port, our_port),
|
||||
Err(_) => log::debug!("Could not connect {} -> {}", external_port, our_port),
|
||||
if let Ok(_) = result {
|
||||
log::info!("Connected {} -> {}", external_port, our_port);
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,9 +56,8 @@ impl ConnectionManager {
|
||||
let result = self
|
||||
.jack_client
|
||||
.connect_ports_by_name(external_port, &our_port);
|
||||
match result {
|
||||
Ok(_) => log::info!("Connected {} -> {}", external_port, our_port),
|
||||
Err(_) => log::debug!("Could not connect {} -> {}", external_port, our_port),
|
||||
if let Ok(_) = result {
|
||||
log::info!("Connected {} -> {}", external_port, our_port);
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,9 +66,8 @@ impl ConnectionManager {
|
||||
let result = self
|
||||
.jack_client
|
||||
.connect_ports_by_name(&our_port, external_port);
|
||||
match result {
|
||||
Ok(_) => log::info!("Connected {} -> {}", our_port, external_port),
|
||||
Err(_) => log::debug!("Could not connect {} -> {}", our_port, external_port),
|
||||
if let Ok(_) = result {
|
||||
log::info!("Connected {} -> {}", our_port, external_port);
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,9 +76,8 @@ impl ConnectionManager {
|
||||
let result = self
|
||||
.jack_client
|
||||
.connect_ports_by_name(&our_port, external_port);
|
||||
match result {
|
||||
Ok(_) => log::info!("Connected {} -> {}", our_port, external_port),
|
||||
Err(_) => log::debug!("Could not connect {} -> {}", our_port, external_port),
|
||||
if let Ok(_) = result {
|
||||
log::info!("Connected {} -> {}", our_port, external_port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,9 @@ pub enum LooperError {
|
||||
|
||||
#[error("Failed to connect to JACK")]
|
||||
JackConnection(&'static std::panic::Location<'static>),
|
||||
|
||||
#[error("Irrecoverable XRUN")]
|
||||
Xrun(&'static std::panic::Location<'static>),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, LooperError>;
|
||||
|
||||
104
src/metronome.rs
104
src/metronome.rs
@ -2,61 +2,95 @@ use crate::*;
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
pub struct Metronome {
|
||||
// Audio playback
|
||||
beep_samples: Arc<AudioChunk>,
|
||||
is_playing: bool,
|
||||
playback_position: usize,
|
||||
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>) -> Self {
|
||||
pub fn new(beep_samples: Arc<AudioChunk>, state: &State) -> Self {
|
||||
Self {
|
||||
beep_samples,
|
||||
is_playing: false,
|
||||
playback_position: 0,
|
||||
click_volume: state.metronome.click_volume,
|
||||
samples_per_beat: state.metronome.samples_per_beat,
|
||||
beat_time: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger a single beep
|
||||
pub fn trigger_beep(&mut self) {
|
||||
self.is_playing = true;
|
||||
self.playback_position = 0;
|
||||
}
|
||||
|
||||
/// Process audio for current buffer, writing to output slice
|
||||
pub fn process_audio(&mut self, output: &mut [f32]) -> Result<()> {
|
||||
// Clear output buffer first
|
||||
for sample in output.iter_mut() {
|
||||
*sample = 0.0;
|
||||
/// 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();
|
||||
}
|
||||
|
||||
// If not playing, output silence
|
||||
if !self.is_playing {
|
||||
return Ok(());
|
||||
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()));
|
||||
}
|
||||
|
||||
// Copy samples from beep_samples to output
|
||||
let beep_length = self.beep_samples.as_ref().sample_count;
|
||||
let output_length = output.len();
|
||||
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
|
||||
};
|
||||
|
||||
let mut output_index = 0;
|
||||
while output_index < output_length && self.playback_position < beep_length {
|
||||
// Copy one sample at a time
|
||||
let mut temp_buffer = [0.0f32; 1];
|
||||
self.beep_samples.copy_samples(&mut temp_buffer, self.playback_position)?;
|
||||
output[output_index] = temp_buffer[0];
|
||||
// Get output buffer for click track
|
||||
let click_output = ports.click_track_out.as_mut_slice(ps);
|
||||
let buffer_size = click_output.len();
|
||||
|
||||
output_index += 1;
|
||||
self.playback_position += 1;
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Stop playing if we've reached the end of the beep
|
||||
if self.playback_position >= beep_length {
|
||||
self.is_playing = false;
|
||||
// Fill remaining buffer with silence
|
||||
click_output[(beat_offset + samples_to_write)..].fill(0.0);
|
||||
}
|
||||
// TODO: Update playback_position
|
||||
// TODO: Stop when beep is complete
|
||||
} 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);
|
||||
|
||||
Ok(())
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,9 +13,7 @@ pub enum JackNotification {
|
||||
port_b: String,
|
||||
connected: bool,
|
||||
},
|
||||
PortRegistered {
|
||||
port_name: String,
|
||||
},
|
||||
PortRegistered {},
|
||||
}
|
||||
|
||||
impl NotificationHandler {
|
||||
@ -74,7 +72,7 @@ impl jack::NotificationHandler for NotificationHandler {
|
||||
};
|
||||
|
||||
if register {
|
||||
let notification = JackNotification::PortRegistered { port_name };
|
||||
let notification = JackNotification::PortRegistered {};
|
||||
self.channel
|
||||
.send(notification)
|
||||
.expect("Could not send port registration notification");
|
||||
|
||||
@ -10,13 +10,18 @@ pub struct ProcessHandler<F: ChunkFactory> {
|
||||
}
|
||||
|
||||
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
pub fn new(ports: JackPorts, mut chunk_factory: F, beep_samples: Arc<AudioChunk>) -> Result<Self> {
|
||||
pub fn new(
|
||||
ports: JackPorts,
|
||||
mut chunk_factory: F,
|
||||
beep_samples: Arc<AudioChunk>,
|
||||
state: &State,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
track: Track::new(&mut chunk_factory)?,
|
||||
playback_position: 0,
|
||||
ports,
|
||||
chunk_factory,
|
||||
metronome: Metronome::new(beep_samples),
|
||||
metronome: Metronome::new(beep_samples, state),
|
||||
})
|
||||
}
|
||||
|
||||
@ -42,9 +47,6 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
|
||||
/// Handle play/mute toggle button (Button 2)
|
||||
pub fn play_toggle(&mut self) -> Result<()> {
|
||||
// Trigger beep for testing
|
||||
self.metronome.trigger_beep();
|
||||
|
||||
match self.track.state() {
|
||||
TrackState::Idle | TrackState::Recording => {
|
||||
if self.track.len() > 0 {
|
||||
@ -63,21 +65,21 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||
fn process(&mut self, client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
||||
// Process MIDI first
|
||||
if midi::process_events(self, ps).is_err() {
|
||||
if let Err(e) = midi::process_events(self, ps) {
|
||||
log::error!("Error processing MIDI events: {}", e);
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
|
||||
if let Err(e) = self.metronome.process(ps, &mut self.ports) {
|
||||
log::error!("Error processing metronome: {}", e);
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
|
||||
let input_buffer = self.ports.audio_in.as_slice(ps);
|
||||
let output_buffer = self.ports.audio_out.as_mut_slice(ps);
|
||||
let click_track_buffer = self.ports.click_track_out.as_mut_slice(ps);
|
||||
|
||||
let jack_buffer_size = client.buffer_size() as usize;
|
||||
|
||||
// Process metronome/click track
|
||||
if self.metronome.process_audio(click_track_buffer).is_err() {
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
|
||||
let mut index = 0;
|
||||
|
||||
while index < jack_buffer_size {
|
||||
@ -86,11 +88,11 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||
// Recording - continue until manually stopped
|
||||
let sample_count_to_append = jack_buffer_size - index;
|
||||
let samples_to_append = &input_buffer[index..index + sample_count_to_append];
|
||||
if self
|
||||
if let Err(e) = self
|
||||
.track
|
||||
.append_samples(samples_to_append, &mut self.chunk_factory)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Error appending samples: {}", e);
|
||||
return jack::Control::Quit;
|
||||
};
|
||||
output_buffer[index..(index + sample_count_to_append)].fill(0.0);
|
||||
@ -101,14 +103,14 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||
let sample_count_to_play = jack_buffer_size - index;
|
||||
let sample_count_to_play =
|
||||
sample_count_to_play.min(self.track.len() - self.playback_position);
|
||||
if self
|
||||
if let Err(e) = self
|
||||
.track
|
||||
.copy_samples(
|
||||
&mut output_buffer[index..(index + sample_count_to_play)],
|
||||
self.playback_position,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Error copying samples: {}", e);
|
||||
return jack::Control::Quit;
|
||||
}
|
||||
index += sample_count_to_play;
|
||||
|
||||
14
src/state.rs
14
src/state.rs
@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct State {
|
||||
pub connections: ConnectionState,
|
||||
pub metronome: MetronomeState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -13,6 +14,12 @@ pub struct ConnectionState {
|
||||
pub click_track_out: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MetronomeState {
|
||||
pub samples_per_beat: u32,
|
||||
pub click_volume: f32, // 0.0 to 1.0
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@ -22,6 +29,13 @@ impl Default for State {
|
||||
audio_out: Vec::new(),
|
||||
click_track_out: Vec::new(),
|
||||
},
|
||||
metronome: MetronomeState {
|
||||
samples_per_beat: 96000, // 120 BPM at 192kHz sample rate
|
||||
click_volume: 0.5, // Default 50% volume
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user