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