Skip to main content

sva_engine/
query.rs

1// Concern: the one table of observations: names, arguments, defaults, what each reads, help; the answer envelope | Non-concern: taking one reading (answer.rs) | IO: (name, Given) -> Representation
2
3use sva_formula::{C64, Line, SpectralSum, Var};
4use sva_samples::measure::harmony::dissonance::Dissonance;
5use sva_samples::measure::harmony::inharmonicity::Inharmonicity;
6use sva_samples::measure::harmony::tonnetz::TonalDistance;
7use sva_samples::measure::harmony::voice_leading::{Norm, VoiceLeading};
8use sva_samples::measure::loudness_pattern::LoudnessPattern;
9use sva_samples::measure::modulation::Modulation;
10use sva_samples::measure::panel::Panel;
11use sva_samples::measure::partials::Partials;
12use sva_samples::measure::periodicity::Periodicity;
13use sva_samples::measure::self_similarity::SelfSimilarity;
14use sva_samples::{
15    Alias, Bands, Buffer, Consumes, Crest, EnvelopeFrame, FormantFrame, LedgerEntry, Loudness,
16    PitchFrame, Score, Source, Spectrum, StereoImage,
17};
18
19use crate::arguments::Arguments;
20use crate::bindings::Binding;
21
22/// The default window a framed reading uses when the observation names none.
23pub const DEFAULT_FRAME_SECS: f64 = 0.05;
24pub const DEFAULT_MAX_PEAKS: usize = 16;
25pub const DEFAULT_LEDGER_DEPTH: usize = 3;
26pub const DEFAULT_OVERSAMPLE: u32 = 4;
27/// How much audio a live device holds ahead of what it plays, where `samples` names one.
28pub const DEFAULT_BUFFER_SECS: f64 = 0.05;
29/// The amplitude, in dB against full scale, under which a harmonic reading drops a partial.
30pub const DEFAULT_FLOOR_DB: f64 = -40.0;
31/// The most partial pairs one `dissonance` frame sums before it refuses.
32pub const DEFAULT_MAX_PAIRS: usize = 1 << 17;
33/// BS.1770's momentary block, which `loudness` integrates over unless asked another.
34pub const DEFAULT_LOUDNESS_WINDOW_SECS: f64 = sva_samples::measure::loudness::MOMENTARY_SECS;
35/// The span of onset-strength envelope one `periodicity` window autocorrelates.
36pub const DEFAULT_PERIODICITY_WINDOW_SECS: f64 = 6.0;
37/// The span of band envelope one `modulation` window transforms.
38pub const DEFAULT_MODULATION_WINDOW_SECS: f64 = 4.0;
39/// The side of one `self_similarity` matrix cell.
40pub const DEFAULT_CELL_SECS: f64 = 0.5;
41/// The length `self_similarity` averages each diagonal over before it picks stripes.
42pub const DEFAULT_SMOOTH_SECS: f64 = 4.0;
43/// The whole width of `self_similarity`'s novelty kernel.
44pub const DEFAULT_NOVELTY_WINDOW_SECS: f64 = 8.0;
45/// A Hann this long parts partials some 20 Hz apart.
46pub const DEFAULT_PARTIALS_FRAME_SECS: f64 = 0.1;
47/// A short note's length, over which a noise's chance peaks average away.
48pub const DEFAULT_PARTIALS_WINDOW_SECS: f64 = 0.5;
49pub const DEFAULT_PARTIALS_HOP_SECS: f64 = 0.005;
50pub const DEFAULT_PARTIALS_FLOOR_DB: f64 = -70.0;
51pub const DEFAULT_PARTIALS_TRACKS: usize = 120;
52/// A loudness pattern's frames step as the panel's do.
53pub const DEFAULT_PATTERN_FRAME_SECS: f64 = sva_samples::measure::panel::FRAME_SECS;
54pub use sva_samples::measure::loudness_pattern::DEFAULT_SPL_DB;
55
56/// Every named argument a reading may take; the engine reads only those `Given` holds.
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum Arg {
59    Peaks,
60    Frame,
61    Depth,
62    Oversample,
63    Brief,
64    Skim,
65    Node,
66    Buffer,
67    Floor,
68    Norm,
69    Pairs,
70    Window,
71    Curve,
72    Bpm,
73    Cell,
74    Smooth,
75    Hop,
76    Spl,
77}
78
79impl Arg {
80    pub fn key(self) -> &'static str {
81        match self {
82            Arg::Peaks => "peaks",
83            Arg::Frame => "frame",
84            Arg::Depth => "depth",
85            Arg::Oversample => "oversample",
86            Arg::Brief => "brief",
87            Arg::Skim => "skim",
88            Arg::Node => "node",
89            Arg::Buffer => "buffer",
90            Arg::Floor => "floor",
91            Arg::Norm => "norm",
92            Arg::Pairs => "pairs",
93            Arg::Window => "window",
94            Arg::Curve => "curve",
95            Arg::Bpm => "bpm",
96            Arg::Cell => "cell",
97            Arg::Smooth => "smooth",
98            Arg::Hop => "hop",
99            Arg::Spl => "spl",
100        }
101    }
102}
103
104/// The arguments a `Representation` is built from, each at its default until written.
105#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct Given {
107    /// `None` until written: each reading then takes its own default.
108    pub peaks: Option<usize>,
109    /// `None` until written: `spectrum` then sizes its transform to the range, and every
110    /// other framed reading steps by `DEFAULT_FRAME_SECS`.
111    pub frame: Option<f64>,
112    pub depth: usize,
113    pub oversample: u32,
114    /// `None` until written: each reading then takes its own default.
115    pub floor_db: Option<f64>,
116    pub norm: Norm,
117    pub max_pairs: usize,
118    /// `None` until written: each windowed reading then takes its own default.
119    pub window: Option<f64>,
120    pub curve: bool,
121    pub bpm: Option<f64>,
122    pub cell_secs: f64,
123    pub smooth_secs: f64,
124    pub hop: Option<f64>,
125    /// `None` until written: the level a full-scale sine plays at, `DEFAULT_SPL_DB`.
126    pub spl_db: Option<f64>,
127}
128
129impl Default for Given {
130    fn default() -> Given {
131        Given {
132            peaks: None,
133            frame: None,
134            depth: DEFAULT_LEDGER_DEPTH,
135            oversample: DEFAULT_OVERSAMPLE,
136            floor_db: None,
137            norm: Norm::L1,
138            max_pairs: DEFAULT_MAX_PAIRS,
139            window: None,
140            curve: false,
141            bpm: None,
142            cell_secs: DEFAULT_CELL_SECS,
143            smooth_secs: DEFAULT_SMOOTH_SECS,
144            hop: None,
145            spl_db: None,
146        }
147    }
148}
149
150/// What a reading reads, which decides whether its answer is exact or measured.
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub enum Reads {
153    /// The written closed form alone: always exact.
154    ClosedForm,
155    /// Whichever the node holds: exact off a closed form, measured off samples.
156    Either,
157    /// Samples alone: always measured.
158    Samples,
159    /// Samples and the graph that rendered them, so no file answers it.
160    Graph,
161    /// The resolved instances, before anything renders; nothing composes for it.
162    Structure,
163}
164
165impl Reads {
166    pub fn consumes(self, closed: bool) -> Consumes {
167        match self {
168            Reads::ClosedForm | Reads::Structure => Consumes::ClosedForm,
169            Reads::Either if closed => Consumes::ClosedForm,
170            Reads::Either | Reads::Samples | Reads::Graph => Consumes::Buffer,
171        }
172    }
173
174    pub fn off_samples(self, sampled: bool) -> bool {
175        match self {
176            Reads::Samples => true,
177            Reads::Either => sampled,
178            Reads::ClosedForm | Reads::Graph | Reads::Structure => false,
179        }
180    }
181}
182
183pub struct Reading {
184    pub name: &'static str,
185    pub takes: &'static [Arg],
186    pub reads: Reads,
187    pub help: &'static str,
188    build: fn(&Given) -> Representation,
189}
190
191impl Reading {
192    pub fn representation(&self, given: &Given) -> Representation {
193        (self.build)(given)
194    }
195}
196
197static LINES: Reading = Reading {
198    name: "lines",
199    takes: &[],
200    reads: Reads::ClosedForm,
201    help: "the exact line list of a closed form, every channel's in channel order with no \
202           channel named; `atoms` names each term's",
203    build: |_| Representation::Lines,
204};
205
206static ATOMS: Reading = Reading {
207    name: "atoms",
208    takes: &[],
209    reads: Reads::ClosedForm,
210    help: "each term of a closed form: its channel, sketch, line, decay, polynomial and window",
211    build: |_| Representation::Atoms,
212};
213
214static SPECTRUM: Reading = Reading {
215    name: "spectrum",
216    takes: &[Arg::Peaks, Arg::Frame],
217    reads: Reads::Either,
218    help: "its lines, or its spectrum averaged over every frame and, power per bin, every \
219           channel; off samples a Hann spans the samples a frame holds, zero past them, each \
220           third-octave band reads as the amplitude of the one sine carrying its power, and, \
221           loudest first, a local maximum is no peak where its bin holds no more than the \
222           peaks kept, and a DC bin 0 holds as its maximum, can leak there, each bounded by \
223           Harris's (1978) Hann sidelobe bound 1/(pi m (m^2 - 1)) at m bins, its sinusoid taken \
224           anywhere within half a bin of its own: a bound where the frame is stationary \
225           sinusoids at the kept peaks",
226    build: |g| Representation::Spectrum {
227        max_peaks: g.peaks.unwrap_or(DEFAULT_MAX_PEAKS),
228        frame_secs: g.frame,
229    },
230};
231
232static ENVELOPE: Reading = Reading {
233    name: "envelope",
234    takes: &[Arg::Frame],
235    reads: Reads::Either,
236    help: "off a closed form the squared modulus of its analytic signal, x^2 + H{x}^2, as \
237           atoms per channel; off samples rms and peak per frame: the rms of every channel's \
238           samples together, the peak of any channel",
239    build: |g| Representation::Envelope {
240        frame_secs: g.frame,
241    },
242};
243
244static DERIVATIVE: Reading = Reading {
245    name: "derivative",
246    takes: &[],
247    reads: Reads::Either,
248    help: "the derivative's atoms per channel, each series listed as `atoms` lists it; off \
249           samples the first difference, (x[n] - x[n-1]) * rate at every sample but the first",
250    build: |_| Representation::Derivative,
251};
252
253static SAMPLES: Reading = Reading {
254    name: "samples",
255    takes: &[Arg::Buffer],
256    reads: Reads::Samples,
257    help: "the samples themselves; a `.wav` destination writes them as audio, an \
258           `alsa:<device>` one plays them as they render",
259    build: |_| Representation::Samples,
260};
261
262static LEDGER: Reading = Reading {
263    name: "ledger",
264    takes: &[Arg::Depth, Arg::Brief, Arg::Skim],
265    reads: Reads::Graph,
266    help: "rms, peak, share and clipping per node under the target",
267    build: |g| Representation::Ledger { depth: g.depth },
268};
269
270static PITCH: Reading = Reading {
271    name: "pitch",
272    takes: &[Arg::Peaks, Arg::Frame],
273    reads: Reads::Either,
274    help: "the `peaks` loudest notes it holds, per frame off samples, every channel's power \
275           per bin averaged; off samples its peaks are picked as `spectrum` picks them, and a series' \
276           fundamental is read within half a bin of its root's peak, the rate over the samples \
277           its Hann spans",
278    build: |g| Representation::Pitch {
279        max_notes: g.peaks.unwrap_or(DEFAULT_MAX_PEAKS),
280        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
281    },
282};
283
284static FORMANTS: Reading = Reading {
285    name: "formants",
286    takes: &[Arg::Peaks, Arg::Frame],
287    reads: Reads::Samples,
288    help: "formant frequencies, bandwidths and levels per frame, the `peaks` lowest, off \
289           every channel's autocorrelation averaged, an all-pole fit of 2 poles and 1 per kHz \
290           of rate, at most 64: a frame or target holding no more samples than its order \
291           refuses, and a tail frame reads zero past its end",
292    build: |g| Representation::Formants {
293        max_formants: g.peaks.unwrap_or(DEFAULT_MAX_PEAKS),
294        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
295    },
296};
297
298static STEREO: Reading = Reading {
299    name: "stereo",
300    takes: &[Arg::Frame],
301    reads: Reads::Samples,
302    help: "correlation, width and balance between exactly two channels, per frame; any other \
303           count refuses",
304    build: |g| Representation::Stereo {
305        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
306    },
307};
308
309static BANDS: Reading = Reading {
310    name: "bands",
311    takes: &[],
312    reads: Reads::Samples,
313    help: "each auditory band's level over time, every channel's band energy averaged, at 2 kHz",
314    build: |_| Representation::Bands,
315};
316
317static CREST: Reading = Reading {
318    name: "crest",
319    takes: &[],
320    reads: Reads::Samples,
321    help: "peak against rms, broadband and per band: any channel's peak against the rms of \
322           every channel's samples together",
323    build: |_| Representation::Crest,
324};
325
326static LOUDNESS: Reading = Reading {
327    name: "loudness",
328    takes: &[Arg::Window],
329    reads: Reads::Samples,
330    help: "integrated, momentary and short-term loudness in LUFS, the momentary blocks `window` \
331           long and the integrated gated over them",
332    build: |g| Representation::Loudness {
333        window_secs: g.window.unwrap_or(DEFAULT_LOUDNESS_WINDOW_SECS),
334    },
335};
336
337static SCORE: Reading = Reading {
338    name: "score",
339    takes: &[],
340    reads: Reads::Samples,
341    help: "its events as a score: each one's onset, placed at the sample its strike rises, its \
342           end and the next, new, a re-strike or held from the start, tonal, noisy-pitched or \
343           noisy, an unpitched hit's kind, its attack, level and periodicity, and its voices, \
344           several where a chord sounds, each with its f0, tonal or noisy, whether it sounds \
345           newly there, its bend and its partials, the loudest new one its main; every \
346           channel's samples averaged",
347    build: |_| Representation::Score,
348};
349
350static LOUDNESS_PATTERN: Reading = Reading {
351    name: "loudness_pattern",
352    takes: &[Arg::Frame, Arg::Spl],
353    reads: Reads::Samples,
354    help: "specific loudness in sone per Cam, band by band every `frame`, and each frame's \
355           total, after the ear's transfer and the excitation's spread, so a sound another \
356           masks adds little, a full-scale sine at `spl` dB SPL; every channel's power per \
357           bin averaged",
358    build: |g| Representation::LoudnessPattern {
359        frame_secs: g.frame.unwrap_or(DEFAULT_PATTERN_FRAME_SECS),
360        spl_db: g.spl_db.unwrap_or(DEFAULT_SPL_DB),
361    },
362};
363
364static PANEL: Reading = Reading {
365    name: "panel",
366    takes: &[Arg::Spl],
367    reads: Reads::Samples,
368    help: "the perceptual axes per note of `score` and over the whole: sharpness in acum, \
369           decay in phon/s, beating in phon, the 10-90 % attack, and each note's pitch to \
370           under a cent; its notes and their pitch off every channel's samples averaged, \
371           every other axis off `loudness_pattern` at its default frame and every channel's \
372           power averaged; a cost subtracting two reads each axis in its just-noticeable \
373           differences",
374    build: |g| Representation::Panel {
375        spl_db: g.spl_db.unwrap_or(DEFAULT_SPL_DB),
376    },
377};
378
379static ALIAS: Reading = Reading {
380    name: "alias",
381    takes: &[Arg::Oversample],
382    reads: Reads::Graph,
383    help: "what folded, against a re-render at a multiple of the rate",
384    build: |g| Representation::Alias {
385        oversample: g.oversample,
386    },
387};
388
389static DISSONANCE: Reading = Reading {
390    name: "dissonance",
391    takes: &[Arg::Frame, Arg::Floor, Arg::Pairs],
392    reads: Reads::Either,
393    help: "Sethares' sensory roughness per frame of its partials over a floor, with its mean, \
394           max and min; measured partials off every channel's power per bin averaged",
395    build: |g| Representation::Harmony {
396        reading: Harmonic::Dissonance {
397            max_pairs: g.max_pairs,
398        },
399        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
400        floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
401    },
402};
403
404static VOICE_LEADING: Reading = Reading {
405    name: "voice_leading",
406    takes: &[Arg::Frame, Arg::Floor, Arg::Norm],
407    reads: Reads::Either,
408    help: "its chords over time, and the smallest voice leading between each two, l1 or l2 \
409           in semitones; measured partials off every channel's power per bin averaged",
410    build: |g| Representation::Harmony {
411        reading: Harmonic::VoiceLeading { norm: g.norm },
412        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
413        floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
414    },
415};
416
417static TONAL_DISTANCE: Reading = Reading {
418    name: "tonal_distance",
419    takes: &[Arg::Frame, Arg::Floor],
420    reads: Reads::Either,
421    help: "its chords over time, the Tonnetz steps between each two triads and each one's \
422           from the first triad; measured partials off every channel's power per bin averaged",
423    build: |g| Representation::Harmony {
424        reading: Harmonic::TonalDistance,
425        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
426        floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
427    },
428};
429
430static INHARMONICITY: Reading = Reading {
431    name: "inharmonicity",
432    takes: &[Arg::Frame, Arg::Floor],
433    reads: Reads::Either,
434    help: "its harmonic series per frame: each member's cents off n f1, the stiff-string B each \
435           fits, and the energy in no series; measured partials off every channel's power per \
436           bin averaged",
437    build: |g| Representation::Harmony {
438        reading: Harmonic::Inharmonicity,
439        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
440        floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
441    },
442};
443
444static PERIODICITY: Reading = Reading {
445    name: "periodicity",
446    takes: &[Arg::Frame, Arg::Window, Arg::Curve],
447    reads: Reads::Samples,
448    help: "how strongly its onset strength repeats per sliding window, broadband, under 150 Hz \
449           and over: the leading periods up to 2 s, or half the window, in seconds and bpm \
450           with their bounds, their \
451           autocorrelation heights, the drift between windows; every channel's band energy \
452           averaged",
453    build: |g| Representation::Periodicity {
454        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
455        window_secs: g.window.unwrap_or(DEFAULT_PERIODICITY_WINDOW_SECS),
456        curve: g.curve,
457    },
458};
459
460static MODULATION: Reading = Reading {
461    name: "modulation",
462    takes: &[Arg::Frame, Arg::Window, Arg::Bpm],
463    reads: Reads::Samples,
464    help: "how its band envelopes move per sliding window, under 150 Hz, to 2 kHz and over: \
465           the leading modulation rates from 0.5 Hz, or the slowest two of whose periods the \
466           window holds, to 20 Hz, in beats where `bpm` is given, \
467           their depth as a modulation index, each with its bounds, the level, and the \
468           spectral centroid's mean and its own swing in Hz; every channel's band energy \
469           averaged",
470    build: |g| Representation::Modulation {
471        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
472        window_secs: g.window.unwrap_or(DEFAULT_MODULATION_WINDOW_SECS),
473        bpm: g.bpm,
474    },
475};
476
477static SELF_SIMILARITY: Reading = Reading {
478    name: "self_similarity",
479    takes: &[
480        Arg::Frame,
481        Arg::Cell,
482        Arg::Smooth,
483        Arg::Window,
484        Arg::Floor,
485        Arg::Peaks,
486        Arg::Curve,
487    ],
488    reads: Reads::Samples,
489    help: "how its stretches resemble one another, by chroma and by band levels, over cells \
490           of `cell`: the repeated stretches as stripes, each pair of spans with its \
491           similarity and, by chroma, its transposition in semitones; Foote's novelty at every \
492           cell boundary and its peaks; `curve=1` adds the cells and the matrix; every \
493           channel's power per bin and band energy averaged",
494    build: |g| Representation::SelfSimilarity {
495        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
496        cell_secs: g.cell_secs,
497        smooth_secs: g.smooth_secs,
498        window_secs: g.window.unwrap_or(DEFAULT_NOVELTY_WINDOW_SECS),
499        floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
500        listed: g.peaks.unwrap_or(DEFAULT_MAX_PEAKS),
501        curve: g.curve,
502    },
503};
504
505static PARTIALS: Reading = Reading {
506    name: "partials",
507    takes: &[Arg::Frame, Arg::Window, Arg::Hop, Arg::Floor, Arg::Peaks],
508    reads: Reads::Samples,
509    help: "each partial as a track: its frequency and amplitude as knot tables for `pwl` and \
510           `pwl_integral`, tonal or breathy by its amplitude's flutter and its band's \
511           coherence, each with its resolution and the residual its tables leave; peaks \
512           of `frame` Hann frames' power averaged over `window`, found every `hop` over the \
513           floor, the `peaks` tracks of most energy kept; every channel's samples averaged",
514    build: |g| Representation::Partials {
515        frame_secs: g.frame.unwrap_or(DEFAULT_PARTIALS_FRAME_SECS),
516        window_secs: g.window.unwrap_or(DEFAULT_PARTIALS_WINDOW_SECS),
517        hop_secs: g.hop.unwrap_or(DEFAULT_PARTIALS_HOP_SECS),
518        floor_db: g.floor_db.unwrap_or(DEFAULT_PARTIALS_FLOOR_DB),
519        tracks: g.peaks.unwrap_or(DEFAULT_PARTIALS_TRACKS),
520    },
521};
522
523static BINDINGS: Reading = Reading {
524    name: "bindings",
525    takes: &[Arg::Node],
526    reads: Reads::Structure,
527    help: "the bindings one instance resolved, each with its source",
528    build: |_| Representation::Bindings,
529};
530
531static ARGUMENTS: Reading = Reading {
532    name: "arguments",
533    takes: &[],
534    reads: Reads::Structure,
535    help: "each builtin call's named arguments as the numbers it was lowered with",
536    build: |_| Representation::Arguments,
537};
538
539/// Every reading a caller may ask for, in the order a listing prints them.
540pub static READINGS: [&Reading; 27] = [
541    &LINES,
542    &ATOMS,
543    &SPECTRUM,
544    &ENVELOPE,
545    &DERIVATIVE,
546    &SAMPLES,
547    &LEDGER,
548    &PITCH,
549    &FORMANTS,
550    &STEREO,
551    &BANDS,
552    &CREST,
553    &LOUDNESS,
554    &SCORE,
555    &ALIAS,
556    &DISSONANCE,
557    &VOICE_LEADING,
558    &TONAL_DISTANCE,
559    &INHARMONICITY,
560    &PERIODICITY,
561    &MODULATION,
562    &SELF_SIMILARITY,
563    &PARTIALS,
564    &LOUDNESS_PATTERN,
565    &PANEL,
566    &BINDINGS,
567    &ARGUMENTS,
568];
569
570#[derive(Clone, Copy, Debug, PartialEq)]
571pub enum Representation {
572    Lines,
573    Atoms,
574    Spectrum {
575        max_peaks: usize,
576        frame_secs: Option<f64>,
577    },
578    Envelope {
579        frame_secs: Option<f64>,
580    },
581    Derivative,
582    Samples,
583    Ledger {
584        depth: usize,
585    },
586    Pitch {
587        max_notes: usize,
588        frame_secs: f64,
589    },
590    Formants {
591        max_formants: usize,
592        frame_secs: f64,
593    },
594    Stereo {
595        frame_secs: f64,
596    },
597    Bands,
598    Crest,
599    /// Momentary blocks of `window_secs`, the integrated loudness gated over them.
600    Loudness {
601        window_secs: f64,
602    },
603    Score,
604    Alias {
605        oversample: u32,
606    },
607    /// The partials it is read from step by `frame_secs` and stop at `floor_db`.
608    Harmony {
609        reading: Harmonic,
610        frame_secs: f64,
611        floor_db: f64,
612    },
613    /// Windows of `window_secs` centred every `frame_secs`; `curve` adds each one's heights.
614    Periodicity {
615        frame_secs: f64,
616        window_secs: f64,
617        curve: bool,
618    },
619    /// Windows of `window_secs` centred every `frame_secs`; `bpm` adds each rate in beats.
620    Modulation {
621        frame_secs: f64,
622        window_secs: f64,
623        bpm: Option<f64>,
624    },
625    /// Cells of `cell_secs` over frames every `frame_secs`; `listed` stripes and peaks are
626    /// listed, and `curve` adds the cells and matrices.
627    SelfSimilarity {
628        frame_secs: f64,
629        cell_secs: f64,
630        smooth_secs: f64,
631        window_secs: f64,
632        floor_db: f64,
633        listed: usize,
634        curve: bool,
635    },
636    Partials {
637        frame_secs: f64,
638        window_secs: f64,
639        hop_secs: f64,
640        floor_db: f64,
641        tracks: usize,
642    },
643    LoudnessPattern {
644        frame_secs: f64,
645        spl_db: f64,
646    },
647    Panel {
648        spl_db: f64,
649    },
650    Bindings,
651    Arguments,
652}
653
654/// The readings taken off one curve of partials.
655#[derive(Clone, Copy, Debug, PartialEq, Eq)]
656pub enum Harmonic {
657    Dissonance { max_pairs: usize },
658    VoiceLeading { norm: Norm },
659    TonalDistance,
660    Inharmonicity,
661}
662
663/// What a caller asked of one node.
664#[derive(Clone, Debug, PartialEq)]
665pub struct Ask {
666    pub node: String,
667    pub representation: Representation,
668}
669
670#[derive(Clone, Debug, PartialEq)]
671pub enum Output {
672    Lines(Vec<Line>),
673    Atoms(Vec<Atom>),
674    Symbolic(Box<SpectralSum>),
675    Samples(Box<Buffer>),
676    Spectrum(Box<Spectrum>),
677    Envelope(Vec<EnvelopeFrame>),
678    Pitch(Vec<PitchFrame>),
679    Formants(Vec<FormantFrame>),
680    Stereo(Box<StereoImage>),
681    Bands(Box<Bands>),
682    Crest(Box<Crest>),
683    Loudness(Box<Loudness>),
684    Score(Box<Score>),
685    Alias(Box<Alias>),
686    Ledger(Vec<LedgerEntry>),
687    Dissonance(Box<Dissonance>),
688    VoiceLeading(Box<VoiceLeading>),
689    TonalDistance(Box<TonalDistance>),
690    Inharmonicity(Box<Inharmonicity>),
691    Periodicity(Box<Periodicity>),
692    Modulation(Box<Modulation>),
693    SelfSimilarity(Box<SelfSimilarity>),
694    Partials(Box<Partials>),
695    LoudnessPattern(Box<LoudnessPattern>),
696    Panel(Box<Panel>),
697    Bindings(Vec<Binding>),
698    Arguments(Vec<Arguments>),
699}
700
701/// `amp (x-polynomial_at)^degree e^(-decay (x-decay_from)) e^(2pi i turns x)` on `[start, end)`
702#[derive(Clone, Debug, PartialEq)]
703pub struct Atom {
704    pub var: Var,
705    /// The lane, or channel, it stands in.
706    pub channel: usize,
707    pub sketch: String,
708    pub amp: C64,
709    pub turns: f64,
710    pub decay: f64,
711    pub decay_from: f64,
712    pub degree: u16,
713    pub polynomial_at: f64,
714    pub start: Option<f64>,
715    pub end: Option<f64>,
716}
717
718/// Every answer says which reading ran and under which profile, so a caller never has to
719/// infer either from the shape of the value.
720#[derive(Clone, Debug, PartialEq)]
721pub struct Answer {
722    pub value: Output,
723    pub source: Source,
724    pub profile: &'static str,
725    pub rate: Option<u32>,
726    /// A series answers the terms it keeps, the tail beside them.
727    pub dropped: Vec<sva_formula::Line>,
728    pub tail_db: Option<f64>,
729}
730
731impl Answer {
732    pub fn whole(
733        value: Output,
734        source: Source,
735        profile: &'static str,
736        rate: Option<u32>,
737    ) -> Answer {
738        Answer {
739            value,
740            source,
741            profile,
742            rate,
743            dropped: Vec::new(),
744            tail_db: None,
745        }
746    }
747}
748
749impl Representation {
750    /// Exhaustive, so a variant with no row is a compile error.
751    pub fn reading(self) -> &'static Reading {
752        match self {
753            Representation::Lines => &LINES,
754            Representation::Atoms => &ATOMS,
755            Representation::Spectrum { .. } => &SPECTRUM,
756            Representation::Envelope { .. } => &ENVELOPE,
757            Representation::Derivative => &DERIVATIVE,
758            Representation::Samples => &SAMPLES,
759            Representation::Ledger { .. } => &LEDGER,
760            Representation::Pitch { .. } => &PITCH,
761            Representation::Formants { .. } => &FORMANTS,
762            Representation::Stereo { .. } => &STEREO,
763            Representation::Bands => &BANDS,
764            Representation::Crest => &CREST,
765            Representation::Loudness { .. } => &LOUDNESS,
766            Representation::Score => &SCORE,
767            Representation::Alias { .. } => &ALIAS,
768            Representation::Harmony { reading, .. } => match reading {
769                Harmonic::Dissonance { .. } => &DISSONANCE,
770                Harmonic::VoiceLeading { .. } => &VOICE_LEADING,
771                Harmonic::TonalDistance => &TONAL_DISTANCE,
772                Harmonic::Inharmonicity => &INHARMONICITY,
773            },
774            Representation::Periodicity { .. } => &PERIODICITY,
775            Representation::Modulation { .. } => &MODULATION,
776            Representation::SelfSimilarity { .. } => &SELF_SIMILARITY,
777            Representation::Partials { .. } => &PARTIALS,
778            Representation::LoudnessPattern { .. } => &LOUDNESS_PATTERN,
779            Representation::Panel { .. } => &PANEL,
780            Representation::Bindings => &BINDINGS,
781            Representation::Arguments => &ARGUMENTS,
782        }
783    }
784
785    pub fn name(self) -> &'static str {
786        self.reading().name
787    }
788
789    pub fn consumes(self, closed: bool) -> Consumes {
790        self.reading().reads.consumes(closed)
791    }
792
793    /// Whether a node's samples alone answer this reading, `sampled` where it holds samples.
794    pub fn off_samples(self, sampled: bool) -> bool {
795        self.reading().reads.off_samples(sampled)
796    }
797
798    pub fn from_name(name: &str) -> Option<Representation> {
799        READINGS
800            .iter()
801            .find(|row| row.name == name)
802            .map(|row| row.representation(&Given::default()))
803    }
804}