Skip to main content

sva_samples/measure/
ledger.rs

1// Concern: attributes a target's energy down its ref tree, one entry per node | Non-concern: rendering the buffers it reads | IO: (buffers, deps, kinds, target) -> entries
2
3use std::collections::{BTreeMap, BTreeSet, VecDeque};
4
5use crate::buffer::Buffer;
6use crate::measure::envelope::rms;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Unit {
10    Amplitude,
11    Hertz,
12    Q,
13    Decibels,
14    Seconds,
15    Radians,
16    Ratio,
17}
18
19impl Unit {
20    pub fn name(self) -> &'static str {
21        match self {
22            Unit::Amplitude => "amplitude",
23            Unit::Hertz => "hz",
24            Unit::Q => "q",
25            Unit::Decibels => "db",
26            Unit::Seconds => "seconds",
27            Unit::Radians => "radians",
28            Unit::Ratio => "ratio",
29        }
30    }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum SignalKind {
35    Audio,
36    Control(Unit),
37}
38
39impl SignalKind {
40    pub fn is_audio(self) -> bool {
41        self == SignalKind::Audio
42    }
43
44    pub fn unit(self) -> Unit {
45        match self {
46            SignalKind::Audio => Unit::Amplitude,
47            SignalKind::Control(u) => u,
48        }
49    }
50}
51
52#[derive(Clone, Debug, PartialEq)]
53pub struct LedgerEntry {
54    pub node: String,
55    pub channel: Option<usize>,
56    pub depth: usize,
57    pub kind: SignalKind,
58    pub rms: f64,
59    pub peak: f64,
60    pub min: f64,
61    pub max: f64,
62    /// The part of its reader's own energy this node accounts for, so one reader's refs add
63    /// to one over a sum. `None` for a non-audio kind, and for a ref no addend isolates.
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/// One entry per component, off what each node gave the node reading it.
71pub fn attribute(
72    buffers: &BTreeMap<String, Buffer>,
73    contributed: &BTreeMap<String, Buffer>,
74    deps: &BTreeMap<String, Vec<String>>,
75    kinds: &BTreeMap<String, SignalKind>,
76    target: &str,
77    range: std::ops::Range<usize>,
78    depth_limit: usize,
79) -> Vec<LedgerEntry> {
80    let held = |path: &str| contributed.get(path).or_else(|| buffers.get(path));
81    let slice = |path: &str, c: usize| -> std::borrow::Cow<'_, [f64]> {
82        held(path).map_or(std::borrow::Cow::Borrowed(&[][..]), |b| {
83            b.window(c, range.clone())
84        })
85    };
86    let width = |path: &str| held(path).map_or(1, |b| b.width);
87    // Over a sum the parts are the whole: one reader's refs add to exactly one.
88    let share_of = |reader: &str, x: &[f64], c: usize| -> f64 {
89        let Some(buffer) = buffers.get(reader).filter(|b| b.width > 0) else {
90            return 0.0;
91        };
92        let y = buffer.window(c % buffer.width, range.clone());
93        let energy: f64 = y.iter().map(|&v| v * v).sum();
94        match energy > 0.0 {
95            true => x.iter().zip(y.iter()).map(|(a, b)| a * b).sum::<f64>() / energy,
96            false => 0.0,
97        }
98    };
99
100    let mut seen: BTreeSet<&str> = BTreeSet::new();
101    let mut queue: VecDeque<(&str, usize, &str)> = VecDeque::new();
102    let mut out = Vec::new();
103    queue.push_back((target, 0, target));
104    seen.insert(target);
105
106    while let Some((path, depth, reader)) = queue.pop_front() {
107        // `kinds` names what holds a buffer; a node composed into another holds none.
108        let kind = kinds.get(path).copied().unwrap_or(SignalKind::Audio);
109        let components = width(path);
110        let gated = kind.is_audio();
111        let attributed = path == target || contributed.contains_key(path);
112        for c in 0..components {
113            let buf = slice(path, c);
114            let node_rms = rms(&buf);
115            let peak = buf.iter().fold(0f64, |a, &x| a.max(x.abs()));
116            let min = buf.iter().fold(f64::INFINITY, |a, &x| a.min(x));
117            let max = buf.iter().fold(f64::NEG_INFINITY, |a, &x| a.max(x));
118            out.push(LedgerEntry {
119                node: path.to_string(),
120                channel: (components > 1).then_some(c),
121                depth,
122                kind,
123                rms: node_rms,
124                peak,
125                min,
126                max,
127                share: (gated && attributed).then(|| share_of(reader, &buf, c)),
128                clipped: gated.then_some(peak > 1.0),
129            });
130        }
131
132        if depth >= depth_limit {
133            continue;
134        }
135        for child in deps.get(path).map(Vec::as_slice).unwrap_or(&[]) {
136            if seen.insert(child.as_str()) {
137                let key = deps
138                    .get_key_value(child.as_str())
139                    .map(|(k, _)| k.as_str())
140                    .unwrap_or(child.as_str());
141                queue.push_back((key, depth + 1, path));
142            }
143        }
144    }
145    out
146}