614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1 | //! two-tier true-peak limiter: parallel soft (dynamic psr ceiling) + hard (absolute contract) tiers. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 2 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 3 | use crate::delay::DelayLine; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 4 | use crate::envelope::AttackRelease; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 5 | use crate::oversample::{design_lowpass_blackman, PolyphaseDownsampler, PolyphaseUpsampler}; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 6 | use crate::sliding_max::SlidingMaxBuffer; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 7 | use crate::util::{db_to_lin, lin_to_db, time_to_alpha}; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 8 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 9 | /// soft tier targets a dynamic ceiling `program_loudness_lufs + max_psr_db`; bounds the |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 10 | /// peak-to-loudness ratio, not a safety contract (that's the hard tier). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 11 | #[derive(Debug, Clone, Copy, PartialEq)] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 12 | pub struct SoftTierConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 13 | /// max peak-to-shortterm-loudness ratio (dB). effective ceiling = `program_lufs + max_psr_db`. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 14 | pub max_psr_db: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 15 | /// fallback ceiling (dBTP) when no program loudness yet (startup, before first agc window). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 16 | pub static_ceiling_dbtp: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 17 | pub attack_ms: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 18 | pub release_ms: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 19 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 20 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 21 | impl Default for SoftTierConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 22 | fn default() -> Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 23 | Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 24 | max_psr_db: 14.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 25 | static_ceiling_dbtp: -6.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 26 | attack_ms: 5.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 27 | release_ms: 200.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 28 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 29 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 30 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 31 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 32 | impl SoftTierConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 33 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 34 | pub fn sanitized(mut self) -> Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 35 | if self.static_ceiling_dbtp > 0.0 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 36 | self.static_ceiling_dbtp = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 37 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 38 | if !self.max_psr_db.is_finite() || self.max_psr_db < 0.0 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 39 | self.max_psr_db = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 40 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 41 | if self.attack_ms < 0.0 || !self.attack_ms.is_finite() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 42 | self.attack_ms = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 43 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 44 | if self.release_ms < 0.0 || !self.release_ms.is_finite() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 45 | self.release_ms = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 46 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 47 | self |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 48 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 49 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 50 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 51 | #[derive(Debug, Clone, Copy, PartialEq)] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 52 | pub struct LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 53 | /// hard-tier output ceiling (dBTP). must be `<= 0.0`. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 54 | pub ceiling_dbtp: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 55 | /// lookahead (ms); sets delay-line length + peak-window size. shared by both tiers. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 56 | pub lookahead_ms: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 57 | /// hard-tier exponential release toward unity (ms). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 58 | pub release_ms: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 59 | /// hard-tier hold after a reduction before release begins (ms). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 60 | pub hold_ms: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 61 | /// oversample factor. 1 disables isp detection; 4 is the bs.1770-4 reference. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 62 | pub oversample: usize, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 63 | /// fir taps for the oversampling filter (odd). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 64 | pub fir_taps: usize, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 65 | /// `None` disables the soft tier → pure brickwall. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 66 | pub soft: Option<SoftTierConfig>, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 67 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 68 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 69 | impl Default for LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 70 | fn default() -> Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 71 | Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 72 | ceiling_dbtp: -0.1, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 73 | lookahead_ms: 2.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 74 | release_ms: 80.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 75 | hold_ms: 5.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 76 | oversample: 4, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 77 | fir_taps: 31, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 78 | soft: Some(SoftTierConfig::default()), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 79 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 80 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 81 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 82 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 83 | /// internal-rate cap (Hz). detector upsamples to `sample_rate × oversample`; above ~192 kHz |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 84 | /// fir cost rises linearly with no isp-detection gain, so drop oversample on high base rates. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 85 | pub const MAX_INTERNAL_RATE_HZ: f32 = 192_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 86 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 87 | impl LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 88 | /// rate-agnostic. callers that know the sample rate should prefer [`Self::sanitize_for_rate`] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 89 | /// so oversample scales down on high-rate inputs. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 90 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 91 | pub fn sanitized(mut self) -> Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 92 | if self.ceiling_dbtp > 0.0 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 93 | self.ceiling_dbtp = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 94 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 95 | self.oversample = self.oversample.clamp(1, 8); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 96 | if self.fir_taps < 5 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 97 | self.fir_taps = 5; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 98 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 99 | if self.fir_taps % 2 == 0 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 100 | self.fir_taps += 1; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 101 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 102 | if let Some(soft) = self.soft { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 103 | self.soft = Some(soft.sanitized()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 104 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 105 | self |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 106 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 107 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 108 | /// caps oversample so post-upsample rate stays ≤ [`MAX_INTERNAL_RATE_HZ`]; always ≥ 1. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 109 | /// at default 4×: 48 kHz → 4×, 96 kHz → 2×, 192 kHz → 1×. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 110 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 111 | pub fn sanitize_for_rate(self, sample_rate: f32) -> Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 112 | let mut s = self.sanitized(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 113 | if sample_rate > 0.0 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 114 | let max_os = |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 115 | (MAX_INTERNAL_RATE_HZ / sample_rate).floor().max(1.0) as usize; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 116 | if s.oversample > max_os { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 117 | s.oversample = max_os; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 118 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 119 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 120 | s |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 121 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 122 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 123 | /// brickwall only (no soft tier). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 124 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 125 | pub fn brickwall_only() -> Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 126 | Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 127 | soft: None, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 128 | ..Self::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 129 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 130 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 131 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 132 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 133 | const MAX_OVERSAMPLE: usize = 8; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 134 | const SILENCE_EPS: f32 = 1.0e-20; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 135 | const OUTPUT_IDLE_EPS: f32 = 1.0e-12; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 136 | const GAIN_IDLE_EPS: f32 = 1.0e-4; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 137 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 138 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 139 | pub enum SetConfigOutcome { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 140 | /// applied in place. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 141 | Applied, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 142 | /// differs in `oversample`/`fir_taps`/`lookahead_ms` (in samples) → would realloc buffers. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 143 | /// limiter unchanged; rebuild from `Limiter::new` on the control thread. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 144 | StructuralChange, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 145 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 146 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 147 | pub struct Limiter { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 148 | cfg: LimiterConfig, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 149 | /// captured at construction so try_set_config can recompute time coeffs without a repass. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 150 | sample_rate: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 151 | ceiling_lin: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 152 | os: usize, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 153 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 154 | // Per-channel oversampler / downsampler / delay-line paths. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 155 | up_l: PolyphaseUpsampler, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 156 | up_r: PolyphaseUpsampler, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 157 | down_l: PolyphaseDownsampler, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 158 | down_r: PolyphaseDownsampler, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 159 | delay_l: DelayLine, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 160 | delay_r: DelayLine, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 161 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 162 | /// sliding-window peak (oversampled domain), shared across channels + tiers. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 163 | peak_buf: SlidingMaxBuffer, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 164 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 165 | // ---- Hard tier state (instant attack + hold + release) ---- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 166 | hard_gain: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 167 | hold_remaining: u32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 168 | hold_samples_os: u32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 169 | hard_release_alpha: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 170 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 171 | // ---- Soft tier state (smooth envelope) ---- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 172 | soft_envelope: Option<AttackRelease>, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 173 | soft_max_psr_db: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 174 | soft_static_ceiling_lin: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 175 | program_loudness_lufs: Option<f32>, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 176 | /// effective soft ceiling (linear), recomputed when program loudness changes. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 177 | soft_ceiling_lin: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 178 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 179 | // scratch buffers (sized for max oversample factor). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 180 | up_buf_l: [f32; MAX_OVERSAMPLE], |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 181 | up_buf_r: [f32; MAX_OVERSAMPLE], |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 182 | gained_buf_l: [f32; MAX_OVERSAMPLE], |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 183 | gained_buf_r: [f32; MAX_OVERSAMPLE], |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 184 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 185 | // telemetry (sampled per frame). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 186 | last_peak_lin: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 187 | last_gr_db: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 188 | last_soft_gr_db: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 189 | last_hard_gr_db: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 190 | last_output_lin: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 191 | idle_drain_frames: usize, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 192 | silent_drain_remaining: usize, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 193 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 194 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 195 | impl Limiter { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 196 | /// allocates fir coeffs, polyphase tables, delay buffers. not rt-safe. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 197 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 198 | pub fn new(cfg: LimiterConfig, sample_rate: f32) -> Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 199 | let cfg = cfg.sanitized(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 200 | let os = cfg.oversample; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 201 | let lowpass = if os > 1 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 202 | design_lowpass_blackman(cfg.fir_taps, 0.45 / os as f32) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 203 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 204 | vec![1.0] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 205 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 206 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 207 | let os_rate = sample_rate * os as f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 208 | let lookahead_samples_os = (cfg.lookahead_ms * 1e-3 * os_rate).round() as usize; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 209 | let lookahead_samples_os = lookahead_samples_os.max(1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 210 | let lookahead_frames = lookahead_samples_os.div_ceil(os); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 211 | let idle_drain_frames = lookahead_frames + cfg.fir_taps * 2 + 2; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 212 | let hold_samples_os = (cfg.hold_ms * 1e-3 * os_rate).round() as u32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 213 | let hard_release_alpha = time_to_alpha(cfg.release_ms, os_rate); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 214 | let ceiling_lin = db_to_lin(cfg.ceiling_dbtp); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 215 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 216 | let (soft_envelope, soft_max_psr_db, soft_static_ceiling_lin, soft_ceiling_lin) = |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 217 | if let Some(soft) = cfg.soft { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 218 | let env = AttackRelease::new(soft.attack_ms, soft.release_ms, os_rate); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 219 | let static_ceiling_lin = db_to_lin(soft.static_ceiling_dbtp); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 220 | ( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 221 | Some(env), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 222 | soft.max_psr_db, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 223 | static_ceiling_lin, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 224 | static_ceiling_lin, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 225 | ) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 226 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 227 | (None, 0.0, 1.0, 1.0) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 228 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 229 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 230 | let mut me = Self { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 231 | cfg, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 232 | sample_rate, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 233 | ceiling_lin, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 234 | os, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 235 | up_l: PolyphaseUpsampler::new(os, &lowpass), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 236 | up_r: PolyphaseUpsampler::new(os, &lowpass), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 237 | down_l: PolyphaseDownsampler::new(os, &lowpass), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 238 | down_r: PolyphaseDownsampler::new(os, &lowpass), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 239 | delay_l: DelayLine::new(lookahead_samples_os), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 240 | delay_r: DelayLine::new(lookahead_samples_os), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 241 | peak_buf: SlidingMaxBuffer::new(lookahead_samples_os), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 242 | hard_gain: 1.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 243 | hold_remaining: 0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 244 | hold_samples_os, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 245 | hard_release_alpha, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 246 | soft_envelope, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 247 | soft_max_psr_db, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 248 | soft_static_ceiling_lin, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 249 | program_loudness_lufs: None, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 250 | soft_ceiling_lin, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 251 | up_buf_l: [0.0; MAX_OVERSAMPLE], |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 252 | up_buf_r: [0.0; MAX_OVERSAMPLE], |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 253 | gained_buf_l: [0.0; MAX_OVERSAMPLE], |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 254 | gained_buf_r: [0.0; MAX_OVERSAMPLE], |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 255 | last_peak_lin: 0.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 256 | last_gr_db: 0.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 257 | last_soft_gr_db: 0.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 258 | last_hard_gr_db: 0.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 259 | last_output_lin: 0.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 260 | idle_drain_frames, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 261 | silent_drain_remaining: 0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 262 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 263 | // seed soft envelope to unity: no phantom gain reduction on the first frames. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 264 | if let Some(env) = &mut me.soft_envelope { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 265 | env.reset(1.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 266 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 267 | me |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 268 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 269 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 270 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 271 | pub fn config(&self) -> LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 272 | self.cfg |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 273 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 274 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 275 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 276 | pub fn ceiling_dbtp(&self) -> f32 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 277 | self.cfg.ceiling_dbtp |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 278 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 279 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 280 | /// applied total reduction `min(soft_gain, hard_gain)` (dB, negative when limiting). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 281 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 282 | pub fn gain_reduction_db(&self) -> f32 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 283 | self.last_gr_db |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 284 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 285 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 286 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 287 | pub fn soft_gain_reduction_db(&self) -> f32 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 288 | self.last_soft_gr_db |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 289 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 290 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 291 | /// non-zero ⇒ soft tier didn't hold the ceiling and the brickwall engaged. routinely |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 292 | /// non-zero on benign material ⇒ soft under-configured (psr too high / attack too slow / |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 293 | /// lookahead too short). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 294 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 295 | pub fn hard_gain_reduction_db(&self) -> f32 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 296 | self.last_hard_gr_db |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 297 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 298 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 299 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 300 | pub fn true_peak_dbtp(&self) -> f32 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 301 | lin_to_db(self.last_peak_lin.max(1e-20)) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 302 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 303 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 304 | /// `program_loudness_lufs + soft.max_psr_db` when both known, else `static_ceiling_dbtp`. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 305 | /// `None` if soft tier disabled. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 306 | #[must_use] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 307 | pub fn effective_soft_ceiling_dbtp(&self) -> Option<f32> { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 308 | self.cfg.soft.map(|_| lin_to_db(self.soft_ceiling_lin)) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 309 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 310 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 311 | /// live-update non-structural params (ceiling, hard release/hold, soft toggle + scalars). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 312 | /// allocation-free, rt-safe. structural changes return [`SetConfigOutcome::StructuralChange`] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 313 | /// and leave the limiter unchanged — see that variant. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 314 | pub fn try_set_config(&mut self, cfg: LimiterConfig) -> SetConfigOutcome { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 315 | let cfg = cfg.sanitized(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 316 | let os_rate = self.sample_rate * cfg.oversample as f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 317 | let new_lookahead_samples_os = |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 318 | ((cfg.lookahead_ms * 1e-3 * os_rate).round() as usize).max(1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 319 | let cur_lookahead_samples_os = self.peak_buf.window(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 320 | if cfg.oversample != self.os |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 321 | || cfg.fir_taps != self.cfg.fir_taps |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 322 | || new_lookahead_samples_os != cur_lookahead_samples_os |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 323 | { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 324 | return SetConfigOutcome::StructuralChange; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 325 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 326 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 327 | self.ceiling_lin = db_to_lin(cfg.ceiling_dbtp); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 328 | self.hard_release_alpha = time_to_alpha(cfg.release_ms, os_rate); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 329 | self.hold_samples_os = (cfg.hold_ms * 1e-3 * os_rate).round() as u32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 330 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 331 | match (cfg.soft, self.cfg.soft) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 332 | (Some(new_soft), Some(_old_soft)) => { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 333 | self.soft_max_psr_db = new_soft.max_psr_db; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 334 | self.soft_static_ceiling_lin = db_to_lin(new_soft.static_ceiling_dbtp); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 335 | if let Some(env) = &mut self.soft_envelope { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 336 | env.set_times(new_soft.attack_ms, new_soft.release_ms, os_rate); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 337 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 338 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 339 | (Some(new_soft), None) => { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 340 | // re-enable: seed envelope to unity, no phantom gain reduction. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 341 | let mut env = AttackRelease::new(new_soft.attack_ms, new_soft.release_ms, os_rate); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 342 | env.reset(1.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 343 | self.soft_envelope = Some(env); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 344 | self.soft_max_psr_db = new_soft.max_psr_db; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 345 | self.soft_static_ceiling_lin = db_to_lin(new_soft.static_ceiling_dbtp); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 346 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 347 | (None, Some(_)) => { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 348 | self.soft_envelope = None; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 349 | self.soft_max_psr_db = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 350 | self.soft_static_ceiling_lin = 1.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 351 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 352 | (None, None) => {} |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 353 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 354 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 355 | self.cfg = cfg; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 356 | self.recompute_soft_ceiling(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 357 | SetConfigOutcome::Applied |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 358 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 359 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 360 | /// program loudness for the dynamic soft ceiling; called by the agc at tick rate. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 361 | /// non-finite ignored. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 362 | pub fn set_program_loudness_lufs(&mut self, lufs: f32) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 363 | if !lufs.is_finite() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 364 | return; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 365 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 366 | self.program_loudness_lufs = Some(lufs); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 367 | self.recompute_soft_ceiling(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 368 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 369 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 370 | /// forget program loudness; soft tier falls back to its static ceiling. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 371 | pub fn clear_program_loudness(&mut self) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 372 | self.program_loudness_lufs = None; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 373 | self.recompute_soft_ceiling(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 374 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 375 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 376 | fn recompute_soft_ceiling(&mut self) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 377 | self.soft_ceiling_lin = match (self.cfg.soft, self.program_loudness_lufs) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 378 | (Some(_), Some(lufs)) => { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 379 | let dynamic_dbtp = (lufs + self.soft_max_psr_db).min(0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 380 | db_to_lin(dynamic_dbtp) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 381 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 382 | (Some(_), None) => self.soft_static_ceiling_lin, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 383 | (None, _) => 1.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 384 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 385 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 386 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 387 | /// allocation-free. output guaranteed within `±ceiling_dbtp` (the hard contract). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 388 | pub fn process_frame(&mut self, left: f32, right: f32) -> (f32, f32) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 389 | // sanitize NaN/Inf to zero; never let garbage into limiter state. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 390 | let left = if left.is_finite() { left } else { 0.0 }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 391 | let right = if right.is_finite() { right } else { 0.0 }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 392 | let input_silent = left.abs() <= SILENCE_EPS && right.abs() <= SILENCE_EPS; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 393 | if input_silent && self.signal_path_idle() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 394 | self.last_peak_lin = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 395 | self.last_gr_db = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 396 | self.last_soft_gr_db = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 397 | self.last_hard_gr_db = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 398 | self.last_output_lin = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 399 | return (0.0, 0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 400 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 401 | if input_silent { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 402 | self.silent_drain_remaining = self.silent_drain_remaining.saturating_sub(1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 403 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 404 | self.silent_drain_remaining = self.idle_drain_frames; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 405 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 406 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 407 | self.up_l.process(left, &mut self.up_buf_l[..self.os]); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 408 | self.up_r.process(right, &mut self.up_buf_r[..self.os]); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 409 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 410 | let mut frame_peak = 0.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 411 | let mut min_soft_gain = 1.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 412 | let mut min_total_gain = 1.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 413 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 414 | for k in 0..self.os { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 415 | let s_l = self.up_buf_l[k]; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 416 | let s_r = self.up_buf_r[k]; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 417 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 418 | let peak = s_l.abs().max(s_r.abs()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 419 | frame_peak = frame_peak.max(peak); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 420 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 421 | let window_peak = self.peak_buf.push_and_max(peak); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 422 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 423 | // ---- Soft tier -------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 424 | let soft_gain = if let Some(env) = &mut self.soft_envelope { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 425 | let target = if window_peak > self.soft_ceiling_lin && window_peak > 1e-20 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 426 | self.soft_ceiling_lin / window_peak |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 427 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 428 | 1.0 |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 429 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 430 | env.process_gain(target) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 431 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 432 | 1.0 |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 433 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 434 | if soft_gain < min_soft_gain { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 435 | min_soft_gain = soft_gain; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 436 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 437 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 438 | // ---- Hard tier -------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 439 | // size hard gain against the peak *after* the soft tier acts, so it doesn't do |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 440 | // redundant work. predicted_post_soft = max(immediate: current soft gain applied; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 441 | // asymptotic: soft converged to target) — the larger/more conservative. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 442 | let predicted_post_soft = if self.soft_envelope.is_some() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 443 | let asymptotic = window_peak.min(self.soft_ceiling_lin); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 444 | let immediate = window_peak * soft_gain; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 445 | asymptotic.max(immediate) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 446 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 447 | window_peak |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 448 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 449 | let hard_target = |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 450 | if predicted_post_soft > self.ceiling_lin && predicted_post_soft > 1e-20 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 451 | self.ceiling_lin / predicted_post_soft |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 452 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 453 | 1.0 |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 454 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 455 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 456 | if hard_target < self.hard_gain { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 457 | self.hard_gain = hard_target; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 458 | self.hold_remaining = self.hold_samples_os; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 459 | } else if self.hold_remaining > 0 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 460 | self.hold_remaining -= 1; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 461 | } else { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 462 | self.hard_gain += self.hard_release_alpha * (hard_target - self.hard_gain); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 463 | if self.hard_gain > hard_target { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 464 | self.hard_gain = hard_target; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 465 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 466 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 467 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 468 | // ---- Combine ---------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 469 | let total_gain = soft_gain.min(self.hard_gain); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 470 | if total_gain < min_total_gain { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 471 | min_total_gain = total_gain; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 472 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 473 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 474 | let d_l = self.delay_l.push_pop(s_l); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 475 | let d_r = self.delay_r.push_pop(s_r); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 476 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 477 | let mut out_l = d_l * total_gain; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 478 | let mut out_r = d_r * total_gain; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 479 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 480 | // defense-in-depth #1: clip in the oversampled domain so overshoots can't enter |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 481 | // the downsampler. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 482 | out_l = out_l.clamp(-self.ceiling_lin, self.ceiling_lin); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 483 | out_r = out_r.clamp(-self.ceiling_lin, self.ceiling_lin); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 484 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 485 | self.gained_buf_l[k] = out_l; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 486 | self.gained_buf_r[k] = out_r; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 487 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 488 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 489 | let mut out_l = self.down_l.process(&self.gained_buf_l[..self.os]); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 490 | let mut out_r = self.down_r.process(&self.gained_buf_r[..self.os]); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 491 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 492 | // defense-in-depth #2: clip post-downsample, guarding against fir ringing nudging |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 493 | // the output above the ceiling. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 494 | out_l = out_l.clamp(-self.ceiling_lin, self.ceiling_lin); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 495 | out_r = out_r.clamp(-self.ceiling_lin, self.ceiling_lin); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 496 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 497 | self.last_peak_lin = frame_peak; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 498 | self.last_soft_gr_db = lin_to_db(min_soft_gain.max(1e-12)); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 499 | self.last_hard_gr_db = lin_to_db(self.hard_gain.max(1e-12)); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 500 | self.last_gr_db = lin_to_db(min_total_gain.max(1e-12)); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 501 | self.last_output_lin = out_l.abs().max(out_r.abs()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 502 | if input_silent |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 503 | && self.silent_drain_remaining == 0 |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 504 | && self.last_output_lin <= OUTPUT_IDLE_EPS |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 505 | && self.gain_state_idle() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 506 | { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 507 | self.reset_signal_path(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 508 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 509 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 510 | (out_l, out_r) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 511 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 512 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 513 | /// process an interleaved stereo buffer in place. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 514 | pub fn process_interleaved_stereo(&mut self, buf: &mut [f32]) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 515 | debug_assert!(buf.len() % 2 == 0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 516 | for frame in buf.chunks_exact_mut(2) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 517 | let (l, r) = self.process_frame(frame[0], frame[1]); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 518 | frame[0] = l; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 519 | frame[1] = r; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 520 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 521 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 522 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 523 | /// reset all internal state; program loudness is also cleared. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 524 | pub fn reset(&mut self) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 525 | self.reset_signal_path(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 526 | self.program_loudness_lufs = None; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 527 | self.recompute_soft_ceiling(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 528 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 529 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 530 | fn reset_signal_path(&mut self) { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 531 | self.up_l.reset(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 532 | self.up_r.reset(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 533 | self.down_l.reset(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 534 | self.down_r.reset(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 535 | self.delay_l.reset(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 536 | self.delay_r.reset(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 537 | self.peak_buf.reset(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 538 | self.hard_gain = 1.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 539 | self.hold_remaining = 0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 540 | if let Some(env) = &mut self.soft_envelope { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 541 | env.reset(1.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 542 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 543 | self.last_peak_lin = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 544 | self.last_gr_db = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 545 | self.last_soft_gr_db = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 546 | self.last_hard_gr_db = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 547 | self.last_output_lin = 0.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 548 | self.silent_drain_remaining = 0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 549 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 550 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 551 | fn signal_path_idle(&self) -> bool { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 552 | self.silent_drain_remaining == 0 |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 553 | && self.last_peak_lin <= OUTPUT_IDLE_EPS |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 554 | && self.last_output_lin <= OUTPUT_IDLE_EPS |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 555 | && self.gain_state_idle() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 556 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 557 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 558 | fn gain_state_idle(&self) -> bool { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 559 | self.hold_remaining == 0 |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 560 | && (self.hard_gain - 1.0).abs() <= GAIN_IDLE_EPS |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 561 | && self |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 562 | .soft_envelope |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 563 | .as_ref() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 564 | .is_none_or(|env| (env.state() - 1.0).abs() <= GAIN_IDLE_EPS) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 565 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 566 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 567 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 568 | #[cfg(test)] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 569 | mod tests { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 570 | use super::*; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 571 | use std::f32::consts::PI; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 572 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 573 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 574 | // sanitize_for_rate: oversample factor scales down so the |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 575 | // internal (post-upsample) rate stays bounded. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 576 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 577 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 578 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 579 | fn sanitize_for_rate_caps_oversample_at_internal_192k() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 580 | // Default config has oversample = 4. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 581 | let default = LimiterConfig::default(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 582 | assert_eq!(default.oversample, 4); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 583 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 584 | // At 48 kHz: 4× = 192 kHz, at the cap, untouched. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 585 | assert_eq!(default.sanitize_for_rate(48_000.0).oversample, 4); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 586 | // At 44.1 kHz: 4× = 176.4 kHz, under the cap. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 587 | assert_eq!(default.sanitize_for_rate(44_100.0).oversample, 4); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 588 | // At 96 kHz: 4× = 384 kHz, exceeds; drop to 2× = 192 kHz. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 589 | assert_eq!(default.sanitize_for_rate(96_000.0).oversample, 2); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 590 | // At 192 kHz: cap forces oversample = 1. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 591 | assert_eq!(default.sanitize_for_rate(192_000.0).oversample, 1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 592 | // Pathological rate above the cap still leaves at least 1. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 593 | assert_eq!(default.sanitize_for_rate(384_000.0).oversample, 1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 594 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 595 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 596 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 597 | fn sanitize_for_rate_preserves_user_lower_oversample() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 598 | // User who explicitly set oversample = 2 at 48 kHz should |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 599 | // keep it; the rate cap doesn't push the value *up*. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 600 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 601 | oversample: 2, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 602 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 603 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 604 | assert_eq!(cfg.sanitize_for_rate(48_000.0).oversample, 2); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 605 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 606 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 607 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 608 | // try_set_config: scalar updates apply in place, structural |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 609 | // changes are rejected. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 610 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 611 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 612 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 613 | fn try_set_config_applies_scalar_changes() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 614 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 615 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 616 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 617 | ceiling_dbtp: -3.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 618 | release_ms: 200.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 619 | hold_ms: 10.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 620 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 621 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 622 | assert_eq!(l.try_set_config(cfg), SetConfigOutcome::Applied); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 623 | assert!((l.ceiling_dbtp() - -3.0).abs() < 1e-6); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 624 | let active = l.config(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 625 | assert!((active.release_ms - 200.0).abs() < 1e-6); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 626 | assert!((active.hold_ms - 10.0).abs() < 1e-6); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 627 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 628 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 629 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 630 | fn try_set_config_can_toggle_soft_tier() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 631 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 632 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 633 | // Start with soft on. Disable it. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 634 | let mut cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 635 | soft: None, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 636 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 637 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 638 | assert_eq!(l.try_set_config(cfg), SetConfigOutcome::Applied); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 639 | assert!(l.config().soft.is_none()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 640 | assert!(l.effective_soft_ceiling_dbtp().is_none()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 641 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 642 | // Re-enable with custom params. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 643 | let new_soft = SoftTierConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 644 | max_psr_db: 10.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 645 | static_ceiling_dbtp: -4.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 646 | attack_ms: 8.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 647 | release_ms: 300.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 648 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 649 | cfg.soft = Some(new_soft); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 650 | assert_eq!(l.try_set_config(cfg), SetConfigOutcome::Applied); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 651 | let active_soft = l.config().soft.expect("soft re-enabled"); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 652 | assert!((active_soft.max_psr_db - 10.0).abs() < 1e-6); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 653 | assert!((active_soft.static_ceiling_dbtp - -4.0).abs() < 1e-6); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 654 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 655 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 656 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 657 | fn try_set_config_rejects_oversample_change() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 658 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 659 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 660 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 661 | oversample: 8, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 662 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 663 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 664 | assert_eq!(l.try_set_config(cfg), SetConfigOutcome::StructuralChange); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 665 | // Limiter unchanged. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 666 | assert_eq!(l.config().oversample, LimiterConfig::default().oversample); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 667 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 668 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 669 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 670 | fn try_set_config_rejects_lookahead_change() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 671 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 672 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 673 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 674 | // resizes delay + peak buffer |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 675 | lookahead_ms: 5.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 676 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 677 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 678 | assert_eq!(l.try_set_config(cfg), SetConfigOutcome::StructuralChange); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 679 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 680 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 681 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 682 | fn try_set_config_rejects_fir_taps_change() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 683 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 684 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 685 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 686 | fir_taps: 63, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 687 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 688 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 689 | assert_eq!(l.try_set_config(cfg), SetConfigOutcome::StructuralChange); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 690 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 691 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 692 | fn run_sine( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 693 | limiter: &mut Limiter, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 694 | freq: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 695 | amp_db: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 696 | samples: usize, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 697 | sr: f32, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 698 | ) -> Vec<f32> { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 699 | let amp = db_to_lin(amp_db); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 700 | let mut out = Vec::with_capacity(samples * 2); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 701 | for n in 0..samples { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 702 | let t = n as f32 / sr; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 703 | let s = amp * (2.0 * PI * freq * t).sin(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 704 | let (l, r) = limiter.process_frame(s, s); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 705 | out.push(l); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 706 | out.push(r); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 707 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 708 | out |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 709 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 710 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 711 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 712 | // Hard-tier contract: holds with or without the soft tier present. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 713 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 714 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 715 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 716 | fn passes_signal_below_both_ceilings_unchanged() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 717 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 718 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 719 | // -18 dBFS is below the default static soft ceiling of -6 dBTP |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 720 | // and the hard ceiling. Neither tier should engage. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 721 | let out = run_sine(&mut l, 440.0, -18.0, 4_800, sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 722 | let max_abs = out.iter().skip(1_000).fold(0.0_f32, |a, &b| a.max(b.abs())); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 723 | let max_db = lin_to_db(max_abs); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 724 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 725 | (max_db - (-18.0)).abs() < 0.5, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 726 | "expected ~-18 dB, got {max_db}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 727 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 728 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 729 | l.gain_reduction_db().abs() < 0.5, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 730 | "expected ~0 GR, got {}", |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 731 | l.gain_reduction_db() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 732 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 733 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 734 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 735 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 736 | fn enforces_hard_ceiling_on_hot_signal_with_soft_tier() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 737 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 738 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 739 | let out = run_sine(&mut l, 440.0, 6.0, 9_600, sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 740 | let ceiling_lin = db_to_lin(-0.1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 741 | let max_abs = out |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 742 | .iter() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 743 | .skip(2_000) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 744 | .fold(0.0_f32, |a, &b| a.max(b.abs())); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 745 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 746 | max_abs <= ceiling_lin + 1e-6, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 747 | "above hard ceiling: max_abs={max_abs}, ceiling_lin={ceiling_lin}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 748 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 749 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 750 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 751 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 752 | fn enforces_hard_ceiling_on_intersample_peak_with_soft_tier() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 753 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 754 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 755 | let ceiling_lin = db_to_lin(-0.1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 756 | let mut max_abs = 0.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 757 | let mut sign = 1.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 758 | let amp = 0.95_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 759 | for n in 0..9_600 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 760 | let s = sign * amp; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 761 | sign = -sign; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 762 | let (lo, ro) = l.process_frame(s, s); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 763 | if n > 1_500 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 764 | max_abs = max_abs.max(lo.abs()).max(ro.abs()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 765 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 766 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 767 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 768 | max_abs <= ceiling_lin + 1e-6, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 769 | "ISP: above hard ceiling: max_abs={max_abs}, ceiling_lin={ceiling_lin}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 770 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 771 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 772 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 773 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 774 | fn enforces_hard_ceiling_on_transient_impulse_with_soft_tier() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 775 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 776 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 777 | let ceiling_lin = db_to_lin(-0.1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 778 | let mut max_abs = 0.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 779 | for n in 0..4_800_usize { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 780 | let s = if n == 1_000 { 4.0 } else { 0.0 }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 781 | let (lo, ro) = l.process_frame(s, s); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 782 | max_abs = max_abs.max(lo.abs()).max(ro.abs()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 783 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 784 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 785 | max_abs <= ceiling_lin + 1e-6, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 786 | "impulse: above hard ceiling: max_abs={max_abs}, ceiling_lin={ceiling_lin}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 787 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 788 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 789 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 790 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 791 | fn brickwall_only_skips_soft_tier_entirely() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 792 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 793 | let mut l = Limiter::new(LimiterConfig::brickwall_only(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 794 | assert!(l.effective_soft_ceiling_dbtp().is_none()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 795 | // Drive a hot signal; brickwall must still hold. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 796 | let out = run_sine(&mut l, 440.0, 6.0, 4_800, sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 797 | let ceiling_lin = db_to_lin(-0.1); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 798 | let max_abs = out.iter().skip(800).fold(0.0_f32, |a, &b| a.max(b.abs())); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 799 | assert!(max_abs <= ceiling_lin + 1e-6); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 800 | // No soft gain reduction should ever have been recorded. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 801 | assert!(l.soft_gain_reduction_db().abs() < 1e-6); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 802 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 803 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 804 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 805 | // Soft tier: static fallback ceiling |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 806 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 807 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 808 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 809 | fn soft_tier_static_ceiling_engages_before_hard() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 810 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 811 | // Static soft ceiling at -6 dBTP, attack short enough to |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 812 | // settle inside the lookahead. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 813 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 814 | lookahead_ms: 5.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 815 | soft: Some(SoftTierConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 816 | static_ceiling_dbtp: -6.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 817 | attack_ms: 1.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 818 | release_ms: 100.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 819 | ..SoftTierConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 820 | }), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 821 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 822 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 823 | let mut l = Limiter::new(cfg, sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 824 | // Drive a +6 dB sine — well above the soft ceiling. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 825 | let out = run_sine(&mut l, 440.0, 6.0, 9_600, sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 826 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 827 | // Output should sit near the soft ceiling, well below hard. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 828 | let soft_ceiling_lin = db_to_lin(-6.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 829 | let max_abs = out |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 830 | .iter() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 831 | .skip(2_000) |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 832 | .fold(0.0_f32, |a, &b| a.max(b.abs())); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 833 | // Allow small overshoot during soft attack (gain hasn't fully |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 834 | // settled when the peak arrives), but it must be well under |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 835 | // the hard ceiling. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 836 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 837 | max_abs <= soft_ceiling_lin * 1.1, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 838 | "output above soft ceiling: max_abs={max_abs}, soft_lin={soft_ceiling_lin}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 839 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 840 | // Soft tier should report meaningful GR; hard tier ideally |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 841 | // does very little once the soft tier has settled. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 842 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 843 | l.soft_gain_reduction_db() < -3.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 844 | "soft GR too small: {}", |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 845 | l.soft_gain_reduction_db() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 846 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 847 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 848 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 849 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 850 | // Soft tier: dynamic ceiling from program loudness |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 851 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 852 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 853 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 854 | fn dynamic_ceiling_tracks_program_loudness() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 855 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 856 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 857 | // Default max_psr_db = 14. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 858 | l.set_program_loudness_lufs(-18.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 859 | let dyn_ceiling = l.effective_soft_ceiling_dbtp().expect("soft tier active"); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 860 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 861 | (dyn_ceiling - (-4.0)).abs() < 1e-3, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 862 | "expected -4 dBTP, got {dyn_ceiling}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 863 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 864 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 865 | // Move the program louder; ceiling rises (and clamps at 0). |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 866 | l.set_program_loudness_lufs(-2.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 867 | let dyn_ceiling = l.effective_soft_ceiling_dbtp().unwrap(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 868 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 869 | (-0.1..=0.0).contains(&dyn_ceiling), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 870 | "expected clamp near 0 dBTP, got {dyn_ceiling}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 871 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 872 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 873 | // Clear it; falls back to static. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 874 | l.clear_program_loudness(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 875 | let fallback = l.effective_soft_ceiling_dbtp().unwrap(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 876 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 877 | (fallback - (-6.0)).abs() < 1e-3, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 878 | "expected static -6 dBTP, got {fallback}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 879 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 880 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 881 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 882 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 883 | fn silence_fast_path_preserves_program_loudness() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 884 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 885 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 886 | l.set_program_loudness_lufs(-18.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 887 | let before = l.effective_soft_ceiling_dbtp().unwrap(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 888 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 889 | for _ in 0..10_000 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 890 | let (lo, ro) = l.process_frame(0.0, 0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 891 | assert_eq!(lo, 0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 892 | assert_eq!(ro, 0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 893 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 894 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 895 | assert_eq!(l.effective_soft_ceiling_dbtp().unwrap(), before); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 896 | assert_eq!(l.gain_reduction_db(), 0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 897 | assert_eq!(l.true_peak_dbtp(), -200.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 898 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 899 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 900 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 901 | fn silence_fast_path_does_not_truncate_lookahead_tail() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 902 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 903 | let mut l = Limiter::new(LimiterConfig::brickwall_only(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 904 | for _ in 0..16 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 905 | let _ = l.process_frame(0.5, 0.5); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 906 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 907 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 908 | let mut delayed_peak = 0.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 909 | for n in 0..200 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 910 | let (lo, ro) = l.process_frame(0.0, 0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 911 | if n > 8 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 912 | delayed_peak = delayed_peak.max(lo.abs()).max(ro.abs()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 913 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 914 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 915 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 916 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 917 | delayed_peak > 1.0e-6, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 918 | "lookahead tail was truncated before it could drain: delayed_peak={delayed_peak}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 919 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 920 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 921 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 922 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 923 | fn dynamic_ceiling_bounds_psr_on_hot_transient() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 924 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 925 | // Long lookahead and fast soft attack so the soft tier |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 926 | // demonstrably catches the transient before the hard tier |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 927 | // needs to. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 928 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 929 | lookahead_ms: 5.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 930 | soft: Some(SoftTierConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 931 | max_psr_db: 14.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 932 | static_ceiling_dbtp: -6.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 933 | attack_ms: 1.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 934 | release_ms: 100.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 935 | }), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 936 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 937 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 938 | let mut l = Limiter::new(cfg, sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 939 | l.set_program_loudness_lufs(-18.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 940 | // Expected dynamic ceiling: -18 + 14 = -4 dBTP ≈ 0.631 lin. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 941 | let dyn_ceil_lin = db_to_lin(-4.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 942 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 943 | // Slam a +6 dBFS impulse. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 944 | let mut max_after = 0.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 945 | for n in 0..4_800_usize { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 946 | let s = if n == 800 { db_to_lin(6.0) } else { 0.0 }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 947 | let (lo, _) = l.process_frame(s, s); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 948 | if n > 700 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 949 | max_after = max_after.max(lo.abs()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 950 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 951 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 952 | // Output should be at or below the dynamic soft ceiling with |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 953 | // a small ringing margin. Critically, the hard tier should |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 954 | // *not* be the thing that catches it — its GR should be small. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 955 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 956 | max_after <= dyn_ceil_lin * 1.15, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 957 | "soft tier didn't bound the transient: max={max_after}, dyn_ceil={dyn_ceil_lin}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 958 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 959 | // The hard tier may snap briefly at peak entry (soft envelope |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 960 | // hasn't ramped yet), then take its release time to recover. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 961 | // We don't require zero hard engagement here — only that it |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 962 | // isn't doing the majority of the work. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 963 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 964 | l.hard_gain_reduction_db().abs() < 4.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 965 | "hard tier engaged unreasonably: {}", |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 966 | l.hard_gain_reduction_db() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 967 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 968 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 969 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 970 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 971 | // Misc |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 972 | // ---------------------------------------------------------------- |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 973 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 974 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 975 | fn nan_inputs_do_not_propagate_with_soft_tier() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 976 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 977 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 978 | for _ in 0..1_000 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 979 | let (lo, ro) = l.process_frame(f32::NAN, f32::INFINITY); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 980 | assert!(lo.is_finite() && ro.is_finite()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 981 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 982 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 983 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 984 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 985 | fn ceiling_clamps_positive_config_to_zero() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 986 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 987 | ceiling_dbtp: 3.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 988 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 989 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 990 | .sanitized(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 991 | assert_eq!(cfg.ceiling_dbtp, 0.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 992 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 993 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 994 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 995 | fn set_program_loudness_ignores_non_finite() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 996 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 997 | let mut l = Limiter::new(LimiterConfig::default(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 998 | // Establish a baseline. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 999 | l.set_program_loudness_lufs(-20.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1000 | let baseline = l.effective_soft_ceiling_dbtp().unwrap(); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1001 | // NaN / Inf should be ignored. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1002 | l.set_program_loudness_lufs(f32::NAN); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1003 | assert_eq!(l.effective_soft_ceiling_dbtp().unwrap(), baseline); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1004 | l.set_program_loudness_lufs(f32::INFINITY); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1005 | assert_eq!(l.effective_soft_ceiling_dbtp().unwrap(), baseline); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1006 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1007 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1008 | #[test] |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1009 | fn soft_tier_reduces_perceived_peak_to_loudness_ratio() { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1010 | // The whole point of the soft tier: a transient on top of a |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1011 | // quieter program should NOT come out near the hard ceiling. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1012 | let sr = 48_000.0; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1013 | let cfg = LimiterConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1014 | lookahead_ms: 5.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1015 | soft: Some(SoftTierConfig { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1016 | max_psr_db: 12.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1017 | static_ceiling_dbtp: -8.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1018 | attack_ms: 1.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1019 | release_ms: 100.0, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1020 | }), |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1021 | ..LimiterConfig::default() |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1022 | }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1023 | let mut brickwall = Limiter::new(LimiterConfig::brickwall_only(), sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1024 | let mut two_tier = Limiter::new(cfg, sr); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1025 | two_tier.set_program_loudness_lufs(-20.0); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1026 | |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1027 | let mut bw_peak = 0.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1028 | let mut tt_peak = 0.0_f32; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1029 | for n in 0..4_800_usize { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1030 | // Quiet program with a single big spike. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1031 | let s = if n == 1_200 { db_to_lin(3.0) } else { 0.01 }; |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1032 | let (lo_bw, _) = brickwall.process_frame(s, s); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1033 | let (lo_tt, _) = two_tier.process_frame(s, s); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1034 | if n > 1_000 { |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1035 | bw_peak = bw_peak.max(lo_bw.abs()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1036 | tt_peak = tt_peak.max(lo_tt.abs()); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1037 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1038 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1039 | // Brickwall lets the spike through near the hard ceiling. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1040 | // Two-tier holds it much lower. |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1041 | assert!( |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1042 | tt_peak < bw_peak * 0.6, |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1043 | "soft tier did not meaningfully reduce peak: bw={bw_peak}, tt={tt_peak}" |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1044 | ); |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1045 | } |
614b4e43f9f3 | atagen | 2026-05-27 15:49 UTC | 1046 | } |