Generate metronome beep
This commit is contained in:
parent
b296b752b3
commit
21f835f6b2
@ -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
|
||||
|
||||
|
||||
@ -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
85
src/metronome.rs
Normal 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)
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user