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.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::*;
|
||||
|
||||
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
|
||||
|
||||
@ -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
@ -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()?;
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user