1use sva_formula::spectral_sum::atom::{Singular, SpectralAtom};
4use sva_formula::{AUDIBLE_CEILING_HZ, Line, SpectralSum, Var, d_dt, envelope, line_atoms};
5use sva_samples::{
6 AliasScore, Buffer, Consumes, Horizon, Peak, PitchFrame, Source, measure::bands,
7 measure::crest, measure::envelope, measure::formants, measure::loudness, 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::Flops {
27 return Ok(Answer::whole(
28 Output::Flops(Box::new(crate::flops::tree_at(render, node))),
29 Source::Exact,
30 profile,
31 None,
32 ));
33 }
34 if representation == Representation::Arguments {
35 return Ok(Answer::whole(
36 Output::Arguments(arguments_under(render, node)),
37 Source::Exact,
38 profile,
39 None,
40 ));
41 }
42 if representation == Representation::Bindings {
43 return Ok(Answer::whole(
44 Output::Bindings(render.bindings.get(&node).cloned().unwrap_or_default()),
45 Source::Exact,
46 profile,
47 None,
48 ));
49 }
50 match representation.consumes(closed) {
51 Consumes::ClosedForm => {
52 let found;
53 let sum = match render.symbolic.get(&node) {
54 Some(held) => held,
55 None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
56 Ok(held) => {
57 found = held;
58 &found
59 }
60 Err(left) => return measured_instead(render, node, representation, left),
61 },
62 };
63 let (value, listed, source) = match exact(render, node, representation, sum) {
64 Err(left) => return measured_instead(render, node, representation, left),
65 Ok(held) => held,
66 };
67 Ok(Answer {
68 value,
69 source,
70 profile,
71 rate: None,
72 tail_db: listed.tail_db(),
73 dropped: listed.dropped,
74 })
75 }
76 _ => {
77 let buffer = render
78 .buffers
79 .get(&node)
80 .ok_or_else(|| unmaterialized(render, node, representation))?;
81 Ok(Answer::whole(
82 measured(render, node, representation, buffer)?,
83 Source::Measured,
84 profile,
85 Some(render.config.rate),
86 ))
87 }
88 }
89}
90
91fn measured_instead(
94 render: &Render,
95 node: sva_formula::NodeId,
96 representation: Representation,
97 left: EngineError,
98) -> Result<Answer, EngineError> {
99 match representation {
100 Representation::Envelope { frame_secs } if left.code() == "cast.left_algebra" => {
101 off_the_grid(render, node, frame_secs)
102 }
103 _ => Err(left),
104 }
105}
106
107fn on_the_grid(render: &Render, node: sva_formula::NodeId) -> Result<Buffer, EngineError> {
110 if let Some(held) = render.buffers.get(&node) {
111 return Ok(held.clone());
112 }
113 let (rate, horizon) = (render.config.rate, render.config.horizon);
114 let profile = &render.config.profile;
115 let written = refs::substituted_closed_form(&render.tys, node);
116 let composed;
117 let sum = match render.symbolic.get(&node) {
118 Some(held) => Some(held),
119 None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
120 Ok(held) => {
121 composed = held;
122 Some(&composed)
123 }
124 Err(left) if written.is_none() => return Err(left),
125 Err(_) => None,
126 },
127 };
128 let taken = match sum {
129 Some(sum) => sva_samples::of_spectral_sum_or_point(
130 sum,
131 written.as_ref(),
132 rate,
133 horizon,
134 profile,
135 AliasScore::NotAsked,
136 ),
137 None => sva_samples::render(
138 written
139 .as_ref()
140 .expect("a node with no sum answers off the closed form written above"),
141 rate,
142 horizon,
143 profile,
144 AliasScore::NotAsked,
145 ),
146 };
147 taken
148 .map(|(buffer, _)| buffer)
149 .map_err(|e| refused(render, node, e.code(), e.to_string()))
150}
151
152fn off_the_grid(
153 render: &Render,
154 node: sva_formula::NodeId,
155 frame_secs: Option<f64>,
156) -> Result<Answer, EngineError> {
157 let rate = render.config.rate;
158 let buffer = on_the_grid(render, node)?;
159 Ok(Answer::whole(
160 Output::Envelope(envelope::trace(
161 buffer.plane(0),
162 f64::from(rate),
163 buffer.origin_secs,
164 frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
165 )),
166 Source::Measured,
167 render.config.profile.name,
168 Some(rate),
169 ))
170}
171
172fn exact(
173 render: &Render,
174 node: sva_formula::NodeId,
175 representation: Representation,
176 sum: &SpectralSum,
177) -> Result<(Output, Listed, Source), EngineError> {
178 let source = Source::Exact;
179 let enumerated = || {
180 listed(
181 render,
182 node,
183 sum,
184 render.config.profile.floor(AUDIBLE_CEILING_HZ),
185 render.config.profile.half_lsb(),
186 )
187 };
188 let (value, listed) = match representation {
189 Representation::Lines => {
190 let listed = enumerated()?;
191 let held = lines(render, node, sum.var, &listed.atoms)?;
192 (Output::Lines(held), listed)
193 }
194 Representation::Spectrum {
195 frame_secs: Some(_),
196 ..
197 } => {
198 return Err(refused(
199 render,
200 node,
201 "engine.observation_needs_samples",
202 "a closed form has no frames to take a spectrum across".to_string(),
203 ));
204 }
205 Representation::Spectrum {
207 frame_secs: None, ..
208 } => {
209 let listed = enumerated()?;
210 let mut held = lines(render, node, sum.var, &listed.atoms)?;
211 held.sort_by(|a, b| a.hz.total_cmp(&b.hz));
212 (Output::Lines(held), listed)
213 }
214 Representation::Atoms => {
215 let listed = enumerated()?;
216 let found = listed.atoms.iter().map(sketch_atom).collect();
217 (Output::Atoms(found), listed)
218 }
219 Representation::Pitch { max_notes, .. } => {
221 let listed = enumerated()?;
222 let held = lines(render, node, sum.var, &listed.atoms)?;
223 let mut found: Vec<Peak> = held
224 .iter()
225 .filter(|l| l.hz > 0.0)
226 .map(|l| Peak {
227 hz: l.hz,
228 db: 20.0 * amplitude_at(&held, l.hz).log10(),
229 })
230 .collect();
231 found.sort_by(|a, b| b.db.total_cmp(&a.db));
232 let frame = PitchFrame {
233 t_secs: render.config.horizon.start_secs,
234 notes: pitch::name_peaks(&found, max_notes),
235 };
236 (Output::Pitch(vec![frame]), listed)
237 }
238 Representation::Derivative => (Output::Symbolic(Box::new(d_dt(sum))), Listed::NONE),
239 Representation::Envelope { .. } => {
240 let held = envelope(sum).map_err(|left| {
241 EngineError::of_closed_form(
242 &left.refusal(),
243 render.tys.locate(left.origin),
244 "read the envelope off sample(...) for a measured one",
245 )
246 })?;
247 (Output::Symbolic(Box::new(held.squared)), Listed::NONE)
248 }
249 other => return Err(not_a_closed_form(render, node, other)),
250 };
251 Ok((value, listed, source))
252}
253
254struct Listed {
257 atoms: Vec<SpectralAtom>,
258 dropped: Vec<Line>,
259}
260
261impl Listed {
262 const NONE: Listed = Listed {
264 atoms: Vec::new(),
265 dropped: Vec::new(),
266 };
267
268 fn tail_db(&self) -> Option<f64> {
271 let loudest = |set: &mut dyn Iterator<Item = f64>| set.fold(0.0f64, f64::max);
272 let gone = loudest(&mut self.dropped.iter().map(|l| l.amp.abs()));
273 let kept = loudest(&mut self.atoms.iter().map(|a| a.c.abs()));
274 (gone > 0.0 && kept > 0.0).then(|| 20.0 * (gone / kept).log10())
275 }
276}
277
278fn listed(
281 render: &Render,
282 node: sva_formula::NodeId,
283 sum: &SpectralSum,
284 floor_db: f64,
285 precision: f64,
286) -> Result<Listed, EngineError> {
287 let mut held = Listed::NONE;
288 for lane in &sum.lanes {
289 held.atoms.extend(lane.clone().expanded().atoms);
290 for series in &lane.series {
291 let Some(found) = line_atoms(series, AUDIBLE_CEILING_HZ, floor_db, precision) else {
292 return Err(unenumerable(render, node));
293 };
294 held.atoms.extend(found.atoms);
295 held.dropped.extend(found.dropped);
296 }
297 }
298 Ok(held)
299}
300
301fn lines(
304 render: &Render,
305 node: sva_formula::NodeId,
306 var: Var,
307 atoms: &[SpectralAtom],
308) -> Result<Vec<Line>, EngineError> {
309 atoms
310 .iter()
311 .map(|a| line_of(var, a).ok_or_else(|| not_a_line(render, node, var, a)))
312 .collect()
313}
314
315fn line_of(var: Var, a: &SpectralAtom) -> Option<Line> {
316 match (var, a.sing) {
317 (Var::F, Singular::Delta { at, order: 0 }) => Some(Line::bare(at, a.c)),
318 (Var::T, Singular::Regular) => {
319 if a.poly > 0 || a.gauss.is_some() || a.ind.is_some() || a.pole.is_some() {
320 return None;
321 }
322 match a.exp {
323 None => Some(Line::bare(0.0, a.c)),
325 Some(e) if e.sigma == 0.0 => Some(Line::bare(e.omega / std::f64::consts::TAU, a.c)),
326 Some(_) => None,
327 }
328 }
329 _ => None,
330 }
331}
332
333fn widening(a: &SpectralAtom) -> Vec<&'static str> {
335 let mut held = Vec::new();
336 if a.poly > 0 {
337 held.push("a polynomial");
338 }
339 if a.exp.is_some_and(|e| e.sigma != 0.0) {
340 held.push("a decaying exponential");
341 }
342 if a.gauss.is_some() {
343 held.push("a Gaussian");
344 }
345 if a.pole.is_some() {
346 held.push("a pole");
347 }
348 if held.is_empty() {
349 held.push("a delta");
350 }
351 held
352}
353
354fn not_a_line(
356 render: &Render,
357 node: sva_formula::NodeId,
358 var: Var,
359 atom: &SpectralAtom,
360) -> EngineError {
361 let mut at = render.tys.locate(atom.origin);
362 if at.node.is_empty() {
363 at = Located::at(render.tys.name(node), None);
364 }
365 let message = match atom.ind {
366 _ if var == Var::F => "this term spreads over `f` rather than standing at one \
367 frequency, and a line in `f` is a delta"
368 .to_string(),
369 Some(window) => format!(
370 "this term is windowed to [{}s, {}s), and a windowed line is that line convolved \
371 with the window's transform, which has a width and is no line",
372 window.l.value(),
373 window.r.value()
374 ),
375 None => format!(
376 "this term carries {}, and a line is a bare turning exponential: each of those \
377 convolves the line with a shape of its own width",
378 widening(atom).join(", ")
379 ),
380 };
381 EngineError::refused(Diagnostic {
382 code: "read.lines_need_unwindowed_lines".to_string(),
383 message,
384 location: at,
385 help: "`--as atoms` states each term as it stands, and `--as lines` of the node under \
386 the window or envelope lists the lines it multiplies"
387 .to_string(),
388 })
389}
390
391fn amplitude_at(held: &[Line], hz: f64) -> f64 {
394 held.iter()
395 .filter(|l| (l.hz - hz).abs() <= f64::EPSILON * hz.abs() || l.hz == -hz)
396 .map(|l| l.amp.abs())
397 .sum()
398}
399
400pub fn sketch_atom(a: &SpectralAtom) -> String {
402 let factors: Vec<&'static str> = a.present().iter().map(|f| f.as_str()).collect();
403 format!("{} x {}", a.c.abs(), factors.join(" times "))
404}
405
406fn measured(
407 render: &Render,
408 node: sva_formula::NodeId,
409 representation: Representation,
410 buffer: &Buffer,
411) -> Result<Output, EngineError> {
412 Ok(match representation {
413 Representation::Alias { oversample } => {
414 let reference = oversampled(render, behind(render, node)?, oversample)?;
415 Output::Alias(Box::new(worst_alias(buffer, &reference, oversample)))
416 }
417 Representation::Ledger { depth } => {
418 Output::Ledger(attributed(render, node, depth, 0..buffer.len())?)
419 }
420 other => {
421 return off_buffer(buffer, other).map_err(|fault| match fault {
422 NoReading::NeedsAGraph => not_a_reading(render, node, other),
423 NoReading::TooNarrow { need, held } => too_narrow(render, node, need, held),
424 });
425 }
426 })
427}
428
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
432pub enum NoReading {
433 NeedsAGraph,
434 TooNarrow { need: usize, held: usize },
435}
436
437pub fn off_buffer(buffer: &Buffer, representation: Representation) -> Result<Output, NoReading> {
439 let sr = f64::from(buffer.rate);
440 let start = buffer.origin_secs;
441 let plane = buffer.plane(0);
442 Ok(match representation {
443 Representation::Samples => Output::Samples(Box::new(buffer.clone())),
444 Representation::Spectrum {
445 max_peaks,
446 frame_secs,
447 } => Output::Spectrum(Box::new(spectrum::analyze(
448 plane, sr, max_peaks, frame_secs,
449 ))),
450 Representation::Envelope { frame_secs } => Output::Envelope(envelope::trace(
451 plane,
452 sr,
453 start,
454 frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
455 )),
456 Representation::Derivative => Output::Samples(Box::new(difference(buffer))),
457 Representation::Pitch {
458 max_notes,
459 frame_secs,
460 } => Output::Pitch(pitch::track(plane, sr, start, frame_secs, max_notes)),
461 Representation::Formants {
462 max_formants,
463 frame_secs,
464 } => Output::Formants(formants::track(
465 plane,
466 sr,
467 start,
468 frame_secs,
469 formants::default_order(sr),
470 max_formants,
471 )),
472 Representation::Stereo { frame_secs } => {
473 if buffer.width < 2 {
474 return Err(NoReading::TooNarrow {
475 need: 2,
476 held: buffer.width,
477 });
478 }
479 let planes: Vec<&[f64]> = (0..buffer.width).map(|c| buffer.plane(c)).collect();
480 Output::Stereo(Box::new(stereo::analyze(
481 &planes,
482 buffer.width,
483 sr,
484 start,
485 frame_secs,
486 )))
487 }
488 Representation::Bands => Output::Bands(Box::new(bands::analyze(plane, sr, start))),
489 Representation::Crest => Output::Crest(Box::new(crest::analyze(plane, sr))),
490 Representation::Loudness => {
491 let planes: Vec<&[f64]> = (0..buffer.width).map(|c| buffer.plane(c)).collect();
492 Output::Loudness(Box::new(loudness::analyze(&planes, sr, start)))
493 }
494 _ => return Err(NoReading::NeedsAGraph),
495 })
496}
497
498fn attributed(
502 render: &Render,
503 node: sva_formula::NodeId,
504 depth: usize,
505 range: std::ops::Range<usize>,
506) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
507 let mut buffers = std::collections::BTreeMap::new();
508 let mut deps = std::collections::BTreeMap::new();
509 let mut kinds = std::collections::BTreeMap::new();
510 for (id, buffer) in &render.buffers {
511 let name = render.tys.name(*id).to_string();
512 let read = refs_read(render, *id)?
513 .into_iter()
514 .map(|op| render.tys.name(op).to_string())
515 .collect();
516 deps.insert(name.clone(), read);
517 kinds.insert(name.clone(), sva_samples::SignalKind::Audio);
518 buffers.insert(name, buffer.clone());
519 }
520 let mut contributed_by = std::collections::BTreeMap::new();
521 for (parent, child) in edges_under(render, node, depth)? {
522 if !render.tys.ty(parent).is_closed_form()
523 && !crate::render::slots::reads_held(render, parent)?
524 {
525 return Err(unmaterialized(
526 render,
527 node,
528 Representation::Ledger { depth },
529 ));
530 }
531 if let Some(held) = contributed(render, parent, child)? {
532 contributed_by.insert(render.tys.name(child).to_string(), held);
533 }
534 }
535 Ok(sva_samples::measure::ledger::attribute(
536 &buffers,
537 &contributed_by,
538 &deps,
539 &kinds,
540 render.tys.name(node),
541 range,
542 depth,
543 ))
544}
545
546pub fn ledger_over(
549 render: &Render,
550 node: sva_formula::NodeId,
551 depth: usize,
552 over: Horizon,
553) -> Result<Answer, EngineError> {
554 let representation = Representation::Ledger { depth };
555 let buffer = render
556 .buffers
557 .get(&node)
558 .ok_or_else(|| unmaterialized(render, node, representation))?;
559 Ok(Answer::whole(
560 Output::Ledger(attributed(render, node, depth, buffer.span_of(over))?),
561 Source::Measured,
562 render.config.profile.name,
563 Some(render.config.rate),
564 ))
565}
566
567fn arguments_under(render: &Render, node: sva_formula::NodeId) -> Vec<crate::Arguments> {
570 let under = |id: sva_formula::NodeId| -> Vec<sva_formula::NodeId> {
571 match render.tys.value(id) {
572 Value::ClosedForm(form) => refs::nodes_in(&form.body),
573 Value::Read { source, .. } | Value::Cast(_, source) => vec![*source],
574 Value::Op { args, .. } => args.clone(),
575 Value::Filter {
576 x, cutoff, q, gain, ..
577 } => vec![*x, *cutoff, *q, *gain],
578 Value::SelfAt(_) | Value::Grid(_) | Value::Solver(_) => Vec::new(),
579 }
580 };
581 let mut names: Vec<&str> = Vec::new();
582 let mut seen = std::collections::BTreeSet::from([node]);
583 let mut level = vec![node];
584 while !level.is_empty() {
585 let mut next = Vec::new();
586 for at in level {
587 let name = render.tys.name(at);
588 if !names.contains(&name) {
589 names.push(name);
590 }
591 next.extend(under(at).into_iter().filter(|c| seen.insert(*c)));
592 }
593 level = next;
594 }
595 names
596 .into_iter()
597 .filter_map(|name| render.tys.arguments(name).cloned())
598 .collect()
599}
600
601fn edges_under(
603 render: &Render,
604 node: sva_formula::NodeId,
605 depth: usize,
606) -> Result<Vec<(sva_formula::NodeId, sva_formula::NodeId)>, EngineError> {
607 let mut seen = std::collections::BTreeSet::from([node]);
608 let (mut level, mut out) = (vec![node], Vec::new());
609 for _ in 0..depth {
610 let mut next = Vec::new();
611 for parent in level {
612 for child in refs_read(render, parent)? {
613 if seen.insert(child) {
614 out.push((parent, child));
615 next.push(child);
616 }
617 }
618 }
619 level = next;
620 }
621 Ok(out)
622}
623
624fn refs_read(
626 render: &Render,
627 node: sva_formula::NodeId,
628) -> Result<Vec<sva_formula::NodeId>, EngineError> {
629 match render.tys.ty(node).is_closed_form() {
630 true => Ok(crate::schedule::read_operands(&render.tys, node)),
631 false => crate::render::slots::refs_read(render, node),
632 }
633}
634
635fn contributed(
639 render: &Render,
640 parent: sva_formula::NodeId,
641 child: sva_formula::NodeId,
642) -> Result<Option<Buffer>, EngineError> {
643 if !render.tys.ty(parent).is_closed_form() {
644 return crate::render::slots::contributed(render, parent, child);
645 }
646 let Value::ClosedForm(form) = render.tys.value(parent) else {
647 return Ok(None);
648 };
649 Ok(separable(&form.body, child)
650 .then(|| collapsed(render, parent, &alone(&form.body, child), form.var))
651 .flatten())
652}
653
654fn collapsed(
655 render: &Render,
656 parent: sva_formula::NodeId,
657 body: &sva_formula::Body,
658 var: Var,
659) -> Option<Buffer> {
660 let sum = refs::spectral_sum_of_body(&render.tys, parent, body, var).ok()?;
661 sva_samples::collapse::of_spectral_sum(
662 &sum,
663 render.config.rate,
664 render.config.horizon,
665 &render.config.profile,
666 AliasScore::NotAsked,
667 )
668 .ok()
669 .map(|(buffer, _)| buffer)
670}
671
672fn alone(f: &sva_formula::Body, child: sva_formula::NodeId) -> sva_formula::Body {
673 match f {
674 sva_formula::Body::Node(id) if *id != child => {
675 sva_formula::Body::Const(sva_formula::C64::ZERO)
676 }
677 other => sva_formula::closed_form::map_children(other, |p| {
678 sva_formula::Part::new(p.origin, alone(&p.body, child))
679 }),
680 }
681}
682
683fn separable(f: &sva_formula::Body, child: sva_formula::NodeId) -> bool {
685 let parts = sva_formula::closed_form::children(f);
686 match f {
687 sva_formula::Body::Add(_) => parts.iter().all(|p| separable(&p.body, child)),
688 _ => {
689 let mut holding = parts.iter().filter(|p| !refs::nodes_in(&p.body).is_empty());
690 match (holding.next(), holding.next()) {
691 (None, _) => true,
692 (Some(only), None) => separable(&only.body, child),
693 _ => !refs::nodes_in(f).contains(&child),
694 }
695 }
696 }
697}
698
699fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
702 if render.tys.ty(node).is_closed_form() {
703 return Ok(node);
704 }
705 match render.tys.value(node) {
706 crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
707 _ => Err(refused(
708 render,
709 node,
710 "engine.alias_needs_a_closed_form",
711 "an alias score is a render against the same closed form oversampled, and this node \
712 is samples with no closed form behind it"
713 .to_string(),
714 )),
715 }
716}
717
718fn worst_alias(buffer: &Buffer, reference: &Buffer, oversample: u32) -> sva_samples::Alias {
720 debug_assert_eq!(
721 buffer.width, reference.width,
722 "the reference is the same form at another rate"
723 );
724 let sr = f64::from(buffer.rate);
725 let width = buffer.width.min(reference.width);
726 sva_samples::worst_alias((0..width).map(|c| {
727 measure_alias(
728 buffer.plane(c),
729 reference.plane(c),
730 oversample as usize,
731 sr,
732 buffer.origin_secs,
733 )
734 }))
735 .expect("a buffer holds at least one component")
736}
737
738fn oversampled(
740 render: &Render,
741 node: sva_formula::NodeId,
742 oversample: u32,
743) -> Result<Buffer, EngineError> {
744 let rate = render.config.rate * oversample;
745 let taken = match refs::spectral_sum_of(&render.tys, node, Var::T) {
746 Ok(sum) => sva_samples::of_spectral_sum(
747 &sum,
748 rate,
749 render.config.horizon,
750 &render.config.profile,
751 AliasScore::NotAsked,
752 ),
753 Err(e) => match refs::substituted_closed_form(&render.tys, node) {
754 Some(form) => sva_samples::render(
755 &form,
756 rate,
757 render.config.horizon,
758 &render.config.profile,
759 AliasScore::NotAsked,
760 ),
761 None => return Err(e),
762 },
763 };
764 taken.map(|(buffer, _)| buffer).map_err(|e| {
765 EngineError::refused(Diagnostic {
766 code: e.code().to_string(),
767 message: e.to_string(),
768 location: Located::at(render.tys.name(node), None),
769 help: "an alias score needs a closed form to oversample".to_string(),
770 })
771 })
772}
773
774fn difference(buffer: &Buffer) -> Buffer {
776 let step = f64::from(buffer.rate);
777 let planes = (0..buffer.width)
778 .map(|c| {
779 let plane = buffer.plane(c);
780 plane
781 .iter()
782 .enumerate()
783 .map(|(i, x)| match i {
784 0 => 0.0,
785 _ => (x - plane[i - 1]) * step,
786 })
787 .collect()
788 })
789 .collect();
790 let mut out = Buffer::of_planes(buffer.rate, planes);
791 out.origin_secs = buffer.origin_secs;
792 out
793}
794
795fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
796 EngineError::refused(Diagnostic {
797 code: code.to_string(),
798 message,
799 location: Located::at(render.tys.name(node), None),
800 help: "ask for a reading this representation answers".to_string(),
801 })
802}
803
804fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
805 refused(
806 render,
807 node,
808 "read.series_not_enumerable",
809 "a series whose term is no line lists no lines, and an empty list would read as \
810 a node with none"
811 .to_string(),
812 )
813}
814
815fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
816 refused(
817 render,
818 node,
819 "engine.observation_needs_samples",
820 format!(
821 "`{}` reads samples, and this node is a closed form",
822 r.name()
823 ),
824 )
825}
826
827fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
828 refused(
829 render,
830 node,
831 "engine.observation_not_wired",
832 format!("`{}` takes no reading off a buffer here", r.name()),
833 )
834}
835
836fn too_narrow(render: &Render, node: sva_formula::NodeId, need: usize, held: usize) -> EngineError {
837 refused(
838 render,
839 node,
840 "type.width_mismatch",
841 format!("this reading needs {need} components and the node holds {held}"),
842 )
843}
844
845fn unmaterialized(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
846 refused(
847 render,
848 node,
849 "engine.not_materialized",
850 format!("`{}` reads samples this render never held", r.name()),
851 )
852}
853
854pub fn answer_buffer(
857 name: &str,
858 buffer: &Buffer,
859 representation: Representation,
860 profile: &'static str,
861) -> Result<Answer, EngineError> {
862 let refused = |code: &str, message: String| {
863 EngineError::refused(Diagnostic {
864 code: code.to_string(),
865 message,
866 location: Located::at(name, None),
867 help: "ask for a reading a buffer answers on its own".to_string(),
868 })
869 };
870 let value = off_buffer(buffer, representation).map_err(|fault| match fault {
871 NoReading::NeedsAGraph => refused(
872 "engine.observation_needs_a_graph",
873 format!(
874 "`{}` reads the tree a node was built from, and a file has none",
875 representation.name()
876 ),
877 ),
878 NoReading::TooNarrow { need, held } => refused(
879 "type.width_mismatch",
880 format!("this reading needs {need} components and the file holds {held}"),
881 ),
882 })?;
883 Ok(Answer::whole(
884 value,
885 Source::Measured,
886 profile,
887 Some(buffer.rate),
888 ))
889}