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::modulation::Modulation;
9use sva_samples::measure::periodicity::Periodicity;
10use sva_samples::measure::self_similarity::SelfSimilarity;
11use sva_samples::{
12    Alias, Bands, Buffer, Consumes, Crest, EnvelopeFrame, FormantFrame, LedgerEntry, Loudness,
13    Onsets, PitchFrame, Source, Spectrum, StereoImage,
14};
15
16use crate::arguments::Arguments;
17use crate::bindings::Binding;
18
19/// The default window a framed reading uses when the observation names none.
20pub const DEFAULT_FRAME_SECS: f64 = 0.05;
21pub const DEFAULT_MAX_PEAKS: usize = 16;
22pub const DEFAULT_LEDGER_DEPTH: usize = 3;
23pub const DEFAULT_OVERSAMPLE: u32 = 4;
24/// How much audio a live device holds ahead of what it plays, where `samples` names one.
25pub const DEFAULT_BUFFER_SECS: f64 = 0.05;
26/// The amplitude, in dB against full scale, under which a harmonic reading drops a partial.
27pub const DEFAULT_FLOOR_DB: f64 = -40.0;
28/// The most partial pairs one `dissonance` frame sums before it refuses.
29pub const DEFAULT_MAX_PAIRS: usize = 1 << 17;
30/// The span of onset-strength envelope one `periodicity` window autocorrelates.
31pub const DEFAULT_PERIODICITY_WINDOW_SECS: f64 = 6.0;
32/// The span of band envelope one `modulation` window transforms.
33pub const DEFAULT_MODULATION_WINDOW_SECS: f64 = 4.0;
34/// The side of one `self_similarity` matrix cell.
35pub const DEFAULT_CELL_SECS: f64 = 0.5;
36/// The length `self_similarity` averages each diagonal over before it picks stripes.
37pub const DEFAULT_SMOOTH_SECS: f64 = 4.0;
38/// The whole width of `self_similarity`'s novelty kernel.
39pub const DEFAULT_NOVELTY_WINDOW_SECS: f64 = 8.0;
40
41/// Every named argument a reading may take; the engine reads only those `Given` holds.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum Arg {
44    Peaks,
45    Frame,
46    Depth,
47    Oversample,
48    Brief,
49    Skim,
50    Node,
51    Buffer,
52    Floor,
53    Norm,
54    Pairs,
55    Window,
56    Curve,
57    Bpm,
58    Cell,
59    Smooth,
60}
61
62impl Arg {
63    pub fn key(self) -> &'static str {
64        match self {
65            Arg::Peaks => "peaks",
66            Arg::Frame => "frame",
67            Arg::Depth => "depth",
68            Arg::Oversample => "oversample",
69            Arg::Brief => "brief",
70            Arg::Skim => "skim",
71            Arg::Node => "node",
72            Arg::Buffer => "buffer",
73            Arg::Floor => "floor",
74            Arg::Norm => "norm",
75            Arg::Pairs => "pairs",
76            Arg::Window => "window",
77            Arg::Curve => "curve",
78            Arg::Bpm => "bpm",
79            Arg::Cell => "cell",
80            Arg::Smooth => "smooth",
81        }
82    }
83}
84
85/// The arguments a `Representation` is built from, each at its default until written.
86#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct Given {
88    pub peaks: usize,
89    /// `None` until written: `spectrum` then sizes its transform to the range, and every
90    /// other framed reading steps by `DEFAULT_FRAME_SECS`.
91    pub frame: Option<f64>,
92    pub depth: usize,
93    pub oversample: u32,
94    pub floor_db: f64,
95    pub norm: Norm,
96    pub max_pairs: usize,
97    /// `None` until written: each windowed reading then takes its own default.
98    pub window: Option<f64>,
99    pub curve: bool,
100    pub bpm: Option<f64>,
101    pub cell_secs: f64,
102    pub smooth_secs: f64,
103}
104
105impl Default for Given {
106    fn default() -> Given {
107        Given {
108            peaks: DEFAULT_MAX_PEAKS,
109            frame: None,
110            depth: DEFAULT_LEDGER_DEPTH,
111            oversample: DEFAULT_OVERSAMPLE,
112            floor_db: DEFAULT_FLOOR_DB,
113            norm: Norm::L1,
114            max_pairs: DEFAULT_MAX_PAIRS,
115            window: None,
116            curve: false,
117            bpm: None,
118            cell_secs: DEFAULT_CELL_SECS,
119            smooth_secs: DEFAULT_SMOOTH_SECS,
120        }
121    }
122}
123
124/// What a reading reads, which decides whether its answer is exact or measured.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub enum Reads {
127    /// The written closed form alone: always exact.
128    ClosedForm,
129    /// Whichever the node holds: exact off a closed form, measured off samples.
130    Either,
131    /// Samples alone: always measured.
132    Samples,
133    /// Samples and the graph that rendered them, so no file answers it.
134    Graph,
135    /// The resolved instances, before anything renders; nothing composes for it.
136    Structure,
137}
138
139impl Reads {
140    pub fn consumes(self, closed: bool) -> Consumes {
141        match self {
142            Reads::ClosedForm | Reads::Structure => Consumes::ClosedForm,
143            Reads::Either if closed => Consumes::ClosedForm,
144            Reads::Either | Reads::Samples | Reads::Graph => Consumes::Buffer,
145        }
146    }
147
148    pub fn off_samples(self, sampled: bool) -> bool {
149        match self {
150            Reads::Samples => true,
151            Reads::Either => sampled,
152            Reads::ClosedForm | Reads::Graph | Reads::Structure => false,
153        }
154    }
155}
156
157pub struct Reading {
158    pub name: &'static str,
159    pub takes: &'static [Arg],
160    pub reads: Reads,
161    pub help: &'static str,
162    build: fn(&Given) -> Representation,
163}
164
165impl Reading {
166    pub fn representation(&self, given: &Given) -> Representation {
167        (self.build)(given)
168    }
169}
170
171static LINES: Reading = Reading {
172    name: "lines",
173    takes: &[],
174    reads: Reads::ClosedForm,
175    help: "the exact line list of a closed form",
176    build: |_| Representation::Lines,
177};
178
179static ATOMS: Reading = Reading {
180    name: "atoms",
181    takes: &[],
182    reads: Reads::ClosedForm,
183    help: "each term of a closed form: its sketch, line, decay, polynomial and window",
184    build: |_| Representation::Atoms,
185};
186
187static SPECTRUM: Reading = Reading {
188    name: "spectrum",
189    takes: &[Arg::Peaks, Arg::Frame],
190    reads: Reads::Either,
191    help: "its lines, or its spectrum averaged over every frame",
192    build: |g| Representation::Spectrum {
193        max_peaks: g.peaks,
194        frame_secs: g.frame,
195    },
196};
197
198static ENVELOPE: Reading = Reading {
199    name: "envelope",
200    takes: &[Arg::Frame],
201    reads: Reads::Either,
202    help: "rms and peak, per frame off samples",
203    build: |g| Representation::Envelope {
204        frame_secs: g.frame,
205    },
206};
207
208static DERIVATIVE: Reading = Reading {
209    name: "derivative",
210    takes: &[],
211    reads: Reads::Either,
212    help: "the derivative, or the first difference of samples",
213    build: |_| Representation::Derivative,
214};
215
216static SAMPLES: Reading = Reading {
217    name: "samples",
218    takes: &[Arg::Buffer],
219    reads: Reads::Samples,
220    help: "the samples themselves; a `.wav` destination writes them as audio, an \
221           `alsa:<device>` one plays them as they render",
222    build: |_| Representation::Samples,
223};
224
225static LEDGER: Reading = Reading {
226    name: "ledger",
227    takes: &[Arg::Depth, Arg::Brief, Arg::Skim],
228    reads: Reads::Graph,
229    help: "rms, peak, share and clipping per node under the target",
230    build: |g| Representation::Ledger { depth: g.depth },
231};
232
233static PITCH: Reading = Reading {
234    name: "pitch",
235    takes: &[Arg::Peaks, Arg::Frame],
236    reads: Reads::Either,
237    help: "the notes it holds, per frame off samples",
238    build: |g| Representation::Pitch {
239        max_notes: g.peaks,
240        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
241    },
242};
243
244static FORMANTS: Reading = Reading {
245    name: "formants",
246    takes: &[Arg::Peaks, Arg::Frame],
247    reads: Reads::Samples,
248    help: "formant frequencies, bandwidths and levels per frame",
249    build: |g| Representation::Formants {
250        max_formants: g.peaks,
251        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
252    },
253};
254
255static STEREO: Reading = Reading {
256    name: "stereo",
257    takes: &[Arg::Frame],
258    reads: Reads::Samples,
259    help: "correlation, width and balance between two channels, per frame",
260    build: |g| Representation::Stereo {
261        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
262    },
263};
264
265static BANDS: Reading = Reading {
266    name: "bands",
267    takes: &[],
268    reads: Reads::Samples,
269    help: "each auditory band's level over time",
270    build: |_| Representation::Bands,
271};
272
273static CREST: Reading = Reading {
274    name: "crest",
275    takes: &[],
276    reads: Reads::Samples,
277    help: "peak against rms, broadband and per band",
278    build: |_| Representation::Crest,
279};
280
281static LOUDNESS: Reading = Reading {
282    name: "loudness",
283    takes: &[],
284    reads: Reads::Samples,
285    help: "integrated, momentary and short-term loudness in LUFS",
286    build: |_| Representation::Loudness,
287};
288
289static ONSETS: Reading = Reading {
290    name: "onsets",
291    takes: &[],
292    reads: Reads::Samples,
293    help: "onset times, their strength and the spacing between them",
294    build: |_| Representation::Onsets,
295};
296
297static ALIAS: Reading = Reading {
298    name: "alias",
299    takes: &[Arg::Oversample],
300    reads: Reads::Graph,
301    help: "what folded, against a re-render at a multiple of the rate",
302    build: |g| Representation::Alias {
303        oversample: g.oversample,
304    },
305};
306
307static DISSONANCE: Reading = Reading {
308    name: "dissonance",
309    takes: &[Arg::Frame, Arg::Floor, Arg::Pairs],
310    reads: Reads::Either,
311    help: "Sethares' sensory roughness per frame of its partials over a floor, with its mean, \
312           max and min",
313    build: |g| Representation::Harmony {
314        reading: Harmonic::Dissonance {
315            max_pairs: g.max_pairs,
316        },
317        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
318        floor_db: g.floor_db,
319    },
320};
321
322static VOICE_LEADING: Reading = Reading {
323    name: "voice_leading",
324    takes: &[Arg::Frame, Arg::Floor, Arg::Norm],
325    reads: Reads::Either,
326    help: "its chords over time, and the smallest voice leading between each two, l1 or l2 \
327           in semitones",
328    build: |g| Representation::Harmony {
329        reading: Harmonic::VoiceLeading { norm: g.norm },
330        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
331        floor_db: g.floor_db,
332    },
333};
334
335static TONAL_DISTANCE: Reading = Reading {
336    name: "tonal_distance",
337    takes: &[Arg::Frame, Arg::Floor],
338    reads: Reads::Either,
339    help: "its chords over time, the Tonnetz steps between each two triads and each one's \
340           from the first",
341    build: |g| Representation::Harmony {
342        reading: Harmonic::TonalDistance,
343        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
344        floor_db: g.floor_db,
345    },
346};
347
348static INHARMONICITY: Reading = Reading {
349    name: "inharmonicity",
350    takes: &[Arg::Frame, Arg::Floor],
351    reads: Reads::Either,
352    help: "its harmonic series per frame: each member's cents off n f1, the stiff-string B each \
353           fits, and the energy in no series",
354    build: |g| Representation::Harmony {
355        reading: Harmonic::Inharmonicity,
356        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
357        floor_db: g.floor_db,
358    },
359};
360
361static PERIODICITY: Reading = Reading {
362    name: "periodicity",
363    takes: &[Arg::Frame, Arg::Window, Arg::Curve],
364    reads: Reads::Samples,
365    help: "how strongly its onset strength repeats per sliding window, broadband, under 150 Hz \
366           and over: the leading periods in seconds and bpm with their bounds, their \
367           autocorrelation heights, the drift between windows",
368    build: |g| Representation::Periodicity {
369        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
370        window_secs: g.window.unwrap_or(DEFAULT_PERIODICITY_WINDOW_SECS),
371        curve: g.curve,
372    },
373};
374
375static MODULATION: Reading = Reading {
376    name: "modulation",
377    takes: &[Arg::Frame, Arg::Window, Arg::Bpm],
378    reads: Reads::Samples,
379    help: "how its band envelopes move per sliding window, under 150 Hz, to 2 kHz and over: \
380           the leading modulation rates from 0.5 to 20 Hz, in beats where `bpm` is given, \
381           their depth as a modulation index, each with its bounds, the level, and the \
382           spectral centroid's mean and its own swing in Hz",
383    build: |g| Representation::Modulation {
384        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
385        window_secs: g.window.unwrap_or(DEFAULT_MODULATION_WINDOW_SECS),
386        bpm: g.bpm,
387    },
388};
389
390static SELF_SIMILARITY: Reading = Reading {
391    name: "self_similarity",
392    takes: &[
393        Arg::Frame,
394        Arg::Cell,
395        Arg::Smooth,
396        Arg::Window,
397        Arg::Floor,
398        Arg::Peaks,
399        Arg::Curve,
400    ],
401    reads: Reads::Samples,
402    help: "how its stretches resemble one another, by chroma and by band levels, over cells \
403           of `cell`: the repeated stretches as stripes, each pair of spans with its \
404           similarity and, by chroma, its transposition in semitones; Foote's novelty at every \
405           cell boundary and its peaks; `curve=1` adds the cells and the matrix",
406    build: |g| Representation::SelfSimilarity {
407        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
408        cell_secs: g.cell_secs,
409        smooth_secs: g.smooth_secs,
410        window_secs: g.window.unwrap_or(DEFAULT_NOVELTY_WINDOW_SECS),
411        floor_db: g.floor_db,
412        listed: g.peaks,
413        curve: g.curve,
414    },
415};
416
417static BINDINGS: Reading = Reading {
418    name: "bindings",
419    takes: &[Arg::Node],
420    reads: Reads::Structure,
421    help: "the bindings one instance resolved, each with its source",
422    build: |_| Representation::Bindings,
423};
424
425static ARGUMENTS: Reading = Reading {
426    name: "arguments",
427    takes: &[],
428    reads: Reads::Structure,
429    help: "each builtin call's named arguments as the numbers it was lowered with",
430    build: |_| Representation::Arguments,
431};
432
433/// Every reading a caller may ask for, in the order a listing prints them.
434pub static READINGS: [&Reading; 24] = [
435    &LINES,
436    &ATOMS,
437    &SPECTRUM,
438    &ENVELOPE,
439    &DERIVATIVE,
440    &SAMPLES,
441    &LEDGER,
442    &PITCH,
443    &FORMANTS,
444    &STEREO,
445    &BANDS,
446    &CREST,
447    &LOUDNESS,
448    &ONSETS,
449    &ALIAS,
450    &DISSONANCE,
451    &VOICE_LEADING,
452    &TONAL_DISTANCE,
453    &INHARMONICITY,
454    &PERIODICITY,
455    &MODULATION,
456    &SELF_SIMILARITY,
457    &BINDINGS,
458    &ARGUMENTS,
459];
460
461#[derive(Clone, Copy, Debug, PartialEq)]
462pub enum Representation {
463    Lines,
464    Atoms,
465    Spectrum {
466        max_peaks: usize,
467        frame_secs: Option<f64>,
468    },
469    Envelope {
470        frame_secs: Option<f64>,
471    },
472    Derivative,
473    Samples,
474    Ledger {
475        depth: usize,
476    },
477    Pitch {
478        max_notes: usize,
479        frame_secs: f64,
480    },
481    Formants {
482        max_formants: usize,
483        frame_secs: f64,
484    },
485    Stereo {
486        frame_secs: f64,
487    },
488    Bands,
489    Crest,
490    Loudness,
491    Onsets,
492    Alias {
493        oversample: u32,
494    },
495    /// The partials it is read from step by `frame_secs` and stop at `floor_db`.
496    Harmony {
497        reading: Harmonic,
498        frame_secs: f64,
499        floor_db: f64,
500    },
501    /// Windows of `window_secs` centred every `frame_secs`; `curve` adds each one's heights.
502    Periodicity {
503        frame_secs: f64,
504        window_secs: f64,
505        curve: bool,
506    },
507    /// Windows of `window_secs` centred every `frame_secs`; `bpm` adds each rate in beats.
508    Modulation {
509        frame_secs: f64,
510        window_secs: f64,
511        bpm: Option<f64>,
512    },
513    /// Cells of `cell_secs` over frames every `frame_secs`; `listed` stripes and peaks are
514    /// listed, and `curve` adds the cells and matrices.
515    SelfSimilarity {
516        frame_secs: f64,
517        cell_secs: f64,
518        smooth_secs: f64,
519        window_secs: f64,
520        floor_db: f64,
521        listed: usize,
522        curve: bool,
523    },
524    Bindings,
525    Arguments,
526}
527
528/// The readings taken off one curve of partials.
529#[derive(Clone, Copy, Debug, PartialEq, Eq)]
530pub enum Harmonic {
531    Dissonance { max_pairs: usize },
532    VoiceLeading { norm: Norm },
533    TonalDistance,
534    Inharmonicity,
535}
536
537/// What a caller asked of one node.
538#[derive(Clone, Debug, PartialEq)]
539pub struct Ask {
540    pub node: String,
541    pub representation: Representation,
542}
543
544#[derive(Clone, Debug, PartialEq)]
545pub enum Output {
546    Lines(Vec<Line>),
547    Atoms(Vec<Atom>),
548    Symbolic(Box<SpectralSum>),
549    Samples(Box<Buffer>),
550    Spectrum(Box<Spectrum>),
551    Envelope(Vec<EnvelopeFrame>),
552    Pitch(Vec<PitchFrame>),
553    Formants(Vec<FormantFrame>),
554    Stereo(Box<StereoImage>),
555    Bands(Box<Bands>),
556    Crest(Box<Crest>),
557    Loudness(Box<Loudness>),
558    Onsets(Box<Onsets>),
559    Alias(Box<Alias>),
560    Ledger(Vec<LedgerEntry>),
561    Dissonance(Box<Dissonance>),
562    VoiceLeading(Box<VoiceLeading>),
563    TonalDistance(Box<TonalDistance>),
564    Inharmonicity(Box<Inharmonicity>),
565    Periodicity(Box<Periodicity>),
566    Modulation(Box<Modulation>),
567    SelfSimilarity(Box<SelfSimilarity>),
568    Bindings(Vec<Binding>),
569    Arguments(Vec<Arguments>),
570}
571
572/// `amp (x-polynomial_at)^degree e^(-decay (x-decay_from)) e^(2pi i turns x)` on `[start, end)`
573#[derive(Clone, Debug, PartialEq)]
574pub struct Atom {
575    pub var: Var,
576    pub sketch: String,
577    pub amp: C64,
578    pub turns: f64,
579    pub decay: f64,
580    pub decay_from: f64,
581    pub degree: u16,
582    pub polynomial_at: f64,
583    pub start: Option<f64>,
584    pub end: Option<f64>,
585}
586
587/// Every answer says which reading ran and under which profile, so a caller never has to
588/// infer either from the shape of the value.
589#[derive(Clone, Debug, PartialEq)]
590pub struct Answer {
591    pub value: Output,
592    pub source: Source,
593    pub profile: &'static str,
594    pub rate: Option<u32>,
595    /// A series answers the terms it keeps, the tail beside them.
596    pub dropped: Vec<sva_formula::Line>,
597    pub tail_db: Option<f64>,
598}
599
600impl Answer {
601    pub fn whole(
602        value: Output,
603        source: Source,
604        profile: &'static str,
605        rate: Option<u32>,
606    ) -> Answer {
607        Answer {
608            value,
609            source,
610            profile,
611            rate,
612            dropped: Vec::new(),
613            tail_db: None,
614        }
615    }
616}
617
618impl Representation {
619    /// Exhaustive, so a variant with no row is a compile error.
620    pub fn reading(self) -> &'static Reading {
621        match self {
622            Representation::Lines => &LINES,
623            Representation::Atoms => &ATOMS,
624            Representation::Spectrum { .. } => &SPECTRUM,
625            Representation::Envelope { .. } => &ENVELOPE,
626            Representation::Derivative => &DERIVATIVE,
627            Representation::Samples => &SAMPLES,
628            Representation::Ledger { .. } => &LEDGER,
629            Representation::Pitch { .. } => &PITCH,
630            Representation::Formants { .. } => &FORMANTS,
631            Representation::Stereo { .. } => &STEREO,
632            Representation::Bands => &BANDS,
633            Representation::Crest => &CREST,
634            Representation::Loudness => &LOUDNESS,
635            Representation::Onsets => &ONSETS,
636            Representation::Alias { .. } => &ALIAS,
637            Representation::Harmony { reading, .. } => match reading {
638                Harmonic::Dissonance { .. } => &DISSONANCE,
639                Harmonic::VoiceLeading { .. } => &VOICE_LEADING,
640                Harmonic::TonalDistance => &TONAL_DISTANCE,
641                Harmonic::Inharmonicity => &INHARMONICITY,
642            },
643            Representation::Periodicity { .. } => &PERIODICITY,
644            Representation::Modulation { .. } => &MODULATION,
645            Representation::SelfSimilarity { .. } => &SELF_SIMILARITY,
646            Representation::Bindings => &BINDINGS,
647            Representation::Arguments => &ARGUMENTS,
648        }
649    }
650
651    pub fn name(self) -> &'static str {
652        self.reading().name
653    }
654
655    pub fn consumes(self, closed: bool) -> Consumes {
656        self.reading().reads.consumes(closed)
657    }
658
659    /// Whether a node's samples alone answer this reading, `sampled` where it holds samples.
660    pub fn off_samples(self, sampled: bool) -> bool {
661        self.reading().reads.off_samples(sampled)
662    }
663
664    pub fn from_name(name: &str) -> Option<Representation> {
665        READINGS
666            .iter()
667            .find(|row| row.name == name)
668            .map(|row| row.representation(&Given::default()))
669    }
670}