1use std::collections::VecDeque;
4use std::time::Duration;
5
6use rodio::source::{Limit, LimitSettings, SeekError, Source};
7use serde::{Deserialize, Serialize};
8
9use super::EffectContext;
10
11const DEFAULT_THRESHOLD_DB: f32 = -1.0;
12const DEFAULT_KNEE_WIDTH_DB: f32 = 4.0;
13const DEFAULT_ATTACK_MS: f32 = 5.0;
14const DEFAULT_RELEASE_MS: f32 = 100.0;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(default)]
19pub struct LimiterSettings {
20 #[serde(alias = "threshold", alias = "threshold_db")]
21 pub threshold_db: f32,
22 #[serde(alias = "knee_width", alias = "knee_width_db")]
23 pub knee_width_db: f32,
24 #[serde(alias = "attack_ms", alias = "attack")]
25 pub attack_ms: f32,
26 #[serde(alias = "release_ms", alias = "release")]
27 pub release_ms: f32,
28}
29
30impl LimiterSettings {
31 pub fn new(threshold_db: f32, knee_width_db: f32, attack_ms: f32, release_ms: f32) -> Self {
33 Self {
34 threshold_db,
35 knee_width_db,
36 attack_ms,
37 release_ms,
38 }
39 }
40}
41
42impl Default for LimiterSettings {
43 fn default() -> Self {
44 Self {
45 threshold_db: DEFAULT_THRESHOLD_DB,
46 knee_width_db: DEFAULT_KNEE_WIDTH_DB,
47 attack_ms: DEFAULT_ATTACK_MS,
48 release_ms: DEFAULT_RELEASE_MS,
49 }
50 }
51}
52
53#[derive(Clone, Serialize, Deserialize)]
55#[serde(default)]
56pub struct LimiterEffect {
57 pub enabled: bool,
58 #[serde(flatten)]
59 pub settings: LimiterSettings,
60 #[serde(skip)]
61 state: Option<LimiterState>,
62}
63
64impl std::fmt::Debug for LimiterEffect {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("LimiterEffect")
67 .field("enabled", &self.enabled)
68 .field("settings", &self.settings)
69 .finish()
70 }
71}
72
73impl Default for LimiterEffect {
74 fn default() -> Self {
75 Self {
76 enabled: false,
77 settings: LimiterSettings::default(),
78 state: None,
79 }
80 }
81}
82
83impl LimiterEffect {
84 pub fn process(&mut self, samples: &[f32], context: &EffectContext, _drain: bool) -> Vec<f32> {
94 if !self.enabled {
95 return samples.to_vec();
96 }
97
98 self.ensure_state(context);
99 let Some(state) = self.state.as_mut() else {
100 return samples.to_vec();
101 };
102
103 if samples.is_empty() {
104 return Vec::new();
105 }
106
107 state.process(samples)
108 }
109
110 pub fn reset_state(&mut self) {
112 if let Some(state) = self.state.as_mut() {
113 state.reset();
114 }
115 self.state = None;
116 }
117
118 fn ensure_state(&mut self, context: &EffectContext) {
119 let settings = sanitize_settings(&self.settings);
120 let channels = context.channels.max(1);
121
122 let needs_reset = self
123 .state
124 .as_ref()
125 .map(|state| !state.matches(context.sample_rate, channels, &settings))
126 .unwrap_or(true);
127
128 if needs_reset {
129 self.state = Some(LimiterState::new(context.sample_rate, channels, settings));
130 }
131 }
132}
133
134#[derive(Clone)]
135struct LimiterState {
136 sample_rate: u32,
137 channels: usize,
138 settings: LimiterSettings,
139 limiter: Limit<ChunkSource>,
140}
141
142impl LimiterState {
143 fn new(sample_rate: u32, channels: usize, settings: LimiterSettings) -> Self {
144 let source = ChunkSource::new(channels as u16, sample_rate);
145 let limiter = source.limit(build_limit_settings(&settings));
146 Self {
147 sample_rate,
148 channels,
149 settings,
150 limiter,
151 }
152 }
153
154 fn matches(&self, sample_rate: u32, channels: usize, settings: &LimiterSettings) -> bool {
155 self.sample_rate == sample_rate
156 && self.channels == channels
157 && (self.settings.threshold_db - settings.threshold_db).abs() < f32::EPSILON
158 && (self.settings.knee_width_db - settings.knee_width_db).abs() < f32::EPSILON
159 && (self.settings.attack_ms - settings.attack_ms).abs() < f32::EPSILON
160 && (self.settings.release_ms - settings.release_ms).abs() < f32::EPSILON
161 }
162
163 fn process(&mut self, samples: &[f32]) -> Vec<f32> {
164 {
165 let inner = self.limiter.inner_mut();
166 inner.push_samples(samples);
167 }
168
169 let mut output = Vec::with_capacity(samples.len());
170 for _ in 0..samples.len() {
171 if let Some(sample) = self.limiter.next() {
172 output.push(sample);
173 } else {
174 break;
175 }
176 }
177 output
178 }
179
180 fn reset(&mut self) {
181 let source = ChunkSource::new(self.channels as u16, self.sample_rate);
182 self.limiter = source.limit(build_limit_settings(&self.settings));
183 }
184}
185
186#[derive(Clone, Debug)]
187struct ChunkSource {
188 channels: u16,
189 sample_rate: u32,
190 queue: VecDeque<f32>,
191}
192
193impl ChunkSource {
194 fn new(channels: u16, sample_rate: u32) -> Self {
195 Self {
196 channels,
197 sample_rate,
198 queue: VecDeque::new(),
199 }
200 }
201
202 fn push_samples(&mut self, samples: &[f32]) {
203 self.queue.extend(samples.iter().copied());
204 }
205}
206
207impl Iterator for ChunkSource {
208 type Item = f32;
209
210 fn next(&mut self) -> Option<Self::Item> {
211 self.queue.pop_front()
212 }
213
214 fn size_hint(&self) -> (usize, Option<usize>) {
215 let len = self.queue.len();
216 (len, Some(len))
217 }
218}
219
220impl Source for ChunkSource {
221 fn current_span_len(&self) -> Option<usize> {
222 Some(self.queue.len())
223 }
224
225 fn channels(&self) -> u16 {
226 self.channels
227 }
228
229 fn sample_rate(&self) -> u32 {
230 self.sample_rate
231 }
232
233 fn total_duration(&self) -> Option<Duration> {
234 None
235 }
236
237 fn try_seek(&mut self, _pos: Duration) -> Result<(), SeekError> {
238 Err(SeekError::NotSupported {
239 underlying_source: "ChunkSource",
240 })
241 }
242}
243
244fn build_limit_settings(settings: &LimiterSettings) -> LimitSettings {
245 LimitSettings::default()
246 .with_threshold(settings.threshold_db)
247 .with_knee_width(settings.knee_width_db)
248 .with_attack(Duration::from_secs_f32(settings.attack_ms / 1000.0))
249 .with_release(Duration::from_secs_f32(settings.release_ms / 1000.0))
250}
251
252fn sanitize_settings(settings: &LimiterSettings) -> LimiterSettings {
253 LimiterSettings {
254 threshold_db: sanitize_threshold_db(settings.threshold_db),
255 knee_width_db: sanitize_knee_width_db(settings.knee_width_db),
256 attack_ms: sanitize_time_ms(settings.attack_ms, DEFAULT_ATTACK_MS),
257 release_ms: sanitize_time_ms(settings.release_ms, DEFAULT_RELEASE_MS),
258 }
259}
260
261fn sanitize_threshold_db(value: f32) -> f32 {
262 if !value.is_finite() {
263 return DEFAULT_THRESHOLD_DB;
264 }
265 value.min(0.0)
266}
267
268fn sanitize_knee_width_db(value: f32) -> f32 {
269 if !value.is_finite() {
270 return DEFAULT_KNEE_WIDTH_DB;
271 }
272 value.max(0.1)
273}
274
275fn sanitize_time_ms(value: f32, fallback: f32) -> f32 {
276 if !value.is_finite() {
277 return fallback;
278 }
279 value.max(0.0)
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 fn context(channels: usize) -> EffectContext {
287 EffectContext {
288 sample_rate: 48_000,
289 channels,
290 container_path: None,
291 impulse_response_spec: None,
292 impulse_response_tail_db: -60.0,
293 }
294 }
295
296 fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
297 (a - b).abs() <= eps
298 }
299
300 #[test]
301 fn limiter_disabled_passthrough() {
302 let mut effect = LimiterEffect::default();
303 let samples = vec![0.25_f32, -0.25, 0.5, -0.5];
304 let output = effect.process(&samples, &context(2), false);
305 assert_eq!(output, samples);
306 }
307
308 #[test]
309 fn limiter_reduces_hot_signal() {
310 let mut effect = LimiterEffect::default();
311 effect.enabled = true;
312 effect.settings.threshold_db = -12.0;
313 effect.settings.knee_width_db = 0.5;
314 effect.settings.attack_ms = 0.0;
315 effect.settings.release_ms = 0.0;
316
317 let samples = vec![1.0_f32, -1.0, 1.0, -1.0];
318 let output = effect.process(&samples, &context(2), false);
319 assert_eq!(output.len(), samples.len());
320 assert!(output.iter().all(|value| value.is_finite()));
321 assert!(output.iter().any(|value| value.abs() < 1.0));
322 }
323
324 #[test]
325 fn limiter_split_matches_single_pass() {
326 let mut settings = LimiterEffect::default();
327 settings.enabled = true;
328 settings.settings.threshold_db = -6.0;
329 settings.settings.knee_width_db = 1.0;
330 settings.settings.attack_ms = 0.0;
331 settings.settings.release_ms = 0.0;
332
333 let samples = vec![1.0_f32, -1.0, 0.8, -0.8, 0.6, -0.6, 0.4, -0.4];
334
335 let mut effect_full = settings.clone();
336 let out_full = effect_full.process(&samples, &context(2), false);
337
338 let mut effect_split = settings;
339 let mid = samples.len() / 2;
340 let mut out_split = effect_split.process(&samples[..mid], &context(2), false);
341 out_split.extend(effect_split.process(&samples[mid..], &context(2), false));
342
343 assert_eq!(out_full.len(), out_split.len());
344 for (a, b) in out_full.iter().zip(out_split.iter()) {
345 assert!(approx_eq(*a, *b, 1e-5));
346 }
347 }
348}