1
0
Fork 0

Abstract the IO backend for the standalone wrapper

This commit is contained in:
Robbert van der Helm 2022-04-22 20:41:21 +02:00
parent 8ba60eeab9
commit 81e56dd018
4 changed files with 101 additions and 12 deletions

View file

@ -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<P: Plugin, Args: IntoIterator<Item = Stri
timesig_denom: 4,
};
let wrapper = match Wrapper::<P>::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::<P, _>::new(backend, config.clone()) {
Ok(wrapper) => wrapper,
Err(err) => {
print_error(err, &config);

View file

@ -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 }
}
}

View file

@ -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<P: Plugin> {
pub(super) wrapper: Arc<Wrapper<P>>,
pub(crate) struct WrapperGuiContext<P: Plugin, B: Backend> {
pub(super) wrapper: Arc<Wrapper<P, B>>,
/// 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<P: Plugin> {
/// 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<P>,
pub(crate) struct WrapperProcessContext<'a, P: Plugin, B: Backend> {
pub(super) wrapper: &'a Wrapper<P, B>,
// TODO: Events
// pub(super) input_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
// pub(super) output_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
pub(super) transport: Transport,
}
impl<P: Plugin> GuiContext for WrapperGuiContext<P> {
impl<P: Plugin, B: Backend> GuiContext for WrapperGuiContext<P, B> {
fn plugin_api(&self) -> PluginApi {
PluginApi::Standalone
}
@ -67,7 +68,7 @@ impl<P: Plugin> GuiContext for WrapperGuiContext<P> {
}
}
impl<P: Plugin> ProcessContext for WrapperProcessContext<'_, P> {
impl<P: Plugin, B: Backend> ProcessContext for WrapperProcessContext<'_, P, B> {
fn plugin_api(&self) -> PluginApi {
PluginApi::Standalone
}

View file

@ -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<P: Plugin> {
pub struct Wrapper<P: Plugin, B: Backend> {
backend: B,
/// The wrapped plugin instance.
plugin: RwLock<P>,
/// 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<P: Plugin> Wrapper<P> {
impl<P: Plugin, B: Backend> Wrapper<P, B> {
/// 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<Arc<Self>, WrapperError> {
pub fn new(backend: B, config: WrapperConfig) -> Result<Arc<Self>, 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<P: Plugin> Wrapper<P> {
fn make_gui_context(
self: Arc<Self>,
new_window_size: Arc<Mutex<Option<(u32, u32)>>>,
) -> Arc<WrapperGuiContext<P>> {
) -> Arc<WrapperGuiContext<P, B>> {
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,