Skip to main content

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}