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}