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