Skip to main content

sva_analysis/experimental/
gain_reduction.rs

1// Concern: a gain-control node's own envelope in dB, before/after an optional input | Non-concern: finding that node (no compressor/limiter type exists) | IO: (gain envelope[, input]) -> GainReduction
2
3use sva_samples::EnvelopeFrame;
4
5use crate::decibels::to_db;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct GainReductionFrame {
9    pub t_secs: f64,
10    pub gain_db: f64,
11    pub input_db: Option<f64>,
12    pub output_db: Option<f64>,
13}
14
15#[derive(Clone, Debug, PartialEq)]
16pub struct GainReduction {
17    pub frames: Vec<GainReductionFrame>,
18}
19
20/// `input` is read by index against `gain`'s own frames — matching `--frame`s over the same
21/// window, exactly as an `envelope` of `@gain-node` already lines up by eye today.
22pub fn analyze(gain: &[EnvelopeFrame], input: Option<&[EnvelopeFrame]>) -> GainReduction {
23    let frames = gain
24        .iter()
25        .enumerate()
26        .map(|(i, g)| {
27            let gain_db = to_db(g.rms);
28            let input_db = input.and_then(|f| f.get(i)).map(|f| to_db(f.rms));
29            GainReductionFrame {
30                t_secs: g.t_secs,
31                gain_db,
32                input_db,
33                output_db: input_db.map(|d| d + gain_db),
34            }
35        })
36        .collect();
37    GainReduction { frames }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    fn envelope(rms: &[f64]) -> Vec<EnvelopeFrame> {
45        rms.iter()
46            .enumerate()
47            .map(|(i, &r)| EnvelopeFrame {
48                t_secs: i as f64 * 0.01,
49                rms: r,
50                peak: r,
51            })
52            .collect()
53    }
54
55    #[test]
56    fn a_half_gain_node_alone_reads_about_minus_6_db() {
57        let g = analyze(&envelope(&[0.5, 0.5]), None);
58        assert!((g.frames[0].gain_db + 6.0206).abs() < 0.01);
59        assert!(g.frames[0].input_db.is_none());
60        assert!(g.frames[0].output_db.is_none());
61    }
62
63    #[test]
64    fn with_an_input_the_output_is_input_plus_gain_in_db() {
65        let gain = envelope(&[0.5]);
66        let input = envelope(&[1.0]);
67        let g = analyze(&gain, Some(&input));
68        let f = g.frames[0];
69        assert!((f.input_db.unwrap() - 0.0).abs() < 1e-9);
70        assert!((f.gain_db + 6.0206).abs() < 0.01);
71        assert!((f.output_db.unwrap() - f.gain_db).abs() < 1e-9);
72    }
73
74    #[test]
75    fn a_shorter_input_leaves_the_tail_frames_without_a_before_after_reading() {
76        let gain = envelope(&[0.5, 0.5, 0.5]);
77        let input = envelope(&[1.0]);
78        let g = analyze(&gain, Some(&input));
79        assert!(g.frames[0].input_db.is_some());
80        assert!(g.frames[1].input_db.is_none());
81        assert!(g.frames[2].output_db.is_none());
82    }
83}