1use 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
24pub 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 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 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
194fn 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
252fn 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
279fn 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
325fn 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
370fn 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
391fn 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
408fn 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
431fn 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
441struct Listed {
444 atoms: Vec<SpectralAtom>,
445 dropped: Vec<Line>,
446}
447
448impl Listed {
449 const NONE: Listed = Listed {
451 atoms: Vec::new(),
452 dropped: Vec::new(),
453 };
454
455 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
465fn 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
490fn 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 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
522fn 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
543fn 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
581pub 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
587fn 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#[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
652pub 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 => {
706 let planes: Vec<&[f64]> = (0..buffer.width()).map(|c| buffer.plane(c)).collect();
707 Output::Onsets(Box::new(onsets::detect(&planes, sr, start)))
708 }
709 Representation::Loudness => {
710 let planes: Vec<&[f64]> = (0..buffer.width()).map(|c| buffer.plane(c)).collect();
711 Output::Loudness(Box::new(loudness::analyze(&planes, sr, start)))
712 }
713 Representation::Harmony {
714 reading,
715 frame_secs,
716 floor_db,
717 } => {
718 let (moments, framing) = framed::moments(plane, sr, start, frame_secs);
719 harmonic(reading, &moments, framing, floor_db).map_err(NoReading::Crowded)?
720 }
721 Representation::Periodicity {
722 frame_secs,
723 window_secs,
724 curve,
725 } => Output::Periodicity(Box::new(
726 periodicity::measure(plane, sr, start, frame_secs, window_secs, curve)
727 .map_err(NoReading::Unread)?,
728 )),
729 Representation::Modulation {
730 frame_secs,
731 window_secs,
732 bpm,
733 } => Output::Modulation(Box::new(
734 modulation::measure(plane, sr, start, frame_secs, window_secs, bpm)
735 .map_err(NoReading::Unread)?,
736 )),
737 Representation::SelfSimilarity {
738 frame_secs,
739 cell_secs,
740 smooth_secs,
741 window_secs,
742 floor_db,
743 listed,
744 curve,
745 } => {
746 let asked = self_similarity::Asked {
747 hop_secs: frame_secs,
748 cell_secs,
749 smooth_secs,
750 window_secs,
751 floor_db,
752 listed,
753 curve,
754 };
755 Output::SelfSimilarity(Box::new(
756 self_similarity::measure(plane, sr, start, asked)
757 .map_err(NoReading::TooManyCells)?,
758 ))
759 }
760 _ => return Err(NoReading::NeedsAGraph),
761 })
762}
763
764fn attributed(
769 render: &Render,
770 node: sva_formula::NodeId,
771 depth: usize,
772) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
773 let holds = |id: sva_formula::NodeId| render.buffers.contains_key(&id);
774 let edges = edges_under(render, node, depth, &holds)?;
775 let under: std::collections::BTreeSet<sva_formula::NodeId> = std::iter::once(node)
776 .chain(edges.iter().map(|(_, child)| *child))
777 .collect();
778 let mut deps = std::collections::BTreeMap::new();
779 let mut kinds = std::collections::BTreeMap::new();
780 for id in render.buffers.keys().filter(|id| under.contains(id)) {
781 let name = render.tys.name(*id).to_string();
782 let read = refs_read(render, *id, &holds)?
783 .into_iter()
784 .map(|op| render.tys.name(op).to_string())
785 .collect();
786 deps.insert(name.clone(), read);
787 kinds.insert(name, sva_samples::SignalKind::Audio);
788 }
789 let mut contributed_by = std::collections::BTreeMap::new();
790 for (parent, child) in &edges {
791 if !crate::render::slots::reads_held(render, *parent)? {
792 return Err(unmaterialized(
793 render,
794 node,
795 Representation::Ledger { depth },
796 ));
797 }
798 if let Some(held) = contributed(render, *parent, *child)? {
799 contributed_by.insert(render.tys.name(*child).to_string(), held);
800 }
801 }
802 let readers: std::collections::BTreeSet<_> = edges.iter().map(|(parent, _)| *parent).collect();
804 let mut buffers = std::collections::BTreeMap::new();
805 for id in render.buffers.keys().filter(|id| under.contains(id)) {
806 let name = render.tys.name(*id);
807 if *id == node || readers.contains(id) || !contributed_by.contains_key(name) {
808 buffers.insert(name.to_string(), own_samples(render, *id)?);
809 }
810 }
811 let len = render.range.map_or(0, |range| range.len());
812 Ok(sva_samples::measure::ledger::attribute(
813 &buffers,
814 &contributed_by,
815 &deps,
816 &kinds,
817 render.tys.name(node),
818 0..len,
819 depth,
820 ))
821}
822
823fn own_samples(render: &Render, id: sva_formula::NodeId) -> Result<Buffer, EngineError> {
825 let range = render.range.expect("a ledger reads a decided range");
826 match render.buffer(id) {
827 Some(_) => render.output(id),
828 None => super::sampled(render, id, range),
829 }
830}
831
832fn arguments_under(render: &Render, node: sva_formula::NodeId) -> Vec<crate::Arguments> {
835 let under = |id: sva_formula::NodeId| -> Vec<sva_formula::NodeId> {
836 match render.tys.value(id) {
837 Value::ClosedForm(form) => refs::nodes_in(&form.body),
838 Value::Read { source, .. } | Value::Cast(_, source) => vec![*source],
839 Value::Op { args, .. } => args.clone(),
840 Value::Filter {
841 x, cutoff, q, gain, ..
842 } => vec![*x, *cutoff, *q, *gain],
843 Value::Solver { varying, .. } => varying.iter().map(|(_, a)| *a).collect(),
844 Value::SelfAt { .. } | Value::Noise(_) => Vec::new(),
845 }
846 };
847 let mut names: Vec<&str> = Vec::new();
848 let mut seen = std::collections::BTreeSet::from([node]);
849 let mut level = vec![node];
850 while !level.is_empty() {
851 let mut next = Vec::new();
852 for at in level {
853 let name = render.tys.name(at);
854 if !names.contains(&name) {
855 names.push(name);
856 }
857 next.extend(under(at).into_iter().filter(|c| seen.insert(*c)));
858 }
859 level = next;
860 }
861 names
862 .into_iter()
863 .filter_map(|name| render.tys.arguments(name).cloned())
864 .collect()
865}
866
867fn edges_under(
869 render: &Render,
870 node: sva_formula::NodeId,
871 depth: usize,
872 holds: &dyn Fn(sva_formula::NodeId) -> bool,
873) -> Result<Vec<(sva_formula::NodeId, sva_formula::NodeId)>, EngineError> {
874 let mut seen = std::collections::BTreeSet::from([node]);
875 let (mut level, mut out) = (vec![node], Vec::new());
876 for _ in 0..depth {
877 let mut next = Vec::new();
878 for parent in level {
879 for child in refs_read(render, parent, holds)? {
880 if seen.insert(child) {
881 out.push((parent, child));
882 next.push(child);
883 }
884 }
885 }
886 level = next;
887 }
888 Ok(out)
889}
890
891fn refs_read(
893 render: &Render,
894 node: sva_formula::NodeId,
895 holds: &dyn Fn(sva_formula::NodeId) -> bool,
896) -> Result<Vec<sva_formula::NodeId>, EngineError> {
897 crate::render::slots::refs_read(render, node, holds)
898}
899
900fn contributed(
904 render: &Render,
905 parent: sva_formula::NodeId,
906 child: sva_formula::NodeId,
907) -> Result<Option<Buffer>, EngineError> {
908 let range = render.range.expect("a ledger reads a decided range");
909 Ok(crate::render::slots::contributed(render, parent, child)?
910 .map(|held| held.over(range, held.extent())))
911}
912
913fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
916 if render.tys.ty(node).is_closed_form() {
917 return Ok(node);
918 }
919 match render.tys.value(node) {
920 crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
921 _ => Err(refused(
922 render,
923 node,
924 "engine.alias_needs_a_closed_form",
925 "an alias score is a render against the same closed form oversampled, and this node \
926 is samples with no closed form behind it"
927 .to_string(),
928 )),
929 }
930}
931
932pub(super) fn alias_db(
934 render: &Render,
935 node: sva_formula::NodeId,
936 buffer: &Buffer,
937) -> Result<f64, EngineError> {
938 let oversample = sva_samples::ALIAS_OVERSAMPLE as u32;
939 let reference = oversampled(render, behind(render, node)?, oversample)?;
940 Ok(worst_alias(buffer, &reference, oversample).asr_db)
941}
942
943fn worst_alias(buffer: &Buffer, reference: &Buffer, oversample: u32) -> sva_samples::Alias {
945 debug_assert_eq!(
946 buffer.width(),
947 reference.width(),
948 "the reference is the same form at another rate"
949 );
950 let sr = f64::from(buffer.rate);
951 let width = buffer.width().min(reference.width());
952 sva_samples::worst_alias((0..width).map(|c| {
953 measure_alias(
954 buffer.plane(c),
955 reference.plane(c),
956 oversample as usize,
957 sr,
958 buffer.origin_secs(),
959 )
960 }))
961 .expect("a buffer holds at least one component")
962}
963
964fn oversampled(
966 render: &Render,
967 node: sva_formula::NodeId,
968 oversample: u32,
969) -> Result<Buffer, EngineError> {
970 let range = render.range.expect("an alias score reads a decided range");
971 let k = i64::from(oversample);
972 let extent = Extent::new(range.start * k, range.end * k);
973 super::finer(render, node, oversample, extent)
974}
975
976fn difference(buffer: &Buffer) -> Buffer {
978 let step = f64::from(buffer.rate);
979 let planes = (0..buffer.width())
980 .map(|c| {
981 let plane = buffer.plane(c);
982 plane
983 .iter()
984 .enumerate()
985 .map(|(i, x)| match i {
986 0 => 0.0,
987 _ => (x - plane[i - 1]) * step,
988 })
989 .collect()
990 })
991 .collect();
992 let mut out = Buffer::of_planes(buffer.rate, planes);
993 out.start = buffer.start;
994 out
995}
996
997fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
998 EngineError::refused(Diagnostic {
999 code: code.to_string(),
1000 message,
1001 location: Located::at(render.tys.name(node), None),
1002 help: "ask for a reading this representation answers".to_string(),
1003 })
1004}
1005
1006fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
1007 refused(
1008 render,
1009 node,
1010 "read.series_not_enumerable",
1011 "a series whose term is no line lists no lines, and an empty list would read as \
1012 a node with none"
1013 .to_string(),
1014 )
1015}
1016
1017fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1018 refused(
1019 render,
1020 node,
1021 "engine.observation_needs_samples",
1022 format!(
1023 "`{}` reads samples, and this node is a closed form",
1024 r.name()
1025 ),
1026 )
1027}
1028
1029fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1030 refused(
1031 render,
1032 node,
1033 "engine.observation_not_wired",
1034 format!("`{}` takes no reading off a buffer here", r.name()),
1035 )
1036}
1037
1038fn too_narrow(render: &Render, node: sva_formula::NodeId, need: usize, held: usize) -> EngineError {
1039 refused(
1040 render,
1041 node,
1042 "type.width_mismatch",
1043 format!("this reading needs {need} components and the node holds {held}"),
1044 )
1045}
1046
1047pub(super) fn unheld(render: &Render, node: sva_formula::NodeId) -> EngineError {
1048 refused(
1049 render,
1050 node,
1051 "engine.not_materialized",
1052 "this render held no samples of it".to_string(),
1053 )
1054}
1055
1056fn unmaterialized(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1057 refused(
1058 render,
1059 node,
1060 "engine.not_materialized",
1061 format!("`{}` reads samples this render never held", r.name()),
1062 )
1063}
1064
1065pub fn answer_buffer(
1068 name: &str,
1069 buffer: &Buffer,
1070 representation: Representation,
1071 profile: &'static str,
1072) -> Result<Answer, EngineError> {
1073 let refused = |code: &str, message: String| {
1074 EngineError::refused(Diagnostic {
1075 code: code.to_string(),
1076 message,
1077 location: Located::at(name, None),
1078 help: "ask for a reading a buffer answers on its own".to_string(),
1079 })
1080 };
1081 let value = off_buffer(buffer, representation).map_err(|fault| match fault {
1082 NoReading::NeedsAGraph => refused(
1083 "engine.observation_needs_a_graph",
1084 format!(
1085 "`{}` reads the tree a node was built from, and a file has none",
1086 representation.name()
1087 ),
1088 ),
1089 NoReading::TooNarrow { need, held } => refused(
1090 "type.width_mismatch",
1091 format!("this reading needs {need} components and the file holds {held}"),
1092 ),
1093 NoReading::Crowded(held) => crowded(name, representation, held),
1094 NoReading::Unread(why) => unread(name, representation, why, |secs| {
1095 format!("analyze a file holding at least {secs}s")
1096 }),
1097 NoReading::TooManyCells(held) => too_many_cells(name, held, |secs| {
1098 format!("analyze a file holding at most {secs}s")
1099 }),
1100 })?;
1101 Ok(Answer::whole(
1102 value,
1103 Source::Measured,
1104 profile,
1105 Some(buffer.rate),
1106 ))
1107}