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