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#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
109pub enum SendEventError {
110 #[error("Failed to send output event: Host output event buffer is full")]
111 HostBufferFull,
112 #[error("Failed to send output event: Host does not have output event buffer")]
113 NoOutputBuffer,
114 #[error(
115 "Failed to send output event: Invalid event type for output config {midi_output_config:?}"
116 )]
117 InvalidEvent { midi_output_config: MidiConfig },
118}
119
120/// Information about the plugin's transport. Depending on the plugin API and the host not all
121/// fields may be available.
122#[derive(Debug)]
123pub struct Transport {
124 /// Whether the transport is currently running.
125 pub playing: bool,
126 /// Whether recording is enabled in the project.
127 pub recording: bool,
128 /// Whether the pre-roll is currently active, if the plugin API reports this information.
129 pub preroll_active: Option<bool>,
130
131 /// The sample rate in Hertz. Also passed in
132 /// [`Plugin::activate()`][crate::plugin::Plugin::activate()], so if you need this then you
133 /// can also store that value.
134 pub sample_rate: f32,
135 /// The project's tempo in beats per minute.
136 pub tempo: Option<f64>,
137 /// The time signature's numerator.
138 pub time_sig_numerator: Option<i32>,
139 /// The time signature's denominator.
140 pub time_sig_denominator: Option<i32>,
141
142 // XXX: VST3 also has a continuous time in samples that ignores loops, but we can't reconstruct
143 // something similar in CLAP so it may be best to just ignore that so you can't rely on it
144 /// The position in the song in samples. Can be used to calculate the time in seconds if needed.
145 pub pos_samples: Option<i64>,
146 /// The position in the song in seconds. Can be used to calculate the time in samples if needed.
147 pub pos_seconds: Option<f64>,
148 /// The position in the song in quarter notes. Can be calculated from the time in seconds and
149 /// the tempo if needed.
150 pub pos_beats: Option<f64>,
151 /// The last bar's start position in beats. Can be calculated from the beat position and time
152 /// signature if needed.
153 pub bar_start_pos_beats: Option<f64>,
154 /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at
155 /// the start of the song. Can be calculated from the beat position and time signature if
156 /// needed.
157 pub bar_number: Option<i32>,
158
159 /// The loop range in samples, if the loop is active and this information is available. None of
160 /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the
161 /// end is exclusive. Can be calculated from the other loop range information if needed.
162 pub loop_range_samples: Option<(i64, i64)>,
163 /// The loop range in seconds, 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_seconds: Option<(f64, f64)>,
167 /// The loop range in quarter notes, if the loop is active and this information is available.
168 /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume
169 /// that the end is exclusive. Can be calculated from the other loop range information if
170 /// needed.
171 pub loop_range_beats: Option<(f64, f64)>,
172}
173
174impl Transport {
175 /// Initialize the transport struct without any information.
176 pub fn new(sample_rate: f32) -> Self {
177 Self {
178 playing: false,
179 recording: false,
180 preroll_active: None,
181
182 sample_rate,
183 tempo: None,
184 time_sig_numerator: None,
185 time_sig_denominator: None,
186
187 pos_samples: None,
188 pos_seconds: None,
189 pos_beats: None,
190 bar_start_pos_beats: None,
191 bar_number: None,
192
193 loop_range_samples: None,
194 loop_range_seconds: None,
195 loop_range_beats: None,
196 }
197 }
198
199 /// The position in the song in samples. Will be calculated from other information if needed.
200 pub fn pos_samples(&self) -> Option<i64> {
201 match (
202 self.pos_samples,
203 self.pos_seconds,
204 self.pos_beats,
205 self.tempo,
206 ) {
207 (Some(pos_samples), _, _, _) => Some(pos_samples),
208 (_, Some(pos_seconds), _, _) => {
209 Some((pos_seconds * self.sample_rate as f64).round() as i64)
210 }
211 (_, _, Some(pos_beats), Some(tempo)) => {
212 Some((pos_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64)
213 }
214 (_, _, _, _) => None,
215 }
216 }
217
218 /// The position in the song in seconds. Can be used to calculate the time in samples if needed.
219 pub fn pos_seconds(&self) -> Option<f64> {
220 match (
221 self.pos_samples,
222 self.pos_seconds,
223 self.pos_beats,
224 self.tempo,
225 ) {
226 (_, Some(pos_seconds), _, _) => Some(pos_seconds),
227 (Some(pos_samples), _, _, _) => Some(pos_samples as f64 / self.sample_rate as f64),
228 (_, _, Some(pos_beats), Some(tempo)) => Some(pos_beats / tempo * 60.0),
229 (_, _, _, _) => None,
230 }
231 }
232
233 /// The position in the song in quarter notes. Will be calculated from other information if
234 /// needed.
235 pub fn pos_beats(&self) -> Option<f64> {
236 match (
237 self.pos_samples,
238 self.pos_seconds,
239 self.pos_beats,
240 self.tempo,
241 ) {
242 (_, _, Some(pos_beats), _) => Some(pos_beats),
243 (_, Some(pos_seconds), _, Some(tempo)) => Some(pos_seconds / 60.0 * tempo),
244 (Some(pos_samples), _, _, Some(tempo)) => {
245 Some(pos_samples as f64 / self.sample_rate as f64 / 60.0 * tempo)
246 }
247 (_, _, _, _) => None,
248 }
249 }
250
251 /// The last bar's start position in beats. Will be calculated from other information if needed.
252 pub fn bar_start_pos_beats(&self) -> Option<f64> {
253 if self.bar_start_pos_beats.is_some() {
254 return self.bar_start_pos_beats;
255 }
256
257 match (
258 self.time_sig_numerator,
259 self.time_sig_denominator,
260 self.pos_beats(),
261 ) {
262 (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => {
263 let quarter_note_bar_length =
264 time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0;
265 Some((pos_beats / quarter_note_bar_length).floor() * quarter_note_bar_length)
266 }
267 (_, _, _) => None,
268 }
269 }
270
271 /// The number of the bar at `bar_start_pos_beats`. This starts at 0 for the very first bar at
272 /// the start of the song. Will be calculated from other information if needed.
273 pub fn bar_number(&self) -> Option<i32> {
274 if self.bar_number.is_some() {
275 return self.bar_number;
276 }
277
278 match (
279 self.time_sig_numerator,
280 self.time_sig_denominator,
281 self.pos_beats(),
282 ) {
283 (Some(time_sig_numerator), Some(time_sig_denominator), Some(pos_beats)) => {
284 let quarter_note_bar_length =
285 time_sig_numerator as f64 / time_sig_denominator as f64 * 4.0;
286 Some((pos_beats / quarter_note_bar_length).floor() as i32)
287 }
288 (_, _, _) => None,
289 }
290 }
291
292 /// The loop range in samples, if the loop is active and this information is available. None of
293 /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the
294 /// end is exclusive. Will be calculated from other information if needed.
295 pub fn loop_range_samples(&self) -> Option<(i64, i64)> {
296 match (
297 self.loop_range_samples,
298 self.loop_range_seconds,
299 self.loop_range_beats,
300 self.tempo,
301 ) {
302 (Some(loop_range_samples), _, _, _) => Some(loop_range_samples),
303 (_, Some((start_seconds, end_seconds)), _, _) => Some((
304 ((start_seconds * self.sample_rate as f64).round() as i64),
305 ((end_seconds * self.sample_rate as f64).round() as i64),
306 )),
307 (_, _, Some((start_beats, end_beats)), Some(tempo)) => Some((
308 (start_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64,
309 (end_beats / tempo * 60.0 * self.sample_rate as f64).round() as i64,
310 )),
311 (_, _, _, _) => None,
312 }
313 }
314
315 /// The loop range in seconds, if the loop is active and this information is available. None of
316 /// the plugin API docs mention whether this is exclusive or inclusive, but just assume that the
317 /// end is exclusive. Will be calculated from other information if needed.
318 pub fn loop_range_seconds(&self) -> Option<(f64, f64)> {
319 match (
320 self.loop_range_samples,
321 self.loop_range_seconds,
322 self.loop_range_beats,
323 self.tempo,
324 ) {
325 (_, Some(loop_range_seconds), _, _) => Some(loop_range_seconds),
326 (Some((start_samples, end_samples)), _, _, _) => Some((
327 start_samples as f64 / self.sample_rate as f64,
328 end_samples as f64 / self.sample_rate as f64,
329 )),
330 (_, _, Some((start_beats, end_beats)), Some(tempo)) => {
331 Some((start_beats / tempo * 60.0, end_beats / tempo * 60.0))
332 }
333 (_, _, _, _) => None,
334 }
335 }
336
337 /// The loop range in quarter notes, if the loop is active and this information is available.
338 /// None of the plugin API docs mention whether this is exclusive or inclusive, but just assume
339 /// that the end is exclusive. Will be calculated from other information if needed.
340 pub fn loop_range_beats(&self) -> Option<(f64, f64)> {
341 match (
342 self.loop_range_samples,
343 self.loop_range_seconds,
344 self.loop_range_beats,
345 self.tempo,
346 ) {
347 (_, _, Some(loop_range_beats), _) => Some(loop_range_beats),
348 (_, Some((start_seconds, end_seconds)), _, Some(tempo)) => {
349 Some((start_seconds / 60.0 * tempo, end_seconds / 60.0 * tempo))
350 }
351 (Some((start_samples, end_samples)), _, _, Some(tempo)) => Some((
352 start_samples as f64 / self.sample_rate as f64 / 60.0 * tempo,
353 end_samples as f64 / self.sample_rate as f64 / 60.0 * tempo,
354 )),
355 (_, _, _, _) => None,
356 }
357 }
358}