1use crate::profile::NoiseProfile;
36use crate::stft::{hop_for, pow_db, Fft, FLOOR_POW};
37use ferraus_core::{AudioBuffer, Result};
38use realfft::num_complex::Complex32;
39use std::collections::VecDeque;
40
41pub const RAYLEIGH_LOG_BIAS_DB: f32 = 2.507;
43pub const RAYLEIGH_STD_DB: f32 = 5.570;
45const SIGMOID_WIDTH_DB: f32 = 1.5;
47const SIGMOID_OFFSET_DB: f32 = 1.5;
50const SPREAD_FLOOR_DB: f32 = 2.5;
53const OPENING_KEEP_DB: f32 = 6.0;
55const EXCESS_CLAMP_DB: f32 = 40.0;
57const FLOOR_MAX_LIFT: f32 = 4.0; const ONSET_MIN_FLUX_DB: f32 = 1.0;
61const ONSET_RATIO: f32 = 3.0;
62const ONSET_HOLD_S: f32 = 0.04;
63
64pub fn log_bias_db(std_db: f32) -> f32 {
67 let r = (std_db / RAYLEIGH_STD_DB).min(1.0);
68 RAYLEIGH_LOG_BIAS_DB * r * r
69}
70
71#[derive(Clone, Debug, PartialEq)]
73pub struct GateConfig {
74 pub reduction_db: f32,
75 pub sensitivity: f32,
76 pub smoothing: usize,
77 pub attack_s: f32,
78 pub release_s: f32,
79 pub artifact_control: f32,
81 pub link: bool,
82 pub preserve_transients: bool,
83}
84
85impl Default for GateConfig {
86 fn default() -> Self {
87 GateConfig {
88 reduction_db: 12.0,
89 sensitivity: 6.0,
90 smoothing: 3,
91 attack_s: 0.020,
92 release_s: 0.120,
93 artifact_control: 0.5,
94 link: true,
95 preserve_transients: true,
96 }
97 }
98}
99
100#[derive(Clone, Debug)]
102pub enum NoiseModel {
103 Profile(Box<NoiseProfile>),
104 Adaptive {
107 noisiness: f32,
108 floor_db: f32,
109 },
110}
111
112#[derive(Clone, Debug)]
115pub struct MinStats {
116 alpha: f32,
117 smoothed: Vec<f32>,
118 sub_min: Vec<f32>,
119 ring: Vec<Vec<f32>>,
120 ring_len: usize,
121 ring_pos: usize,
122 sub_count: usize,
123 sub_len: usize,
124 bias: f32,
125 pub noise: Vec<f32>,
126 init: bool,
127}
128
129const MINSTATS_BIAS: f32 = 2.2;
132const MINSTATS_SUBWINDOWS: usize = 8;
133
134impl MinStats {
135 pub fn new(bins: usize, frame_s: f32) -> MinStats {
137 let window_frames = (1.5 / frame_s).round().max(8.0) as usize;
138 let sub_len = (window_frames / MINSTATS_SUBWINDOWS).max(1);
139 let alpha = (-frame_s / 0.07).exp().clamp(0.0, 0.95);
141 MinStats {
142 alpha,
143 smoothed: vec![0.0; bins],
144 sub_min: vec![f32::INFINITY; bins],
145 ring: vec![vec![f32::INFINITY; bins]; MINSTATS_SUBWINDOWS],
146 ring_len: 0,
147 ring_pos: 0,
148 sub_count: 0,
149 sub_len,
150 bias: MINSTATS_BIAS,
151 noise: vec![FLOOR_POW; bins],
152 init: false,
153 }
154 }
155
156 pub fn seed(&mut self, noise_pow: &[f32]) {
158 self.smoothed.copy_from_slice(noise_pow);
159 for r in &mut self.ring {
160 for (m, n) in r.iter_mut().zip(noise_pow) {
161 *m = n / self.bias;
162 }
163 }
164 self.ring_len = self.ring.len();
165 self.sub_min.iter_mut().for_each(|m| *m = f32::INFINITY);
166 self.noise.copy_from_slice(noise_pow);
167 self.init = true;
168 }
169
170 pub fn update(&mut self, p: &[f32]) {
171 if !self.init {
172 self.smoothed.copy_from_slice(p);
173 self.init = true;
174 } else {
175 let a = self.alpha;
176 for (s, x) in self.smoothed.iter_mut().zip(p) {
177 *s = a * *s + (1.0 - a) * x;
178 }
179 }
180 for (m, s) in self.sub_min.iter_mut().zip(&self.smoothed) {
181 if *s < *m {
182 *m = *s;
183 }
184 }
185 self.sub_count += 1;
186 if self.sub_count >= self.sub_len {
187 self.ring[self.ring_pos].copy_from_slice(&self.sub_min);
188 self.ring_pos = (self.ring_pos + 1) % self.ring.len();
189 self.ring_len = (self.ring_len + 1).min(self.ring.len());
190 self.sub_min.iter_mut().for_each(|m| *m = f32::INFINITY);
191 self.sub_count = 0;
192 }
193 for k in 0..self.noise.len() {
194 let mut m = self.sub_min[k];
195 for r in self.ring.iter().take(self.ring_len) {
196 m = m.min(r[k]);
197 }
198 if !m.is_finite() {
199 m = self.smoothed[k];
200 }
201 self.noise[k] = (m * self.bias).max(FLOOR_POW);
202 }
203 }
204}
205
206struct Frame {
207 spec: Vec<Vec<Complex32>>,
208 pow: Vec<Vec<f32>>,
209 noise: Vec<Vec<f32>>,
210 raw: Vec<Vec<f32>>,
211 open: Vec<Vec<f32>>,
212 blended: Vec<Vec<f32>>,
213 flux: f32,
214 onset: bool,
215}
216
217impl Frame {
218 fn new(channels: usize, groups: usize, bins: usize) -> Frame {
219 let g = || vec![vec![0f32; bins]; groups];
220 Frame {
221 spec: vec![vec![Complex32::new(0.0, 0.0); bins]; channels],
222 pow: g(),
223 noise: g(),
224 raw: g(),
225 open: g(),
226 blended: g(),
227 flux: 0.0,
228 onset: false,
229 }
230 }
231}
232
233enum Thresholds {
234 Profile {
236 main: Vec<Vec<f32>>,
237 open: Vec<Vec<f32>>,
238 },
239 Adaptive {
240 offset_db: f32,
241 floor_db: f32,
242 },
243}
244
245#[derive(Clone, Debug, Default)]
247pub struct EngineStats {
248 pub frames: usize,
249 pub onsets: usize,
250 pub mean_gain_db: f64,
252}
253
254pub struct Engine {
256 cfg: GateConfig,
257 channels: usize,
258 groups: usize,
259 n: usize,
260 hop: usize,
261 bins: usize,
262 fft: Fft,
263 scale2: f32,
264 inbuf: Vec<Vec<f32>>,
265 ola: Vec<Vec<f32>>,
266 thresholds: Thresholds,
267 trackers: Vec<MinStats>,
268 queue: VecDeque<Frame>,
269 pool: Vec<Frame>,
270 blended_count: usize,
271 blend_prev: Vec<Vec<f32>>,
272 prev_level: Vec<Vec<f32>>,
273 prev_flux: f32,
274 smooth_db: Vec<Vec<f32>>,
275 flux_avg: f32,
276 flux_alpha: f32,
277 hold: usize,
278 hold_frames: usize,
279 lookahead: usize,
280 a_att: f32,
281 a_rel: f32,
282 gmin: f32,
283 kernel: Vec<f32>,
284 frame_index: usize,
285 level: Vec<f32>,
287 excess: Vec<f32>,
288 scratch: Vec<f32>,
289 scratch2: Vec<f32>,
290 target: Vec<f32>,
291 gain: Vec<Vec<f32>>,
292 pub stats: EngineStats,
293 gain_sum: f64,
294}
295
296impl Engine {
297 pub fn new(cfg: GateConfig, noise: &NoiseModel, sample_rate: u32, channels: usize) -> Engine {
298 let channels = channels.max(1);
299 let n = match noise {
300 NoiseModel::Profile(p) => p.fft_size,
301 NoiseModel::Adaptive { .. } => crate::stft::fft_size_for(sample_rate),
302 };
303 let hop = hop_for(n);
304 let fft = Fft::new(n);
305 let bins = fft.bins();
306 let groups = if cfg.link { 1 } else { channels };
307 let frame_s = hop as f32 / sample_rate.max(1) as f32;
308 let mut trackers = vec![MinStats::new(bins, frame_s); groups];
309 let thresholds = match noise {
310 NoiseModel::Profile(p) => {
311 let mut main = Vec::new();
312 let mut open = Vec::new();
313 for (g, tracker) in trackers.iter_mut().enumerate() {
314 let (mean, std) = p.stats_for(g, cfg.link, channels);
315 let mean_pow_db: Vec<f32> = mean.iter().zip(std).map(|(m, s)| m + log_bias_db(*s)).collect();
316 main.push(
317 mean_pow_db
318 .iter()
319 .zip(std)
320 .map(|(m, s)| m + cfg.sensitivity * s.max(SPREAD_FLOOR_DB) / RAYLEIGH_STD_DB)
321 .collect(),
322 );
323 let seed: Vec<f32> = mean_pow_db.iter().map(|d| 10f32.powf(d / 10.0).max(FLOOR_POW)).collect();
324 tracker.seed(&seed);
325 open.push(mean_pow_db);
326 }
327 Thresholds::Profile { main, open }
328 }
329 NoiseModel::Adaptive { noisiness, floor_db } => {
330 Thresholds::Adaptive { offset_db: 20.0 * noisiness.clamp(0.0, 1.0), floor_db: *floor_db }
331 }
332 };
333 let slew = |t: f32| {
335 if t <= 0.0 {
336 f32::INFINITY
337 } else {
338 cfg.reduction_db.abs() * frame_s / t
339 }
340 };
341 let lookahead = ((cfg.attack_s / frame_s).ceil() as usize).clamp(1, 8);
342 let r = cfg.smoothing.min(64);
343 let kernel: Vec<f32> = (0..=2 * r)
345 .map(|i| {
346 let x = std::f32::consts::PI * 2.0 * (i + 1) as f32 / (2 * r + 2) as f32;
347 0.5 - 0.5 * x.cos()
348 })
349 .collect();
350 let flux_tau = 0.5f32;
351 Engine {
352 channels,
353 groups,
354 n,
355 hop,
356 bins,
357 scale2: fft.mag_scale * fft.mag_scale,
358 fft,
359 inbuf: vec![vec![0.0; n]; channels],
360 ola: vec![vec![0.0; n]; channels],
361 thresholds,
362 trackers,
363 queue: VecDeque::new(),
364 pool: Vec::new(),
365 blended_count: 0,
366 blend_prev: vec![vec![0.0; bins]; groups],
367 prev_level: vec![vec![f32::NEG_INFINITY; bins]; groups],
368 prev_flux: 0.0,
369 smooth_db: vec![vec![-cfg.reduction_db.abs(); bins]; groups],
370 flux_avg: 0.0,
371 flux_alpha: (-frame_s / flux_tau).exp(),
372 hold: 0,
373 hold_frames: (ONSET_HOLD_S / frame_s).ceil() as usize,
374 lookahead,
375 a_att: slew(cfg.attack_s),
376 a_rel: slew(cfg.release_s),
377 gmin: 10f32.powf(-cfg.reduction_db.abs() / 20.0),
378 kernel,
379 frame_index: 0,
380 level: vec![0.0; bins],
381 excess: vec![0.0; bins],
382 scratch: vec![0.0; bins],
383 scratch2: vec![0.0; bins],
384 target: vec![0.0; bins],
385 gain: vec![vec![1.0; bins]; groups],
386 stats: EngineStats::default(),
387 gain_sum: 0.0,
388 cfg,
389 }
390 }
391
392 pub fn fft_size(&self) -> usize {
393 self.n
394 }
395
396 pub fn hop(&self) -> usize {
397 self.hop
398 }
399
400 pub fn latency(&self) -> usize {
403 self.n - self.hop
404 }
405
406 pub fn push_hop(&mut self, input: &[&[f32]], out: &mut [Vec<f32>]) {
408 let (n, hop) = (self.n, self.hop);
409 for c in 0..self.channels {
410 let buf = &mut self.inbuf[c];
411 buf.copy_within(hop.., 0);
412 let src = input[c.min(input.len() - 1)];
413 buf[n - hop..].copy_from_slice(&src[..hop]);
414 }
415 let mut frame = self.pool.pop().unwrap_or_else(|| Frame::new(self.channels, self.groups, self.bins));
416 self.analyze(&mut frame);
417 self.queue.push_back(frame);
418 let len = self.queue.len();
420 if len >= 2 && self.blended_count < len - 1 {
421 self.blend(len - 2);
422 self.blended_count = len - 1;
423 }
424 if self.blended_count > self.lookahead {
426 self.output_front(out);
427 }
428 }
429
430 fn analyze(&mut self, frame: &mut Frame) {
431 let idx = self.frame_index;
432 self.frame_index += 1;
433 let full = idx + 1 >= self.n / self.hop; for c in 0..self.channels {
435 self.fft.forward(&self.inbuf[c], &mut frame.spec[c]);
436 }
437 let scale2 = self.scale2;
438 let bins = self.bins;
439 let rdb = self.cfg.reduction_db.abs();
440 let mut max_flux = 0f32;
441 for g in 0..self.groups {
442 let pow = &mut frame.pow[g];
444 if self.cfg.link {
445 pow.iter_mut().for_each(|p| *p = 0.0);
446 let inv = 1.0 / self.channels as f32;
447 for c in 0..self.channels {
448 for (p, z) in pow.iter_mut().zip(&frame.spec[c]) {
449 *p += z.norm_sqr() * scale2 * inv;
450 }
451 }
452 } else {
453 for (p, z) in pow.iter_mut().zip(&frame.spec[g]) {
454 *p = z.norm_sqr() * scale2;
455 }
456 }
457 for (l, p) in self.level.iter_mut().zip(pow.iter()) {
459 *l = pow_db(*p);
460 }
461 if full {
462 self.trackers[g].update(pow);
463 }
464 frame.noise[g].copy_from_slice(&self.trackers[g].noise);
465 let (main_off, open_off) = match &self.thresholds {
467 Thresholds::Profile { .. } => (0.0, 0.0),
468 Thresholds::Adaptive { offset_db, floor_db } => (offset_db + floor_db, *floor_db),
469 };
470 for k in 0..bins {
472 let thr = match &self.thresholds {
473 Thresholds::Profile { main, .. } => main[g][k],
474 Thresholds::Adaptive { .. } => pow_db(frame.noise[g][k]) + main_off,
475 };
476 self.excess[k] = (self.level[k] - thr).clamp(-EXCESS_CLAMP_DB, EXCESS_CLAMP_DB);
477 }
478 let mut flux = 0f32;
480 if full {
481 let prev = &self.prev_level[g];
482 for ((e, l), p) in self.excess.iter().zip(&self.level).zip(prev) {
483 if *e > 0.0 && p.is_finite() {
484 flux += (l - p).max(0.0);
485 }
486 }
487 flux /= bins as f32;
488 }
489 self.prev_level[g].copy_from_slice(&self.level);
490 max_flux = max_flux.max(flux);
491 smooth_to_gain(&self.excess, &self.kernel, rdb, &mut frame.raw[g], &mut self.scratch, &mut self.scratch2);
492 if self.cfg.preserve_transients {
493 for k in 0..bins {
494 let thr = match &self.thresholds {
495 Thresholds::Profile { open, .. } => open[g][k],
496 Thresholds::Adaptive { .. } => pow_db(frame.noise[g][k]) + open_off,
497 };
498 self.excess[k] = (self.level[k] - thr).clamp(-EXCESS_CLAMP_DB, EXCESS_CLAMP_DB);
499 }
500 smooth_to_gain(
501 &self.excess,
502 &self.kernel,
503 rdb,
504 &mut frame.open[g],
505 &mut self.scratch,
506 &mut self.scratch2,
507 );
508 }
509 }
510 frame.flux = max_flux;
511 frame.onset = false;
512 }
513
514 fn blend(&mut self, i: usize) {
517 let alpha = self.cfg.artifact_control.clamp(0.0, 1.0);
518 let bins = self.bins;
519 let (head, tail) = self.queue.make_contiguous().split_at_mut(i + 1);
520 let cur = &mut head[i];
521 let next = &tail[0];
522 for g in 0..self.groups {
523 let prev = &self.blend_prev[g];
524 let raw = &cur.raw[g];
525 let nxt = &next.raw[g];
526 let out = &mut cur.blended[g];
527 if alpha <= 0.0 {
528 out.copy_from_slice(raw);
529 } else {
530 for k in 0..bins {
531 let lo = k.saturating_sub(1);
532 let hi = (k + 1).min(bins - 1);
533 let mut v = [0f32; 9];
534 let mut m = 0;
535 for j in lo..=hi {
536 v[m] = prev[j];
537 v[m + 1] = raw[j];
538 v[m + 2] = nxt[j];
539 m += 3;
540 }
541 let med = median(&mut v[..m]);
542 out[k] = raw[k] + alpha * (med - raw[k]);
543 }
544 }
545 self.blend_prev[g].copy_from_slice(&cur.raw[g]);
546 }
547 if self.cfg.preserve_transients {
549 let f = cur.flux;
550 let thr = (ONSET_RATIO * self.flux_avg).max(ONSET_MIN_FLUX_DB);
551 cur.onset = f > thr && f >= self.prev_flux && f > next.flux;
552 self.prev_flux = f;
553 self.flux_avg = self.flux_alpha * self.flux_avg + (1.0 - self.flux_alpha) * f;
554 }
555 }
556
557 fn output_front(&mut self, out: &mut [Vec<f32>]) {
558 let la = self.lookahead;
559 let bins = self.bins;
560 let transient_ahead = self.queue.iter().take(la + 1).any(|f| f.onset);
561 if self.queue[0].onset {
562 self.hold = self.hold_frames;
563 self.stats.onsets += 1;
564 }
565 let transient = self.cfg.preserve_transients && (transient_ahead || self.hold > 0);
566 self.hold = self.hold.saturating_sub(1);
567 let alpha = self.cfg.artifact_control.clamp(0.0, 1.0);
568 let gmin = self.gmin;
569 let floor_cap = (gmin * FLOOR_MAX_LIFT).min(1.0);
570 let mut target = std::mem::take(&mut self.target);
571 for g in 0..self.groups {
572 target.copy_from_slice(&self.queue[0].blended[g]);
574 for f in self.queue.iter().skip(1).take(la) {
575 for (t, b) in target.iter_mut().zip(&f.blended[g]) {
576 if *b > *t {
577 *t = *b;
578 }
579 }
580 }
581 let f0 = &self.queue[0];
582 let smooth = &mut self.smooth_db[g];
583 let gain = &mut self.gain[g];
584 for k in 0..bins {
585 let target = target[k];
586 let s = smooth[k];
587 let ns = if transient {
588 let t = target.max(f0.open[g][k]);
589 if t > s {
590 t
591 } else {
592 t.max(s - self.a_rel)
593 }
594 } else if target > s {
595 target.min(s + self.a_att)
596 } else {
597 target.max(s - self.a_rel)
598 };
599 smooth[k] = ns;
600 let mut gl = if ns == 0.0 { 1.0 } else { 10f32.powf(ns / 20.0) };
601 if alpha > 0.0 {
602 let p = f0.pow[g][k].max(FLOOR_POW);
603 let gf = (gmin * (f0.noise[g][k] / p).sqrt()).min(floor_cap);
604 if gf > gl {
605 gl += alpha * (gf - gl);
606 }
607 }
608 gain[k] = gl;
609 self.gain_sum += (20.0 * gl.max(1e-6).log10()) as f64;
610 }
611 }
612 self.target = target;
613 self.stats.frames += 1;
614 let mut frame = self.queue.pop_front().expect("queue not empty");
615 self.blended_count -= 1;
616 let (n, hop) = (self.n, self.hop);
617 for (c, (spec, o)) in frame.spec.iter_mut().zip(out.iter_mut()).enumerate() {
618 let g = if self.cfg.link { 0 } else { c };
619 for (z, gl) in spec.iter_mut().zip(&self.gain[g]) {
620 *z *= *gl;
621 }
622 let t = self.fft.inverse_windowed(spec);
623 let acc = &mut self.ola[c];
624 for (a, x) in acc.iter_mut().zip(t) {
625 *a += x;
626 }
627 o.extend_from_slice(&acc[..hop]);
628 acc.copy_within(hop.., 0);
629 acc[n - hop..].iter_mut().for_each(|v| *v = 0.0);
630 }
631 self.pool.push(frame);
632 }
633
634 pub fn process_buffer(
637 &mut self,
638 buf: &AudioBuffer,
639 progress: &mut dyn FnMut(f64) -> Result<()>,
640 ) -> Result<AudioBuffer> {
641 let total = buf.frames();
642 let ch = self.channels;
643 let hop = self.hop;
644 let skip = self.latency();
645 let mut out: Vec<Vec<f32>> = (0..ch).map(|_| Vec::with_capacity(total + self.n)).collect();
646 let mut chunk = vec![vec![0f32; hop]; ch];
647 let mut pos = 0usize;
648 let mut tick = 0usize;
649 while out[0].len() < skip + total {
650 for (c, dst) in chunk.iter_mut().enumerate() {
651 let src = &buf.data[c.min(buf.channels() - 1)];
652 for (i, d) in dst.iter_mut().enumerate() {
653 *d = src.get(pos + i).copied().unwrap_or(0.0);
654 }
655 }
656 pos += hop;
657 let refs: Vec<&[f32]> = chunk.iter().map(|c| c.as_slice()).collect();
658 self.push_hop(&refs, &mut out);
659 tick += 1;
660 if tick.is_multiple_of(256) {
661 progress((pos as f64 / total.max(1) as f64).min(1.0))?;
662 }
663 }
664 if self.stats.frames > 0 {
665 self.stats.mean_gain_db = self.gain_sum / (self.stats.frames * self.bins * self.groups) as f64;
666 }
667 let data = out
668 .into_iter()
669 .map(|mut c| {
670 c.drain(..skip);
671 c.truncate(total);
672 c
673 })
674 .collect();
675 Ok(AudioBuffer::from_planar(buf.sample_rate, data))
676 }
677}
678
679fn smooth_to_gain(
689 excess: &[f32],
690 kernel: &[f32],
691 reduction_db: f32,
692 out: &mut [f32],
693 lin: &mut [f32],
694 tmp: &mut [f32],
695) {
696 let bins = excess.len();
697 let r = kernel.len() / 2;
698 if reduction_db == 0.0 {
699 out.iter_mut().for_each(|g| *g = 0.0);
700 return;
701 }
702 let to_lin = std::f32::consts::LN_10 / 20.0;
703 for (l, e) in lin.iter_mut().zip(excess) {
704 let sig = 1.0 / (1.0 + (-(e - SIGMOID_OFFSET_DB) / SIGMOID_WIDTH_DB).exp());
705 *l = (-reduction_db * (1.0 - sig) * to_lin).exp();
706 }
707 if r == 0 {
708 for (o, l) in out.iter_mut().zip(lin.iter()) {
709 *o = 20.0 * l.max(1e-9).log10();
710 }
711 return;
712 }
713 for (k, t) in tmp.iter_mut().enumerate() {
716 let lo = k.saturating_sub(1);
717 let hi = (k + 1).min(bins - 1);
718 *t = lin[lo..=hi].iter().copied().fold(f32::INFINITY, f32::min);
719 }
720 for (k, o) in out.iter_mut().enumerate() {
721 let lo = k.saturating_sub(1);
722 let hi = (k + 1).min(bins - 1);
723 *o = tmp[lo..=hi].iter().copied().fold(0.0, f32::max);
724 }
725 for ((t, e), (l, o)) in tmp.iter_mut().zip(excess).zip(lin.iter().zip(out.iter())) {
726 *t = if *e > OPENING_KEEP_DB { *l } else { *o };
727 }
728 for k in 0..bins {
729 let lo = k.saturating_sub(r);
730 let hi = (k + r).min(bins - 1);
731 let mut acc = 0f32;
732 let mut wsum = 0f32;
733 for (j, g) in tmp.iter().enumerate().take(hi + 1).skip(lo) {
734 let w = kernel[j + r - k];
735 acc += w * g;
736 wsum += w;
737 }
738 out[k] = 20.0 * (acc / wsum).max(tmp[k]).max(1e-9).log10();
739 }
740}
741
742fn median(v: &mut [f32]) -> f32 {
743 v.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
744 let m = v.len();
745 if m % 2 == 1 {
746 v[m / 2]
747 } else {
748 0.5 * (v[m / 2 - 1] + v[m / 2])
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755 use ferraus_core::testing::{noise, sine};
756
757 #[test]
758 fn unity_gain_reconstructs() {
759 let x = ferraus_core::testing::add(&sine(48_000, 440.0, 0.5, 0.5), &noise(48_000, 0.5, 0.1, 3));
760 let cfg = GateConfig { reduction_db: 0.0, ..Default::default() };
761 let mut e = Engine::new(cfg, &NoiseModel::Adaptive { noisiness: 0.3, floor_db: 0.0 }, 48_000, 1);
762 let y = e.process_buffer(&x, &mut |_| Ok(())).unwrap();
763 assert_eq!(y.frames(), x.frames());
764 let err = ferraus_core::testing::rms_diff(&x, &y);
765 assert!(20.0 * err.log10() < -120.0, "error {} dB", 20.0 * err.log10());
766 }
767
768 #[test]
769 fn minstats_tracks_white_noise_power() {
770 let sr = 48_000;
771 let x = noise(sr, 3.0, 0.3, 9);
772 let n = 2048;
773 let mut fft = Fft::new(n);
774 let bins = fft.bins();
775 let mut ms = MinStats::new(bins, 512.0 / sr as f32);
776 let mut spec = vec![Complex32::new(0.0, 0.0); bins];
777 let mut p = vec![0f32; bins];
778 let mut true_mean = vec![0f64; bins];
779 let mut frames = 0;
780 let mut s = 0;
781 while s + n <= x.frames() {
782 fft.forward(&x.data[0][s..s + n], &mut spec);
783 for (pp, z) in p.iter_mut().zip(&spec) {
784 *pp = z.norm_sqr() * fft.mag_scale * fft.mag_scale;
785 }
786 for (t, v) in true_mean.iter_mut().zip(&p) {
787 *t += *v as f64;
788 }
789 frames += 1;
790 ms.update(&p);
791 s += 512;
792 }
793 let est: f64 = ms.noise[20..1000].iter().map(|v| *v as f64).sum::<f64>();
794 let tru: f64 = true_mean[20..1000].iter().sum::<f64>() / frames as f64;
795 let ratio_db = 10.0 * (est / tru).log10();
796 assert!(ratio_db.abs() < 1.5, "min-stats bias {ratio_db:.2} dB");
797 }
798}