Skip to main content

sva_engine/render/
answer.rs

1// Concern: takes one reading off the representation a node declares, a ledger edge by edge | Non-concern: naming the observations (query.rs), the arithmetic of one | IO: (&Render, node) -> Answer
2
3use sva_formula::closed_form::Edge;
4use sva_formula::spectral_sum::atom::{Singular, SpectralAtom};
5use sva_formula::{Line, SpectralSum, Var, d_dt, envelope_read, line_atoms_read};
6use sva_samples::measure::harmony::dissonance::Crowded;
7use sva_samples::measure::harmony::{
8    self, Framing, Moment, chords, dissonance, inharmonicity, measured as framed, tonnetz,
9    voice_leading,
10};
11use sva_samples::measure::{modulation, periodicity, self_similarity, sliding};
12use sva_samples::{
13    Buffer, Consumes, Extent, Peak, PitchFrame, Profile, Source, measure::bands, measure::crest,
14    measure::envelope, measure::formants, measure::loudness, measure::onsets, measure::pitch,
15    measure::spectrum, measure::stereo, measure_alias,
16};
17
18use crate::error::{Diagnostic, EngineError, Located};
19use crate::query::{Answer, Atom, DEFAULT_FRAME_SECS, Harmonic, Output, Representation};
20use crate::refs;
21use crate::render::Render;
22use crate::typing::Value;
23
24/// A closed form answers off its spectral sum and a buffer off its samples; its own `Ty`
25/// decides which, never a flag on the observation.
26pub fn answer(
27    render: &Render,
28    node: sva_formula::NodeId,
29    representation: Representation,
30) -> Result<Answer, EngineError> {
31    let closed = render.tys.ty(node).is_closed_form();
32    let profile = render.config.profile.name;
33    if representation == Representation::Arguments {
34        return Ok(Answer::whole(
35            Output::Arguments(arguments_under(render, node)),
36            Source::Exact,
37            profile,
38            None,
39        ));
40    }
41    if representation == Representation::Bindings {
42        return Ok(Answer::whole(
43            Output::Bindings(render.bindings.get(&node).cloned().unwrap_or_default()),
44            Source::Exact,
45            profile,
46            None,
47        ));
48    }
49    match representation.consumes(closed) {
50        Consumes::ClosedForm => {
51            let found;
52            let sum = match render.symbolic.get(&node) {
53                Some(held) => held,
54                None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
55                    Ok(held) => {
56                        found = held;
57                        &found
58                    }
59                    Err(left) => return Err(unsummed(representation, left)),
60                },
61            };
62            let (value, listed, source) = exact(render, node, representation, sum)
63                .map_err(|left| not_exact(representation, left))?;
64            Ok(Answer {
65                value,
66                source,
67                profile,
68                rate: None,
69                tail_db: listed.tail_db(),
70                dropped: listed.dropped,
71            })
72        }
73        _ => {
74            if render.buffer(node).is_none() {
75                return Err(unmaterialized(render, node, representation));
76            }
77            let buffer = render.output(node)?;
78            Ok(Answer::whole(
79                measured(render, node, representation, &buffer)?,
80                Source::Measured,
81                profile,
82                Some(render.config.rate),
83            ))
84        }
85    }
86}
87
88fn exact(
89    render: &Render,
90    node: sva_formula::NodeId,
91    representation: Representation,
92    sum: &SpectralSum,
93) -> Result<(Output, Listed, Source), EngineError> {
94    let source = Source::Exact;
95    let enumerated = || listed(render, node, sum, &render.config.profile);
96    let (value, listed) = match representation {
97        Representation::Lines => {
98            let listed = enumerated()?;
99            let held = lines(render, node, sum.var, &listed.atoms)?;
100            (Output::Lines(held), listed)
101        }
102        Representation::Spectrum {
103            frame_secs: Some(_),
104            ..
105        } => return Err(unframed(render, node, "spectrum is its lines", "the lines")),
106        Representation::Envelope {
107            frame_secs: Some(_),
108        } => {
109            return Err(unframed(
110                render,
111                node,
112                "envelope is its exact squared magnitude",
113                "the exact envelope",
114            ));
115        }
116        // A pair's spectrum is its whole line list; no estimate, no peaks.
117        Representation::Spectrum {
118            frame_secs: None, ..
119        } => {
120            let listed = enumerated()?;
121            let mut held = lines(render, node, sum.var, &listed.atoms)?;
122            held.sort_by(|a, b| a.hz.total_cmp(&b.hz));
123            (Output::Lines(held), listed)
124        }
125        Representation::Atoms => {
126            let listed = enumerated()?;
127            let found = listed.atoms.iter().map(|a| atom_of(sum.var, a)).collect();
128            (Output::Atoms(found), listed)
129        }
130        // A pair states its partials; `max_notes` is the question, not a cut answer.
131        Representation::Pitch { max_notes, .. } => {
132            let listed = enumerated()?;
133            let held = lines(render, node, sum.var, &listed.atoms)?;
134            let mut found: Vec<Peak> = harmony::partials(held.iter().map(|l| (l.hz, l.amp)))
135                .into_iter()
136                .map(|p| Peak {
137                    hz: p.hz,
138                    db: 20.0 * p.amplitude.log10(),
139                })
140                .collect();
141            found.sort_by(|a, b| b.db.total_cmp(&a.db));
142            let start = render.read_from.ok_or_else(|| {
143                refused(
144                    render,
145                    node,
146                    "engine.not_materialized",
147                    "an exact `pitch` frame starts at the sample the render reads its root \
148                     from, and this render, not asked for `pitch`, decided none"
149                        .to_string(),
150                )
151            })?;
152            let frame = PitchFrame {
153                t_secs: start as f64 / f64::from(render.config.rate),
154                notes: pitch::name_peaks(&found, max_notes),
155            };
156            (Output::Pitch(vec![frame]), listed)
157        }
158        Representation::Derivative => {
159            let slope = d_dt(sum).map_err(|left| {
160                EngineError::of_closed_form(
161                    &left.refusal(),
162                    render.tys.locate(left.origin),
163                    "read the derivative of a lower order or degree",
164                )
165            })?;
166            (Output::Symbolic(Box::new(slope)), Listed::NONE)
167        }
168        Representation::Envelope { frame_secs: None } => {
169            let held =
170                refs::read_through(&render.tys, |t| envelope_read(sum, t)).map_err(|left| {
171                    EngineError::of_closed_form(
172                        &left.refusal(),
173                        render.tys.locate(left.origin),
174                        "read the envelope off sample(...) for a measured one",
175                    )
176                })?;
177            (Output::Symbolic(Box::new(held.squared)), Listed::NONE)
178        }
179        Representation::Harmony {
180            reading,
181            frame_secs,
182            floor_db,
183        } => {
184            let (moments, framing, listed) = exact_moments(render, node, sum, frame_secs)?;
185            let value = harmonic(reading, &moments, framing, floor_db)
186                .map_err(|held| crowded(render.tys.name(node), representation, held))?;
187            (value, listed)
188        }
189        other => return Err(not_a_closed_form(render, node, other)),
190    };
191    Ok((value, listed, source))
192}
193
194/// Each instant on the frame grid over the range, its first component's partials exactly:
195/// every atom a partial at its carrier, `omega/2pi`, its complex value there merged with the
196/// others at that carrier, so a decay, polynomial, Gaussian or window weighs it as it stands.
197fn exact_moments(
198    render: &Render,
199    node: sva_formula::NodeId,
200    sum: &SpectralSum,
201    frame_secs: f64,
202) -> Result<(Vec<Moment>, Framing, Listed), EngineError> {
203    let range = match (render.range, &render.unranged) {
204        (Some(range), _) => range,
205        (None, Some(refused)) => return Err(refused.clone()),
206        (None, None) => unreachable!("a harmonic reading decides its range before it answers"),
207    };
208    let rate = f64::from(render.config.rate);
209    let hop = ((frame_secs * rate).round() as usize).max(1);
210    let first = SpectralSum::of(sum.var, sum.lanes.iter().take(1).cloned().collect());
211    let listed = listed(render, node, &first, &render.config.profile)?;
212    let instants: Vec<f64> = (range.start..range.end)
213        .step_by(hop)
214        .map(|n| n as f64 / rate)
215        .collect();
216    let moments = match sum.var {
217        Var::F => {
218            let held = lines(render, node, Var::F, &listed.atoms)?;
219            let partials = harmony::partials(held.iter().map(|l| (l.hz, l.amp)));
220            instants
221                .iter()
222                .map(|t| Moment {
223                    t_secs: *t,
224                    partials: partials.clone(),
225                })
226                .collect()
227        }
228        Var::T => instants
229            .iter()
230            .map(|t| {
231                let carried = listed.atoms.iter().map(|a| {
232                    let value = (!a.is_delta()).then(|| a.smooth_at(*t)).flatten();
233                    let hz = a.exp.map_or(0.0, |e| e.omega / std::f64::consts::TAU);
234                    value
235                        .map(|v| (hz, v))
236                        .ok_or_else(|| no_partial(render, node, a, *t))
237                });
238                Ok(Moment {
239                    t_secs: *t,
240                    partials: harmony::partials(carried.collect::<Result<Vec<_>, _>>()?),
241                })
242            })
243            .collect::<Result<_, EngineError>>()?,
244    };
245    let framing = Framing {
246        hop_secs: hop as f64 / rate,
247        window: None,
248    };
249    Ok((moments, framing, listed))
250}
251
252/// One harmonic reading off a curve of partials, whichever source took them.
253fn harmonic(
254    reading: Harmonic,
255    moments: &[Moment],
256    framing: Framing,
257    floor_db: f64,
258) -> Result<Output, Crowded> {
259    let chords = || chords::segment(moments, framing.hop_secs, floor_db);
260    Ok(match reading {
261        Harmonic::Dissonance { max_pairs } => Output::Dissonance(Box::new(dissonance::measure(
262            moments, framing, floor_db, max_pairs,
263        )?)),
264        Harmonic::VoiceLeading { norm } => Output::VoiceLeading(Box::new(voice_leading::analyze(
265            chords(),
266            framing,
267            floor_db,
268            norm,
269        ))),
270        Harmonic::TonalDistance => {
271            Output::TonalDistance(Box::new(tonnetz::analyze(chords(), framing, floor_db)))
272        }
273        Harmonic::Inharmonicity => {
274            Output::Inharmonicity(Box::new(inharmonicity::measure(moments, framing, floor_db)))
275        }
276    })
277}
278
279/// An impulse stands at no frequency, and a pole has no value at its own instant.
280fn no_partial(
281    render: &Render,
282    node: sva_formula::NodeId,
283    atom: &SpectralAtom,
284    t: f64,
285) -> EngineError {
286    let mut at = render.tys.locate(atom.origin);
287    if at.node.is_empty() {
288        at = Located::at(render.tys.name(node), None);
289    }
290    let message = match atom.is_delta() {
291        true => "this term is an impulse in `t`, which stands at no one frequency, so it is \
292                 no partial"
293            .to_string(),
294        false => {
295            format!("this term has a pole at {t}s, an instant of the curve, where it has no value")
296        }
297    };
298    EngineError::refused(Diagnostic {
299        code: "read.no_partial_at_instant".to_string(),
300        message,
301        location: at,
302        help: "a harmonic reading of `sample(...)` of it, as `sample(@x([0, 1s]))`, is \
303               measured off frames of its samples instead"
304            .to_string(),
305    })
306}
307
308fn crowded(name: &str, representation: Representation, held: Crowded) -> EngineError {
309    let reading = representation.name();
310    EngineError::refused(Diagnostic {
311        code: "read.too_many_partial_pairs".to_string(),
312        message: format!(
313            "at {}s, {} partials may stand over the floor, {} pairs, past the {} asked for",
314            held.t_secs, held.partials, held.pairs, held.cap
315        ),
316        location: Located::at(name, None),
317        help: format!(
318            "ask for more pairs, as `{reading}(pairs={})`, or raise the floor so fewer partials \
319             pair, as `{reading}(floor=-30db)`",
320            held.pairs
321        ),
322    })
323}
324
325/// Each names the window that would read, and `longer` how to hand it a target that long.
326fn unread(
327    name: &str,
328    representation: Representation,
329    why: sliding::Unread,
330    longer: impl Fn(f64) -> String,
331) -> EngineError {
332    let reading = representation.name();
333    let (code, message, help) = match why {
334        sliding::Unread::WindowUnderTwoPeriods {
335            window_secs,
336            least_secs,
337        } => (
338            "read.window_under_two_periods",
339            format!(
340                "a {window_secs}s window holds under two of the longest period `{reading}` \
341                 reads, {}s",
342                least_secs / 2.0
343            ),
344            format!(
345                "ask a window of {least_secs}s or longer, as `{reading}(window={least_secs}s)`"
346            ),
347        ),
348        sliding::Unread::TargetShorterThanWindow {
349            held_secs,
350            window_secs,
351            least_secs,
352        } => (
353            "read.target_under_window",
354            format!("the target holds {held_secs}s, under the {window_secs}s window"),
355            format!(
356                "{}, or ask a window as short as {least_secs}s, as \
357                 `{reading}(window={least_secs}s)`",
358                longer(window_secs)
359            ),
360        ),
361    };
362    EngineError::refused(Diagnostic {
363        code: code.to_string(),
364        message,
365        location: Located::at(name, None),
366        help,
367    })
368}
369
370/// Names the cell that fits and, through `shorter`, how to hand it a target that short.
371fn too_many_cells(
372    name: &str,
373    held: self_similarity::TooManyCells,
374    shorter: impl Fn(f64) -> String,
375) -> EngineError {
376    EngineError::refused(Diagnostic {
377        code: "read.too_many_cells".to_string(),
378        message: format!(
379            "the target's {}s at cell={}s is {} cells a side, past the {} a matrix holds",
380            held.held_secs, held.cell_secs, held.cells, held.cap
381        ),
382        location: Located::at(name, None),
383        help: format!(
384            "ask a coarser cell, as `self_similarity(cell={}s)`, or {}",
385            held.least_cell_secs,
386            shorter(held.most_secs)
387        ),
388    })
389}
390
391/// A closed form's envelope is exact or refused, never measured unasked: a refusal of its
392/// sum says so, and names `sample(...)` as the way to ask for the measured one.
393fn not_exact(representation: Representation, left: EngineError) -> EngineError {
394    match (representation, left) {
395        (Representation::Envelope { frame_secs: None }, EngineError::Refused(cause)) => {
396            EngineError::refused(Diagnostic {
397                message: format!("this closed form has no exact envelope: {}", cause.message),
398                help: "a measured envelope is asked for explicitly: read `envelope` of \
399                       `sample(...)` of it, as `sample(@x([0, 1s]))`"
400                    .to_string(),
401                ..*cause
402            })
403        }
404        (_, left) => left,
405    }
406}
407
408/// A closed form with no atom sum has no exact partials; the measured ones are asked for
409/// explicitly, never taken in their place.
410fn unsummed(representation: Representation, left: EngineError) -> EngineError {
411    match (representation, left) {
412        (Representation::Harmony { .. }, EngineError::Refused(cause)) => {
413            EngineError::refused(Diagnostic {
414                message: format!(
415                    "`{}` of a closed form reads its exact partials, and it has none: {}",
416                    representation.name(),
417                    cause.message
418                ),
419                help: format!(
420                    "a measured `{}` is asked for explicitly: read it of `sample(...)` of it, as \
421                     `sample(@x([0, 1s]))`",
422                    representation.name()
423                ),
424                ..*cause
425            })
426        }
427        (_, left) => not_exact(representation, left),
428    }
429}
430
431/// A closed form holds no frames, so a written `frame` is refused rather than ignored.
432fn unframed(render: &Render, node: sva_formula::NodeId, is: &str, bare: &str) -> EngineError {
433    EngineError::refused(Diagnostic {
434        code: "engine.observation_needs_samples".to_string(),
435        message: format!("a closed form's {is}, and it has no frames to take it across"),
436        location: Located::at(render.tys.name(node), None),
437        help: format!("drop `frame` for {bare}, or read `sample(...)` of it to measure frames"),
438    })
439}
440
441/// Every atom the spectral sum stands for, beside the terms its series truncated away or left
442/// above the ceiling: `atoms` alone holds only the terms already written out.
443struct Listed {
444    atoms: Vec<SpectralAtom>,
445    dropped: Vec<Line>,
446}
447
448impl Listed {
449    /// A reading that enumerates no series leaves nothing out.
450    const NONE: Listed = Listed {
451        atoms: Vec::new(),
452        dropped: Vec::new(),
453    };
454
455    /// The tail is the loudest line left out against the loudest one kept, so
456    /// it is one ratio over the whole answer, never a per-series one carried upward.
457    fn tail_db(&self) -> Option<f64> {
458        let loudest = |set: &mut dyn Iterator<Item = f64>| set.fold(0.0f64, f64::max);
459        let gone = loudest(&mut self.dropped.iter().map(|l| l.amp.abs()));
460        let kept = loudest(&mut self.atoms.iter().map(|a| a.c.abs()));
461        (gone > 0.0 && kept > 0.0).then(|| 20.0 * (gone / kept).log10())
462    }
463}
464
465/// Each lane's series enumerated under the band a rate-free reading is taken in. A series
466/// whose term no line closed form reads yields nothing, and nothing is not an exact empty answer.
467fn listed(
468    render: &Render,
469    node: sva_formula::NodeId,
470    sum: &SpectralSum,
471    profile: &Profile,
472) -> Result<Listed, EngineError> {
473    let ceiling = profile.ceiling_hz;
474    let band = (ceiling, profile.floor(ceiling), profile.half_lsb());
475    let mut held = Listed::NONE;
476    for lane in &sum.lanes {
477        held.atoms.extend(lane.clone().expanded().atoms);
478        for series in &lane.series {
479            let found = refs::read_through(&render.tys, |t| line_atoms_read(series, band, t));
480            let Some(found) = found else {
481                return Err(unenumerable(render, node));
482            };
483            held.atoms.extend(found.atoms);
484            held.dropped.extend(found.dropped);
485        }
486    }
487    Ok(held)
488}
489
490/// The exact line list of a pair: in `t` a bare turning exponential, in `f` the delta it
491/// duals to. A list every other atom was dropped from answers a spectrum this node has not.
492fn lines(
493    render: &Render,
494    node: sva_formula::NodeId,
495    var: Var,
496    atoms: &[SpectralAtom],
497) -> Result<Vec<Line>, EngineError> {
498    atoms
499        .iter()
500        .map(|a| line_of(var, a).ok_or_else(|| not_a_line(render, node, var, a)))
501        .collect()
502}
503
504fn line_of(var: Var, a: &SpectralAtom) -> Option<Line> {
505    match (var, a.sing) {
506        (Var::F, Singular::Delta { at, order: 0 }) => Some(Line::bare(at, a.c)),
507        (Var::T, Singular::Regular) => {
508            if !a.poly.is_one() || a.gauss.is_some() || a.ind.is_some() || a.pole.is_some() {
509                return None;
510            }
511            match a.exp {
512                // A constant turns at no rate, which is the line at zero hertz.
513                None => Some(Line::bare(0.0, a.c)),
514                Some(e) if e.sigma == 0.0 => Some(Line::bare(e.omega / std::f64::consts::TAU, a.c)),
515                Some(_) => None,
516            }
517        }
518        _ => None,
519    }
520}
521
522/// What gave this atom a width; a turning exponential is the line itself and never among it.
523fn widening(a: &SpectralAtom) -> Vec<&'static str> {
524    let mut held = Vec::new();
525    if !a.poly.is_one() {
526        held.push("a polynomial");
527    }
528    if a.exp.is_some_and(|e| e.sigma != 0.0) {
529        held.push("a decaying exponential");
530    }
531    if a.gauss.is_some() {
532        held.push("a Gaussian");
533    }
534    if a.pole.is_some() {
535        held.push("a pole");
536    }
537    if held.is_empty() {
538        held.push("a delta");
539    }
540    held
541}
542
543/// Convolved into a line, each shape beside the turning exponential answers a band.
544fn not_a_line(
545    render: &Render,
546    node: sva_formula::NodeId,
547    var: Var,
548    atom: &SpectralAtom,
549) -> EngineError {
550    let mut at = render.tys.locate(atom.origin);
551    if at.node.is_empty() {
552        at = Located::at(render.tys.name(node), None);
553    }
554    let message = match atom.ind {
555        _ if var == Var::F => "this term spreads over `f` rather than standing at one \
556                               frequency, and a line in `f` is a delta"
557            .to_string(),
558        Some(window) => format!(
559            "this term is windowed to [{}s, {}s), and a windowed line is that line convolved \
560             with the window's transform, which has a width and is no line",
561            window.l.value(),
562            window.r.value()
563        ),
564        None => format!(
565            "this term carries {}, and a line is a bare turning exponential: each of those \
566             convolves the line with a shape of its own width",
567            widening(atom).join(", ")
568        ),
569    };
570    EngineError::refused(Diagnostic {
571        code: "read.lines_need_unwindowed_lines".to_string(),
572        message,
573        location: at,
574        help: "`atoms` states each term as it stands, `lines` of the node under the window \
575               or envelope lists the lines it multiplies, and `pitch` or `spectrum` of \
576               `sample(...)` of it measures it frame by frame"
577            .to_string(),
578    })
579}
580
581/// One atom as the six factors it is present in, which is what `atoms` answers with.
582pub fn sketch_atom(a: &SpectralAtom) -> String {
583    let factors: Vec<&'static str> = a.present().iter().map(|f| f.as_str()).collect();
584    format!("{} x {}", a.c.abs(), factors.join(" times "))
585}
586
587/// The atom's own fields, read off as it holds them; a constant turns at zero and decays at
588/// none. Adding zero writes a negative zero as the zero it equals.
589fn atom_of(var: Var, a: &SpectralAtom) -> Atom {
590    let (sigma, omega, mu) = a.exp.map_or((0.0, 0.0, 0.0), |e| (e.sigma, e.omega, e.mu));
591    let bounded = |edge: Edge| Some(edge.value() + 0.0).filter(|x| x.is_finite());
592    Atom {
593        var,
594        sketch: sketch_atom(a),
595        amp: a.c,
596        turns: omega / std::f64::consts::TAU + 0.0,
597        decay: 0.0 - sigma,
598        decay_from: mu + 0.0,
599        degree: a.poly.degree,
600        polynomial_at: a.poly.at + 0.0,
601        start: a.ind.and_then(|i| bounded(i.l)),
602        end: a.ind.and_then(|i| bounded(i.r)),
603    }
604}
605
606fn measured(
607    render: &Render,
608    node: sva_formula::NodeId,
609    representation: Representation,
610    buffer: &Buffer,
611) -> Result<Output, EngineError> {
612    Ok(match representation {
613        Representation::Alias { oversample } => {
614            let reference = oversampled(render, behind(render, node)?, oversample)?;
615            Output::Alias(Box::new(worst_alias(buffer, &reference, oversample)))
616        }
617        Representation::Ledger { depth } => Output::Ledger(attributed(render, node, depth)?),
618        other => {
619            return off_buffer(buffer, other).map_err(|fault| match fault {
620                NoReading::NeedsAGraph => not_a_reading(render, node, other),
621                NoReading::TooNarrow { need, held } => too_narrow(render, node, need, held),
622                NoReading::Crowded(held) => crowded(render.tys.name(node), other, held),
623                NoReading::Unread(why) => unread(render.tys.name(node), other, why, |secs| {
624                    format!(
625                        "render an interval of at least {secs}s, as `@x([0, {secs}s])` writes one"
626                    )
627                }),
628                NoReading::TooManyCells(held) => {
629                    too_many_cells(render.tys.name(node), held, |secs| {
630                        format!(
631                            "render an interval of at most {secs}s, as `@x([0, {secs}s])` \
632                             writes one"
633                        )
634                    })
635                }
636            });
637        }
638    })
639}
640
641/// Why a buffer answered nothing: the reading needs the tree the node was built from, or
642/// the buffer is narrower than the reading needs.
643#[derive(Clone, Copy, Debug, PartialEq)]
644pub enum NoReading {
645    NeedsAGraph,
646    TooNarrow { need: usize, held: usize },
647    Crowded(Crowded),
648    Unread(sliding::Unread),
649    TooManyCells(self_similarity::TooManyCells),
650}
651
652/// Every reading a buffer answers on its own, with no graph behind it.
653pub fn off_buffer(buffer: &Buffer, representation: Representation) -> Result<Output, NoReading> {
654    let sr = f64::from(buffer.rate);
655    let start = buffer.origin_secs();
656    let plane = buffer.plane(0);
657    Ok(match representation {
658        Representation::Samples => Output::Samples(Box::new(buffer.clone())),
659        Representation::Spectrum {
660            max_peaks,
661            frame_secs,
662        } => Output::Spectrum(Box::new(spectrum::analyze(
663            plane, sr, max_peaks, frame_secs,
664        ))),
665        Representation::Envelope { frame_secs } => Output::Envelope(envelope::trace(
666            &buffer.slices(),
667            sr,
668            start,
669            frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
670        )),
671        Representation::Derivative => Output::Samples(Box::new(difference(buffer))),
672        Representation::Pitch {
673            max_notes,
674            frame_secs,
675        } => Output::Pitch(pitch::track(plane, sr, start, frame_secs, max_notes)),
676        Representation::Formants {
677            max_formants,
678            frame_secs,
679        } => Output::Formants(formants::track(
680            plane,
681            sr,
682            start,
683            frame_secs,
684            formants::default_order(sr),
685            max_formants,
686        )),
687        Representation::Stereo { frame_secs } => {
688            if buffer.width() < 2 {
689                return Err(NoReading::TooNarrow {
690                    need: 2,
691                    held: buffer.width(),
692                });
693            }
694            let planes: Vec<&[f64]> = (0..buffer.width()).map(|c| buffer.plane(c)).collect();
695            Output::Stereo(Box::new(stereo::analyze(
696                &planes,
697                buffer.width(),
698                sr,
699                start,
700                frame_secs,
701            )))
702        }
703        Representation::Bands => Output::Bands(Box::new(bands::analyze(plane, sr, start))),
704        Representation::Crest => Output::Crest(Box::new(crest::analyze(plane, sr))),
705        Representation::Onsets => Output::Onsets(Box::new(onsets::detect(plane, sr, start))),
706        Representation::Loudness => {
707            let planes: Vec<&[f64]> = (0..buffer.width()).map(|c| buffer.plane(c)).collect();
708            Output::Loudness(Box::new(loudness::analyze(&planes, sr, start)))
709        }
710        Representation::Harmony {
711            reading,
712            frame_secs,
713            floor_db,
714        } => {
715            let (moments, framing) = framed::moments(plane, sr, start, frame_secs);
716            harmonic(reading, &moments, framing, floor_db).map_err(NoReading::Crowded)?
717        }
718        Representation::Periodicity {
719            frame_secs,
720            window_secs,
721            curve,
722        } => Output::Periodicity(Box::new(
723            periodicity::measure(plane, sr, start, frame_secs, window_secs, curve)
724                .map_err(NoReading::Unread)?,
725        )),
726        Representation::Modulation {
727            frame_secs,
728            window_secs,
729            bpm,
730        } => Output::Modulation(Box::new(
731            modulation::measure(plane, sr, start, frame_secs, window_secs, bpm)
732                .map_err(NoReading::Unread)?,
733        )),
734        Representation::SelfSimilarity {
735            frame_secs,
736            cell_secs,
737            smooth_secs,
738            window_secs,
739            floor_db,
740            listed,
741            curve,
742        } => {
743            let asked = self_similarity::Asked {
744                hop_secs: frame_secs,
745                cell_secs,
746                smooth_secs,
747                window_secs,
748                floor_db,
749                listed,
750                curve,
751            };
752            Output::SelfSimilarity(Box::new(
753                self_similarity::measure(plane, sr, start, asked)
754                    .map_err(NoReading::TooManyCells)?,
755            ))
756        }
757        _ => return Err(NoReading::NeedsAGraph),
758    })
759}
760
761/// Every held buffer under the target over the root's range, its own refs beside it, so the
762/// reading can share the target's energy down the tree it was built from. A ref the closed form
763/// adds stands there as what it contributed to the node reading it; one no addend isolates is
764/// left unattributed.
765fn attributed(
766    render: &Render,
767    node: sva_formula::NodeId,
768    depth: usize,
769) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
770    let holds = |id: sva_formula::NodeId| render.buffers.contains_key(&id);
771    let edges = edges_under(render, node, depth, &holds)?;
772    let under: std::collections::BTreeSet<sva_formula::NodeId> = std::iter::once(node)
773        .chain(edges.iter().map(|(_, child)| *child))
774        .collect();
775    let mut deps = std::collections::BTreeMap::new();
776    let mut kinds = std::collections::BTreeMap::new();
777    for id in render.buffers.keys().filter(|id| under.contains(id)) {
778        let name = render.tys.name(*id).to_string();
779        let read = refs_read(render, *id, &holds)?
780            .into_iter()
781            .map(|op| render.tys.name(op).to_string())
782            .collect();
783        deps.insert(name.clone(), read);
784        kinds.insert(name, sva_samples::SignalKind::Audio);
785    }
786    let mut contributed_by = std::collections::BTreeMap::new();
787    for (parent, child) in &edges {
788        if !crate::render::slots::reads_held(render, *parent)? {
789            return Err(unmaterialized(
790                render,
791                node,
792                Representation::Ledger { depth },
793            ));
794        }
795        if let Some(held) = contributed(render, *parent, *child)? {
796            contributed_by.insert(render.tys.name(*child).to_string(), held);
797        }
798    }
799    // A row's own samples are read where it reads rows itself or no addend isolates it.
800    let readers: std::collections::BTreeSet<_> = edges.iter().map(|(parent, _)| *parent).collect();
801    let mut buffers = std::collections::BTreeMap::new();
802    for id in render.buffers.keys().filter(|id| under.contains(id)) {
803        let name = render.tys.name(*id);
804        if *id == node || readers.contains(id) || !contributed_by.contains_key(name) {
805            buffers.insert(name.to_string(), own_samples(render, *id)?);
806        }
807    }
808    let len = render.range.map_or(0, |range| range.len());
809    Ok(sva_samples::measure::ledger::attribute(
810        &buffers,
811        &contributed_by,
812        &deps,
813        &kinds,
814        render.tys.name(node),
815        0..len,
816        depth,
817    ))
818}
819
820/// A row's own samples over the range, held or pulled.
821fn own_samples(render: &Render, id: sva_formula::NodeId) -> Result<Buffer, EngineError> {
822    let range = render.range.expect("a ledger reads a decided range");
823    match render.buffer(id) {
824        Some(_) => render.output(id),
825        None => super::sampled(render, id, range),
826    }
827}
828
829/// What every instance under `node` was lowered with, `node` first, then breadth first. The
830/// walk is over what each node was lowered to, so it needs no buffer.
831fn arguments_under(render: &Render, node: sva_formula::NodeId) -> Vec<crate::Arguments> {
832    let under = |id: sva_formula::NodeId| -> Vec<sva_formula::NodeId> {
833        match render.tys.value(id) {
834            Value::ClosedForm(form) => refs::nodes_in(&form.body),
835            Value::Read { source, .. } | Value::Cast(_, source) => vec![*source],
836            Value::Op { args, .. } => args.clone(),
837            Value::Filter {
838                x, cutoff, q, gain, ..
839            } => vec![*x, *cutoff, *q, *gain],
840            Value::Solver { varying, .. } => varying.iter().map(|(_, a)| *a).collect(),
841            Value::SelfAt { .. } | Value::Noise(_) => Vec::new(),
842        }
843    };
844    let mut names: Vec<&str> = Vec::new();
845    let mut seen = std::collections::BTreeSet::from([node]);
846    let mut level = vec![node];
847    while !level.is_empty() {
848        let mut next = Vec::new();
849        for at in level {
850            let name = render.tys.name(at);
851            if !names.contains(&name) {
852                names.push(name);
853            }
854            next.extend(under(at).into_iter().filter(|c| seen.insert(*c)));
855        }
856        level = next;
857    }
858    names
859        .into_iter()
860        .filter_map(|name| render.tys.arguments(name).cloned())
861        .collect()
862}
863
864/// The tree a ledger walks; a node several read is attributed to the first to reach it.
865fn edges_under(
866    render: &Render,
867    node: sva_formula::NodeId,
868    depth: usize,
869    holds: &dyn Fn(sva_formula::NodeId) -> bool,
870) -> Result<Vec<(sva_formula::NodeId, sva_formula::NodeId)>, EngineError> {
871    let mut seen = std::collections::BTreeSet::from([node]);
872    let (mut level, mut out) = (vec![node], Vec::new());
873    for _ in 0..depth {
874        let mut next = Vec::new();
875        for parent in level {
876            for child in refs_read(render, parent, holds)? {
877                if seen.insert(child) {
878                    out.push((parent, child));
879                    next.push(child);
880                }
881            }
882        }
883        level = next;
884    }
885    Ok(out)
886}
887
888/// Every ref one node's renderer reads, one row per name.
889fn refs_read(
890    render: &Render,
891    node: sva_formula::NodeId,
892    holds: &dyn Fn(sva_formula::NodeId) -> bool,
893) -> Result<Vec<sva_formula::NodeId>, EngineError> {
894    crate::render::slots::refs_read(render, node, holds)
895}
896
897/// What one ref contributed to the node reading it, at that node's own offset and window: its
898/// renderer with every other slot silenced. Two under one product have no addend apiece and no
899/// share either, so that edge is left unattributed.
900fn contributed(
901    render: &Render,
902    parent: sva_formula::NodeId,
903    child: sva_formula::NodeId,
904) -> Result<Option<Buffer>, EngineError> {
905    let range = render.range.expect("a ledger reads a decided range");
906    Ok(crate::render::slots::contributed(render, parent, child)?
907        .map(|held| held.over(range, held.extent())))
908}
909
910/// The closed form an alias score oversamples: the node itself where it is one, and the operand of
911/// the `sample(...)` that collapsed it where it is not.
912fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
913    if render.tys.ty(node).is_closed_form() {
914        return Ok(node);
915    }
916    match render.tys.value(node) {
917        crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
918        _ => Err(refused(
919            render,
920            node,
921            "engine.alias_needs_a_closed_form",
922            "an alias score is a render against the same closed form oversampled, and this node \
923             is samples with no closed form behind it"
924                .to_string(),
925        )),
926    }
927}
928
929/// The score a point sampling's label carries where a reading asks for one.
930pub(super) fn alias_db(
931    render: &Render,
932    node: sva_formula::NodeId,
933    buffer: &Buffer,
934) -> Result<f64, EngineError> {
935    let oversample = sva_samples::ALIAS_OVERSAMPLE as u32;
936    let reference = oversampled(render, behind(render, node)?, oversample)?;
937    Ok(worst_alias(buffer, &reference, oversample).asr_db)
938}
939
940/// Each component scored against its own reference; the worst answers.
941fn worst_alias(buffer: &Buffer, reference: &Buffer, oversample: u32) -> sva_samples::Alias {
942    debug_assert_eq!(
943        buffer.width(),
944        reference.width(),
945        "the reference is the same form at another rate"
946    );
947    let sr = f64::from(buffer.rate);
948    let width = buffer.width().min(reference.width());
949    sva_samples::worst_alias((0..width).map(|c| {
950        measure_alias(
951            buffer.plane(c),
952            reference.plane(c),
953            oversample as usize,
954            sr,
955            buffer.origin_secs(),
956        )
957    }))
958    .expect("a buffer holds at least one component")
959}
960
961/// The node read at a multiple of the rate by its own route: what an alias score is against.
962fn oversampled(
963    render: &Render,
964    node: sva_formula::NodeId,
965    oversample: u32,
966) -> Result<Buffer, EngineError> {
967    let range = render.range.expect("an alias score reads a decided range");
968    let k = i64::from(oversample);
969    let extent = Extent::new(range.start * k, range.end * k);
970    super::finer(render, node, oversample, extent)
971}
972
973/// A first difference on the grid, which is what a derivative is once the closed form is gone.
974fn difference(buffer: &Buffer) -> Buffer {
975    let step = f64::from(buffer.rate);
976    let planes = (0..buffer.width())
977        .map(|c| {
978            let plane = buffer.plane(c);
979            plane
980                .iter()
981                .enumerate()
982                .map(|(i, x)| match i {
983                    0 => 0.0,
984                    _ => (x - plane[i - 1]) * step,
985                })
986                .collect()
987        })
988        .collect();
989    let mut out = Buffer::of_planes(buffer.rate, planes);
990    out.start = buffer.start;
991    out
992}
993
994fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
995    EngineError::refused(Diagnostic {
996        code: code.to_string(),
997        message,
998        location: Located::at(render.tys.name(node), None),
999        help: "ask for a reading this representation answers".to_string(),
1000    })
1001}
1002
1003fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
1004    refused(
1005        render,
1006        node,
1007        "read.series_not_enumerable",
1008        "a series whose term is no line lists no lines, and an empty list would read as \
1009         a node with none"
1010            .to_string(),
1011    )
1012}
1013
1014fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1015    refused(
1016        render,
1017        node,
1018        "engine.observation_needs_samples",
1019        format!(
1020            "`{}` reads samples, and this node is a closed form",
1021            r.name()
1022        ),
1023    )
1024}
1025
1026fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1027    refused(
1028        render,
1029        node,
1030        "engine.observation_not_wired",
1031        format!("`{}` takes no reading off a buffer here", r.name()),
1032    )
1033}
1034
1035fn too_narrow(render: &Render, node: sva_formula::NodeId, need: usize, held: usize) -> EngineError {
1036    refused(
1037        render,
1038        node,
1039        "type.width_mismatch",
1040        format!("this reading needs {need} components and the node holds {held}"),
1041    )
1042}
1043
1044pub(super) fn unheld(render: &Render, node: sva_formula::NodeId) -> EngineError {
1045    refused(
1046        render,
1047        node,
1048        "engine.not_materialized",
1049        "this render held no samples of it".to_string(),
1050    )
1051}
1052
1053fn unmaterialized(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1054    refused(
1055        render,
1056        node,
1057        "engine.not_materialized",
1058        format!("`{}` reads samples this render never held", r.name()),
1059    )
1060}
1061
1062/// One reading off a buffer nothing rendered — an external file, say — under the profile the
1063/// caller names. Every representation needing the graph behind it refuses here.
1064pub fn answer_buffer(
1065    name: &str,
1066    buffer: &Buffer,
1067    representation: Representation,
1068    profile: &'static str,
1069) -> Result<Answer, EngineError> {
1070    let refused = |code: &str, message: String| {
1071        EngineError::refused(Diagnostic {
1072            code: code.to_string(),
1073            message,
1074            location: Located::at(name, None),
1075            help: "ask for a reading a buffer answers on its own".to_string(),
1076        })
1077    };
1078    let value = off_buffer(buffer, representation).map_err(|fault| match fault {
1079        NoReading::NeedsAGraph => refused(
1080            "engine.observation_needs_a_graph",
1081            format!(
1082                "`{}` reads the tree a node was built from, and a file has none",
1083                representation.name()
1084            ),
1085        ),
1086        NoReading::TooNarrow { need, held } => refused(
1087            "type.width_mismatch",
1088            format!("this reading needs {need} components and the file holds {held}"),
1089        ),
1090        NoReading::Crowded(held) => crowded(name, representation, held),
1091        NoReading::Unread(why) => unread(name, representation, why, |secs| {
1092            format!("analyze a file holding at least {secs}s")
1093        }),
1094        NoReading::TooManyCells(held) => too_many_cells(name, held, |secs| {
1095            format!("analyze a file holding at most {secs}s")
1096        }),
1097    })?;
1098    Ok(Answer::whole(
1099        value,
1100        Source::Measured,
1101        profile,
1102        Some(buffer.rate),
1103    ))
1104}