1
0
Fork 0

Add AsyncExecutor support to ProcessContext

This commit is contained in:
Robbert van der Helm 2022-10-22 01:15:16 +02:00
parent ec8e99cf44
commit 84f834abb6
27 changed files with 107 additions and 46 deletions

View file

@ -8,6 +8,8 @@ code then it will not be listed here.
## [2022-10-22] ## [2022-10-22]
- The `&mut impl ProcessContext` argument to `Plugin::process()` needs to be
changed to `&mut impl ProcessContext<Self>`.
- The `&mut impl InitContext` argument to `Plugin::initialize()` needs to be - The `&mut impl InitContext` argument to `Plugin::initialize()` needs to be
changed to `&mut impl InitContext<Self>`. changed to `&mut impl InitContext<Self>`.
- NIH-plug has gained support for asynchronously running background tasks in a - NIH-plug has gained support for asynchronously running background tasks in a

View file

@ -83,7 +83,7 @@ Scroll down for more information on the underlying plugin framework.
- Standalone binaries can be made by calling `nih_export_standalone(Foo)` from - Standalone binaries can be made by calling `nih_export_standalone(Foo)` from
your `main()` function. Standalones come with a CLI for configuration and full your `main()` function. Standalones come with a CLI for configuration and full
JACK audio, MIDI, and transport support. JACK audio, MIDI, and transport support.
- Declarative parameter handling without any boilerplate. - Rich declarative parameter system without any boilerplate.
- Define parameters for your plugin by adding `FloatParam`, `IntParam`, - Define parameters for your plugin by adding `FloatParam`, `IntParam`,
`BoolParam`, and `EnumParam<T>` fields to your parameter struct, assign `BoolParam`, and `EnumParam<T>` fields to your parameter struct, assign
stable IDs to them with the `#[id = "foobar"]`, and a `#[derive(Params)]` stable IDs to them with the `#[id = "foobar"]`, and a `#[derive(Params)]`
@ -107,6 +107,8 @@ Scroll down for more information on the underlying plugin framework.
trait to enable compile time generated parameters and other bespoke trait to enable compile time generated parameters and other bespoke
functionality. functionality.
- Stateful. Behaves mostly like JUCE, just without all of the boilerplate. - Stateful. Behaves mostly like JUCE, just without all of the boilerplate.
- Comes with a simple yet powerful way to asynchronously run background tasks
from a plugin that's both type-safe and realtime-safe.
- Does not make any assumptions on how you want to process audio, but does come - Does not make any assumptions on how you want to process audio, but does come
with utilities and adapters to help with common access patterns. with utilities and adapters to help with common access patterns.
- Efficiently iterate over an audio buffer either per-sample per-channel, - Efficiently iterate over an audio buffer either per-sample per-channel,

View file

@ -356,7 +356,7 @@ impl Plugin for Crisp {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
for (_, mut block) in buffer.iter_blocks(BLOCK_SIZE) { for (_, mut block) in buffer.iter_blocks(BLOCK_SIZE) {
let mut rm_outputs = [[0.0; NUM_CHANNELS as usize]; BLOCK_SIZE]; let mut rm_outputs = [[0.0; NUM_CHANNELS as usize]; BLOCK_SIZE];

View file

@ -228,7 +228,7 @@ impl Plugin for Crossover {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
aux: &mut AuxiliaryBuffers, aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
// Right now both crossover types only do 24 dB/octave Linkwitz-Riley style crossovers // Right now both crossover types only do 24 dB/octave Linkwitz-Riley style crossovers
match self.params.crossover_type.value() { match self.params.crossover_type.value() {

View file

@ -288,7 +288,7 @@ impl Plugin for Diopser {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
// Since this is an expensive operation, only update the filters when it's actually // Since this is an expensive operation, only update the filters when it's actually
// necessary, and allow smoothing only every n samples using the automation precision // necessary, and allow smoothing only every n samples using the automation precision

View file

@ -156,7 +156,7 @@ impl Plugin for Gain {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
for channel_samples in buffer.iter_samples() { for channel_samples in buffer.iter_samples() {
// Smoothing is optionally built into the parameters themselves // Smoothing is optionally built into the parameters themselves

View file

@ -174,7 +174,7 @@ impl Plugin for Gain {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
for channel_samples in buffer.iter_samples() { for channel_samples in buffer.iter_samples() {
let mut amplitude = 0.0; let mut amplitude = 0.0;

View file

@ -119,7 +119,7 @@ impl Plugin for Gain {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
for channel_samples in buffer.iter_samples() { for channel_samples in buffer.iter_samples() {
let mut amplitude = 0.0; let mut amplitude = 0.0;

View file

@ -118,7 +118,7 @@ impl Plugin for Gain {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
for channel_samples in buffer.iter_samples() { for channel_samples in buffer.iter_samples() {
let mut amplitude = 0.0; let mut amplitude = 0.0;

View file

@ -44,7 +44,7 @@ impl Plugin for MidiInverter {
&mut self, &mut self,
_buffer: &mut Buffer, _buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
// We'll invert the channel, note index, velocity, pressure, CC value, pitch bend, and // We'll invert the channel, note index, velocity, pressure, CC value, pitch bend, and
// anything else that is invertable for all events we receive // anything else that is invertable for all events we receive

View file

@ -177,7 +177,7 @@ impl Plugin for PolyModSynth {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
// NIH-plug has a block-splitting adapter for `Buffer`. While this works great for effect // NIH-plug has a block-splitting adapter for `Buffer`. While this works great for effect
// plugins, for polyphonic synths the block size should be `min(MAX_BLOCK_SIZE, // plugins, for polyphonic synths the block size should be `min(MAX_BLOCK_SIZE,
@ -443,7 +443,7 @@ impl PolyModSynth {
/// voice will be stolen. Returns a reference to the new voice. /// voice will be stolen. Returns a reference to the new voice.
fn start_voice( fn start_voice(
&mut self, &mut self,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
sample_offset: u32, sample_offset: u32,
voice_id: Option<i32>, voice_id: Option<i32>,
channel: u8, channel: u8,
@ -544,7 +544,7 @@ impl PolyModSynth {
/// matching voices. /// matching voices.
fn choke_voices( fn choke_voices(
&mut self, &mut self,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
sample_offset: u32, sample_offset: u32,
voice_id: Option<i32>, voice_id: Option<i32>,
channel: u8, channel: u8,

View file

@ -145,7 +145,7 @@ impl Plugin for Sine {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
let mut next_event = context.next_event(); let mut next_event = context.next_event();
for (sample_id, channel_samples) in buffer.iter_samples().enumerate() { for (sample_id, channel_samples) in buffer.iter_samples().enumerate() {

View file

@ -132,7 +132,7 @@ impl Plugin for Stft {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
self.stft self.stft
.process_overlap_add(buffer, 1, |_channel_idx, real_fft_buffer| { .process_overlap_add(buffer, 1, |_channel_idx, real_fft_buffer| {

View file

@ -174,7 +174,7 @@ impl Plugin for LoudnessWarWinner {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
for mut channel_samples in buffer.iter_samples() { for mut channel_samples in buffer.iter_samples() {
let output_gain = self.params.output_gain.smoothed.next(); let output_gain = self.params.output_gain.smoothed.next();

View file

@ -224,7 +224,7 @@ impl Plugin for PubertySimulator {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
// Compensate for the window function, the overlap, and the extra gain introduced by the // Compensate for the window function, the overlap, and the extra gain introduced by the
// IDFT operation // IDFT operation

View file

@ -200,7 +200,7 @@ impl Plugin for SafetyLimiter {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, _aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext, _context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
// Don't do anything when bouncing // Don't do anything when bouncing
if self.buffer_config.process_mode == ProcessMode::Offline { if self.buffer_config.process_mode == ProcessMode::Offline {

View file

@ -346,7 +346,7 @@ impl Plugin for SpectralCompressor {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
aux: &mut AuxiliaryBuffers, aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
) -> ProcessStatus { ) -> ProcessStatus {
// If the window size has changed since the last process call, reset the buffers and chance // If the window size has changed since the last process call, reset the buffers and chance
// our latency. All of these buffers already have enough capacity so this won't allocate. // our latency. All of these buffers already have enough capacity so this won't allocate.

View file

@ -50,7 +50,11 @@ pub trait InitContext<P: Plugin> {
// //
// The implementing wrapper needs to be able to handle concurrent requests, and it should perform // The implementing wrapper needs to be able to handle concurrent requests, and it should perform
// the actual callback within [MainThreadQueue::do_maybe_async]. // the actual callback within [MainThreadQueue::do_maybe_async].
pub trait ProcessContext { pub trait ProcessContext<P: Plugin> {
/// Run a task on a background thread. This allows defering expensive background tasks for
/// alter. As long as creating the `task` is realtime-safe, this operation is too.
fn execute_async(&self, task: <P::AsyncExecutor as AsyncExecutor>::Task);
/// Get the current plugin API. /// Get the current plugin API.
fn plugin_api(&self) -> PluginApi; fn plugin_api(&self) -> PluginApi;

View file

@ -10,6 +10,8 @@ mod linux;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod windows; mod windows;
use crate::prelude::AsyncExecutor;
#[cfg(all(target_family = "unix", not(target_os = "macos")))] #[cfg(all(target_family = "unix", not(target_os = "macos")))]
pub(crate) use self::linux::LinuxEventLoop as OsEventLoop; pub(crate) use self::linux::LinuxEventLoop as OsEventLoop;
// For now, also use the Linux event loop on macOS so it at least compiles // For now, also use the Linux event loop on macOS so it at least compiles
@ -38,6 +40,9 @@ where
{ {
/// Create and start a new event loop. The thread this is called on will be designated as the /// Create and start a new event loop. The thread this is called on will be designated as the
/// main thread, so this should be called when constructing the wrapper. /// main thread, so this should be called when constructing the wrapper.
//
// TODO: This use of `Weak` is a bit weird outside the context of the VST3 wrapper, but the
// alternatives didn't feel right either.
fn new_and_spawn(executor: Weak<E>) -> Self; fn new_and_spawn(executor: Weak<E>) -> Self;
/// Either post the function to the task queue so it can be delegated to the main thread, or /// Either post the function to the task queue so it can be delegated to the main thread, or
@ -63,3 +68,11 @@ pub(crate) trait MainThreadExecutor<T>: Send + Sync {
/// assume (and can only assume) that this is called from the main thread. /// assume (and can only assume) that this is called from the main thread.
unsafe fn execute(&self, task: T); unsafe fn execute(&self, task: T);
} }
/// An adapter implementation to allow any [`AsyncExecutor`] to function as a
/// [`MainThreadExecutor`]. Used only in the standalone wrapper.
impl<T: Send, E: AsyncExecutor<Task = T>> MainThreadExecutor<T> for E {
unsafe fn execute(&self, task: T) {
AsyncExecutor::execute(self, task);
}
}

View file

@ -195,7 +195,7 @@ pub trait Plugin: Default + Send + 'static {
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
aux: &mut AuxiliaryBuffers, aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext, context: &mut impl ProcessContext<Self>,
) -> ProcessStatus; ) -> ProcessStatus;
/// Called when the plugin is deactivated. The host will call /// Called when the plugin is deactivated. The host will call

View file

@ -2,9 +2,10 @@ use atomic_refcell::AtomicRefMut;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Arc; use std::sync::Arc;
use super::wrapper::{OutputParamEvent, Wrapper}; use super::wrapper::{OutputParamEvent, Task, Wrapper};
use crate::async_executor::AsyncExecutor; use crate::async_executor::AsyncExecutor;
use crate::context::{GuiContext, InitContext, PluginApi, ProcessContext, Transport}; use crate::context::{GuiContext, InitContext, PluginApi, ProcessContext, Transport};
use crate::event_loop::EventLoop;
use crate::midi::NoteEvent; use crate::midi::NoteEvent;
use crate::params::internals::ParamPtr; use crate::params::internals::ParamPtr;
use crate::plugin::ClapPlugin; use crate::plugin::ClapPlugin;
@ -131,7 +132,15 @@ impl<P: ClapPlugin> InitContext<P> for WrapperInitContext<'_, P> {
} }
} }
impl<P: ClapPlugin> ProcessContext for WrapperProcessContext<'_, P> { impl<P: ClapPlugin> ProcessContext<P> for WrapperProcessContext<'_, P> {
fn execute_async(
&self,
task: <<P as crate::prelude::Plugin>::AsyncExecutor as AsyncExecutor>::Task,
) {
let task_posted = self.wrapper.do_maybe_async(Task::PluginTask(task));
nih_debug_assert!(task_posted, "The task queue is full, dropping task...");
}
fn plugin_api(&self) -> PluginApi { fn plugin_api(&self) -> PluginApi {
PluginApi::Clap PluginApi::Clap
} }

View file

@ -76,6 +76,7 @@ use std::time::Duration;
use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext}; use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext};
use super::descriptor::PluginDescriptor; use super::descriptor::PluginDescriptor;
use super::util::ClapPtr; use super::util::ClapPtr;
use crate::async_executor::AsyncExecutor;
use crate::buffer::Buffer; use crate::buffer::Buffer;
use crate::context::Transport; use crate::context::Transport;
use crate::editor::{Editor, ParentWindowHandle}; use crate::editor::{Editor, ParentWindowHandle};
@ -84,7 +85,7 @@ use crate::midi::{MidiConfig, NoteEvent};
use crate::params::internals::ParamPtr; use crate::params::internals::ParamPtr;
use crate::params::{ParamFlags, Params}; use crate::params::{ParamFlags, Params};
use crate::plugin::{ use crate::plugin::{
AuxiliaryBuffers, BufferConfig, BusConfig, ClapPlugin, ProcessMode, ProcessStatus, AuxiliaryBuffers, BufferConfig, BusConfig, ClapPlugin, Plugin, ProcessMode, ProcessStatus,
}; };
use crate::util::permit_alloc; use crate::util::permit_alloc;
use crate::wrapper::clap::util::{read_stream, write_stream}; use crate::wrapper::clap::util::{read_stream, write_stream};
@ -257,7 +258,7 @@ pub struct Wrapper<P: ClapPlugin> {
/// implementations. Instead, we'll post tasks to this queue, ask the host to call /// implementations. Instead, we'll post tasks to this queue, ask the host to call
/// [`on_main_thread()`][Self::on_main_thread()] on the main thread, and then continue to pop /// [`on_main_thread()`][Self::on_main_thread()] on the main thread, and then continue to pop
/// tasks off this queue there until it is empty. /// tasks off this queue there until it is empty.
tasks: ArrayQueue<Task>, tasks: ArrayQueue<Task<P>>,
/// The ID of the main thread. In practice this is the ID of the thread that created this /// The ID of the main thread. In practice this is the ID of the thread that created this
/// object. If the host supports the thread check extension (and /// object. If the host supports the thread check extension (and
/// [`host_thread_check`][Self::host_thread_check] thus contains a value), then that extension /// [`host_thread_check`][Self::host_thread_check] thus contains a value), then that extension
@ -268,8 +269,10 @@ pub struct Wrapper<P: ClapPlugin> {
/// Tasks that can be sent from the plugin to be executed on the main thread in a non-blocking /// Tasks that can be sent from the plugin to be executed on the main thread in a non-blocking
/// realtime-safe way. Instead of using a random thread or the OS' event loop like in the Linux /// realtime-safe way. Instead of using a random thread or the OS' event loop like in the Linux
/// implementation, this uses [`clap_host::request_callback()`] instead. /// implementation, this uses [`clap_host::request_callback()`] instead.
#[derive(Debug)] #[allow(clippy::enum_variant_names)]
pub enum Task { pub enum Task<P: Plugin> {
/// Execute one of the plugin's background tasks.
PluginTask(<P::AsyncExecutor as AsyncExecutor>::Task),
/// Inform the host that the latency has changed. /// Inform the host that the latency has changed.
LatencyChanged, LatencyChanged,
/// Inform the host that the voice info has changed. /// Inform the host that the voice info has changed.
@ -311,12 +314,12 @@ pub enum OutputParamEvent {
/// Because CLAP has this [`clap_host::request_host_callback()`] function, we don't need to use /// Because CLAP has this [`clap_host::request_host_callback()`] function, we don't need to use
/// `OsEventLoop` and can instead just request a main thread callback directly. /// `OsEventLoop` and can instead just request a main thread callback directly.
impl<P: ClapPlugin> EventLoop<Task, Wrapper<P>> for Wrapper<P> { impl<P: ClapPlugin> EventLoop<Task<P>, Wrapper<P>> for Wrapper<P> {
fn new_and_spawn(_executor: std::sync::Weak<Self>) -> Self { fn new_and_spawn(_executor: std::sync::Weak<Self>) -> Self {
panic!("What are you doing"); panic!("What are you doing");
} }
fn do_maybe_async(&self, task: Task) -> bool { fn do_maybe_async(&self, task: Task<P>) -> bool {
if self.is_main_thread() { if self.is_main_thread() {
unsafe { self.execute(task) }; unsafe { self.execute(task) };
true true
@ -346,10 +349,11 @@ impl<P: ClapPlugin> EventLoop<Task, Wrapper<P>> for Wrapper<P> {
} }
} }
impl<P: ClapPlugin> MainThreadExecutor<Task> for Wrapper<P> { impl<P: ClapPlugin> MainThreadExecutor<Task<P>> for Wrapper<P> {
unsafe fn execute(&self, task: Task) { unsafe fn execute(&self, task: Task<P>) {
// This function is always called from the main thread, from [Self::on_main_thread]. // This function is always called from the main thread, from [Self::on_main_thread].
match task { match task {
Task::PluginTask(task) => AsyncExecutor::execute(&self.async_executor, task),
Task::LatencyChanged => match &*self.host_latency.borrow() { Task::LatencyChanged => match &*self.host_latency.borrow() {
Some(host_latency) => { Some(host_latency) => {
// XXX: The CLAP docs mention that you should request a restart if this happens // XXX: The CLAP docs mention that you should request a restart if this happens

View file

@ -5,6 +5,7 @@ use super::backend::Backend;
use super::wrapper::{GuiTask, Wrapper}; use super::wrapper::{GuiTask, Wrapper};
use crate::async_executor::AsyncExecutor; use crate::async_executor::AsyncExecutor;
use crate::context::{GuiContext, InitContext, PluginApi, ProcessContext, Transport}; use crate::context::{GuiContext, InitContext, PluginApi, ProcessContext, Transport};
use crate::event_loop::EventLoop;
use crate::midi::NoteEvent; use crate::midi::NoteEvent;
use crate::params::internals::ParamPtr; use crate::params::internals::ParamPtr;
use crate::plugin::Plugin; use crate::plugin::Plugin;
@ -96,7 +97,12 @@ impl<P: Plugin, B: Backend> InitContext<P> for WrapperInitContext<'_, P, B> {
} }
} }
impl<P: Plugin, B: Backend> ProcessContext for WrapperProcessContext<'_, P, B> { impl<P: Plugin, B: Backend> ProcessContext<P> for WrapperProcessContext<'_, P, B> {
fn execute_async(&self, task: <P::AsyncExecutor as AsyncExecutor>::Task) {
let task_posted = self.wrapper.event_loop.do_maybe_async(task);
nih_debug_assert!(task_posted, "The task queue is full, dropping task...");
}
fn plugin_api(&self) -> PluginApi { fn plugin_api(&self) -> PluginApi {
PluginApi::Standalone PluginApi::Standalone
} }

View file

@ -13,8 +13,10 @@ use std::thread;
use super::backend::Backend; use super::backend::Backend;
use super::config::WrapperConfig; use super::config::WrapperConfig;
use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext}; use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext};
use crate::async_executor::AsyncExecutor;
use crate::context::Transport; use crate::context::Transport;
use crate::editor::{Editor, ParentWindowHandle}; use crate::editor::{Editor, ParentWindowHandle};
use crate::event_loop::{EventLoop, OsEventLoop};
use crate::midi::NoteEvent; use crate::midi::NoteEvent;
use crate::params::internals::ParamPtr; use crate::params::internals::ParamPtr;
use crate::params::{ParamFlags, Params}; use crate::params::{ParamFlags, Params};
@ -36,7 +38,7 @@ pub struct Wrapper<P: Plugin, B: Backend> {
/// The wrapped plugin instance. /// The wrapped plugin instance.
plugin: Mutex<P>, plugin: Mutex<P>,
/// The plugin's background task executor. /// The plugin's background task executor.
pub async_executor: P::AsyncExecutor, pub async_executor: Arc<P::AsyncExecutor>,
/// The plugin's parameters. These are fetched once during initialization. That way the /// The plugin's parameters. These are fetched once during initialization. That way the
/// `ParamPtr`s are guaranteed to live at least as long as this object and we can interact with /// `ParamPtr`s are guaranteed to live at least as long as this object and we can interact with
/// the `Params` object without having to acquire a lock on `plugin`. /// the `Params` object without having to acquire a lock on `plugin`.
@ -52,6 +54,14 @@ pub struct Wrapper<P: Plugin, B: Backend> {
/// creating an editor. /// creating an editor.
pub editor: Option<Arc<Mutex<Box<dyn Editor>>>>, pub editor: Option<Arc<Mutex<Box<dyn Editor>>>>,
/// A realtime-safe task queue so the plugin can schedule tasks that need to be run later on the
/// GUI thread. See the same field in the VST3 wrapper for more information on why this looks
/// the way it does.
///
/// This is only used for executing [`AsyncExecutor`] tasks, so it's parameterized directly over
/// that using a special `MainThreadExecutor` wrapper around `AsyncExecutor`.
pub(crate) event_loop: OsEventLoop<<P::AsyncExecutor as AsyncExecutor>::Task, P::AsyncExecutor>,
config: WrapperConfig, config: WrapperConfig,
/// The bus and buffer configurations are static for the standalone target. /// The bus and buffer configurations are static for the standalone target.
@ -134,7 +144,7 @@ impl<P: Plugin, B: Backend> Wrapper<P, B> {
/// not accept the IO configuration from the wrapper config. /// not accept the IO configuration from the wrapper config.
pub fn new(backend: B, config: WrapperConfig) -> Result<Arc<Self>, WrapperError> { pub fn new(backend: B, config: WrapperConfig) -> Result<Arc<Self>, WrapperError> {
let plugin = P::default(); let plugin = P::default();
let async_executor = plugin.async_executor(); let async_executor = Arc::new(plugin.async_executor());
let params = plugin.params(); let params = plugin.params();
let editor = plugin.editor().map(|editor| Arc::new(Mutex::new(editor))); let editor = plugin.editor().map(|editor| Arc::new(Mutex::new(editor)));
@ -181,7 +191,7 @@ impl<P: Plugin, B: Backend> Wrapper<P, B> {
backend: AtomicRefCell::new(backend), backend: AtomicRefCell::new(backend),
plugin: Mutex::new(plugin), plugin: Mutex::new(plugin),
async_executor, async_executor: async_executor.clone(),
params, params,
known_parameters: param_map.iter().map(|(_, ptr, _)| *ptr).collect(), known_parameters: param_map.iter().map(|(_, ptr, _)| *ptr).collect(),
param_map: param_map param_map: param_map
@ -190,6 +200,8 @@ impl<P: Plugin, B: Backend> Wrapper<P, B> {
.collect(), .collect(),
editor, editor,
event_loop: OsEventLoop::new_and_spawn(Arc::downgrade(&async_executor)),
bus_config: BusConfig { bus_config: BusConfig {
num_input_channels: config.input_channels.unwrap_or(P::DEFAULT_INPUT_CHANNELS), num_input_channels: config.input_channels.unwrap_or(P::DEFAULT_INPUT_CHANNELS),
num_output_channels: config.output_channels.unwrap_or(P::DEFAULT_OUTPUT_CHANNELS), num_output_channels: config.output_channels.unwrap_or(P::DEFAULT_OUTPUT_CHANNELS),

View file

@ -134,7 +134,12 @@ impl<P: Vst3Plugin> InitContext<P> for WrapperInitContext<'_, P> {
} }
} }
impl<P: Vst3Plugin> ProcessContext for WrapperProcessContext<'_, P> { impl<P: Vst3Plugin> ProcessContext<P> for WrapperProcessContext<'_, P> {
fn execute_async(&self, task: <P::AsyncExecutor as AsyncExecutor>::Task) {
let task_posted = self.inner.do_maybe_async(Task::PluginTask(task));
nih_debug_assert!(task_posted, "The task queue is full, dropping task...");
}
fn plugin_api(&self) -> PluginApi { fn plugin_api(&self) -> PluginApi {
PluginApi::Vst3 PluginApi::Vst3
} }

View file

@ -14,6 +14,7 @@ use super::note_expressions::NoteExpressionController;
use super::param_units::ParamUnits; use super::param_units::ParamUnits;
use super::util::{ObjectPtr, VstPtr, VST3_MIDI_PARAMS_END, VST3_MIDI_PARAMS_START}; use super::util::{ObjectPtr, VstPtr, VST3_MIDI_PARAMS_END, VST3_MIDI_PARAMS_START};
use super::view::WrapperView; use super::view::WrapperView;
use crate::async_executor::AsyncExecutor;
use crate::buffer::Buffer; use crate::buffer::Buffer;
use crate::context::Transport; use crate::context::Transport;
use crate::editor::Editor; use crate::editor::Editor;
@ -21,7 +22,7 @@ use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop};
use crate::midi::{MidiConfig, NoteEvent}; use crate::midi::{MidiConfig, NoteEvent};
use crate::params::internals::ParamPtr; use crate::params::internals::ParamPtr;
use crate::params::{ParamFlags, Params}; use crate::params::{ParamFlags, Params};
use crate::plugin::{BufferConfig, BusConfig, ProcessMode, ProcessStatus, Vst3Plugin}; use crate::plugin::{BufferConfig, BusConfig, Plugin, ProcessMode, ProcessStatus, Vst3Plugin};
use crate::wrapper::state::{self, PluginState}; use crate::wrapper::state::{self, PluginState};
use crate::wrapper::util::{hash_param_id, process_wrapper}; use crate::wrapper::util::{hash_param_id, process_wrapper};
@ -59,7 +60,7 @@ pub(crate) struct WrapperInner<P: Vst3Plugin> {
/// reason to mutably borrow the event loop, so reads will never be contested. /// reason to mutably borrow the event loop, so reads will never be contested.
/// ///
/// TODO: Is there a better type for Send+Sync late initialization? /// TODO: Is there a better type for Send+Sync late initialization?
pub event_loop: AtomicRefCell<Option<OsEventLoop<Task, Self>>>, pub event_loop: AtomicRefCell<Option<OsEventLoop<Task<P>, Self>>>,
/// Whether the plugin is currently processing audio. In other words, the last state /// Whether the plugin is currently processing audio. In other words, the last state
/// `IAudioProcessor::setActive()` has been called with. /// `IAudioProcessor::setActive()` has been called with.
@ -150,8 +151,10 @@ pub(crate) struct WrapperInner<P: Vst3Plugin> {
/// Tasks that can be sent from the plugin to be executed on the main thread in a non-blocking /// Tasks that can be sent from the plugin to be executed on the main thread in a non-blocking
/// realtime-safe way (either a random thread or `IRunLoop` on Linux, the OS' message loop on /// realtime-safe way (either a random thread or `IRunLoop` on Linux, the OS' message loop on
/// Windows and macOS). /// Windows and macOS).
#[derive(Debug, Clone)] #[allow(clippy::enum_variant_names)]
pub enum Task { pub enum Task<P: Plugin> {
/// Execute one of the plugin's background tasks.
PluginTask(<P::AsyncExecutor as AsyncExecutor>::Task),
/// Trigger a restart with the given restart flags. This is a bit set of the flags from /// Trigger a restart with the given restart flags. This is a bit set of the flags from
/// [`vst3_sys::vst::RestartFlags`]. /// [`vst3_sys::vst::RestartFlags`].
TriggerRestart(i32), TriggerRestart(i32),
@ -353,7 +356,7 @@ impl<P: Vst3Plugin> WrapperInner<P> {
/// ///
/// If the task queue is full, then this will return false. /// If the task queue is full, then this will return false.
#[must_use] #[must_use]
pub fn do_maybe_async(&self, task: Task) -> bool { pub fn do_maybe_async(&self, task: Task<P>) -> bool {
let event_loop = self.event_loop.borrow(); let event_loop = self.event_loop.borrow();
let event_loop = event_loop.as_ref().unwrap(); let event_loop = event_loop.as_ref().unwrap();
if event_loop.is_main_thread() { if event_loop.is_main_thread() {
@ -514,8 +517,8 @@ impl<P: Vst3Plugin> WrapperInner<P> {
} }
} }
impl<P: Vst3Plugin> MainThreadExecutor<Task> for WrapperInner<P> { impl<P: Vst3Plugin> MainThreadExecutor<Task<P>> for WrapperInner<P> {
unsafe fn execute(&self, task: Task) { unsafe fn execute(&self, task: Task<P>) {
// This function is always called from the main thread // This function is always called from the main thread
// TODO: When we add GUI resizing and context menus, this should propagate those events to // TODO: When we add GUI resizing and context menus, this should propagate those events to
// `IRunLoop` on Linux to keep REAPER happy. That does mean a double spool, but we can // `IRunLoop` on Linux to keep REAPER happy. That does mean a double spool, but we can
@ -523,6 +526,7 @@ impl<P: Vst3Plugin> MainThreadExecutor<Task> for WrapperInner<P> {
// function for checking if a to be scheduled task can be handled right there and // function for checking if a to be scheduled task can be handled right there and
// then). // then).
match task { match task {
Task::PluginTask(task) => AsyncExecutor::execute(&self.async_executor, task),
Task::TriggerRestart(flags) => match &*self.component_handler.borrow() { Task::TriggerRestart(flags) => match &*self.component_handler.borrow() {
Some(handler) => { Some(handler) => {
handler.restart_component(flags); handler.restart_component(flags);

View file

@ -96,7 +96,7 @@ struct RunLoopEventHandler<P: Vst3Plugin> {
/// implementations. Instead, we'll post tasks to this queue, ask the host to call /// implementations. Instead, we'll post tasks to this queue, ask the host to call
/// [`on_main_thread()`][Self::on_main_thread()] on the main thread, and then continue to pop /// [`on_main_thread()`][Self::on_main_thread()] on the main thread, and then continue to pop
/// tasks off this queue there until it is empty. /// tasks off this queue there until it is empty.
tasks: ArrayQueue<Task>, tasks: ArrayQueue<Task<P>>,
} }
impl<P: Vst3Plugin> WrapperView<P> { impl<P: Vst3Plugin> WrapperView<P> {
@ -162,7 +162,7 @@ impl<P: Vst3Plugin> WrapperView<P> {
/// run on the host's UI thread. If not, then this will return an `Err` value containing the /// run on the host's UI thread. If not, then this will return an `Err` value containing the
/// task so it can be run elsewhere. /// task so it can be run elsewhere.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn do_maybe_in_run_loop(&self, task: Task) -> Result<(), Task> { pub fn do_maybe_in_run_loop(&self, task: Task<P>) -> Result<(), Task<P>> {
match &*self.run_loop_event_handler.0.read() { match &*self.run_loop_event_handler.0.read() {
Some(run_loop) => run_loop.post_task(task), Some(run_loop) => run_loop.post_task(task),
None => Err(task), None => Err(task),
@ -174,7 +174,7 @@ impl<P: Vst3Plugin> WrapperView<P> {
/// task so it can be run elsewhere. /// task so it can be run elsewhere.
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
#[must_use] #[must_use]
pub fn do_maybe_in_run_loop(&self, task: Task) -> Result<(), Task> { pub fn do_maybe_in_run_loop(&self, task: Task<P>) -> Result<(), Task<P>> {
Err(task) Err(task)
} }
} }
@ -222,7 +222,7 @@ impl<P: Vst3Plugin> RunLoopEventHandler<P> {
/// Post a task to the tasks queue so it will be run on the host's GUI thread later. Returns the /// Post a task to the tasks queue so it will be run on the host's GUI thread later. Returns the
/// task if the queue is full and the task could not be posted. /// task if the queue is full and the task could not be posted.
pub fn post_task(&self, task: Task) -> Result<(), Task> { pub fn post_task(&self, task: Task<P>) -> Result<(), Task<P>> {
self.tasks.push(task)?; self.tasks.push(task)?;
// We need to use a Unix domain socket to let the host know to call our event handler. In // We need to use a Unix domain socket to let the host know to call our event handler. In