1use crate::auto_makeup::MeasuredMakeup;
16use crate::detector::{DetectionMode, LevelDetector};
17use crate::envelope::DualRelease;
18use crate::lookahead::LookaheadBuffer;
19use math_audio_iir_fir::{Biquad, BiquadFilterType, peq_butterworth_highpass};
20
21const RMS_WINDOW_MS: f32 = 10.0;
26const MEASURED_MAKEUP_SMOOTHING_MS: f32 = 1000.0;
27const MAX_LOOKAHEAD_MS: f32 = 20.0;
28const DUAL_RELEASE_SLOW_MULTIPLIER: f32 = 4.0;
29
30#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum DynamicsMode {
37 Compress,
38 Expand,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq)]
43pub enum GateState {
44 Open,
45 Hold,
46 Closing,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq)]
51pub enum SidechainFilterMode {
52 Off,
54 Hpf { freq_hz: f32, order_index: usize },
57 Tilt { tilt_db: f32 },
60}
61
62pub struct DynamicsCore {
70 mode: DynamicsMode,
71 channels: usize,
72 sample_rate: u32,
73
74 envelope: Vec<f32>,
76 attack_coeff: f32,
77 release_coeff: f32,
78 attack_ms: f32,
79 release_ms: f32,
80
81 level_detectors: Vec<LevelDetector>,
83 detection_mode_index: usize, sidechain_hpf_biquads: Vec<Vec<Biquad>>,
87 sidechain_hpf_hz: f32,
88 sidechain_hpf_order_index: usize, sidechain_tilt_biquads: Vec<Biquad>,
90 sidechain_tilt_db: f32,
91 sidechain_filter_mode: SidechainFilterMode,
92
93 dual_release: Vec<DualRelease>,
95 program_dependent_release: bool,
96
97 measured_makeup: MeasuredMakeup,
99
100 lookahead_buffer: LookaheadBuffer,
102 lookahead_ms: f32,
103 lookahead_frame_buf: Vec<f32>,
104
105 gate_state: Vec<GateState>,
107 hold_counter: Vec<usize>,
108 hysteresis_db: f32,
109 hold_ms: f32,
110 range_db: f32,
111}
112
113impl DynamicsCore {
114 pub fn new(mode: DynamicsMode, channels: usize, sample_rate: u32) -> Self {
119 let detection_mode = DetectionMode::Peak;
120 let max_lookahead_samples =
121 (MAX_LOOKAHEAD_MS * 0.001 * sample_rate as f32).round() as usize;
122 let attack_ms = 10.0;
123 let release_ms = 100.0;
124
125 let mut core = Self {
126 mode,
127 channels,
128 sample_rate,
129
130 envelope: vec![0.0; channels],
131 attack_coeff: 0.0,
132 release_coeff: 0.0,
133 attack_ms,
134 release_ms,
135
136 level_detectors: (0..channels)
137 .map(|_| LevelDetector::new(detection_mode, sample_rate))
138 .collect(),
139 detection_mode_index: 0,
140
141 sidechain_hpf_biquads: Vec::new(),
142 sidechain_hpf_hz: 0.0,
143 sidechain_hpf_order_index: 0,
144 sidechain_tilt_biquads: Vec::new(),
145 sidechain_tilt_db: 0.0,
146 sidechain_filter_mode: SidechainFilterMode::Off,
147
148 dual_release: (0..channels)
149 .map(|_| {
150 DualRelease::new(
151 release_ms,
152 release_ms * DUAL_RELEASE_SLOW_MULTIPLIER,
153 sample_rate,
154 )
155 })
156 .collect(),
157 program_dependent_release: false,
158
159 measured_makeup: MeasuredMakeup::new(MEASURED_MAKEUP_SMOOTHING_MS, sample_rate),
160
161 lookahead_buffer: LookaheadBuffer::new(max_lookahead_samples.max(1), channels),
162 lookahead_ms: 0.0,
163 lookahead_frame_buf: vec![0.0; channels],
164
165 gate_state: vec![GateState::Open; channels],
166 hold_counter: vec![0; channels],
167 hysteresis_db: 3.0,
168 hold_ms: 50.0,
169 range_db: 40.0,
170 };
171
172 core.attack_coeff = time_to_coeff(attack_ms, sample_rate);
173 core.release_coeff = time_to_coeff(release_ms, sample_rate);
174 core.lookahead_buffer.set_delay(1);
176
177 core
178 }
179
180 pub fn initialize(&mut self, sample_rate: u32) {
184 self.sample_rate = sample_rate;
185
186 self.attack_coeff = time_to_coeff(self.attack_ms, sample_rate);
188 self.release_coeff = time_to_coeff(self.release_ms, sample_rate);
189
190 self.rebuild_sidechain_hpf_internal();
192 self.rebuild_sidechain_tilt_internal();
193
194 let mode = self.detection_mode();
196 self.level_detectors = (0..self.channels)
197 .map(|_| LevelDetector::new(mode, sample_rate))
198 .collect();
199
200 let max_lookahead_samples =
202 (MAX_LOOKAHEAD_MS * 0.001 * sample_rate as f32).round() as usize;
203 self.lookahead_buffer
204 .resize(max_lookahead_samples.max(1), self.channels);
205 if self.lookahead_ms > 0.0 {
206 self.lookahead_buffer
207 .set_delay_ms(self.lookahead_ms, sample_rate);
208 } else {
209 self.lookahead_buffer.set_delay(1);
210 }
211
212 self.dual_release = (0..self.channels)
214 .map(|_| {
215 DualRelease::new(
216 self.release_ms,
217 self.release_ms * DUAL_RELEASE_SLOW_MULTIPLIER,
218 sample_rate,
219 )
220 })
221 .collect();
222
223 self.measured_makeup = MeasuredMakeup::new(MEASURED_MAKEUP_SMOOTHING_MS, sample_rate);
225
226 self.lookahead_frame_buf.resize(self.channels, 0.0);
228 }
229
230 pub fn reset(&mut self) {
233 self.envelope.fill(0.0);
234
235 self.gate_state.fill(GateState::Open);
237 self.hold_counter.fill(0);
238
239 self.rebuild_sidechain_hpf_internal();
241 self.rebuild_sidechain_tilt_internal();
242
243 for det in &mut self.level_detectors {
245 det.reset();
246 }
247
248 self.lookahead_buffer.reset();
250
251 for dr in &mut self.dual_release {
253 dr.reset();
254 }
255
256 self.measured_makeup.reset();
258 }
259
260 pub fn set_attack_release(&mut self, attack_ms: f32, release_ms: f32) {
264 self.attack_ms = attack_ms;
265 self.release_ms = release_ms;
266 self.attack_coeff = time_to_coeff(attack_ms, self.sample_rate);
267 self.release_coeff = time_to_coeff(release_ms, self.sample_rate);
268
269 for dr in &mut self.dual_release {
271 dr.set_times(
272 release_ms,
273 release_ms * DUAL_RELEASE_SLOW_MULTIPLIER,
274 self.sample_rate,
275 );
276 }
277 }
278
279 pub fn set_sidechain_hpf(&mut self, freq_hz: f32, order_index: usize) {
283 self.sidechain_hpf_hz = freq_hz;
284 self.sidechain_hpf_order_index = order_index;
285 self.sidechain_filter_mode = if freq_hz > 0.0 {
286 SidechainFilterMode::Hpf {
287 freq_hz,
288 order_index,
289 }
290 } else {
291 SidechainFilterMode::Off
292 };
293 self.rebuild_sidechain_hpf_internal();
294 self.sidechain_tilt_biquads.clear();
295 self.sidechain_tilt_db = 0.0;
296 }
297
298 pub fn set_sidechain_tilt(&mut self, tilt_db: f32) {
304 self.sidechain_tilt_db = tilt_db;
305 if tilt_db.abs() < 0.01 {
306 self.sidechain_filter_mode = SidechainFilterMode::Off;
307 self.sidechain_tilt_biquads.clear();
308 return;
310 }
311 self.sidechain_filter_mode = SidechainFilterMode::Tilt { tilt_db };
312 self.sidechain_hpf_biquads.clear();
314 self.sidechain_hpf_hz = 0.0;
315 self.rebuild_sidechain_tilt_internal();
316 }
317
318 pub fn set_sidechain_filter(&mut self, mode: SidechainFilterMode) {
320 match mode {
321 SidechainFilterMode::Off => {
322 self.sidechain_hpf_biquads.clear();
323 self.sidechain_hpf_hz = 0.0;
324 self.sidechain_tilt_biquads.clear();
325 self.sidechain_tilt_db = 0.0;
326 self.sidechain_filter_mode = SidechainFilterMode::Off;
327 }
328 SidechainFilterMode::Hpf {
329 freq_hz,
330 order_index,
331 } => {
332 self.set_sidechain_hpf(freq_hz, order_index);
333 }
334 SidechainFilterMode::Tilt { tilt_db } => {
335 self.set_sidechain_tilt(tilt_db);
336 }
337 }
338 }
339
340 pub fn set_detection_mode(&mut self, mode_index: usize) {
342 self.detection_mode_index = mode_index;
343 let mode = self.detection_mode();
344 for det in &mut self.level_detectors {
345 det.set_mode(mode);
346 }
347 }
348
349 pub fn set_lookahead_ms(&mut self, ms: f32) {
351 self.lookahead_ms = ms.clamp(0.0, MAX_LOOKAHEAD_MS);
352 if self.lookahead_ms > 0.0 {
353 self.lookahead_buffer
354 .set_delay_ms(self.lookahead_ms, self.sample_rate);
355 } else {
356 self.lookahead_buffer.set_delay(1);
357 }
358 }
359
360 pub fn set_program_dependent_release(&mut self, enabled: bool) {
362 self.program_dependent_release = enabled;
363 }
364
365 pub fn set_expand_params(&mut self, hysteresis_db: f32, hold_ms: f32, range_db: f32) {
367 self.hysteresis_db = hysteresis_db;
368 self.hold_ms = hold_ms;
369 self.range_db = range_db;
370 }
371
372 pub fn mode(&self) -> DynamicsMode {
374 self.mode
375 }
376
377 pub fn channels(&self) -> usize {
379 self.channels
380 }
381
382 #[inline]
391 pub fn apply_sidechain_filter(&mut self, ch: usize, sample: f32) -> f32 {
392 match self.sidechain_filter_mode {
393 SidechainFilterMode::Off => sample,
394 SidechainFilterMode::Hpf { .. } => {
395 if ch >= self.sidechain_hpf_biquads.len() {
396 return sample;
397 }
398 let biquads: &mut [Biquad] = &mut self.sidechain_hpf_biquads[ch];
399 let mut x = sample as f64;
400 for bq in biquads.iter_mut() {
401 x = bq.process(x);
402 }
403 x as f32
404 }
405 SidechainFilterMode::Tilt { .. } => {
406 if ch >= self.sidechain_tilt_biquads.len() {
407 return sample;
408 }
409 self.sidechain_tilt_biquads[ch].process(sample as f64) as f32
410 }
411 }
412 }
413
414 #[inline]
419 pub fn detect_level(&mut self, ch: usize, sample: f32) -> f32 {
420 if self.detection_mode_index == 0 {
421 sample.abs()
423 } else {
424 self.level_detectors[ch].process_linear(sample)
426 }
427 }
428
429 #[inline]
434 pub fn calculate_gain_reduction(
435 &self,
436 input_db: f32,
437 threshold: f32,
438 ratio: f32,
439 knee_db: f32,
440 ) -> f32 {
441 match self.mode {
442 DynamicsMode::Compress => calculate_compress_gr(input_db, threshold, ratio, knee_db),
443 DynamicsMode::Expand => {
444 calculate_expand_atten(input_db, threshold, ratio, knee_db, self.range_db)
445 }
446 }
447 }
448
449 #[inline]
454 pub fn apply_envelope(&mut self, ch: usize, target_gr: f32) -> f32 {
455 let coeff = if target_gr > self.envelope[ch] {
456 self.attack_coeff
458 } else {
459 match self.mode {
461 DynamicsMode::Compress if self.program_dependent_release => {
462 self.dual_release[ch].process(target_gr)
463 }
464 _ => self.release_coeff,
465 }
466 };
467
468 self.envelope[ch] = target_gr + coeff * (self.envelope[ch] - target_gr);
470 self.envelope[ch]
471 }
472
473 #[inline]
482 pub fn process_gate_state(
483 &mut self,
484 ch: usize,
485 input_db: f32,
486 threshold: f32,
487 ratio: f32,
488 knee_db: f32,
489 ) -> f32 {
490 let hold_samples = (self.hold_ms * 0.001 * self.sample_rate as f32) as usize;
491 let open_th = threshold;
492 let close_th = threshold - self.hysteresis_db;
493
494 match self.gate_state[ch] {
495 GateState::Open => {
496 if input_db < open_th {
497 self.gate_state[ch] = GateState::Hold;
498 self.hold_counter[ch] = hold_samples;
499 }
500 0.0
501 }
502 GateState::Hold => {
503 if input_db >= open_th {
504 self.gate_state[ch] = GateState::Open;
505 self.hold_counter[ch] = 0;
506 0.0
507 } else if self.hold_counter[ch] > 0 {
508 self.hold_counter[ch] -= 1;
509 0.0
510 } else if input_db < close_th {
511 self.gate_state[ch] = GateState::Closing;
512 self.calculate_gain_reduction(input_db, threshold, ratio, knee_db)
513 } else {
514 0.0
515 }
516 }
517 GateState::Closing => {
518 if input_db >= open_th {
519 self.gate_state[ch] = GateState::Open;
520 0.0
521 } else {
522 self.calculate_gain_reduction(input_db, threshold, ratio, knee_db)
523 }
524 }
525 }
526 }
527
528 #[inline]
534 pub fn envelope_db(&self, ch: usize) -> f32 {
535 self.envelope[ch]
536 }
537
538 #[inline]
540 pub fn measured_makeup_db(&self) -> f32 {
541 self.measured_makeup.makeup_db()
542 }
543
544 #[inline]
546 pub fn measured_makeup_linear(&self) -> f32 {
547 self.measured_makeup.makeup_linear()
548 }
549
550 #[inline]
552 pub fn update_measured_makeup(&mut self, gain_reduction: f32) {
553 self.measured_makeup.update(gain_reduction);
554 }
555
556 #[inline]
559 pub fn lookahead_process_frame(&mut self, input: &[f32], output: &mut [f32]) {
560 self.lookahead_buffer.process_frame(input, output);
561 }
562
563 pub fn lookahead_delay_samples(&self) -> usize {
565 if self.lookahead_ms <= 0.0 {
566 return 0;
567 }
568 (self.lookahead_ms * 0.001 * self.sample_rate as f32).round() as usize
569 }
570
571 #[inline]
576 pub fn lookahead_frame_buf(&mut self) -> &mut [f32] {
577 &mut self.lookahead_frame_buf
578 }
579
580 pub fn gate_state(&self, ch: usize) -> GateState {
582 self.gate_state[ch]
583 }
584
585 pub fn range_db(&self) -> f32 {
587 self.range_db
588 }
589
590 fn detection_mode(&self) -> DetectionMode {
595 if self.detection_mode_index == 1 {
596 DetectionMode::Rms {
597 window_ms: RMS_WINDOW_MS,
598 }
599 } else {
600 DetectionMode::Peak
601 }
602 }
603
604 fn rebuild_sidechain_hpf_internal(&mut self) {
605 let fc = self.sidechain_hpf_hz.max(0.0);
606 if fc > 0.0 && self.sample_rate > 0 {
607 let order = match self.sidechain_hpf_order_index {
608 1 => 4,
609 _ => 2,
610 };
611 let peq = peq_butterworth_highpass(order, fc as f64, self.sample_rate as f64);
612 let sections: Vec<Biquad> = peq.into_iter().map(|(_, bq)| bq).collect();
613 self.sidechain_hpf_biquads = (0..self.channels).map(|_| sections.clone()).collect();
614 } else {
615 self.sidechain_hpf_biquads.clear();
616 }
617 }
618
619 fn rebuild_sidechain_tilt_internal(&mut self) {
620 let tilt = self.sidechain_tilt_db;
621 if tilt.abs() < 0.01 || self.sample_rate == 0 {
622 self.sidechain_tilt_biquads.clear();
623 return;
624 }
625 let shelf_freq = 1000.0;
627 let q = 0.707; self.sidechain_tilt_biquads = (0..self.channels)
629 .map(|_| {
630 Biquad::new(
631 BiquadFilterType::Highshelf,
632 shelf_freq,
633 self.sample_rate as f64,
634 q,
635 tilt as f64,
636 )
637 })
638 .collect();
639 }
640}
641
642#[inline]
648fn time_to_coeff(time_ms: f32, sample_rate: u32) -> f32 {
649 if time_ms <= 0.0 {
650 0.0
651 } else {
652 (-1.0 / (time_ms * 0.001 * sample_rate as f32)).exp()
653 }
654}
655
656#[inline]
660fn calculate_compress_gr(input_db: f32, threshold: f32, ratio: f32, knee: f32) -> f32 {
661 let slope = 1.0 - 1.0 / ratio.max(1.0);
662 if knee < 0.1 {
663 if input_db <= threshold {
664 0.0
665 } else {
666 (input_db - threshold) * slope
667 }
668 } else if input_db < threshold - knee / 2.0 {
669 0.0
670 } else if input_db > threshold + knee / 2.0 {
671 (input_db - threshold) * slope
672 } else {
673 let overshoot = input_db - threshold + knee / 2.0;
674 let kf = overshoot / knee;
675 kf * kf * (knee / 2.0) * slope
676 }
677}
678
679#[inline]
684fn calculate_expand_atten(
685 input_db: f32,
686 threshold: f32,
687 ratio: f32,
688 knee: f32,
689 range_db: f32,
690) -> f32 {
691 let slope = 1.0 - 1.0 / ratio.max(1.0);
692 let atten = if knee < 0.1 {
693 if input_db >= threshold {
694 0.0
695 } else {
696 (threshold - input_db) * slope
697 }
698 } else if input_db > threshold + knee / 2.0 {
699 0.0
700 } else if input_db < threshold - knee / 2.0 {
701 (threshold - input_db) * slope
702 } else {
703 let below = threshold + knee / 2.0 - input_db;
704 let kf = below / knee;
705 kf * kf * (knee / 2.0) * slope
706 };
707 atten.min(range_db.max(0.0))
708}
709
710#[cfg(test)]
715mod tests {
716 use super::*;
717
718 const SR: u32 = 48000;
719
720 #[test]
721 fn test_compress_gain_reduction() {
722 let core = DynamicsCore::new(DynamicsMode::Compress, 2, SR);
723
724 let gr = core.calculate_gain_reduction(-30.0, -20.0, 4.0, 0.0);
726 assert_eq!(gr, 0.0);
727
728 let gr = core.calculate_gain_reduction(-20.0, -20.0, 4.0, 0.0);
730 assert_eq!(gr, 0.0);
731
732 let gr = core.calculate_gain_reduction(-8.0, -20.0, 4.0, 0.0);
735 assert!((gr - 9.0).abs() < 0.01, "expected ~9.0, got {gr}");
736
737 let gr = core.calculate_gain_reduction(-20.0, -20.0, 4.0, 6.0);
739 assert!(gr > 0.0 && gr < 3.0, "knee GR should be moderate, got {gr}");
742 }
743
744 #[test]
745 fn test_expand_attenuation() {
746 let mut core = DynamicsCore::new(DynamicsMode::Expand, 1, SR);
747 core.set_expand_params(3.0, 50.0, 40.0);
748
749 let atten = core.calculate_gain_reduction(-10.0, -20.0, 4.0, 0.0);
751 assert_eq!(atten, 0.0);
752
753 let atten = core.calculate_gain_reduction(-20.0, -20.0, 4.0, 0.0);
755 assert_eq!(atten, 0.0);
756
757 let atten = core.calculate_gain_reduction(-32.0, -20.0, 4.0, 0.0);
760 assert!((atten - 9.0).abs() < 0.01, "expected ~9.0, got {atten}");
761
762 let atten = core.calculate_gain_reduction(-80.0, -20.0, 4.0, 0.0);
765 assert!(
766 (atten - 40.0).abs() < 0.01,
767 "expected range cap at 40.0, got {atten}"
768 );
769 }
770
771 #[test]
772 fn test_envelope_attack_release() {
773 let mut core = DynamicsCore::new(DynamicsMode::Compress, 1, SR);
774 core.set_attack_release(1.0, 50.0); let mut env = 0.0f32;
778 for _ in 0..480 {
779 env = core.apply_envelope(0, 10.0);
781 }
782 assert!(
784 env > 9.0,
785 "after 10ms attack (1ms time constant), envelope should be near 10.0, got {env}"
786 );
787
788 for _ in 0..24000 {
790 env = core.apply_envelope(0, 0.0);
792 }
793 assert!(
795 env < 0.1,
796 "after 500ms release (50ms time constant), envelope should be near 0, got {env}"
797 );
798 }
799
800 #[test]
801 fn test_gate_state_machine() {
802 let mut core = DynamicsCore::new(DynamicsMode::Expand, 1, SR);
803 core.set_expand_params(3.0, 0.0, 40.0); core.set_attack_release(0.1, 50.0);
805
806 let threshold = -20.0;
807 let ratio = 4.0;
808 let knee = 0.0;
809
810 assert_eq!(core.gate_state(0), GateState::Open);
812
813 let atten = core.process_gate_state(0, -10.0, threshold, ratio, knee);
815 assert_eq!(atten, 0.0);
816 assert_eq!(core.gate_state(0), GateState::Open);
817
818 let atten = core.process_gate_state(0, -25.0, threshold, ratio, knee);
820 assert_eq!(atten, 0.0); assert_eq!(core.gate_state(0), GateState::Hold);
822
823 let atten = core.process_gate_state(0, -25.0, threshold, ratio, knee);
826 assert!(atten > 0.0, "should be expanding now, got {atten}");
827 assert_eq!(core.gate_state(0), GateState::Closing);
828
829 let atten = core.process_gate_state(0, -10.0, threshold, ratio, knee);
831 assert_eq!(atten, 0.0);
832 assert_eq!(core.gate_state(0), GateState::Open);
833 }
834
835 #[test]
836 fn test_sidechain_hpf() {
837 let mut core = DynamicsCore::new(DynamicsMode::Compress, 1, SR);
838 core.set_sidechain_hpf(200.0, 0); let mut low_energy = 0.0f32;
842 let freq = 50.0;
843 for i in 0..SR {
844 let sample = (2.0 * std::f32::consts::PI * freq * i as f32 / SR as f32).sin();
845 let filtered = core.apply_sidechain_filter(0, sample);
846 low_energy += filtered * filtered;
847 }
848
849 core.set_sidechain_hpf(200.0, 0);
851
852 let mut high_energy = 0.0f32;
854 let freq = 1000.0;
855 for i in 0..SR {
856 let sample = (2.0 * std::f32::consts::PI * freq * i as f32 / SR as f32).sin();
857 let filtered = core.apply_sidechain_filter(0, sample);
858 high_energy += filtered * filtered;
859 }
860
861 assert!(
862 high_energy > low_energy * 10.0,
863 "HPF should strongly attenuate 50Hz vs 1kHz: low={low_energy}, high={high_energy}"
864 );
865 }
866
867 #[test]
868 fn test_detection_peak_vs_rms() {
869 let mut core = DynamicsCore::new(DynamicsMode::Compress, 1, SR);
870
871 core.set_detection_mode(0);
873 let peak_level = core.detect_level(0, 0.5);
874 assert!((peak_level - 0.5).abs() < 0.001, "peak should be 0.5");
875
876 let peak_neg = core.detect_level(0, -0.5);
878 assert!(
879 (peak_neg - 0.5).abs() < 0.001,
880 "peak of negative should be 0.5"
881 );
882
883 core.set_detection_mode(1);
885
886 let window_len = (RMS_WINDOW_MS * 0.001 * SR as f32).round() as usize;
888 let mut rms_level = 0.0f32;
889 for _ in 0..window_len + 1 {
890 rms_level = core.detect_level(0, 0.5);
891 }
892 assert!(
894 (rms_level - 0.5).abs() < 0.05,
895 "RMS of constant 0.5 should be ~0.5, got {rms_level}"
896 );
897
898 core.set_detection_mode(0);
900 let peak_half = core.detect_level(0, 1.0);
901
902 core.set_detection_mode(1);
903 for i in 0..window_len + 1 {
905 let sample = if i % 2 == 0 { 1.0 } else { 0.0 };
906 rms_level = core.detect_level(0, sample);
907 }
908 assert!(
911 rms_level < peak_half,
912 "RMS should be less than peak for alternating signal: rms={rms_level}, peak={peak_half}"
913 );
914 }
915
916 #[test]
917 fn test_no_allocations_in_hot_path() {
918 let mut core = DynamicsCore::new(DynamicsMode::Compress, 2, SR);
922 core.set_sidechain_hpf(100.0, 0);
923 core.set_detection_mode(0);
924 core.set_attack_release(5.0, 50.0);
925
926 for i in 0..10000 {
928 let sample = (i as f32 * 0.01).sin();
929 let ch = i % 2;
930
931 let filtered = core.apply_sidechain_filter(ch, sample);
932 let level = core.detect_level(ch, filtered);
933
934 let input_db = if level < 1e-10 {
935 -120.0
936 } else {
937 20.0 * level.log10()
938 };
939
940 let gr = core.calculate_gain_reduction(input_db, -20.0, 4.0, 6.0);
941 let _env = core.apply_envelope(ch, gr);
942 }
943
944 assert!(core.envelope_db(0).is_finite());
947 assert!(core.envelope_db(1).is_finite());
948 }
949
950 #[test]
951 fn test_lookahead_process_frame() {
952 let mut core = DynamicsCore::new(DynamicsMode::Compress, 2, SR);
953 core.set_lookahead_ms(5.0); let delay = core.lookahead_delay_samples();
956 assert_eq!(delay, 240);
957
958 let mut output = vec![0.0f32; 2];
960 for frame in 0..240 {
961 let input = [frame as f32, (frame as f32) * 10.0];
962 core.lookahead_process_frame(&input, &mut output);
963 assert_eq!(output[0], 0.0);
965 assert_eq!(output[1], 0.0);
966 }
967
968 let input = [240.0, 2400.0];
970 core.lookahead_process_frame(&input, &mut output);
971 assert!((output[0] - 0.0).abs() < 0.001);
972 assert!((output[1] - 0.0).abs() < 0.001);
973
974 let input = [241.0, 2410.0];
976 core.lookahead_process_frame(&input, &mut output);
977 assert!((output[0] - 1.0).abs() < 0.001);
978 assert!((output[1] - 10.0).abs() < 0.001);
979 }
980
981 #[test]
982 fn test_reset_clears_state() {
983 let mut core = DynamicsCore::new(DynamicsMode::Expand, 2, SR);
984 core.set_expand_params(3.0, 50.0, 40.0);
985
986 for _ in 0..1000 {
988 core.apply_envelope(0, 10.0);
989 core.apply_envelope(1, 5.0);
990 }
991 assert!(core.envelope_db(0) > 0.0);
992 assert!(core.envelope_db(1) > 0.0);
993
994 core.reset();
995
996 assert_eq!(core.envelope_db(0), 0.0);
997 assert_eq!(core.envelope_db(1), 0.0);
998 assert_eq!(core.gate_state(0), GateState::Open);
999 assert_eq!(core.gate_state(1), GateState::Open);
1000 }
1001
1002 #[test]
1003 fn test_measured_makeup() {
1004 let mut core = DynamicsCore::new(DynamicsMode::Compress, 1, SR);
1005
1006 for _ in 0..480000 {
1008 core.update_measured_makeup(6.0);
1009 }
1010 let makeup_db = core.measured_makeup_db();
1011 assert!(
1012 (makeup_db - 6.0).abs() < 0.1,
1013 "measured makeup should converge to ~6dB, got {makeup_db}"
1014 );
1015
1016 core.reset();
1018 assert!(core.measured_makeup_db().abs() < 0.01);
1019 }
1020
1021 #[test]
1022 fn test_expand_with_gate_state_machine_and_envelope() {
1023 let mut core = DynamicsCore::new(DynamicsMode::Expand, 1, SR);
1025 core.set_expand_params(3.0, 0.0, 40.0);
1026 core.set_attack_release(0.1, 10.0);
1027
1028 let threshold = -20.0;
1029 let ratio = 4.0;
1030 let knee = 0.0;
1031
1032 for _ in 0..4800 {
1034 let target = core.process_gate_state(0, -40.0, threshold, ratio, knee);
1035 core.apply_envelope(0, target);
1036 }
1037
1038 let env = core.envelope_db(0);
1040 assert!(
1041 env > 5.0,
1042 "after sustained below-threshold signal, envelope should show attenuation, got {env}"
1043 );
1044
1045 for _ in 0..4800 {
1047 let target = core.process_gate_state(0, -10.0, threshold, ratio, knee);
1048 core.apply_envelope(0, target);
1049 }
1050
1051 let env = core.envelope_db(0);
1052 assert!(
1053 env < 0.5,
1054 "after above-threshold signal, envelope should recover, got {env}"
1055 );
1056 }
1057}