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}