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, every channel's in channel order with no \
176           channel named; `atoms` names each term's",
177    build: |_| Representation::Lines,
178};
179
180static ATOMS: Reading = Reading {
181    name: "atoms",
182    takes: &[],
183    reads: Reads::ClosedForm,
184    help: "each term of a closed form: its channel, sketch, line, decay, polynomial and window",
185    build: |_| Representation::Atoms,
186};
187
188static SPECTRUM: Reading = Reading {
189    name: "spectrum",
190    takes: &[Arg::Peaks, Arg::Frame],
191    reads: Reads::Either,
192    help: "its lines, or its spectrum averaged over every frame and, power per bin, every \
193           channel; off samples a Hann spans the samples a frame holds, zero past them, each \
194           third-octave band reads as the amplitude of the one sine carrying its power, and, \
195           loudest first, a local maximum is no peak where its bin holds no more than the \
196           peaks kept, and a DC bin 0 holds as its maximum, can leak there, each bounded by \
197           Harris's (1978) Hann sidelobe bound 1/(pi m (m^2 - 1)) at m bins, its sinusoid taken \
198           anywhere within half a bin of its own: a bound where the frame is stationary \
199           sinusoids at the kept peaks",
200    build: |g| Representation::Spectrum {
201        max_peaks: g.peaks,
202        frame_secs: g.frame,
203    },
204};
205
206static ENVELOPE: Reading = Reading {
207    name: "envelope",
208    takes: &[Arg::Frame],
209    reads: Reads::Either,
210    help: "off a closed form the squared modulus of its analytic signal, x^2 + H{x}^2, as \
211           atoms per channel; off samples rms and peak per frame: the rms of every channel's \
212           samples together, the peak of any channel",
213    build: |g| Representation::Envelope {
214        frame_secs: g.frame,
215    },
216};
217
218static DERIVATIVE: Reading = Reading {
219    name: "derivative",
220    takes: &[],
221    reads: Reads::Either,
222    help: "the derivative's atoms per channel, each series listed as `atoms` lists it; off \
223           samples the first difference, (x[n] - x[n-1]) * rate at every sample but the first",
224    build: |_| Representation::Derivative,
225};
226
227static SAMPLES: Reading = Reading {
228    name: "samples",
229    takes: &[Arg::Buffer],
230    reads: Reads::Samples,
231    help: "the samples themselves; a `.wav` destination writes them as audio, an \
232           `alsa:<device>` one plays them as they render",
233    build: |_| Representation::Samples,
234};
235
236static LEDGER: Reading = Reading {
237    name: "ledger",
238    takes: &[Arg::Depth, Arg::Brief, Arg::Skim],
239    reads: Reads::Graph,
240    help: "rms, peak, share and clipping per node under the target",
241    build: |g| Representation::Ledger { depth: g.depth },
242};
243
244static PITCH: Reading = Reading {
245    name: "pitch",
246    takes: &[Arg::Peaks, Arg::Frame],
247    reads: Reads::Either,
248    help: "the `peaks` loudest notes it holds, per frame off samples, every channel's power \
249           per bin averaged; off samples its peaks are picked as `spectrum` picks them, and a series' \
250           fundamental is read within half a bin of its root's peak, the rate over the samples \
251           its Hann spans",
252    build: |g| Representation::Pitch {
253        max_notes: g.peaks,
254        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
255    },
256};
257
258static FORMANTS: Reading = Reading {
259    name: "formants",
260    takes: &[Arg::Peaks, Arg::Frame],
261    reads: Reads::Samples,
262    help: "formant frequencies, bandwidths and levels per frame, the `peaks` lowest, off \
263           every channel's autocorrelation averaged, an all-pole fit of 2 poles and 1 per kHz \
264           of rate, at most 64: a frame or target holding no more samples than its order \
265           refuses, and a tail frame reads zero past its end",
266    build: |g| Representation::Formants {
267        max_formants: g.peaks,
268        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
269    },
270};
271
272static STEREO: Reading = Reading {
273    name: "stereo",
274    takes: &[Arg::Frame],
275    reads: Reads::Samples,
276    help: "correlation, width and balance between exactly two channels, per frame; any other \
277           count refuses",
278    build: |g| Representation::Stereo {
279        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
280    },
281};
282
283static BANDS: Reading = Reading {
284    name: "bands",
285    takes: &[],
286    reads: Reads::Samples,
287    help: "each auditory band's level over time, every channel's band energy averaged",
288    build: |_| Representation::Bands,
289};
290
291static CREST: Reading = Reading {
292    name: "crest",
293    takes: &[],
294    reads: Reads::Samples,
295    help: "peak against rms, broadband and per band: any channel's peak against the rms of \
296           every channel's samples together",
297    build: |_| Representation::Crest,
298};
299
300static LOUDNESS: Reading = Reading {
301    name: "loudness",
302    takes: &[],
303    reads: Reads::Samples,
304    help: "integrated, momentary and short-term loudness in LUFS",
305    build: |_| Representation::Loudness,
306};
307
308static ONSETS: Reading = Reading {
309    name: "onsets",
310    takes: &[],
311    reads: Reads::Samples,
312    help: "onset times, their strength and the spacing between them, each channel's spectral \
313           flux summed",
314    build: |_| Representation::Onsets,
315};
316
317static ALIAS: Reading = Reading {
318    name: "alias",
319    takes: &[Arg::Oversample],
320    reads: Reads::Graph,
321    help: "what folded, against a re-render at a multiple of the rate",
322    build: |g| Representation::Alias {
323        oversample: g.oversample,
324    },
325};
326
327static DISSONANCE: Reading = Reading {
328    name: "dissonance",
329    takes: &[Arg::Frame, Arg::Floor, Arg::Pairs],
330    reads: Reads::Either,
331    help: "Sethares' sensory roughness per frame of its partials over a floor, with its mean, \
332           max and min; measured partials off every channel's power per bin averaged",
333    build: |g| Representation::Harmony {
334        reading: Harmonic::Dissonance {
335            max_pairs: g.max_pairs,
336        },
337        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
338        floor_db: g.floor_db,
339    },
340};
341
342static VOICE_LEADING: Reading = Reading {
343    name: "voice_leading",
344    takes: &[Arg::Frame, Arg::Floor, Arg::Norm],
345    reads: Reads::Either,
346    help: "its chords over time, and the smallest voice leading between each two, l1 or l2 \
347           in semitones; measured partials off every channel's power per bin averaged",
348    build: |g| Representation::Harmony {
349        reading: Harmonic::VoiceLeading { norm: g.norm },
350        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
351        floor_db: g.floor_db,
352    },
353};
354
355static TONAL_DISTANCE: Reading = Reading {
356    name: "tonal_distance",
357    takes: &[Arg::Frame, Arg::Floor],
358    reads: Reads::Either,
359    help: "its chords over time, the Tonnetz steps between each two triads and each one's \
360           from the first triad; measured partials off every channel's power per bin averaged",
361    build: |g| Representation::Harmony {
362        reading: Harmonic::TonalDistance,
363        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
364        floor_db: g.floor_db,
365    },
366};
367
368static INHARMONICITY: Reading = Reading {
369    name: "inharmonicity",
370    takes: &[Arg::Frame, Arg::Floor],
371    reads: Reads::Either,
372    help: "its harmonic series per frame: each member's cents off n f1, the stiff-string B each \
373           fits, and the energy in no series; measured partials off every channel's power per \
374           bin averaged",
375    build: |g| Representation::Harmony {
376        reading: Harmonic::Inharmonicity,
377        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
378        floor_db: g.floor_db,
379    },
380};
381
382static PERIODICITY: Reading = Reading {
383    name: "periodicity",
384    takes: &[Arg::Frame, Arg::Window, Arg::Curve],
385    reads: Reads::Samples,
386    help: "how strongly its onset strength repeats per sliding window, broadband, under 150 Hz \
387           and over: the leading periods up to 2 s, or half the window, in seconds and bpm \
388           with their bounds, their \
389           autocorrelation heights, the drift between windows; every channel's band energy \
390           averaged",
391    build: |g| Representation::Periodicity {
392        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
393        window_secs: g.window.unwrap_or(DEFAULT_PERIODICITY_WINDOW_SECS),
394        curve: g.curve,
395    },
396};
397
398static MODULATION: Reading = Reading {
399    name: "modulation",
400    takes: &[Arg::Frame, Arg::Window, Arg::Bpm],
401    reads: Reads::Samples,
402    help: "how its band envelopes move per sliding window, under 150 Hz, to 2 kHz and over: \
403           the leading modulation rates from 0.5 Hz, or the slowest two of whose periods the \
404           window holds, to 20 Hz, in beats where `bpm` is given, \
405           their depth as a modulation index, each with its bounds, the level, and the \
406           spectral centroid's mean and its own swing in Hz; every channel's band energy \
407           averaged",
408    build: |g| Representation::Modulation {
409        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
410        window_secs: g.window.unwrap_or(DEFAULT_MODULATION_WINDOW_SECS),
411        bpm: g.bpm,
412    },
413};
414
415static SELF_SIMILARITY: Reading = Reading {
416    name: "self_similarity",
417    takes: &[
418        Arg::Frame,
419        Arg::Cell,
420        Arg::Smooth,
421        Arg::Window,
422        Arg::Floor,
423        Arg::Peaks,
424        Arg::Curve,
425    ],
426    reads: Reads::Samples,
427    help: "how its stretches resemble one another, by chroma and by band levels, over cells \
428           of `cell`: the repeated stretches as stripes, each pair of spans with its \
429           similarity and, by chroma, its transposition in semitones; Foote's novelty at every \
430           cell boundary and its peaks; `curve=1` adds the cells and the matrix; every \
431           channel's power per bin and band energy averaged",
432    build: |g| Representation::SelfSimilarity {
433        frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
434        cell_secs: g.cell_secs,
435        smooth_secs: g.smooth_secs,
436        window_secs: g.window.unwrap_or(DEFAULT_NOVELTY_WINDOW_SECS),
437        floor_db: g.floor_db,
438        listed: g.peaks,
439        curve: g.curve,
440    },
441};
442
443static BINDINGS: Reading = Reading {
444    name: "bindings",
445    takes: &[Arg::Node],
446    reads: Reads::Structure,
447    help: "the bindings one instance resolved, each with its source",
448    build: |_| Representation::Bindings,
449};
450
451static ARGUMENTS: Reading = Reading {
452    name: "arguments",
453    takes: &[],
454    reads: Reads::Structure,
455    help: "each builtin call's named arguments as the numbers it was lowered with",
456    build: |_| Representation::Arguments,
457};
458
459/// Every reading a caller may ask for, in the order a listing prints them.
460pub static READINGS: [&Reading; 24] = [
461    &LINES,
462    &ATOMS,
463    &SPECTRUM,
464    &ENVELOPE,
465    &DERIVATIVE,
466    &SAMPLES,
467    &LEDGER,
468    &PITCH,
469    &FORMANTS,
470    &STEREO,
471    &BANDS,
472    &CREST,
473    &LOUDNESS,
474    &ONSETS,
475    &ALIAS,
476    &DISSONANCE,
477    &VOICE_LEADING,
478    &TONAL_DISTANCE,
479    &INHARMONICITY,
480    &PERIODICITY,
481    &MODULATION,
482    &SELF_SIMILARITY,
483    &BINDINGS,
484    &ARGUMENTS,
485];
486
487#[derive(Clone, Copy, Debug, PartialEq)]
488pub enum Representation {
489    Lines,
490    Atoms,
491    Spectrum {
492        max_peaks: usize,
493        frame_secs: Option<f64>,
494    },
495    Envelope {
496        frame_secs: Option<f64>,
497    },
498    Derivative,
499    Samples,
500    Ledger {
501        depth: usize,
502    },
503    Pitch {
504        max_notes: usize,
505        frame_secs: f64,
506    },
507    Formants {
508        max_formants: usize,
509        frame_secs: f64,
510    },
511    Stereo {
512        frame_secs: f64,
513    },
514    Bands,
515    Crest,
516    Loudness,
517    Onsets,
518    Alias {
519        oversample: u32,
520    },
521    /// The partials it is read from step by `frame_secs` and stop at `floor_db`.
522    Harmony {
523        reading: Harmonic,
524        frame_secs: f64,
525        floor_db: f64,
526    },
527    /// Windows of `window_secs` centred every `frame_secs`; `curve` adds each one's heights.
528    Periodicity {
529        frame_secs: f64,
530        window_secs: f64,
531        curve: bool,
532    },
533    /// Windows of `window_secs` centred every `frame_secs`; `bpm` adds each rate in beats.
534    Modulation {
535        frame_secs: f64,
536        window_secs: f64,
537        bpm: Option<f64>,
538    },
539    /// Cells of `cell_secs` over frames every `frame_secs`; `listed` stripes and peaks are
540    /// listed, and `curve` adds the cells and matrices.
541    SelfSimilarity {
542        frame_secs: f64,
543        cell_secs: f64,
544        smooth_secs: f64,
545        window_secs: f64,
546        floor_db: f64,
547        listed: usize,
548        curve: bool,
549    },
550    Bindings,
551    Arguments,
552}
553
554/// The readings taken off one curve of partials.
555#[derive(Clone, Copy, Debug, PartialEq, Eq)]
556pub enum Harmonic {
557    Dissonance { max_pairs: usize },
558    VoiceLeading { norm: Norm },
559    TonalDistance,
560    Inharmonicity,
561}
562
563/// What a caller asked of one node.
564#[derive(Clone, Debug, PartialEq)]
565pub struct Ask {
566    pub node: String,
567    pub representation: Representation,
568}
569
570#[derive(Clone, Debug, PartialEq)]
571pub enum Output {
572    Lines(Vec<Line>),
573    Atoms(Vec<Atom>),
574    Symbolic(Box<SpectralSum>),
575    Samples(Box<Buffer>),
576    Spectrum(Box<Spectrum>),
577    Envelope(Vec<EnvelopeFrame>),
578    Pitch(Vec<PitchFrame>),
579    Formants(Vec<FormantFrame>),
580    Stereo(Box<StereoImage>),
581    Bands(Box<Bands>),
582    Crest(Box<Crest>),
583    Loudness(Box<Loudness>),
584    Onsets(Box<Onsets>),
585    Alias(Box<Alias>),
586    Ledger(Vec<LedgerEntry>),
587    Dissonance(Box<Dissonance>),
588    VoiceLeading(Box<VoiceLeading>),
589    TonalDistance(Box<TonalDistance>),
590    Inharmonicity(Box<Inharmonicity>),
591    Periodicity(Box<Periodicity>),
592    Modulation(Box<Modulation>),
593    SelfSimilarity(Box<SelfSimilarity>),
594    Bindings(Vec<Binding>),
595    Arguments(Vec<Arguments>),
596}
597
598/// `amp (x-polynomial_at)^degree e^(-decay (x-decay_from)) e^(2pi i turns x)` on `[start, end)`
599#[derive(Clone, Debug, PartialEq)]
600pub struct Atom {
601    pub var: Var,
602    /// The lane, or channel, it stands in.
603    pub channel: usize,
604    pub sketch: String,
605    pub amp: C64,
606    pub turns: f64,
607    pub decay: f64,
608    pub decay_from: f64,
609    pub degree: u16,
610    pub polynomial_at: f64,
611    pub start: Option<f64>,
612    pub end: Option<f64>,
613}
614
615/// Every answer says which reading ran and under which profile, so a caller never has to
616/// infer either from the shape of the value.
617#[derive(Clone, Debug, PartialEq)]
618pub struct Answer {
619    pub value: Output,
620    pub source: Source,
621    pub profile: &'static str,
622    pub rate: Option<u32>,
623    /// A series answers the terms it keeps, the tail beside them.
624    pub dropped: Vec<sva_formula::Line>,
625    pub tail_db: Option<f64>,
626}
627
628impl Answer {
629    pub fn whole(
630        value: Output,
631        source: Source,
632        profile: &'static str,
633        rate: Option<u32>,
634    ) -> Answer {
635        Answer {
636            value,
637            source,
638            profile,
639            rate,
640            dropped: Vec::new(),
641            tail_db: None,
642        }
643    }
644}
645
646impl Representation {
647    /// Exhaustive, so a variant with no row is a compile error.
648    pub fn reading(self) -> &'static Reading {
649        match self {
650            Representation::Lines => &LINES,
651            Representation::Atoms => &ATOMS,
652            Representation::Spectrum { .. } => &SPECTRUM,
653            Representation::Envelope { .. } => &ENVELOPE,
654            Representation::Derivative => &DERIVATIVE,
655            Representation::Samples => &SAMPLES,
656            Representation::Ledger { .. } => &LEDGER,
657            Representation::Pitch { .. } => &PITCH,
658            Representation::Formants { .. } => &FORMANTS,
659            Representation::Stereo { .. } => &STEREO,
660            Representation::Bands => &BANDS,
661            Representation::Crest => &CREST,
662            Representation::Loudness => &LOUDNESS,
663            Representation::Onsets => &ONSETS,
664            Representation::Alias { .. } => &ALIAS,
665            Representation::Harmony { reading, .. } => match reading {
666                Harmonic::Dissonance { .. } => &DISSONANCE,
667                Harmonic::VoiceLeading { .. } => &VOICE_LEADING,
668                Harmonic::TonalDistance => &TONAL_DISTANCE,
669                Harmonic::Inharmonicity => &INHARMONICITY,
670            },
671            Representation::Periodicity { .. } => &PERIODICITY,
672            Representation::Modulation { .. } => &MODULATION,
673            Representation::SelfSimilarity { .. } => &SELF_SIMILARITY,
674            Representation::Bindings => &BINDINGS,
675            Representation::Arguments => &ARGUMENTS,
676        }
677    }
678
679    pub fn name(self) -> &'static str {
680        self.reading().name
681    }
682
683    pub fn consumes(self, closed: bool) -> Consumes {
684        self.reading().reads.consumes(closed)
685    }
686
687    /// Whether a node's samples alone answer this reading, `sampled` where it holds samples.
688    pub fn off_samples(self, sampled: bool) -> bool {
689        self.reading().reads.off_samples(sampled)
690    }
691
692    pub fn from_name(name: &str) -> Option<Representation> {
693        READINGS
694            .iter()
695            .find(|row| row.name == name)
696            .map(|row| row.representation(&Given::default()))
697    }
698}