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