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, end_secs) =
218 exact_moments(render, node, sum, frame_secs)?;
219 let value = harmonic(reading, &moments, (framing, end_secs), floor_db)
220 .map_err(|held| crowded(render.tys.name(node), representation, held))?;
221 (value, listed)
222 }
223 other => return Err(not_a_closed_form(render, node, other)),
224 };
225 Ok((value, listed, source))
226}
227
228fn exact_moments(
233 render: &Render,
234 node: sva_formula::NodeId,
235 sum: &SpectralSum,
236 frame_secs: f64,
237) -> Result<(Vec<Moment>, Framing, Listed, f64), EngineError> {
238 let range = match (render.range, &render.unranged) {
239 (Some(range), _) => range,
240 (None, Some(refused)) => return Err(refused.clone()),
241 (None, None) => unreachable!("a harmonic reading decides its range before it answers"),
242 };
243 let rate = f64::from(render.config.rate);
244 let hop = ((frame_secs * rate).round() as usize).max(1);
245 let each = per_channel(render, node, sum)?;
246 let instants: Vec<f64> = (range.start..range.end)
247 .step_by(hop)
248 .map(|n| n as f64 / rate)
249 .collect();
250 let moments = match sum.var {
251 Var::F => {
252 let partials = harmony::averaged(
253 each.iter()
254 .map(|one| {
255 let held = lines(render, node, Var::F, &one.atoms)?;
256 Ok(harmony::partials(held.iter().map(|l| (l.hz, l.amp))))
257 })
258 .collect::<Result<_, EngineError>>()?,
259 );
260 instants
261 .iter()
262 .map(|t| Moment {
263 t_secs: *t,
264 partials: partials.clone(),
265 })
266 .collect()
267 }
268 Var::T => instants
269 .iter()
270 .map(|t| {
271 let at = |one: &Listed| {
272 let carried = one.atoms.iter().map(|a| {
273 let value = (!a.is_delta()).then(|| a.smooth_at(*t)).flatten();
274 let hz = a.exp.map_or(0.0, |e| e.omega / std::f64::consts::TAU);
275 value
276 .map(|v| (hz, v))
277 .ok_or_else(|| no_partial(render, node, a, *t))
278 });
279 Ok(harmony::partials(carried.collect::<Result<Vec<_>, _>>()?))
280 };
281 Ok(Moment {
282 t_secs: *t,
283 partials: harmony::averaged(each.iter().map(at).collect::<Result<_, _>>()?),
284 })
285 })
286 .collect::<Result<_, EngineError>>()?,
287 };
288 let framing = Framing {
289 hop_secs: hop as f64 / rate,
290 window: None,
291 };
292 let end_secs = range.end as f64 / rate;
293 Ok((moments, framing, Listed::joined(each), end_secs))
294}
295
296fn harmonic(
299 reading: Harmonic,
300 moments: &[Moment],
301 (framing, end_secs): (Framing, f64),
302 floor_db: f64,
303) -> Result<Output, Crowded> {
304 let chords = || chords::segment(moments, framing.hop_secs, end_secs, floor_db);
305 Ok(match reading {
306 Harmonic::Dissonance { max_pairs } => Output::Dissonance(Box::new(dissonance::measure(
307 moments, framing, floor_db, max_pairs,
308 )?)),
309 Harmonic::VoiceLeading { norm } => Output::VoiceLeading(Box::new(voice_leading::analyze(
310 chords(),
311 framing,
312 floor_db,
313 norm,
314 ))),
315 Harmonic::TonalDistance => {
316 Output::TonalDistance(Box::new(tonnetz::analyze(chords(), framing, floor_db)))
317 }
318 Harmonic::Inharmonicity => {
319 Output::Inharmonicity(Box::new(inharmonicity::measure(moments, framing, floor_db)))
320 }
321 })
322}
323
324fn no_partial(
326 render: &Render,
327 node: sva_formula::NodeId,
328 atom: &SpectralAtom,
329 t: f64,
330) -> EngineError {
331 let mut at = render.tys.locate(atom.origin);
332 if at.node.is_empty() {
333 at = Located::at(render.tys.name(node), None);
334 }
335 let message = match atom.is_delta() {
336 true => "this term is an impulse in `t`, which stands at no one frequency, so it is \
337 no partial"
338 .to_string(),
339 false => {
340 format!("this term has a pole at {t}s, an instant of the curve, where it has no value")
341 }
342 };
343 EngineError::refused(Diagnostic {
344 code: "read.no_partial_at_instant".to_string(),
345 message,
346 location: at,
347 help: "a harmonic reading of `sample(...)` of it, as `sample(@x([0, 1s]))`, is \
348 measured off frames of its samples instead"
349 .to_string(),
350 })
351}
352
353fn crowded(name: &str, representation: Representation, held: Crowded) -> EngineError {
354 let reading = representation.name();
355 EngineError::refused(Diagnostic {
356 code: "read.too_many_partial_pairs".to_string(),
357 message: format!(
358 "at {}s, {} partials may stand over the floor, {} pairs, past the {} asked for",
359 held.t_secs, held.partials, held.pairs, held.cap
360 ),
361 location: Located::at(name, None),
362 help: format!(
363 "ask for more pairs, as `{reading}(pairs={})`, or raise the floor so fewer partials \
364 pair, as `{reading}(floor=-30db)`",
365 held.pairs
366 ),
367 })
368}
369
370fn unread(
373 name: &str,
374 representation: Representation,
375 why: sliding::Unread,
376 longer: impl Fn(f64) -> String,
377 faster: impl Fn(f64) -> String,
378) -> EngineError {
379 let reading = representation.name();
380 let (code, message, help) = match why {
381 sliding::Unread::RateUnderBands { rate_hz, least_hz } => {
382 under_bands(reading, rate_hz, least_hz, faster)
383 }
384 sliding::Unread::WindowUnderTwoPeriods {
385 window_secs,
386 least_secs,
387 } => (
388 "read.window_under_two_periods",
389 format!(
390 "a {window_secs}s window holds under two of the shortest period `{reading}` \
391 reads, {}s",
392 least_secs / 2.0
393 ),
394 format!(
395 "ask a window of {least_secs}s or longer, as `{reading}(window={least_secs}s)`"
396 ),
397 ),
398 sliding::Unread::TargetShorterThanWindow {
399 held_secs,
400 window_secs,
401 least_secs,
402 } => (
403 "read.target_under_window",
404 format!("the target holds {held_secs}s, under the {window_secs}s window"),
405 match least_secs <= held_secs {
406 true => format!(
407 "{}, or ask a window as long as the target, as \
408 `{reading}(window={held_secs}s)`",
409 longer(window_secs)
410 ),
411 false => format!(
412 "{}; no window under {least_secs}s holds two of the shortest period",
413 longer(window_secs)
414 ),
415 },
416 ),
417 };
418 EngineError::refused(Diagnostic {
419 code: code.to_string(),
420 message,
421 location: Located::at(name, None),
422 help,
423 })
424}
425
426fn under_bands(
428 reading: &str,
429 rate_hz: f64,
430 least_hz: f64,
431 faster: impl Fn(f64) -> String,
432) -> (&'static str, String, String) {
433 (
434 "read.rate_under_bands",
435 format!(
436 "`{reading}` reads ERB bands, and {rate_hz} Hz samples place none: the first band's \
437 centre stands at {:.2} Hz, over {} of the rate",
438 least_hz * bands::HIGHEST_CENTRE_OF_RATE,
439 bands::HIGHEST_CENTRE_OF_RATE
440 ),
441 faster((least_hz * 100.0).ceil() / 100.0),
442 )
443}
444
445fn rate_under(
447 name: &str,
448 held: sva_samples::RateUnderWeighting,
449 faster: impl Fn(f64) -> String,
450) -> EngineError {
451 EngineError::refused(Diagnostic {
452 code: "read.rate_under_weighting".to_string(),
453 message: format!(
454 "BS.1770's K-weighting shelf stands at {} Hz, at or past the Nyquist of {} Hz \
455 samples, where its bilinear design has no filter",
456 held.least_hz / 2.0,
457 held.rate_hz
458 ),
459 location: Located::at(name, None),
460 help: faster(held.least_hz),
461 })
462}
463
464fn block_under(name: &str, (window_secs, samples, rate): (f64, usize, u32)) -> EngineError {
465 let least = sva_samples::measure::loudness::LEAST_BLOCK_SAMPLES;
466 let secs = least as f64 / f64::from(rate);
467 EngineError::refused(Diagnostic {
468 code: "read.block_under_samples".to_string(),
469 message: format!(
470 "a {window_secs}s `loudness` window holds {samples} samples at {rate} Hz, under the \
471 {least} a block a quarter of itself apart needs"
472 ),
473 location: Located::at(name, None),
474 help: format!("ask a window of {secs}s or longer, as `loudness(window={secs}s)`"),
475 })
476}
477
478fn under_order(
481 name: &str,
482 why: formants::UnderOrder,
483 longer: impl Fn(f64) -> String,
484) -> EngineError {
485 let (code, message, help) = match why {
486 formants::UnderOrder::Frame {
487 frame_secs,
488 held,
489 order,
490 least_secs,
491 } => (
492 "read.frame_under_order",
493 format!(
494 "a {frame_secs}s frame holds {held} samples, and an order-{order} all-pole fit \
495 reads {} lags",
496 order + 1
497 ),
498 format!("ask a frame of {least_secs}s or longer, as `formants(frame={least_secs}s)`"),
499 ),
500 formants::UnderOrder::Target {
501 held,
502 order,
503 least_secs,
504 } => (
505 "read.target_under_order",
506 format!(
507 "the target holds {held} samples, and an order-{order} all-pole fit reads {} \
508 lags",
509 order + 1
510 ),
511 longer(least_secs),
512 ),
513 };
514 EngineError::refused(Diagnostic {
515 code: code.to_string(),
516 message,
517 location: Located::at(name, None),
518 help,
519 })
520}
521
522fn unfit(
525 name: &str,
526 why: self_similarity::Unfit,
527 shorter: impl Fn(f64) -> String,
528 faster: impl Fn(f64) -> String,
529) -> EngineError {
530 let (code, message, help) = match why {
531 self_similarity::Unfit::RateUnderBands { rate_hz, least_hz } => {
532 under_bands("self_similarity", rate_hz, least_hz, faster)
533 }
534 self_similarity::Unfit::TooManyCells(held) => (
535 "read.too_many_cells",
536 format!(
537 "the target's {}s at cell={}s is {} cells a side, past the {} a matrix holds",
538 held.held_secs, held.cell_secs, held.cells, held.cap
539 ),
540 format!(
541 "ask a coarser cell, as `self_similarity(cell={}s)`, or {}",
542 held.least_cell_secs,
543 shorter(held.most_secs)
544 ),
545 ),
546 self_similarity::Unfit::CellUnderBandStep {
547 cell_secs,
548 least_cell_secs,
549 band_hz,
550 } => (
551 "read.cell_under_band_step",
552 format!(
553 "a {cell_secs}s cell holds no sample of the {band_hz} Hz band envelopes its \
554 band levels are read off"
555 ),
556 format!(
557 "ask a cell of {least_cell_secs}s or coarser, as \
558 `self_similarity(cell={least_cell_secs}s)`"
559 ),
560 ),
561 self_similarity::Unfit::KernelPastCap {
562 window_secs,
563 kernel_cells,
564 cap,
565 cell_secs,
566 widest_secs,
567 } => (
568 "read.kernel_past_cap",
569 format!(
570 "a {window_secs}s novelty window at cell={cell_secs}s is {kernel_cells} cells \
571 a side, past the {cap} a matrix holds"
572 ),
573 format!(
574 "ask a window of at most {widest_secs}s, as \
575 `self_similarity(window={widest_secs}s)`, or a coarser cell"
576 ),
577 ),
578 };
579 EngineError::refused(Diagnostic {
580 code: code.to_string(),
581 message,
582 location: Located::at(name, None),
583 help,
584 })
585}
586
587fn not_exact(representation: Representation, left: EngineError) -> EngineError {
590 match (representation, left) {
591 (Representation::Envelope { frame_secs: None }, EngineError::Refused(cause)) => {
592 EngineError::refused(Diagnostic {
593 message: format!("this closed form has no exact envelope: {}", cause.message),
594 help: "a measured envelope is asked for explicitly: read `envelope` of \
595 `sample(...)` of it, as `sample(@x([0, 1s]))`"
596 .to_string(),
597 ..*cause
598 })
599 }
600 (_, left) => left,
601 }
602}
603
604fn unsummed(representation: Representation, left: EngineError) -> EngineError {
607 match (representation, left) {
608 (Representation::Harmony { .. }, EngineError::Refused(cause)) => {
609 EngineError::refused(Diagnostic {
610 message: format!(
611 "`{}` of a closed form reads its exact partials, and it has none: {}",
612 representation.name(),
613 cause.message
614 ),
615 help: format!(
616 "a measured `{}` is asked for explicitly: read it of `sample(...)` of it, as \
617 `sample(@x([0, 1s]))`",
618 representation.name()
619 ),
620 ..*cause
621 })
622 }
623 (_, left) => not_exact(representation, left),
624 }
625}
626
627fn unframed(render: &Render, node: sva_formula::NodeId, is: &str, bare: &str) -> EngineError {
629 EngineError::refused(Diagnostic {
630 code: "engine.observation_needs_samples".to_string(),
631 message: format!("a closed form's {is}, and it has no frames to take it across"),
632 location: Located::at(render.tys.name(node), None),
633 help: format!("drop `frame` for {bare}, or read `sample(...)` of it to measure frames"),
634 })
635}
636
637struct Listed {
640 atoms: Vec<SpectralAtom>,
641 dropped: Vec<Line>,
642}
643
644impl Listed {
645 const NONE: Listed = Listed {
647 atoms: Vec::new(),
648 dropped: Vec::new(),
649 };
650
651 fn joined(each: Vec<Listed>) -> Listed {
652 let mut held = Listed::NONE;
653 for one in each {
654 held.atoms.extend(one.atoms);
655 held.dropped.extend(one.dropped);
656 }
657 held
658 }
659
660 fn tail_db(&self) -> Option<f64> {
663 let loudest = |set: &mut dyn Iterator<Item = f64>| set.fold(0.0f64, f64::max);
664 let gone = loudest(&mut self.dropped.iter().map(|l| l.amp.abs()));
665 let kept = loudest(&mut self.atoms.iter().map(|a| a.c.abs()));
666 (gone > 0.0 && kept > 0.0).then(|| 20.0 * (gone / kept).log10())
667 }
668}
669
670fn listed(
673 render: &Render,
674 node: sva_formula::NodeId,
675 sum: &SpectralSum,
676 profile: &Profile,
677) -> Result<Listed, EngineError> {
678 let ceiling = profile.ceiling_hz;
679 let band = (ceiling, profile.floor(ceiling), profile.half_lsb());
680 let mut held = Listed::NONE;
681 for lane in &sum.lanes {
682 held.atoms.extend(lane.clone().expanded().atoms);
683 for series in &lane.series {
684 let found = refs::read_through(&render.tys, |t| line_atoms_read(series, band, t));
685 let Some(found) = found else {
686 return Err(unenumerable(render, node));
687 };
688 held.atoms.extend(found.atoms);
689 held.dropped.extend(found.dropped);
690 }
691 }
692 Ok(held)
693}
694
695fn per_channel(
697 render: &Render,
698 node: sva_formula::NodeId,
699 sum: &SpectralSum,
700) -> Result<Vec<Listed>, EngineError> {
701 sum.lanes
702 .iter()
703 .map(|lane| {
704 let one = SpectralSum::of(sum.var, vec![lane.clone()]);
705 listed(render, node, &one, &render.config.profile)
706 })
707 .collect()
708}
709
710fn in_power(per_channel: Vec<Vec<Line>>) -> Vec<Line> {
714 let count = per_channel.len() as f64;
715 let mut power: Vec<(Line, f64)> = Vec::new();
716 for lines in per_channel {
717 let mut channel: Vec<Line> = Vec::new();
718 for line in lines {
719 match channel
720 .iter_mut()
721 .find(|l| l.hz.to_bits() == line.hz.to_bits())
722 {
723 Some(held) => held.amp = held.amp + line.amp,
724 None => channel.push(line),
725 }
726 }
727 for line in channel {
728 let squared = line.amp.abs() * line.amp.abs();
729 match power
730 .iter_mut()
731 .find(|(l, _)| l.hz.to_bits() == line.hz.to_bits())
732 {
733 Some((_, sum)) => *sum += squared,
734 None => power.push((line, squared)),
735 }
736 }
737 }
738 power
739 .into_iter()
740 .filter(|(_, sum)| *sum > 0.0)
741 .map(|(line, sum)| Line {
742 amp: C64::real((sum / count).sqrt()),
743 ..line
744 })
745 .collect()
746}
747
748fn lines(
751 render: &Render,
752 node: sva_formula::NodeId,
753 var: Var,
754 atoms: &[SpectralAtom],
755) -> Result<Vec<Line>, EngineError> {
756 atoms
757 .iter()
758 .map(|a| line_of(var, a).ok_or_else(|| not_a_line(render, node, var, a)))
759 .collect()
760}
761
762fn line_of(var: Var, a: &SpectralAtom) -> Option<Line> {
763 match (var, a.sing) {
764 (Var::F, Singular::Delta { at, order: 0 }) => Some(Line::bare(at, a.c)),
765 (Var::T, Singular::Regular) => {
766 if !a.poly.is_one() || a.gauss.is_some() || a.ind.is_some() || a.pole.is_some() {
767 return None;
768 }
769 match a.exp {
770 None => Some(Line::bare(0.0, a.c)),
772 Some(e) if e.sigma == 0.0 => Some(Line::bare(e.omega / std::f64::consts::TAU, a.c)),
773 Some(_) => None,
774 }
775 }
776 _ => None,
777 }
778}
779
780fn widening(a: &SpectralAtom) -> Vec<&'static str> {
782 let mut held = Vec::new();
783 if !a.poly.is_one() {
784 held.push("a polynomial");
785 }
786 if a.exp.is_some_and(|e| e.sigma != 0.0) {
787 held.push("a decaying exponential");
788 }
789 if a.gauss.is_some() {
790 held.push("a Gaussian");
791 }
792 if a.pole.is_some() {
793 held.push("a pole");
794 }
795 if held.is_empty() {
796 held.push("a delta");
797 }
798 held
799}
800
801fn not_a_line(
803 render: &Render,
804 node: sva_formula::NodeId,
805 var: Var,
806 atom: &SpectralAtom,
807) -> EngineError {
808 let mut at = render.tys.locate(atom.origin);
809 if at.node.is_empty() {
810 at = Located::at(render.tys.name(node), None);
811 }
812 let message = match atom.ind {
813 _ if var == Var::F => "this term spreads over `f` rather than standing at one \
814 frequency, and a line in `f` is a delta"
815 .to_string(),
816 Some(window) => format!(
817 "this term is windowed to [{}s, {}s), and a windowed line is that line convolved \
818 with the window's transform, which has a width and is no line",
819 window.l.value(),
820 window.r.value()
821 ),
822 None => format!(
823 "this term carries {}, and a line is a bare turning exponential: each of those \
824 convolves the line with a shape of its own width",
825 widening(atom).join(", ")
826 ),
827 };
828 EngineError::refused(Diagnostic {
829 code: "read.lines_need_unwindowed_lines".to_string(),
830 message,
831 location: at,
832 help: "`atoms` states each term as it stands, `lines` of the node under the window \
833 or envelope lists the lines it multiplies, and `pitch` or `spectrum` of \
834 `sample(...)` of it measures it frame by frame"
835 .to_string(),
836 })
837}
838
839fn sketch_atom(a: &SpectralAtom) -> String {
841 let factors: Vec<&'static str> = a.present().iter().map(|f| f.as_str()).collect();
842 format!("{} x {}", a.c.abs(), factors.join(" times "))
843}
844
845pub fn atoms_of(sum: &SpectralSum) -> Vec<Atom> {
847 sum.lanes
848 .iter()
849 .enumerate()
850 .flat_map(|(c, lane)| lane.atoms.iter().map(move |a| atom_of(sum.var, a, c)))
851 .collect()
852}
853
854fn atom_of(var: Var, a: &SpectralAtom, channel: usize) -> Atom {
857 let (sigma, omega, mu) = a.exp.map_or((0.0, 0.0, 0.0), |e| (e.sigma, e.omega, e.mu));
858 let bounded = |edge: Edge| Some(edge.value() + 0.0).filter(|x| x.is_finite());
859 Atom {
860 var,
861 channel,
862 sketch: sketch_atom(a),
863 amp: a.c,
864 turns: omega / std::f64::consts::TAU + 0.0,
865 decay: 0.0 - sigma,
866 decay_from: mu + 0.0,
867 degree: a.poly.degree,
868 polynomial_at: a.poly.at + 0.0,
869 start: a.ind.and_then(|i| bounded(i.l)),
870 end: a.ind.and_then(|i| bounded(i.r)),
871 }
872}
873
874fn measured(
875 render: &Render,
876 node: sva_formula::NodeId,
877 representation: Representation,
878 buffer: &Buffer,
879) -> Result<Output, EngineError> {
880 Ok(match representation {
881 Representation::Alias { oversample } => {
882 let reference = oversampled(render, behind(render, node)?, oversample)?;
883 Output::Alias(Box::new(worst_alias(
884 render, node, buffer, &reference, oversample,
885 )?))
886 }
887 Representation::Ledger { depth } => Output::Ledger(attributed(render, node, depth)?),
888 other => {
889 let name = render.tys.name(node);
890 let longer = |secs: f64| {
891 format!("render an interval of at least {secs}s, as `@x([0, {secs}s])` writes one")
892 };
893 let shorter = |secs: f64| {
894 format!("render an interval of at most {secs}s, as `@x([0, {secs}s])` writes one")
895 };
896 let faster = |hz: f64| format!("render at a rate over {hz} Hz, as `--rate 48000` asks");
897 return off_buffer(buffer, other).map_err(|fault| match fault {
898 NoReading::NeedsAGraph => not_a_reading(render, node, other),
899 NoReading::WidthMismatch { need, held } => width_mismatch(render, node, need, held),
900 NoReading::Crowded(held) => crowded(name, other, held),
901 NoReading::Unread(why) => unread(name, other, why, longer, faster),
902 NoReading::RateUnder(held) => rate_under(name, held, faster),
903 NoReading::BlockUnder {
904 window_secs,
905 samples,
906 rate,
907 } => block_under(name, (window_secs, samples, rate)),
908 NoReading::UnderOrder(why) => under_order(name, why, longer),
909 NoReading::Unfit(why) => unfit(name, why, shorter, faster),
910 });
911 }
912 })
913}
914
915#[derive(Clone, Copy, Debug, PartialEq)]
918pub enum NoReading {
919 NeedsAGraph,
920 WidthMismatch {
921 need: usize,
922 held: usize,
923 },
924 Crowded(Crowded),
925 Unread(sliding::Unread),
926 Unfit(self_similarity::Unfit),
927 RateUnder(sva_samples::RateUnderWeighting),
928 BlockUnder {
929 window_secs: f64,
930 samples: usize,
931 rate: u32,
932 },
933 UnderOrder(formants::UnderOrder),
934}
935
936pub fn off_buffer(buffer: &Buffer, representation: Representation) -> Result<Output, NoReading> {
938 let sr = f64::from(buffer.rate);
939 let start = buffer.origin_secs();
940 let planes = buffer.slices();
941 Ok(match representation {
942 Representation::Samples => Output::Samples(Box::new(buffer.clone())),
943 Representation::Spectrum {
944 max_peaks,
945 frame_secs,
946 } => Output::Spectrum(Box::new(spectrum::analyze(
947 &planes, sr, max_peaks, frame_secs,
948 ))),
949 Representation::Envelope { frame_secs } => Output::Envelope(envelope::trace(
950 &buffer.slices(),
951 sr,
952 start,
953 frame_secs.unwrap_or(DEFAULT_FRAME_SECS),
954 )),
955 Representation::Derivative => Output::Samples(Box::new(difference(buffer))),
956 Representation::Pitch {
957 max_notes,
958 frame_secs,
959 } => Output::Pitch(pitch::track(&planes, sr, start, frame_secs, max_notes)),
960 Representation::Formants {
961 max_formants,
962 frame_secs,
963 } => Output::Formants(
964 formants::track(
965 &planes,
966 sr,
967 start,
968 frame_secs,
969 formants::default_order(sr),
970 max_formants,
971 )
972 .map_err(NoReading::UnderOrder)?,
973 ),
974 Representation::Stereo { frame_secs } => {
975 if buffer.width() != 2 {
976 return Err(NoReading::WidthMismatch {
977 need: 2,
978 held: buffer.width(),
979 });
980 }
981 Output::Stereo(Box::new(stereo::analyze(
982 &planes,
983 buffer.width(),
984 sr,
985 start,
986 frame_secs,
987 )))
988 }
989 Representation::Bands => Output::Bands(Box::new(bands::analyze(&planes, sr, start))),
990 Representation::Crest => Output::Crest(Box::new(crest::analyze(&planes, sr))),
991 Representation::Onsets => Output::Onsets(Box::new(onsets::detect(&planes, sr, start))),
992 Representation::Loudness { window_secs } => Output::Loudness(Box::new(
993 loudness::analyze(&planes, sr, start, window_secs).map_err(|why| match why {
994 sva_samples::Unweighted::Rate(held) => NoReading::RateUnder(held),
995 sva_samples::Unweighted::Block {
996 window_secs,
997 samples,
998 } => NoReading::BlockUnder {
999 window_secs,
1000 samples,
1001 rate: buffer.rate,
1002 },
1003 })?,
1004 )),
1005 Representation::Harmony {
1006 reading,
1007 frame_secs,
1008 floor_db,
1009 } => {
1010 let (moments, framing) = framed::moments(&planes, sr, start, frame_secs);
1011 let end_secs = buffer.end() as f64 / sr;
1012 harmonic(reading, &moments, (framing, end_secs), floor_db)
1013 .map_err(NoReading::Crowded)?
1014 }
1015 Representation::Periodicity {
1016 frame_secs,
1017 window_secs,
1018 curve,
1019 } => Output::Periodicity(Box::new(
1020 periodicity::measure(&planes, sr, start, frame_secs, window_secs, curve)
1021 .map_err(NoReading::Unread)?,
1022 )),
1023 Representation::Modulation {
1024 frame_secs,
1025 window_secs,
1026 bpm,
1027 } => Output::Modulation(Box::new(
1028 modulation::measure(&planes, sr, start, frame_secs, window_secs, bpm)
1029 .map_err(NoReading::Unread)?,
1030 )),
1031 Representation::SelfSimilarity {
1032 frame_secs,
1033 cell_secs,
1034 smooth_secs,
1035 window_secs,
1036 floor_db,
1037 listed,
1038 curve,
1039 } => {
1040 let asked = self_similarity::Asked {
1041 hop_secs: frame_secs,
1042 cell_secs,
1043 smooth_secs,
1044 window_secs,
1045 floor_db,
1046 listed,
1047 curve,
1048 };
1049 Output::SelfSimilarity(Box::new(
1050 self_similarity::measure(&planes, sr, start, asked).map_err(NoReading::Unfit)?,
1051 ))
1052 }
1053 _ => return Err(NoReading::NeedsAGraph),
1054 })
1055}
1056
1057fn attributed(
1059 render: &Render,
1060 node: sva_formula::NodeId,
1061 depth: usize,
1062) -> Result<Vec<sva_samples::LedgerEntry>, EngineError> {
1063 super::ledger::entries(render, node, depth).unwrap_or_else(|| {
1064 Err(EngineError::refused(Diagnostic {
1065 code: "engine.not_materialized".to_string(),
1066 message: format!(
1067 "a ledger is tallied block by block as the render pulls, and this render was \
1068 not asked for `ledger` of `{}` to depth {depth}",
1069 render.tys.name(node)
1070 ),
1071 location: Located::at(render.tys.name(node), None),
1072 help: "ask for the ledger with the render it reads, as `--representation ledger`"
1073 .to_string(),
1074 }))
1075 })
1076}
1077
1078fn arguments_under(render: &Render, node: sva_formula::NodeId) -> Vec<crate::Arguments> {
1081 let under = |id: sva_formula::NodeId| -> Vec<sva_formula::NodeId> {
1082 match render.tys.value(id) {
1083 Value::ClosedForm(form) => refs::nodes_in(&form.body),
1084 Value::Read { source, .. } | Value::Cast(_, source) => vec![*source],
1085 Value::Op { args, .. } => args.clone(),
1086 Value::Filter {
1087 x, cutoff, q, gain, ..
1088 } => vec![*x, *cutoff, *q, *gain],
1089 Value::Solver { varying, .. } => varying.iter().map(|(_, a)| *a).collect(),
1090 Value::SelfAt { .. } | Value::Noise(_) | Value::Recorded(_) => Vec::new(),
1091 }
1092 };
1093 let mut names: Vec<&str> = Vec::new();
1094 let mut seen = std::collections::BTreeSet::from([node]);
1095 let mut level = vec![node];
1096 while !level.is_empty() {
1097 let mut next = Vec::new();
1098 for at in level {
1099 let name = render.tys.name(at);
1100 if !names.contains(&name) {
1101 names.push(name);
1102 }
1103 next.extend(under(at).into_iter().filter(|c| seen.insert(*c)));
1104 }
1105 level = next;
1106 }
1107 names
1108 .into_iter()
1109 .filter_map(|name| render.tys.arguments(name).cloned())
1110 .collect()
1111}
1112
1113fn behind(render: &Render, node: sva_formula::NodeId) -> Result<sva_formula::NodeId, EngineError> {
1116 if render.tys.ty(node).is_closed_form() {
1117 return Ok(node);
1118 }
1119 match render.tys.value(node) {
1120 crate::typing::Value::Cast(crate::cast::Cast::Sample, source) => Ok(*source),
1121 _ => Err(refused(
1122 render,
1123 node,
1124 "engine.alias_needs_a_closed_form",
1125 "an alias score is a render against the same closed form oversampled, and this node \
1126 is samples with no closed form behind it"
1127 .to_string(),
1128 )),
1129 }
1130}
1131
1132pub(super) fn alias_db(
1134 render: &Render,
1135 node: sva_formula::NodeId,
1136 buffer: &Buffer,
1137 oversample: u32,
1138) -> Result<f64, EngineError> {
1139 let reference = oversampled(render, behind(render, node)?, oversample)?;
1140 Ok(worst_alias(render, node, buffer, &reference, oversample)?.asr_db)
1141}
1142
1143fn worst_alias(
1145 render: &Render,
1146 node: sva_formula::NodeId,
1147 buffer: &Buffer,
1148 reference: &Buffer,
1149 oversample: u32,
1150) -> Result<sva_samples::Alias, EngineError> {
1151 debug_assert_eq!(
1152 buffer.width(),
1153 reference.width(),
1154 "the reference is the same form at another rate"
1155 );
1156 let sr = f64::from(buffer.rate);
1157 let width = buffer.width().min(reference.width());
1158 let scored = (0..width).map(|c| {
1159 measure_alias(
1160 buffer.plane(c),
1161 reference.plane(c),
1162 oversample as usize,
1163 sr,
1164 buffer.origin_secs(),
1165 )
1166 });
1167 let scored = scored.collect::<Result<Vec<_>, _>>().map_err(|short| {
1168 let secs = (short.frame as f64 / sr * 1000.0).ceil() / 1000.0;
1169 EngineError::refused(Diagnostic {
1170 code: "read.target_under_frame".to_string(),
1171 message: format!(
1172 "the target holds {} samples, fewer than the {}-sample frame an alias score reads",
1173 short.held, short.frame
1174 ),
1175 location: Located::at(render.tys.name(node), None),
1176 help: format!(
1177 "render an interval of at least {secs}s, as `@x([0, {secs}s])` writes one"
1178 ),
1179 })
1180 })?;
1181 Ok(sva_samples::worst_alias(scored).expect("a buffer holds at least one component"))
1182}
1183
1184fn oversampled(
1186 render: &Render,
1187 node: sva_formula::NodeId,
1188 oversample: u32,
1189) -> Result<Buffer, EngineError> {
1190 let range = render.range.expect("an alias score reads a decided range");
1191 let k = i64::from(oversample);
1192 let extent = Extent::new(range.start * k, range.end * k);
1193 super::finer(render, node, oversample, extent)
1194}
1195
1196fn difference(buffer: &Buffer) -> Buffer {
1199 let step = f64::from(buffer.rate);
1200 let planes = (0..buffer.width())
1201 .map(|c| {
1202 let plane = buffer.plane(c);
1203 plane.windows(2).map(|x| (x[1] - x[0]) * step).collect()
1204 })
1205 .collect();
1206 let mut out = Buffer::of_planes(buffer.rate, planes);
1207 out.start = buffer.start + buffer.plane(0).len().min(1) as i64;
1208 out
1209}
1210
1211fn refused(render: &Render, node: sva_formula::NodeId, code: &str, message: String) -> EngineError {
1212 EngineError::refused(Diagnostic {
1213 code: code.to_string(),
1214 message,
1215 location: Located::at(render.tys.name(node), None),
1216 help: "ask for a reading this representation answers".to_string(),
1217 })
1218}
1219
1220fn unenumerable(render: &Render, node: sva_formula::NodeId) -> EngineError {
1221 refused(
1222 render,
1223 node,
1224 "read.series_not_enumerable",
1225 "a series whose term is no line lists no lines, and an empty list would read as \
1226 a node with none"
1227 .to_string(),
1228 )
1229}
1230
1231fn not_a_closed_form(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1232 refused(
1233 render,
1234 node,
1235 "engine.observation_needs_samples",
1236 format!(
1237 "`{}` reads samples, and this node is a closed form",
1238 r.name()
1239 ),
1240 )
1241}
1242
1243fn not_a_reading(render: &Render, node: sva_formula::NodeId, r: Representation) -> EngineError {
1244 refused(
1245 render,
1246 node,
1247 "engine.observation_not_wired",
1248 format!("`{}` takes no reading off a buffer here", r.name()),
1249 )
1250}
1251
1252fn width_mismatch(
1253 render: &Render,
1254 node: sva_formula::NodeId,
1255 need: usize,
1256 held: usize,
1257) -> EngineError {
1258 EngineError::refused(Diagnostic {
1259 code: "type.width_mismatch".to_string(),
1260 message: width_message(need, held, "node"),
1261 location: Located::at(render.tys.name(node), None),
1262 help: format!("read a node of exactly {need} components, as `join(@a, @b)` writes one"),
1263 })
1264}
1265
1266fn width_message(need: usize, held: usize, holder: &str) -> String {
1268 format!("this reading needs exactly {need} components and the {holder} holds {held}")
1269}
1270
1271pub(super) fn unheld(render: &Render, node: sva_formula::NodeId) -> EngineError {
1272 refused(
1273 render,
1274 node,
1275 "engine.not_materialized",
1276 "this render held no samples of it".to_string(),
1277 )
1278}
1279
1280pub(super) fn unmaterialized(
1281 render: &Render,
1282 node: sva_formula::NodeId,
1283 r: Representation,
1284) -> EngineError {
1285 refused(
1286 render,
1287 node,
1288 "engine.not_materialized",
1289 format!("`{}` reads samples this render never held", r.name()),
1290 )
1291}
1292
1293pub fn answer_buffer(
1296 name: &str,
1297 buffer: &Buffer,
1298 representation: Representation,
1299 profile: &'static str,
1300) -> Result<Answer, EngineError> {
1301 let refused = |code: &str, message: String| {
1302 EngineError::refused(Diagnostic {
1303 code: code.to_string(),
1304 message,
1305 location: Located::at(name, None),
1306 help: "ask for a reading a buffer answers on its own".to_string(),
1307 })
1308 };
1309 let longer = |secs: f64| format!("analyze a file holding at least {secs}s");
1310 let shorter = |secs: f64| format!("analyze a file holding at most {secs}s");
1311 let faster = |hz: f64| format!("analyze a file sampled over {hz} Hz");
1312 let value = off_buffer(buffer, representation).map_err(|fault| match fault {
1313 NoReading::NeedsAGraph => refused(
1314 "engine.observation_needs_a_graph",
1315 format!(
1316 "`{}` reads the tree a node was built from, and a file has none",
1317 representation.name()
1318 ),
1319 ),
1320 NoReading::WidthMismatch { need, held } => EngineError::refused(Diagnostic {
1321 code: "type.width_mismatch".to_string(),
1322 message: width_message(need, held, "file"),
1323 location: Located::at(name, None),
1324 help: format!("analyze a file of exactly {need} channels"),
1325 }),
1326 NoReading::Crowded(held) => crowded(name, representation, held),
1327 NoReading::Unread(why) => unread(name, representation, why, longer, faster),
1328 NoReading::Unfit(why) => unfit(name, why, shorter, faster),
1329 NoReading::RateUnder(held) => rate_under(name, held, faster),
1330 NoReading::BlockUnder {
1331 window_secs,
1332 samples,
1333 rate,
1334 } => block_under(name, (window_secs, samples, rate)),
1335 NoReading::UnderOrder(why) => under_order(name, why, longer),
1336 })?;
1337 Ok(Answer::whole(
1338 value,
1339 Source::Measured,
1340 profile,
1341 Some(buffer.rate),
1342 ))
1343}