1#![forbid(unsafe_code)]
7#![allow(clippy::cast_sign_loss)]
8#![allow(clippy::cast_possible_wrap)]
9#![allow(clippy::cast_lossless)]
10#![allow(clippy::needless_pass_by_value)]
11#![allow(clippy::needless_range_loop)]
12#![allow(clippy::get_first)]
13#![allow(clippy::doc_markdown)]
14
15use bytes::{Bytes, BytesMut};
16use std::collections::VecDeque;
17
18use crate::error::{GraphError, GraphResult};
19use crate::frame::FilterFrame;
20use crate::node::{Node, NodeId, NodeState, NodeType};
21use crate::port::{AudioPortFormat, InputPort, OutputPort, PortFormat, PortId, PortType};
22
23use oximedia_audio::{AudioBuffer, AudioFrame, ChannelLayout};
24use oximedia_core::SampleFormat;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
28pub enum LimiterMode {
29 #[default]
31 Brickwall,
32 Soft,
34}
35
36#[derive(Clone, Debug)]
38pub struct LimiterConfig {
39 pub ceiling_db: f64,
41 pub release_ms: f64,
43 pub mode: LimiterMode,
45 pub true_peak: bool,
47 pub lookahead_ms: f64,
49}
50
51impl Default for LimiterConfig {
52 fn default() -> Self {
53 Self {
54 ceiling_db: -0.3,
55 release_ms: 100.0,
56 mode: LimiterMode::Brickwall,
57 true_peak: true,
58 lookahead_ms: 5.0,
59 }
60 }
61}
62
63impl LimiterConfig {
64 #[must_use]
66 pub fn new(ceiling_db: f64) -> Self {
67 Self {
68 ceiling_db,
69 ..Default::default()
70 }
71 }
72
73 #[must_use]
75 pub fn with_release(mut self, release_ms: f64) -> Self {
76 self.release_ms = release_ms;
77 self
78 }
79
80 #[must_use]
82 pub fn with_mode(mut self, mode: LimiterMode) -> Self {
83 self.mode = mode;
84 self
85 }
86
87 #[must_use]
89 pub fn with_true_peak(mut self, enabled: bool) -> Self {
90 self.true_peak = enabled;
91 self
92 }
93
94 #[must_use]
96 pub fn with_lookahead(mut self, lookahead_ms: f64) -> Self {
97 self.lookahead_ms = lookahead_ms;
98 self
99 }
100
101 #[must_use]
103 pub fn brickwall(ceiling_db: f64) -> Self {
104 Self::new(ceiling_db).with_mode(LimiterMode::Brickwall)
105 }
106
107 #[must_use]
109 pub fn soft(ceiling_db: f64) -> Self {
110 Self::new(ceiling_db).with_mode(LimiterMode::Soft)
111 }
112
113 #[must_use]
115 pub fn db_to_linear(db: f64) -> f64 {
116 10.0_f64.powf(db / 20.0)
117 }
118
119 #[must_use]
121 pub fn linear_to_db(linear: f64) -> f64 {
122 if linear <= 0.0 {
123 f64::NEG_INFINITY
124 } else {
125 20.0 * linear.log10()
126 }
127 }
128}
129
130#[derive(Clone, Debug)]
132struct TruePeakDetector {
133 oversample: usize,
135 filter_coeffs: Vec<f64>,
137 history: Vec<VecDeque<f64>>,
139}
140
141impl TruePeakDetector {
142 fn new(channels: usize) -> Self {
144 let oversample = 4;
146 let filter_coeffs = Self::create_filter_coeffs();
147 let history = vec![VecDeque::with_capacity(filter_coeffs.len()); channels];
148
149 Self {
150 oversample,
151 filter_coeffs,
152 history,
153 }
154 }
155
156 fn create_filter_coeffs() -> Vec<f64> {
158 vec![
160 0.0, 0.0625, 0.0, 0.125, 0.0, 0.25, 0.0, 0.5, 1.0, 0.5, 0.0, 0.25, 0.0, 0.125, 0.0,
161 0.0625,
162 ]
163 }
164
165 fn detect(&mut self, channel: usize, sample: f64) -> f64 {
167 if channel >= self.history.len() {
168 return sample.abs();
169 }
170
171 self.history[channel].push_back(sample);
173 if self.history[channel].len() > self.filter_coeffs.len() {
174 self.history[channel].pop_front();
175 }
176
177 let mut peak = sample.abs();
179
180 for phase in 0..self.oversample {
181 let mut sum = 0.0;
182 for (i, &coeff) in self.filter_coeffs.iter().enumerate() {
183 let idx = (i * self.oversample + phase) / self.oversample;
184 if idx < self.history[channel].len() {
185 let hist_sample = self.history[channel].get(idx).copied().unwrap_or(0.0);
186 sum += hist_sample * coeff;
187 }
188 }
189 peak = peak.max(sum.abs());
190 }
191
192 peak
193 }
194
195 fn reset(&mut self) {
197 for hist in &mut self.history {
198 hist.clear();
199 }
200 }
201}
202
203#[derive(Clone, Debug)]
205struct LookaheadBuffer {
206 buffers: Vec<VecDeque<f64>>,
208 peak_buffer: VecDeque<f64>,
210 delay_samples: usize,
212}
213
214impl LookaheadBuffer {
215 fn new(lookahead_ms: f64, sample_rate: f64, channels: usize) -> Self {
217 let delay_samples = ((lookahead_ms * 0.001 * sample_rate) as usize).max(1);
218
219 Self {
220 buffers: vec![VecDeque::with_capacity(delay_samples + 1); channels],
221 peak_buffer: VecDeque::with_capacity(delay_samples + 1),
222 delay_samples,
223 }
224 }
225
226 fn process(&mut self, channel: usize, sample: f64, peak: f64) -> (f64, f64) {
228 if channel >= self.buffers.len() {
229 return (sample, peak);
230 }
231
232 self.buffers[channel].push_back(sample);
233 if channel == 0 {
234 self.peak_buffer.push_back(peak);
235 }
236
237 let delayed_sample = if self.buffers[channel].len() > self.delay_samples {
238 self.buffers[channel].pop_front().unwrap_or(sample)
239 } else {
240 0.0
241 };
242
243 let lookahead_peak = if channel == 0 && self.peak_buffer.len() > self.delay_samples {
245 self.peak_buffer.pop_front();
246 self.peak_buffer.iter().copied().fold(0.0_f64, f64::max)
247 } else {
248 self.peak_buffer.iter().copied().fold(0.0_f64, f64::max)
249 };
250
251 (delayed_sample, lookahead_peak)
252 }
253
254 fn reset(&mut self) {
256 for buffer in &mut self.buffers {
257 buffer.clear();
258 }
259 self.peak_buffer.clear();
260 }
261}
262
263#[derive(Clone, Debug)]
265struct GainSmoother {
266 current_gain: f64,
268 release_coeff: f64,
270}
271
272impl GainSmoother {
273 fn new(release_ms: f64, sample_rate: f64) -> Self {
275 let release_coeff = if release_ms > 0.0 {
276 (-1.0 / (release_ms * 0.001 * sample_rate)).exp()
277 } else {
278 0.0
279 };
280
281 Self {
282 current_gain: 1.0,
283 release_coeff,
284 }
285 }
286
287 fn update(&mut self, target_gain: f64) -> f64 {
289 if target_gain < self.current_gain {
290 self.current_gain = target_gain;
292 } else {
293 self.current_gain =
295 self.release_coeff * self.current_gain + (1.0 - self.release_coeff) * target_gain;
296 }
297 self.current_gain
298 }
299
300 fn reset(&mut self) {
302 self.current_gain = 1.0;
303 }
304}
305
306struct LimiterState {
308 ceiling: f64,
310 true_peak_detector: TruePeakDetector,
312 lookahead: LookaheadBuffer,
314 gain_smoother: GainSmoother,
316 gain_reduction_db: f64,
318 mode: LimiterMode,
320}
321
322impl LimiterState {
323 fn new(config: &LimiterConfig, sample_rate: f64, channels: usize) -> Self {
325 let ceiling = LimiterConfig::db_to_linear(config.ceiling_db);
326
327 Self {
328 ceiling,
329 true_peak_detector: TruePeakDetector::new(channels),
330 lookahead: LookaheadBuffer::new(config.lookahead_ms, sample_rate, channels),
331 gain_smoother: GainSmoother::new(config.release_ms, sample_rate),
332 gain_reduction_db: 0.0,
333 mode: config.mode,
334 }
335 }
336
337 fn soft_limit(sample: f64, ceiling: f64) -> f64 {
339 if sample.abs() <= ceiling {
340 sample
341 } else {
342 let sign = sample.signum();
343 let excess = sample.abs() - ceiling;
344 let range = 1.0 - ceiling;
345 let saturated = ceiling + range * (excess / range).tanh();
346 sign * saturated.min(1.0)
347 }
348 }
349
350 fn process(&mut self, samples: &mut [Vec<f64>], config: &LimiterConfig) {
352 let sample_count = samples.get(0).map_or(0, Vec::len);
353 let channels = samples.len();
354
355 for i in 0..sample_count {
356 let mut peak = 0.0_f64;
358 for ch in 0..channels {
359 if i < samples[ch].len() {
360 let sample_peak = if config.true_peak {
361 self.true_peak_detector.detect(ch, samples[ch][i])
362 } else {
363 samples[ch][i].abs()
364 };
365 peak = peak.max(sample_peak);
366 }
367 }
368
369 let target_gain = if peak > self.ceiling {
371 self.ceiling / peak
372 } else {
373 1.0
374 };
375
376 for ch in 0..channels {
378 if i < samples[ch].len() {
379 let (delayed_sample, lookahead_peak) =
380 self.lookahead.process(ch, samples[ch][i], peak);
381
382 let lookahead_gain = if lookahead_peak > self.ceiling {
384 self.ceiling / lookahead_peak
385 } else {
386 1.0
387 };
388
389 let final_target = target_gain.min(lookahead_gain);
391 let smoothed_gain = self.gain_smoother.update(final_target);
392
393 let output = match self.mode {
395 LimiterMode::Brickwall => {
396 (delayed_sample * smoothed_gain).clamp(-self.ceiling, self.ceiling)
397 }
398 LimiterMode::Soft => {
399 Self::soft_limit(delayed_sample * smoothed_gain, self.ceiling)
400 }
401 };
402
403 samples[ch][i] = output;
404 }
405 }
406
407 if target_gain < 1.0 {
409 self.gain_reduction_db = LimiterConfig::linear_to_db(target_gain);
410 } else {
411 self.gain_reduction_db *= 0.999;
413 }
414 }
415 }
416
417 fn reset(&mut self) {
419 self.true_peak_detector.reset();
420 self.lookahead.reset();
421 self.gain_smoother.reset();
422 self.gain_reduction_db = 0.0;
423 }
424}
425
426pub struct LimiterFilter {
443 id: NodeId,
444 name: String,
445 state: NodeState,
446 config: LimiterConfig,
447 limiter_state: Option<LimiterState>,
448 inputs: Vec<InputPort>,
449 outputs: Vec<OutputPort>,
450}
451
452impl LimiterFilter {
453 #[must_use]
455 pub fn new(id: NodeId, name: impl Into<String>, config: LimiterConfig) -> Self {
456 let audio_format = PortFormat::Audio(AudioPortFormat::any());
457
458 Self {
459 id,
460 name: name.into(),
461 state: NodeState::Idle,
462 config,
463 limiter_state: None,
464 inputs: vec![InputPort::new(PortId(0), "input", PortType::Audio)
465 .with_format(audio_format.clone())],
466 outputs: vec![
467 OutputPort::new(PortId(0), "output", PortType::Audio).with_format(audio_format)
468 ],
469 }
470 }
471
472 #[must_use]
474 pub fn config(&self) -> &LimiterConfig {
475 &self.config
476 }
477
478 pub fn set_config(&mut self, config: LimiterConfig) {
480 self.config = config;
481 self.limiter_state = None; }
483
484 #[must_use]
486 pub fn gain_reduction_db(&self) -> f64 {
487 self.limiter_state
488 .as_ref()
489 .map_or(0.0, |s| s.gain_reduction_db)
490 }
491
492 fn frame_to_samples(frame: &AudioFrame) -> Vec<Vec<f64>> {
494 let channels = frame.channels.count();
495 let sample_count = frame.sample_count();
496
497 if sample_count == 0 {
498 return vec![Vec::new(); channels];
499 }
500
501 let mut output = vec![Vec::with_capacity(sample_count); channels];
502
503 match &frame.samples {
504 AudioBuffer::Interleaved(data) => {
505 Self::convert_interleaved(data, frame.format, channels, &mut output);
506 }
507 AudioBuffer::Planar(planes) => {
508 Self::convert_planar(planes, frame.format, &mut output);
509 }
510 }
511
512 output
513 }
514
515 fn convert_interleaved(
517 data: &Bytes,
518 format: SampleFormat,
519 channels: usize,
520 output: &mut [Vec<f64>],
521 ) {
522 let bytes_per_sample = format.bytes_per_sample();
523 if bytes_per_sample == 0 || channels == 0 {
524 return;
525 }
526
527 let sample_count = data.len() / (bytes_per_sample * channels);
528
529 for i in 0..sample_count {
530 for ch in 0..channels {
531 let offset = (i * channels + ch) * bytes_per_sample;
532 if offset + bytes_per_sample <= data.len() {
533 let sample =
534 Self::bytes_to_f64(&data[offset..offset + bytes_per_sample], format);
535 output[ch].push(sample);
536 }
537 }
538 }
539 }
540
541 fn convert_planar(planes: &[Bytes], format: SampleFormat, output: &mut [Vec<f64>]) {
543 let bytes_per_sample = format.bytes_per_sample();
544 if bytes_per_sample == 0 {
545 return;
546 }
547
548 for (ch, plane) in planes.iter().enumerate() {
549 if ch >= output.len() {
550 break;
551 }
552 let sample_count = plane.len() / bytes_per_sample;
553 for i in 0..sample_count {
554 let offset = i * bytes_per_sample;
555 if offset + bytes_per_sample <= plane.len() {
556 let sample =
557 Self::bytes_to_f64(&plane[offset..offset + bytes_per_sample], format);
558 output[ch].push(sample);
559 }
560 }
561 }
562 }
563
564 fn bytes_to_f64(bytes: &[u8], format: SampleFormat) -> f64 {
566 match format {
567 SampleFormat::U8 => {
568 if bytes.is_empty() {
569 return 0.0;
570 }
571 (f64::from(bytes[0]) - 128.0) / 128.0
572 }
573 SampleFormat::S16 => {
574 if bytes.len() < 2 {
575 return 0.0;
576 }
577 let sample = i16::from_le_bytes([bytes[0], bytes[1]]);
578 f64::from(sample) / f64::from(i16::MAX)
579 }
580 SampleFormat::S32 => {
581 if bytes.len() < 4 {
582 return 0.0;
583 }
584 let sample = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
585 f64::from(sample) / f64::from(i32::MAX)
586 }
587 SampleFormat::F32 => {
588 if bytes.len() < 4 {
589 return 0.0;
590 }
591 f64::from(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
592 }
593 SampleFormat::F64 => {
594 if bytes.len() < 8 {
595 return 0.0;
596 }
597 f64::from_le_bytes([
598 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
599 ])
600 }
601 _ => 0.0,
602 }
603 }
604
605 fn samples_to_frame(
607 samples: Vec<Vec<f64>>,
608 format: SampleFormat,
609 sample_rate: u32,
610 channels: ChannelLayout,
611 ) -> AudioFrame {
612 let channel_count = channels.count();
613 if samples.is_empty() || samples[0].is_empty() || channel_count == 0 {
614 return AudioFrame::new(format, sample_rate, channels);
615 }
616
617 let sample_count = samples[0].len();
618 let bytes_per_sample = format.bytes_per_sample();
619 let mut buffer = BytesMut::with_capacity(sample_count * channel_count * bytes_per_sample);
620
621 for i in 0..sample_count {
622 for ch in 0..channel_count {
623 let sample = if ch < samples.len() && i < samples[ch].len() {
624 samples[ch][i]
625 } else {
626 0.0
627 };
628 Self::f64_to_bytes(sample, format, &mut buffer);
629 }
630 }
631
632 let mut frame = AudioFrame::new(format, sample_rate, channels);
633 frame.samples = AudioBuffer::Interleaved(buffer.freeze());
634 frame
635 }
636
637 fn f64_to_bytes(sample: f64, format: SampleFormat, buffer: &mut BytesMut) {
639 let clamped = sample.clamp(-1.0, 1.0);
640
641 match format {
642 SampleFormat::U8 => {
643 let value = ((clamped * 128.0) + 128.0) as u8;
644 buffer.extend_from_slice(&[value]);
645 }
646 SampleFormat::S16 => {
647 let value = (clamped * f64::from(i16::MAX)) as i16;
648 buffer.extend_from_slice(&value.to_le_bytes());
649 }
650 SampleFormat::S32 => {
651 let value = (clamped * f64::from(i32::MAX)) as i32;
652 buffer.extend_from_slice(&value.to_le_bytes());
653 }
654 SampleFormat::F32 => {
655 #[allow(clippy::cast_possible_truncation)]
656 let value = clamped as f32;
657 buffer.extend_from_slice(&value.to_le_bytes());
658 }
659 SampleFormat::F64 => {
660 buffer.extend_from_slice(&clamped.to_le_bytes());
661 }
662 _ => {}
663 }
664 }
665}
666
667impl Node for LimiterFilter {
668 fn id(&self) -> NodeId {
669 self.id
670 }
671
672 fn name(&self) -> &str {
673 &self.name
674 }
675
676 fn node_type(&self) -> NodeType {
677 NodeType::Filter
678 }
679
680 fn state(&self) -> NodeState {
681 self.state
682 }
683
684 fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
685 if !self.state.can_transition_to(state) {
686 return Err(GraphError::InvalidStateTransition {
687 node: self.id,
688 from: self.state.to_string(),
689 to: state.to_string(),
690 });
691 }
692 self.state = state;
693 Ok(())
694 }
695
696 fn inputs(&self) -> &[InputPort] {
697 &self.inputs
698 }
699
700 fn outputs(&self) -> &[OutputPort] {
701 &self.outputs
702 }
703
704 fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
705 let frame = match input {
706 Some(FilterFrame::Audio(frame)) => frame,
707 Some(_) => {
708 return Err(GraphError::PortTypeMismatch {
709 expected: "Audio".to_string(),
710 actual: "Video".to_string(),
711 });
712 }
713 None => return Ok(None),
714 };
715
716 if self.limiter_state.is_none() {
718 let channels = frame.channels.count();
719 self.limiter_state = Some(LimiterState::new(
720 &self.config,
721 f64::from(frame.sample_rate),
722 channels,
723 ));
724 }
725
726 let mut samples = Self::frame_to_samples(&frame);
728
729 if let Some(ref mut limiter_state) = self.limiter_state {
731 limiter_state.process(&mut samples, &self.config);
732 }
733
734 let output_frame = Self::samples_to_frame(
736 samples,
737 frame.format,
738 frame.sample_rate,
739 frame.channels.clone(),
740 );
741
742 Ok(Some(FilterFrame::Audio(output_frame)))
743 }
744
745 fn reset(&mut self) -> GraphResult<()> {
746 if let Some(ref mut state) = self.limiter_state {
747 state.reset();
748 }
749 self.set_state(NodeState::Idle)
750 }
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn test_limiter_mode_default() {
759 assert_eq!(LimiterMode::default(), LimiterMode::Brickwall);
760 }
761
762 #[test]
763 fn test_limiter_config() {
764 let config = LimiterConfig::new(-0.3)
765 .with_release(100.0)
766 .with_mode(LimiterMode::Soft)
767 .with_true_peak(true)
768 .with_lookahead(5.0);
769
770 assert!((config.ceiling_db - (-0.3)).abs() < f64::EPSILON);
771 assert!((config.release_ms - 100.0).abs() < f64::EPSILON);
772 assert_eq!(config.mode, LimiterMode::Soft);
773 assert!(config.true_peak);
774 assert!((config.lookahead_ms - 5.0).abs() < f64::EPSILON);
775 }
776
777 #[test]
778 fn test_preset_configs() {
779 let brickwall = LimiterConfig::brickwall(-0.5);
780 assert_eq!(brickwall.mode, LimiterMode::Brickwall);
781 assert!((brickwall.ceiling_db - (-0.5)).abs() < f64::EPSILON);
782
783 let soft = LimiterConfig::soft(-1.0);
784 assert_eq!(soft.mode, LimiterMode::Soft);
785 }
786
787 #[test]
788 fn test_db_conversion() {
789 let linear = LimiterConfig::db_to_linear(0.0);
790 assert!((linear - 1.0).abs() < f64::EPSILON);
791
792 let db = LimiterConfig::linear_to_db(1.0);
793 assert!(db.abs() < f64::EPSILON);
794 }
795
796 #[test]
797 fn test_soft_limit() {
798 let ceiling = 0.9;
799
800 let result = LimiterState::soft_limit(0.5, ceiling);
802 assert!((result - 0.5).abs() < f64::EPSILON);
803
804 let result = LimiterState::soft_limit(0.9, ceiling);
806 assert!((result - 0.9).abs() < f64::EPSILON);
807
808 let result = LimiterState::soft_limit(1.5, ceiling);
810 assert!(result > ceiling);
811 assert!(result <= 1.0);
812
813 let result = LimiterState::soft_limit(-1.5, ceiling);
815 assert!(result < -ceiling);
816 assert!(result >= -1.0);
817 }
818
819 #[test]
820 fn test_true_peak_detector() {
821 let mut detector = TruePeakDetector::new(2);
822
823 let peak = detector.detect(0, 0.5);
824 assert!(peak >= 0.5);
825 assert!(peak.is_finite());
826
827 detector.reset();
828 assert!(detector.history[0].is_empty());
829 }
830
831 #[test]
832 fn test_lookahead_buffer() {
833 let mut buffer = LookaheadBuffer::new(1.0, 48000.0, 2);
834
835 let (delayed, _peak) = buffer.process(0, 1.0, 1.0);
837 assert!(delayed.abs() < f64::EPSILON); buffer.reset();
841 assert!(buffer.buffers[0].is_empty());
842 }
843
844 #[test]
845 fn test_gain_smoother() {
846 let mut smoother = GainSmoother::new(100.0, 48000.0);
847
848 let gain = smoother.update(0.5);
850 assert!((gain - 0.5).abs() < f64::EPSILON);
851
852 let gain1 = smoother.update(1.0);
854 let gain2 = smoother.update(1.0);
855 assert!(gain1 < gain2);
856 assert!(gain2 < 1.0);
857
858 smoother.reset();
859 assert!((smoother.current_gain - 1.0).abs() < f64::EPSILON);
860 }
861
862 #[test]
863 fn test_limiter_filter_creation() {
864 let config = LimiterConfig::brickwall(-0.3);
865 let filter = LimiterFilter::new(NodeId(1), "limiter", config);
866
867 assert_eq!(filter.id(), NodeId(1));
868 assert_eq!(filter.name(), "limiter");
869 assert_eq!(filter.node_type(), NodeType::Filter);
870 }
871
872 #[test]
873 fn test_limiter_filter_ports() {
874 let config = LimiterConfig::default();
875 let filter = LimiterFilter::new(NodeId(0), "test", config);
876
877 assert_eq!(filter.inputs().len(), 1);
878 assert_eq!(filter.outputs().len(), 1);
879 assert_eq!(filter.inputs()[0].port_type, PortType::Audio);
880 }
881
882 #[test]
883 fn test_process_none() {
884 let config = LimiterConfig::default();
885 let mut filter = LimiterFilter::new(NodeId(0), "test", config);
886
887 let result = filter.process(None).expect("process should succeed");
888 assert!(result.is_none());
889 }
890
891 #[test]
892 fn test_process_audio() {
893 let config = LimiterConfig::brickwall(-6.0);
894 let mut filter = LimiterFilter::new(NodeId(0), "test", config);
895
896 let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Mono);
897 let mut samples = BytesMut::new();
898 for _ in 0..100 {
899 samples.extend_from_slice(&1.0f32.to_le_bytes()); }
901 frame.samples = AudioBuffer::Interleaved(samples.freeze());
902
903 let result = filter
904 .process(Some(FilterFrame::Audio(frame)))
905 .expect("process should succeed");
906 assert!(result.is_some());
907
908 if let Some(FilterFrame::Audio(output)) = result {
909 if let AudioBuffer::Interleaved(data) = &output.samples {
910 let last_offset = data.len() - 4;
912 let sample = f32::from_le_bytes([
913 data[last_offset],
914 data[last_offset + 1],
915 data[last_offset + 2],
916 data[last_offset + 3],
917 ]);
918 let ceiling = LimiterConfig::db_to_linear(-6.0);
919 assert!(sample.abs() <= ceiling as f32 + 0.01);
920 }
921 }
922 }
923
924 #[test]
925 fn test_gain_reduction_metering() {
926 let config = LimiterConfig::default();
927 let filter = LimiterFilter::new(NodeId(0), "test", config);
928
929 assert!(filter.gain_reduction_db().abs() < f64::EPSILON);
931 }
932
933 #[test]
934 fn test_state_transitions() {
935 let config = LimiterConfig::default();
936 let mut filter = LimiterFilter::new(NodeId(0), "test", config);
937
938 assert!(filter.set_state(NodeState::Processing).is_ok());
939 assert_eq!(filter.state(), NodeState::Processing);
940
941 assert!(filter.reset().is_ok());
942 assert_eq!(filter.state(), NodeState::Idle);
943 }
944}