Generate metronome beep

This commit is contained in:
Geens 2025-06-06 19:24:21 +02:00
parent b296b752b3
commit 21f835f6b2
4 changed files with 111 additions and 4 deletions

View File

@ -9,7 +9,7 @@ The system provides a 5x5 matrix of loop tracks organized in columns and rows, w
- **Behringer FCB1010**: MIDI foot controller with 10 footswitches (1-10), UP/DOWN buttons, and 2 expression pedals - **Behringer FCB1010**: MIDI foot controller with 10 footswitches (1-10), UP/DOWN buttons, and 2 expression pedals
- **Behringer UMC404HD**: Audio interface for input/output - **Behringer UMC404HD**: Audio interface for input/output
- **Raspberry Pi**: Running Linux - **Single Board Computer**: Running Linux
- **TFT Display**: 215x135mm color display, no touchscreen - **TFT Display**: 215x135mm color display, no touchscreen
- **Audio**: JACK audio connect kit - **Audio**: JACK audio connect kit

View File

@ -3,6 +3,7 @@ mod audio_chunk;
mod chunk_factory; mod chunk_factory;
mod connection_manager; mod connection_manager;
mod looper_error; mod looper_error;
mod metronome;
mod midi; mod midi;
mod notification_handler; mod notification_handler;
mod process_handler; mod process_handler;
@ -18,6 +19,7 @@ use chunk_factory::ChunkFactory;
use connection_manager::ConnectionManager; use connection_manager::ConnectionManager;
use looper_error::LooperError; use looper_error::LooperError;
use looper_error::Result; use looper_error::Result;
use metronome::generate_beep;
use notification_handler::JackNotification; use notification_handler::JackNotification;
use notification_handler::NotificationHandler; use notification_handler::NotificationHandler;
use process_handler::ProcessHandler; use process_handler::ProcessHandler;
@ -29,6 +31,7 @@ use track::TrackState;
pub struct JackPorts { pub struct JackPorts {
pub audio_in: jack::Port<jack::AudioIn>, pub audio_in: jack::Port<jack::AudioIn>,
pub audio_out: jack::Port<jack::AudioOut>, pub audio_out: jack::Port<jack::AudioOut>,
pub click_track_out: jack::Port<jack::AudioOut>,
pub midi_in: jack::Port<jack::MidiIn>, pub midi_in: jack::Port<jack::MidiIn>,
} }
@ -40,10 +43,13 @@ async fn main() {
let (jack_client, ports) = setup_jack(); let (jack_client, ports) = setup_jack();
let allocator = Allocator::spawn(jack_client.sample_rate(), 3); let mut allocator = Allocator::spawn(jack_client.sample_rate(), 3);
let beep_samples = generate_beep(jack_client.sample_rate() as u32, &mut allocator)
.expect("Could not generate beep samples");
let process_handler = let process_handler =
ProcessHandler::new(ports, allocator).expect("Could not create process handler"); ProcessHandler::new(ports, allocator, beep_samples).expect("Could not create process handler");
let notification_handler = NotificationHandler::new(); let notification_handler = NotificationHandler::new();
let mut notification_channel = notification_handler.subscribe(); let mut notification_channel = notification_handler.subscribe();
@ -102,6 +108,9 @@ fn setup_jack() -> (jack::Client, JackPorts) {
let audio_out = jack_client let audio_out = jack_client
.register_port("audio_out", jack::AudioOut::default()) .register_port("audio_out", jack::AudioOut::default())
.expect("Could not create audio_out port"); .expect("Could not create audio_out port");
let click_track_out = jack_client
.register_port("click_track", jack::AudioOut::default())
.expect("Could not create click_track_out port");
let midi_in = jack_client let midi_in = jack_client
.register_port("midi_in", jack::MidiIn::default()) .register_port("midi_in", jack::MidiIn::default())
.expect("Could not create midi_in port"); .expect("Could not create midi_in port");
@ -109,6 +118,7 @@ fn setup_jack() -> (jack::Client, JackPorts) {
let ports = JackPorts { let ports = JackPorts {
audio_in, audio_in,
audio_out, audio_out,
click_track_out,
midi_in, midi_in,
}; };

85
src/metronome.rs Normal file
View File

@ -0,0 +1,85 @@
use crate::*;
use std::f32::consts::TAU;
pub struct Metronome {
beep_samples: Arc<AudioChunk>,
is_playing: bool,
playback_position: usize,
}
impl Metronome {
pub fn new(beep_samples: Arc<AudioChunk>) -> Self {
Self {
beep_samples,
is_playing: false,
playback_position: 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;
}
// If not playing, output silence
if !self.is_playing {
return Ok(());
}
// Copy samples from beep_samples to output
let beep_length = self.beep_samples.as_ref().sample_count;
let output_length = output.len();
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];
output_index += 1;
self.playback_position += 1;
}
// Stop playing if we've reached the end of the beep
if self.playback_position >= beep_length {
self.is_playing = false;
}
// TODO: Update playback_position
// TODO: Stop when beep is complete
Ok(())
}
}
/// 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)
}

View File

@ -1,19 +1,22 @@
use crate::*; use crate::*;
use crate::metronome::Metronome;
pub struct ProcessHandler<F: ChunkFactory> { pub struct ProcessHandler<F: ChunkFactory> {
track: Track, track: Track,
playback_position: usize, playback_position: usize,
pub ports: JackPorts, pub ports: JackPorts,
chunk_factory: F, chunk_factory: F,
metronome: Metronome,
} }
impl<F: ChunkFactory> ProcessHandler<F> { impl<F: ChunkFactory> ProcessHandler<F> {
pub fn new(ports: JackPorts, mut chunk_factory: F) -> Result<Self> { pub fn new(ports: JackPorts, mut chunk_factory: F, beep_samples: Arc<AudioChunk>) -> Result<Self> {
Ok(Self { Ok(Self {
track: Track::new(&mut chunk_factory)?, track: Track::new(&mut chunk_factory)?,
playback_position: 0, playback_position: 0,
ports, ports,
chunk_factory, chunk_factory,
metronome: Metronome::new(beep_samples),
}) })
} }
@ -39,6 +42,9 @@ impl<F: ChunkFactory> ProcessHandler<F> {
/// Handle play/mute toggle button (Button 2) /// Handle play/mute toggle button (Button 2)
pub fn play_toggle(&mut self) -> Result<()> { pub fn play_toggle(&mut self) -> Result<()> {
// Trigger beep for testing
self.metronome.trigger_beep();
match self.track.state() { match self.track.state() {
TrackState::Idle | TrackState::Recording => { TrackState::Idle | TrackState::Recording => {
if self.track.len() > 0 { if self.track.len() > 0 {
@ -63,9 +69,15 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
let input_buffer = self.ports.audio_in.as_slice(ps); let input_buffer = self.ports.audio_in.as_slice(ps);
let output_buffer = self.ports.audio_out.as_mut_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; 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; let mut index = 0;
while index < jack_buffer_size { while index < jack_buffer_size {