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::{
12 beating, modulation, partials, periodicity, self_similarity, sharpness, sliding,
13};
14use sva_samples::{
15 Buffer, Consumes, Extent, Peak, PitchFrame, Profile, Source, measure::bands, measure::crest,
16 measure::envelope, measure::formants, measure::loudness, measure::loudness_pattern,
17 measure::pitch, measure::score, measure::spectrum, measure::stereo, measure_alias,
18};
19
20use crate::error::{Diagnostic, EngineError, Located};
21use crate::query::{Answer, Atom, DEFAULT_FRAME_SECS, Harmonic, Output, Representation};
22use crate::refs;
23use crate::render::Render;
24use crate::typing::Value;
25
26pub fn answer(
29 render: &Render,
30 node: sva_formula::NodeId,
31 representation: Representation,
32) -> Result<Answer, EngineError> {
33 let closed = render.tys.ty(node).is_closed_form();
34 let profile = render.config.profile.name;
35 if representation == Representation::Arguments {
36 return Ok(Answer::whole(
37 Output::Arguments(arguments_under(render, node)),
38 Source::Exact,
39 profile,
40 None,
41 ));
42 }
43 if representation == Representation::Bindings {
44 return Ok(Answer::whole(
45 Output::Bindings(render.bindings.get(&node).cloned().unwrap_or_default()),
46 Source::Exact,
47 profile,
48 None,
49 ));
50 }
51 match representation.consumes(closed) {
52 Consumes::ClosedForm => {
53 let found;
54 let sum = match render.symbolic.get(&node) {
55 Some(held) => held,
56 None => match refs::spectral_sum_of(&render.tys, node, render.tys.var(node)) {
57 Ok(held) => {
58 found = held;
59 &found
60 }
61 Err(left) => return Err(unsummed(representation, left)),
62 },
63 };
64 let (value, listed, source) = exact(render, node, representation, sum)
65 .map_err(|left| not_exact(representation, left))?;
66 Ok(Answer {
67 value,
68 source,
69 profile,
70 rate: None,
71 tail_db: listed.tail_db(),
72 dropped: listed.dropped,
73 })
74 }
75 _ => {
76 if render.buffer(node).is_none() {
77 return Err(unmaterialized(render, node, representation));
78 }
79 let buffer = render.output(node)?;
80 Ok(Answer::whole(
81 measured(render, node, representation, &buffer)?,
82 Source::Measured,
83 profile,
84 Some(render.config.rate),
85 ))
86 }
87 }
88}
89
90fn exact(
91 render: &Render,
92 node: sva_formula::NodeId,
93 representation: Representation,
94 sum: &SpectralSum,
95) -> Result<(Output, Listed, Source), EngineError> {
96 let source = Source::Exact;
97 let enumerated = || listed(render, node, sum, &render.config.profile);
98 let (value, listed) = match representation {
99 Representation::Lines => {
100 let listed = enumerated()?;
101 let held = lines(render, node, sum.var, &listed.atoms)?;
102 (Output::Lines(held), listed)
103 }
104 Representation::Spectrum {
105 frame_secs: Some(_),
106 ..
107 } => return Err(unframed(render, node, "spectrum is its lines", "the lines")),
108 Representation::Envelope {
109 frame_secs: Some(_),
110 } => {
111 return Err(unframed(
112 render,
113 node,
114 "envelope is its exact squared magnitude",
115 "the exact envelope",
116 ));
117 }
118 Representation::Spectrum {
120 frame_secs: None, ..
121 } => {
122 let each = per_channel(render, node, sum)?;
123 let mut held = match each.as_slice() {
124 [one] => lines(render, node, sum.var, &one.atoms)?,
125 _ => in_power(
126 each.iter()
127 .map(|one| lines(render, node, sum.var, &one.atoms))
128 .collect::<Result<_, _>>()?,
129 ),
130 };
131 held.sort_by(|a, b| a.hz.total_cmp(&b.hz));
132 (Output::Lines(held), Listed::joined(each))
133 }
134 Representation::Atoms => {
135 let each = per_channel(render, node, sum)?;
136 let found = each
137 .iter()
138 .enumerate()
139 .flat_map(|(c, one)| one.atoms.iter().map(move |a| atom_of(sum.var, a, c)))
140 .collect();
141 (Output::Atoms(found), Listed::joined(each))
142 }
143 Representation::Pitch { max_notes, .. } => {
145 let each = per_channel(render, node, sum)?;
146 let partials = each
147 .iter()
148 .map(|one| {
149 let held = lines(render, node, sum.var, &one.atoms)?;
150 Ok(harmony::partials(held.iter().map(|l| (l.hz, l.amp))))
151 })
152 .collect::<Result<_, EngineError>>()?;
153 let mut found: Vec<Peak> = harmony::averaged(partials)
154 .into_iter()
155 .map(|p| Peak {
156 hz: p.hz,
157 db: 20.0 * libm::log10(p.amplitude),
158 })
159 .collect();
160 found.sort_by(|a, b| b.db.total_cmp(&a.db));
161 let start = render.read_from.ok_or_else(|| {
162 refused(
163 render,
164 node,
165 "engine.not_materialized",
166 "an exact `pitch` frame starts at the sample the render reads its root \
167 from, and this render, not asked for `pitch`, decided none"
168 .to_string(),
169 )
170 })?;
171 let frame = PitchFrame {
172 t_secs: start as f64 / f64::from(render.config.rate),
173 notes: pitch::name_peaks(&found, max_notes),
174 };
175 (Output::Pitch(vec![frame]), Listed::joined(each))
176 }
177 Representation::Derivative => {
179 let each = per_channel(render, node, sum)?;
180 let series_free = SpectralSum::of(
181 sum.var,
182 each.iter()
183 .map(|one| sva_formula::Lane::of(one.atoms.clone()))
184 .collect(),
185 );
186 let slope = d_dt(&series_free).map_err(|left| {
187 EngineError::of_closed_form(
188 &left.refusal(),
189 render.tys.locate(left.origin),
190 "read the derivative of a lower order or degree",
191 )
192 })?;
193 let dropped = Listed::joined(each).dropped.into_iter().map(|l| Line {
194 amp: l.amp * C64::new(0.0, std::f64::consts::TAU * l.hz),
195 ..l
196 });
197 let differentiated = Listed {
198 atoms: slope.atoms().copied().collect(),
199 dropped: dropped.collect(),
200 };
201 (Output::Symbolic(Box::new(slope)), differentiated)
202 }
203 Representation::Envelope { frame_secs: None } => {
204 let held =
205 refs::read_through(&render.tys, |t| envelope_read(sum, t)).map_err(|left| {
206 EngineError::of_closed_form(
207 &left.refusal(),
208 render.tys.locate(left.origin),
209 "read the envelope off sample(...) for a measured one",
210 )
211 })?;
212 (Output::Symbolic(Box::new(held.squared)), Listed::NONE)
213 }
214 Representation::Harmony {
215 reading,
216 frame_secs,
217 floor_db,
218 } => {
219 let (moments, framing, listed, end_secs) =
220 exact_moments(render, node, sum, frame_secs)?;
221 let value = harmonic(reading, &moments, (framing, end_secs), floor_db)
222 .map_err(|held| crowded(render.tys.name(node), representation, held))?;
223 (value, listed)
224 }
225 other => return Err(not_a_closed_form(render, node, other)),
226 };
227 Ok((value, listed, source))
228}
229
230fn exact_moments(
235 render: &Render,
236 node: sva_formula::NodeId,
237 sum: &SpectralSum,
238 frame_secs: f64,
239) -> Result<(Vec<Moment>, Framing, Listed, f64), EngineError> {
240 let range = match (render.range, &render.unranged) {
241 (Some(range), _) => range,
242 (None, Some(refused)) => return Err(refused.clone()),
243 (None, None) => unreachable!("a harmonic reading decides its range before it answers"),
244 };
245 let rate = f64::from(render.config.rate);
246 let hop = ((frame_secs * rate).round() as usize).max(1);
247 let each = per_channel(render, node, sum)?;
248 let instants: Vec<f64> = (range.start..range.end)
249 .step_by(hop)
250 .map(|n| n as f64 / rate)
251 .collect();
252 let moments = match sum.var {
253 Var::F => {
254 let partials = harmony::averaged(
255 each.iter()
256 .map(|one| {
257 let held = lines(render, node, Var::F, &one.atoms)?;
258 Ok(harmony::partials(held.iter().map(|l| (l.hz, l.amp))))
259 })
260 .collect::<Result<_, EngineError>>()?,
261 );
262 instants
263 .iter()
264 .map(|t| Moment {
265 t_secs: *t,
266 partials: partials.clone(),
267 })
268 .collect()
269 }
270 Var::T => instants
271 .iter()
272 .map(|t| {
273 let at = |one: &Listed| {
274 let carried = one.atoms.iter().map(|a| {
275 let value = (!a.is_delta()).then(|| a.smooth_at(*t)).flatten();
276 let hz = a.exp.map_or(0.0, |e| e.omega / std::f64::consts::TAU);
277 value
278 .map(|v| (hz, v))
279 .ok_or_else(|| no_partial(render, node, a, *t))
280 });
281 Ok(harmony::partials(carried.collect::<Result<Vec<_>, _>>()?))
282 };
283 Ok(Moment {
284 t_secs: *t,
285 partials: harmony::averaged(each.iter().map(at).collect::<Result<_, _>>()?),
286 })
287 })
288 .collect::<Result<_, EngineError>>()?,
289 };
290 let framing = Framing {
291 hop_secs: hop as f64 / rate,
292 window: None,
293 };
294 let end_secs = range.end as f64 / rate;
295 Ok((moments, framing, Listed::joined(each), end_secs))
296}
297
298fn harmonic(
301 reading: Harmonic,
302 moments: &[Moment],
303 (framing, end_secs): (Framing, f64),
304 floor_db: f64,
305) -> Result<Output, Crowded> {
306 let chords = || chords::segment(moments, framing.hop_secs, end_secs, floor_db);
307 Ok(match reading {
308 Harmonic::Dissonance { max_pairs } => Output::Dissonance(Box::new(dissonance::measure(
309 moments, framing, floor_db, max_pairs,
310 )?)),
311 Harmonic::VoiceLeading { norm } => Output::VoiceLeading(Box::new(voice_leading::analyze(
312 chords(),
313 framing,
314 floor_db,
315 norm,
316 ))),
317 Harmonic::TonalDistance => {
318 Output::TonalDistance(Box::new(tonnetz::analyze(chords(), framing, floor_db)))
319 }
320 Harmonic::Inharmonicity => {
321 Output::Inharmonicity(Box::new(inharmonicity::measure(moments, framing, floor_db)))
322 }
323 })
324}
325
326fn no_partial(
328 render: &Render,
329 node: sva_formula::NodeId,
330 atom: &SpectralAtom,
331 t: f64,
332) -> EngineError {
333 let mut at = render.tys.locate(atom.origin);
334 if at.node.is_empty() {
335 at = Located::at(render.tys.name(node), None);
336 }
337 let message = match atom.is_delta() {
338 true => "this term is an impulse in `t`, which stands at no one frequency, so it is \
339 no partial"
340 .to_string(),
341 false => {
342 format!("this term has a pole at {t}s, an instant of the curve, where it has no value")
343 }
344 };
345 EngineError::refused(Diagnostic {
346 code: "read.no_partial_at_instant".to_string(),
347 message,
348 location: at,
349 help: "a harmonic reading of `sample(...)` of it, as `sample(@x([0, 1s]))`, is \
350 measured off frames of its samples instead"
351 .to_string(),
352 })
353}
354
355fn crowded(name: &str, representation: Representation, held: Crowded) -> EngineError {
356 let reading = representation.name();
357 EngineError::refused(Diagnostic {
358 code: "read.too_many_partial_pairs".to_string(),
359 message: format!(
360 "at {}s, {} partials may stand over the floor, {} pairs, past the {} asked for",
361 held.t_secs, held.partials, held.pairs, held.cap
362 ),
363 location: Located::at(name, None),
364 help: format!(
365 "ask for more pairs, as `{reading}(pairs={})`, or raise the floor so fewer partials \
366 pair, as `{reading}(floor=-30db)`",
367 held.pairs
368 ),
369 })
370}
371
372fn unread(
375 name: &str,
376 representation: Representation,
377 why: sliding::Unread,
378 longer: impl Fn(f64) -> String,
379 faster: impl Fn(f64) -> String,
380) -> EngineError {
381 let reading = representation.name();
382 let (code, message, help) = match why {
383 sliding::Unread::RateUnderBands { rate_hz, least_hz } => {
384 under_bands(reading, rate_hz, least_hz, faster)
385 }
386 sliding::Unread::WindowUnderTwoPeriods {
387 window_secs,
388 least_secs,
389 } => (
390 "read.window_under_two_periods",
391 format!(
392 "a {window_secs}s window holds under two of the shortest period `{reading}` \
393 reads, {}s",
394 least_secs / 2.0
395 ),
396 format!(
397 "ask a window of {least_secs}s or longer, as `{reading}(window={least_secs}s)`"
398 ),
399 ),
400 sliding::Unread::TargetShorterThanWindow {
401 held_secs,
402 window_secs,
403 least_secs,
404 } => (
405 "read.target_under_window",
406 format!("the target holds {held_secs}s, under the {window_secs}s window"),
407 match least_secs <= held_secs {
408 true => format!(
409 "{}, or ask a window as long as the target, as \
410 `{reading}(window={held_secs}s)`",
411 longer(window_secs)
412 ),
413 false => format!(
414 "{}; no window under {least_secs}s holds two of the shortest period",
415 longer(window_secs)
416 ),
417 },
418 ),
419 };
420 EngineError::refused(Diagnostic {
421 code: code.to_string(),
422 message,
423 location: Located::at(name, None),
424 help,
425 })
426}
427
428fn under_bands(
430 reading: &str,
431 rate_hz: f64,
432 least_hz: f64,
433 faster: impl Fn(f64) -> String,
434) -> (&'static str, String, String) {
435 (
436 "read.rate_under_bands",
437 format!(
438 "`{reading}` reads ERB bands, and {rate_hz} Hz samples place none: the first band's \
439 centre stands at {:.2} Hz, over {} of the rate",
440 least_hz * bands::HIGHEST_CENTRE_OF_RATE,
441 bands::HIGHEST_CENTRE_OF_RATE
442 ),
443 faster((least_hz * 100.0).ceil() / 100.0),
444 )
445}
446
447fn rate_under(
449 name: &str,
450 held: sva_samples::RateUnderWeighting,
451 faster: impl Fn(f64) -> String,
452) -> EngineError {
453 EngineError::refused(Diagnostic {
454 code: "read.rate_under_weighting".to_string(),
455 message: format!(
456 "BS.1770's K-weighting shelf stands at {} Hz, at or past the Nyquist of {} Hz \
457 samples, where its bilinear design has no filter",
458 held.least_hz / 2.0,
459 held.rate_hz
460 ),
461 location: Located::at(name, None),
462 help: faster(held.least_hz),
463 })
464}
465
466fn block_under(name: &str, (window_secs, samples, rate): (f64, usize, u32)) -> EngineError {
467 let least = sva_samples::measure::loudness::LEAST_BLOCK_SAMPLES;
468 let secs = least as f64 / f64::from(rate);
469 EngineError::refused(Diagnostic {
470 code: "read.block_under_samples".to_string(),
471 message: format!(
472 "a {window_secs}s `loudness` window holds {samples} samples at {rate} Hz, under the \
473 {least} a block a quarter of itself apart needs"
474 ),
475 location: Located::at(name, None),
476 help: format!("ask a window of {secs}s or longer, as `loudness(window={secs}s)`"),
477 })
478}
479
480fn under_order(
483 name: &str,
484 why: formants::UnderOrder,
485 longer: impl Fn(f64) -> String,
486) -> EngineError {
487 let (code, message, help) = match why {
488 formants::UnderOrder::Frame {
489 frame_secs,
490 held,
491 order,
492 least_secs,
493 } => (
494 "read.frame_under_order",
495 format!(
496 "a {frame_secs}s frame holds {held} samples, and an order-{order} all-pole fit \
497 reads {} lags",
498 order + 1
499 ),
500 format!("ask a frame of {least_secs}s or longer, as `formants(frame={least_secs}s)`"),
501 ),
502 formants::UnderOrder::Target {
503 held,
504 order,
505 least_secs,
506 } => (
507 "read.target_under_order",
508 format!(
509 "the target holds {held} samples, and an order-{order} all-pole fit reads {} \
510 lags",
511 order + 1
512 ),
513 longer(least_secs),
514 ),
515 };
516 EngineError::refused(Diagnostic {
517 code: code.to_string(),
518 message,
519 location: Located::at(name, None),
520 help,
521 })
522}
523
524fn short_window(name: &str, why: beating::ShortWindow) -> EngineError {
525 let beating::ShortWindow {
526 window_secs,
527 frame_secs,
528 least_secs,
529 } = why;
530 let frame = window_secs / beating::LEAST_FRAMES as f64;
531 EngineError::refused(Diagnostic {
532 code: "read.window_under_frames".to_string(),
533 message: format!(
534 "a {window_secs}s window holds under the {} frames of {frame_secs}s a band's trend \
535 is fitted over",
536 beating::LEAST_FRAMES
537 ),
538 location: Located::at(name, None),
539 help: format!(
540 "ask a window of {least_secs}s or longer, as `beating(window={least_secs}s)`, or a \
541 frame of {frame}s or shorter, as `beating(window={window_secs}s, frame={frame}s)`"
542 ),
543 })
544}
545
546fn unfit(
549 name: &str,
550 why: self_similarity::Unfit,
551 shorter: impl Fn(f64) -> String,
552 faster: impl Fn(f64) -> String,
553) -> EngineError {
554 let (code, message, help) = match why {
555 self_similarity::Unfit::RateUnderBands { rate_hz, least_hz } => {
556 under_bands("self_similarity", rate_hz, least_hz, faster)
557 }
558 self_similarity::Unfit::TooManyCells(held) => (
559 "read.too_many_cells",
560 format!(
561 "the target's {}s at cell={}s is {} cells a side, past the {} a matrix holds",
562 held.held_secs, held.cell_secs, held.cells, held.cap
563 ),
564 format!(
565 "ask a coarser cell, as `self_similarity(cell={}s)`, or {}",
566 held.least_cell_secs,
567 shorter(held.most_secs)
568 ),
569 ),
570 self_similarity::Unfit::CellUnderBandStep {
571 cell_secs,
572 least_cell_secs,
573 band_hz,
574 } => (
575 "read.cell_under_band_step",
576 format!(
577 "a {cell_secs}s cell holds no sample of the {band_hz} Hz band envelopes its \
578 band levels are read off"
579 ),
580 format!(
581 "ask a cell of {least_cell_secs}s or coarser, as \
582 `self_similarity(cell={least_cell_secs}s)`"
583 ),
584 ),
585 self_similarity::Unfit::KernelPastCap {
586 window_secs,
587 kernel_cells,
588 cap,
589 cell_secs,
590 widest_secs,
591 } => (
592 "read.kernel_past_cap",
593 format!(
594 "a {window_secs}s novelty window at cell={cell_secs}s is {kernel_cells} cells \
595 a side, past the {cap} a matrix holds"
596 ),
597 format!(
598 "ask a window of at most {widest_secs}s, as \
599 `self_similarity(window={widest_secs}s)`, or a coarser cell"
600 ),
601 ),
602 };
603 EngineError::refused(Diagnostic {
604 code: code.to_string(),
605 message,
606 location: Located::at(name, None),
607 help,
608 })
609}
610
611fn not_exact(representation: Representation, left: EngineError) -> EngineError {
614 match (representation, left) {
615 (Representation::Envelope { frame_secs: None }, EngineError::Refused(cause)) => {
616 EngineError::refused(Diagnostic {
617 message: format!("this closed form has no exact envelope: {}", cause.message),
618 help: "a measured envelope is asked for explicitly: read `envelope` of \
619 `sample(...)` of it, as `sample(@x([0, 1s]))`"
620 .to_string(),
621 ..*cause
622 })
623 }
624 (_, left) => left,
625 }
626}
627
628fn unsummed(representation: Representation, left: EngineError) -> EngineError {
631 match (representation, left) {
632 (Representation::Harmony { .. }, EngineError::Refused(cause)) => {
633 EngineError::refused(Diagnostic {
634 message: format!(
635 "`{}` of a closed form reads its exact partials, and it has none: {}",
636 representation.name(),
637 cause.message
638 ),
639 help: format!(
640 "a measured `{}` is asked for explicitly: read it of `sample(...)` of it, as \
641 `sample(@x([0, 1s]))`",
642 representation.name()
643 ),
644 ..*cause
645 })
646 }
647 (_, left) => not_exact(representation, left),
648 }
649}
650
651fn unframed(render: &Render, node: sva_formula::NodeId, is: &str, bare: &str) -> EngineError {
653 EngineError::refused(Diagnostic {
654 code: "engine.observation_needs_samples".to_string(),
655 message: format!("a closed form's {is}, and it has no frames to take it across"),
656 location: Located::at(render.tys.name(node), None),
657 help: format!("drop `frame` for {bare}, or read `sample(...)` of it to measure frames"),
658 })
659}
660
661struct Listed {
664 atoms: Vec<SpectralAtom>,
665 dropped: Vec<Line>,
666}
667
668impl Listed {
669 const NONE: Listed = Listed {
671 atoms: Vec::new(),
672 dropped: Vec::new(),
673 };
674
675 fn joined(each: Vec<Listed>) -> Listed {
676 let mut held = Listed::NONE;
677 for one in each {
678 held.atoms.extend(one.atoms);
679 held.dropped.extend(one.dropped);
680 }
681 held
682 }
683
684 fn tail_db(&self) -> Option<f64> {
687 let loudest = |set: &mut dyn Iterator<Item = f64>| set.fold(0.0f64, f64::max);
688 let gone = loudest(&mut self.dropped.iter().map(|l| l.amp.abs()));
689 let kept = loudest(&mut self.atoms.iter().map(|a| a.c.abs()));
690 (gone > 0.0 && kept > 0.0).then(|| 20.0 * libm::log10(gone / kept))
691 }
692}
693
694fn listed(
697 render: &Render,
698 node: sva_formula::NodeId,
699 sum: &SpectralSum,
700 profile: &Profile,
701) -> Result<Listed, EngineError> {
702 let ceiling = profile.ceiling_hz;
703 let band = (ceiling, profile.floor(ceiling), profile.half_lsb());
704 let mut held = Listed::NONE;
705 for lane in &sum.lanes {
706 held.atoms.extend(lane.clone().expanded().atoms);
707 for series in &lane.series {
708 let found = refs::read_through(&render.tys, |t| line_atoms_read(series, band, t));
709 let Some(found) = found else {
710 return Err(unenumerable(render, node));
711 };
712 held.atoms.extend(found.atoms);
713 held.dropped.extend(found.dropped);
714 }
715 }
716 Ok(held)
717}
718
719fn per_channel(
721 render: &Render,
722 node: sva_formula::NodeId,
723 sum: &SpectralSum,
724) -> Result<Vec<Listed>, EngineError> {
725 sum.lanes
726 .iter()
727 .map(|lane| {
728 let one = SpectralSum::of(sum.var, vec![lane.clone()]);
729 listed(render, node, &one, &render.config.profile)
730 })
731 .collect()
732}
733
734fn in_power(per_channel: Vec<Vec<Line>>) -> Vec<Line> {
738 let count = per_channel.len() as f64;
739 let mut power: Vec<(Line, f64)> = Vec::new();
740 for lines in per_channel {
741 let mut channel: Vec<Line> = Vec::new();
742 for line in lines {
743 match channel
744 .iter_mut()
745 .find(|l| l.hz.to_bits() == line.hz.to_bits())
746 {
747 Some(held) => held.amp = held.amp + line.amp,
748 None => channel.push(line),
749 }
750 }
751 for line in channel {
752 let squared = line.amp.abs() * line.amp.abs();
753 match power
754 .iter_mut()
755 .find(|(l, _)| l.hz.to_bits() == line.hz.to_bits())
756 {
757 Some((_, sum)) => *sum += squared,
758 None => power.push((line, squared)),
759 }
760 }
761 }
762 power
763 .into_iter()
764 .filter(|(_, sum)| *sum > 0.0)
765 .map(|(line, sum)| Line {
766 amp: C64::real((sum / count).sqrt()),
767 ..line
768 })
769 .collect()
770}
771
772fn lines(
775 render: &Render,
776 node: sva_formula::NodeId,
777 var: Var,
778 atoms: &[SpectralAtom],
779) -> Result<Vec<Line>, EngineError> {
780 atoms
781 .iter()
782 .map(|a| line_of(var, a).ok_or_else(|| not_a_line(render, node, var, a)))
783 .collect()
784}
785
786fn line_of(var: Var, a: &SpectralAtom) -> Option<Line> {
787 match (var, a.sing) {
788 (Var::F, Singular::Delta { at, order: 0 }) => Some(Line::bare(at, a.c)),
789 (Var::T, Singular::Regular) => {
790 if !a.poly.is_one() || a.gauss.is_some() || a.ind.is_some() || a.pole.is_some() {
791 return None;
792 }
793 match a.exp {
794 None => Some(Line::bare(0.0, a.c)),
796 Some(e) if e.sigma == 0.0 => Some(Line::bare(e.omega / std::f64::consts::TAU, a.c)),
797 Some(_) => None,
798 }
799 }
800 _ => None,
801 }
802}
803
804fn widening(a: &SpectralAtom) -> Vec<&'static str> {
806 let mut held = Vec::new();
807 if !a.poly.is_one() {
808 held.push("a polynomial");
809 }
810 if a.exp.is_some_and(|e| e.sigma != 0.0) {
811 held.push("a decaying exponential");
812 }
813 if a.gauss.is_some() {
814 held.push("a Gaussian");
815 }
816 if a.pole.is_some() {
817 held.push("a pole");
818 }
819 if held.is_empty() {
820 held.push("a delta");
821 }
822 held
823}
824
825fn not_a_line(
827 render: &Render,
828 node: sva_formula::NodeId,
829 var: Var,
830 atom: &SpectralAtom,
831) -> EngineError {
832 let mut at = render.tys.locate(atom.origin);
833 if at.node.is_empty() {
834 at = Located::at(render.tys.name(node), None);
835 }
836 let message = match atom.ind {
837 _ if var == Var::F => "this term spreads over `f` rather than standing at one \
838 frequency, and a line in `f` is a delta"
839 .to_string(),
840 Some(window) => format!(
841 "this term is windowed to [{}s, {}s), and a windowed line is that line convolved \
842 with the window's transform, which has a width and is no line",
843 window.l.value(),
844 window.r.value()
845 ),
846 None => format!(
847 "this term carries {}, and a line is a bare turning exponential: each of those \
848 convolves the line with a shape of its own width",
849 widening(atom).join(", ")
850 ),
851 };
852 EngineError::refused(Diagnostic {
853 code: "read.lines_need_unwindowed_lines".to_string(),
854 message,
855 location: at,
856 help: "`atoms` states each term as it stands, `lines` of the node under the window \
857 or envelope lists the lines it multiplies, and `pitch` or `spectrum` of \
858 `sample(...)` of it measures it frame by frame"
859 .to_string(),
860 })
861}
862
863fn sketch_atom(a: &SpectralAtom) -> String {
865 let factors: Vec<&'static str> = a.present().iter().map(|f| f.as_str()).collect();
866 format!("{} x {}", a.c.abs(), factors.join(" times "))
867}
868
869pub fn atoms_of(sum: &SpectralSum) -> Vec<Atom> {
871 sum.lanes
872 .iter()
873 .enumerate()
874 .flat_map(|(c, lane)| lane.atoms.iter().map(move |a| atom_of(sum.var, a, c)))
875 .collect()
876}
877
878fn atom_of(var: Var, a: &SpectralAtom, channel: usize) -> Atom {
881 let (sigma, omega, mu) = a.exp.map_or((0.0, 0.0, 0.0), |e| (e.sigma, e.omega, e.mu));
882 let bounded = |edge: Edge| Some(edge.value() + 0.0).filter(|x| x.is_finite());
883 Atom {
884 var,
885 channel,
886 sketch: sketch_atom(a),
887 amp: a.c,
888 turns: omega / std::f64::consts::TAU + 0.0,
889 decay: 0.0 - sigma,
890 decay_from: mu + 0.0,
891 degree: a.poly.degree,
892 polynomial_at: a.poly.at + 0.0,
893 start: a.ind.and_then(|i| bounded(i.l)),
894 end: a.ind.and_then(|i| bounded(i.r)),
895 }
896}
897
898fn measured(
899 render: &Render,
900 node: sva_formula::NodeId,
901 representation: Representation,
902 buffer: &Buffer,
903) -> Result<Output, EngineError> {
904 Ok(match representation {
905 Representation::Alias { oversample } => {
906 let reference = oversampled(render, behind(render, node)?, oversample)?;
907 Output::Alias(Box::new(worst_alias(
908 render, node, buffer, &reference, oversample,
909 )?))
910 }
911 Representation::Ledger { depth } => Output::Ledger(attributed(render, node, depth)?),
912 other => {
913 let name = render.tys.name(node);
914 let longer = |secs: f64| {
915 format!("render an interval of at least {secs}s, as `@x([0, {secs}s])` writes one")
916 };
917 let shorter = |secs: f64| {
918 format!("render an interval of at most {secs}s, as `@x([0, {secs}s])` writes one")
919 };
920 let faster = |hz: f64| format!("render at a rate over {hz} Hz, as `--rate 48000` asks");
921 return off_buffer(buffer, other).map_err(|fault| match fault {
922 NoReading::NeedsAGraph => not_a_reading(render, node, other),
923 NoReading::WidthMismatch { need, held } => width_mismatch(render, node, need, held),
924 NoReading::Crowded(held) => crowded(name, other, held),
925 NoReading::Unread(why) => unread(name, other, why, longer, faster),
926 NoReading::RateUnder(held) => rate_under(name, held, faster),
927 NoReading::BlockUnder {
928 window_secs,
929 samples,
930 rate,
931 } => block_under(name, (window_secs, samples, rate)),
932 NoReading::UnderOrder(why) => under_order(name, why, longer),
933 NoReading::Unfit(why) => unfit(name, why, shorter, faster),
934 NoReading::ShortWindow(why) => short_window(name, why),
935 });
936 }
937 })
938}
939
940#[derive(Clone, Copy, Debug, PartialEq)]
943pub enum NoReading {
944 NeedsAGraph,
945 WidthMismatch {
946 need: usize,
947 held: usize,
948 },
949 Crowded(Crowded),
950 Unread(sliding::Unread),
951 Unfit(self_similarity::Unfit),
952 RateUnder(sva_samples::RateUnderWeighting),
953 BlockUnder {
954 window_secs: f64,
955 samples: usize,
956 rate: u32,
957 },
958 UnderOrder(formants::UnderOrder),
959 ShortWindow(beating::ShortWindow),
960}
961
962pub fn off_buffer(buffer: &Buffer, representation: Representation) -> Result<Output, NoReading> {
964 let sr = f64::from(buffer.rate);
965 let start = buffer.origin_secs();
966 let planes = buffer.slices();
967 Ok(match representation {
968 Representation::Samples => Output::Samples(Box::new(buffer.clone())),
969 Representation::Spectrum {
970 max_peaks,
971 frame_secs,
972 } => Output::Spectrum(Box::new(spectrum::analyze(
973 &planes, sr, max_peaks, frame_secs,
974 ))),
975 Representation::Envelope { frame_secs } => Output::Envelope(envelope::trace(
976 &buffer.slices(),
977 sr,
978 start,
979 frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
980 )),
981 Representation::Derivative => Output::Samples(Box::new(difference(buffer))),
982 Representation::Pitch {
983 max_notes,
984 frame_secs,
985 } => Output::Pitch(pitch::track(&planes, sr, start, frame_secs, max_notes)),
986 Representation::Formants {
987 max_formants,
988 frame_secs,
989 } => Output::Formants(
990 formants::track(
991 &planes,
992 sr,
993 start,
994 frame_secs,
995 formants::default_order(sr),
996 max_formants,
997 )
998 .map_err(NoReading::UnderOrder)?,
999 ),
1000 Representation::Stereo { frame_secs } => {
1001 if buffer.width() != 2 {
1002 return Err(NoReading::WidthMismatch {
1003 need: 2,
1004 held: buffer.width(),
1005 });
1006 }
1007 Output::Stereo(Box::new(stereo::analyze(
1008 &planes,
1009 buffer.width(),
1010 sr,
1011 start,
1012 frame_secs,
1013 )))
1014 }
1015 Representation::Bands => Output::Bands(Box::new(bands::analyze(&planes, sr, start))),
1016 Representation::Crest => Output::Crest(Box::new(crest::analyze(&planes, sr))),
1017 Representation::Score => Output::Score(Box::new(score::measure(&planes, sr, start))),
1018 Representation::LoudnessPattern { frame_secs, spl_db } => {
1019 Output::LoudnessPattern(Box::new(loudness_pattern::measure(
1020 &planes, sr, start, frame_secs, spl_db,
1021 )))
1022 }
1023 Representation::Sharpness { frame_secs, spl_db } => Output::Sharpness(Box::new(
1024 sharpness::measure(&planes, sr, start, frame_secs, spl_db),
1025 )),
1026 Representation::Beating {
1027 frame_secs,
1028 window_secs,
1029 spl_db,
1030 } => Output::Beating(Box::new(
1031 beating::measure(&planes, sr, start, (frame_secs, window_secs), spl_db)
1032 .map_err(NoReading::ShortWindow)?,
1033 )),
1034 Representation::Loudness { window_secs } => Output::Loudness(Box::new(
1035 loudness::analyze(&planes, sr, start, window_secs).map_err(|why| match why {
1036 sva_samples::Unweighted::Rate(held) => NoReading::RateUnder(held),
1037 sva_samples::Unweighted::Block {
1038 window_secs,
1039 samples,
1040 } => NoReading::BlockUnder {
1041 window_secs,
1042 samples,
1043 rate: buffer.rate,
1044 },
1045 })?,
1046 )),
1047 Representation::Harmony {
1048 reading,
1049 frame_secs,
1050 floor_db,
1051 } => {
1052 let (moments, framing) = framed::moments(&planes, sr, start, frame_secs);
1053 let end_secs = buffer.end() as f64 / sr;
1054 harmonic(reading, &moments, (framing, end_secs), floor_db)
1055 .map_err(NoReading::Crowded)?
1056 }
1057 Representation::Periodicity {
1058 frame_secs,
1059 window_secs,
1060 curve,
1061 } => Output::Periodicity(Box::new(
1062 periodicity::measure(&planes, sr, start, frame_secs, window_secs, curve)
1063 .map_err(NoReading::Unread)?,
1064 )),
1065 Representation::Modulation {
1066 frame_secs,
1067 window_secs,
1068 bpm,
1069 } => Output::Modulation(Box::new(
1070 modulation::measure(&planes, sr, start, frame_secs, window_secs, bpm)
1071 .map_err(NoReading::Unread)?,
1072 )),
1073 Representation::SelfSimilarity {
1074 frame_secs,
1075 cell_secs,
1076 smooth_secs,
1077 window_secs,
1078 floor_db,
1079 listed,
1080 curve,
1081 } => {
1082 let asked = self_similarity::Asked {
1083 hop_secs: frame_secs,
1084 cell_secs,
1085 smooth_secs,
1086 window_secs,
1087 floor_db,
1088 listed,
1089 curve,
1090 };
1091 Output::SelfSimilarity(Box::new(
1092 self_similarity::measure(&planes, sr, start, asked).map_err(NoReading::Unfit)?,
1093 ))
1094 }
1095 Representation::Partials {
1096 frame_secs,
1097 window_secs,
1098 hop_secs,
1099 floor_db,
1100 tracks,
1101 } => {
1102 let asked = partials::Asked {
1103 frame_secs,
1104 window_secs,
1105 hop_secs,
1106 floor_db,
1107 tracks,
1108 };
1109 Output::Partials(Box::new(partials::measure(&planes, sr, start, asked)))
1110 }
1111 _ => return Err(NoReading::NeedsAGraph),
1112 })
1113}
1114
1115fn attributed(
1117 render: &Render,
1118 node: sva_formula::NodeId,
1119 depth: usize,
1120) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
1121 super::ledger::entries(render, node, depth).unwrap_or_else(|| {
1122 Err(EngineError::refused(Diagnostic {
1123 code: "engine.not_materialized".to_string(),
1124 message: format!(
1125 "a ledger is tallied block by block as the render pulls, and this render was \
1126 not asked for `ledger` of `{}` to depth {depth}",
1127 render.tys.name(node)
1128 ),
1129 location: Located::at(render.tys.name(node), None),
1130 help: "ask for the ledger with the render it reads, as `--representation ledger`"
1131 .to_string(),
1132 }))
1133 })
1134}
1135
1136fn arguments_under(render: &Render, node: sva_formula::NodeId) -> Vec<crate::Arguments> {
1139 let under = |id: sva_formula::NodeId| -> Vec<sva_formula::NodeId> {
1140 match render.tys.value(id) {
1141 Value::ClosedForm(form) => refs::nodes_in(&form.body),
1142 Value::Read { source, .. } | Value::Cast(_, source) => vec![*source],
1143 Value::Op { args, .. } => args.clone(),
1144 Value::Filter {
1145 x, cutoff, q, gain, ..
1146 } => vec![*x, *cutoff, *q, *gain],
1147 Value::Solver { varying, .. } => varying.iter().map(|(_, a)| *a).collect(),
1148 Value::SelfAt { .. } | Value::Noise(_) | Value::Recorded(_) => Vec::new(),
1149 }
1150 };
1151 let mut names: Vec<&str> = Vec::new();
1152 let mut seen = std::collections::BTreeSet::from([node]);
1153 let mut level = vec![node];
1154 while !level.is_empty() {
1155 let mut next = Vec::new();
1156 for at in level {
1157 let name = render.tys.name(at);
1158 if !names.contains(&name) {
1159 names.push(name);
1160 }
1161 next.extend(under(at).into_iter().filter(|c| seen.insert(*c)));
1162 }
1163 level = next;
1164 }
1165 names
1166 .into_iter()
1167 .filter_map(|name| render.tys.arguments(name).cloned())
1168 .collect()
1169}
1170
1171fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
1174 if render.tys.ty(node).is_closed_form() {
1175 return Ok(node);
1176 }
1177 match render.tys.value(node) {
1178 crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
1179 _ => Err(refused(
1180 render,
1181 node,
1182 "engine.alias_needs_a_closed_form",
1183 "an alias score is a render against the same closed form oversampled, and this node \
1184 is samples with no closed form behind it"
1185 .to_string(),
1186 )),
1187 }
1188}
1189
1190pub(super) fn alias_db(
1192 render: &Render,
1193 node: sva_formula::NodeId,
1194 buffer: &Buffer,
1195 oversample: u32,
1196) -> Result<f64, EngineError> {
1197 let reference = oversampled(render, behind(render, node)?, oversample)?;
1198 Ok(worst_alias(render, node, buffer, &reference, oversample)?.asr_db)
1199}
1200
1201fn worst_alias(
1203 render: &Render,
1204 node: sva_formula::NodeId,
1205 buffer: &Buffer,
1206 reference: &Buffer,
1207 oversample: u32,
1208) -> Result<sva_samples::Alias, EngineError> {
1209 debug_assert_eq!(
1210 buffer.width(),
1211 reference.width(),
1212 "the reference is the same form at another rate"
1213 );
1214 let sr = f64::from(buffer.rate);
1215 let width = buffer.width().min(reference.width());
1216 let scored = (0..width).map(|c| {
1217 measure_alias(
1218 buffer.plane(c),
1219 reference.plane(c),
1220 oversample as usize,
1221 sr,
1222 buffer.origin_secs(),
1223 )
1224 });
1225 let scored = scored.collect::<Result<Vec<_>, _>>().map_err(|short| {
1226 let secs = (short.frame as f64 / sr * 1000.0).ceil() / 1000.0;
1227 EngineError::refused(Diagnostic {
1228 code: "read.target_under_frame".to_string(),
1229 message: format!(
1230 "the target holds {} samples, fewer than the {}-sample frame an alias score reads",
1231 short.held, short.frame
1232 ),
1233 location: Located::at(render.tys.name(node), None),
1234 help: format!(
1235 "render an interval of at least {secs}s, as `@x([0, {secs}s])` writes one"
1236 ),
1237 })
1238 })?;
1239 Ok(sva_samples::worst_alias(scored).expect("a buffer holds at least one component"))
1240}
1241
1242fn oversampled(
1244 render: &Render,
1245 node: sva_formula::NodeId,
1246 oversample: u32,
1247) -> Result<Buffer, EngineError> {
1248 let range = render.range.expect("an alias score reads a decided range");
1249 let k = i64::from(oversample);
1250 let extent = Extent::new(range.start * k, range.end * k);
1251 super::finer(render, node, oversample, extent)
1252}
1253
1254fn difference(buffer: &Buffer) -> Buffer {
1257 let step = f64::from(buffer.rate);
1258 let planes = (0..buffer.width())
1259 .map(|c| {
1260 let plane = buffer.plane(c);
1261 plane.windows(2).map(|x| (x[1] - x[0]) * step).collect()
1262 })
1263 .collect();
1264 let mut out = Buffer::of_planes(buffer.rate, planes);
1265 out.start = buffer.start + buffer.plane(0).len().min(1) as i64;
1266 out
1267}
1268
1269fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
1270 EngineError::refused(Diagnostic {
1271 code: code.to_string(),
1272 message,
1273 location: Located::at(render.tys.name(node), None),
1274 help: "ask for a reading this representation answers".to_string(),
1275 })
1276}
1277
1278fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
1279 refused(
1280 render,
1281 node,
1282 "read.series_not_enumerable",
1283 "a series whose term is no line lists no lines, and an empty list would read as \
1284 a node with none"
1285 .to_string(),
1286 )
1287}
1288
1289fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1290 refused(
1291 render,
1292 node,
1293 "engine.observation_needs_samples",
1294 format!(
1295 "`{}` reads samples, and this node is a closed form",
1296 r.name()
1297 ),
1298 )
1299}
1300
1301fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1302 refused(
1303 render,
1304 node,
1305 "engine.observation_not_wired",
1306 format!("`{}` takes no reading off a buffer here", r.name()),
1307 )
1308}
1309
1310fn width_mismatch(
1311 render: &Render,
1312 node: sva_formula::NodeId,
1313 need: usize,
1314 held: usize,
1315) -> EngineError {
1316 EngineError::refused(Diagnostic {
1317 code: "type.width_mismatch".to_string(),
1318 message: width_message(need, held, "node"),
1319 location: Located::at(render.tys.name(node), None),
1320 help: format!("read a node of exactly {need} components, as `join(@a, @b)` writes one"),
1321 })
1322}
1323
1324fn width_message(need: usize, held: usize, holder: &str) -> String {
1326 format!("this reading needs exactly {need} components and the {holder} holds {held}")
1327}
1328
1329pub(super) fn unheld(render: &Render, node: sva_formula::NodeId) -> EngineError {
1330 refused(
1331 render,
1332 node,
1333 "engine.not_materialized",
1334 "this render held no samples of it".to_string(),
1335 )
1336}
1337
1338pub(super) fn unmaterialized(
1339 render: &Render,
1340 node: sva_formula::NodeId,
1341 r: Representation,
1342) -> EngineError {
1343 refused(
1344 render,
1345 node,
1346 "engine.not_materialized",
1347 format!("`{}` reads samples this render never held", r.name()),
1348 )
1349}
1350
1351pub fn answer_buffer(
1354 name: &str,
1355 buffer: &Buffer,
1356 representation: Representation,
1357 profile: &'static str,
1358) -> Result<Answer, EngineError> {
1359 let refused = |code: &str, message: String| {
1360 EngineError::refused(Diagnostic {
1361 code: code.to_string(),
1362 message,
1363 location: Located::at(name, None),
1364 help: "ask for a reading a buffer answers on its own".to_string(),
1365 })
1366 };
1367 let longer = |secs: f64| format!("analyze a file holding at least {secs}s");
1368 let shorter = |secs: f64| format!("analyze a file holding at most {secs}s");
1369 let faster = |hz: f64| format!("analyze a file sampled over {hz} Hz");
1370 let value = off_buffer(buffer, representation).map_err(|fault| match fault {
1371 NoReading::NeedsAGraph => refused(
1372 "engine.observation_needs_a_graph",
1373 format!(
1374 "`{}` reads the tree a node was built from, and a file has none",
1375 representation.name()
1376 ),
1377 ),
1378 NoReading::WidthMismatch { need, held } => EngineError::refused(Diagnostic {
1379 code: "type.width_mismatch".to_string(),
1380 message: width_message(need, held, "file"),
1381 location: Located::at(name, None),
1382 help: format!("analyze a file of exactly {need} channels"),
1383 }),
1384 NoReading::Crowded(held) => crowded(name, representation, held),
1385 NoReading::Unread(why) => unread(name, representation, why, longer, faster),
1386 NoReading::Unfit(why) => unfit(name, why, shorter, faster),
1387 NoReading::RateUnder(held) => rate_under(name, held, faster),
1388 NoReading::BlockUnder {
1389 window_secs,
1390 samples,
1391 rate,
1392 } => block_under(name, (window_secs, samples, rate)),
1393 NoReading::UnderOrder(why) => under_order(name, why, longer),
1394 NoReading::ShortWindow(why) => short_window(name, why),
1395 })?;
1396 Ok(Answer::whole(
1397 value,
1398 Source::Measured,
1399 profile,
1400 Some(buffer.rate),
1401 ))
1402}