Skip to main content

sound_to_patch/report/
layers.rs

1//! The `layers.json` report: the `stem` group and the `pitch` and
2//! `envelope` layer groups, with a convention for every number.
3
4use serde::Serialize;
5
6use crate::audio::{
7    BAND_EDGE_CONVENTION, Band, HARD_LIMITED_CONVENTION, STEREO_CONVENTION, Stem, StereoScreen,
8    TEMPO_CONVENTION, Tempo,
9};
10use crate::confidence::{Confidence, Estimate, Level, NoSpread, Spread};
11use crate::dsp::{
12    BAND_CONVENTION, Descriptors, FLATNESS_CONVENTION, LEVEL_CONVENTION, SHAPE_CONVENTION, Stats,
13};
14use crate::estimate::envelope::{
15    ENVELOPE_CONVENTION, EnvelopeFlags, EnvelopeLayer, KNEE_CONVENTION, LEVEL_SPREAD_CONVENTION,
16    NoteEnvelope, PUMP_CONVENTION, Pump, TAIL_CONVENTION, TailVotes, lock_name,
17};
18use crate::estimate::pitch::{
19    DROP_CONVENTION, DropFit, GLIDE_CONVENTION, GlideFit, LINE_CONVENTION, PitchFlags, PitchLayer,
20    Rated, VIBRATO_CONVENTION, VibratoFit,
21};
22use crate::estimate::voices::{JOIN_CONVENTION, Join, VOICES_CONVENTION, VoicesLayer};
23use crate::notes::{FinalNotes, TICKS_PER_BEAT, TranscriptionRecord};
24use crate::preset::edits::Edit;
25use crate::preset::mapper::pitch_envelope::group_of;
26use crate::preset::mapper::{Mapped, Unrepresentable};
27use crate::report::NOTES_FILE;
28use crate::synth::Synth;
29use crate::units::{
30    Bpm, Cents, Correlation, Decibels, Degrees, Hz, Normalized, Ratio, Seconds, Semitones,
31};
32
33/// The schema name and version that `layers.json` declares.
34pub const SCHEMA: &str = "layers/1";
35
36/// The convention of the statistics over active frames.
37const STATISTICS_CONVENTION: &str = "median and interquartile range over active frames, \
38quantiles by linear interpolation at q (n - 1) between the sorted values; a frame is active \
39when its RMS lies within 40 dB of the loudest frame";
40
41/// The convention of the final note list.
42const NOTES_CONVENTION: &str = "the notes of notes.mid, one track on channel 1 at the used \
43tempo, 480 ticks per quarter note; a time becomes round(seconds * bpm / 60 * 480) ticks, and \
44onset_s and end_s are the written ticks in seconds; list_onset_s and list_end_s are the note in \
45the cleaned note list, or the earliest onset and latest end of the notes a pitch drop merged; key \
46is the nearest key of the pitch, of f_end for a pitch drop and of the mean pitch for a note a \
47vibrato covers over half its length, when within a semitone; only glides, drops and vibratos above \
48low confidence in a pitch group above low confidence count; a drop merges the notes that start \
49from 30 ms before its onset until five time constants after it, each within 30 ms of the end of \
50the one before and on a key from f_end to the drop's start pitch; end_rule names what set the end: \
51knee is the note-off knee; no_knee is 50 ms before the list end for a note the envelope layer read \
52without a knee; unread is the same cut for a note the envelope layer skipped, with no pad end; a \
53line is mono when the voices layer reads at most one voice at a time or counts none, unless the \
54notes step ran with --profile pad; pad is, for a note of a polyphonic line, where its amp proxy falls 20 dB below its \
55peak plus its sustain level, first after its knee or its peak, or, for a note the envelope layer \
56skipped, where its full-band level falls 20 dB below the median of the last 30 % of its held part, \
57from the first frame within 1 dB of its peak to 25 ms before its list end, first after its peak, \
58both searched no later than the list end; minimum is the \
59decoder's minimum length at the tempo by the BPM rule, when a knee, no-knee or pad cut would go \
60below it, and a note the list gives shorter keeps its length; next_note is the next note-on of a \
61mono line, which the minimum does not hold back; glide is 5 ms after the next note-on, rounded up \
62to whole ticks, at a glide join of a mono line, whose later note starts at the glide's start or at \
63its own onset, whichever comes first; no end lies after the list end, except at a glide join; of \
64the notes of a mono line that start on one tick only one stays, and dropped counts the others: a \
65note without the octave flag before one with it, then the longer, then the lower key; level_db is \
66the note's full-band level, as level_spread_db defines it, the highest of the notes a drop merged, \
67or, when that span holds no frame, the same level from its start to its end before the joins and \
68cuts of a mono line, on a mono line no later than the next later onset; \
69velocity is level when level_spread_db exceeds 3 dB: \
70the loudest note gets 127 and a note d dB under it round(127 * 10^(-d / 40)), at least 1, and a note \
71without a level 100; else constant, 100 for every note; octave_uncertain is the transcriber's \
72octave flag, null when none ran; bend_events counts the pitch bends: on a mono line, a note whose \
73f0 contour leaves its key by 50 cents or more, of one sign, over adjacent counted frames that span \
74the tracker's reach of the note, a quarter frame plus one period, outside every counted glide, drop \
75and vibrato span, gets a bend at each counted contour frame, clamped to 2 semitones, at the frame's audio time, its \
76track time less the track's lag of a quarter frame less half a period; a frame counts only when \
77what it reads, from a quarter frame before it to one period after it, lies inside the note's list \
78onset and end, each one model frame of 11.6 ms inward, and outside every other note of the list; \
79the bend is centred again at the note's end or the next note-on, whichever comes \
80first; a host must set a bend range of 2 semitones";
81
82/// Renders the report of a stem as pretty-printed JSON with a final
83/// newline. The same stem always gives the same bytes.
84///
85/// `source` is the stem's file name without its folder and the SHA-256 of
86/// the file's bytes in lower-case hex, `tempo` the
87/// given and refined tempo, `transcription` the record of the
88/// transcriber that gave the notes, or `None` when none ran, and
89/// `notes` the final note list that `notes.mid` holds, or `None` without
90/// notes, and `layers` the pitch, envelope and voices layers with the
91/// mapper's output for them, which each group splits by its name.
92///
93/// # Errors
94///
95/// Returns a [`serde_json::Error`] only if a value cannot be written, which
96/// the finite unit types never cause.
97pub fn layers_json(
98    stem: &Stem,
99    descriptors: &Descriptors,
100    synth: Synth,
101    (source, source_sha256): (&str, &str),
102    tempo: &Tempo,
103    (transcription, notes): (Option<&TranscriptionRecord>, Option<&FinalNotes>),
104    ((pitch, envelope, voices), mapped): ((&PitchLayer, &EnvelopeLayer, &VoicesLayer), &Mapped),
105) -> Result<String, serde_json::Error> {
106    let report = Layers {
107        schema: SCHEMA,
108        source,
109        source_sha256,
110        synth,
111        stem: StemGroup::new(stem, descriptors, tempo),
112        transcription,
113        notes: notes.map(NotesGroup::new),
114        pitch: PitchGroup::new(pitch, Mapping::of(mapped, "pitch")),
115        voices: VoicesGroup::new(voices, Mapping::of(mapped, "voices")),
116        envelope: EnvelopeGroup::new(envelope, Mapping::of(mapped, "envelope")),
117        conventions: Conventions::new(descriptors.frame_len),
118    };
119    let mut text = serde_json::to_string_pretty(&report)?;
120    text.push('\n');
121    Ok(text)
122}
123
124#[derive(Serialize)]
125struct Layers<'a> {
126    schema: &'static str,
127    /// The file name of the stem, without its folder.
128    source: &'a str,
129    /// The SHA-256 of the stem file's bytes, which tells apart two stems
130    /// of one file name.
131    source_sha256: &'a str,
132    synth: Synth,
133    stem: StemGroup<'a>,
134    /// The transcriber's model and settings, `null` when none ran.
135    transcription: Option<&'a TranscriptionRecord>,
136    /// The final note list of `notes.mid`, `null` without notes.
137    notes: Option<NotesGroup>,
138    pitch: PitchGroup<'a>,
139    voices: VoicesGroup<'a>,
140    envelope: EnvelopeGroup<'a>,
141    conventions: Conventions,
142}
143
144/// The final note list: the notes of `notes.mid` and how each was written.
145#[derive(Serialize)]
146struct NotesGroup {
147    file: &'static str,
148    ticks_per_beat: u16,
149    velocity: &'static str,
150    bend_events: usize,
151    dropped: usize,
152    list: Vec<FinalRecord>,
153}
154
155#[derive(Serialize)]
156struct FinalRecord {
157    onset_s: Seconds,
158    end_s: Seconds,
159    list_onset_s: Seconds,
160    list_end_s: Seconds,
161    key: u8,
162    velocity: u8,
163    end_rule: &'static str,
164    level_db: Option<Decibels>,
165    octave_uncertain: Option<bool>,
166}
167
168impl NotesGroup {
169    fn new(notes: &FinalNotes) -> Self {
170        Self {
171            file: NOTES_FILE,
172            ticks_per_beat: TICKS_PER_BEAT,
173            velocity: if notes.level_velocity {
174                "level"
175            } else {
176                "constant"
177            },
178            bend_events: notes.bends.len(),
179            dropped: notes.dropped,
180            list: notes
181                .notes
182                .iter()
183                .map(|n| FinalRecord {
184                    onset_s: notes.seconds(n.start),
185                    end_s: notes.seconds(n.end),
186                    list_onset_s: n.list_onset,
187                    list_end_s: n.list_end,
188                    key: n.key,
189                    velocity: n.velocity.get(),
190                    end_rule: n.end_rule.name(),
191                    level_db: n.level,
192                    octave_uncertain: n.octave_uncertain,
193                })
194                .collect(),
195        }
196    }
197}
198
199/// The envelope layer as a layer group, with the mapper's edits for it.
200#[derive(Serialize)]
201struct EnvelopeGroup<'a> {
202    values: Option<EnvelopeValues<'a>>,
203    confidence: &'a Confidence<NoSpread>,
204    flags: EnvelopeFlags,
205    needs_render: bool,
206    #[serde(flatten)]
207    mapping: Mapping<'a>,
208}
209
210/// The mapper's output for one group: the edits whose reason names it and
211/// the misses whose field lies in it.
212#[derive(Serialize)]
213struct Mapping<'a> {
214    suggested_edits: Vec<EditRecord<'a>>,
215    unrepresentable: Vec<Miss<'a>>,
216}
217
218/// An edit-list record with what the mapper knows of it: an interim law
219/// and the confidence, which a route cannot recover from the registry.
220#[derive(Serialize)]
221struct EditRecord<'a> {
222    #[serde(flatten)]
223    edit: &'a Edit,
224    uncalibrated: bool,
225    confidence: Level,
226}
227
228/// An `unrepresentable` entry with its residual.
229#[derive(Serialize)]
230struct Miss<'a> {
231    #[serde(flatten)]
232    entry: &'a Unrepresentable,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    residual: Option<f64>,
235}
236
237impl<'a> Mapping<'a> {
238    fn of(mapped: &'a Mapped, group: &str) -> Self {
239        Self {
240            suggested_edits: mapped
241                .edits
242                .iter()
243                .filter(|edit| edit.edit.reason.as_deref() == Some(group))
244                .map(|edit| EditRecord {
245                    edit: &edit.edit,
246                    uncalibrated: edit.uncalibrated,
247                    confidence: edit.confidence,
248                })
249                .collect(),
250            unrepresentable: mapped
251                .unrepresentable
252                .iter()
253                .filter(|entry| group_of(&entry.field) == Some(group))
254                .map(|entry| Miss {
255                    entry,
256                    residual: entry.residual(),
257                })
258                .collect(),
259        }
260    }
261}
262
263#[derive(Serialize)]
264struct EnvelopeValues<'a> {
265    attack_s: Statistic<'a, Seconds>,
266    hold_s: Statistic<'a, Seconds>,
267    decay_s: Statistic<'a, Seconds>,
268    sustain_db: Statistic<'a, Decibels>,
269    note_off_offset_s: Statistic<'a, Seconds>,
270    release_s: Statistic<'a, Seconds>,
271    tail_rt60_s: Statistic<'a, Seconds>,
272    tail_weight: Statistic<'a, Ratio>,
273    release_confidence: Option<Level>,
274    tail_votes: TailVotes,
275    tail_vote_count: usize,
276    tail_is_reverb: bool,
277    level_spread_db: Value<'a, Decibels, NoSpread>,
278    ripple_db: Option<Decibels>,
279    fit_residual_db: Option<Decibels>,
280    n_notes_used: usize,
281    overlap_fraction: f64,
282    pump_depth_db: Option<Decibels>,
283    pump_recovery_s: Option<Seconds>,
284    pump_tau_ms: Option<f64>,
285    pump_locked_to: Option<&'static str>,
286    pump_beats: usize,
287    pump_bpm: Option<Bpm>,
288    notes: Vec<NoteRecord>,
289}
290
291#[derive(Serialize)]
292struct NoteRecord {
293    onset_s: Seconds,
294    peak_db: Decibels,
295    attack_s: Option<Seconds>,
296    attack_upper_bound: bool,
297    knee_s: Option<Seconds>,
298    pooled: bool,
299}
300
301impl<'a> EnvelopeGroup<'a> {
302    fn new(layer: &'a EnvelopeLayer, mapping: Mapping<'a>) -> Self {
303        let pump = layer.pump.as_ref();
304        Self {
305            values: layer.confidence.is_estimate().then(|| EnvelopeValues {
306                attack_s: Statistic::of(&layer.attack),
307                hold_s: Statistic::of(&layer.hold),
308                decay_s: Statistic::of(&layer.decay),
309                sustain_db: Statistic::of(&layer.sustain),
310                note_off_offset_s: Statistic::of(&layer.note_off_offset),
311                release_s: Statistic::of(&layer.release),
312                tail_rt60_s: Statistic::of(&layer.tail_rt60),
313                tail_weight: Statistic::of(&layer.tail_weight),
314                release_confidence: layer.release_confidence,
315                tail_votes: layer.tail_votes,
316                tail_vote_count: layer.tail_votes.count(),
317                tail_is_reverb: false,
318                level_spread_db: Value::of(&layer.level_spread),
319                ripple_db: layer.ripple,
320                fit_residual_db: layer.residual,
321                n_notes_used: layer.pooled.iter().filter(|&&p| p).count(),
322                overlap_fraction: layer.overlap_fraction,
323                pump_depth_db: pump.map(|p| p.depth),
324                pump_recovery_s: pump.and_then(|p| p.back),
325                pump_tau_ms: pump.and_then(|p| p.tau).map(|t| 1000.0 * t.get()),
326                pump_locked_to: pump.map(|p| lock_name(p.lock)),
327                pump_beats: pump.map_or(0, |p| p.beats),
328                pump_bpm: pump.map(Pump::bpm),
329                notes: layer
330                    .notes
331                    .iter()
332                    .zip(&layer.pooled)
333                    .map(|(n, &pooled)| NoteRecord::of(n, pooled))
334                    .collect(),
335            }),
336            confidence: &layer.confidence,
337            flags: layer.flags,
338            needs_render: false,
339            mapping,
340        }
341    }
342}
343
344impl NoteRecord {
345    fn of(note: &NoteEnvelope, pooled: bool) -> Self {
346        Self {
347            onset_s: note.onset,
348            peak_db: note.peak_db,
349            attack_s: note.attack,
350            attack_upper_bound: note.attack_upper_bound,
351            knee_s: note.knee,
352            pooled,
353        }
354    }
355}
356
357/// The voices layer as a layer group, with the mapper's edits for it.
358#[derive(Serialize)]
359struct VoicesGroup<'a> {
360    values: Option<VoicesValues<'a>>,
361    confidence: &'a Confidence<NoSpread>,
362    flags: VoicesFlags,
363    needs_render: bool,
364    #[serde(flatten)]
365    mapping: Mapping<'a>,
366}
367
368/// The voices layer reads no flags yet.
369#[derive(Serialize)]
370struct VoicesFlags {}
371
372impl<'a> VoicesGroup<'a> {
373    fn new(layer: &'a VoicesLayer, mapping: Mapping<'a>) -> Self {
374        Self {
375            values: layer.confidence.is_estimate().then(|| VoicesValues {
376                mono: layer.mono,
377                max_polyphony: layer.max_polyphony,
378                mean_polyphony: layer.mean_polyphony,
379                joins: layer.joins.iter().map(JoinRecord::of).collect(),
380            }),
381            confidence: &layer.confidence,
382            flags: VoicesFlags {},
383            needs_render: false,
384            mapping,
385        }
386    }
387}
388
389#[derive(Serialize)]
390struct VoicesValues<'a> {
391    mono: bool,
392    max_polyphony: usize,
393    mean_polyphony: f64,
394    joins: Vec<JoinRecord<'a>>,
395}
396
397#[derive(Serialize)]
398struct JoinRecord<'a> {
399    time_s: Seconds,
400    gap_ms: f64,
401    dip_db: Option<Decibels>,
402    centroid_change: Option<f64>,
403    legato: Option<bool>,
404    confidence: &'a Confidence<NoSpread>,
405}
406
407impl<'a> JoinRecord<'a> {
408    fn of(join: &'a Join) -> Self {
409        Self {
410            time_s: join.time,
411            gap_ms: 1000.0 * join.gap,
412            dip_db: join.dip,
413            centroid_change: join.centroid_change,
414            legato: join.legato,
415            confidence: &join.confidence,
416        }
417    }
418}
419
420/// The pitch layer as a layer group, with the mapper's edits for it.
421#[derive(Serialize)]
422struct PitchGroup<'a> {
423    values: Option<PitchValues<'a>>,
424    confidence: &'a Confidence<NoSpread>,
425    flags: PitchFlags,
426    needs_render: bool,
427    #[serde(flatten)]
428    mapping: Mapping<'a>,
429}
430
431impl<'a> PitchGroup<'a> {
432    fn new(layer: &'a PitchLayer, mapping: Mapping<'a>) -> Self {
433        Self {
434            values: layer.confidence.is_estimate().then(|| PitchValues {
435                glides: layer.glides.iter().map(GlideRecord::of).collect(),
436                drops: layer.drops.iter().map(DropRecord::of).collect(),
437                vibrato: layer.vibrato.iter().map(VibratoRecord::of).collect(),
438            }),
439            confidence: &layer.confidence,
440            flags: layer.flags,
441            needs_render: false,
442            mapping,
443        }
444    }
445}
446
447#[derive(Serialize)]
448struct PitchValues<'a> {
449    glides: Vec<GlideRecord<'a>>,
450    drops: Vec<DropRecord<'a>>,
451    vibrato: Vec<VibratoRecord<'a>>,
452}
453
454#[derive(Serialize)]
455struct GlideRecord<'a> {
456    start_s: Seconds,
457    interval_st: Semitones,
458    time_s: Seconds,
459    curve: f64,
460    from_silence: bool,
461    confidence: &'a Confidence<NoSpread>,
462}
463
464impl<'a> GlideRecord<'a> {
465    fn of(glide: &'a Rated<GlideFit>) -> Self {
466        let g = &glide.value;
467        Self {
468            start_s: g.start,
469            interval_st: g.interval,
470            time_s: g.time,
471            curve: g.curve,
472            from_silence: g.from_silence,
473            confidence: &glide.confidence,
474        }
475    }
476}
477
478#[derive(Serialize)]
479struct DropRecord<'a> {
480    onset_s: Seconds,
481    f_end_hz: Hz,
482    df_st: Option<Semitones>,
483    df_hz: f64,
484    tau_s: Seconds,
485    residual: f64,
486    confidence: &'a Confidence<NoSpread>,
487}
488
489impl<'a> DropRecord<'a> {
490    fn of(drop: &'a Rated<DropFit>) -> Self {
491        let d = &drop.value;
492        Self {
493            onset_s: d.onset,
494            f_end_hz: d.f_end,
495            df_st: d.df(),
496            df_hz: d.df_hz,
497            tau_s: d.tau,
498            residual: d.residual,
499            confidence: &drop.confidence,
500        }
501    }
502}
503
504#[derive(Serialize)]
505struct VibratoRecord<'a> {
506    start_s: Seconds,
507    end_s: Seconds,
508    rate_hz: Hz,
509    extent_ct: Cents,
510    extent_raw_ct: Cents,
511    extent_factor: f64,
512    share: f64,
513    confidence: &'a Confidence<NoSpread>,
514}
515
516impl<'a> VibratoRecord<'a> {
517    fn of(vibrato: &'a Rated<VibratoFit>) -> Self {
518        let v = &vibrato.value;
519        Self {
520            start_s: v.start,
521            end_s: v.end,
522            rate_hz: v.rate,
523            extent_ct: v.extent(),
524            extent_raw_ct: v.extent_raw,
525            extent_factor: v.factor,
526            share: v.share,
527            confidence: &vibrato.confidence,
528        }
529    }
530}
531
532#[derive(Serialize)]
533struct StemGroup<'a> {
534    bpm: Bpm,
535    bpm_given: Bpm,
536    bpm_refined: Value<'a, Bpm, NoSpread>,
537    duration_s: Seconds,
538    sample_rate_hz: u32,
539    band_edge_hz: Value<'a, Hz, Hz>,
540    hard_limited: bool,
541    peak_dbfs: Value<'a, Decibels, Decibels>,
542    rms_dbfs: Value<'a, Decibels, Decibels>,
543    crest_db: Value<'a, Decibels, Decibels>,
544    centroid_hz: Statistic<'a, Hz>,
545    /// `None` without a note list.
546    centroid_over_f0: Option<Statistic<'a, Ratio>>,
547    spread_hz: Statistic<'a, Hz>,
548    rolloff85_hz: Statistic<'a, Hz>,
549    rolloff95_hz: Statistic<'a, Hz>,
550    flatness: Statistic<'a, Normalized>,
551    band_share: BandShares<'a>,
552    stereo: Stereo<'a>,
553}
554
555impl<'a> StemGroup<'a> {
556    fn new(stem: &'a Stem, descriptors: &'a Descriptors, tempo: &'a Tempo) -> Self {
557        Self {
558            bpm: stem.grid().bpm(),
559            bpm_given: tempo.given(),
560            bpm_refined: Value::of(tempo.refined()),
561            duration_s: stem.duration(),
562            sample_rate_hz: stem.sample_rate().get(),
563            band_edge_hz: Value::of(stem.band_edge()),
564            hard_limited: stem.hard_limited(),
565            peak_dbfs: Value::of(&descriptors.peak_dbfs),
566            rms_dbfs: Value::of(&descriptors.rms_dbfs),
567            crest_db: Value::of(&descriptors.crest_db),
568            centroid_hz: Statistic::of(&descriptors.centroid),
569            centroid_over_f0: stem
570                .median_f0()
571                .map(|_| Statistic::of(&descriptors.centroid_over_f0)),
572            spread_hz: Statistic::of(&descriptors.spread),
573            rolloff85_hz: Statistic::of(&descriptors.rolloff85),
574            rolloff95_hz: Statistic::of(&descriptors.rolloff95),
575            flatness: Statistic::of(&descriptors.flatness),
576            band_share: BandShares {
577                value: descriptors.band_share.value().map(|shares| {
578                    shares
579                        .iter()
580                        .map(|share| BandPart {
581                            lo_hz: share.lo,
582                            hi_hz: share.hi,
583                            share: share.share,
584                        })
585                        .collect()
586                }),
587                confidence: descriptors.band_share.confidence(),
588            },
589            stereo: Stereo::of(stem.stereo()),
590        }
591    }
592}
593
594/// One value and its confidence; `value` is `null` for no estimate.
595#[derive(Serialize)]
596#[serde(bound = "V: Serialize")]
597struct Value<'a, V, S: Spread> {
598    value: Option<&'a V>,
599    confidence: &'a Confidence<S>,
600}
601
602impl<'a, V, S: Spread> Value<'a, V, S> {
603    fn of(estimate: &'a Estimate<V, S>) -> Self {
604        Self {
605            value: estimate.value(),
606            confidence: estimate.confidence(),
607        }
608    }
609}
610
611/// The median and IQR over active frames and their confidence; both are
612/// `null` for no estimate.
613#[derive(Serialize)]
614struct Statistic<'a, T: Spread> {
615    median: Option<T>,
616    iqr: Option<T>,
617    confidence: &'a Confidence<T>,
618}
619
620impl<'a, T: Spread> Statistic<'a, T> {
621    fn of(estimate: &'a Estimate<Stats<T>, T>) -> Self {
622        Self {
623            median: estimate.value().map(Stats::median),
624            iqr: estimate.value().map(Stats::iqr),
625            confidence: estimate.confidence(),
626        }
627    }
628}
629
630#[derive(Serialize)]
631struct BandShares<'a> {
632    value: Option<Vec<BandPart>>,
633    confidence: &'a Confidence<Normalized>,
634}
635
636#[derive(Serialize)]
637struct BandPart {
638    lo_hz: Hz,
639    hi_hz: Hz,
640    share: Normalized,
641}
642
643#[derive(Serialize)]
644struct Stereo<'a> {
645    full: StereoBand,
646    sub: StereoBand,
647    low_mid: StereoBand,
648    high: StereoBand,
649    confidence: &'a Confidence<NoSpread>,
650}
651
652impl<'a> Stereo<'a> {
653    fn of(screen: &'a StereoScreen) -> Self {
654        let band = |band| {
655            let stats = screen.band(band);
656            StereoBand {
657                rho: stats.rho,
658                sm_db: stats.sm_db,
659                ipd_deg: stats.ipd_deg,
660            }
661        };
662        Self {
663            full: band(Band::Full),
664            sub: band(Band::Sub),
665            low_mid: band(Band::LowMid),
666            high: band(Band::High),
667            confidence: screen.confidence(),
668        }
669    }
670}
671
672#[derive(Serialize)]
673struct StereoBand {
674    rho: Option<Correlation>,
675    sm_db: Option<Decibels>,
676    ipd_deg: Option<Degrees>,
677}
678
679/// The convention of every number in the `stem` group, keyed by its field.
680#[derive(Serialize)]
681struct Conventions {
682    frame_size: usize,
683    statistics: &'static str,
684    spectral: &'static str,
685    bpm: &'static str,
686    bpm_given: &'static str,
687    bpm_refined: &'static str,
688    duration_s: &'static str,
689    sample_rate_hz: &'static str,
690    band_edge_hz: &'static str,
691    hard_limited: &'static str,
692    peak_dbfs: &'static str,
693    rms_dbfs: &'static str,
694    crest_db: &'static str,
695    centroid_hz: &'static str,
696    centroid_over_f0: &'static str,
697    spread_hz: &'static str,
698    rolloff85_hz: &'static str,
699    rolloff95_hz: &'static str,
700    flatness: &'static str,
701    band_share: &'static str,
702    stereo: &'static str,
703    confidence: &'static str,
704    pitch: &'static str,
705    voices: &'static str,
706    joins: &'static str,
707    glides: &'static str,
708    drops: &'static str,
709    sub_band_line: &'static str,
710    vibrato: &'static str,
711    envelope: &'static str,
712    note_off: &'static str,
713    tail: &'static str,
714    level_spread_db: &'static str,
715    pump: &'static str,
716    notes: &'static str,
717}
718
719impl Conventions {
720    fn new(frame_size: usize) -> Self {
721        Self {
722            frame_size,
723            statistics: STATISTICS_CONVENTION,
724            spectral: "magnitude weights for shape, power for band shares; periodic Hann frames \
725of frame_size samples, hop frame_size / 4, centred and zero-padded; at 44.1 kHz frame_size is \
7268192 when the note list has a median f0 below 100 Hz, else 4096, and 4096 without a note list; at \
727another rate it is the power of two nearest, in log, to that length times the rate over 44100 Hz, \
728the same at 32 and 48 kHz and twice it at 88.2 and 96 kHz",
729            bpm: "the tempo of the beat grid and of every later step: bpm_refined when its \
730confidence is high, else bpm_given",
731            bpm_given: "given by --bpm",
732            bpm_refined: TEMPO_CONVENTION,
733            duration_s: "decoded frames over the sample rate",
734            sample_rate_hz: "the native rate of the file; no resampling",
735            band_edge_hz: BAND_EDGE_CONVENTION,
736            hard_limited: HARD_LIMITED_CONVENTION,
737            peak_dbfs: LEVEL_CONVENTION,
738            rms_dbfs: LEVEL_CONVENTION,
739            crest_db: LEVEL_CONVENTION,
740            centroid_hz: SHAPE_CONVENTION,
741            centroid_over_f0: "the centroid of each active frame over the median f0 of the note \
742list, MIDI pitch to hertz with A4 = 440 Hz; null without a note list",
743            spread_hz: SHAPE_CONVENTION,
744            rolloff85_hz: SHAPE_CONVENTION,
745            rolloff95_hz: SHAPE_CONVENTION,
746            flatness: FLATNESS_CONVENTION,
747            band_share: BAND_CONVENTION,
748            stereo: STEREO_CONVENTION,
749            confidence: "level, reasons and spread of each estimate; the spread of a statistic \
750is its interquartile range and that of the band edge one bin width, in the unit of the value; \
751estimate false means no value, with the reasons why",
752            voices: VOICES_CONVENTION,
753            joins: JOIN_CONVENTION,
754            pitch: "the pitch layer of a monophonic line, read from the f0 track, the note list \
755and the sub-band line; values is null when the line plays chords or has no notes; every time is \
756audio time from the start of the stem",
757            glides: GLIDE_CONVENTION,
758            drops: DROP_CONVENTION,
759            sub_band_line: LINE_CONVENTION,
760            vibrato: VIBRATO_CONVENTION,
761            envelope: ENVELOPE_CONVENTION,
762            note_off: KNEE_CONVENTION,
763            tail: TAIL_CONVENTION,
764            level_spread_db: LEVEL_SPREAD_CONVENTION,
765            pump: PUMP_CONVENTION,
766            notes: NOTES_CONVENTION,
767        }
768    }
769}