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