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::spectral_sum::atom::{Singular, SpectralAtom};
4use sva_formula::{Line, SpectralSum, Var, d_dt, envelope_read, line_atoms_read};
5use sva_samples::{
6    Buffer, Consumes, Extent, Peak, PitchFrame, Profile, Source, measure::bands, measure::crest,
7    measure::envelope, measure::formants, measure::loudness, measure::onsets, measure::pitch,
8    measure::spectrum, measure::stereo, measure_alias,
9};
10
11use crate::error::{Diagnostic, EngineError, Located};
12use crate::query::{Answer, DEFAULT_FRAME_SECS, Output, Representation};
13use crate::refs;
14use crate::render::Render;
15use crate::typing::Value;
16
17/// A closed form answers off its spectral sum and a buffer off its samples; its own `Ty`
18/// decides which, never a flag on the observation.
19pub fn answer(
20    render: &Render,
21    node: sva_formula::NodeId,
22    representation: Representation,
23) -> Result<Answer, EngineError> {
24    let closed = render.tys.ty(node).is_closed_form();
25    let profile = render.config.profile.name;
26    if representation == Representation::Arguments {
27        return Ok(Answer::whole(
28            Output::Arguments(arguments_under(render, node)),
29            Source::Exact,
30            profile,
31            None,
32        ));
33    }
34    if representation == Representation::Bindings {
35        return Ok(Answer::whole(
36            Output::Bindings(render.bindings.get(&node).cloned().unwrap_or_default()),
37            Source::Exact,
38            profile,
39            None,
40        ));
41    }
42    match representation.consumes(closed) {
43        Consumes::ClosedForm => {
44            let found;
45            let sum = match render.symbolic.get(&node) {
46                Some(held) => held,
47                None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
48                    Ok(held) => {
49                        found = held;
50                        &found
51                    }
52                    Err(left) => return measured_instead(render, node, representation, left),
53                },
54            };
55            let (value, listed, source) = match exact(render, node, representation, sum) {
56                Err(left) => return measured_instead(render, node, representation, left),
57                Ok(held) => held,
58            };
59            Ok(Answer {
60                value,
61                source,
62                profile,
63                rate: None,
64                tail_db: listed.tail_db(),
65                dropped: listed.dropped,
66            })
67        }
68        _ => {
69            if render.buffer(node).is_none() {
70                return Err(unmaterialized(render, node, representation));
71            }
72            let buffer = render.output(node)?;
73            Ok(Answer::whole(
74                measured(render, node, representation, &buffer)?,
75                Source::Measured,
76                profile,
77                Some(render.config.rate),
78            ))
79        }
80    }
81}
82
83/// A closed form no atom sum reaches answers no reading symbolically. An envelope is the one
84/// FORMAT 9.3 already names off `sample(...)`, so it is measured here rather than refused.
85fn measured_instead(
86    render: &Render,
87    node: sva_formula::NodeId,
88    representation: Representation,
89    left: EngineError,
90) -> Result<Answer, EngineError> {
91    match representation {
92        Representation::Envelope { frame_secs } if left.code() == "cast.left_algebra" => {
93            off_the_grid(render, node, frame_secs)
94        }
95        _ => Err(left),
96    }
97}
98
99/// The samples this node reaches: the buffer the render holds, or the collapse it would have run,
100/// down to FORMAT 9.1's row 4 over the written closed form.
101pub(super) fn on_the_grid(
102    render: &Render,
103    node: sva_formula::NodeId,
104) -> Result<Buffer, EngineError> {
105    if render.buffer(node).is_some() {
106        return render.output(node);
107    }
108    let extent = render.range.ok_or_else(|| {
109        render.unranged.clone().unwrap_or_else(|| {
110            refused(
111                render,
112                node,
113                "engine.not_materialized",
114                "a measured envelope reads samples, and this render read none".to_string(),
115            )
116        })
117    })?;
118    collapsed_over(render, node, extent)
119}
120
121fn collapsed_over(
122    render: &Render,
123    node: sva_formula::NodeId,
124    extent: sva_samples::Extent,
125) -> Result<Buffer, EngineError> {
126    super::sampled(render, node, extent)
127}
128
129fn off_the_grid(
130    render: &Render,
131    node: sva_formula::NodeId,
132    frame_secs: Option<f64>,
133) -> Result<Answer, EngineError> {
134    let rate = render.config.rate;
135    let buffer = on_the_grid(render, node)?;
136    Ok(Answer::whole(
137        Output::Envelope(envelope::trace(
138            &buffer.slices(),
139            f64::from(rate),
140            buffer.origin_secs(),
141            frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
142        )),
143        Source::Measured,
144        render.config.profile.name,
145        Some(rate),
146    ))
147}
148
149fn exact(
150    render: &Render,
151    node: sva_formula::NodeId,
152    representation: Representation,
153    sum: &SpectralSum,
154) -> Result<(Output, Listed, Source), EngineError> {
155    let source = Source::Exact;
156    let enumerated = || listed(render, node, sum, &render.config.profile);
157    let (value, listed) = match representation {
158        Representation::Lines => {
159            let listed = enumerated()?;
160            let held = lines(render, node, sum.var, &listed.atoms)?;
161            (Output::Lines(held), listed)
162        }
163        Representation::Spectrum {
164            frame_secs: Some(_),
165            ..
166        } => {
167            return Err(EngineError::refused(Diagnostic {
168                code: "engine.observation_needs_samples".to_string(),
169                message: "a closed form's spectrum is its lines, and it has no frames to \
170                          take a spectrum across"
171                    .to_string(),
172                location: Located::at(render.tys.name(node), None),
173                help: "drop `frame` for the lines, or read `sample(...)` of it to measure \
174                       frames"
175                    .to_string(),
176            }));
177        }
178        // FORMAT 14.1: a pair's spectrum is its whole line list; no estimate, no peaks.
179        Representation::Spectrum {
180            frame_secs: None, ..
181        } => {
182            let listed = enumerated()?;
183            let mut held = lines(render, node, sum.var, &listed.atoms)?;
184            held.sort_by(|a, b| a.hz.total_cmp(&b.hz));
185            (Output::Lines(held), listed)
186        }
187        Representation::Atoms => {
188            let listed = enumerated()?;
189            let found = listed.atoms.iter().map(sketch_atom).collect();
190            (Output::Atoms(found), listed)
191        }
192        // A pair states its partials; `max_notes` is the question, not a cut answer.
193        Representation::Pitch { max_notes, .. } => {
194            let listed = enumerated()?;
195            let held = lines(render, node, sum.var, &listed.atoms)?;
196            let mut found: Vec<Peak> = held
197                .iter()
198                .filter(|l| l.hz > 0.0)
199                .map(|l| Peak {
200                    hz: l.hz,
201                    db: 20.0 * amplitude_at(&held, l.hz).log10(),
202                })
203                .collect();
204            found.sort_by(|a, b| b.db.total_cmp(&a.db));
205            let frame = PitchFrame {
206                t_secs: render
207                    .range
208                    .map_or(0.0, |range| range.start_secs(render.config.rate)),
209                notes: pitch::name_peaks(&found, max_notes),
210            };
211            (Output::Pitch(vec![frame]), listed)
212        }
213        Representation::Derivative => {
214            let slope = d_dt(sum).map_err(|left| {
215                EngineError::of_closed_form(
216                    &left.refusal(),
217                    render.tys.locate(left.origin),
218                    "read the derivative of a lower order or degree",
219                )
220            })?;
221            (Output::Symbolic(Box::new(slope)), Listed::NONE)
222        }
223        Representation::Envelope { .. } => {
224            let held =
225                refs::read_through(&render.tys, |t| envelope_read(sum, t)).map_err(|left| {
226                    EngineError::of_closed_form(
227                        &left.refusal(),
228                        render.tys.locate(left.origin),
229                        "read the envelope off sample(...) for a measured one",
230                    )
231                })?;
232            (Output::Symbolic(Box::new(held.squared)), Listed::NONE)
233        }
234        other => return Err(not_a_closed_form(render, node, other)),
235    };
236    Ok((value, listed, source))
237}
238
239/// Every atom the spectral sum stands for, beside the terms its series truncated away or left
240/// above the ceiling: `atoms` alone holds only the terms already written out.
241struct Listed {
242    atoms: Vec<SpectralAtom>,
243    dropped: Vec<Line>,
244}
245
246impl Listed {
247    /// A reading that enumerates no series leaves nothing out.
248    const NONE: Listed = Listed {
249        atoms: Vec::new(),
250        dropped: Vec::new(),
251    };
252
253    /// FORMAT 9.3: the tail is the loudest line left out against the loudest one kept, so
254    /// it is one ratio over the whole answer, never a per-series one carried upward.
255    fn tail_db(&self) -> Option<f64> {
256        let loudest = |set: &mut dyn Iterator<Item = f64>| set.fold(0.0f64, f64::max);
257        let gone = loudest(&mut self.dropped.iter().map(|l| l.amp.abs()));
258        let kept = loudest(&mut self.atoms.iter().map(|a| a.c.abs()));
259        (gone > 0.0 && kept > 0.0).then(|| 20.0 * (gone / kept).log10())
260    }
261}
262
263/// Each lane's series enumerated under the band a rate-free reading is taken in. A series
264/// whose term no line closed form reads yields nothing, and nothing is not an exact empty answer.
265fn listed(
266    render: &Render,
267    node: sva_formula::NodeId,
268    sum: &SpectralSum,
269    profile: &Profile,
270) -> Result<Listed, EngineError> {
271    let ceiling = profile.ceiling_hz;
272    let band = (ceiling, profile.floor(ceiling), profile.half_lsb());
273    let mut held = Listed::NONE;
274    for lane in &sum.lanes {
275        held.atoms.extend(lane.clone().expanded().atoms);
276        for series in &lane.series {
277            let found = refs::read_through(&render.tys, |t| line_atoms_read(series, band, t));
278            let Some(found) = found else {
279                return Err(unenumerable(render, node));
280            };
281            held.atoms.extend(found.atoms);
282            held.dropped.extend(found.dropped);
283        }
284    }
285    Ok(held)
286}
287
288/// The exact line list of a pair: in `t` a bare turning exponential, in `f` the delta it
289/// duals to. A list every other atom was dropped from answers a spectrum this node has not.
290fn lines(
291    render: &Render,
292    node: sva_formula::NodeId,
293    var: Var,
294    atoms: &[SpectralAtom],
295) -> Result<Vec<Line>, EngineError> {
296    atoms
297        .iter()
298        .map(|a| line_of(var, a).ok_or_else(|| not_a_line(render, node, var, a)))
299        .collect()
300}
301
302fn line_of(var: Var, a: &SpectralAtom) -> Option<Line> {
303    match (var, a.sing) {
304        (Var::F, Singular::Delta { at, order: 0 }) => Some(Line::bare(at, a.c)),
305        (Var::T, Singular::Regular) => {
306            if !a.poly.is_one() || a.gauss.is_some() || a.ind.is_some() || a.pole.is_some() {
307                return None;
308            }
309            match a.exp {
310                // A constant turns at no rate, which is the line at zero hertz.
311                None => Some(Line::bare(0.0, a.c)),
312                Some(e) if e.sigma == 0.0 => Some(Line::bare(e.omega / std::f64::consts::TAU, a.c)),
313                Some(_) => None,
314            }
315        }
316        _ => None,
317    }
318}
319
320/// What gave this atom a width; a turning exponential is the line itself and never among it.
321fn widening(a: &SpectralAtom) -> Vec<&'static str> {
322    let mut held = Vec::new();
323    if !a.poly.is_one() {
324        held.push("a polynomial");
325    }
326    if a.exp.is_some_and(|e| e.sigma != 0.0) {
327        held.push("a decaying exponential");
328    }
329    if a.gauss.is_some() {
330        held.push("a Gaussian");
331    }
332    if a.pole.is_some() {
333        held.push("a pole");
334    }
335    if held.is_empty() {
336        held.push("a delta");
337    }
338    held
339}
340
341/// Convolved into a line, each shape beside the turning exponential answers a band.
342fn not_a_line(
343    render: &Render,
344    node: sva_formula::NodeId,
345    var: Var,
346    atom: &SpectralAtom,
347) -> EngineError {
348    let mut at = render.tys.locate(atom.origin);
349    if at.node.is_empty() {
350        at = Located::at(render.tys.name(node), None);
351    }
352    let message = match atom.ind {
353        _ if var == Var::F => "this term spreads over `f` rather than standing at one \
354                               frequency, and a line in `f` is a delta"
355            .to_string(),
356        Some(window) => format!(
357            "this term is windowed to [{}s, {}s), and a windowed line is that line convolved \
358             with the window's transform, which has a width and is no line",
359            window.l.value(),
360            window.r.value()
361        ),
362        None => format!(
363            "this term carries {}, and a line is a bare turning exponential: each of those \
364             convolves the line with a shape of its own width",
365            widening(atom).join(", ")
366        ),
367    };
368    EngineError::refused(Diagnostic {
369        code: "read.lines_need_unwindowed_lines".to_string(),
370        message,
371        location: at,
372        help: "`atoms` states each term as it stands, `lines` of the node under the window \
373               or envelope lists the lines it multiplies, and `pitch` or `spectrum` of \
374               `sample(...)` of it measures it frame by frame"
375            .to_string(),
376    })
377}
378
379/// The amplitude a partial sounds at: a real wave carries it in a conjugate pair, half in
380/// each, and a measured peak reads the pair's sum.
381fn amplitude_at(held: &[Line], hz: f64) -> f64 {
382    held.iter()
383        .filter(|l| (l.hz - hz).abs() <= f64::EPSILON * hz.abs() || l.hz == -hz)
384        .map(|l| l.amp.abs())
385        .sum()
386}
387
388/// One atom as the six factors it is present in, which is what `atoms` answers with.
389pub fn sketch_atom(a: &SpectralAtom) -> String {
390    let factors: Vec<&'static str> = a.present().iter().map(|f| f.as_str()).collect();
391    format!("{} x {}", a.c.abs(), factors.join(" times "))
392}
393
394fn measured(
395    render: &Render,
396    node: sva_formula::NodeId,
397    representation: Representation,
398    buffer: &Buffer,
399) -> Result<Output, EngineError> {
400    Ok(match representation {
401        Representation::Alias { oversample } => {
402            let reference = oversampled(render, behind(render, node)?, oversample)?;
403            Output::Alias(Box::new(worst_alias(buffer, &reference, oversample)))
404        }
405        Representation::Ledger { depth } => Output::Ledger(attributed(render, node, depth)?),
406        other => {
407            return off_buffer(buffer, other).map_err(|fault| match fault {
408                NoReading::NeedsAGraph => not_a_reading(render, node, other),
409                NoReading::TooNarrow { need, held } => too_narrow(render, node, need, held),
410            });
411        }
412    })
413}
414
415/// Why a buffer answered nothing: the reading needs the tree the node was built from, or
416/// the buffer is narrower than the reading needs.
417#[derive(Clone, Copy, Debug, PartialEq, Eq)]
418pub enum NoReading {
419    NeedsAGraph,
420    TooNarrow { need: usize, held: usize },
421}
422
423/// Every reading a buffer answers on its own, with no graph behind it.
424pub fn off_buffer(buffer: &Buffer, representation: Representation) -> Result<Output, NoReading> {
425    let sr = f64::from(buffer.rate);
426    let start = buffer.origin_secs();
427    let plane = buffer.plane(0);
428    Ok(match representation {
429        Representation::Samples => Output::Samples(Box::new(buffer.clone())),
430        Representation::Spectrum {
431            max_peaks,
432            frame_secs,
433        } => Output::Spectrum(Box::new(spectrum::analyze(
434            plane, sr, max_peaks, frame_secs,
435        ))),
436        Representation::Envelope { frame_secs } => Output::Envelope(envelope::trace(
437            &buffer.slices(),
438            sr,
439            start,
440            frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
441        )),
442        Representation::Derivative => Output::Samples(Box::new(difference(buffer))),
443        Representation::Pitch {
444            max_notes,
445            frame_secs,
446        } => Output::Pitch(pitch::track(plane, sr, start, frame_secs, max_notes)),
447        Representation::Formants {
448            max_formants,
449            frame_secs,
450        } => Output::Formants(formants::track(
451            plane,
452            sr,
453            start,
454            frame_secs,
455            formants::default_order(sr),
456            max_formants,
457        )),
458        Representation::Stereo { frame_secs } => {
459            if buffer.width() < 2 {
460                return Err(NoReading::TooNarrow {
461                    need: 2,
462                    held: buffer.width(),
463                });
464            }
465            let planes: Vec<&[f64]> = (0..buffer.width()).map(|c| buffer.plane(c)).collect();
466            Output::Stereo(Box::new(stereo::analyze(
467                &planes,
468                buffer.width(),
469                sr,
470                start,
471                frame_secs,
472            )))
473        }
474        Representation::Bands => Output::Bands(Box::new(bands::analyze(plane, sr, start))),
475        Representation::Crest => Output::Crest(Box::new(crest::analyze(plane, sr))),
476        Representation::Onsets => Output::Onsets(Box::new(onsets::detect(plane, sr, start))),
477        Representation::Loudness => {
478            let planes: Vec<&[f64]> = (0..buffer.width()).map(|c| buffer.plane(c)).collect();
479            Output::Loudness(Box::new(loudness::analyze(&planes, sr, start)))
480        }
481        _ => return Err(NoReading::NeedsAGraph),
482    })
483}
484
485/// Every held buffer under the target over the root's range, its own refs beside it, so the
486/// reading can share the target's energy down the tree it was built from. A ref the closed form
487/// adds stands there as what it contributed to the node reading it; one no addend isolates is
488/// left unattributed.
489fn attributed(
490    render: &Render,
491    node: sva_formula::NodeId,
492    depth: usize,
493) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
494    let holds = |id: sva_formula::NodeId| render.buffers.contains_key(&id);
495    let edges = edges_under(render, node, depth, &holds)?;
496    let under: std::collections::BTreeSet<sva_formula::NodeId> = std::iter::once(node)
497        .chain(edges.iter().map(|(_, child)| *child))
498        .collect();
499    let mut deps = std::collections::BTreeMap::new();
500    let mut kinds = std::collections::BTreeMap::new();
501    for id in render.buffers.keys().filter(|id| under.contains(id)) {
502        let name = render.tys.name(*id).to_string();
503        let read = refs_read(render, *id, &holds)?
504            .into_iter()
505            .map(|op| render.tys.name(op).to_string())
506            .collect();
507        deps.insert(name.clone(), read);
508        kinds.insert(name, sva_samples::SignalKind::Audio);
509    }
510    let mut contributed_by = std::collections::BTreeMap::new();
511    for (parent, child) in &edges {
512        if !crate::render::slots::reads_held(render, *parent)? {
513            return Err(unmaterialized(
514                render,
515                node,
516                Representation::Ledger { depth },
517            ));
518        }
519        if let Some(held) = contributed(render, *parent, *child)? {
520            contributed_by.insert(render.tys.name(*child).to_string(), held);
521        }
522    }
523    // A row's own samples are read where it reads rows itself or no addend isolates it.
524    let readers: std::collections::BTreeSet<_> = edges.iter().map(|(parent, _)| *parent).collect();
525    let mut buffers = std::collections::BTreeMap::new();
526    for id in render.buffers.keys().filter(|id| under.contains(id)) {
527        let name = render.tys.name(*id);
528        if *id == node || readers.contains(id) || !contributed_by.contains_key(name) {
529            buffers.insert(name.to_string(), own_samples(render, *id)?);
530        }
531    }
532    let len = render.range.map_or(0, |range| range.len());
533    Ok(sva_samples::measure::ledger::attribute(
534        &buffers,
535        &contributed_by,
536        &deps,
537        &kinds,
538        render.tys.name(node),
539        0..len,
540        depth,
541    ))
542}
543
544/// A row's own samples over the range, held or pulled.
545fn own_samples(render: &Render, id: sva_formula::NodeId) -> Result<Buffer, EngineError> {
546    let range = render.range.expect("a ledger reads a decided range");
547    match render.buffer(id) {
548        Some(_) => render.output(id),
549        None => collapsed_over(render, id, range),
550    }
551}
552
553/// What every instance under `node` was lowered with, `node` first, then breadth first. The
554/// walk is over what each node was lowered to, so it needs no buffer.
555fn arguments_under(render: &Render, node: sva_formula::NodeId) -> Vec<crate::Arguments> {
556    let under = |id: sva_formula::NodeId| -> Vec<sva_formula::NodeId> {
557        match render.tys.value(id) {
558            Value::ClosedForm(form) => refs::nodes_in(&form.body),
559            Value::Read { source, .. } | Value::Cast(_, source) => vec![*source],
560            Value::Op { args, .. } => args.clone(),
561            Value::Filter {
562                x, cutoff, q, gain, ..
563            } => vec![*x, *cutoff, *q, *gain],
564            Value::Solver { varying, .. } => varying.iter().map(|(_, a)| *a).collect(),
565            Value::SelfAt { .. } | Value::Noise(_) => Vec::new(),
566        }
567    };
568    let mut names: Vec<&str> = Vec::new();
569    let mut seen = std::collections::BTreeSet::from([node]);
570    let mut level = vec![node];
571    while !level.is_empty() {
572        let mut next = Vec::new();
573        for at in level {
574            let name = render.tys.name(at);
575            if !names.contains(&name) {
576                names.push(name);
577            }
578            next.extend(under(at).into_iter().filter(|c| seen.insert(*c)));
579        }
580        level = next;
581    }
582    names
583        .into_iter()
584        .filter_map(|name| render.tys.arguments(name).cloned())
585        .collect()
586}
587
588/// The tree a ledger walks; a node several read is attributed to the first to reach it.
589fn edges_under(
590    render: &Render,
591    node: sva_formula::NodeId,
592    depth: usize,
593    holds: &dyn Fn(sva_formula::NodeId) -> bool,
594) -> Result<Vec<(sva_formula::NodeId, sva_formula::NodeId)>, EngineError> {
595    let mut seen = std::collections::BTreeSet::from([node]);
596    let (mut level, mut out) = (vec![node], Vec::new());
597    for _ in 0..depth {
598        let mut next = Vec::new();
599        for parent in level {
600            for child in refs_read(render, parent, holds)? {
601                if seen.insert(child) {
602                    out.push((parent, child));
603                    next.push(child);
604                }
605            }
606        }
607        level = next;
608    }
609    Ok(out)
610}
611
612/// Every ref one node's renderer reads, one row per name.
613fn refs_read(
614    render: &Render,
615    node: sva_formula::NodeId,
616    holds: &dyn Fn(sva_formula::NodeId) -> bool,
617) -> Result<Vec<sva_formula::NodeId>, EngineError> {
618    crate::render::slots::refs_read(render, node, holds)
619}
620
621/// What one ref contributed to the node reading it, at that node's own offset and window: its
622/// renderer with every other slot silenced. Two under one product have no addend apiece and no
623/// share either, so that edge is left unattributed.
624fn contributed(
625    render: &Render,
626    parent: sva_formula::NodeId,
627    child: sva_formula::NodeId,
628) -> Result<Option<Buffer>, EngineError> {
629    let range = render.range.expect("a ledger reads a decided range");
630    Ok(crate::render::slots::contributed(render, parent, child)?
631        .map(|held| held.over(range, held.extent())))
632}
633
634/// The closed form an alias score oversamples: the node itself where it is one, and the operand of
635/// the `sample(...)` that collapsed it where it is not.
636fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
637    if render.tys.ty(node).is_closed_form() {
638        return Ok(node);
639    }
640    match render.tys.value(node) {
641        crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
642        _ => Err(refused(
643            render,
644            node,
645            "engine.alias_needs_a_closed_form",
646            "an alias score is a render against the same closed form oversampled, and this node \
647             is samples with no closed form behind it"
648                .to_string(),
649        )),
650    }
651}
652
653/// The score a point sampling's label carries where a reading asks for one.
654pub(super) fn alias_db(
655    render: &Render,
656    node: sva_formula::NodeId,
657    buffer: &Buffer,
658) -> Result<f64, EngineError> {
659    let oversample = sva_samples::ALIAS_OVERSAMPLE as u32;
660    let reference = oversampled(render, behind(render, node)?, oversample)?;
661    Ok(worst_alias(buffer, &reference, oversample).asr_db)
662}
663
664/// Each component scored against its own reference; the worst answers.
665fn worst_alias(buffer: &Buffer, reference: &Buffer, oversample: u32) -> sva_samples::Alias {
666    debug_assert_eq!(
667        buffer.width(),
668        reference.width(),
669        "the reference is the same form at another rate"
670    );
671    let sr = f64::from(buffer.rate);
672    let width = buffer.width().min(reference.width());
673    sva_samples::worst_alias((0..width).map(|c| {
674        measure_alias(
675            buffer.plane(c),
676            reference.plane(c),
677            oversample as usize,
678            sr,
679            buffer.origin_secs(),
680        )
681    }))
682    .expect("a buffer holds at least one component")
683}
684
685/// The node read at a multiple of the rate by its own route: what an alias score is against.
686fn oversampled(
687    render: &Render,
688    node: sva_formula::NodeId,
689    oversample: u32,
690) -> Result<Buffer, EngineError> {
691    let range = render.range.expect("an alias score reads a decided range");
692    let k = i64::from(oversample);
693    let extent = Extent::new(range.start * k, range.end * k);
694    super::finer(render, node, oversample, extent)
695}
696
697/// A first difference on the grid, which is what a derivative is once the closed form is gone.
698fn difference(buffer: &Buffer) -> Buffer {
699    let step = f64::from(buffer.rate);
700    let planes = (0..buffer.width())
701        .map(|c| {
702            let plane = buffer.plane(c);
703            plane
704                .iter()
705                .enumerate()
706                .map(|(i, x)| match i {
707                    0 => 0.0,
708                    _ => (x - plane[i - 1]) * step,
709                })
710                .collect()
711        })
712        .collect();
713    let mut out = Buffer::of_planes(buffer.rate, planes);
714    out.start = buffer.start;
715    out
716}
717
718fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
719    EngineError::refused(Diagnostic {
720        code: code.to_string(),
721        message,
722        location: Located::at(render.tys.name(node), None),
723        help: "ask for a reading this representation answers".to_string(),
724    })
725}
726
727fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
728    refused(
729        render,
730        node,
731        "read.series_not_enumerable",
732        "a series whose term is no line lists no lines, and an empty list would read as \
733         a node with none"
734            .to_string(),
735    )
736}
737
738fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
739    refused(
740        render,
741        node,
742        "engine.observation_needs_samples",
743        format!(
744            "`{}` reads samples, and this node is a closed form",
745            r.name()
746        ),
747    )
748}
749
750fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
751    refused(
752        render,
753        node,
754        "engine.observation_not_wired",
755        format!("`{}` takes no reading off a buffer here", r.name()),
756    )
757}
758
759fn too_narrow(render: &Render, node: sva_formula::NodeId, need: usize, held: usize) -> EngineError {
760    refused(
761        render,
762        node,
763        "type.width_mismatch",
764        format!("this reading needs {need} components and the node holds {held}"),
765    )
766}
767
768pub(super) fn unheld(render: &Render, node: sva_formula::NodeId) -> EngineError {
769    refused(
770        render,
771        node,
772        "engine.not_materialized",
773        "this render held no samples of it".to_string(),
774    )
775}
776
777fn unmaterialized(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
778    refused(
779        render,
780        node,
781        "engine.not_materialized",
782        format!("`{}` reads samples this render never held", r.name()),
783    )
784}
785
786/// One reading off a buffer nothing rendered — an external file, say — under the profile the
787/// caller names. Every representation needing the graph behind it refuses here.
788pub fn answer_buffer(
789    name: &str,
790    buffer: &Buffer,
791    representation: Representation,
792    profile: &'static str,
793) -> Result<Answer, EngineError> {
794    let refused = |code: &str, message: String| {
795        EngineError::refused(Diagnostic {
796            code: code.to_string(),
797            message,
798            location: Located::at(name, None),
799            help: "ask for a reading a buffer answers on its own".to_string(),
800        })
801    };
802    let value = off_buffer(buffer, representation).map_err(|fault| match fault {
803        NoReading::NeedsAGraph => refused(
804            "engine.observation_needs_a_graph",
805            format!(
806                "`{}` reads the tree a node was built from, and a file has none",
807                representation.name()
808            ),
809        ),
810        NoReading::TooNarrow { need, held } => refused(
811            "type.width_mismatch",
812            format!("this reading needs {need} components and the file holds {held}"),
813        ),
814    })?;
815    Ok(Answer::whole(
816        value,
817        Source::Measured,
818        profile,
819        Some(buffer.rate),
820    ))
821}