1use 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
17pub 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
83fn 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
99pub(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 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 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
239struct Listed {
242 atoms: Vec<SpectralAtom>,
243 dropped: Vec<Line>,
244}
245
246impl Listed {
247 const NONE: Listed = Listed {
249 atoms: Vec::new(),
250 dropped: Vec::new(),
251 };
252
253 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
263fn 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
288fn 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 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
320fn 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
341fn 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
379fn 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
388pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
418pub enum NoReading {
419 NeedsAGraph,
420 TooNarrow { need: usize, held: usize },
421}
422
423pub 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
485fn 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 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
544fn 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
553fn 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
588fn 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
612fn 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
621fn 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
634fn 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
653pub(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
664fn 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
685fn 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
697fn 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
786pub 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}