1use 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, Prune, PrunedBands, Source, Spectrum, StereoImage,
14};
15
16use crate::arguments::Arguments;
17use crate::bindings::Binding;
18
19pub 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;
24pub const DEFAULT_BUFFER_SECS: f64 = 0.05;
26pub const DEFAULT_FLOOR_DB: f64 = -40.0;
28pub const DEFAULT_PRUNE_DB: f64 = 1.0;
32pub const DEFAULT_BAND_FLOOR_DB: f64 = -80.0;
34pub const DEFAULT_MAX_PAIRS: usize = 1 << 17;
36pub const DEFAULT_PERIODICITY_WINDOW_SECS: f64 = 6.0;
38pub const DEFAULT_MODULATION_WINDOW_SECS: f64 = 4.0;
40pub const DEFAULT_CELL_SECS: f64 = 0.5;
42pub const DEFAULT_SMOOTH_SECS: f64 = 4.0;
44pub const DEFAULT_NOVELTY_WINDOW_SECS: f64 = 8.0;
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum Arg {
50 Peaks,
51 Frame,
52 Depth,
53 Oversample,
54 Brief,
55 Skim,
56 Node,
57 Buffer,
58 Floor,
59 Norm,
60 Pairs,
61 Window,
62 Curve,
63 Bpm,
64 Cell,
65 Smooth,
66 Prune,
67}
68
69impl Arg {
70 pub fn key(self) -> &'static str {
71 match self {
72 Arg::Peaks => "peaks",
73 Arg::Frame => "frame",
74 Arg::Depth => "depth",
75 Arg::Oversample => "oversample",
76 Arg::Brief => "brief",
77 Arg::Skim => "skim",
78 Arg::Node => "node",
79 Arg::Buffer => "buffer",
80 Arg::Floor => "floor",
81 Arg::Norm => "norm",
82 Arg::Pairs => "pairs",
83 Arg::Window => "window",
84 Arg::Curve => "curve",
85 Arg::Bpm => "bpm",
86 Arg::Cell => "cell",
87 Arg::Smooth => "smooth",
88 Arg::Prune => "prune",
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq)]
95pub struct Given {
96 pub peaks: usize,
97 pub frame: Option<f64>,
100 pub depth: usize,
101 pub oversample: u32,
102 pub floor_db: Option<f64>,
104 pub norm: Norm,
105 pub max_pairs: usize,
106 pub window: Option<f64>,
108 pub curve: bool,
109 pub bpm: Option<f64>,
110 pub cell_secs: f64,
111 pub smooth_secs: f64,
112 pub prune_db: Option<f64>,
114}
115
116impl Default for Given {
117 fn default() -> Given {
118 Given {
119 peaks: DEFAULT_MAX_PEAKS,
120 frame: None,
121 depth: DEFAULT_LEDGER_DEPTH,
122 oversample: DEFAULT_OVERSAMPLE,
123 floor_db: None,
124 norm: Norm::L1,
125 max_pairs: DEFAULT_MAX_PAIRS,
126 window: None,
127 curve: false,
128 bpm: None,
129 cell_secs: DEFAULT_CELL_SECS,
130 smooth_secs: DEFAULT_SMOOTH_SECS,
131 prune_db: None,
132 }
133 }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub enum Reads {
139 ClosedForm,
141 Either,
143 Samples,
145 Graph,
147 Structure,
149}
150
151impl Reads {
152 pub fn consumes(self, closed: bool) -> Consumes {
153 match self {
154 Reads::ClosedForm | Reads::Structure => Consumes::ClosedForm,
155 Reads::Either if closed => Consumes::ClosedForm,
156 Reads::Either | Reads::Samples | Reads::Graph => Consumes::Buffer,
157 }
158 }
159
160 pub fn off_samples(self, sampled: bool) -> bool {
161 match self {
162 Reads::Samples => true,
163 Reads::Either => sampled,
164 Reads::ClosedForm | Reads::Graph | Reads::Structure => false,
165 }
166 }
167}
168
169pub struct Reading {
170 pub name: &'static str,
171 pub takes: &'static [Arg],
172 pub reads: Reads,
173 pub help: &'static str,
174 build: fn(&Given) -> Representation,
175}
176
177impl Reading {
178 pub fn representation(&self, given: &Given) -> Representation {
179 (self.build)(given)
180 }
181}
182
183static LINES: Reading = Reading {
184 name: "lines",
185 takes: &[],
186 reads: Reads::ClosedForm,
187 help: "the exact line list of a closed form, every channel's in channel order with no \
188 channel named; `atoms` names each term's",
189 build: |_| Representation::Lines,
190};
191
192static ATOMS: Reading = Reading {
193 name: "atoms",
194 takes: &[],
195 reads: Reads::ClosedForm,
196 help: "each term of a closed form: its channel, sketch, line, decay, polynomial and window",
197 build: |_| Representation::Atoms,
198};
199
200static SPECTRUM: Reading = Reading {
201 name: "spectrum",
202 takes: &[Arg::Peaks, Arg::Frame],
203 reads: Reads::Either,
204 help: "its lines, or its spectrum averaged over every frame and, power per bin, every \
205 channel; off samples a Hann spans the samples a frame holds, zero past them, each \
206 third-octave band reads as the amplitude of the one sine carrying its power, and, \
207 loudest first, a local maximum is no peak where its bin holds no more than the \
208 peaks kept, and a DC bin 0 holds as its maximum, can leak there, each bounded by \
209 Harris's (1978) Hann sidelobe bound 1/(pi m (m^2 - 1)) at m bins, its sinusoid taken \
210 anywhere within half a bin of its own: a bound where the frame is stationary \
211 sinusoids at the kept peaks",
212 build: |g| Representation::Spectrum {
213 max_peaks: g.peaks,
214 frame_secs: g.frame,
215 },
216};
217
218static ENVELOPE: Reading = Reading {
219 name: "envelope",
220 takes: &[Arg::Frame],
221 reads: Reads::Either,
222 help: "off a closed form the squared modulus of its analytic signal, x^2 + H{x}^2, as \
223 atoms per channel; off samples rms and peak per frame: the rms of every channel's \
224 samples together, the peak of any channel",
225 build: |g| Representation::Envelope {
226 frame_secs: g.frame,
227 },
228};
229
230static DERIVATIVE: Reading = Reading {
231 name: "derivative",
232 takes: &[],
233 reads: Reads::Either,
234 help: "the derivative's atoms per channel, each series listed as `atoms` lists it; off \
235 samples the first difference, (x[n] - x[n-1]) * rate at every sample but the first",
236 build: |_| Representation::Derivative,
237};
238
239static SAMPLES: Reading = Reading {
240 name: "samples",
241 takes: &[Arg::Buffer],
242 reads: Reads::Samples,
243 help: "the samples themselves; a `.wav` destination writes them as audio, an \
244 `alsa:<device>` one plays them as they render",
245 build: |_| Representation::Samples,
246};
247
248static LEDGER: Reading = Reading {
249 name: "ledger",
250 takes: &[Arg::Depth, Arg::Brief, Arg::Skim],
251 reads: Reads::Graph,
252 help: "rms, peak, share and clipping per node under the target",
253 build: |g| Representation::Ledger { depth: g.depth },
254};
255
256static PITCH: Reading = Reading {
257 name: "pitch",
258 takes: &[Arg::Peaks, Arg::Frame],
259 reads: Reads::Either,
260 help: "the `peaks` loudest notes it holds, per frame off samples, every channel's power \
261 per bin averaged; off samples its peaks are picked as `spectrum` picks them, and a series' \
262 fundamental is read within half a bin of its root's peak, the rate over the samples \
263 its Hann spans",
264 build: |g| Representation::Pitch {
265 max_notes: g.peaks,
266 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
267 },
268};
269
270static FORMANTS: Reading = Reading {
271 name: "formants",
272 takes: &[Arg::Peaks, Arg::Frame],
273 reads: Reads::Samples,
274 help: "formant frequencies, bandwidths and levels per frame, the `peaks` lowest, off \
275 every channel's autocorrelation averaged, an all-pole fit of 2 poles and 1 per kHz \
276 of rate, at most 64: a frame or target holding no more samples than its order \
277 refuses, and a tail frame reads zero past its end",
278 build: |g| Representation::Formants {
279 max_formants: g.peaks,
280 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
281 },
282};
283
284static STEREO: Reading = Reading {
285 name: "stereo",
286 takes: &[Arg::Frame],
287 reads: Reads::Samples,
288 help: "correlation, width and balance between exactly two channels, per frame; any other \
289 count refuses",
290 build: |g| Representation::Stereo {
291 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
292 },
293};
294
295static BANDS: Reading = Reading {
296 name: "bands",
297 takes: &[Arg::Prune, Arg::Floor],
298 reads: Reads::Samples,
299 help: "each auditory band's level over time, every channel's band energy averaged, at \
300 2 kHz; `prune` instead reads each band's track at every source sample and answers \
301 knots whose line, in dB, stays within `prune` of every one, the track cut to \
302 `floor` under the band's peak, with the largest error the line reached there",
303 build: |g| Representation::Bands {
304 prune: g.prune_db.map(|eps_db| Prune {
305 eps_db,
306 floor_db: g.floor_db.unwrap_or(DEFAULT_BAND_FLOOR_DB),
307 }),
308 },
309};
310
311static CREST: Reading = Reading {
312 name: "crest",
313 takes: &[],
314 reads: Reads::Samples,
315 help: "peak against rms, broadband and per band: any channel's peak against the rms of \
316 every channel's samples together",
317 build: |_| Representation::Crest,
318};
319
320static LOUDNESS: Reading = Reading {
321 name: "loudness",
322 takes: &[],
323 reads: Reads::Samples,
324 help: "integrated, momentary and short-term loudness in LUFS",
325 build: |_| Representation::Loudness,
326};
327
328static ONSETS: Reading = Reading {
329 name: "onsets",
330 takes: &[],
331 reads: Reads::Samples,
332 help: "onset times, their strength and the spacing between them, each channel's spectral \
333 flux summed",
334 build: |_| Representation::Onsets,
335};
336
337static ALIAS: Reading = Reading {
338 name: "alias",
339 takes: &[Arg::Oversample],
340 reads: Reads::Graph,
341 help: "what folded, against a re-render at a multiple of the rate",
342 build: |g| Representation::Alias {
343 oversample: g.oversample,
344 },
345};
346
347static DISSONANCE: Reading = Reading {
348 name: "dissonance",
349 takes: &[Arg::Frame, Arg::Floor, Arg::Pairs],
350 reads: Reads::Either,
351 help: "Sethares' sensory roughness per frame of its partials over a floor, with its mean, \
352 max and min; measured partials off every channel's power per bin averaged",
353 build: |g| Representation::Harmony {
354 reading: Harmonic::Dissonance {
355 max_pairs: g.max_pairs,
356 },
357 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
358 floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
359 },
360};
361
362static VOICE_LEADING: Reading = Reading {
363 name: "voice_leading",
364 takes: &[Arg::Frame, Arg::Floor, Arg::Norm],
365 reads: Reads::Either,
366 help: "its chords over time, and the smallest voice leading between each two, l1 or l2 \
367 in semitones; measured partials off every channel's power per bin averaged",
368 build: |g| Representation::Harmony {
369 reading: Harmonic::VoiceLeading { norm: g.norm },
370 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
371 floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
372 },
373};
374
375static TONAL_DISTANCE: Reading = Reading {
376 name: "tonal_distance",
377 takes: &[Arg::Frame, Arg::Floor],
378 reads: Reads::Either,
379 help: "its chords over time, the Tonnetz steps between each two triads and each one's \
380 from the first triad; measured partials off every channel's power per bin averaged",
381 build: |g| Representation::Harmony {
382 reading: Harmonic::TonalDistance,
383 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
384 floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
385 },
386};
387
388static INHARMONICITY: Reading = Reading {
389 name: "inharmonicity",
390 takes: &[Arg::Frame, Arg::Floor],
391 reads: Reads::Either,
392 help: "its harmonic series per frame: each member's cents off n f1, the stiff-string B each \
393 fits, and the energy in no series; measured partials off every channel's power per \
394 bin averaged",
395 build: |g| Representation::Harmony {
396 reading: Harmonic::Inharmonicity,
397 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
398 floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
399 },
400};
401
402static PERIODICITY: Reading = Reading {
403 name: "periodicity",
404 takes: &[Arg::Frame, Arg::Window, Arg::Curve],
405 reads: Reads::Samples,
406 help: "how strongly its onset strength repeats per sliding window, broadband, under 150 Hz \
407 and over: the leading periods up to 2 s, or half the window, in seconds and bpm \
408 with their bounds, their \
409 autocorrelation heights, the drift between windows; every channel's band energy \
410 averaged",
411 build: |g| Representation::Periodicity {
412 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
413 window_secs: g.window.unwrap_or(DEFAULT_PERIODICITY_WINDOW_SECS),
414 curve: g.curve,
415 },
416};
417
418static MODULATION: Reading = Reading {
419 name: "modulation",
420 takes: &[Arg::Frame, Arg::Window, Arg::Bpm],
421 reads: Reads::Samples,
422 help: "how its band envelopes move per sliding window, under 150 Hz, to 2 kHz and over: \
423 the leading modulation rates from 0.5 Hz, or the slowest two of whose periods the \
424 window holds, to 20 Hz, in beats where `bpm` is given, \
425 their depth as a modulation index, each with its bounds, the level, and the \
426 spectral centroid's mean and its own swing in Hz; every channel's band energy \
427 averaged",
428 build: |g| Representation::Modulation {
429 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
430 window_secs: g.window.unwrap_or(DEFAULT_MODULATION_WINDOW_SECS),
431 bpm: g.bpm,
432 },
433};
434
435static SELF_SIMILARITY: Reading = Reading {
436 name: "self_similarity",
437 takes: &[
438 Arg::Frame,
439 Arg::Cell,
440 Arg::Smooth,
441 Arg::Window,
442 Arg::Floor,
443 Arg::Peaks,
444 Arg::Curve,
445 ],
446 reads: Reads::Samples,
447 help: "how its stretches resemble one another, by chroma and by band levels, over cells \
448 of `cell`: the repeated stretches as stripes, each pair of spans with its \
449 similarity and, by chroma, its transposition in semitones; Foote's novelty at every \
450 cell boundary and its peaks; `curve=1` adds the cells and the matrix; every \
451 channel's power per bin and band energy averaged",
452 build: |g| Representation::SelfSimilarity {
453 frame_secs: g.frame.unwrap_or(DEFAULT_FRAME_SECS),
454 cell_secs: g.cell_secs,
455 smooth_secs: g.smooth_secs,
456 window_secs: g.window.unwrap_or(DEFAULT_NOVELTY_WINDOW_SECS),
457 floor_db: g.floor_db.unwrap_or(DEFAULT_FLOOR_DB),
458 listed: g.peaks,
459 curve: g.curve,
460 },
461};
462
463static BINDINGS: Reading = Reading {
464 name: "bindings",
465 takes: &[Arg::Node],
466 reads: Reads::Structure,
467 help: "the bindings one instance resolved, each with its source",
468 build: |_| Representation::Bindings,
469};
470
471static ARGUMENTS: Reading = Reading {
472 name: "arguments",
473 takes: &[],
474 reads: Reads::Structure,
475 help: "each builtin call's named arguments as the numbers it was lowered with",
476 build: |_| Representation::Arguments,
477};
478
479pub static READINGS: [&Reading; 24] = [
481 &LINES,
482 &ATOMS,
483 &SPECTRUM,
484 &ENVELOPE,
485 &DERIVATIVE,
486 &SAMPLES,
487 &LEDGER,
488 &PITCH,
489 &FORMANTS,
490 &STEREO,
491 &BANDS,
492 &CREST,
493 &LOUDNESS,
494 &ONSETS,
495 &ALIAS,
496 &DISSONANCE,
497 &VOICE_LEADING,
498 &TONAL_DISTANCE,
499 &INHARMONICITY,
500 &PERIODICITY,
501 &MODULATION,
502 &SELF_SIMILARITY,
503 &BINDINGS,
504 &ARGUMENTS,
505];
506
507#[derive(Clone, Copy, Debug, PartialEq)]
508pub enum Representation {
509 Lines,
510 Atoms,
511 Spectrum {
512 max_peaks: usize,
513 frame_secs: Option<f64>,
514 },
515 Envelope {
516 frame_secs: Option<f64>,
517 },
518 Derivative,
519 Samples,
520 Ledger {
521 depth: usize,
522 },
523 Pitch {
524 max_notes: usize,
525 frame_secs: f64,
526 },
527 Formants {
528 max_formants: usize,
529 frame_secs: f64,
530 },
531 Stereo {
532 frame_secs: f64,
533 },
534 Bands {
536 prune: Option<Prune>,
537 },
538 Crest,
539 Loudness,
540 Onsets,
541 Alias {
542 oversample: u32,
543 },
544 Harmony {
546 reading: Harmonic,
547 frame_secs: f64,
548 floor_db: f64,
549 },
550 Periodicity {
552 frame_secs: f64,
553 window_secs: f64,
554 curve: bool,
555 },
556 Modulation {
558 frame_secs: f64,
559 window_secs: f64,
560 bpm: Option<f64>,
561 },
562 SelfSimilarity {
565 frame_secs: f64,
566 cell_secs: f64,
567 smooth_secs: f64,
568 window_secs: f64,
569 floor_db: f64,
570 listed: usize,
571 curve: bool,
572 },
573 Bindings,
574 Arguments,
575}
576
577#[derive(Clone, Copy, Debug, PartialEq, Eq)]
579pub enum Harmonic {
580 Dissonance { max_pairs: usize },
581 VoiceLeading { norm: Norm },
582 TonalDistance,
583 Inharmonicity,
584}
585
586#[derive(Clone, Debug, PartialEq)]
588pub struct Ask {
589 pub node: String,
590 pub representation: Representation,
591}
592
593#[derive(Clone, Debug, PartialEq)]
594pub enum Output {
595 Lines(Vec<Line>),
596 Atoms(Vec<Atom>),
597 Symbolic(Box<SpectralSum>),
598 Samples(Box<Buffer>),
599 Spectrum(Box<Spectrum>),
600 Envelope(Vec<EnvelopeFrame>),
601 Pitch(Vec<PitchFrame>),
602 Formants(Vec<FormantFrame>),
603 Stereo(Box<StereoImage>),
604 Bands(Box<Bands>),
605 PrunedBands(Box<PrunedBands>),
606 Crest(Box<Crest>),
607 Loudness(Box<Loudness>),
608 Onsets(Box<Onsets>),
609 Alias(Box<Alias>),
610 Ledger(Vec<LedgerEntry>),
611 Dissonance(Box<Dissonance>),
612 VoiceLeading(Box<VoiceLeading>),
613 TonalDistance(Box<TonalDistance>),
614 Inharmonicity(Box<Inharmonicity>),
615 Periodicity(Box<Periodicity>),
616 Modulation(Box<Modulation>),
617 SelfSimilarity(Box<SelfSimilarity>),
618 Bindings(Vec<Binding>),
619 Arguments(Vec<Arguments>),
620}
621
622#[derive(Clone, Debug, PartialEq)]
624pub struct Atom {
625 pub var: Var,
626 pub channel: usize,
628 pub sketch: String,
629 pub amp: C64,
630 pub turns: f64,
631 pub decay: f64,
632 pub decay_from: f64,
633 pub degree: u16,
634 pub polynomial_at: f64,
635 pub start: Option<f64>,
636 pub end: Option<f64>,
637}
638
639#[derive(Clone, Debug, PartialEq)]
642pub struct Answer {
643 pub value: Output,
644 pub source: Source,
645 pub profile: &'static str,
646 pub rate: Option<u32>,
647 pub dropped: Vec<sva_formula::Line>,
649 pub tail_db: Option<f64>,
650}
651
652impl Answer {
653 pub fn whole(
654 value: Output,
655 source: Source,
656 profile: &'static str,
657 rate: Option<u32>,
658 ) -> Answer {
659 Answer {
660 value,
661 source,
662 profile,
663 rate,
664 dropped: Vec::new(),
665 tail_db: None,
666 }
667 }
668}
669
670impl Representation {
671 pub fn reading(self) -> &'static Reading {
673 match self {
674 Representation::Lines => &LINES,
675 Representation::Atoms => &ATOMS,
676 Representation::Spectrum { .. } => &SPECTRUM,
677 Representation::Envelope { .. } => &ENVELOPE,
678 Representation::Derivative => &DERIVATIVE,
679 Representation::Samples => &SAMPLES,
680 Representation::Ledger { .. } => &LEDGER,
681 Representation::Pitch { .. } => &PITCH,
682 Representation::Formants { .. } => &FORMANTS,
683 Representation::Stereo { .. } => &STEREO,
684 Representation::Bands { .. } => &BANDS,
685 Representation::Crest => &CREST,
686 Representation::Loudness => &LOUDNESS,
687 Representation::Onsets => &ONSETS,
688 Representation::Alias { .. } => &ALIAS,
689 Representation::Harmony { reading, .. } => match reading {
690 Harmonic::Dissonance { .. } => &DISSONANCE,
691 Harmonic::VoiceLeading { .. } => &VOICE_LEADING,
692 Harmonic::TonalDistance => &TONAL_DISTANCE,
693 Harmonic::Inharmonicity => &INHARMONICITY,
694 },
695 Representation::Periodicity { .. } => &PERIODICITY,
696 Representation::Modulation { .. } => &MODULATION,
697 Representation::SelfSimilarity { .. } => &SELF_SIMILARITY,
698 Representation::Bindings => &BINDINGS,
699 Representation::Arguments => &ARGUMENTS,
700 }
701 }
702
703 pub fn name(self) -> &'static str {
704 self.reading().name
705 }
706
707 pub fn consumes(self, closed: bool) -> Consumes {
708 self.reading().reads.consumes(closed)
709 }
710
711 pub fn off_samples(self, sampled: bool) -> bool {
713 self.reading().reads.off_samples(sampled)
714 }
715
716 pub fn from_name(name: &str) -> Option<Representation> {
717 READINGS
718 .iter()
719 .find(|row| row.name == name)
720 .map(|row| row.representation(&Given::default()))
721 }
722}