Abstract the IO backend for the standalone wrapper
This commit is contained in:
parent
8ba60eeab9
commit
81e56dd018
|
@ -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);
|
||||
|
|
79
src/wrapper/standalone/backend.rs
Normal file
79
src/wrapper/standalone/backend.rs
Normal 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 }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue