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}