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