Clean metronome and add unit test for sync_offset issue

This commit is contained in:
geens 2025-08-17 23:05:57 +02:00
parent 58267eb112
commit 8360429954
12 changed files with 1320 additions and 515 deletions

View File

@ -36,3 +36,10 @@ fn default_config_path() -> String {
path.push("state.json");
path.to_string_lossy().to_string()
}
#[cfg(test)]
impl Default for Args {
fn default() -> Self {
Self::parse_from([std::ffi::OsString::new();0])
}
}

View File

@ -0,0 +1,99 @@
use crate::{JackPorts, Result as LooperResult};
use jack::{Frames, RawMidi, ProcessScope};
/// A thin abstraction over the data provided by the audio backend for one process cycle.
/// This allows the `ProcessHandler`'s core logic to be tested without a live JACK server.
pub trait AudioBackend<'a> {
fn n_frames(&self) -> Frames;
fn last_frame_time(&self) -> u32;
fn audio_input(&self) -> &[f32];
fn audio_output(&mut self) -> &mut [f32];
fn click_output(&mut self) -> &mut [f32];
fn midi_input(&self) -> impl Iterator<Item = RawMidi<'_>>;
fn midi_output(&mut self, time: u32, bytes: &[u8]) -> LooperResult<()>;
}
pub struct JackAudioBackend<'a> {
scope: &'a ProcessScope,
ports: &'a mut JackPorts,
}
impl<'a> JackAudioBackend<'a> {
pub fn new(scope: &'a ProcessScope, ports: &'a mut JackPorts) -> Self {
Self { scope, ports }
}
}
impl<'a> AudioBackend<'a> for JackAudioBackend<'a> {
fn n_frames(&self) -> Frames {
self.scope.n_frames()
}
fn last_frame_time(&self) -> u32 {
self.scope.last_frame_time()
}
fn audio_input(&self) -> &[f32] {
self.ports.audio_in.as_slice(self.scope)
}
fn audio_output(&mut self) -> &mut [f32] {
self.ports.audio_out.as_mut_slice(self.scope)
}
fn click_output(&mut self) -> &mut [f32] {
self.ports.click_track_out.as_mut_slice(self.scope)
}
fn midi_input(&self) -> impl Iterator<Item = RawMidi<'_>> {
self.ports.midi_in.iter(self.scope)
}
fn midi_output(&mut self, time: u32, bytes: &[u8]) -> LooperResult<()> {
let mut writer = self.ports.midi_out.writer(self.scope);
writer.write(&RawMidi { time, bytes })
.map_err(|_| crate::LooperError::Midi(std::panic::Location::caller()))
}
}
pub struct MockAudioBackend<'a> {
pub n_frames_val: Frames,
pub last_frame_time_val: u32,
pub audio_input_buffer: &'a [f32],
pub audio_output_buffer: &'a mut [f32],
pub click_output_buffer: &'a mut [f32],
pub midi_input_events: Vec<RawMidi<'a>>,
pub midi_output_events: Vec<(u32, Vec<u8>)>, // Store time and bytes
}
impl<'a> AudioBackend<'a> for MockAudioBackend<'a> {
fn n_frames(&self) -> Frames {
self.n_frames_val
}
fn last_frame_time(&self) -> u32 {
self.last_frame_time_val
}
fn audio_input(&self) -> &[f32] {
self.audio_input_buffer
}
fn audio_output(&mut self) -> &mut [f32] {
self.audio_output_buffer
}
fn click_output(&mut self) -> &mut [f32] {
self.click_output_buffer
}
fn midi_input(&self) -> impl Iterator<Item = jack::RawMidi<'_>> {
self.midi_input_events.clone().into_iter()
}
fn midi_output(&mut self, time: u32, bytes: &[u8]) -> LooperResult<()> {
self.midi_output_events.push((time, bytes.to_vec()));
Ok(())
}
}

View File

@ -1,9 +1,9 @@
use crate::*;
pub struct Column<const ROWS: usize> {
frames_per_beat: usize,
tracks: [Track; ROWS],
playback_position: usize,
pub(crate) frames_per_beat: usize,
pub(crate) tracks: [Track; ROWS],
pub(crate) playback_position: usize,
}
impl<const ROWS: usize> Column<ROWS> {
@ -117,11 +117,10 @@ impl<const ROWS: usize> Column<ROWS> {
Ok(())
}
pub fn process(
pub fn process<'a, A: AudioBackend<'a>>(
&mut self,
audio_backend: &mut A,
timing: &BufferTiming,
input_buffer: &[f32],
output_buffer: &mut [f32],
scratch_pad: &mut [f32],
chunk_factory: &mut impl ChunkFactory,
controllers: &ColumnControllers,
@ -135,7 +134,7 @@ impl<const ROWS: usize> Column<ROWS> {
if old_len == 0 {
self.playback_position = 0; // Start at beat 0 for first recording
} else {
let idle_time = input_buffer.len() - beat_index as usize;
let idle_time = audio_backend.audio_input().len() - beat_index as usize;
self.playback_position = old_len - idle_time;
}
}
@ -145,15 +144,15 @@ impl<const ROWS: usize> Column<ROWS> {
let track_controllers = controllers.to_track_controllers(row);
track.process(
audio_backend,
self.playback_position,
timing.beat_in_buffer,
input_buffer,
scratch_pad,
chunk_factory,
&track_controllers,
)?;
for (output_val, scratch_pad_val) in output_buffer.iter_mut().zip(scratch_pad.iter()) {
for (output_val, scratch_pad_val) in audio_backend.audio_output().iter_mut().zip(scratch_pad.iter()) {
*output_val += *scratch_pad_val;
}
}
@ -167,11 +166,11 @@ impl<const ROWS: usize> Column<ROWS> {
// Update playback position
if new_len > 0 {
self.playback_position = (self.playback_position + input_buffer.len()) % new_len;
self.playback_position = (self.playback_position + audio_backend.audio_input().len()) % new_len;
} else {
// During recording of first track, don't use modulo - let position grow
// so that beat calculation works correctly
self.playback_position += input_buffer.len();
self.playback_position += audio_backend.audio_input().len();
}
// Send beat change message if a beat occurred in this buffer

View File

@ -1,5 +1,6 @@
mod allocator;
mod args;
mod audio_backend;
mod audio_chunk;
mod audio_data;
mod beep;
@ -24,6 +25,7 @@ use std::sync::Arc;
use allocator::Allocator;
use args::Args;
use audio_backend::AudioBackend;
use audio_chunk::AudioChunk;
use audio_data::AudioData;
use beep::generate_beep;
@ -47,12 +49,15 @@ use post_record_handler::PostRecordController;
use post_record_handler::PostRecordHandler;
use post_record_handler::PostRecordResponse;
use process_handler::ProcessHandler;
use state::MetronomeState;
use state::State;
use tap_tempo::TapTempo;
use track::Track;
use track::TrackState;
use track_matrix::TrackMatrix;
use crate::audio_backend::JackAudioBackend;
const COLS: usize = 5;
const ROWS: usize = 5;
@ -71,7 +76,7 @@ async fn main() {
.init()
.expect("Could not initialize logger");
let (jack_client, ports) = setup_jack();
let (jack_client, mut ports) = setup_jack();
let mut allocator = Allocator::spawn(jack_client.sample_rate(), 3);
@ -97,9 +102,9 @@ async fn main() {
let (mut post_record_handler, post_record_controller) =
PostRecordHandler::new(&args).expect("Could not create post-record handler");
let process_handler = ProcessHandler::<_, COLS, ROWS>::new(
&jack_client,
ports,
let mut process_handler = ProcessHandler::<_, COLS, ROWS>::new(
jack_client.sample_rate(),
jack_client.buffer_size() as usize,
allocator,
beep_samples,
&state.borrow(),
@ -117,7 +122,18 @@ async fn main() {
.expect("Could not create connection manager");
let _active_client = jack_client
.activate_async(notification_handler, process_handler)
.activate_async(
notification_handler,
jack::contrib::ClosureProcessHandler::new(move |_client, process_scope| {
let mut audio_backend = JackAudioBackend::new(process_scope, &mut ports);
if let Err(e) = process_handler.process(&mut audio_backend) {
log::error!("Error processing audio: {}", e);
jack::Control::Quit
} else {
jack::Control::Continue
}
})
)
.expect("Could not activate Jack");
loop {
@ -193,3 +209,12 @@ fn setup_jack() -> (jack::Client, JackPorts) {
(jack_client, ports)
}
/*
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> jack::ProcessHandler
for ProcessHandler<F, COLS, ROWS>
{
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
}
}
*/

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
use crate::*;
/// Process MIDI events
pub fn process_events<F: ChunkFactory, const COLS: usize, const ROWS: usize>(
pub fn process_events<'a, A: AudioBackend<'a>, F: ChunkFactory, const COLS: usize, const ROWS: usize>(
audio_backend: &mut A,
process_handler: &mut ProcessHandler<F, COLS, ROWS>,
ps: &jack::ProcessScope,
) -> Result<()> {
// First, collect all MIDI events into a fixed-size array
// This avoids allocations while solving borrow checker issues
@ -12,7 +12,7 @@ pub fn process_events<F: ChunkFactory, const COLS: usize, const ROWS: usize>(
let mut event_count = 0;
// Collect events from the MIDI input iterator
let midi_input = process_handler.ports().midi_in.iter(ps);
let midi_input = audio_backend.midi_input();
for midi_event in midi_input {
if event_count < MAX_EVENTS && midi_event.bytes.len() >= 3 {
raw_events[event_count][0] = midi_event.bytes[0];
@ -59,7 +59,7 @@ pub fn process_events<F: ChunkFactory, const COLS: usize, const ROWS: usize>(
process_handler.handle_button_4()?;
}
24 if value_num > 0 => {
process_handler.handle_button_5(ps)?;
process_handler.handle_button_5(audio_backend)?;
}
25 if value_num > 0 => {
process_handler.handle_button_6()?;

View File

@ -4,7 +4,7 @@ use tokio::sync::{broadcast, watch};
#[derive(Debug)]
pub struct PersistenceManagerController {
sender: kanal::Sender<PersistenceUpdate>,
pub(crate) sender: kanal::Sender<PersistenceUpdate>,
}
#[derive(Debug)]

View File

@ -22,8 +22,8 @@ pub struct PostRecordResponse {
/// RT-side interface for post-record operations
#[derive(Debug)]
pub struct PostRecordController {
request_sender: kanal::Sender<PostRecordRequest>,
response_receiver: kanal::Receiver<PostRecordResponse>,
pub(crate) request_sender: kanal::Sender<PostRecordRequest>,
pub(crate) response_receiver: kanal::Receiver<PostRecordResponse>,
}
impl PostRecordController {

View File

@ -1,10 +1,9 @@
use crate::*;
use crate::{audio_backend::AudioBackend, *};
pub struct ProcessHandler<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
post_record_controller: PostRecordController,
osc: OscController,
persistence: PersistenceManagerController,
ports: JackPorts,
metronome: Metronome,
track_matrix: TrackMatrix<F, COLS, ROWS>,
selected_row: usize,
@ -16,8 +15,8 @@ pub struct ProcessHandler<F: ChunkFactory, const COLS: usize, const ROWS: usize>
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, COLS, ROWS> {
pub fn new(
client: &jack::Client,
ports: JackPorts,
sample_rate: usize,
buffer_size: usize,
chunk_factory: F,
beep_samples: Arc<AudioChunk>,
state: &State,
@ -25,26 +24,21 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
osc: OscController,
persistence: PersistenceManagerController,
) -> Result<Self> {
let track_matrix = TrackMatrix::new(client, chunk_factory, state)?;
let track_matrix = TrackMatrix::new(buffer_size, chunk_factory, state)?;
Ok(Self {
post_record_controller,
osc,
persistence,
ports,
metronome: Metronome::new(beep_samples, state),
track_matrix,
selected_row: 0,
selected_column: 0,
last_volume_setting: 1.0,
sample_rate: client.sample_rate(),
tap_tempo: TapTempo::new(client.sample_rate()),
sample_rate,
tap_tempo: TapTempo::new(sample_rate),
})
}
pub fn ports(&self) -> &JackPorts {
&self.ports
}
pub fn handle_pedal_a(&mut self, value: u8) -> Result<()> {
self.last_volume_setting = (value as f32) / 127.0;
@ -106,13 +100,13 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
self.track_matrix.handle_clear_button(&controllers)
}
pub fn handle_button_5(&mut self, ps: &jack::ProcessScope) -> Result<()> {
pub fn handle_button_5<'a, A: AudioBackend<'a>>(&mut self, audio_backend: &A) -> Result<()> {
if !self.track_matrix.is_all_tracks_cleared() {
// Ignore tap if not all tracks are clear
return Ok(());
}
let current_frame_time = ps.last_frame_time();
let current_frame_time = audio_backend.last_frame_time();
let tempo_updated = self.tap_tempo.handle_tap(current_frame_time);
@ -172,28 +166,13 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
self.osc.selected_row_changed(self.selected_row)?;
Ok(())
}
}
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> jack::ProcessHandler
for ProcessHandler<F, COLS, ROWS>
{
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
if let Err(e) = self.process_with_error_handling(ps) {
log::error!("Error processing audio: {}", e);
jack::Control::Quit
} else {
jack::Control::Continue
}
}
}
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, COLS, ROWS> {
fn process_with_error_handling(&mut self, ps: &jack::ProcessScope) -> Result<()> {
pub fn process<'a, A: AudioBackend<'a>>(&mut self, audio_backend: &mut A) -> Result<()> {
// Process metronome and get beat timing information
let timing = self.metronome.process(ps, &mut self.ports, &self.osc)?;
let timing = self.metronome.process(audio_backend, &self.osc)?;
// Process MIDI
midi::process_events(self, ps)?;
midi::process_events(audio_backend, self)?;
// Process audio
let controllers = MatrixControllers::new(
@ -203,15 +182,547 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
self.sample_rate,
);
self.track_matrix
.process(ps, &mut self.ports, &timing, &controllers)?;
.process(audio_backend, &timing, &controllers)?;
let input_buffer = self.ports.audio_in.as_slice(ps);
let output_buffer = self.ports.audio_out.as_mut_slice(ps);
for i in 0..output_buffer.len() {
output_buffer[i] = output_buffer[i] + (input_buffer[i] * self.last_volume_setting);
for i in 0..audio_backend.audio_output().len() {
audio_backend.audio_output()[i] = audio_backend.audio_output()[i] + (audio_backend.audio_input()[i] * self.last_volume_setting);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio_backend::MockAudioBackend;
use crate::chunk_factory::mock::MockFactory;
const fn button_record_midi() -> jack::RawMidi<'static> {
static BUTTON_1_MIDI_BYTES: [u8; 3] = [0xB0, 0x14, 0x7F];
let raw_midi = jack::RawMidi {
bytes: &BUTTON_1_MIDI_BYTES,
time: 0,
};
raw_midi
}
const fn button_down_midi() -> jack::RawMidi<'static> {
static BUTTON_DOWN_MIDI_BYTES: [u8; 3] = [0xB0, 0x1F, 0x7F]; // Controller 31, Value 127
let raw_midi = jack::RawMidi {
bytes: &BUTTON_DOWN_MIDI_BYTES,
time: 0,
};
raw_midi
}
/// Integration test for checking recording auto stop
/// frame beat comment
/// 0 0 Warm up, no commands here
/// 10 Press record
/// 20 24 Record track (0, 0) starts
/// 30 Record track (0, 0) first beat continues
/// 40 48 Record track (0, 0) begin second beat
/// 50 Record track (0, 0) second beat continues
/// 60 Record track (0, 0) second beat continues
/// 70 72 Record track (0, 0) 3rd beat starts
/// 80 Press record end
/// 90 96 Track (0, 0) stops recording, starts playing first beat
/// 100 Press down
/// 110 Press record
/// 120 120 Recording track (0, 1) starts recording at second column beat
/// 130 Recording track (0, 1) continues recording
/// 140 144 Track (0, 1) records for second beat, column is at 3rd beat
/// 150 Column beat 3 continues
/// 160 168 Column wraps to first beat, track (0, 1) records 3rd beat
/// 170 Continue playback and recording
/// 180 Continue playback and recording
/// 190 192 Recording track (0, 1) auto stops, mixed playback starts
/// !Consolidate frames
/// 200 Check consolidated output
#[tokio::test]
async fn test_recording_autostop_sync() {
const FRAMES_PER_BEAT: usize = 24;
const BUFFER_SIZE: usize = 10;
// Setup
let chunk_factory = MockFactory::new(
(0..50).map(|_| AudioChunk::allocate(1024)).collect()
);
let beep = Arc::new(AudioChunk {
samples: vec![1.0; 1].into_boxed_slice(),
sample_count: 1,
next: None,
});
let state = State {
metronome: MetronomeState {
frames_per_beat: FRAMES_PER_BEAT,
click_volume: 1.0,
},
..Default::default()
};
let (mut post_record_handler, post_record_controller) = PostRecordHandler::new(&Args::default()).unwrap();
let (osc_tx, _osc_rx) = kanal::bounded(64);
let osc = OscController::new(osc_tx);
let (pers_tx, _pers_rx) = kanal::bounded(64);
let persistence = PersistenceManagerController { sender: pers_tx };
let mut handler = ProcessHandler::<_, 5, 5>::new(
48000,
BUFFER_SIZE,
chunk_factory,
beep,
&state,
post_record_controller,
osc,
persistence,
).unwrap();
let audio_output_buffer = &mut [0.0; BUFFER_SIZE];
let click_output_buffer = &mut [0.0; BUFFER_SIZE];
// Warm up, no commands here
let mut last_frame_time = 0;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Empty);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Press record
last_frame_time = 10;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![button_record_midi()],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Empty);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Record track (0, 0) starts
last_frame_time = 20;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Record track (0, 0) first beat continues
last_frame_time = 30;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Record track (0, 0) begin second beat
last_frame_time = 40;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Record track (0, 0) second beat continues
last_frame_time = 50;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Record track (0, 0) second beat continues
last_frame_time = 60;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Record track (0, 0) 3rd beat starts
last_frame_time = 70;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Press record end
last_frame_time = 80;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![button_record_midi()],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Track (0, 0) stops recording, starts playing first beat
last_frame_time = 90;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == last_frame_time as f32), "expected to hear the input signal for the first part");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording started at frame 24");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Press down
last_frame_time = 100;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![button_down_midi()],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording started at frame 24, second buffer");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording started at frame 24, 3rd buffer");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Press record
last_frame_time = 110;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![button_record_midi()],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording started at frame 24, 3rd buffer");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0), "expected to hear the input + recording started at frame 24, 4th buffer");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
// Recording track (0, 1) starts recording at second column beat
last_frame_time = 120;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
// Recording track (0, 1) continues recording
last_frame_time = 130;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 70.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
// Track (0, 1) records for second beat, column is at 3rd beat
last_frame_time = 140;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 70.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 80.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
// Column beat 3 continues
last_frame_time = 150;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 80.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 90.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
// Column wraps to first beat, track (0, 1) records 3rd beat
last_frame_time = 160;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 90.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
// Continue playback and recording
last_frame_time = 170;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
// Continue playback and recording
last_frame_time = 180;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
// Recording track (0, 1) auto stops, mixed playback starts
last_frame_time = 190;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0 + 120.0), "expected to hear the input + track (0, 0) + track (0, 1)");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0 + 120.0), "expected to hear the input + track (0, 0) + track (0, 1)");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Playing);
// Process audio
let prh_result = tokio::time::timeout(std::time::Duration::from_secs(1), post_record_handler.run()).await;
assert!(prh_result.is_err(), "Expected timeout, got {prh_result:#?}");
// Playback consolidated
last_frame_time = 200;
let input = [last_frame_time as f32; BUFFER_SIZE];
let mut backend = MockAudioBackend {
n_frames_val: BUFFER_SIZE as _,
last_frame_time_val: last_frame_time,
audio_input_buffer: &input,
audio_output_buffer,
click_output_buffer,
midi_input_events: vec![],
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0 + 130.0), "expected to hear the input + track (0, 0) + track (0, 1)");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0 + 130.0), "expected to hear the input + track (0, 0) + track (0, 1)");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Playing);
}
}

View File

@ -8,7 +8,7 @@ pub struct State {
pub track_volumes: TrackVolumes,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ConnectionState {
pub midi_in: Vec<String>,
pub midi_out: Vec<String>,

View File

@ -1,10 +1,10 @@
use crate::*;
pub struct Track {
audio_data: AudioData,
current_state: TrackState,
next_state: TrackState,
volume: f32,
pub(crate) audio_data: AudioData,
pub(crate) current_state: TrackState,
pub(crate) next_state: TrackState,
pub(crate) volume: f32,
}
#[derive(Debug, Clone, PartialEq)]
@ -147,11 +147,11 @@ impl Track {
}
/// Audio processing
pub fn process(
pub fn process<'a, A: AudioBackend<'a>>(
&mut self,
audio_backend: &mut A,
playback_position: usize,
beat_in_buffer: Option<u32>,
input_buffer: &[f32],
output_buffer: &mut [f32],
chunk_factory: &mut impl ChunkFactory,
controllers: &TrackControllers,
@ -160,7 +160,7 @@ impl Track {
None => {
// No beat in this buffer - process entire buffer with current state
self.process_audio_range(
input_buffer,
audio_backend.audio_input(),
output_buffer,
playback_position,
chunk_factory,
@ -170,7 +170,7 @@ impl Track {
// Process samples before beat with current state
if beat_index_in_buffer > 0 {
self.process_audio_range(
&input_buffer[..beat_index_in_buffer as _],
&audio_backend.audio_input()[..beat_index_in_buffer as _],
&mut output_buffer[..beat_index_in_buffer as _],
playback_position,
chunk_factory,
@ -189,7 +189,7 @@ impl Track {
}
self.process_audio_range(
&input_buffer[beat_index_in_buffer as _..],
&audio_backend.audio_input()[beat_index_in_buffer as _..],
&mut output_buffer[beat_index_in_buffer as _..],
post_beat_position,
chunk_factory,

View File

@ -1,18 +1,18 @@
use crate::*;
pub struct TrackMatrix<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
chunk_factory: F,
columns: [Column<ROWS>; COLS],
scratch_pad: Box<[f32]>,
pub(crate) chunk_factory: F,
pub(crate) columns: [Column<ROWS>; COLS],
pub(crate) scratch_pad: Box<[f32]>,
}
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS, ROWS> {
pub fn new(client: &jack::Client, chunk_factory: F, state: &State) -> Result<Self> {
pub fn new(buffer_size: usize, chunk_factory: F, state: &State) -> Result<Self> {
let columns = std::array::from_fn(|_| Column::new(state.metronome.frames_per_beat));
Ok(Self {
chunk_factory,
columns,
scratch_pad: vec![0.0; client.buffer_size() as usize].into_boxed_slice(),
scratch_pad: vec![0.0; buffer_size].into_boxed_slice(),
})
}
@ -52,10 +52,9 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
}
}
pub fn process(
pub fn process<'a, A: AudioBackend<'a>>(
&mut self,
ps: &jack::ProcessScope,
ports: &mut JackPorts,
audio_backend: &mut A,
timing: &BufferTiming,
controllers: &MatrixControllers,
) -> Result<()> {
@ -75,17 +74,14 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
}
// Process audio
let input_buffer = ports.audio_in.as_slice(ps);
let output_buffer = ports.audio_out.as_mut_slice(ps);
output_buffer.fill(0.0);
audio_backend.audio_output().fill(0.0);
for (i, column) in &mut self.columns.iter_mut().enumerate() {
let controllers = controllers.to_column_controllers(i);
column.process(
audio_backend,
&timing,
input_buffer,
output_buffer,
&mut self.scratch_pad,
&mut self.chunk_factory,
&controllers,
@ -94,4 +90,4 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
Ok(())
}
}
}