From 4e021dd0bb84b3162d5b17ee72891542e500300d Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 14 Jun 2022 22:48:36 +0200 Subject: [PATCH] Add input and output events to Backend trait --- src/wrapper/standalone/backend.rs | 7 +- src/wrapper/standalone/backend/dummy.rs | 11 +- src/wrapper/standalone/backend/jack.rs | 19 ++- src/wrapper/standalone/wrapper.rs | 170 ++++++++++++------------ 4 files changed, 117 insertions(+), 90 deletions(-) diff --git a/src/wrapper/standalone/backend.rs b/src/wrapper/standalone/backend.rs index e100286f..e1af0876 100644 --- a/src/wrapper/standalone/backend.rs +++ b/src/wrapper/standalone/backend.rs @@ -1,6 +1,7 @@ pub use self::dummy::Dummy; pub use self::jack::Jack; pub use crate::buffer::Buffer; +use crate::midi::NoteEvent; mod dummy; mod jack; @@ -12,7 +13,9 @@ pub trait Backend: 'static + Send + Sync { /// buffers for the wrapped plugin's outputs. Any inputs will have already been copied to this /// buffer. This will block until the process callback returns `false`. /// - /// TODO: MIDI /// TODO: Auxiliary inputs and outputs - fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool + 'static + Send); + fn run( + &mut self, + cb: impl FnMut(&mut Buffer, &[NoteEvent], &mut Vec) -> bool + 'static + Send, + ); } diff --git a/src/wrapper/standalone/backend/dummy.rs b/src/wrapper/standalone/backend/dummy.rs index f248a24a..af501fac 100644 --- a/src/wrapper/standalone/backend/dummy.rs +++ b/src/wrapper/standalone/backend/dummy.rs @@ -3,6 +3,7 @@ use std::time::{Duration, Instant}; use super::super::config::WrapperConfig; use super::Backend; use crate::buffer::Buffer; +use crate::midi::NoteEvent; /// This backend doesn't input or output any audio or MIDI. It only exists so the standalone /// application can continue to run even when there is no audio backend available. This can be @@ -12,7 +13,10 @@ pub struct Dummy { } impl Backend for Dummy { - fn run(&mut self, mut cb: impl FnMut(&mut Buffer) -> bool + 'static + Send) { + fn run( + &mut self, + mut cb: impl FnMut(&mut Buffer, &[NoteEvent], &mut Vec) -> bool + 'static + Send, + ) { // We can't really do anything meaningful here, so we'll simply periodically call the // callback with empty buffers. let interval = @@ -33,6 +37,8 @@ impl Backend for Dummy { }) } + // These will never actually be used + let mut midi_output_events = Vec::with_capacity(1024); loop { let period_start = Instant::now(); @@ -40,7 +46,8 @@ impl Backend for Dummy { channel.fill(0.0); } - if !cb(&mut buffer) { + midi_output_events.clear(); + if !cb(&mut buffer, &[], &mut midi_output_events) { break; } diff --git a/src/wrapper/standalone/backend/jack.rs b/src/wrapper/standalone/backend/jack.rs index 870d8c01..31512789 100644 --- a/src/wrapper/standalone/backend/jack.rs +++ b/src/wrapper/standalone/backend/jack.rs @@ -10,7 +10,7 @@ use jack::{ use super::super::config::WrapperConfig; use super::Backend; use crate::buffer::Buffer; -use crate::midi::MidiConfig; +use crate::midi::{MidiConfig, NoteEvent}; use crate::plugin::Plugin; /// Uses JACK audio and MIDI. @@ -32,7 +32,10 @@ enum Task { } impl Backend for Jack { - fn run(&mut self, mut cb: impl FnMut(&mut Buffer) -> bool + 'static + Send) { + fn run( + &mut self, + mut cb: impl FnMut(&mut Buffer, &[NoteEvent], &mut Vec) -> bool + 'static + Send, + ) { let client = self.client.take().unwrap(); let buffer_size = client.buffer_size(); @@ -43,9 +46,14 @@ impl Backend for Jack { }) } + let mut input_events = Vec::with_capacity(2048); + let mut output_events = Vec::with_capacity(2048); + let (control_sender, control_receiver) = channel::bounded(32); let inputs = self.inputs.clone(); let outputs = self.outputs.clone(); + let midi_input = self.midi_input.clone(); + let midi_output = self.midi_output.clone(); let process_handler = ClosureProcessHandler::new(move |_client, ps| { // In theory we could handle `num_frames <= buffer_size`, but JACK will never chop up // buffers like that so we'll just make it easier for ourselves by not supporting that @@ -76,7 +84,12 @@ impl Backend for Jack { }) } - if cb(&mut buffer) { + // TODO: Read MIDI, convert to events + + output_events.clear(); + if cb(&mut buffer, &input_events, &mut output_events) { + // TODO: Convert output events to MIDI, send to the output port + Control::Continue } else { control_sender.send(Task::Shutdown).unwrap(); diff --git a/src/wrapper/standalone/wrapper.rs b/src/wrapper/standalone/wrapper.rs index 63ec5246..d0fd34c7 100644 --- a/src/wrapper/standalone/wrapper.rs +++ b/src/wrapper/standalone/wrapper.rs @@ -364,96 +364,100 @@ impl Wrapper { // TODO: We should add a way to pull the transport information from the JACK backend let mut num_processed_samples = 0; - self.clone().backend.borrow_mut().run(move |buffer| { - if should_terminate.load(Ordering::SeqCst) { - return false; - } - - // TODO: Process incoming events - - let sample_rate = self.buffer_config.sample_rate; - let mut transport = Transport::new(sample_rate); - transport.pos_samples = Some(num_processed_samples); - transport.tempo = Some(self.config.tempo as f64); - transport.time_sig_numerator = Some(self.config.timesig_num as i32); - transport.time_sig_denominator = Some(self.config.timesig_denom as i32); - transport.playing = true; - - if let ProcessStatus::Error(err) = self.plugin.write().process( - buffer, - // TODO: Provide extra inputs and outputs in the JACk backend - &mut AuxiliaryBuffers { - inputs: &mut [], - outputs: &mut [], - }, - &mut self.make_process_context(transport), - ) { - nih_error!("The plugin returned an error while processing:"); - nih_error!("{}", err); - - let push_successful = gui_task_sender.send(GuiTask::Close).is_ok(); - nih_debug_assert!( - push_successful, - "Could not queue window close, the editor will remain open" - ); - - return false; - } - - // We'll always write these events to the first sample, so even when we add note output we - // shouldn't have to think about interleaving events here - let mut parameter_values_changed = false; - while let Some((param_ptr, normalized_value)) = self.unprocessed_param_changes.pop() { - unsafe { param_ptr.set_normalized_value(normalized_value) }; - unsafe { param_ptr.update_smoother(sample_rate, false) }; - parameter_values_changed = true; - } - - // Allow the editor to react to the new parameter values if the editor uses a reactive data - // binding model - if parameter_values_changed { - self.notify_param_values_changed(); - } - - // TODO: MIDI output - - // After processing audio, we'll check if the editor has sent us updated plugin state. - // We'll restore that here on the audio thread to prevent changing the values during the - // process call and also to prevent inconsistent state when the host also wants to load - // plugin state. - // FIXME: Zero capacity channels allocate on receiving, find a better alternative that - // doesn't do that - let updated_state = permit_alloc(|| self.updated_state_receiver.try_recv()); - if let Ok(state) = updated_state { - unsafe { - state::deserialize_object( - &state, - self.params.clone(), - |param_id| self.param_map.get(param_id).copied(), - Some(&self.buffer_config), - ); + self.clone() + .backend + .borrow_mut() + .run(move |buffer, input_events, output_events| { + if should_terminate.load(Ordering::SeqCst) { + return false; } - self.notify_param_values_changed(); + // TODO: Do something with the input and output events - // TODO: Normally we'd also call initialize after deserializing state, but that's - // not guaranteed to be realtime safe. Should we do it anyways? - self.plugin.write().reset(); + let sample_rate = self.buffer_config.sample_rate; + let mut transport = Transport::new(sample_rate); + transport.pos_samples = Some(num_processed_samples); + transport.tempo = Some(self.config.tempo as f64); + transport.time_sig_numerator = Some(self.config.timesig_num as i32); + transport.time_sig_denominator = Some(self.config.timesig_denom as i32); + transport.playing = true; - // We'll pass the state object back to the GUI thread so deallocation can happen - // there without potentially blocking the audio thread - if let Err(err) = self.updated_state_sender.send(state) { - nih_debug_assert_failure!( - "Failed to send state object back to GUI thread: {}", - err + if let ProcessStatus::Error(err) = self.plugin.write().process( + buffer, + // TODO: Provide extra inputs and outputs in the JACk backend + &mut AuxiliaryBuffers { + inputs: &mut [], + outputs: &mut [], + }, + &mut self.make_process_context(transport), + ) { + nih_error!("The plugin returned an error while processing:"); + nih_error!("{}", err); + + let push_successful = gui_task_sender.send(GuiTask::Close).is_ok(); + nih_debug_assert!( + push_successful, + "Could not queue window close, the editor will remain open" ); - }; - } - num_processed_samples += buffer.len() as i64; + return false; + } - true - }); + // We'll always write these events to the first sample, so even when we add note output we + // shouldn't have to think about interleaving events here + let mut parameter_values_changed = false; + while let Some((param_ptr, normalized_value)) = self.unprocessed_param_changes.pop() + { + unsafe { param_ptr.set_normalized_value(normalized_value) }; + unsafe { param_ptr.update_smoother(sample_rate, false) }; + parameter_values_changed = true; + } + + // Allow the editor to react to the new parameter values if the editor uses a reactive data + // binding model + if parameter_values_changed { + self.notify_param_values_changed(); + } + + // TODO: MIDI output + + // After processing audio, we'll check if the editor has sent us updated plugin state. + // We'll restore that here on the audio thread to prevent changing the values during the + // process call and also to prevent inconsistent state when the host also wants to load + // plugin state. + // FIXME: Zero capacity channels allocate on receiving, find a better alternative that + // doesn't do that + let updated_state = permit_alloc(|| self.updated_state_receiver.try_recv()); + if let Ok(state) = updated_state { + unsafe { + state::deserialize_object( + &state, + self.params.clone(), + |param_id| self.param_map.get(param_id).copied(), + Some(&self.buffer_config), + ); + } + + self.notify_param_values_changed(); + + // TODO: Normally we'd also call initialize after deserializing state, but that's + // not guaranteed to be realtime safe. Should we do it anyways? + self.plugin.write().reset(); + + // We'll pass the state object back to the GUI thread so deallocation can happen + // there without potentially blocking the audio thread + if let Err(err) = self.updated_state_sender.send(state) { + nih_debug_assert_failure!( + "Failed to send state object back to GUI thread: {}", + err + ); + }; + } + + num_processed_samples += buffer.len() as i64; + + true + }); } /// Tell the editor that the parameter values have changed, if the plugin has an editor.