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 UMC404HD**: Audio interface for input/output
- **Raspberry Pi**: Running Linux
- **Single Board Computer**: Running Linux
- **TFT Display**: 215x135mm color display, no touchscreen
- **Audio**: JACK audio connect kit

View File

@ -3,6 +3,7 @@ mod audio_chunk;
mod chunk_factory;
mod connection_manager;
mod looper_error;
mod metronome;
mod midi;
mod notification_handler;
mod process_handler;
@ -18,6 +19,7 @@ use chunk_factory::ChunkFactory;
use connection_manager::ConnectionManager;
use looper_error::LooperError;
use looper_error::Result;
use metronome::generate_beep;
use notification_handler::JackNotification;
use notification_handler::NotificationHandler;
use process_handler::ProcessHandler;
@ -29,6 +31,7 @@ use track::TrackState;
pub struct JackPorts {
pub audio_in: jack::Port<jack::AudioIn>,
pub audio_out: jack::Port<jack::AudioOut>,
pub click_track_out: jack::Port<jack::AudioOut>,
pub midi_in: jack::Port<jack::MidiIn>,
}
@ -40,10 +43,13 @@ async fn main() {
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 =
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 mut notification_channel = notification_handler.subscribe();
@ -102,6 +108,9 @@ fn setup_jack() -> (jack::Client, JackPorts) {
let audio_out = jack_client
.register_port("audio_out", jack::AudioOut::default())
.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
.register_port("midi_in", jack::MidiIn::default())
.expect("Could not create midi_in port");
@ -109,6 +118,7 @@ fn setup_jack() -> (jack::Client, JackPorts) {
let ports = JackPorts {
audio_in,
audio_out,
click_track_out,
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::metronome::Metronome;
pub struct ProcessHandler<F: ChunkFactory> {
track: Track,
playback_position: usize,
pub ports: JackPorts,
chunk_factory: F,
metronome: Metronome,
}
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 {
track: Track::new(&mut chunk_factory)?,
playback_position: 0,
ports,
chunk_factory,
metronome: Metronome::new(beep_samples),
})
}
@ -39,6 +42,9 @@ 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,9 +69,15 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
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 {