beamer_core/
bypass.rs

1//! Bypass handling with smooth crossfading.
2//!
3//! This module provides utilities for implementing soft bypass in audio plugins.
4//! Soft bypass crossfades between wet (processed) and dry (passthrough) signals
5//! to avoid clicks when bypass is toggled.
6//!
7//! # Overview
8//!
9//! - [`BypassState`] - Current bypass state (Active, Bypassed, or transitioning)
10//! - [`CrossfadeCurve`] - Crossfade curve shape (Linear, EqualPower, SCurve)
11//! - [`BypassHandler`] - Main utility for handling bypass with automatic crossfading
12//!
13//! # Example
14//!
15//! ```ignore
16//! use beamer_core::{BypassHandler, CrossfadeCurve, Buffer, AudioProcessor, AuxiliaryBuffers, ProcessContext};
17//!
18//! struct MyPlugin {
19//!     bypass_handler: BypassHandler,
20//!     // ...
21//! }
22//!
23//! impl AudioProcessor for MyPlugin {
24//!     fn setup(&mut self, sample_rate: f64, _max_buffer_size: usize) {
25//!         // 10ms crossfade with equal-power curve
26//!         let ramp_samples = (sample_rate * 0.01) as u32;
27//!         self.bypass_handler = BypassHandler::new(ramp_samples, CrossfadeCurve::EqualPower);
28//!     }
29//!
30//!     fn process(&mut self, buffer: &mut Buffer, aux: &mut AuxiliaryBuffers, ctx: &ProcessContext) {
31//!         let is_bypassed = self.params.bypass_value() > 0.5;
32//!
33//!         self.bypass_handler.process(buffer, is_bypassed, |buf| {
34//!             // Your DSP code here - only called when processing is needed
35//!             self.apply_reverb(buf, aux);
36//!         });
37//!     }
38//!
39//!     fn bypass_ramp_samples(&self) -> u32 {
40//!         self.bypass_handler.ramp_samples()
41//!     }
42//! }
43//! ```
44
45use crate::buffer::Buffer;
46use crate::sample::Sample;
47
48// =============================================================================
49// BypassState
50// =============================================================================
51
52/// Current state of the bypass handler.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum BypassState {
55    /// Plugin is processing normally (not bypassed).
56    Active,
57    /// Transitioning from active to bypassed (crossfading to dry).
58    RampingToBypassed,
59    /// Plugin is fully bypassed (passthrough only).
60    Bypassed,
61    /// Transitioning from bypassed to active (crossfading to wet).
62    RampingToActive,
63}
64
65// =============================================================================
66// CrossfadeCurve
67// =============================================================================
68
69/// Crossfade curve shape for bypass transitions.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum CrossfadeCurve {
72    /// Linear crossfade. Simple but may have slight loudness dip at center.
73    /// gain = position (0.0 to 1.0)
74    #[default]
75    Linear,
76
77    /// Equal-power crossfade. Maintains constant loudness during transition.
78    /// gain = sin(position * PI/2) for fade-in, cos(position * PI/2) for fade-out
79    EqualPower,
80
81    /// S-curve crossfade. Attempt at faster start/end, smoother middle.
82    /// gain = 3x^2 - 2x^3 (smoothstep)
83    SCurve,
84}
85
86impl CrossfadeCurve {
87    /// Calculate wet and dry gains for a given ramp position.
88    ///
89    /// # Arguments
90    /// * `t` - Normalized position (0.0 = fully wet, 1.0 = fully dry)
91    ///
92    /// # Returns
93    /// Tuple of (wet_gain, dry_gain) as the specified sample type
94    #[inline]
95    pub fn gains<S: Sample>(&self, t: f64) -> (S, S) {
96        let (wet, dry) = match self {
97            CrossfadeCurve::Linear => (1.0 - t, t),
98            CrossfadeCurve::EqualPower => {
99                let angle = t * std::f64::consts::FRAC_PI_2;
100                (angle.cos(), angle.sin())
101            }
102            CrossfadeCurve::SCurve => {
103                let smooth = t * t * (3.0 - 2.0 * t); // smoothstep
104                (1.0 - smooth, smooth)
105            }
106        };
107        (S::from_f64(wet), S::from_f64(dry))
108    }
109}
110
111// =============================================================================
112// BypassHandler
113// =============================================================================
114
115/// Utility for handling bypass with smooth crossfading.
116///
117/// Maintains bypass state and provides automatic crossfade between
118/// wet (processed) and dry (passthrough) signals when bypass is toggled.
119///
120/// # Sample Type Flexibility
121///
122/// BypassHandler is not generic over sample type - instead, the `process()`
123/// method is generic. This means a single BypassHandler instance can process
124/// both `Buffer<f32>` and `Buffer<f64>` buffers, and plugins don't need
125/// separate handlers for different precision modes.
126///
127/// # Real-Time Safety
128///
129/// This struct performs no heap allocations and is safe to use in
130/// audio processing callbacks.
131///
132/// # Example
133///
134/// ```ignore
135/// use beamer_core::{BypassHandler, CrossfadeCurve, Buffer};
136///
137/// struct MyPlugin {
138///     bypass_handler: BypassHandler,
139///     // ...
140/// }
141///
142/// impl AudioProcessor for MyPlugin {
143///     fn setup(&mut self, sample_rate: f64, _max_buffer_size: usize) {
144///         // 10ms crossfade with equal-power curve
145///         let ramp_samples = (sample_rate * 0.01) as u32;
146///         self.bypass_handler = BypassHandler::new(ramp_samples, CrossfadeCurve::EqualPower);
147///     }
148///
149///     fn process(&mut self, buffer: &mut Buffer, aux: &mut AuxiliaryBuffers, ctx: &ProcessContext) {
150///         let is_bypassed = self.params.bypass_value() > 0.5;
151///
152///         self.bypass_handler.process(buffer, is_bypassed, |buf| {
153///             // Your DSP code here - only called when processing is needed
154///             self.apply_reverb(buf, aux);
155///         });
156///     }
157///
158///     fn bypass_ramp_samples(&self) -> u32 {
159///         self.bypass_handler.ramp_samples()
160///     }
161/// }
162/// ```
163pub struct BypassHandler {
164    /// Current bypass state
165    state: BypassState,
166    /// Current position in ramp (0 = start, ramp_samples = end)
167    ramp_position: u32,
168    /// Total ramp length in samples
169    ramp_samples: u32,
170    /// Crossfade curve to use
171    curve: CrossfadeCurve,
172}
173
174impl BypassHandler {
175    /// Create a new bypass handler.
176    ///
177    /// # Arguments
178    /// * `ramp_samples` - Number of samples for crossfade (0 = instant bypass)
179    /// * `curve` - Crossfade curve shape
180    pub fn new(ramp_samples: u32, curve: CrossfadeCurve) -> Self {
181        Self {
182            state: BypassState::Active,
183            ramp_position: 0,
184            ramp_samples,
185            curve,
186        }
187    }
188
189    /// Get the current bypass state.
190    #[inline]
191    pub fn state(&self) -> BypassState {
192        self.state
193    }
194
195    /// Returns true if currently in a ramping (crossfading) state.
196    #[inline]
197    pub fn is_ramping(&self) -> bool {
198        matches!(
199            self.state,
200            BypassState::RampingToBypassed | BypassState::RampingToActive
201        )
202    }
203
204    /// Returns true if fully bypassed (not ramping).
205    #[inline]
206    pub fn is_bypassed(&self) -> bool {
207        self.state == BypassState::Bypassed
208    }
209
210    /// Returns true if fully active (not ramping, not bypassed).
211    #[inline]
212    pub fn is_active(&self) -> bool {
213        self.state == BypassState::Active
214    }
215
216    /// Get the configured ramp length in samples.
217    #[inline]
218    pub fn ramp_samples(&self) -> u32 {
219        self.ramp_samples
220    }
221
222    /// Set the ramp length. Takes effect on next state transition.
223    pub fn set_ramp_samples(&mut self, samples: u32) {
224        self.ramp_samples = samples;
225    }
226
227    /// Set the crossfade curve. Takes effect on next state transition.
228    pub fn set_curve(&mut self, curve: CrossfadeCurve) {
229        self.curve = curve;
230    }
231
232    /// Update bypass target state.
233    ///
234    /// Call this at the start of each process() with the current bypass parameter value.
235    /// State transitions happen automatically.
236    ///
237    /// When `ramp_samples == 0` (instant bypass), state snaps directly to
238    /// `Bypassed` or `Active` without passing through ramping states.
239    pub fn set_bypass(&mut self, bypassed: bool) {
240        // Handle instant bypass (zero ramp) - snap directly to final state
241        if self.ramp_samples == 0 {
242            let target = if bypassed {
243                BypassState::Bypassed
244            } else {
245                BypassState::Active
246            };
247            if self.state != target {
248                self.state = target;
249                self.ramp_position = 0;
250            }
251            return;
252        }
253
254        match (self.state, bypassed) {
255            // Start ramping to bypassed
256            (BypassState::Active, true) => {
257                self.state = BypassState::RampingToBypassed;
258                self.ramp_position = 0;
259            }
260            // Reverse: was ramping to bypass, now going back to active
261            (BypassState::RampingToBypassed, false) => {
262                self.state = BypassState::RampingToActive;
263                // Keep current ramp_position for smooth reversal
264            }
265            // Start ramping to active
266            (BypassState::Bypassed, false) => {
267                self.state = BypassState::RampingToActive;
268                self.ramp_position = self.ramp_samples;
269            }
270            // Reverse: was ramping to active, now going back to bypass
271            (BypassState::RampingToActive, true) => {
272                self.state = BypassState::RampingToBypassed;
273                // Keep current ramp_position for smooth reversal
274            }
275            // Already in correct stable state, or continuing ramp
276            _ => {}
277        }
278    }
279
280    /// Process audio with bypass handling.
281    ///
282    /// This is the main method plugins should call. It handles:
283    /// - Passthrough when fully bypassed
284    /// - Normal processing when fully active
285    /// - Crossfading during transitions
286    ///
287    /// # Type Parameter
288    ///
289    /// `S` is the sample type (`f32` or `f64`). The same BypassHandler instance
290    /// can process buffers of either precision.
291    ///
292    /// # Arguments
293    /// * `buffer` - Audio buffer to process
294    /// * `bypassed` - Current bypass parameter state (true = bypassed)
295    /// * `process_fn` - Closure that performs DSP (only called when needed)
296    ///
297    /// # Behavior
298    ///
299    /// | State | process_fn called? | Output |
300    /// |-------|-------------------|--------|
301    /// | Active | Yes | Wet signal |
302    /// | RampingToBypassed | Yes | Crossfade wet→dry |
303    /// | Bypassed | No | Dry passthrough |
304    /// | RampingToActive | Yes | Crossfade dry→wet |
305    pub fn process<S: Sample, F>(&mut self, buffer: &mut Buffer<S>, bypassed: bool, process_fn: F)
306    where
307        F: FnOnce(&mut Buffer<S>),
308    {
309        // Update target state
310        self.set_bypass(bypassed);
311
312        let num_samples = buffer.num_samples();
313
314        match self.state {
315            BypassState::Bypassed => {
316                // Fully bypassed: just copy input to output
317                buffer.copy_to_output();
318            }
319
320            BypassState::Active => {
321                // Fully active: just run DSP
322                process_fn(buffer);
323            }
324
325            BypassState::RampingToBypassed | BypassState::RampingToActive => {
326                // Need to crossfade - run DSP first to get wet signal
327                process_fn(buffer);
328
329                // Apply per-sample crossfade
330                self.apply_crossfade(buffer, num_samples);
331            }
332        }
333    }
334
335    fn apply_crossfade<S: Sample>(&mut self, buffer: &mut Buffer<S>, num_samples: usize) {
336        // Guard: instant bypass when ramp_samples is 0
337        if self.ramp_samples == 0 {
338            return;
339        }
340
341        // Guard: no channels to process
342        let num_channels = buffer.num_input_channels().min(buffer.num_output_channels());
343        if num_channels == 0 {
344            return;
345        }
346
347        let ramp_samples_f = self.ramp_samples as f64;
348        let ramping_to_bypass = self.state == BypassState::RampingToBypassed;
349
350        // Process sample by sample
351        for sample_idx in 0..num_samples {
352            // Calculate normalized position (0.0 = wet, 1.0 = dry)
353            let t = (self.ramp_position as f64) / ramp_samples_f;
354            let (wet_gain, dry_gain): (S, S) = self.curve.gains(t);
355
356            // Apply crossfade to all channels for this sample
357            for ch in 0..num_channels {
358                let dry = buffer.input(ch)[sample_idx];
359                let wet = buffer.output(ch)[sample_idx];
360                buffer.output(ch)[sample_idx] = wet * wet_gain + dry * dry_gain;
361            }
362
363            // Advance ramp position (once per sample)
364            if ramping_to_bypass {
365                self.ramp_position = (self.ramp_position + 1).min(self.ramp_samples);
366            } else {
367                self.ramp_position = self.ramp_position.saturating_sub(1);
368            }
369        }
370
371        // Check if ramp complete
372        if ramping_to_bypass && self.ramp_position >= self.ramp_samples {
373            self.state = BypassState::Bypassed;
374        } else if !ramping_to_bypass && self.ramp_position == 0 {
375            self.state = BypassState::Active;
376        }
377    }
378}
379
380impl Default for BypassHandler {
381    /// Create a bypass handler with default settings (64 samples, linear curve).
382    fn default() -> Self {
383        Self::new(64, CrossfadeCurve::Linear)
384    }
385}