diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 6484090b..4f4af44f 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -9,7 +9,12 @@ code then it will not be listed here. ## [2022-10-22] - The `Editor` trait and the `ParentWindowHandle` struct have been moved from - `nih_plug::plugin` to a new `nih_plug::editor` module. + `nih_plug::plugin` to a new `nih_plug::editor` module. If you only use the + prelude module then you won't need to change anything. +- The `nih_plug::context` module has been split up into + `nih_plug::context::init`, `nih_plug::context::process`, and + `nih_plug::context::gui` to make it clearer which structs go with which + context. You again don't have to change anything if you use the prelude. - NIH-plug has gained support for asynchronously running background tasks in a simple, type-safe, and realtime-safe way. This sadly does mean that every `Plugin` instance now needs to define a `BackgroundTask` type definition and diff --git a/src/context.rs b/src/context.rs index 9e6371bd..07417bec 100644 --- a/src/context.rs +++ b/src/context.rs @@ -2,249 +2,9 @@ use std::fmt::Display; -use crate::midi::NoteEvent; -use crate::params::internals::ParamPtr; -use crate::params::Param; -use crate::plugin::Plugin; -use crate::wrapper::state::PluginState; - -/// Callbacks the plugin can make while it is being initialized. This is passed to the plugin during -/// [`Plugin::initialize()`][crate::plugin::Plugin::initialize()]. -// -// # Safety -// -// The implementing wrapper needs to be able to handle concurrent requests, and it should perform -// the actual callback within [MainThreadQueue::do_maybe_async]. -pub trait InitContext { - /// Get the current plugin API. - fn plugin_api(&self) -> PluginApi; - - /// Run a task directly on this thread. This ensures that the task has finished executing before - /// the plugin finishes initializing. - /// - /// # Note - /// - /// There is no asynchronous alternative for this function as that may result in incorrect - /// behavior when doing offline rendering. - fn execute(&self, task: P::BackgroundTask); - - /// Update the current latency of the plugin. If the plugin is currently processing audio, then - /// this may cause audio playback to be restarted. - fn set_latency_samples(&self, samples: u32); - - /// Set the current voice **capacity** for this plugin (so not the number of currently active - /// voices). This may only be called if - /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] - /// is set. `capacity` must be between 1 and the configured maximum capacity. Changing this at - /// runtime allows the host to better optimize polyphonic modulation, or to switch to strictly - /// monophonic modulation when dropping the capacity down to 1. - fn set_current_voice_capacity(&self, capacity: u32); -} - -/// Contains both context data and callbacks the plugin can use during processing. Most notably this -/// is how a plugin sends and receives note events, gets transport information, and accesses -/// sidechain inputs and auxiliary outputs. This is passed to the plugin during as part of -/// [`Plugin::process()`][crate::plugin::Plugin::process()]. -// -// # Safety -// -// The implementing wrapper needs to be able to handle concurrent requests, and it should perform -// the actual callback within [MainThreadQueue::do_maybe_async]. -pub trait ProcessContext { - /// Get the current plugin API. - fn plugin_api(&self) -> PluginApi; - - /// 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. - /// - /// # Note - /// - /// Scheduling the same task multiple times will cause those duplicate tasks to pile up. Try to - /// either prevnt this from happening, or check whether the task still needs to be completed in - /// your task executor. - fn execute_async(&self, task: P::BackgroundTask); - - /// Get information about the current transport position and status. - fn transport(&self) -> &Transport; - - /// Returns the next note event, if there is one. Use [`NoteEvent::timing()`] to get the event's - /// timing within the buffer. Only available when - /// [`Plugin::MIDI_INPUT`][crate::prelude::Plugin::MIDI_INPUT] is set. - /// - /// # Usage - /// - /// You will likely want to use this with a loop, since there may be zero, one, or more events - /// for a sample: - /// - /// ```ignore - /// let mut next_event = context.next_event(); - /// for (sample_id, channel_samples) in buffer.iter_samples().enumerate() { - /// while let Some(event) = next_event { - /// if event.timing() != sample_id as u32 { - /// break; - /// } - /// - /// match event { - /// NoteEvent::NoteOn { note, velocity, .. } => { ... }, - /// NoteEvent::NoteOff { note, .. } if note == 69 => { ... }, - /// NoteEvent::PolyPressure { note, pressure, .. } { ... }, - /// _ => (), - /// } - /// - /// next_event = context.next_event(); - /// } - /// - /// // Do something with `channel_samples`... - /// } - /// - /// ProcessStatus::Normal - /// ``` - fn next_event(&mut self) -> Option; - - /// Send an event to the host. Only available when - /// [`Plugin::MIDI_OUTPUT`][crate::prelude::Plugin::MIDI_INPUT] is set. Will not do anything - /// otherwise. - fn send_event(&mut self, event: NoteEvent); - - /// Update the current latency of the plugin. If the plugin is currently processing audio, then - /// this may cause audio playback to be restarted. - fn set_latency_samples(&self, samples: u32); - - /// Set the current voice **capacity** for this plugin (so not the number of currently active - /// voices). This may only be called if - /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] - /// is set. `capacity` must be between 1 and the configured maximum capacity. Changing this at - /// runtime allows the host to better optimize polyphonic modulation, or to switch to strictly - /// monophonic modulation when dropping the capacity down to 1. - fn set_current_voice_capacity(&self, capacity: u32); - - // TODO: Add this, this works similar to [GuiContext::set_parameter] but it adds the parameter - // change to a queue (or directly to the VST3 plugin's parameter output queues) instead of - // using main thread host automation (and all the locks involved there). - // fn set_parameter(&self, param: &P, value: P::Plain); -} - -/// Callbacks the plugin can make when the user interacts with its GUI such as updating parameter -/// values. This is passed to the plugin during [`Editor::spawn()`][crate::prelude::Editor::spawn()]. All of -/// these functions assume they're being called from the main GUI thread. -// -// # Safety -// -// The implementing wrapper can assume that everything is being called from the main thread. Since -// NIH-plug doesn't own the GUI event loop, this invariant cannot be part of the interface. -pub trait GuiContext: Send + Sync + 'static { - /// Get the current plugin API. This may be useful to display in the plugin's GUI as part of an - /// about screen. - fn plugin_api(&self) -> PluginApi; - - /// Ask the host to resize the editor window to the size specified by - /// [`Editor::size()`][crate::prelude::Editor::size()]. This will return false if the host - /// somehow didn't like this and rejected the resize, in which case the window should revert to - /// its old size. You should only actually resize your embedded window once this returns `true`. - /// - /// TODO: Host->Plugin resizing has not been implemented yet - fn request_resize(&self) -> bool; - - /// Inform the host a parameter will be automated. Create a [`ParamSetter`] and use - /// [`ParamSetter::begin_set_parameter()`] instead for a safe, user friendly API. - /// - /// # Safety - /// - /// The implementing function still needs to check if `param` actually exists. This function is - /// mostly marked as unsafe for API reasons. - unsafe fn raw_begin_set_parameter(&self, param: ParamPtr); - - /// Inform the host a parameter is being automated with an already normalized value. Create a - /// [`ParamSetter`] and use [`ParamSetter::set_parameter()`] instead for a safe, user friendly - /// API. - /// - /// # Safety - /// - /// The implementing function still needs to check if `param` actually exists. This function is - /// mostly marked as unsafe for API reasons. - unsafe fn raw_set_parameter_normalized(&self, param: ParamPtr, normalized: f32); - - /// Inform the host a parameter has been automated. Create a [`ParamSetter`] and use - /// [`ParamSetter::end_set_parameter()`] instead for a safe, user friendly API. - /// - /// # Safety - /// - /// The implementing function still needs to check if `param` actually exists. This function is - /// mostly marked as unsafe for API reasons. - unsafe fn raw_end_set_parameter(&self, param: ParamPtr); - - /// Serialize the plugin's current state to a serde-serializable object. Useful for implementing - /// preset handling within a plugin's GUI. - fn get_state(&self) -> PluginState; - - /// Restore the state from a previously serialized state object. This will block the GUI thread - /// until the state has been restored and a parameter value rescan has been requested from the - /// host. If the plugin is currently processing audio, then the parameter values will be - /// restored at the end of the current processing cycle. - fn set_state(&self, state: PluginState); -} - -/// Information about the plugin's transport. Depending on the plugin API and the host not all -/// fields may be available. -#[derive(Debug)] -pub struct Transport { - /// Whether the transport is currently running. - pub playing: bool, - /// Whether recording is enabled in the project. - pub recording: bool, - /// Whether the pre-roll is currently active, if the plugin API reports this information. - pub preroll_active: Option, - - /// The sample rate in Hertz. Also passed in - /// [`Plugin::initialize()`][crate::prelude::Plugin::initialize()], so if you need this then you - /// can also store that value. - pub sample_rate: f32, - /// The project's tempo in beats per minute. - pub tempo: Option, - /// The time signature's numerator. - pub time_sig_numerator: Option, - /// The time signature's denominator. - pub time_sig_denominator: Option, - - // XXX: VST3 also has a continuous time in samples that ignores loops, but we can't reconstruct - // something similar in CLAP so it may be best to just ignore that so you can't rely on it - /// The position in the song in samples. Can be used to calculate the time in seconds if needed. - pub(crate) pos_samples: Option, - /// The position in the song in quarter notes. Can be used to calculate the time in samples if - /// needed. - pub(crate) pos_seconds: Option, - /// The position in the song in quarter notes. Can be calculated from the time in seconds and - /// the tempo if needed. - pub(crate) pos_beats: Option, - /// The last bar's start position in beats. Can be calculated from the beat position and time - /// signature if needed. - pub(crate) bar_start_pos_beats: Option, - /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at - /// the start of the song. Can be calculated from the beat position and time signature if - /// needed. - pub(crate) bar_number: Option, - - /// The loop range in samples, if the loop is active and this information is available. None of - /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the - /// end is exclusive. Can be calculated from the other loop range information if needed. - pub(crate) loop_range_samples: Option<(i64, i64)>, - /// The loop range in seconds, if the loop is active and this information is available. None of - /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the - /// end is exclusive. Can be calculated from the other loop range information if needed. - pub(crate) loop_range_seconds: Option<(f64, f64)>, - /// The loop range in quarter notes, if the loop is active and this information is available. - /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume - /// that the end is exclusive. Can be calculated from the other loop range information if - /// needed. - pub(crate) loop_range_beats: Option<(f64, f64)>, -} - -/// A convenience helper for setting parameter values. Any changes made here will be broadcasted to -/// the host and reflected in the plugin's [`Params`][crate::params::Params] object. These -/// functions should only be called from the main thread. -pub struct ParamSetter<'a> { - pub raw_context: &'a dyn GuiContext, -} +pub mod gui; +pub mod init; +pub mod process; /// The currently active plugin API. This may be useful to display in an about screen in the /// plugin's GUI for debugging purposes. @@ -264,245 +24,3 @@ impl Display for PluginApi { } } } - -// TODO: These conversions have not really been tested yet, there might be an error in there somewhere -impl Transport { - /// Initialize the transport struct without any information. - pub(crate) fn new(sample_rate: f32) -> Self { - Self { - playing: false, - recording: false, - preroll_active: None, - - sample_rate, - tempo: None, - time_sig_numerator: None, - time_sig_denominator: None, - - pos_samples: None, - pos_seconds: None, - pos_beats: None, - bar_start_pos_beats: None, - bar_number: None, - - loop_range_samples: None, - loop_range_seconds: None, - loop_range_beats: None, - } - } - - /// The position in the song in samples. Will be calculated from other information if needed. - pub fn pos_samples(&self) -> Option { - match ( - self.pos_samples, - self.pos_seconds, - self.pos_beats, - self.tempo, - ) { - (Some(pos_samples), _, _, _) => Some(pos_samples), - (_, Some(pos_seconds), _, _) => { - Some((pos_seconds * self.sample_rate as f64).round() as i64) - } - (_, _, Some(pos_beats), Some(tempo)) => { - Some((pos_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64) - } - (_, _, _, _) => None, - } - } - - /// The position in the song in quarter notes. Will be calculated from other information if - /// needed. - pub fn pos_seconds(&self) -> Option { - match ( - self.pos_samples, - self.pos_seconds, - self.pos_beats, - self.tempo, - ) { - (_, Some(pos_seconds), _, _) => Some(pos_seconds), - (Some(pos_samples), _, _, _) => Some(pos_samples as f64 / self.sample_rate as f64), - (_, _, Some(pos_beats), Some(tempo)) => Some(pos_beats / tempo * 60.0), - (_, _, _, _) => None, - } - } - - /// The position in the song in quarter notes. Will be calculated from other information if - /// needed. - pub fn pos_beats(&self) -> Option { - match ( - self.pos_samples, - self.pos_seconds, - self.pos_beats, - self.tempo, - ) { - (_, _, Some(pos_beats), _) => Some(pos_beats), - (_, Some(pos_seconds), _, Some(tempo)) => Some(pos_seconds / 60.0 * tempo), - (Some(pos_samples), _, _, Some(tempo)) => { - Some(pos_samples as f64 / self.sample_rate as f64 / 60.0 * tempo) - } - (_, _, _, _) => None, - } - } - - /// The last bar's start position in beats. Will be calculated from other information if needed. - pub fn bar_start_pos_beats(&self) -> Option { - if self.bar_start_pos_beats.is_some() { - return self.bar_start_pos_beats; - } - - match ( - self.time_sig_numerator, - self.time_sig_denominator, - self.pos_beats(), - ) { - (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => { - let quarter_note_bar_length = - time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0; - Some((pos_beats / quarter_note_bar_length).floor() * quarter_note_bar_length) - } - (_, _, _) => None, - } - } - - /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at - /// the start of the song. Will be calculated from other information if needed. - pub fn bar_number(&self) -> Option { - if self.bar_number.is_some() { - return self.bar_number; - } - - match ( - self.time_sig_numerator, - self.time_sig_denominator, - self.pos_beats(), - ) { - (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => { - let quarter_note_bar_length = - time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0; - Some((pos_beats / quarter_note_bar_length).floor() as i32) - } - (_, _, _) => None, - } - } - - /// The loop range in samples, if the loop is active and this information is available. None of - /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the - /// end is exclusive. Will be calculated from other information if needed. - pub fn loop_range_samples(&self) -> Option<(i64, i64)> { - match ( - self.loop_range_samples, - self.loop_range_seconds, - self.loop_range_beats, - self.tempo, - ) { - (Some(loop_range_samples), _, _, _) => Some(loop_range_samples), - (_, Some((start_seconds, end_seconds)), _, _) => Some(( - ((start_seconds * self.sample_rate as f64).round() as i64), - ((end_seconds * self.sample_rate as f64).round() as i64), - )), - (_, _, Some((start_beats, end_beats)), Some(tempo)) => Some(( - (start_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64, - (end_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64, - )), - (_, _, _, _) => None, - } - } - - /// The loop range in seconds, if the loop is active and this information is available. None of - /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the - /// end is exclusive. Will be calculated from other information if needed. - pub fn loop_range_seconds(&self) -> Option<(f64, f64)> { - match ( - self.loop_range_samples, - self.loop_range_seconds, - self.loop_range_beats, - self.tempo, - ) { - (_, Some(loop_range_seconds), _, _) => Some(loop_range_seconds), - (Some((start_samples, end_samples)), _, _, _) => Some(( - start_samples as f64 / self.sample_rate as f64, - end_samples as f64 / self.sample_rate as f64, - )), - (_, _, Some((start_beats, end_beats)), Some(tempo)) => { - Some((start_beats / tempo * 60.0, end_beats / tempo * 60.0)) - } - (_, _, _, _) => None, - } - } - - /// The loop range in quarter notes, if the loop is active and this information is available. - /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume - /// that the end is exclusive. Will be calculated from other information if needed. - pub fn loop_range_beats(&self) -> Option<(f64, f64)> { - match ( - self.loop_range_samples, - self.loop_range_seconds, - self.loop_range_beats, - self.tempo, - ) { - (_, _, Some(loop_range_beats), _) => Some(loop_range_beats), - (_, Some((start_seconds, end_seconds)), _, Some(tempo)) => { - Some((start_seconds / 60.0 * tempo, end_seconds / 60.0 * tempo)) - } - (Some((start_samples, end_samples)), _, _, Some(tempo)) => Some(( - start_samples as f64 / self.sample_rate as f64 / 60.0 * tempo, - end_samples as f64 / self.sample_rate as f64 / 60.0 * tempo, - )), - (_, _, _, _) => None, - } - } -} - -impl<'a> ParamSetter<'a> { - pub fn new(context: &'a dyn GuiContext) -> Self { - Self { - raw_context: context, - } - } - - /// Inform the host that you will start automating a parameter. This needs to be called before - /// calling [`set_parameter()`][Self::set_parameter()] for the specified parameter. - pub fn begin_set_parameter(&self, param: &P) { - unsafe { self.raw_context.raw_begin_set_parameter(param.as_ptr()) }; - } - - /// Set a parameter to the specified parameter value. You will need to call - /// [`begin_set_parameter()`][Self::begin_set_parameter()] before and - /// [`end_set_parameter()`][Self::end_set_parameter()] after calling this so the host can - /// properly record automation for the parameter. This can be called multiple times in a row - /// before calling [`end_set_parameter()`][Self::end_set_parameter()], for instance when moving - /// a slider around. - /// - /// This function assumes you're already calling this from a GUI thread. Calling any of these - /// functions from any other thread may result in unexpected behavior. - pub fn set_parameter(&self, param: &P, value: P::Plain) { - let ptr = param.as_ptr(); - let normalized = param.preview_normalized(value); - unsafe { - self.raw_context - .raw_set_parameter_normalized(ptr, normalized) - }; - } - - /// Set a parameter to an already normalized value. Works exactly the same as - /// [`set_parameter()`][Self::set_parameter()] and needs to follow the same rules, but this may - /// be useful when implementing a GUI. - /// - /// This does not perform any snapping. Consider converting the normalized value to a plain - /// value and setting that with [`set_parameter()`][Self::set_parameter()] instead so the - /// normalized value known to the host matches `param.normalized_value()`. - pub fn set_parameter_normalized(&self, param: &P, normalized: f32) { - let ptr = param.as_ptr(); - unsafe { - self.raw_context - .raw_set_parameter_normalized(ptr, normalized) - }; - } - - /// Inform the host that you are done automating a parameter. This needs to be called after one - /// or more [`set_parameter()`][Self::set_parameter()] calls for a parameter so the host knows - /// the automation gesture has finished. - pub fn end_set_parameter(&self, param: &P) { - unsafe { self.raw_context.raw_end_set_parameter(param.as_ptr()) }; - } -} diff --git a/src/context/gui.rs b/src/context/gui.rs new file mode 100644 index 00000000..4f69aab8 --- /dev/null +++ b/src/context/gui.rs @@ -0,0 +1,127 @@ +//! A context passed to a plugin's editor. + +use super::PluginApi; +use crate::params::internals::ParamPtr; +use crate::params::Param; +use crate::wrapper::state::PluginState; + +/// Callbacks the plugin can make when the user interacts with its GUI such as updating parameter +/// values. This is passed to the plugin during [`Editor::spawn()`][crate::prelude::Editor::spawn()]. All of +/// these functions assume they're being called from the main GUI thread. +// +// # Safety +// +// The implementing wrapper can assume that everything is being called from the main thread. Since +// NIH-plug doesn't own the GUI event loop, this invariant cannot be part of the interface. +pub trait GuiContext: Send + Sync + 'static { + /// Get the current plugin API. This may be useful to display in the plugin's GUI as part of an + /// about screen. + fn plugin_api(&self) -> PluginApi; + + /// Ask the host to resize the editor window to the size specified by + /// [`Editor::size()`][crate::prelude::Editor::size()]. This will return false if the host + /// somehow didn't like this and rejected the resize, in which case the window should revert to + /// its old size. You should only actually resize your embedded window once this returns `true`. + /// + /// TODO: Host->Plugin resizing has not been implemented yet + fn request_resize(&self) -> bool; + + /// Inform the host a parameter will be automated. Create a [`ParamSetter`] and use + /// [`ParamSetter::begin_set_parameter()`] instead for a safe, user friendly API. + /// + /// # Safety + /// + /// The implementing function still needs to check if `param` actually exists. This function is + /// mostly marked as unsafe for API reasons. + unsafe fn raw_begin_set_parameter(&self, param: ParamPtr); + + /// Inform the host a parameter is being automated with an already normalized value. Create a + /// [`ParamSetter`] and use [`ParamSetter::set_parameter()`] instead for a safe, user friendly + /// API. + /// + /// # Safety + /// + /// The implementing function still needs to check if `param` actually exists. This function is + /// mostly marked as unsafe for API reasons. + unsafe fn raw_set_parameter_normalized(&self, param: ParamPtr, normalized: f32); + + /// Inform the host a parameter has been automated. Create a [`ParamSetter`] and use + /// [`ParamSetter::end_set_parameter()`] instead for a safe, user friendly API. + /// + /// # Safety + /// + /// The implementing function still needs to check if `param` actually exists. This function is + /// mostly marked as unsafe for API reasons. + unsafe fn raw_end_set_parameter(&self, param: ParamPtr); + + /// Serialize the plugin's current state to a serde-serializable object. Useful for implementing + /// preset handling within a plugin's GUI. + fn get_state(&self) -> PluginState; + + /// Restore the state from a previously serialized state object. This will block the GUI thread + /// until the state has been restored and a parameter value rescan has been requested from the + /// host. If the plugin is currently processing audio, then the parameter values will be + /// restored at the end of the current processing cycle. + fn set_state(&self, state: PluginState); +} + +/// A convenience helper for setting parameter values. Any changes made here will be broadcasted to +/// the host and reflected in the plugin's [`Params`][crate::params::Params] object. These +/// functions should only be called from the main thread. +pub struct ParamSetter<'a> { + pub raw_context: &'a dyn GuiContext, +} + +impl<'a> ParamSetter<'a> { + pub fn new(context: &'a dyn GuiContext) -> Self { + Self { + raw_context: context, + } + } + + /// Inform the host that you will start automating a parameter. This needs to be called before + /// calling [`set_parameter()`][Self::set_parameter()] for the specified parameter. + pub fn begin_set_parameter(&self, param: &P) { + unsafe { self.raw_context.raw_begin_set_parameter(param.as_ptr()) }; + } + + /// Set a parameter to the specified parameter value. You will need to call + /// [`begin_set_parameter()`][Self::begin_set_parameter()] before and + /// [`end_set_parameter()`][Self::end_set_parameter()] after calling this so the host can + /// properly record automation for the parameter. This can be called multiple times in a row + /// before calling [`end_set_parameter()`][Self::end_set_parameter()], for instance when moving + /// a slider around. + /// + /// This function assumes you're already calling this from a GUI thread. Calling any of these + /// functions from any other thread may result in unexpected behavior. + pub fn set_parameter(&self, param: &P, value: P::Plain) { + let ptr = param.as_ptr(); + let normalized = param.preview_normalized(value); + unsafe { + self.raw_context + .raw_set_parameter_normalized(ptr, normalized) + }; + } + + /// Set a parameter to an already normalized value. Works exactly the same as + /// [`set_parameter()`][Self::set_parameter()] and needs to follow the same rules, but this may + /// be useful when implementing a GUI. + /// + /// This does not perform any snapping. Consider converting the normalized value to a plain + /// value and setting that with [`set_parameter()`][Self::set_parameter()] instead so the + /// normalized value known to the host matches `param.normalized_value()`. + pub fn set_parameter_normalized(&self, param: &P, normalized: f32) { + let ptr = param.as_ptr(); + unsafe { + self.raw_context + .raw_set_parameter_normalized(ptr, normalized) + }; + } + + /// Inform the host that you are done automating a parameter. This needs to be called after one + /// or more [`set_parameter()`][Self::set_parameter()] calls for a parameter so the host knows + /// the automation gesture has finished. + pub fn end_set_parameter(&self, param: &P) { + unsafe { self.raw_context.raw_end_set_parameter(param.as_ptr()) }; + } +} diff --git a/src/context/init.rs b/src/context/init.rs new file mode 100644 index 00000000..107a8f0f --- /dev/null +++ b/src/context/init.rs @@ -0,0 +1,37 @@ +//! A context passed during plugin initialization. + +use super::PluginApi; +use crate::plugin::Plugin; + +/// Callbacks the plugin can make while it is being initialized. This is passed to the plugin during +/// [`Plugin::initialize()`][crate::plugin::Plugin::initialize()]. +// +// # Safety +// +// The implementing wrapper needs to be able to handle concurrent requests, and it should perform +// the actual callback within [MainThreadQueue::do_maybe_async]. +pub trait InitContext { + /// Get the current plugin API. + fn plugin_api(&self) -> PluginApi; + + /// Run a task directly on this thread. This ensures that the task has finished executing before + /// the plugin finishes initializing. + /// + /// # Note + /// + /// There is no asynchronous alternative for this function as that may result in incorrect + /// behavior when doing offline rendering. + fn execute(&self, task: P::BackgroundTask); + + /// Update the current latency of the plugin. If the plugin is currently processing audio, then + /// this may cause audio playback to be restarted. + fn set_latency_samples(&self, samples: u32); + + /// Set the current voice **capacity** for this plugin (so not the number of currently active + /// voices). This may only be called if + /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] + /// is set. `capacity` must be between 1 and the configured maximum capacity. Changing this at + /// runtime allows the host to better optimize polyphonic modulation, or to switch to strictly + /// monophonic modulation when dropping the capacity down to 1. + fn set_current_voice_capacity(&self, capacity: u32); +} diff --git a/src/context/process.rs b/src/context/process.rs new file mode 100644 index 00000000..8e5154e5 --- /dev/null +++ b/src/context/process.rs @@ -0,0 +1,330 @@ +//! A context passed during the process function. + +use super::PluginApi; +use crate::midi::NoteEvent; +use crate::plugin::Plugin; + +/// Contains both context data and callbacks the plugin can use during processing. Most notably this +/// is how a plugin sends and receives note events, gets transport information, and accesses +/// sidechain inputs and auxiliary outputs. This is passed to the plugin during as part of +/// [`Plugin::process()`][crate::plugin::Plugin::process()]. +// +// # Safety +// +// The implementing wrapper needs to be able to handle concurrent requests, and it should perform +// the actual callback within [MainThreadQueue::do_maybe_async]. +pub trait ProcessContext { + /// Get the current plugin API. + fn plugin_api(&self) -> PluginApi; + + /// 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. + /// + /// # Note + /// + /// Scheduling the same task multiple times will cause those duplicate tasks to pile up. Try to + /// either prevnt this from happening, or check whether the task still needs to be completed in + /// your task executor. + fn execute_async(&self, task: P::BackgroundTask); + + /// Get information about the current transport position and status. + fn transport(&self) -> &Transport; + + /// Returns the next note event, if there is one. Use [`NoteEvent::timing()`] to get the event's + /// timing within the buffer. Only available when + /// [`Plugin::MIDI_INPUT`][crate::prelude::Plugin::MIDI_INPUT] is set. + /// + /// # Usage + /// + /// You will likely want to use this with a loop, since there may be zero, one, or more events + /// for a sample: + /// + /// ```ignore + /// let mut next_event = context.next_event(); + /// for (sample_id, channel_samples) in buffer.iter_samples().enumerate() { + /// while let Some(event) = next_event { + /// if event.timing() != sample_id as u32 { + /// break; + /// } + /// + /// match event { + /// NoteEvent::NoteOn { note, velocity, .. } => { ... }, + /// NoteEvent::NoteOff { note, .. } if note == 69 => { ... }, + /// NoteEvent::PolyPressure { note, pressure, .. } { ... }, + /// _ => (), + /// } + /// + /// next_event = context.next_event(); + /// } + /// + /// // Do something with `channel_samples`... + /// } + /// + /// ProcessStatus::Normal + /// ``` + fn next_event(&mut self) -> Option; + + /// Send an event to the host. Only available when + /// [`Plugin::MIDI_OUTPUT`][crate::prelude::Plugin::MIDI_INPUT] is set. Will not do anything + /// otherwise. + fn send_event(&mut self, event: NoteEvent); + + /// Update the current latency of the plugin. If the plugin is currently processing audio, then + /// this may cause audio playback to be restarted. + fn set_latency_samples(&self, samples: u32); + + /// Set the current voice **capacity** for this plugin (so not the number of currently active + /// voices). This may only be called if + /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] + /// is set. `capacity` must be between 1 and the configured maximum capacity. Changing this at + /// runtime allows the host to better optimize polyphonic modulation, or to switch to strictly + /// monophonic modulation when dropping the capacity down to 1. + fn set_current_voice_capacity(&self, capacity: u32); + + // TODO: Add this, this works similar to [GuiContext::set_parameter] but it adds the parameter + // change to a queue (or directly to the VST3 plugin's parameter output queues) instead of + // using main thread host automation (and all the locks involved there). + // fn set_parameter(&self, param: &P, value: P::Plain); +} + +/// Information about the plugin's transport. Depending on the plugin API and the host not all +/// fields may be available. +#[derive(Debug)] +pub struct Transport { + /// Whether the transport is currently running. + pub playing: bool, + /// Whether recording is enabled in the project. + pub recording: bool, + /// Whether the pre-roll is currently active, if the plugin API reports this information. + pub preroll_active: Option, + + /// The sample rate in Hertz. Also passed in + /// [`Plugin::initialize()`][crate::prelude::Plugin::initialize()], so if you need this then you + /// can also store that value. + pub sample_rate: f32, + /// The project's tempo in beats per minute. + pub tempo: Option, + /// The time signature's numerator. + pub time_sig_numerator: Option, + /// The time signature's denominator. + pub time_sig_denominator: Option, + + // XXX: VST3 also has a continuous time in samples that ignores loops, but we can't reconstruct + // something similar in CLAP so it may be best to just ignore that so you can't rely on it + /// The position in the song in samples. Can be used to calculate the time in seconds if needed. + pub(crate) pos_samples: Option, + /// The position in the song in quarter notes. Can be used to calculate the time in samples if + /// needed. + pub(crate) pos_seconds: Option, + /// The position in the song in quarter notes. Can be calculated from the time in seconds and + /// the tempo if needed. + pub(crate) pos_beats: Option, + /// The last bar's start position in beats. Can be calculated from the beat position and time + /// signature if needed. + pub(crate) bar_start_pos_beats: Option, + /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at + /// the start of the song. Can be calculated from the beat position and time signature if + /// needed. + pub(crate) bar_number: Option, + + /// The loop range in samples, if the loop is active and this information is available. None of + /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the + /// end is exclusive. Can be calculated from the other loop range information if needed. + pub(crate) loop_range_samples: Option<(i64, i64)>, + /// The loop range in seconds, if the loop is active and this information is available. None of + /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the + /// end is exclusive. Can be calculated from the other loop range information if needed. + pub(crate) loop_range_seconds: Option<(f64, f64)>, + /// The loop range in quarter notes, if the loop is active and this information is available. + /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume + /// that the end is exclusive. Can be calculated from the other loop range information if + /// needed. + pub(crate) loop_range_beats: Option<(f64, f64)>, +} + +impl Transport { + /// Initialize the transport struct without any information. + pub(crate) fn new(sample_rate: f32) -> Self { + Self { + playing: false, + recording: false, + preroll_active: None, + + sample_rate, + tempo: None, + time_sig_numerator: None, + time_sig_denominator: None, + + pos_samples: None, + pos_seconds: None, + pos_beats: None, + bar_start_pos_beats: None, + bar_number: None, + + loop_range_samples: None, + loop_range_seconds: None, + loop_range_beats: None, + } + } + + /// The position in the song in samples. Will be calculated from other information if needed. + pub fn pos_samples(&self) -> Option { + match ( + self.pos_samples, + self.pos_seconds, + self.pos_beats, + self.tempo, + ) { + (Some(pos_samples), _, _, _) => Some(pos_samples), + (_, Some(pos_seconds), _, _) => { + Some((pos_seconds * self.sample_rate as f64).round() as i64) + } + (_, _, Some(pos_beats), Some(tempo)) => { + Some((pos_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64) + } + (_, _, _, _) => None, + } + } + + /// The position in the song in quarter notes. Will be calculated from other information if + /// needed. + pub fn pos_seconds(&self) -> Option { + match ( + self.pos_samples, + self.pos_seconds, + self.pos_beats, + self.tempo, + ) { + (_, Some(pos_seconds), _, _) => Some(pos_seconds), + (Some(pos_samples), _, _, _) => Some(pos_samples as f64 / self.sample_rate as f64), + (_, _, Some(pos_beats), Some(tempo)) => Some(pos_beats / tempo * 60.0), + (_, _, _, _) => None, + } + } + + /// The position in the song in quarter notes. Will be calculated from other information if + /// needed. + pub fn pos_beats(&self) -> Option { + match ( + self.pos_samples, + self.pos_seconds, + self.pos_beats, + self.tempo, + ) { + (_, _, Some(pos_beats), _) => Some(pos_beats), + (_, Some(pos_seconds), _, Some(tempo)) => Some(pos_seconds / 60.0 * tempo), + (Some(pos_samples), _, _, Some(tempo)) => { + Some(pos_samples as f64 / self.sample_rate as f64 / 60.0 * tempo) + } + (_, _, _, _) => None, + } + } + + /// The last bar's start position in beats. Will be calculated from other information if needed. + pub fn bar_start_pos_beats(&self) -> Option { + if self.bar_start_pos_beats.is_some() { + return self.bar_start_pos_beats; + } + + match ( + self.time_sig_numerator, + self.time_sig_denominator, + self.pos_beats(), + ) { + (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => { + let quarter_note_bar_length = + time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0; + Some((pos_beats / quarter_note_bar_length).floor() * quarter_note_bar_length) + } + (_, _, _) => None, + } + } + + /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at + /// the start of the song. Will be calculated from other information if needed. + pub fn bar_number(&self) -> Option { + if self.bar_number.is_some() { + return self.bar_number; + } + + match ( + self.time_sig_numerator, + self.time_sig_denominator, + self.pos_beats(), + ) { + (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => { + let quarter_note_bar_length = + time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0; + Some((pos_beats / quarter_note_bar_length).floor() as i32) + } + (_, _, _) => None, + } + } + + /// The loop range in samples, if the loop is active and this information is available. None of + /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the + /// end is exclusive. Will be calculated from other information if needed. + pub fn loop_range_samples(&self) -> Option<(i64, i64)> { + match ( + self.loop_range_samples, + self.loop_range_seconds, + self.loop_range_beats, + self.tempo, + ) { + (Some(loop_range_samples), _, _, _) => Some(loop_range_samples), + (_, Some((start_seconds, end_seconds)), _, _) => Some(( + ((start_seconds * self.sample_rate as f64).round() as i64), + ((end_seconds * self.sample_rate as f64).round() as i64), + )), + (_, _, Some((start_beats, end_beats)), Some(tempo)) => Some(( + (start_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64, + (end_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64, + )), + (_, _, _, _) => None, + } + } + + /// The loop range in seconds, if the loop is active and this information is available. None of + /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the + /// end is exclusive. Will be calculated from other information if needed. + pub fn loop_range_seconds(&self) -> Option<(f64, f64)> { + match ( + self.loop_range_samples, + self.loop_range_seconds, + self.loop_range_beats, + self.tempo, + ) { + (_, Some(loop_range_seconds), _, _) => Some(loop_range_seconds), + (Some((start_samples, end_samples)), _, _, _) => Some(( + start_samples as f64 / self.sample_rate as f64, + end_samples as f64 / self.sample_rate as f64, + )), + (_, _, Some((start_beats, end_beats)), Some(tempo)) => { + Some((start_beats / tempo * 60.0, end_beats / tempo * 60.0)) + } + (_, _, _, _) => None, + } + } + + /// The loop range in quarter notes, if the loop is active and this information is available. + /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume + /// that the end is exclusive. Will be calculated from other information if needed. + pub fn loop_range_beats(&self) -> Option<(f64, f64)> { + match ( + self.loop_range_samples, + self.loop_range_seconds, + self.loop_range_beats, + self.tempo, + ) { + (_, _, Some(loop_range_beats), _) => Some(loop_range_beats), + (_, Some((start_seconds, end_seconds)), _, Some(tempo)) => { + Some((start_seconds / 60.0 * tempo, end_seconds / 60.0 * tempo)) + } + (Some((start_samples, end_samples)), _, _, Some(tempo)) => Some(( + start_samples as f64 / self.sample_rate as f64 / 60.0 * tempo, + end_samples as f64 / self.sample_rate as f64 / 60.0 * tempo, + )), + (_, _, _, _) => None, + } + } +} diff --git a/src/editor.rs b/src/editor.rs index 31ef2e9a..67393f30 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -4,7 +4,7 @@ use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; use std::any::Any; use std::sync::Arc; -use crate::context::GuiContext; +use crate::context::gui::GuiContext; /// An editor for a [`Plugin`][crate::prelude::Plugin]. pub trait Editor: Send { diff --git a/src/plugin.rs b/src/plugin.rs index 2aab1ab8..0160a32b 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use crate::buffer::Buffer; -use crate::context::{InitContext, ProcessContext}; +use crate::context::init::InitContext; +use crate::context::process::ProcessContext; use crate::editor::Editor; use crate::midi::MidiConfig; use crate::params::Params; diff --git a/src/prelude.rs b/src/prelude.rs index 36e22c4f..61faf6f2 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -11,7 +11,9 @@ pub use crate::formatters; pub use crate::util; pub use crate::buffer::Buffer; -pub use crate::context::{GuiContext, InitContext, ParamSetter, PluginApi, ProcessContext}; +pub use crate::context::gui::{GuiContext, ParamSetter}; +pub use crate::context::init::InitContext; +pub use crate::context::process::ProcessContext; // This also includes the derive macro pub use crate::editor::{Editor, ParentWindowHandle}; pub use crate::midi::{control_change, MidiConfig, NoteEvent}; diff --git a/src/wrapper/clap/context.rs b/src/wrapper/clap/context.rs index 9a5fd428..c4ecfe66 100644 --- a/src/wrapper/clap/context.rs +++ b/src/wrapper/clap/context.rs @@ -3,7 +3,10 @@ use std::collections::VecDeque; use std::sync::Arc; use super::wrapper::{OutputParamEvent, Task, Wrapper}; -use crate::context::{GuiContext, InitContext, PluginApi, ProcessContext, Transport}; +use crate::context::gui::GuiContext; +use crate::context::init::InitContext; +use crate::context::process::{ProcessContext, Transport}; +use crate::context::PluginApi; use crate::event_loop::EventLoop; use crate::midi::NoteEvent; use crate::params::internals::ParamPtr; diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index 1fff8391..661d1d9d 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -77,7 +77,7 @@ use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContex use super::descriptor::PluginDescriptor; use super::util::ClapPtr; use crate::buffer::Buffer; -use crate::context::Transport; +use crate::context::process::Transport; use crate::editor::{Editor, ParentWindowHandle}; use crate::event_loop::{EventLoop, MainThreadExecutor, TASK_QUEUE_CAPACITY}; use crate::midi::{MidiConfig, NoteEvent}; diff --git a/src/wrapper/standalone/backend.rs b/src/wrapper/standalone/backend.rs index 7778b4c8..c56d7fc6 100644 --- a/src/wrapper/standalone/backend.rs +++ b/src/wrapper/standalone/backend.rs @@ -1,4 +1,4 @@ -use crate::context::Transport; +use crate::context::process::Transport; use crate::midi::NoteEvent; mod cpal; diff --git a/src/wrapper/standalone/backend/cpal.rs b/src/wrapper/standalone/backend/cpal.rs index 12145c5d..63b4e36b 100644 --- a/src/wrapper/standalone/backend/cpal.rs +++ b/src/wrapper/standalone/backend/cpal.rs @@ -9,7 +9,7 @@ use rtrb::RingBuffer; use super::super::config::WrapperConfig; use super::Backend; use crate::buffer::Buffer; -use crate::context::Transport; +use crate::context::process::Transport; use crate::midi::{MidiConfig, NoteEvent}; use crate::plugin::{AuxiliaryIOConfig, BusConfig, Plugin}; diff --git a/src/wrapper/standalone/backend/dummy.rs b/src/wrapper/standalone/backend/dummy.rs index b59767ad..90158c5b 100644 --- a/src/wrapper/standalone/backend/dummy.rs +++ b/src/wrapper/standalone/backend/dummy.rs @@ -3,7 +3,7 @@ use std::time::{Duration, Instant}; use super::super::config::WrapperConfig; use super::Backend; use crate::buffer::Buffer; -use crate::context::Transport; +use crate::context::process::Transport; use crate::midi::NoteEvent; use crate::plugin::{AuxiliaryIOConfig, BusConfig, Plugin}; diff --git a/src/wrapper/standalone/backend/jack.rs b/src/wrapper/standalone/backend/jack.rs index 69c319ec..66725ad4 100644 --- a/src/wrapper/standalone/backend/jack.rs +++ b/src/wrapper/standalone/backend/jack.rs @@ -11,7 +11,7 @@ use parking_lot::Mutex; use super::super::config::WrapperConfig; use super::Backend; use crate::buffer::Buffer; -use crate::context::Transport; +use crate::context::process::Transport; use crate::midi::{MidiConfig, NoteEvent}; use crate::plugin::Plugin; diff --git a/src/wrapper/standalone/context.rs b/src/wrapper/standalone/context.rs index 292fc38e..1317b2df 100644 --- a/src/wrapper/standalone/context.rs +++ b/src/wrapper/standalone/context.rs @@ -3,7 +3,10 @@ use std::sync::Arc; use super::backend::Backend; use super::wrapper::{GuiTask, Wrapper}; -use crate::context::{GuiContext, InitContext, PluginApi, ProcessContext, Transport}; +use crate::context::gui::GuiContext; +use crate::context::init::InitContext; +use crate::context::process::{ProcessContext, Transport}; +use crate::context::PluginApi; use crate::event_loop::EventLoop; use crate::midi::NoteEvent; use crate::params::internals::ParamPtr; diff --git a/src/wrapper/standalone/wrapper.rs b/src/wrapper/standalone/wrapper.rs index c3952d4f..056abdbc 100644 --- a/src/wrapper/standalone/wrapper.rs +++ b/src/wrapper/standalone/wrapper.rs @@ -13,7 +13,7 @@ use std::thread; use super::backend::Backend; use super::config::WrapperConfig; use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext}; -use crate::context::Transport; +use crate::context::process::Transport; use crate::editor::{Editor, ParentWindowHandle}; use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; use crate::midi::NoteEvent; diff --git a/src/wrapper/vst3/context.rs b/src/wrapper/vst3/context.rs index 61dcf241..8aba52f3 100644 --- a/src/wrapper/vst3/context.rs +++ b/src/wrapper/vst3/context.rs @@ -5,7 +5,10 @@ use std::sync::Arc; use vst3_sys::vst::IComponentHandler; use super::inner::{Task, WrapperInner}; -use crate::context::{GuiContext, InitContext, PluginApi, ProcessContext, Transport}; +use crate::context::gui::GuiContext; +use crate::context::init::InitContext; +use crate::context::process::{ProcessContext, Transport}; +use crate::context::PluginApi; use crate::midi::NoteEvent; use crate::params::internals::ParamPtr; use crate::plugin::Vst3Plugin; diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index 8c8b70eb..893b16a0 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -15,7 +15,7 @@ use super::param_units::ParamUnits; use super::util::{ObjectPtr, VstPtr, VST3_MIDI_PARAMS_END, VST3_MIDI_PARAMS_START}; use super::view::WrapperView; use crate::buffer::Buffer; -use crate::context::Transport; +use crate::context::process::Transport; use crate::editor::Editor; use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; use crate::midi::{MidiConfig, NoteEvent}; diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index be824675..98caea1c 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -24,7 +24,7 @@ use super::util::{ }; use super::view::WrapperView; use crate::buffer::Buffer; -use crate::context::Transport; +use crate::context::process::Transport; use crate::midi::{MidiConfig, NoteEvent}; use crate::params::ParamFlags; use crate::plugin::{