Skip to main content

alpha2_audio/
compressor.rs

1#![allow(clippy::cast_precision_loss)]
2
3use crate::{ProcessCtx, Processor, StateBlob, StateError};
4
5/// Largest ratio a compressor renders.
6pub const MAX_RATIO: f32 = 100.0;
7
8/// Linked-channel compressor with deterministic lookahead and soft attack/release.
9#[derive(Clone, Debug)]
10pub struct Compressor {
11    threshold_db: f32,
12    ratio: f32,
13    attack_ms: f32,
14    release_ms: f32,
15    lookahead_ms: f32,
16    envelope: f32,
17    attack_coefficient: f32,
18    release_coefficient: f32,
19    /// Linear threshold; envelopes at or under it skip the logarithm.
20    threshold_amplitude: f32,
21    delay: [Vec<f32>; 2],
22    cursor: usize,
23    latency: u32,
24    bypass: bool,
25}
26
27impl Compressor {
28    /// Creates a compressor; invalid parameters are replaced by conservative defaults.
29    #[must_use]
30    pub fn new(
31        threshold_db: f32,
32        ratio: f32,
33        attack_ms: f32,
34        release_ms: f32,
35        lookahead_ms: f32,
36    ) -> Self {
37        Self {
38            threshold_db: finite(threshold_db, -12.0).clamp(-96.0, 0.0),
39            ratio: finite(ratio, 4.0).clamp(1.0, MAX_RATIO),
40            attack_ms: positive(attack_ms, 10.0),
41            release_ms: positive(release_ms, 100.0),
42            lookahead_ms: finite(lookahead_ms, 0.0).clamp(0.0, 100.0),
43            envelope: 0.0,
44            attack_coefficient: 0.0,
45            release_coefficient: 0.0,
46            threshold_amplitude: 0.0,
47            delay: [Vec::new(), Vec::new()],
48            cursor: 0,
49            latency: 0,
50            bypass: false,
51        }
52    }
53
54    /// Enables latency-preserving bypass.
55    pub fn set_bypass(&mut self, bypass: bool) {
56        self.bypass = bypass;
57    }
58}
59
60impl Processor for Compressor {
61    fn prepare(&mut self, sample_rate: u32, _max_block: usize) {
62        let rate = sample_rate.max(1) as f32;
63        self.attack_coefficient = libm::expf(-1.0 / (self.attack_ms * 0.001 * rate));
64        self.release_coefficient = libm::expf(-1.0 / (self.release_ms * 0.001 * rate));
65        // Rounded down slightly so the decibel comparison below still decides borderline cases.
66        self.threshold_amplitude = libm::exp10f(self.threshold_db / 20.0) * (1.0 - 1.0e-4);
67        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
68        {
69            self.latency = (self.lookahead_ms * 0.001 * rate).round() as u32;
70        }
71        let size = usize::try_from(self.latency).unwrap_or(0).max(1);
72        self.delay = [vec![0.0; size], vec![0.0; size]];
73        self.cursor = 0;
74    }
75
76    fn process(&mut self, _ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
77        let channels = inputs.len().min(outputs.len()).min(2);
78        let frames = inputs
79            .iter()
80            .take(channels)
81            .map(|input| input.len())
82            .chain(outputs.iter().take(channels).map(|output| output.len()))
83            .min()
84            .unwrap_or(0);
85        if self.delay[0].is_empty() {
86            return;
87        }
88        for frame in 0..frames {
89            let peak = inputs[..channels]
90                .iter()
91                .map(|channel| finite(channel[frame], 0.0).abs())
92                .fold(0.0_f32, f32::max);
93            if !self.envelope.is_finite() {
94                self.envelope = 0.0;
95            }
96            let coefficient = if peak > self.envelope {
97                self.attack_coefficient
98            } else {
99                self.release_coefficient
100            };
101            self.envelope = coefficient * self.envelope + (1.0 - coefficient) * peak;
102            // Keep a released envelope out of the slow subnormal range.
103            if self.envelope < 1.0e-30 {
104                self.envelope = 0.0;
105            }
106            // Under the threshold the gain is exactly one. Over it, `exp10f` is as portable as
107            // `powf(10, x)` and about nine times cheaper.
108            let reduction_db = if self.envelope <= self.threshold_amplitude {
109                0.0
110            } else {
111                let level_db = 20.0 * libm::log10f(self.envelope.max(1.0e-12));
112                if level_db > self.threshold_db {
113                    (self.threshold_db + (level_db - self.threshold_db) / self.ratio) - level_db
114                } else {
115                    0.0
116                }
117            };
118            let gain = if self.bypass || reduction_db == 0.0 {
119                1.0
120            } else {
121                libm::exp10f(reduction_db / 20.0)
122            };
123            for channel in 0..channels {
124                let input = finite(inputs[channel][frame], 0.0);
125                let delayed = if self.latency == 0 {
126                    input
127                } else {
128                    let delayed = self.delay[channel][self.cursor];
129                    self.delay[channel][self.cursor] = input;
130                    finite(delayed, 0.0)
131                };
132                outputs[channel][frame] = delayed * gain;
133            }
134            if self.latency != 0 {
135                self.cursor = (self.cursor + 1) % self.delay[0].len();
136            }
137        }
138    }
139
140    fn reset(&mut self) {
141        self.envelope = 0.0;
142        self.cursor = 0;
143        for channel in &mut self.delay {
144            channel.fill(0.0);
145        }
146    }
147    fn latency_samples(&self) -> u32 {
148        self.latency
149    }
150    fn history_samples(&self) -> u32 {
151        if self.ratio <= 1.0 {
152            return 0;
153        }
154        crate::processor::settling_samples(f64::from(
155            self.attack_coefficient.max(self.release_coefficient),
156        ))
157    }
158    fn save_state(&self) -> StateBlob {
159        let mut bytes = Vec::with_capacity(16 + self.delay.iter().map(Vec::len).sum::<usize>() * 4);
160        bytes.extend_from_slice(&self.envelope.to_le_bytes());
161        bytes.extend_from_slice(&(u64::try_from(self.cursor).unwrap_or(0)).to_le_bytes());
162        bytes.extend_from_slice(&self.latency.to_le_bytes());
163        for channel in &self.delay {
164            for value in channel {
165                bytes.extend_from_slice(&value.to_le_bytes());
166            }
167        }
168        StateBlob::new(bytes)
169    }
170    fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
171        let bytes = state.as_bytes();
172        let expected = 16 + self.delay.iter().map(Vec::len).sum::<usize>() * 4;
173        if bytes.len() != expected {
174            return Err(StateError::Mismatch);
175        }
176        let envelope = f32::from_le_bytes(bytes[0..4].try_into().expect("fixed range"));
177        let cursor = usize::try_from(u64::from_le_bytes(
178            bytes[4..12].try_into().expect("fixed range"),
179        ))
180        .ok();
181        let latency = u32::from_le_bytes(bytes[12..16].try_into().expect("fixed range"));
182        if latency != self.latency {
183            return Err(StateError::Mismatch);
184        }
185        let values = bytes[16..]
186            .as_chunks::<4>()
187            .0
188            .iter()
189            .map(|word| f32::from_le_bytes(*word));
190        if !envelope.is_finite()
191            || envelope < 0.0
192            || cursor.is_none_or(|value| value >= self.delay[0].len())
193            || values.clone().any(|value| !value.is_finite())
194        {
195            return Err(StateError::Invalid);
196        }
197        self.envelope = envelope;
198        self.cursor = cursor.unwrap_or(0);
199        for (target, value) in self.delay.iter_mut().flatten().zip(values) {
200            *target = value;
201        }
202        Ok(())
203    }
204}
205
206fn finite(value: f32, fallback: f32) -> f32 {
207    if value.is_finite() { value } else { fallback }
208}
209fn positive(value: f32, fallback: f32) -> f32 {
210    let value = finite(value, fallback);
211    if value > 0.0 { value } else { fallback }
212}