beamer_core/process_context.rs
1//! Transport and process context for audio plugins.
2//!
3//! This module provides [`Transport`] for DAW timing/playback state and
4//! [`ProcessContext`] which bundles transport with sample rate and buffer size.
5//!
6//! # Example: Tempo-Synced Effect
7//!
8//! ```ignore
9//! fn process(&mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, context: &ProcessContext) {
10//! // Calculate LFO rate synced to tempo
11//! let lfo_hz = if let Some(tempo) = context.transport.tempo {
12//! tempo / 60.0 / 4.0 // 1 cycle per 4 beats
13//! } else {
14//! 2.0 // Fallback to 2 Hz
15//! };
16//!
17//! let samples_per_cycle = context.sample_rate / lfo_hz;
18//! // ...
19//! }
20//! ```
21//!
22//! # Example: Accessing MIDI CC Values
23//!
24//! ```ignore
25//! fn process(&mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, context: &ProcessContext) {
26//! if let Some(cc) = context.midi_cc() {
27//! let pitch_bend = cc.pitch_bend(); // -1.0 to 1.0
28//! let mod_wheel = cc.mod_wheel(); // 0.0 to 1.0
29//! let volume = cc.cc(7); // 0.0 to 1.0
30//! }
31//! }
32//! ```
33
34use crate::midi_cc_state::MidiCcState;
35
36// =============================================================================
37// FrameRate Enum
38// =============================================================================
39
40/// SMPTE frame rate for video synchronization.
41///
42/// Used with [`Transport::frame_rate`] for film/video sync applications.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44#[repr(u32)]
45pub enum FrameRate {
46 /// 24 fps (film)
47 #[default]
48 Fps24 = 0,
49 /// 25 fps (PAL video)
50 Fps25 = 1,
51 /// 29.97 fps non-drop (NTSC video)
52 Fps2997 = 2,
53 /// 30 fps
54 Fps30 = 3,
55 /// 29.97 fps drop-frame (NTSC broadcast)
56 Fps2997Drop = 4,
57 /// 30 fps drop-frame
58 Fps30Drop = 5,
59 /// 50 fps
60 Fps50 = 10,
61 /// 59.94 fps
62 Fps5994 = 11,
63 /// 60 fps
64 Fps60 = 12,
65 /// 59.94 fps drop-frame
66 Fps5994Drop = 13,
67 /// 60 fps drop-frame
68 Fps60Drop = 14,
69}
70
71impl FrameRate {
72 /// Returns the frames per second as an f64.
73 ///
74 /// Drop-frame rates return their actual (non-integer) values.
75 #[inline]
76 pub fn fps(&self) -> f64 {
77 match self {
78 Self::Fps24 => 24.0,
79 Self::Fps25 => 25.0,
80 Self::Fps2997 | Self::Fps2997Drop => 30000.0 / 1001.0, // 29.97...
81 Self::Fps30 | Self::Fps30Drop => 30.0,
82 Self::Fps50 => 50.0,
83 Self::Fps5994 | Self::Fps5994Drop => 60000.0 / 1001.0, // 59.94...
84 Self::Fps60 | Self::Fps60Drop => 60.0,
85 }
86 }
87
88 /// Returns true if this is a drop-frame format.
89 #[inline]
90 pub fn is_drop_frame(&self) -> bool {
91 matches!(
92 self,
93 Self::Fps2997Drop | Self::Fps30Drop | Self::Fps5994Drop | Self::Fps60Drop
94 )
95 }
96
97 /// Creates a FrameRate from raw frames-per-second and drop-frame flag.
98 ///
99 /// This is the canonical conversion from VST3's FrameRate struct.
100 /// Returns `None` for unsupported frame rates.
101 ///
102 /// # Arguments
103 /// * `fps` - Frames per second (24, 25, 29, 30, 50, 59, 60)
104 /// * `is_drop` - True if drop-frame timecode (only affects 29.97, 30, 59.94, 60)
105 #[inline]
106 pub fn from_raw(fps: u32, is_drop: bool) -> Option<Self> {
107 match fps {
108 24 => Some(Self::Fps24),
109 25 => Some(Self::Fps25),
110 29 if is_drop => Some(Self::Fps2997Drop),
111 29 => Some(Self::Fps2997),
112 30 if is_drop => Some(Self::Fps30Drop),
113 30 => Some(Self::Fps30),
114 50 => Some(Self::Fps50),
115 59 if is_drop => Some(Self::Fps5994Drop),
116 59 => Some(Self::Fps5994),
117 60 if is_drop => Some(Self::Fps60Drop),
118 60 => Some(Self::Fps60),
119 _ => None,
120 }
121 }
122}
123
124// =============================================================================
125// Transport Struct
126// =============================================================================
127
128/// Host transport and timing information.
129///
130/// Contains tempo, time signature, playback position, and transport state.
131/// All timing fields are `Option<T>` because not all hosts provide all data.
132/// Playback state fields (`is_playing`, etc.) are always valid.
133///
134/// # Field Availability
135///
136/// Different DAWs provide different subsets of transport information:
137/// - **Tempo/time signature**: Most DAWs provide these
138/// - **Musical position**: Common but not universal
139/// - **SMPTE/timecode**: Only in video-oriented DAWs
140/// - **System time**: Rarely provided
141///
142/// Always check `Option` fields before use and provide sensible fallbacks.
143///
144/// # Example
145///
146/// ```ignore
147/// // Safe tempo access with fallback
148/// let tempo = context.transport.tempo.unwrap_or(120.0);
149///
150/// // Check if we have valid musical position
151/// if let Some(beats) = context.transport.project_time_beats {
152/// // Sync effect to beat position
153/// }
154///
155/// // Transport state is always valid
156/// if context.transport.is_playing {
157/// // Process audio
158/// } else {
159/// // Maybe bypass or fade out
160/// }
161/// ```
162#[derive(Debug, Clone, Copy, Default)]
163pub struct Transport {
164 // =========================================================================
165 // Tempo and Time Signature
166 // =========================================================================
167 /// Current tempo in BPM (beats per minute).
168 ///
169 /// Typically 20-300, but can be any positive value.
170 pub tempo: Option<f64>,
171
172 /// Time signature numerator (e.g., 4 in 4/4, 3 in 3/4, 6 in 6/8).
173 pub time_sig_numerator: Option<i32>,
174
175 /// Time signature denominator (e.g., 4 in 4/4, 4 in 3/4, 8 in 6/8).
176 pub time_sig_denominator: Option<i32>,
177
178 // =========================================================================
179 // Position
180 // =========================================================================
181 /// Project time in samples from the start of the timeline.
182 ///
183 /// This is the primary sample-accurate position. Always increments
184 /// during playback, may jump on loop or locate.
185 pub project_time_samples: Option<i64>,
186
187 /// Project time in quarter notes (musical time).
188 ///
189 /// Takes tempo changes into account. 1.0 = one quarter note.
190 pub project_time_beats: Option<f64>,
191
192 /// Position of the last bar start in quarter notes.
193 ///
194 /// Useful for bar-synchronized effects (e.g., 4-bar delay).
195 pub bar_position_beats: Option<f64>,
196
197 // =========================================================================
198 // Loop/Cycle
199 // =========================================================================
200 /// Loop/cycle start position in quarter notes.
201 pub cycle_start_beats: Option<f64>,
202
203 /// Loop/cycle end position in quarter notes.
204 pub cycle_end_beats: Option<f64>,
205
206 // =========================================================================
207 // Transport State (always valid)
208 // =========================================================================
209 /// True if transport is currently playing.
210 ///
211 /// This is always valid (not an Option) because VST3 always provides it.
212 pub is_playing: bool,
213
214 /// True if recording is active.
215 pub is_recording: bool,
216
217 /// True if loop/cycle mode is enabled.
218 pub is_cycle_active: bool,
219
220 // =========================================================================
221 // Advanced Timing
222 // =========================================================================
223 /// System time in nanoseconds.
224 ///
225 /// Can be used to sync to wall-clock time. Rarely provided by hosts.
226 pub system_time_ns: Option<i64>,
227
228 /// Continuous time in samples (doesn't reset on loop).
229 ///
230 /// Unlike `project_time_samples`, this never jumps during cycle playback -
231 /// it always increments monotonically.
232 pub continuous_time_samples: Option<i64>,
233
234 /// Samples until next MIDI beat clock (24 ppqn).
235 ///
236 /// Used for generating MIDI clock messages or syncing to external gear.
237 pub samples_to_next_clock: Option<i32>,
238
239 // =========================================================================
240 // SMPTE/Timecode
241 // =========================================================================
242 /// SMPTE offset in subframes (1/80th of a frame).
243 ///
244 /// For video synchronization. Divide by 80 to get frame offset.
245 pub smpte_offset_subframes: Option<i32>,
246
247 /// SMPTE frame rate.
248 pub frame_rate: Option<FrameRate>,
249}
250
251impl Transport {
252 /// Returns the time signature as a tuple (numerator, denominator).
253 ///
254 /// Returns `None` if either component is unavailable.
255 ///
256 /// # Example
257 ///
258 /// ```ignore
259 /// if let Some((num, denom)) = transport.time_signature() {
260 /// println!("Playing in {}/{} time", num, denom);
261 /// }
262 /// ```
263 #[inline]
264 pub fn time_signature(&self) -> Option<(i32, i32)> {
265 match (self.time_sig_numerator, self.time_sig_denominator) {
266 (Some(num), Some(denom)) => Some((num, denom)),
267 _ => None,
268 }
269 }
270
271 /// Returns the loop/cycle range in quarter notes as (start, end).
272 ///
273 /// Returns `None` if either endpoint is unavailable.
274 ///
275 /// # Example
276 ///
277 /// ```ignore
278 /// if let Some((start, end)) = transport.cycle_range() {
279 /// let loop_length_beats = end - start;
280 /// }
281 /// ```
282 #[inline]
283 pub fn cycle_range(&self) -> Option<(f64, f64)> {
284 match (self.cycle_start_beats, self.cycle_end_beats) {
285 (Some(start), Some(end)) => Some((start, end)),
286 _ => None,
287 }
288 }
289
290 /// Returns true if loop is active and has valid range.
291 #[inline]
292 pub fn is_looping(&self) -> bool {
293 self.is_cycle_active && self.cycle_range().is_some()
294 }
295
296 /// Returns true if any timing info is available.
297 #[inline]
298 pub fn has_timing_info(&self) -> bool {
299 self.tempo.is_some()
300 || self.project_time_samples.is_some()
301 || self.project_time_beats.is_some()
302 }
303
304 /// Returns true if time signature info is complete.
305 #[inline]
306 pub fn has_time_signature(&self) -> bool {
307 self.time_sig_numerator.is_some() && self.time_sig_denominator.is_some()
308 }
309
310 /// Converts SMPTE subframes to (frames, subframes) tuple.
311 ///
312 /// Subframes are 0-79 within each frame.
313 /// Uses Euclidean division to correctly handle negative offsets.
314 #[inline]
315 pub fn smpte_frames(&self) -> Option<(i32, i32)> {
316 self.smpte_offset_subframes
317 .map(|sf| (sf.div_euclid(80), sf.rem_euclid(80)))
318 }
319}
320
321// =============================================================================
322// ProcessContext Struct
323// =============================================================================
324
325/// Complete processing context for a single `process()` call.
326///
327/// Contains sample rate, buffer size, transport/timing information, and
328/// optional MIDI CC state for direct access to controller values.
329/// Passed as the third parameter to [`Processor::process()`].
330///
331/// # Lifetime
332///
333/// ProcessContext is valid only within a single `process()` call.
334/// Do not store references to it across calls.
335///
336/// # Example
337///
338/// ```ignore
339/// impl Processor for MyDelayPlugin {
340/// fn process(&mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, context: &ProcessContext) {
341/// // Calculate tempo-synced delay time
342/// let delay_samples = if let Some(tempo) = context.transport.tempo {
343/// // Quarter note delay
344/// let quarter_note_sec = 60.0 / tempo;
345/// (quarter_note_sec * context.sample_rate) as usize
346/// } else {
347/// // Fallback: 500ms
348/// (0.5 * context.sample_rate) as usize
349/// };
350///
351/// // Access MIDI CC values directly
352/// if let Some(cc) = context.midi_cc() {
353/// let mod_depth = cc.mod_wheel();
354/// }
355///
356/// // Use context.num_samples for buffer size
357/// for i in 0..context.num_samples {
358/// // Process...
359/// }
360/// }
361/// }
362/// ```
363#[derive(Debug, Clone)]
364pub struct ProcessContext<'a> {
365 /// Current sample rate in Hz.
366 ///
367 /// Same value passed to [`Processor::setup()`], provided here
368 /// for convenience during processing.
369 pub sample_rate: f64,
370
371 /// Number of samples in this processing block.
372 ///
373 /// Same as [`Buffer::num_samples()`], provided here for convenience.
374 pub num_samples: usize,
375
376 /// Host transport and timing information.
377 pub transport: Transport,
378
379 /// MIDI CC state for direct access to controller values.
380 ///
381 /// Only present if the plugin returned `Some(MidiCcConfig)` from
382 /// `midi_cc_config()`. Use [`ProcessContext::midi_cc()`] to access.
383 midi_cc_state: Option<&'a MidiCcState>,
384}
385
386impl<'a> ProcessContext<'a> {
387 /// Creates a new ProcessContext.
388 ///
389 /// This is called by the VST3 wrapper, not by plugin code.
390 #[inline]
391 pub fn new(sample_rate: f64, num_samples: usize, transport: Transport) -> Self {
392 Self {
393 sample_rate,
394 num_samples,
395 transport,
396 midi_cc_state: None,
397 }
398 }
399
400 /// Creates a new ProcessContext with MIDI CC state.
401 ///
402 /// This is called by the VST3 wrapper when the plugin has MIDI CC config.
403 #[inline]
404 pub fn with_midi_cc(
405 sample_rate: f64,
406 num_samples: usize,
407 transport: Transport,
408 midi_cc_state: &'a MidiCcState,
409 ) -> Self {
410 Self {
411 sample_rate,
412 num_samples,
413 transport,
414 midi_cc_state: Some(midi_cc_state),
415 }
416 }
417
418 /// Creates a context with default (empty) transport.
419 ///
420 /// Used when the host doesn't provide ProcessContext.
421 #[inline]
422 pub fn with_empty_transport(sample_rate: f64, num_samples: usize) -> Self {
423 Self {
424 sample_rate,
425 num_samples,
426 transport: Transport::default(),
427 midi_cc_state: None,
428 }
429 }
430
431 /// Returns MIDI CC state for direct access to controller values.
432 ///
433 /// Only returns `Some` if the plugin returned `Some(MidiCcConfig)` from
434 /// `midi_cc_config()`.
435 ///
436 /// # Example
437 ///
438 /// ```ignore
439 /// fn process(&mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, context: &ProcessContext) {
440 /// if let Some(cc) = context.midi_cc() {
441 /// let pitch_bend = cc.pitch_bend(); // -1.0 to 1.0
442 /// let mod_wheel = cc.mod_wheel(); // 0.0 to 1.0
443 /// let volume = cc.cc(7); // 0.0 to 1.0
444 /// }
445 /// }
446 /// ```
447 #[inline]
448 pub fn midi_cc(&self) -> Option<&MidiCcState> {
449 self.midi_cc_state
450 }
451
452 /// Calculates the duration of this buffer in seconds.
453 #[inline]
454 pub fn buffer_duration(&self) -> f64 {
455 self.num_samples as f64 / self.sample_rate
456 }
457
458 /// Calculates samples per beat at the current tempo.
459 ///
460 /// Returns `None` if tempo is unavailable.
461 ///
462 /// # Example
463 ///
464 /// ```ignore
465 /// if let Some(spb) = context.samples_per_beat() {
466 /// let delay_samples = spb * 0.25; // 16th note delay
467 /// }
468 /// ```
469 #[inline]
470 pub fn samples_per_beat(&self) -> Option<f64> {
471 self.transport
472 .tempo
473 .map(|tempo| self.sample_rate * 60.0 / tempo)
474 }
475}
476
477impl Default for ProcessContext<'_> {
478 fn default() -> Self {
479 Self {
480 sample_rate: 44100.0,
481 num_samples: 0,
482 transport: Transport::default(),
483 midi_cc_state: None,
484 }
485 }
486}