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