Clean metronome and add unit test for sync_offset issue
This commit is contained in:
parent
58267eb112
commit
8360429954
@ -36,3 +36,10 @@ fn default_config_path() -> String {
|
|||||||
path.push("state.json");
|
path.push("state.json");
|
||||||
path.to_string_lossy().to_string()
|
path.to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl Default for Args {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::parse_from([std::ffi::OsString::new();0])
|
||||||
|
}
|
||||||
|
}
|
||||||
99
audio_engine/src/audio_backend.rs
Normal file
99
audio_engine/src/audio_backend.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub struct Column<const ROWS: usize> {
|
pub struct Column<const ROWS: usize> {
|
||||||
frames_per_beat: usize,
|
pub(crate) frames_per_beat: usize,
|
||||||
tracks: [Track; ROWS],
|
pub(crate) tracks: [Track; ROWS],
|
||||||
playback_position: usize,
|
pub(crate) playback_position: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const ROWS: usize> Column<ROWS> {
|
impl<const ROWS: usize> Column<ROWS> {
|
||||||
@ -117,11 +117,10 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process(
|
pub fn process<'a, A: AudioBackend<'a>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
audio_backend: &mut A,
|
||||||
timing: &BufferTiming,
|
timing: &BufferTiming,
|
||||||
input_buffer: &[f32],
|
|
||||||
output_buffer: &mut [f32],
|
|
||||||
scratch_pad: &mut [f32],
|
scratch_pad: &mut [f32],
|
||||||
chunk_factory: &mut impl ChunkFactory,
|
chunk_factory: &mut impl ChunkFactory,
|
||||||
controllers: &ColumnControllers,
|
controllers: &ColumnControllers,
|
||||||
@ -135,7 +134,7 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
if old_len == 0 {
|
if old_len == 0 {
|
||||||
self.playback_position = 0; // Start at beat 0 for first recording
|
self.playback_position = 0; // Start at beat 0 for first recording
|
||||||
} else {
|
} 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;
|
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);
|
let track_controllers = controllers.to_track_controllers(row);
|
||||||
|
|
||||||
track.process(
|
track.process(
|
||||||
|
audio_backend,
|
||||||
self.playback_position,
|
self.playback_position,
|
||||||
timing.beat_in_buffer,
|
timing.beat_in_buffer,
|
||||||
input_buffer,
|
|
||||||
scratch_pad,
|
scratch_pad,
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
&track_controllers,
|
&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;
|
*output_val += *scratch_pad_val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,11 +166,11 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
|
|
||||||
// Update playback position
|
// Update playback position
|
||||||
if new_len > 0 {
|
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 {
|
} else {
|
||||||
// During recording of first track, don't use modulo - let position grow
|
// During recording of first track, don't use modulo - let position grow
|
||||||
// so that beat calculation works correctly
|
// 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
|
// Send beat change message if a beat occurred in this buffer
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
mod allocator;
|
mod allocator;
|
||||||
mod args;
|
mod args;
|
||||||
|
mod audio_backend;
|
||||||
mod audio_chunk;
|
mod audio_chunk;
|
||||||
mod audio_data;
|
mod audio_data;
|
||||||
mod beep;
|
mod beep;
|
||||||
@ -24,6 +25,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use allocator::Allocator;
|
use allocator::Allocator;
|
||||||
use args::Args;
|
use args::Args;
|
||||||
|
use audio_backend::AudioBackend;
|
||||||
use audio_chunk::AudioChunk;
|
use audio_chunk::AudioChunk;
|
||||||
use audio_data::AudioData;
|
use audio_data::AudioData;
|
||||||
use beep::generate_beep;
|
use beep::generate_beep;
|
||||||
@ -47,12 +49,15 @@ use post_record_handler::PostRecordController;
|
|||||||
use post_record_handler::PostRecordHandler;
|
use post_record_handler::PostRecordHandler;
|
||||||
use post_record_handler::PostRecordResponse;
|
use post_record_handler::PostRecordResponse;
|
||||||
use process_handler::ProcessHandler;
|
use process_handler::ProcessHandler;
|
||||||
|
use state::MetronomeState;
|
||||||
use state::State;
|
use state::State;
|
||||||
use tap_tempo::TapTempo;
|
use tap_tempo::TapTempo;
|
||||||
use track::Track;
|
use track::Track;
|
||||||
use track::TrackState;
|
use track::TrackState;
|
||||||
use track_matrix::TrackMatrix;
|
use track_matrix::TrackMatrix;
|
||||||
|
|
||||||
|
use crate::audio_backend::JackAudioBackend;
|
||||||
|
|
||||||
const COLS: usize = 5;
|
const COLS: usize = 5;
|
||||||
const ROWS: usize = 5;
|
const ROWS: usize = 5;
|
||||||
|
|
||||||
@ -71,7 +76,7 @@ async fn main() {
|
|||||||
.init()
|
.init()
|
||||||
.expect("Could not initialize logger");
|
.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);
|
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) =
|
let (mut post_record_handler, post_record_controller) =
|
||||||
PostRecordHandler::new(&args).expect("Could not create post-record handler");
|
PostRecordHandler::new(&args).expect("Could not create post-record handler");
|
||||||
|
|
||||||
let process_handler = ProcessHandler::<_, COLS, ROWS>::new(
|
let mut process_handler = ProcessHandler::<_, COLS, ROWS>::new(
|
||||||
&jack_client,
|
jack_client.sample_rate(),
|
||||||
ports,
|
jack_client.buffer_size() as usize,
|
||||||
allocator,
|
allocator,
|
||||||
beep_samples,
|
beep_samples,
|
||||||
&state.borrow(),
|
&state.borrow(),
|
||||||
@ -117,7 +122,18 @@ async fn main() {
|
|||||||
.expect("Could not create connection manager");
|
.expect("Could not create connection manager");
|
||||||
|
|
||||||
let _active_client = jack_client
|
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");
|
.expect("Could not activate Jack");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@ -193,3 +209,12 @@ fn setup_jack() -> (jack::Client, JackPorts) {
|
|||||||
|
|
||||||
(jack_client, ports)
|
(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
@ -1,9 +1,9 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Process MIDI events
|
/// 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>,
|
process_handler: &mut ProcessHandler<F, COLS, ROWS>,
|
||||||
ps: &jack::ProcessScope,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// First, collect all MIDI events into a fixed-size array
|
// First, collect all MIDI events into a fixed-size array
|
||||||
// This avoids allocations while solving borrow checker issues
|
// 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;
|
let mut event_count = 0;
|
||||||
|
|
||||||
// Collect events from the MIDI input iterator
|
// 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 {
|
for midi_event in midi_input {
|
||||||
if event_count < MAX_EVENTS && midi_event.bytes.len() >= 3 {
|
if event_count < MAX_EVENTS && midi_event.bytes.len() >= 3 {
|
||||||
raw_events[event_count][0] = midi_event.bytes[0];
|
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()?;
|
process_handler.handle_button_4()?;
|
||||||
}
|
}
|
||||||
24 if value_num > 0 => {
|
24 if value_num > 0 => {
|
||||||
process_handler.handle_button_5(ps)?;
|
process_handler.handle_button_5(audio_backend)?;
|
||||||
}
|
}
|
||||||
25 if value_num > 0 => {
|
25 if value_num > 0 => {
|
||||||
process_handler.handle_button_6()?;
|
process_handler.handle_button_6()?;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use tokio::sync::{broadcast, watch};
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PersistenceManagerController {
|
pub struct PersistenceManagerController {
|
||||||
sender: kanal::Sender<PersistenceUpdate>,
|
pub(crate) sender: kanal::Sender<PersistenceUpdate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@ -22,8 +22,8 @@ pub struct PostRecordResponse {
|
|||||||
/// RT-side interface for post-record operations
|
/// RT-side interface for post-record operations
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PostRecordController {
|
pub struct PostRecordController {
|
||||||
request_sender: kanal::Sender<PostRecordRequest>,
|
pub(crate) request_sender: kanal::Sender<PostRecordRequest>,
|
||||||
response_receiver: kanal::Receiver<PostRecordResponse>,
|
pub(crate) response_receiver: kanal::Receiver<PostRecordResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostRecordController {
|
impl PostRecordController {
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
use crate::*;
|
use crate::{audio_backend::AudioBackend, *};
|
||||||
|
|
||||||
pub struct ProcessHandler<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
|
pub struct ProcessHandler<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
|
||||||
post_record_controller: PostRecordController,
|
post_record_controller: PostRecordController,
|
||||||
osc: OscController,
|
osc: OscController,
|
||||||
persistence: PersistenceManagerController,
|
persistence: PersistenceManagerController,
|
||||||
ports: JackPorts,
|
|
||||||
metronome: Metronome,
|
metronome: Metronome,
|
||||||
track_matrix: TrackMatrix<F, COLS, ROWS>,
|
track_matrix: TrackMatrix<F, COLS, ROWS>,
|
||||||
selected_row: usize,
|
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> {
|
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, COLS, ROWS> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
client: &jack::Client,
|
sample_rate: usize,
|
||||||
ports: JackPorts,
|
buffer_size: usize,
|
||||||
chunk_factory: F,
|
chunk_factory: F,
|
||||||
beep_samples: Arc<AudioChunk>,
|
beep_samples: Arc<AudioChunk>,
|
||||||
state: &State,
|
state: &State,
|
||||||
@ -25,26 +24,21 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
|||||||
osc: OscController,
|
osc: OscController,
|
||||||
persistence: PersistenceManagerController,
|
persistence: PersistenceManagerController,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let track_matrix = TrackMatrix::new(client, chunk_factory, state)?;
|
let track_matrix = TrackMatrix::new(buffer_size, chunk_factory, state)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
post_record_controller,
|
post_record_controller,
|
||||||
osc,
|
osc,
|
||||||
persistence,
|
persistence,
|
||||||
ports,
|
|
||||||
metronome: Metronome::new(beep_samples, state),
|
metronome: Metronome::new(beep_samples, state),
|
||||||
track_matrix,
|
track_matrix,
|
||||||
selected_row: 0,
|
selected_row: 0,
|
||||||
selected_column: 0,
|
selected_column: 0,
|
||||||
last_volume_setting: 1.0,
|
last_volume_setting: 1.0,
|
||||||
sample_rate: client.sample_rate(),
|
sample_rate,
|
||||||
tap_tempo: TapTempo::new(client.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<()> {
|
pub fn handle_pedal_a(&mut self, value: u8) -> Result<()> {
|
||||||
self.last_volume_setting = (value as f32) / 127.0;
|
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)
|
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() {
|
if !self.track_matrix.is_all_tracks_cleared() {
|
||||||
// Ignore tap if not all tracks are clear
|
// Ignore tap if not all tracks are clear
|
||||||
return Ok(());
|
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);
|
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)?;
|
self.osc.selected_row_changed(self.selected_row)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> jack::ProcessHandler
|
pub fn process<'a, A: AudioBackend<'a>>(&mut self, audio_backend: &mut A) -> Result<()> {
|
||||||
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<()> {
|
|
||||||
// Process metronome and get beat timing information
|
// 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
|
// Process MIDI
|
||||||
midi::process_events(self, ps)?;
|
midi::process_events(audio_backend, self)?;
|
||||||
|
|
||||||
// Process audio
|
// Process audio
|
||||||
let controllers = MatrixControllers::new(
|
let controllers = MatrixControllers::new(
|
||||||
@ -203,15 +182,547 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
|||||||
self.sample_rate,
|
self.sample_rate,
|
||||||
);
|
);
|
||||||
self.track_matrix
|
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);
|
for i in 0..audio_backend.audio_output().len() {
|
||||||
let output_buffer = self.ports.audio_out.as_mut_slice(ps);
|
audio_backend.audio_output()[i] = audio_backend.audio_output()[i] + (audio_backend.audio_input()[i] * self.last_volume_setting);
|
||||||
|
|
||||||
for i in 0..output_buffer.len() {
|
|
||||||
output_buffer[i] = output_buffer[i] + (input_buffer[i] * self.last_volume_setting);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ pub struct State {
|
|||||||
pub track_volumes: TrackVolumes,
|
pub track_volumes: TrackVolumes,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct ConnectionState {
|
pub struct ConnectionState {
|
||||||
pub midi_in: Vec<String>,
|
pub midi_in: Vec<String>,
|
||||||
pub midi_out: Vec<String>,
|
pub midi_out: Vec<String>,
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
audio_data: AudioData,
|
pub(crate) audio_data: AudioData,
|
||||||
current_state: TrackState,
|
pub(crate) current_state: TrackState,
|
||||||
next_state: TrackState,
|
pub(crate) next_state: TrackState,
|
||||||
volume: f32,
|
pub(crate) volume: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@ -147,11 +147,11 @@ impl Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Audio processing
|
/// Audio processing
|
||||||
pub fn process(
|
pub fn process<'a, A: AudioBackend<'a>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
audio_backend: &mut A,
|
||||||
playback_position: usize,
|
playback_position: usize,
|
||||||
beat_in_buffer: Option<u32>,
|
beat_in_buffer: Option<u32>,
|
||||||
input_buffer: &[f32],
|
|
||||||
output_buffer: &mut [f32],
|
output_buffer: &mut [f32],
|
||||||
chunk_factory: &mut impl ChunkFactory,
|
chunk_factory: &mut impl ChunkFactory,
|
||||||
controllers: &TrackControllers,
|
controllers: &TrackControllers,
|
||||||
@ -160,7 +160,7 @@ impl Track {
|
|||||||
None => {
|
None => {
|
||||||
// No beat in this buffer - process entire buffer with current state
|
// No beat in this buffer - process entire buffer with current state
|
||||||
self.process_audio_range(
|
self.process_audio_range(
|
||||||
input_buffer,
|
audio_backend.audio_input(),
|
||||||
output_buffer,
|
output_buffer,
|
||||||
playback_position,
|
playback_position,
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
@ -170,7 +170,7 @@ impl Track {
|
|||||||
// Process samples before beat with current state
|
// Process samples before beat with current state
|
||||||
if beat_index_in_buffer > 0 {
|
if beat_index_in_buffer > 0 {
|
||||||
self.process_audio_range(
|
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 _],
|
&mut output_buffer[..beat_index_in_buffer as _],
|
||||||
playback_position,
|
playback_position,
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
@ -189,7 +189,7 @@ impl Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.process_audio_range(
|
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 _..],
|
&mut output_buffer[beat_index_in_buffer as _..],
|
||||||
post_beat_position,
|
post_beat_position,
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub struct TrackMatrix<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
|
pub struct TrackMatrix<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
|
||||||
chunk_factory: F,
|
pub(crate) chunk_factory: F,
|
||||||
columns: [Column<ROWS>; COLS],
|
pub(crate) columns: [Column<ROWS>; COLS],
|
||||||
scratch_pad: Box<[f32]>,
|
pub(crate) scratch_pad: Box<[f32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS, ROWS> {
|
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));
|
let columns = std::array::from_fn(|_| Column::new(state.metronome.frames_per_beat));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
chunk_factory,
|
chunk_factory,
|
||||||
columns,
|
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,
|
&mut self,
|
||||||
ps: &jack::ProcessScope,
|
audio_backend: &mut A,
|
||||||
ports: &mut JackPorts,
|
|
||||||
timing: &BufferTiming,
|
timing: &BufferTiming,
|
||||||
controllers: &MatrixControllers,
|
controllers: &MatrixControllers,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@ -75,17 +74,14 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process audio
|
// Process audio
|
||||||
let input_buffer = ports.audio_in.as_slice(ps);
|
audio_backend.audio_output().fill(0.0);
|
||||||
let output_buffer = ports.audio_out.as_mut_slice(ps);
|
|
||||||
output_buffer.fill(0.0);
|
|
||||||
|
|
||||||
for (i, column) in &mut self.columns.iter_mut().enumerate() {
|
for (i, column) in &mut self.columns.iter_mut().enumerate() {
|
||||||
let controllers = controllers.to_column_controllers(i);
|
let controllers = controllers.to_column_controllers(i);
|
||||||
|
|
||||||
column.process(
|
column.process(
|
||||||
|
audio_backend,
|
||||||
&timing,
|
&timing,
|
||||||
input_buffer,
|
|
||||||
output_buffer,
|
|
||||||
&mut self.scratch_pad,
|
&mut self.scratch_pad,
|
||||||
&mut self.chunk_factory,
|
&mut self.chunk_factory,
|
||||||
&controllers,
|
&controllers,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user