Skip to main content

sva_samples/measure/
ledger.rs

1// Concern: attributes a target's energy down its ref tree, one entry per node, a stretch at a time | Non-concern: rendering the buffers it reads | IO: (sources, deps, target) -> Tally -> entries
2
3use std::collections::{BTreeMap, BTreeSet, VecDeque};
4
5use crate::buffer::Buffer;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum Unit {
9    Amplitude,
10    Hertz,
11    Q,
12    Decibels,
13    Seconds,
14    Radians,
15    Ratio,
16}
17
18impl Unit {
19    pub fn name(self) -> &'static str {
20        match self {
21            Unit::Amplitude => "amplitude",
22            Unit::Hertz => "hz",
23            Unit::Q => "q",
24            Unit::Decibels => "db",
25            Unit::Seconds => "seconds",
26            Unit::Radians => "radians",
27            Unit::Ratio => "ratio",
28        }
29    }
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum SignalKind {
34    Audio,
35    Control(Unit),
36}
37
38impl SignalKind {
39    pub fn is_audio(self) -> bool {
40        self == SignalKind::Audio
41    }
42
43    pub fn unit(self) -> Unit {
44        match self {
45            SignalKind::Audio => Unit::Amplitude,
46            SignalKind::Control(u) => u,
47        }
48    }
49}
50
51#[derive(Clone, Debug, PartialEq)]
52pub struct LedgerEntry {
53    pub node: String,
54    pub channel: Option<usize>,
55    pub depth: usize,
56    pub kind: SignalKind,
57    pub rms: f64,
58    pub peak: f64,
59    pub min: f64,
60    pub max: f64,
61    /// The part of its reader's own energy this node accounts for, so one reader's refs add
62    /// to one over a sum. `None` for a non-audio kind, and for a ref no addend isolates.
63    pub share: Option<f64>,
64    /// A sample outside [-1, 1], the full scale a `.wav` destination clamps to, so a peak of
65    /// exactly 1.0 is not clipped.
66    pub clipped: Option<bool>,
67}
68
69/// `read`: what a row gave its reader; `own`: a reader's own samples, which shares are of.
70pub struct Sources<'a> {
71    pub read: &'a BTreeMap<String, usize>,
72    pub own: &'a BTreeMap<String, usize>,
73    pub attributed: &'a BTreeSet<String>,
74    pub widths: &'a [usize],
75}
76
77/// Each sum one running fold in sample order: the bits a sum over the whole buffer gives.
78pub struct Tally {
79    rows: Vec<Row>,
80}
81
82struct Row {
83    node: String,
84    depth: usize,
85    kind: SignalKind,
86    attributed: bool,
87    read: Option<usize>,
88    reader: Option<usize>,
89    channels: Vec<Sums>,
90}
91
92#[derive(Clone, Copy)]
93struct Sums {
94    squares: f64,
95    count: usize,
96    peak: f64,
97    min: f64,
98    max: f64,
99    product: f64,
100    energy: f64,
101}
102
103impl Sums {
104    fn new() -> Sums {
105        // What an empty `sum` folds from, so a running fold is that sum's bits.
106        let zero: f64 = std::iter::empty::<f64>().sum();
107        Sums {
108            squares: zero,
109            count: 0,
110            peak: 0.0,
111            min: f64::INFINITY,
112            max: f64::NEG_INFINITY,
113            product: zero,
114            energy: zero,
115        }
116    }
117}
118
119impl Tally {
120    pub fn new(
121        sources: &Sources,
122        deps: &BTreeMap<String, Vec<String>>,
123        kinds: &BTreeMap<String, SignalKind>,
124        target: &str,
125        depth_limit: usize,
126    ) -> Tally {
127        let width = |path: &str| sources.read.get(path).map_or(1, |s| sources.widths[*s]);
128        let mut seen: BTreeSet<&str> = BTreeSet::new();
129        let mut queue: VecDeque<(&str, usize, &str)> = VecDeque::new();
130        let mut rows = Vec::new();
131        queue.push_back((target, 0, target));
132        seen.insert(target);
133        while let Some((path, depth, reader)) = queue.pop_front() {
134            // `kinds` names what holds a buffer; a node composed into another holds none.
135            rows.push(Row {
136                node: path.to_string(),
137                depth,
138                kind: kinds.get(path).copied().unwrap_or(SignalKind::Audio),
139                attributed: path == target || sources.attributed.contains(path),
140                read: sources.read.get(path).copied(),
141                reader: sources
142                    .own
143                    .get(reader)
144                    .copied()
145                    .filter(|s| sources.widths[*s] > 0),
146                channels: vec![Sums::new(); width(path)],
147            });
148            if depth >= depth_limit {
149                continue;
150            }
151            for child in deps.get(path).map(Vec::as_slice).unwrap_or(&[]) {
152                if seen.insert(child.as_str()) {
153                    let key = deps
154                        .get_key_value(child.as_str())
155                        .map(|(k, _)| k.as_str())
156                        .unwrap_or(child.as_str());
157                    queue.push_back((key, depth + 1, path));
158                }
159            }
160        }
161        Tally { rows }
162    }
163
164    pub fn add(&mut self, held: &[Option<&Buffer>], range: std::ops::Range<usize>) {
165        let empty: &[f64] = &[];
166        let slice = |source: Option<usize>, c: usize| -> Option<std::borrow::Cow<'_, [f64]>> {
167            let buffer = held[source?]?;
168            Some(buffer.plane_slice(c % buffer.width(), range.clone()))
169        };
170        for row in &mut self.rows {
171            for (c, sums) in row.channels.iter_mut().enumerate() {
172                let x = match row.read {
173                    Some(source) => slice(Some(source), c).expect("a row's own source"),
174                    None => std::borrow::Cow::Borrowed(empty),
175                };
176                sums.count += x.len();
177                sums.squares = x.iter().map(|&v| v * v).fold(sums.squares, |a, b| a + b);
178                sums.peak = x.iter().fold(sums.peak, |a, &v| a.max(v.abs()));
179                sums.min = x.iter().fold(sums.min, |a, &v| a.min(v));
180                sums.max = x.iter().fold(sums.max, |a, &v| a.max(v));
181                if let Some(y) = slice(row.reader, c) {
182                    sums.energy = y.iter().map(|&v| v * v).fold(sums.energy, |a, b| a + b);
183                    let products = x.iter().zip(y.iter()).map(|(a, b)| a * b);
184                    sums.product = products.fold(sums.product, |a, b| a + b);
185                }
186            }
187        }
188    }
189
190    pub fn entries(&self) -> Vec<LedgerEntry> {
191        let mut out = Vec::new();
192        for row in &self.rows {
193            let components = row.channels.len();
194            let gated = row.kind.is_audio();
195            for (c, sums) in row.channels.iter().enumerate() {
196                let rms = match sums.count {
197                    0 => 0.0,
198                    count => (sums.squares / count as f64).sqrt(),
199                };
200                // Over a sum the parts are the whole: one reader's refs add to exactly one.
201                let share = || match (row.reader, sums.energy > 0.0) {
202                    (Some(_), true) => sums.product / sums.energy,
203                    _ => 0.0,
204                };
205                out.push(LedgerEntry {
206                    node: row.node.clone(),
207                    channel: (components > 1).then_some(c),
208                    depth: row.depth,
209                    kind: row.kind,
210                    rms,
211                    peak: sums.peak,
212                    min: sums.min,
213                    max: sums.max,
214                    share: (gated && row.attributed).then(share),
215                    clipped: gated.then_some(sums.peak > 1.0),
216                });
217            }
218        }
219        out
220    }
221}