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 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
|
||||||
|
|
||||||
|
|||||||
@ -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
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::*;
|
||||||
|
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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user