bbx_dsp/blocks/effectors/
gain.rs

1//! Gain control block with dB input.
2
3#[cfg(feature = "simd")]
4use bbx_core::simd::apply_gain;
5
6use crate::{
7    block::{Block, DEFAULT_EFFECTOR_INPUT_COUNT, DEFAULT_EFFECTOR_OUTPUT_COUNT},
8    context::DspContext,
9    parameter::{ModulationOutput, Parameter},
10    sample::Sample,
11    smoothing::LinearSmoothedValue,
12};
13
14/// Maximum buffer size for stack-allocated smoothing cache.
15const MAX_BUFFER_SIZE: usize = 4096;
16
17/// A gain control block that applies amplitude scaling.
18///
19/// Level is specified in decibels (dB).
20pub struct GainBlock<S: Sample> {
21    /// Gain level in dB (-80 to +30).
22    pub level_db: Parameter<S>,
23
24    /// Base gain multiplier (linear) applied statically to the signal.
25    pub base_gain: S,
26
27    /// Smoothed linear gain value for click-free parameter changes.
28    gain_smoother: LinearSmoothedValue<S>,
29}
30
31impl<S: Sample> GainBlock<S> {
32    /// Minimum gain in dB (silence threshold).
33    const MIN_DB: f64 = -80.0;
34    /// Maximum gain in dB.
35    const MAX_DB: f64 = 30.0;
36
37    /// Create a new `GainBlock` with the given level in dB and an optional base gain multiplier.
38    pub fn new(level_db: f64, base_gain: Option<f64>) -> Self {
39        let clamped_db = level_db.clamp(Self::MIN_DB, Self::MAX_DB);
40        let initial_gain = Self::db_to_linear(clamped_db);
41
42        Self {
43            level_db: Parameter::Constant(S::from_f64(level_db)),
44            base_gain: S::from_f64(base_gain.unwrap_or(1.0)),
45            gain_smoother: LinearSmoothedValue::new(S::from_f64(initial_gain)),
46        }
47    }
48
49    /// Create a unity gain (0 dB) block.
50    pub fn unity() -> Self {
51        Self::new(0.0, None)
52    }
53
54    /// Convert dB to linear gain with range clamping.
55    #[inline]
56    fn db_to_linear(db: f64) -> f64 {
57        let clamped = db.clamp(Self::MIN_DB, Self::MAX_DB);
58        10.0_f64.powf(clamped / 20.0)
59    }
60}
61
62impl<S: Sample> Block<S> for GainBlock<S> {
63    fn process(&mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], modulation_values: &[S], context: &DspContext) {
64        let level_db = self.level_db.get_value(modulation_values).to_f64();
65        let target_gain = S::from_f64(Self::db_to_linear(level_db));
66
67        let current_target = self.gain_smoother.target();
68        if (target_gain - current_target).abs() > S::EPSILON {
69            self.gain_smoother.set_target_value(target_gain);
70        }
71
72        let num_channels = inputs.len().min(outputs.len());
73
74        if !self.gain_smoother.is_smoothing() {
75            let gain = self.gain_smoother.current() * self.base_gain;
76
77            #[cfg(feature = "simd")]
78            {
79                for ch in 0..num_channels {
80                    let len = inputs[ch].len().min(outputs[ch].len());
81                    apply_gain(&inputs[ch][..len], &mut outputs[ch][..len], gain);
82                }
83                return;
84            }
85
86            #[cfg(not(feature = "simd"))]
87            {
88                for ch in 0..num_channels {
89                    let len = inputs[ch].len().min(outputs[ch].len());
90                    for i in 0..len {
91                        outputs[ch][i] = inputs[ch][i] * gain;
92                    }
93                }
94                return;
95            }
96        }
97
98        let len = inputs.first().map_or(0, |ch| ch.len().min(context.buffer_size));
99        debug_assert!(len <= MAX_BUFFER_SIZE, "buffer_size exceeds MAX_BUFFER_SIZE");
100
101        let mut gain_values: [S; MAX_BUFFER_SIZE] = [S::ZERO; MAX_BUFFER_SIZE];
102        for gain_value in gain_values.iter_mut().take(len) {
103            *gain_value = self.gain_smoother.get_next_value() * self.base_gain;
104        }
105
106        for ch in 0..num_channels {
107            let ch_len = inputs[ch].len().min(outputs[ch].len()).min(len);
108            for (i, &gain) in gain_values.iter().enumerate().take(ch_len) {
109                outputs[ch][i] = inputs[ch][i] * gain;
110            }
111        }
112    }
113
114    #[inline]
115    fn input_count(&self) -> usize {
116        DEFAULT_EFFECTOR_INPUT_COUNT
117    }
118
119    #[inline]
120    fn output_count(&self) -> usize {
121        DEFAULT_EFFECTOR_OUTPUT_COUNT
122    }
123
124    #[inline]
125    fn modulation_outputs(&self) -> &[ModulationOutput] {
126        &[]
127    }
128
129    fn set_smoothing(&mut self, sample_rate: f64, ramp_time_ms: f64) {
130        self.gain_smoother.reset(sample_rate, ramp_time_ms);
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::channel::ChannelLayout;
138
139    fn test_context(buffer_size: usize) -> DspContext {
140        DspContext {
141            sample_rate: 44100.0,
142            num_channels: 2,
143            buffer_size,
144            current_sample: 0,
145            channel_layout: ChannelLayout::Stereo,
146        }
147    }
148
149    #[test]
150    fn test_gain_input_output_counts_f32() {
151        let gain = GainBlock::<f32>::new(0.0, None);
152        assert_eq!(gain.input_count(), DEFAULT_EFFECTOR_INPUT_COUNT);
153        assert_eq!(gain.output_count(), DEFAULT_EFFECTOR_OUTPUT_COUNT);
154    }
155
156    #[test]
157    fn test_gain_input_output_counts_f64() {
158        let gain = GainBlock::<f64>::new(0.0, None);
159        assert_eq!(gain.input_count(), DEFAULT_EFFECTOR_INPUT_COUNT);
160        assert_eq!(gain.output_count(), DEFAULT_EFFECTOR_OUTPUT_COUNT);
161    }
162
163    #[test]
164    fn test_unity_gain_passthrough_f32() {
165        let mut gain = GainBlock::<f32>::unity();
166        let context = test_context(4);
167
168        let input = [0.5f32, -0.5, 0.25, -0.25];
169        let mut output = [0.0f32; 4];
170
171        let inputs: [&[f32]; 1] = [&input];
172        let mut outputs: [&mut [f32]; 1] = [&mut output];
173
174        for _ in 0..10 {
175            gain.process(&inputs, &mut outputs, &[], &context);
176        }
177
178        for (i, (&inp, &out)) in input.iter().zip(output.iter()).enumerate() {
179            assert!(
180                (inp - out).abs() < 1e-5,
181                "Unity gain should passthrough: input[{}]={}, output[{}]={}",
182                i,
183                inp,
184                i,
185                out
186            );
187        }
188    }
189
190    #[test]
191    fn test_unity_gain_passthrough_f64() {
192        let mut gain = GainBlock::<f64>::unity();
193        let context = test_context(4);
194
195        let input = [0.5f64, -0.5, 0.25, -0.25];
196        let mut output = [0.0f64; 4];
197
198        let inputs: [&[f64]; 1] = [&input];
199        let mut outputs: [&mut [f64]; 1] = [&mut output];
200
201        for _ in 0..10 {
202            gain.process(&inputs, &mut outputs, &[], &context);
203        }
204
205        for (i, (&inp, &out)) in input.iter().zip(output.iter()).enumerate() {
206            assert!(
207                (inp - out).abs() < 1e-10,
208                "Unity gain should passthrough: input[{}]={}, output[{}]={}",
209                i,
210                inp,
211                i,
212                out
213            );
214        }
215    }
216
217    #[test]
218    fn test_silence_at_min_db_f32() {
219        let mut gain = GainBlock::<f32>::new(-80.0, None);
220        let context = test_context(4);
221
222        let input = [1.0f32; 4];
223        let mut output = [1.0f32; 4];
224
225        let inputs: [&[f32]; 1] = [&input];
226        let mut outputs: [&mut [f32]; 1] = [&mut output];
227
228        for _ in 0..10 {
229            gain.process(&inputs, &mut outputs, &[], &context);
230        }
231
232        for (i, &out) in output.iter().enumerate() {
233            assert!(
234                out.abs() < 0.001,
235                "Output should be nearly silent at -80dB: output[{}]={}",
236                i,
237                out
238            );
239        }
240    }
241
242    #[test]
243    fn test_silence_at_min_db_f64() {
244        let mut gain = GainBlock::<f64>::new(-80.0, None);
245        let context = test_context(4);
246
247        let input = [1.0f64; 4];
248        let mut output = [1.0f64; 4];
249
250        let inputs: [&[f64]; 1] = [&input];
251        let mut outputs: [&mut [f64]; 1] = [&mut output];
252
253        for _ in 0..10 {
254            gain.process(&inputs, &mut outputs, &[], &context);
255        }
256
257        for (i, &out) in output.iter().enumerate() {
258            assert!(
259                out.abs() < 0.001,
260                "Output should be nearly silent at -80dB: output[{}]={}",
261                i,
262                out
263            );
264        }
265    }
266
267    #[test]
268    fn test_amplification_at_positive_db_f32() {
269        let mut gain = GainBlock::<f32>::new(6.0, None);
270        let context = test_context(4);
271
272        let input = [0.5f32; 4];
273        let mut output = [0.0f32; 4];
274
275        let inputs: [&[f32]; 1] = [&input];
276        let mut outputs: [&mut [f32]; 1] = [&mut output];
277
278        for _ in 0..10 {
279            gain.process(&inputs, &mut outputs, &[], &context);
280        }
281
282        let expected_linear = 10.0_f32.powf(6.0 / 20.0);
283        for (i, &out) in output.iter().enumerate() {
284            let expected = 0.5 * expected_linear;
285            assert!(
286                (out - expected).abs() < 0.05,
287                "Output should be amplified at +6dB: expected={}, output[{}]={}",
288                expected,
289                i,
290                out
291            );
292        }
293    }
294
295    #[test]
296    fn test_amplification_at_positive_db_f64() {
297        let mut gain = GainBlock::<f64>::new(6.0, None);
298        let context = test_context(4);
299
300        let input = [0.5f64; 4];
301        let mut output = [0.0f64; 4];
302
303        let inputs: [&[f64]; 1] = [&input];
304        let mut outputs: [&mut [f64]; 1] = [&mut output];
305
306        for _ in 0..10 {
307            gain.process(&inputs, &mut outputs, &[], &context);
308        }
309
310        let expected_linear = 10.0_f64.powf(6.0 / 20.0);
311        for (i, &out) in output.iter().enumerate() {
312            let expected = 0.5 * expected_linear;
313            assert!(
314                (out - expected).abs() < 0.05,
315                "Output should be amplified at +6dB: expected={}, output[{}]={}",
316                expected,
317                i,
318                out
319            );
320        }
321    }
322
323    #[test]
324    fn test_attenuation_at_negative_db_f32() {
325        let mut gain = GainBlock::<f32>::new(-6.0, None);
326        let context = test_context(4);
327
328        let input = [1.0f32; 4];
329        let mut output = [0.0f32; 4];
330
331        let inputs: [&[f32]; 1] = [&input];
332        let mut outputs: [&mut [f32]; 1] = [&mut output];
333
334        for _ in 0..10 {
335            gain.process(&inputs, &mut outputs, &[], &context);
336        }
337
338        let expected_linear = 10.0_f32.powf(-6.0 / 20.0);
339        for (i, &out) in output.iter().enumerate() {
340            assert!(
341                (out - expected_linear).abs() < 0.05,
342                "Output should be attenuated at -6dB: expected={}, output[{}]={}",
343                expected_linear,
344                i,
345                out
346            );
347        }
348    }
349
350    #[test]
351    fn test_attenuation_at_negative_db_f64() {
352        let mut gain = GainBlock::<f64>::new(-6.0, None);
353        let context = test_context(4);
354
355        let input = [1.0f64; 4];
356        let mut output = [0.0f64; 4];
357
358        let inputs: [&[f64]; 1] = [&input];
359        let mut outputs: [&mut [f64]; 1] = [&mut output];
360
361        for _ in 0..10 {
362            gain.process(&inputs, &mut outputs, &[], &context);
363        }
364
365        let expected_linear = 10.0_f64.powf(-6.0 / 20.0);
366        for (i, &out) in output.iter().enumerate() {
367            assert!(
368                (out - expected_linear).abs() < 0.05,
369                "Output should be attenuated at -6dB: expected={}, output[{}]={}",
370                expected_linear,
371                i,
372                out
373            );
374        }
375    }
376
377    #[test]
378    fn test_base_gain_multiplier_f32() {
379        let mut gain = GainBlock::<f32>::new(0.0, Some(0.5));
380        let context = test_context(4);
381
382        let input = [1.0f32; 4];
383        let mut output = [0.0f32; 4];
384
385        let inputs: [&[f32]; 1] = [&input];
386        let mut outputs: [&mut [f32]; 1] = [&mut output];
387
388        for _ in 0..10 {
389            gain.process(&inputs, &mut outputs, &[], &context);
390        }
391
392        for (i, &out) in output.iter().enumerate() {
393            assert!(
394                (out - 0.5).abs() < 0.05,
395                "Base gain of 0.5 should halve output: output[{}]={}",
396                i,
397                out
398            );
399        }
400    }
401
402    #[test]
403    fn test_multichannel_processing_f32() {
404        let mut gain = GainBlock::<f32>::new(0.0, None);
405        let context = test_context(4);
406
407        let input_l = [1.0f32, 0.5, 0.25, 0.125];
408        let input_r = [0.8f32, 0.4, 0.2, 0.1];
409        let mut output_l = [0.0f32; 4];
410        let mut output_r = [0.0f32; 4];
411
412        let inputs: [&[f32]; 2] = [&input_l, &input_r];
413        let mut outputs: [&mut [f32]; 2] = [&mut output_l, &mut output_r];
414
415        for _ in 0..10 {
416            gain.process(&inputs, &mut outputs, &[], &context);
417        }
418
419        for (i, (&inp, &out)) in input_l.iter().zip(output_l.iter()).enumerate() {
420            assert!(
421                (inp - out).abs() < 0.05,
422                "Left channel passthrough: input[{}]={}, output[{}]={}",
423                i,
424                inp,
425                i,
426                out
427            );
428        }
429        for (i, (&inp, &out)) in input_r.iter().zip(output_r.iter()).enumerate() {
430            assert!(
431                (inp - out).abs() < 0.05,
432                "Right channel passthrough: input[{}]={}, output[{}]={}",
433                i,
434                inp,
435                i,
436                out
437            );
438        }
439    }
440
441    #[test]
442    fn test_db_clamping_below_min_f32() {
443        let mut gain = GainBlock::<f32>::new(-100.0, None);
444        let context = test_context(4);
445
446        let input = [1.0f32; 4];
447        let mut output = [0.0f32; 4];
448
449        let inputs: [&[f32]; 1] = [&input];
450        let mut outputs: [&mut [f32]; 1] = [&mut output];
451
452        for _ in 0..10 {
453            gain.process(&inputs, &mut outputs, &[], &context);
454        }
455
456        let expected = 10.0_f32.powf(-80.0 / 20.0);
457        for &out in &output {
458            assert!(out.abs() < expected * 2.0, "Should clamp to -80dB minimum");
459        }
460    }
461
462    #[test]
463    fn test_db_clamping_above_max_f32() {
464        let mut gain = GainBlock::<f32>::new(50.0, None);
465        let context = test_context(4);
466
467        let input = [0.1f32; 4];
468        let mut output = [0.0f32; 4];
469
470        let inputs: [&[f32]; 1] = [&input];
471        let mut outputs: [&mut [f32]; 1] = [&mut output];
472
473        for _ in 0..10 {
474            gain.process(&inputs, &mut outputs, &[], &context);
475        }
476
477        let max_gain = 10.0_f32.powf(30.0 / 20.0);
478        for &out in &output {
479            assert!(
480                out <= 0.1 * max_gain * 1.1,
481                "Should clamp to +30dB maximum, got {}",
482                out
483            );
484        }
485    }
486
487    #[test]
488    fn test_silence_input_f32() {
489        let mut gain = GainBlock::<f32>::new(20.0, None);
490        let context = test_context(4);
491
492        let input = [0.0f32; 4];
493        let mut output = [1.0f32; 4];
494
495        let inputs: [&[f32]; 1] = [&input];
496        let mut outputs: [&mut [f32]; 1] = [&mut output];
497
498        gain.process(&inputs, &mut outputs, &[], &context);
499
500        for (i, &out) in output.iter().enumerate() {
501            assert!(
502                out.abs() < 1e-10,
503                "Silence input should produce silence: output[{}]={}",
504                i,
505                out
506            );
507        }
508    }
509
510    #[test]
511    fn test_silence_input_f64() {
512        let mut gain = GainBlock::<f64>::new(20.0, None);
513        let context = test_context(4);
514
515        let input = [0.0f64; 4];
516        let mut output = [1.0f64; 4];
517
518        let inputs: [&[f64]; 1] = [&input];
519        let mut outputs: [&mut [f64]; 1] = [&mut output];
520
521        gain.process(&inputs, &mut outputs, &[], &context);
522
523        for (i, &out) in output.iter().enumerate() {
524            assert!(
525                out.abs() < 1e-15,
526                "Silence input should produce silence: output[{}]={}",
527                i,
528                out
529            );
530        }
531    }
532}