From 81e56dd018361d58c6ca4244c8b69de9d7461a10 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 22 Apr 2022 20:41:21 +0200 Subject: [PATCH] Abstract the IO backend for the standalone wrapper --- src/wrapper/standalone.rs | 6 ++- src/wrapper/standalone/backend.rs | 79 +++++++++++++++++++++++++++++++ src/wrapper/standalone/context.rs | 13 ++--- src/wrapper/standalone/wrapper.rs | 15 ++++-- 4 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 src/wrapper/standalone/backend.rs diff --git a/src/wrapper/standalone.rs b/src/wrapper/standalone.rs index 5e266205..7993ca4e 100644 --- a/src/wrapper/standalone.rs +++ b/src/wrapper/standalone.rs @@ -4,6 +4,7 @@ use self::wrapper::{Wrapper, WrapperConfig, WrapperError}; use crate::plugin::Plugin; +mod backend; mod context; mod wrapper; @@ -73,7 +74,10 @@ pub fn nih_export_standalone_with_args::new(config.clone()) { + // TODO: We should try JACK first, then CPAL, and then fall back to the dummy backend. With a + // command line option to override this behavior. + let backend = backend::Dummy::new(config.clone()); + let wrapper = match Wrapper::::new(backend, config.clone()) { Ok(wrapper) => wrapper, Err(err) => { print_error(err, &config); diff --git a/src/wrapper/standalone/backend.rs b/src/wrapper/standalone/backend.rs new file mode 100644 index 00000000..87e00b21 --- /dev/null +++ b/src/wrapper/standalone/backend.rs @@ -0,0 +1,79 @@ +use std::time::{Duration, Instant}; + +use crate::buffer::Buffer; + +use super::wrapper::WrapperConfig; + +/// An audio+MIDI backend for the standalone wrapper. +pub trait Backend: 'static + Send + Sync { + /// Start processing audio and MIDI on this thread. The process callback will be called whenever + /// there's a new block of audio to be processed. The process callback receives the audio + /// 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 + fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool); +} + +/// Uses JACK audio and MIDI. +pub struct Jack { + // TODO +} + +/// 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 +/// useful for testing plugin GUIs. +pub struct Dummy { + config: WrapperConfig, +} + +impl Backend for Jack { + fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool) { + todo!() + } +} + +impl Backend for Dummy { + fn run(&mut self, mut cb: impl FnMut(&mut Buffer) -> bool) { + // We can't really do anything meaningful here, so we'll simply periodically call the + // callback with empty buffers. + let interval = + Duration::from_secs_f32(self.config.period_size as f32 / self.config.sample_rate); + + let mut channels = vec![ + vec![0.0f32; self.config.period_size as usize]; + self.config.output_channels as usize + ]; + let mut buffer = Buffer::default(); + unsafe { + buffer.with_raw_vec(|output_slices| { + // SAFETY: `channels` is no longer used directly after this + *output_slices = channels + .iter_mut() + .map(|channel| &mut *(channel.as_mut_slice() as *mut [f32])) + .collect(); + }) + } + + loop { + let period_start = Instant::now(); + + for channel in buffer.as_slice() { + channel.fill(0.0); + } + + if !cb(&mut buffer) { + break; + } + + let period_end = Instant::now(); + std::thread::sleep((period_start + interval).saturating_duration_since(period_end)); + } + } +} + +impl Dummy { + pub fn new(config: WrapperConfig) -> Self { + Self { config } + } +} diff --git a/src/wrapper/standalone/context.rs b/src/wrapper/standalone/context.rs index 205f80ae..fd7c87e3 100644 --- a/src/wrapper/standalone/context.rs +++ b/src/wrapper/standalone/context.rs @@ -1,6 +1,7 @@ use parking_lot::Mutex; use std::sync::Arc; +use super::backend::Backend; use super::wrapper::Wrapper; use crate::context::{GuiContext, PluginApi, ProcessContext, Transport}; use crate::midi::NoteEvent; @@ -10,8 +11,8 @@ use crate::plugin::Plugin; /// A [`GuiContext`] implementation for the wrapper. This is passed to the plugin in /// [`Editor::spawn()`][crate::prelude::Editor::spawn()] so it can interact with the rest of the plugin and /// with the host for things like setting parameters. -pub(crate) struct WrapperGuiContext { - pub(super) wrapper: Arc>, +pub(crate) struct WrapperGuiContext { + pub(super) wrapper: Arc>, /// If the widnow should be resized, then we will write the size here. This will be set on the /// window at the start of the next frame. @@ -21,15 +22,15 @@ pub(crate) struct WrapperGuiContext { /// A [`ProcessContext`] implementation for the standalone wrapper. This is a separate object so it /// can hold on to lock guards for event queues. Otherwise reading these events would require /// constant unnecessary atomic operations to lock the uncontested RwLocks. -pub(crate) struct WrapperProcessContext<'a, P: Plugin> { - pub(super) wrapper: &'a Wrapper

, +pub(crate) struct WrapperProcessContext<'a, P: Plugin, B: Backend> { + pub(super) wrapper: &'a Wrapper, // TODO: Events // pub(super) input_events_guard: AtomicRefMut<'a, VecDeque>, // pub(super) output_events_guard: AtomicRefMut<'a, VecDeque>, pub(super) transport: Transport, } -impl GuiContext for WrapperGuiContext

{ +impl GuiContext for WrapperGuiContext { fn plugin_api(&self) -> PluginApi { PluginApi::Standalone } @@ -67,7 +68,7 @@ impl GuiContext for WrapperGuiContext

{ } } -impl ProcessContext for WrapperProcessContext<'_, P> { +impl ProcessContext for WrapperProcessContext<'_, P, B> { fn plugin_api(&self) -> PluginApi { PluginApi::Standalone } diff --git a/src/wrapper/standalone/wrapper.rs b/src/wrapper/standalone/wrapper.rs index f5f7077d..195da2d7 100644 --- a/src/wrapper/standalone/wrapper.rs +++ b/src/wrapper/standalone/wrapper.rs @@ -4,6 +4,7 @@ use raw_window_handle::HasRawWindowHandle; use std::any::Any; use std::sync::Arc; +use super::backend::Backend; use super::context::{WrapperGuiContext, WrapperProcessContext}; use crate::context::Transport; use crate::plugin::{BufferConfig, BusConfig, Editor, ParentWindowHandle, Plugin}; @@ -34,7 +35,9 @@ pub struct WrapperConfig { pub timesig_denom: u32, } -pub struct Wrapper { +pub struct Wrapper { + backend: B, + /// The wrapped plugin instance. plugin: RwLock

, /// The plugin's editor, if it has one. This object does not do anything on its own, but we need @@ -87,14 +90,16 @@ impl WindowHandler for WrapperWindowHandler { } } -impl Wrapper

{ +impl Wrapper { /// Instantiate a new instance of the standalone wrapper. Returns an error if the plugin does /// not accept the IO configuration from the wrapper config. - pub fn new(config: WrapperConfig) -> Result, WrapperError> { + pub fn new(backend: B, config: WrapperConfig) -> Result, WrapperError> { let plugin = P::default(); let editor = plugin.editor().map(Arc::from); let wrapper = Arc::new(Wrapper { + backend, + plugin: RwLock::new(plugin), editor, @@ -196,14 +201,14 @@ impl Wrapper

{ fn make_gui_context( self: Arc, new_window_size: Arc>>, - ) -> Arc> { + ) -> Arc> { Arc::new(WrapperGuiContext { wrapper: self, new_window_size, }) } - fn make_process_context(&self, transport: Transport) -> WrapperProcessContext<'_, P> { + fn make_process_context(&self, transport: Transport) -> WrapperProcessContext<'_, P, B> { WrapperProcessContext { wrapper: self, transport,