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, a ref no addend isolates, and under a
63    /// silent reader.
64    pub share: Option<f64>,
65    /// A sample outside [-1, 1], the full scale a `.wav` destination clamps to, so a peak of
66    /// exactly 1.0 is not clipped.
67    pub clipped: Option<bool>,
68}
69
70/// `read`: what a row gave its reader; `own`: a reader's own samples, which shares are of.
71pub struct Sources<'a> {
72    pub read: &'a BTreeMap<String, usize>,
73    pub own: &'a BTreeMap<String, usize>,
74    pub attributed: &'a BTreeSet<String>,
75    pub widths: &'a [usize],
76}
77
78/// Each sum one running fold in sample order: the bits a sum over the whole buffer gives.
79pub struct Tally {
80    rows: Vec<Row>,
81}
82
83struct Row {
84    node: String,
85    depth: usize,
86    kind: SignalKind,
87    attributed: bool,
88    read: Option<usize>,
89    reader: Option<usize>,
90    channels: Vec<Sums>,
91}
92
93#[derive(Clone, Copy)]
94struct Sums {
95    squares: f64,
96    count: usize,
97    peak: f64,
98    min: f64,
99    max: f64,
100    product: f64,
101    energy: f64,
102}
103
104impl Sums {
105    fn new() -> Sums {
106        // What an empty `sum` folds from, so a running fold is that sum's bits.
107        let zero: f64 = std::iter::empty::<f64>().sum();
108        Sums {
109            squares: zero,
110            count: 0,
111            peak: 0.0,
112            min: f64::INFINITY,
113            max: f64::NEG_INFINITY,
114            product: zero,
115            energy: zero,
116        }
117    }
118}
119
120impl Tally {
121    pub fn new(
122        sources: &Sources,
123        deps: &BTreeMap<String, Vec<String>>,
124        kinds: &BTreeMap<String, SignalKind>,
125        target: &str,
126        depth_limit: usize,
127    ) -> Tally {
128        let width = |path: &str| sources.read.get(path).map_or(1, |s| sources.widths[*s]);
129        let mut seen: BTreeSet<&str> = BTreeSet::new();
130        let mut queue: VecDeque<(&str, usize, &str)> = VecDeque::new();
131        let mut rows = Vec::new();
132        queue.push_back((target, 0, target));
133        seen.insert(target);
134        while let Some((path, depth, reader)) = queue.pop_front() {
135            // `kinds` names what holds a buffer; a node composed into another holds none.
136            rows.push(Row {
137                node: path.to_string(),
138                depth,
139                kind: kinds.get(path).copied().unwrap_or(SignalKind::Audio),
140                attributed: path == target || sources.attributed.contains(path),
141                read: sources.read.get(path).copied(),
142                reader: sources
143                    .own
144                    .get(reader)
145                    .copied()
146                    .filter(|s| sources.widths[*s] > 0),
147                channels: vec![Sums::new(); width(path)],
148            });
149            if depth >= depth_limit {
150                continue;
151            }
152            for child in deps.get(path).map(Vec::as_slice).unwrap_or(&[]) {
153                if seen.insert(child.as_str()) {
154                    let key = deps
155                        .get_key_value(child.as_str())
156                        .map(|(k, _)| k.as_str())
157                        .unwrap_or(child.as_str());
158                    queue.push_back((key, depth + 1, path));
159                }
160            }
161        }
162        Tally { rows }
163    }
164
165    pub fn add(&mut self, held: &[Option<&Buffer>], range: std::ops::Range<usize>) {
166        let empty: &[f64] = &[];
167        let slice = |source: Option<usize>, c: usize| -> Option<std::borrow::Cow<'_, [f64]>> {
168            let buffer = held[source?]?;
169            Some(buffer.plane_slice(c % buffer.width(), range.clone()))
170        };
171        for row in &mut self.rows {
172            for (c, sums) in row.channels.iter_mut().enumerate() {
173                let x = match row.read {
174                    Some(source) => slice(Some(source), c).expect("a row's own source"),
175                    None => std::borrow::Cow::Borrowed(empty),
176                };
177                sums.count += x.len();
178                sums.squares = x.iter().map(|&v| v * v).fold(sums.squares, |a, b| a + b);
179                sums.peak = x.iter().fold(sums.peak, |a, &v| a.max(v.abs()));
180                sums.min = x.iter().fold(sums.min, |a, &v| a.min(v));
181                sums.max = x.iter().fold(sums.max, |a, &v| a.max(v));
182                if let Some(y) = slice(row.reader, c) {
183                    sums.energy = y.iter().map(|&v| v * v).fold(sums.energy, |a, b| a + b);
184                    let products = x.iter().zip(y.iter()).map(|(a, b)| a * b);
185                    sums.product = products.fold(sums.product, |a, b| a + b);
186                }
187            }
188        }
189    }
190
191    pub fn entries(&self) -> Vec<LedgerEntry> {
192        let mut out = Vec::new();
193        for row in &self.rows {
194            let components = row.channels.len();
195            let gated = row.kind.is_audio();
196            for (c, sums) in row.channels.iter().enumerate() {
197                let rms = match sums.count {
198                    0 => 0.0,
199                    count => (sums.squares / count as f64).sqrt(),
200                };
201                // Over a sum the parts are the whole: one reader's refs add to exactly one.
202                let share = || match (row.reader, sums.energy > 0.0) {
203                    (Some(_), true) => Some(sums.product / sums.energy),
204                    (Some(_), false) => None,
205                    (None, _) => Some(0.0),
206                };
207                out.push(LedgerEntry {
208                    node: row.node.clone(),
209                    channel: (components > 1).then_some(c),
210                    depth: row.depth,
211                    kind: row.kind,
212                    rms,
213                    peak: sums.peak,
214                    min: sums.min,
215                    max: sums.max,
216                    share: (gated && row.attributed).then(share).flatten(),
217                    clipped: gated.then_some(sums.peak > 1.0),
218                });
219            }
220        }
221        out
222    }
223}