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