Skip to main content

nice_plug_core/context/
process.rs

1//! A context passed during the process function.
2
3use crate::{
4    midi::{MidiConfig, PluginNoteEvent},
5    plugin::Plugin,
6};
7
8use super::PluginApi;
9
10/// Contains both context data and callbacks the plugin can use during processing. Most notably this
11/// is how a plugin sends and receives note events, gets transport information, and accesses
12/// sidechain inputs and auxiliary outputs. This is passed to the plugin during as part of
13/// [`Plugin::process()`][crate::plugin::Plugin::process()].
14//
15// # Safety
16//
17// The implementing wrapper needs to be able to handle concurrent requests, and it should perform
18// the actual callback within [MainThreadQueue::schedule_gui].
19pub trait ProcessContext<P: Plugin> {
20    /// Get the current plugin API.
21    fn plugin_api(&self) -> PluginApi;
22
23    /// Execute a task on a background thread using `[Plugin::task_executor]`. This allows you to
24    /// defer expensive tasks for later without blocking either the process function or the GUI
25    /// thread. As long as creating the `task` is realtime-safe, this operation is too.
26    ///
27    /// # Note
28    ///
29    /// Scheduling the same task multiple times will cause those duplicate tasks to pile up. Try to
30    /// either prevent this from happening, or check whether the task still needs to be completed in
31    /// your task executor.
32    fn execute_background(&self, task: P::BackgroundTask);
33
34    /// Execute a task on a background thread using `[Plugin::task_executor]`. As long as creating
35    /// the `task` is realtime-safe, this operation is too.
36    ///
37    /// # Note
38    ///
39    /// Scheduling the same task multiple times will cause those duplicate tasks to pile up. Try to
40    /// either prevent this from happening, or check whether the task still needs to be completed in
41    /// your task executor.
42    fn execute_gui(&self, task: P::BackgroundTask);
43
44    /// Get information about the current transport position and status.
45    fn transport(&self) -> &Transport;
46
47    /// Returns the next note event, if there is one. Use
48    /// [`NoteEvent::timing()`][crate::midi::NoteEvent::timing()] to get the event's timing
49    /// within the buffer. Only available when [`Plugin::MIDI_INPUT`] is set.
50    ///
51    /// # Usage
52    ///
53    /// You will likely want to use this with a loop, since there may be zero, one, or more events
54    /// for a sample:
55    ///
56    /// ```ignore
57    /// let mut next_event = context.next_event();
58    /// for (sample_id, channel_samples) in buffer.iter_samples().enumerate() {
59    ///     while let Some(event) = next_event {
60    ///         if event.timing() != sample_id as u32 {
61    ///             break;
62    ///         }
63    ///
64    ///         match event {
65    ///             NoteEvent::NoteOn { note, velocity, .. } => { ... },
66    ///             NoteEvent::NoteOff { note, .. } if note == 69 => { ... },
67    ///             NoteEvent::PolyPressure { note, pressure, .. } { ... },
68    ///             _ => (),
69    ///         }
70    ///
71    ///         next_event = context.next_event();
72    ///     }
73    ///
74    ///     // Do something with `channel_samples`...
75    /// }
76    ///
77    /// ProcessStatus::Normal
78    /// ```
79    fn next_event(&mut self) -> Option<PluginNoteEvent<P>>;
80
81    /// Try to send an event to the host's output event buffer. Only available when
82    /// [`Plugin::MIDI_OUTPUT`] is set.
83    fn try_send_event(
84        &mut self,
85        event: PluginNoteEvent<P>,
86    ) -> Result<(), (PluginNoteEvent<P>, SendEventError)>;
87
88    /// Update the current latency of the plugin. If the plugin is currently processing audio, then
89    /// this may cause audio playback to be restarted.
90    fn set_latency_samples(&self, samples: u32);
91
92    /// Request the plugin to be restarted.
93    fn request_restart(&self);
94
95    /// Set the current voice **capacity** for this plugin (so not the number of currently active
96    /// voices). This may only be called if `ClapPlugin::CLAP_POLY_MODULATION_CONFIG` is set.
97    /// `capacity` must be between 1 and the configured maximum capacity. Changing this at runtime
98    /// allows the host to better optimize polyphonic modulation, or to switch to strictly monophonic
99    /// modulation when dropping the capacity down to 1.
100    fn set_current_voice_capacity(&self, capacity: u32);
101
102    // TODO: Add this, this works similar to [GuiContext::set_parameter] but it adds the parameter
103    //       change to a queue (or directly to the VST3 plugin's parameter output queues) instead of
104    //       using main thread host automation (and all the locks involved there).
105    // fn set_parameter<P: Param>(&self, param: &P, value: P::Plain);
106}
107
108/// An error occurred while sending an event with [`ProcessContext::try_send_event()`].
109#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
110pub enum SendEventError {
111    /// The host's output event buffer is full.
112    #[error("Failed to send output event: Host output event buffer is full")]
113    HostBufferFull,
114    /// The host does not have an output event buffer.
115    #[error("Failed to send output event: Host does not have output event buffer")]
116    NoOutputBuffer,
117    /// Invalid event type for the plugin's [`Plugin::MIDI_OUTPUT`] configuration.
118    #[error(
119        "Failed to send output event: Invalid event type for output config {midi_output_config:?}"
120    )]
121    InvalidEvent { midi_output_config: MidiConfig },
122}
123
124/// Information about the plugin's transport. Depending on the plugin API and the host not all
125/// fields may be available.
126#[derive(Debug)]
127pub struct Transport {
128    /// Whether the transport is currently running.
129    pub playing: bool,
130    /// Whether recording is enabled in the project.
131    pub recording: bool,
132    /// Whether the pre-roll is currently active, if the plugin API reports this information.
133    pub preroll_active: Option<bool>,
134
135    /// The sample rate in Hertz. Also passed in
136    /// [`Plugin::activate()`][crate::plugin::Plugin::activate()], so if you need this then you
137    /// can also store that value.
138    pub sample_rate: f32,
139    /// The project's tempo in beats per minute.
140    pub tempo: Option<f64>,
141    /// The time signature's numerator.
142    pub time_sig_numerator: Option<i32>,
143    /// The time signature's denominator.
144    pub time_sig_denominator: Option<i32>,
145
146    // XXX: VST3 also has a continuous time in samples that ignores loops, but we can't reconstruct
147    //      something similar in CLAP so it may be best to just ignore that so you can't rely on it
148    /// The position in the song in samples. Can be used to calculate the time in seconds if needed.
149    pub pos_samples: Option<i64>,
150    /// The position in the song in seconds. Can be used to calculate the time in samples if needed.
151    pub pos_seconds: Option<f64>,
152    /// The position in the song in quarter notes. Can be calculated from the time in seconds and
153    /// the tempo if needed.
154    pub pos_beats: Option<f64>,
155    /// The last bar's start position in beats. Can be calculated from the beat position and time
156    /// signature if needed.
157    pub bar_start_pos_beats: Option<f64>,
158    /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at
159    /// the start of the song. Can be calculated from the beat position and time signature if
160    /// needed.
161    pub bar_number: Option<i32>,
162
163    /// The loop range in samples, if the loop is active and this information is available. None of
164    /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the
165    /// end is exclusive. Can be calculated from the other loop range information if needed.
166    pub loop_range_samples: Option<(i64, i64)>,
167    /// The loop range in seconds, if the loop is active and this information is available. None of
168    /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the
169    /// end is exclusive. Can be calculated from the other loop range information if needed.
170    pub loop_range_seconds: Option<(f64, f64)>,
171    /// The loop range in quarter notes, if the loop is active and this information is available.
172    /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume
173    /// that the end is exclusive. Can be calculated from the other loop range information if
174    /// needed.
175    pub loop_range_beats: Option<(f64, f64)>,
176}
177
178impl Transport {
179    /// Initialize the transport struct without any information.
180    pub fn new(sample_rate: f32) -> Self {
181        Self {
182            playing: false,
183            recording: false,
184            preroll_active: None,
185
186            sample_rate,
187            tempo: None,
188            time_sig_numerator: None,
189            time_sig_denominator: None,
190
191            pos_samples: None,
192            pos_seconds: None,
193            pos_beats: None,
194            bar_start_pos_beats: None,
195            bar_number: None,
196
197            loop_range_samples: None,
198            loop_range_seconds: None,
199            loop_range_beats: None,
200        }
201    }
202
203    /// The position in the song in samples. Will be calculated from other information if needed.
204    pub fn pos_samples(&self) -> Option<i64> {
205        match (
206            self.pos_samples,
207            self.pos_seconds,
208            self.pos_beats,
209            self.tempo,
210        ) {
211            (Some(pos_samples), _, _, _) => Some(pos_samples),
212            (_, Some(pos_seconds), _, _) => {
213                Some((pos_seconds * self.sample_rate as f64).round() as i64)
214            }
215            (_, _, Some(pos_beats), Some(tempo)) => {
216                Some((pos_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64)
217            }
218            (_, _, _, _) => None,
219        }
220    }
221
222    /// The position in the song in seconds. Can be used to calculate the time in samples if needed.
223    pub fn pos_seconds(&self) -> Option<f64> {
224        match (
225            self.pos_samples,
226            self.pos_seconds,
227            self.pos_beats,
228            self.tempo,
229        ) {
230            (_, Some(pos_seconds), _, _) => Some(pos_seconds),
231            (Some(pos_samples), _, _, _) => Some(pos_samples as f64 / self.sample_rate as f64),
232            (_, _, Some(pos_beats), Some(tempo)) => Some(pos_beats / tempo * 60.0),
233            (_, _, _, _) => None,
234        }
235    }
236
237    /// The position in the song in quarter notes. Will be calculated from other information if
238    /// needed.
239    pub fn pos_beats(&self) -> Option<f64> {
240        match (
241            self.pos_samples,
242            self.pos_seconds,
243            self.pos_beats,
244            self.tempo,
245        ) {
246            (_, _, Some(pos_beats), _) => Some(pos_beats),
247            (_, Some(pos_seconds), _, Some(tempo)) => Some(pos_seconds / 60.0 * tempo),
248            (Some(pos_samples), _, _, Some(tempo)) => {
249                Some(pos_samples as f64 / self.sample_rate as f64 / 60.0 * tempo)
250            }
251            (_, _, _, _) => None,
252        }
253    }
254
255    /// The last bar's start position in beats. Will be calculated from other information if needed.
256    pub fn bar_start_pos_beats(&self) -> Option<f64> {
257        if self.bar_start_pos_beats.is_some() {
258            return self.bar_start_pos_beats;
259        }
260
261        match (
262            self.time_sig_numerator,
263            self.time_sig_denominator,
264            self.pos_beats(),
265        ) {
266            (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => {
267                let quarter_note_bar_length =
268                    time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0;
269                Some((pos_beats / quarter_note_bar_length).floor() * quarter_note_bar_length)
270            }
271            (_, _, _) => None,
272        }
273    }
274
275    /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at
276    /// the start of the song. Will be calculated from other information if needed.
277    pub fn bar_number(&self) -> Option<i32> {
278        if self.bar_number.is_some() {
279            return self.bar_number;
280        }
281
282        match (
283            self.time_sig_numerator,
284            self.time_sig_denominator,
285            self.pos_beats(),
286        ) {
287            (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => {
288                let quarter_note_bar_length =
289                    time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0;
290                Some((pos_beats / quarter_note_bar_length).floor() as i32)
291            }
292            (_, _, _) => None,
293        }
294    }
295
296    /// The loop range in samples, if the loop is active and this information is available. None of
297    /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the
298    /// end is exclusive. Will be calculated from other information if needed.
299    pub fn loop_range_samples(&self) -> Option<(i64, i64)> {
300        match (
301            self.loop_range_samples,
302            self.loop_range_seconds,
303            self.loop_range_beats,
304            self.tempo,
305        ) {
306            (Some(loop_range_samples), _, _, _) => Some(loop_range_samples),
307            (_, Some((start_seconds, end_seconds)), _, _) => Some((
308                ((start_seconds * self.sample_rate as f64).round() as i64),
309                ((end_seconds * self.sample_rate as f64).round() as i64),
310            )),
311            (_, _, Some((start_beats, end_beats)), Some(tempo)) => Some((
312                (start_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64,
313                (end_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64,
314            )),
315            (_, _, _, _) => None,
316        }
317    }
318
319    /// The loop range in seconds, if the loop is active and this information is available. None of
320    /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the
321    /// end is exclusive. Will be calculated from other information if needed.
322    pub fn loop_range_seconds(&self) -> Option<(f64, f64)> {
323        match (
324            self.loop_range_samples,
325            self.loop_range_seconds,
326            self.loop_range_beats,
327            self.tempo,
328        ) {
329            (_, Some(loop_range_seconds), _, _) => Some(loop_range_seconds),
330            (Some((start_samples, end_samples)), _, _, _) => Some((
331                start_samples as f64 / self.sample_rate as f64,
332                end_samples as f64 / self.sample_rate as f64,
333            )),
334            (_, _, Some((start_beats, end_beats)), Some(tempo)) => {
335                Some((start_beats / tempo * 60.0, end_beats / tempo * 60.0))
336            }
337            (_, _, _, _) => None,
338        }
339    }
340
341    /// The loop range in quarter notes, if the loop is active and this information is available.
342    /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume
343    /// that the end is exclusive. Will be calculated from other information if needed.
344    pub fn loop_range_beats(&self) -> Option<(f64, f64)> {
345        match (
346            self.loop_range_samples,
347            self.loop_range_seconds,
348            self.loop_range_beats,
349            self.tempo,
350        ) {
351            (_, _, Some(loop_range_beats), _) => Some(loop_range_beats),
352            (_, Some((start_seconds, end_seconds)), _, Some(tempo)) => {
353                Some((start_seconds / 60.0 * tempo, end_seconds / 60.0 * tempo))
354            }
355            (Some((start_samples, end_samples)), _, _, Some(tempo)) => Some((
356                start_samples as f64 / self.sample_rate as f64 / 60.0 * tempo,
357                end_samples as f64 / self.sample_rate as f64 / 60.0 * tempo,
358            )),
359            (_, _, _, _) => None,
360        }
361    }
362}