Skip to main content

sound_to_patch/render/
spread.rs

1//! The repeat-render spread: the noise floor that every later comparison
2//! cites.
3//!
4//! Renders of one preset differ a little. On one Vital instance each
5//! render differs from the one before, and a fresh instance renders
6//! bit-identically to any other fresh instance: an ignored Vital test pins
7//! both. So [`draw`] renders the [`SPREAD_RUNS`] draws of a spread in
8//! sequence on one instance and refuses a set with a repeated render.
9//!
10//! For each output the spread is the 95th percentile of the distances over
11//! every pair of renders, 190 pairs for 20 renders, in the output's own
12//! unit. The distance of a scalar output is the absolute difference.
13//!
14//! A clip compares with the renders by the median of its distances from
15//! them. Its tolerance comes from the same median taken for each render
16//! against the other renders, so the clip and the renders are measured
17//! alike: the tolerance is the median at rank [`tolerance_rank`], which a
18//! clip of the same sound exceeds 1 time in 21 for 20 renders. A floor
19//! keeps an output
20//! that every render repeats from turning the comparison into an equality
21//! test.
22//!
23//! A recording made elsewhere compares through [`compare_fitted`]: one
24//! gain matches its level to the renders, and the verdict covers the
25//! outputs of the sound's shape, [`ClipOutput::SHAPE`].
26//!
27//! The repository keeps no spread of record. The reference recordings page
28//! holds the metrics of each recording.
29
30use std::collections::{BTreeMap, HashSet};
31use std::fmt;
32use std::num::NonZeroUsize;
33
34use serde::Serialize;
35use serde_json::Value;
36use sha2::{Digest, Sha256};
37
38use super::recipe::Recipe;
39use super::session::{BoxedRenderer, RESTART_AFTER, RenderOutcome, RenderSession};
40use super::{HostIdentity, Oversampling, RenderError, RenderJob, Rendered};
41use crate::audio::{Audio, AudioError, SampleRate};
42use crate::cast;
43use crate::dsp::{Summary, quantile};
44use crate::units::{Bpm, Decibels};
45
46/// How many renders a spread takes.
47pub const SPREAD_RUNS: usize = 20;
48/// The fewest renders a spread takes: fewer leave no leave-one-out median
49/// of two distances.
50pub const MIN_RUNS: usize = 3;
51/// The quantile of the pairwise distances.
52pub const SPREAD_QUANTILE: f64 = 0.95;
53/// The share of clips of the same sound, in percent, that the tolerance
54/// covers.
55pub const COVERAGE_PERCENT: usize = 95;
56/// The name of the spread statistic, as the spread file records it.
57pub const STATISTIC: &str = "p95_pairwise_distance";
58/// The name of the comparison tolerance, as the spread file records it.
59pub const TOLERANCE: &str = "conformal_95_leave_one_out_median";
60/// A clip starts at its first sample within this many dB of its peak, so
61/// a lead-in of silence or latency does not count.
62pub const ONSET_RANGE_DB: Decibels = Decibels::from_const(40.0);
63/// The envelope frames per second: one frame is 10 ms.
64pub const ENVELOPE_FRAMES_PER_S: u32 = 100;
65/// The envelope distance counts the frames within this many dB of the
66/// loudest frame of either clip.
67pub const ENVELOPE_RANGE_DB: Decibels = Decibels::from_const(40.0);
68/// The floor of a tolerance in dB: a level difference below it counts as
69/// none.
70pub const LEVEL_FLOOR_DB: f64 = 0.01;
71
72/// The level of a silent frame, in dB: far below any level that counts.
73const SILENT_DB: f64 = -400.0;
74
75/// A spread that cannot be measured or compared.
76#[derive(Debug)]
77pub enum SpreadError {
78    /// A render failed on both tries.
79    Render(String),
80    /// A spread asked for too few or too many renders.
81    Runs(usize),
82    /// Some renders repeat others, so they are not independent draws.
83    Repeated {
84        /// How many renders differ.
85        distinct: usize,
86        /// How many renders came back.
87        renders: usize,
88    },
89    /// A render or the clip is silent.
90    Silent(String),
91    /// Two clips have different sample rates.
92    SampleRate {
93        /// The clip's rate.
94        clip: SampleRate,
95        /// The renders' rate.
96        renders: SampleRate,
97    },
98    /// A number of the layer report has a field name with no known unit.
99    UnknownUnit(String),
100    /// A comparison found no spread row for a clip output.
101    MissingRow(&'static str),
102    /// A recording could not be scaled by its fitted gain.
103    Audio(AudioError),
104}
105
106impl fmt::Display for SpreadError {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::Render(cause) => write!(f, "a render of the spread failed: {cause}"),
110            Self::Runs(runs) => write!(
111                f,
112                "a spread takes {MIN_RUNS} to {RESTART_AFTER} renders, not {runs}: fewer leave no leave-one-out median, and more pass the restart, whose fresh instance repeats the first draw. Give a count in that range"
113            ),
114            Self::Repeated { distinct, renders } => write!(
115                f,
116                "only {distinct} of {renders} renders differ, so the spread would count repeats as draws. Render the draws in sequence on one plug-in instance, with no restart between them"
117            ),
118            Self::Silent(what) => {
119                write!(f, "{what} is silent: no sample lies above the tail floor")
120            }
121            Self::SampleRate { clip, renders } => write!(
122                f,
123                "the clip runs at {clip} and the renders at {renders}. Render at the clip's sample rate"
124            ),
125            Self::UnknownUnit(path) => write!(
126                f,
127                "the layer report number {path} has no known unit. Name the field with a unit suffix, or add it to the unitless fields of render::spread"
128            ),
129            Self::Audio(error) => write!(f, "the recording cannot take its fitted gain: {error}"),
130            Self::MissingRow(output) => write!(
131                f,
132                "the spread has no row for {output}, so the comparison would pass without it. Compare with the rows of clip_rows"
133            ),
134        }
135    }
136}
137
138impl std::error::Error for SpreadError {}
139
140/// Renders `job` `count` times in sequence on one renderer that `spawn`
141/// starts.
142///
143/// A fresh instance renders the same clip as any other fresh one, and each
144/// later render on one instance differs. So the draws come from one
145/// instance, and the set fails [`distinct`] when a render repeats. A retry
146/// after a crash starts a fresh instance, whose first render repeats the
147/// first draw, so it fails too. For the same reason `count` stops at
148/// [`RESTART_AFTER`], where the session restarts its instance.
149///
150/// # Errors
151///
152/// Returns [`SpreadError::Runs`] for a `count` below [`MIN_RUNS`] or above
153/// [`RESTART_AFTER`], [`SpreadError::Render`] for a render that failed on
154/// both tries and [`SpreadError::Repeated`] for a set with a repeated
155/// render.
156pub fn draw(
157    spawn: impl Fn(usize) -> Result<BoxedRenderer, RenderError> + Send + Sync + 'static,
158    job: &RenderJob,
159    count: usize,
160) -> Result<Vec<Rendered>, SpreadError> {
161    if count < MIN_RUNS || !u64::try_from(count).is_ok_and(|count| count <= RESTART_AFTER) {
162        return Err(SpreadError::Runs(count));
163    }
164    // One borrowed job per call, so the draws share one copy of the
165    // state; the session keeps its instance between calls.
166    let session = RenderSession::new(NonZeroUsize::MIN, spawn);
167    let mut renders = Vec::with_capacity(count);
168    for _ in 0..count {
169        for outcome in session.render_all(std::slice::from_ref(job)) {
170            match outcome {
171                RenderOutcome::Rendered(rendered) => renders.push(rendered),
172                RenderOutcome::Failed(failure) => {
173                    return Err(SpreadError::Render(
174                        failure
175                            .errors
176                            .last()
177                            .map_or_else(|| "no error".to_owned(), ToString::to_string),
178                    ));
179                }
180            }
181        }
182    }
183    distinct(&renders)?;
184    Ok(renders)
185}
186
187/// Checks that no two renders hold the same samples, by the SHA-256 of
188/// each render's sample rate and samples.
189///
190/// # Errors
191///
192/// Returns [`SpreadError::Repeated`] when a render repeats another.
193pub fn distinct(renders: &[Rendered]) -> Result<(), SpreadError> {
194    let hashes: HashSet<[u8; 32]> = renders
195        .iter()
196        .map(|rendered| {
197            let audio = &rendered.audio;
198            let mut hash = Sha256::new();
199            hash.update(audio.sample_rate().get().to_le_bytes());
200            for channel in audio.channels() {
201                for sample in channel {
202                    hash.update(sample.to_le_bytes());
203                }
204            }
205            hash.finalize().into()
206        })
207        .collect();
208    if hashes.len() == renders.len() {
209        Ok(())
210    } else {
211        Err(SpreadError::Repeated {
212            distinct: hashes.len(),
213            renders: renders.len(),
214        })
215    }
216}
217
218/// What a comparison measures of one clip.
219///
220/// The clip runs from its onset, the first sample within [`ONSET_RANGE_DB`]
221/// of the peak, to its end, the last sample above the recipe's tail floor.
222/// Both channels count.
223#[derive(Clone, Debug, PartialEq)]
224pub struct ClipMetrics {
225    /// The sample rate of the clip.
226    pub sample_rate: SampleRate,
227    /// The RMS level from onset to end, in dBFS.
228    pub rms_db: f64,
229    /// The largest sample magnitude, in dBFS.
230    pub peak_db: f64,
231    /// From onset to end, in s.
232    pub duration_s: f64,
233    /// The RMS level of each 10 ms frame from the onset to the end, in
234    /// dBFS.
235    pub envelope_db: Vec<f64>,
236    /// The mean square over the channels of each frame from the onset to
237    /// the end, summed: entry `n` covers the first `n` frames.
238    energy: Vec<f64>,
239}
240
241impl ClipMetrics {
242    /// Measures `audio`.
243    ///
244    /// # Errors
245    ///
246    /// Returns [`SpreadError::Silent`], naming `what`, for a clip whose
247    /// every sample lies at or below the tail floor.
248    pub fn of(audio: &Audio, what: &str) -> Result<Self, SpreadError> {
249        let silent = || SpreadError::Silent(what.to_owned());
250        let channels = audio.channels();
251        let magnitude = |frame: usize| {
252            channels
253                .iter()
254                .map(|channel| channel[frame].abs())
255                .fold(0.0, f64::max)
256        };
257        let frames = audio.frames();
258        let peak = (0..frames).map(magnitude).fold(0.0, f64::max);
259        let floor = Recipe::current().tail_floor_gain();
260        if peak <= floor {
261            return Err(silent());
262        }
263        let threshold = peak
264            * Decibels::clamped(-ONSET_RANGE_DB.get())
265                .to_amplitude_gain()
266                .unwrap_or(0.0);
267        let onset = (0..frames)
268            .find(|frame| magnitude(*frame) >= threshold)
269            .ok_or_else(silent)?;
270        let end = (onset..frames)
271            .rev()
272            .find(|frame| magnitude(*frame) > floor)
273            .ok_or_else(silent)?
274            + 1;
275        let count = cast::usize_to_f64(channels.len());
276        let mut energy = Vec::with_capacity(end - onset + 1);
277        energy.push(0.0);
278        let mut sum = 0.0;
279        for frame in onset..end {
280            sum += channels.iter().map(|c| c[frame] * c[frame]).sum::<f64>() / count;
281            energy.push(sum);
282        }
283        let mean_square =
284            |from: usize, to: usize| (energy[to] - energy[from]) / cast::usize_to_f64(to - from);
285        let length = end - onset;
286        let step = usize::try_from(audio.sample_rate().get() / ENVELOPE_FRAMES_PER_S)
287            .unwrap_or(1)
288            .max(1);
289        let envelope_db = (0..length)
290            .step_by(step)
291            .map(|from| power_db(mean_square(from, (from + step).min(length))))
292            .collect();
293        Ok(Self {
294            sample_rate: audio.sample_rate(),
295            rms_db: power_db(mean_square(0, length)),
296            peak_db: 20.0 * peak.log10(),
297            duration_s: cast::usize_to_f64(length) / f64::from(audio.sample_rate().get()),
298            envelope_db,
299            energy,
300        })
301    }
302
303    /// The RMS level of the first `frames` frames after the onset, in dBFS.
304    fn rms_over(&self, frames: usize) -> f64 {
305        power_db(self.energy[frames] / cast::usize_to_f64(frames))
306    }
307
308    fn frames(&self) -> usize {
309        self.energy.len() - 1
310    }
311}
312
313/// An output of a clip that a spread covers.
314#[derive(Clone, Copy, Debug, PartialEq, Eq)]
315pub enum ClipOutput {
316    /// The RMS level. Two clips compare over their common span, the
317    /// shorter clip's length from the onset, so a cut tail does not change
318    /// the level.
319    RmsDb,
320    /// [`ClipMetrics::peak_db`].
321    PeakDb,
322    /// The peak over the RMS level, in dB: the crest factor. Two clips
323    /// compare with the RMS level over their common span, as
324    /// [`ClipOutput::RmsDb`] does. A gain shifts the peak and the RMS level
325    /// alike, so it moves the ratio only where it moves the clip's end.
326    PeakToRmsDb,
327    /// [`ClipMetrics::duration_s`].
328    DurationS,
329    /// The envelope distance, [`envelope_distance`].
330    EnvelopeDistanceDb,
331}
332
333impl ClipOutput {
334    /// Every clip output.
335    pub const ALL: [Self; 5] = [
336        Self::RmsDb,
337        Self::PeakDb,
338        Self::PeakToRmsDb,
339        Self::DurationS,
340        Self::EnvelopeDistanceDb,
341    ];
342
343    /// The outputs of the sound's shape, which [`compare_fitted`] compares
344    /// after a gain fit: the peak-to-RMS ratio, the duration to the tail
345    /// floor and the envelope distance. The RMS level and the peak follow
346    /// the level, so the fit takes them out. The duration and the
347    /// envelope still depend on the level, which is why the fit comes
348    /// first.
349    pub const SHAPE: [Self; 3] = [Self::PeakToRmsDb, Self::DurationS, Self::EnvelopeDistanceDb];
350
351    /// The name of the output in a spread file.
352    #[must_use]
353    pub fn name(self) -> &'static str {
354        match self {
355            Self::RmsDb => "rms_db",
356            Self::PeakDb => "peak_db",
357            Self::PeakToRmsDb => "peak_to_rms_db",
358            Self::DurationS => "duration_s",
359            Self::EnvelopeDistanceDb => "envelope_distance_db",
360        }
361    }
362
363    /// The unit of the output.
364    #[must_use]
365    pub fn unit(self) -> &'static str {
366        match self {
367            Self::DurationS => "s",
368            Self::RmsDb | Self::PeakDb | Self::PeakToRmsDb | Self::EnvelopeDistanceDb => "dB",
369        }
370    }
371
372    /// The value of the output for one clip; `None` for the envelope
373    /// distance, which only two clips have. The RMS level is the one from
374    /// onset to end, here and in the peak-to-RMS ratio.
375    #[must_use]
376    pub fn value(self, clip: &ClipMetrics) -> Option<f64> {
377        match self {
378            Self::RmsDb => Some(clip.rms_db),
379            Self::PeakDb => Some(clip.peak_db),
380            Self::PeakToRmsDb => Some(clip.peak_db - clip.rms_db),
381            Self::DurationS => Some(clip.duration_s),
382            Self::EnvelopeDistanceDb => None,
383        }
384    }
385
386    /// The distance of two clips on the output.
387    #[must_use]
388    pub fn distance(self, a: &ClipMetrics, b: &ClipMetrics) -> f64 {
389        match self {
390            Self::RmsDb => {
391                let frames = a.frames().min(b.frames());
392                (a.rms_over(frames) - b.rms_over(frames)).abs()
393            }
394            Self::PeakDb => (a.peak_db - b.peak_db).abs(),
395            Self::PeakToRmsDb => {
396                let frames = a.frames().min(b.frames());
397                ((a.peak_db - a.rms_over(frames)) - (b.peak_db - b.rms_over(frames))).abs()
398            }
399            Self::DurationS => (a.duration_s - b.duration_s).abs(),
400            Self::EnvelopeDistanceDb => envelope_distance(a, b),
401        }
402    }
403
404    /// The floor of the tolerance: [`LEVEL_FLOOR_DB`] for a level and one
405    /// sample for the duration.
406    #[must_use]
407    pub fn floor(self, sample_rate: SampleRate) -> f64 {
408        match self {
409            Self::DurationS => 1.0 / f64::from(sample_rate.get()),
410            Self::RmsDb | Self::PeakDb | Self::PeakToRmsDb | Self::EnvelopeDistanceDb => {
411                LEVEL_FLOOR_DB
412            }
413        }
414    }
415}
416
417/// The envelope distance of two clips, in dB.
418///
419/// It is the mean absolute level difference over the frames where either
420/// envelope lies within [`ENVELOPE_RANGE_DB`] of the loudest frame of both.
421/// A frame below that range, or past the end of the shorter clip, counts
422/// at the bottom of the range, so a longer tail adds to the distance. The
423/// frames align at the two onsets.
424#[must_use]
425pub fn envelope_distance(a: &ClipMetrics, b: &ClipMetrics) -> f64 {
426    let loudest = a
427        .envelope_db
428        .iter()
429        .chain(&b.envelope_db)
430        .fold(SILENT_DB, |x, y| x.max(*y));
431    let bottom = loudest - ENVELOPE_RANGE_DB.get();
432    let level =
433        |envelope: &[f64], frame: usize| envelope.get(frame).copied().unwrap_or(bottom).max(bottom);
434    let frames = a.envelope_db.len().max(b.envelope_db.len());
435    let (sum, count) = (0..frames)
436        .map(|frame| (level(&a.envelope_db, frame), level(&b.envelope_db, frame)))
437        .filter(|(x, y)| x.max(*y) > bottom)
438        .fold((0.0, 0_usize), |(sum, count), (x, y)| {
439            (sum + (x - y).abs(), count + 1)
440        });
441    if count == 0 {
442        0.0
443    } else {
444        sum / cast::usize_to_f64(count)
445    }
446}
447
448/// The rank, counted from 1 in ascending order, of the leave-one-out
449/// median that is the tolerance of `count` renders: `⌈0.95 (count + 1)⌉`,
450/// at most `count`.
451///
452/// A clip of the same sound and the renders are exchangeable, so the
453/// clip's median lies at or below the median at this rank with a
454/// probability of the rank over `count + 1`: 20/21 for 20 renders. With
455/// fewer than 19 renders the rank stops at `count`, and the coverage falls
456/// below 95 %.
457#[must_use]
458pub fn tolerance_rank(count: usize) -> usize {
459    (COVERAGE_PERCENT * (count + 1)).div_ceil(100).min(count)
460}
461
462/// The spread of one output.
463#[derive(Clone, Debug, PartialEq, Serialize)]
464pub struct SpreadRow {
465    /// The output: a [`ClipOutput`] name, or the path of a number in the
466    /// layer report, such as `stem.rms_dbfs`.
467    pub output: String,
468    /// The unit of the output, of its spread and of its tolerance.
469    pub unit: String,
470    /// The [`SPREAD_QUANTILE`] of the distances over every pair of
471    /// renders: how far two renders lie apart.
472    pub spread: f64,
473    /// The leave-one-out median at rank [`tolerance_rank`]: for each
474    /// render, the median of its distances from the other renders. A clip
475    /// compares with the renders against it.
476    pub tolerance: f64,
477    /// The least tolerance a comparison uses, so float noise on an output
478    /// that every render repeats does not count as a difference. Always
479    /// positive.
480    pub floor: f64,
481    /// How many renders lie within the limit of the other renders, each
482    /// held out in turn and compared as a clip: a check of the coverage
483    /// that [`tolerance_rank`] states.
484    pub held_out_within: usize,
485    /// A diagnostic of the draws: the median distance of the renders next
486    /// to each other in the sequence over the median distance of all
487    /// pairs. Near 1 when a draw does not depend on the one before; absent
488    /// when every pair is 0. With 19 adjacent pairs it is noisy.
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub adjacent_ratio: Option<f64>,
491    /// A diagnostic of the draws: the lag-1 autocorrelation of the output's
492    /// value over the sequence, [`lag1_autocorrelation`]. Absent for an
493    /// output of two clips and for a value that every render repeats.
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub lag1_autocorrelation: Option<f64>,
496}
497
498impl SpreadRow {
499    /// The spread row of `count` renders whose pairwise distances
500    /// `distance` gives, with the output's `values` in draw order when it
501    /// has one per render.
502    ///
503    /// Returns `None` for fewer than [`MIN_RUNS`] renders, for a `floor`
504    /// that is not positive and finite, which would let a difference over
505    /// a limit of 0 be no number, and for a distance that is not finite.
506    #[must_use]
507    pub fn of(
508        output: &str,
509        unit: &str,
510        count: usize,
511        distance: impl Fn(usize, usize) -> f64,
512        values: Option<&[f64]>,
513        floor: f64,
514    ) -> Option<Self> {
515        if count < MIN_RUNS || !(floor.is_finite() && floor > 0.0) {
516            return None;
517        }
518        let mut matrix = vec![0.0; count * count];
519        for i in 0..count {
520            for j in i + 1..count {
521                let d = distance(i, j);
522                matrix[i * count + j] = d;
523                matrix[j * count + i] = d;
524            }
525        }
526        let at = |i: usize, j: usize| matrix[i * count + j];
527        let all: Vec<usize> = (0..count).collect();
528        let pairs: Vec<f64> = (0..count)
529            .flat_map(|i| (i + 1..count).map(move |j| (i, j)))
530            .map(|(i, j)| at(i, j))
531            .collect();
532        let tolerance = tolerance_of(&all, at)?;
533        let mut held_out_within = 0;
534        for held in 0..count {
535            let rest: Vec<usize> = (0..count).filter(|i| *i != held).collect();
536            let difference = median(&rest.iter().map(|j| at(held, *j)).collect::<Vec<_>>())?;
537            if difference <= tolerance_of(&rest, at)?.max(floor) {
538                held_out_within += 1;
539            }
540        }
541        let adjacent = median(&(1..count).map(|i| at(i - 1, i)).collect::<Vec<_>>())?;
542        let typical = median(&pairs)?;
543        Some(Self {
544            output: output.to_owned(),
545            unit: unit.to_owned(),
546            spread: sorted_quantile(pairs, SPREAD_QUANTILE)?,
547            tolerance,
548            floor,
549            held_out_within,
550            adjacent_ratio: (typical > 0.0).then(|| adjacent / typical),
551            lag1_autocorrelation: values.and_then(lag1_autocorrelation),
552        })
553    }
554
555    /// The spread row of a scalar output from its value in each render, in
556    /// draw order. Returns `None` as [`SpreadRow::of`] does.
557    #[must_use]
558    pub fn of_values(output: &str, unit: &str, values: &[f64], floor: f64) -> Option<Self> {
559        Self::of(
560            output,
561            unit,
562            values.len(),
563            |i, j| (values[i] - values[j]).abs(),
564            Some(values),
565            floor,
566        )
567    }
568
569    /// The tolerance a comparison uses: the larger of
570    /// [`SpreadRow::tolerance`] and [`SpreadRow::floor`].
571    #[must_use]
572    pub fn limit(&self) -> f64 {
573        self.tolerance.max(self.floor)
574    }
575}
576
577/// The tolerance of the renders `of`: the leave-one-out median at rank
578/// [`tolerance_rank`], each median over the distances `at` gives from one
579/// render to the others.
580fn tolerance_of(of: &[usize], at: impl Fn(usize, usize) -> f64) -> Option<f64> {
581    let mut medians = of
582        .iter()
583        .map(|i| {
584            median(
585                &of.iter()
586                    .filter(|j| *j != i)
587                    .map(|j| at(*i, *j))
588                    .collect::<Vec<_>>(),
589            )
590        })
591        .collect::<Option<Vec<_>>>()?;
592    medians.sort_by(f64::total_cmp);
593    medians
594        .get(tolerance_rank(of.len()).checked_sub(1)?)
595        .copied()
596}
597
598/// The lag-1 autocorrelation of `values` in draw order: the sum of
599/// products of each deviation from the mean with the next one, over the
600/// sum of squared deviations.
601///
602/// A diagnostic: for independent draws it lies near `-1 / n`, within about
603/// `2 / √n`, so ±0.45 for 20 renders. A drift over the sequence takes it
604/// towards 1. Returns `None` for fewer than two values or values that do
605/// not vary.
606#[must_use]
607pub fn lag1_autocorrelation(values: &[f64]) -> Option<f64> {
608    // The values are compared, not the variance: the mean of repeated
609    // copies of one value is not exactly that value, so the variance of
610    // their deviations is a tiny number above 0. One value repeats itself.
611    let first = *values.first()?;
612    if values.iter().all(|value| value.total_cmp(&first).is_eq()) {
613        return None;
614    }
615    // The sums run over the steps from the first value, which are exact for
616    // values near it, so a rounded mean cannot swamp deviations of one unit
617    // in the last place.
618    let steps: Vec<f64> = values.iter().map(|value| value - first).collect();
619    let mean = steps.iter().sum::<f64>() / cast::usize_to_f64(steps.len());
620    let deviation: Vec<f64> = steps.iter().map(|step| step - mean).collect();
621    let variance: f64 = deviation.iter().map(|d| d * d).sum();
622    let lagged: f64 = deviation.windows(2).map(|pair| pair[0] * pair[1]).sum();
623    (variance > 0.0 && variance.is_finite()).then(|| lagged / variance)
624}
625
626/// The spread rows of the clip outputs, one per [`ClipOutput`].
627///
628/// # Errors
629///
630/// Returns [`SpreadError::Runs`] for fewer than [`MIN_RUNS`] clips and
631/// [`SpreadError::SampleRate`] when the clips' sample rates differ.
632pub fn clip_rows(clips: &[ClipMetrics]) -> Result<Vec<SpreadRow>, SpreadError> {
633    let Some(rate) = shared_rate(clips)?.filter(|_| clips.len() >= MIN_RUNS) else {
634        return Err(SpreadError::Runs(clips.len()));
635    };
636    ClipOutput::ALL
637        .into_iter()
638        .map(|output| {
639            let values: Option<Vec<f64>> = clips.iter().map(|clip| output.value(clip)).collect();
640            SpreadRow::of(
641                output.name(),
642                output.unit(),
643                clips.len(),
644                |i, j| output.distance(&clips[i], &clips[j]),
645                values.as_deref(),
646                output.floor(rate),
647            )
648            .ok_or(SpreadError::MissingRow(output.name()))
649        })
650        .collect()
651}
652
653fn shared_rate(clips: &[ClipMetrics]) -> Result<Option<SampleRate>, SpreadError> {
654    let Some(first) = clips.first() else {
655        return Ok(None);
656    };
657    match clips
658        .iter()
659        .find(|clip| clip.sample_rate != first.sample_rate)
660    {
661        Some(other) => Err(SpreadError::SampleRate {
662            clip: other.sample_rate,
663            renders: first.sample_rate,
664        }),
665        None => Ok(Some(first.sample_rate)),
666    }
667}
668
669/// The spread rows of the numbers of layer reports, one report per render.
670///
671/// A number that some report lacks, or holds as `null`, gets no row: it has
672/// no value to compare in that render. The floor of a level in dB is
673/// [`LEVEL_FLOOR_DB`]; any other unit takes the float32 resolution of the
674/// largest value, as the reports carry estimates made in float32 audio,
675/// and at least the smallest normal float32, for a value that every render
676/// holds at 0.
677///
678/// # Errors
679///
680/// Returns [`SpreadError::Runs`] for fewer than [`MIN_RUNS`] reports and
681/// [`SpreadError::UnknownUnit`] for a field with no known unit.
682pub fn layer_rows(reports: &[Value]) -> Result<Vec<SpreadRow>, SpreadError> {
683    if reports.len() < MIN_RUNS {
684        return Err(SpreadError::Runs(reports.len()));
685    }
686    let mut paths: BTreeMap<String, (String, Vec<f64>)> = BTreeMap::new();
687    for report in reports {
688        for (path, unit, value) in layer_values(report)? {
689            paths
690                .entry(path)
691                .or_insert_with(|| (unit, Vec::new()))
692                .1
693                .push(value);
694        }
695    }
696    Ok(paths
697        .iter()
698        .filter(|(_, (_, values))| values.len() == reports.len())
699        .filter_map(|(path, (unit, values))| {
700            let floor = if unit == "dB" {
701                LEVEL_FLOOR_DB
702            } else {
703                (values.iter().fold(0.0, |max: f64, v| max.max(v.abs())) * f64::from(f32::EPSILON))
704                    .max(f64::from(f32::MIN_POSITIVE))
705            };
706            SpreadRow::of_values(path, unit, values, floor)
707        })
708        .collect())
709}
710
711/// The numbers of a layer report that a spread covers, with their units.
712///
713/// They are each estimate and each median of every group but the ones
714/// that hold no estimate: `schema`, `source`, `synth`, `transcription` and
715/// `conventions`.
716///
717/// The path drops the `value` or `median` key, such as `stem.rms_dbfs` or
718/// `stem.band_share[1].share`. Left out are confidences, IQRs, band
719/// edges, the inputs (`bpm`, `bpm_given`, `bpm_refined` and
720/// `sample_rate_hz`), the format constant `ticks_per_beat`, the mapper's
721/// `suggested_edits` and `unrepresentable`, and every `null`. The unit
722/// comes from the field name's suffix (`_hz`, `_s`, `_ms`, `_db`, `_dbfs`,
723/// `_deg`, `_st`, `_ct`), or from [`NAMED_UNITS`] for a name without one.
724///
725/// # Errors
726///
727/// Returns [`SpreadError::UnknownUnit`] for a field that has neither.
728pub fn layer_values(report: &Value) -> Result<Vec<(String, String, f64)>, SpreadError> {
729    let mut values = Vec::new();
730    if let Some(root) = report.as_object() {
731        for (key, group) in root {
732            if !NON_LAYERS.contains(&key.as_str()) {
733                walk(key, key, group, &mut values)?;
734            }
735        }
736    }
737    Ok(values)
738}
739
740/// The fields of a layer report that have no unit suffix, with their units.
741///
742/// The units are `ratio` for a number without a unit, `count` for a count,
743/// `st` for a MIDI key, whose distances are semitones, `velocity` for a
744/// MIDI velocity and `BPM` for a tempo.
745pub const NAMED_UNITS: [(&str, &str); 20] = [
746    ("bend_events", "count"),
747    ("centroid_change", "ratio"),
748    ("centroid_over_f0", "ratio"),
749    ("curve", "ratio"),
750    ("dropped", "count"),
751    ("extent_factor", "ratio"),
752    ("flatness", "ratio"),
753    ("key", "st"),
754    ("max_polyphony", "count"),
755    ("mean_polyphony", "count"),
756    ("n_notes_used", "count"),
757    ("overlap_fraction", "ratio"),
758    ("pump_beats", "count"),
759    ("pump_bpm", "BPM"),
760    ("residual", "ratio"),
761    ("rho", "ratio"),
762    ("share", "ratio"),
763    ("tail_vote_count", "count"),
764    ("tail_weight", "ratio"),
765    ("velocity", "velocity"),
766];
767
768/// The top-level keys of a layer report that hold no estimates.
769const NON_LAYERS: [&str; 5] = ["schema", "source", "synth", "transcription", "conventions"];
770/// Keys that hold no estimate: a confidence, a spread, a band edge, an
771/// input of the run, a constant of the file format or the mapper's output.
772const SKIPPED: [&str; 11] = [
773    "confidence",
774    "iqr",
775    "lo_hz",
776    "hi_hz",
777    "bpm",
778    "bpm_given",
779    "bpm_refined",
780    "sample_rate_hz",
781    "ticks_per_beat",
782    "suggested_edits",
783    "unrepresentable",
784];
785
786fn walk(
787    path: &str,
788    name: &str,
789    value: &Value,
790    out: &mut Vec<(String, String, f64)>,
791) -> Result<(), SpreadError> {
792    match value {
793        Value::Object(map) => {
794            for (key, inner) in map {
795                if SKIPPED.contains(&key.as_str()) {
796                    continue;
797                }
798                if key == "value" || key == "median" {
799                    walk(path, name, inner, out)?;
800                } else {
801                    walk(&format!("{path}.{key}"), key, inner, out)?;
802                }
803            }
804        }
805        Value::Array(items) => {
806            for (index, inner) in items.iter().enumerate() {
807                walk(&format!("{path}[{index}]"), name, inner, out)?;
808            }
809        }
810        Value::Number(number) => {
811            if let Some(number) = number.as_f64() {
812                let unit =
813                    unit_of(name).ok_or_else(|| SpreadError::UnknownUnit(path.to_owned()))?;
814                out.push((path.to_owned(), unit.to_owned(), number));
815            }
816        }
817        _ => {}
818    }
819    Ok(())
820}
821
822fn unit_of(name: &str) -> Option<&'static str> {
823    if let Some((_, unit)) = NAMED_UNITS.iter().find(|(named, _)| *named == name) {
824        return Some(unit);
825    }
826    [
827        ("_dbfs", "dB"),
828        ("_db", "dB"),
829        ("_hz", "Hz"),
830        ("_deg", "deg"),
831        ("_ms", "ms"),
832        ("_s", "s"),
833        ("_st", "st"),
834        ("_ct", "ct"),
835    ]
836    .iter()
837    .find(|(suffix, _)| name.ends_with(suffix))
838    .map(|(_, unit)| *unit)
839}
840
841/// The spread of one preset on one synth build and quality setting.
842#[derive(Clone, Debug, PartialEq, Serialize)]
843pub struct SpreadTable {
844    /// What rendered: the synth, its build, the host and the architecture.
845    pub identity: HostIdentity,
846    /// The recipe version.
847    pub recipe_version: u32,
848    /// The quality setting: the oversampling of every render.
849    pub oversampling: Oversampling,
850    /// How many renders, each a distinct draw.
851    pub n: usize,
852    /// The spread statistic, [`STATISTIC`].
853    pub statistic: String,
854    /// The comparison tolerance, [`TOLERANCE`].
855    pub tolerance: String,
856    /// The share of clips of the same sound that the tolerance covers: the
857    /// [`tolerance_rank`] of `n` over `n + 1`.
858    pub coverage: f64,
859    /// The SHA-256 of the preset file.
860    pub preset_sha256: String,
861    /// The SHA-256 of the MIDI file the notes came from, when they came
862    /// from one.
863    #[serde(skip_serializing_if = "Option::is_none")]
864    pub midi_sha256: Option<String>,
865    /// The tempo.
866    pub bpm: Bpm,
867    /// The spread of each output.
868    pub outputs: Vec<SpreadRow>,
869}
870
871impl SpreadTable {
872    /// The table of `renders`, the draws of one preset, with `outputs`.
873    ///
874    /// # Errors
875    ///
876    /// Returns [`SpreadError::Render`] when no render came back.
877    pub fn new(
878        renders: &[Rendered],
879        preset_sha256: String,
880        bpm: Bpm,
881        outputs: Vec<SpreadRow>,
882    ) -> Result<Self, SpreadError> {
883        let first = renders
884            .first()
885            .ok_or_else(|| SpreadError::Render("no render came back".to_owned()))?;
886        Ok(Self {
887            identity: first.record.identity.clone(),
888            recipe_version: first.record.recipe_version,
889            oversampling: first.record.oversampling,
890            n: renders.len(),
891            statistic: STATISTIC.to_owned(),
892            tolerance: TOLERANCE.to_owned(),
893            coverage: cast::usize_to_f64(tolerance_rank(renders.len()))
894                / cast::usize_to_f64(renders.len() + 1),
895            preset_sha256,
896            midi_sha256: first.record.midi_sha256.clone(),
897            bpm,
898            outputs,
899        })
900    }
901}
902
903/// How a clip compares with renders of the same preset on one output.
904#[derive(Clone, Debug, PartialEq, Serialize)]
905pub struct Comparison {
906    /// The output.
907    pub output: String,
908    /// The unit.
909    pub unit: String,
910    /// The clip's value; absent for a distance.
911    #[serde(skip_serializing_if = "Option::is_none")]
912    pub clip: Option<f64>,
913    /// The median of the renders' values; absent for a distance.
914    #[serde(skip_serializing_if = "Option::is_none")]
915    pub renders: Option<f64>,
916    /// The median, over the renders, of the distance between the clip and
917    /// one render.
918    pub difference: f64,
919    /// The tolerance: the larger of the row's tolerance and floor, so
920    /// always positive.
921    pub limit: f64,
922    /// The difference over the limit: at most 1 for a match.
923    pub ratio: f64,
924    /// Whether the difference lies within the limit.
925    pub within: bool,
926}
927
928/// Compares `clip` with each of `renders` on every clip output.
929///
930/// The difference is the median over the renders of the clip's distance
931/// from one render. A clip of the same sound lies within the row's limit
932/// with the coverage of [`tolerance_rank`].
933///
934/// # Errors
935///
936/// Returns [`SpreadError::SampleRate`] when the clip and the renders have
937/// different sample rates, [`SpreadError::Runs`] for no render and
938/// [`SpreadError::MissingRow`] for a clip output that `rows` lacks.
939pub fn compare(
940    clip: &ClipMetrics,
941    renders: &[ClipMetrics],
942    rows: &[SpreadRow],
943) -> Result<Vec<Comparison>, SpreadError> {
944    compare_on(&ClipOutput::ALL, clip, renders, rows)
945}
946
947/// A fitted gain beyond this many dB, either way, is a finding to explain:
948/// within it a level difference is no fault, since a comparison of two
949/// sounds matches their loudness first.
950pub const GAIN_RANGE_DB: f64 = 3.0;
951
952/// How a recording compares with renders of the same preset once a gain
953/// matches its level to theirs.
954#[derive(Clone, Debug, PartialEq, Serialize)]
955pub struct FittedComparison {
956    /// The gain applied to the recording, in dB: [`fit_gain`].
957    pub gain_db: f64,
958    /// Whether the gain lies beyond [`GAIN_RANGE_DB`]: a finding, not a
959    /// failure.
960    pub gain_beyond_range: bool,
961    /// The comparisons of the recording after the gain on the
962    /// [`ClipOutput::SHAPE`] outputs.
963    pub comparisons: Vec<Comparison>,
964    /// Whether every comparison lies within its limit.
965    pub within: bool,
966}
967
968/// The gain in dB that matches the level of `clip` to `renders`.
969///
970/// It is the median over the renders of their RMS level minus the clip's,
971/// each pair over its common span, as [`ClipOutput::RmsDb`] compares them.
972/// Returns `None` for no render.
973#[must_use]
974pub fn fit_gain(clip: &ClipMetrics, renders: &[ClipMetrics]) -> Option<f64> {
975    median(
976        &renders
977            .iter()
978            .map(|render| {
979                let frames = clip.frames().min(render.frames());
980                render.rms_over(frames) - clip.rms_over(frames)
981            })
982            .collect::<Vec<_>>(),
983    )
984}
985
986/// Fits the gain of `recording` to `renders`, applies it and compares the
987/// result on the [`ClipOutput::SHAPE`] outputs.
988///
989/// The gain moves the recording's end too, since the end is the last
990/// sample above a fixed tail floor, so the duration compares after it.
991///
992/// # Errors
993///
994/// Returns [`SpreadError::Silent`], naming `what`, for a silent
995/// recording, [`SpreadError::Audio`] for a recording the gain cannot
996/// scale, and the errors of [`compare`].
997pub fn compare_fitted(
998    recording: &Audio,
999    what: &str,
1000    renders: &[ClipMetrics],
1001    rows: &[SpreadRow],
1002) -> Result<FittedComparison, SpreadError> {
1003    let clip = ClipMetrics::of(recording, what)?;
1004    check_rate(&clip, renders)?;
1005    let gain_db = fit_gain(&clip, renders).ok_or(SpreadError::Runs(renders.len()))?;
1006    let amplitude = Decibels::clamped(gain_db)
1007        .to_amplitude_gain()
1008        .unwrap_or(1.0);
1009    let scaled = Audio::new(
1010        recording.sample_rate(),
1011        recording
1012            .channels()
1013            .iter()
1014            .map(|channel| channel.iter().map(|x| x * amplitude).collect())
1015            .collect(),
1016    )
1017    .map_err(SpreadError::Audio)?;
1018    let fitted = ClipMetrics::of(&scaled, what)?;
1019    let comparisons = compare_on(&ClipOutput::SHAPE, &fitted, renders, rows)?;
1020    Ok(FittedComparison {
1021        gain_db,
1022        gain_beyond_range: gain_db.abs() > GAIN_RANGE_DB,
1023        within: comparisons.iter().all(|comparison| comparison.within),
1024        comparisons,
1025    })
1026}
1027
1028fn check_rate(clip: &ClipMetrics, renders: &[ClipMetrics]) -> Result<(), SpreadError> {
1029    match shared_rate(renders)? {
1030        Some(rate) if rate != clip.sample_rate => Err(SpreadError::SampleRate {
1031            clip: clip.sample_rate,
1032            renders: rate,
1033        }),
1034        _ => Ok(()),
1035    }
1036}
1037
1038fn compare_on(
1039    outputs: &[ClipOutput],
1040    clip: &ClipMetrics,
1041    renders: &[ClipMetrics],
1042    rows: &[SpreadRow],
1043) -> Result<Vec<Comparison>, SpreadError> {
1044    check_rate(clip, renders)?;
1045    outputs
1046        .iter()
1047        .map(|&output| {
1048            let row = rows
1049                .iter()
1050                .find(|row| row.output == output.name())
1051                .ok_or(SpreadError::MissingRow(output.name()))?;
1052            let difference = median(
1053                &renders
1054                    .iter()
1055                    .map(|render| output.distance(clip, render))
1056                    .collect::<Vec<_>>(),
1057            )
1058            .ok_or(SpreadError::Runs(renders.len()))?;
1059            let limit = row.limit();
1060            Ok(Comparison {
1061                output: row.output.clone(),
1062                unit: row.unit.clone(),
1063                clip: output.value(clip),
1064                renders: renders
1065                    .iter()
1066                    .map(|render| output.value(render))
1067                    .collect::<Option<Vec<_>>>()
1068                    .and_then(|values| median(&values)),
1069                difference,
1070                limit,
1071                ratio: difference / limit,
1072                within: difference <= limit,
1073            })
1074        })
1075        .collect()
1076}
1077
1078fn median(values: &[f64]) -> Option<f64> {
1079    Summary::of(values).map(Summary::median)
1080}
1081
1082fn sorted_quantile(mut values: Vec<f64>, q: f64) -> Option<f64> {
1083    values.sort_by(f64::total_cmp);
1084    quantile(&values, q)
1085}
1086
1087fn power_db(power: f64) -> f64 {
1088    Decibels::from_power_ratio(power).map_or(SILENT_DB, Decibels::get)
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093    use super::*;
1094
1095    fn tone(gain: f64, seconds: f64, lead: usize) -> Audio {
1096        let rate = 44_100.0;
1097        let frames = cast::round_to_usize(seconds * rate).unwrap();
1098        let samples: Vec<f64> = (0..lead)
1099            .map(|_| 0.0)
1100            .chain((0..frames).map(|n| {
1101                gain * (std::f64::consts::TAU * 440.0 * cast::usize_to_f64(n) / rate).sin()
1102            }))
1103            .collect();
1104        Audio::new(
1105            SampleRate::new(44_100).unwrap(),
1106            vec![samples.clone(), samples],
1107        )
1108        .unwrap()
1109    }
1110
1111    fn metrics(gain: f64, seconds: f64, lead: usize) -> ClipMetrics {
1112        ClipMetrics::of(&tone(gain, seconds, lead), "the tone").unwrap()
1113    }
1114
1115    #[test]
1116    fn a_sine_has_its_level_peak_and_length() {
1117        let metrics = metrics(0.5, 1.0, 4410);
1118        // A sine of amplitude 0.5 has an RMS of 0.5 / √2: -9.03 dBFS.
1119        assert!((metrics.rms_db + 9.03).abs() < 0.01, "{}", metrics.rms_db);
1120        assert!((metrics.peak_db + 6.02).abs() < 0.01, "{}", metrics.peak_db);
1121        // The lead-in of 0.1 s does not count.
1122        assert!(
1123            (metrics.duration_s - 1.0).abs() < 0.001,
1124            "{}",
1125            metrics.duration_s
1126        );
1127        assert_eq!(metrics.envelope_db.len(), 100);
1128        let silent = ClipMetrics::of(&tone(0.0, 1.0, 0), "the clip").unwrap_err();
1129        assert!(
1130            silent.to_string().starts_with("the clip is silent"),
1131            "{silent}"
1132        );
1133    }
1134
1135    #[test]
1136    fn the_envelope_distance_aligns_onsets_and_counts_a_longer_tail() {
1137        let a = metrics(0.5, 1.0, 0);
1138        let late = metrics(0.5, 1.0, 22_050);
1139        assert!(envelope_distance(&a, &late) < 1e-6);
1140        let quieter = metrics(0.25, 1.0, 0);
1141        assert!((envelope_distance(&a, &quieter) - 6.02).abs() < 0.01);
1142        let longer = metrics(0.5, 2.0, 0);
1143        // Half the counted frames lie past the shorter clip, 40 dB apart.
1144        assert!((envelope_distance(&a, &longer) - 20.0).abs() < 0.5);
1145    }
1146
1147    #[test]
1148    fn the_rms_level_compares_over_the_common_span() {
1149        // A clip whose second half is 20 dB quieter has a lower RMS from
1150        // onset to end, but over the first half it matches a clip cut there.
1151        let rate = 44_100;
1152        let full: Vec<f64> = (0..rate)
1153            .map(|n| {
1154                let gain = if n < rate / 2 { 0.5 } else { 0.05 };
1155                gain * (std::f64::consts::TAU * 440.0 * f64::from(n) / f64::from(rate)).sin()
1156            })
1157            .collect();
1158        let rate = SampleRate::new(44_100).unwrap();
1159        let long = ClipMetrics::of(&Audio::new(rate, vec![full.clone()]).unwrap(), "a").unwrap();
1160        let cut = ClipMetrics::of(
1161            &Audio::new(rate, vec![full[..22_050].to_vec()]).unwrap(),
1162            "b",
1163        )
1164        .unwrap();
1165        assert!(cut.rms_db - long.rms_db > 2.0);
1166        assert!(ClipOutput::RmsDb.distance(&long, &cut) < 1e-9);
1167    }
1168
1169    #[test]
1170    fn a_layer_report_gives_its_estimates_with_units() {
1171        let report = serde_json::json!({
1172            "schema": "layers/1",
1173            "conventions": { "frame_size": 2048 },
1174            "stem": {
1175                "bpm": 120.0,
1176                "duration_s": 4.5,
1177                "rms_dbfs": { "value": -16.6, "confidence": { "spread": 1.0 } },
1178                "centroid_hz": { "median": 238.2, "iqr": 115.8 },
1179                "band_share": { "value": [{ "lo_hz": 20.0, "hi_hz": 60.0, "share": 0.1 }] },
1180                "stereo": { "full": { "rho": 0.49, "ipd_deg": 67.1 } },
1181                "crest_db": { "value": null }
1182            }
1183        });
1184        let values = layer_values(&report).unwrap();
1185        let named: Vec<(&str, &str)> = values
1186            .iter()
1187            .map(|(path, unit, _)| (path.as_str(), unit.as_str()))
1188            .collect();
1189        assert_eq!(
1190            named,
1191            [
1192                ("stem.duration_s", "s"),
1193                ("stem.rms_dbfs", "dB"),
1194                ("stem.centroid_hz", "Hz"),
1195                ("stem.band_share[0].share", "ratio"),
1196                ("stem.stereo.full.rho", "ratio"),
1197                ("stem.stereo.full.ipd_deg", "deg"),
1198            ]
1199        );
1200    }
1201
1202    #[test]
1203    fn a_field_with_no_known_unit_is_refused() {
1204        let report = serde_json::json!({ "stem": { "notes": { "value": 3 } } });
1205        let error = layer_values(&report).unwrap_err();
1206        assert!(
1207            error.to_string().contains("stem.notes has no known unit"),
1208            "{error}"
1209        );
1210    }
1211
1212    #[test]
1213    fn a_layer_number_that_one_report_lacks_gets_no_row() {
1214        let report = |rms: f64, centroid: Option<f64>| {
1215            serde_json::json!({
1216                "stem": { "rms_dbfs": { "value": rms }, "centroid_hz": { "median": centroid } }
1217            })
1218        };
1219        let reports = [
1220            report(-16.0, Some(200.0)),
1221            report(-16.2, None),
1222            report(-16.1, Some(210.0)),
1223        ];
1224        let rows = layer_rows(&reports).unwrap();
1225        let names: Vec<&str> = rows.iter().map(|row| row.output.as_str()).collect();
1226        assert_eq!(names, ["stem.rms_dbfs"]);
1227        assert_eq!(rows[0].floor, LEVEL_FLOOR_DB);
1228        assert!((rows[0].spread - 0.19).abs() < 1e-9, "{:?}", rows[0]);
1229    }
1230
1231    #[test]
1232    fn the_spread_is_the_95th_percentile_of_the_pairs() {
1233        let values: Vec<f64> = (0..20).map(f64::from).collect();
1234        // 190 pairs; |i - j| = d occurs 20 - d times.
1235        let mut pairs: Vec<f64> = Vec::new();
1236        for i in 0..20 {
1237            for j in i + 1..20 {
1238                pairs.push(f64::from(j - i));
1239            }
1240        }
1241        assert_eq!(pairs.len(), 190);
1242        pairs.sort_by(f64::total_cmp);
1243        let expected = quantile(&pairs, 0.95).unwrap();
1244        let row = SpreadRow::of_values("x", "dB", &values, 1e-9).unwrap();
1245        assert!((row.spread - expected).abs() < 1e-12);
1246        assert!(SpreadRow::of_values("x", "dB", &values[..2], 1e-9).is_none());
1247    }
1248
1249    #[test]
1250    fn the_tolerance_rank_covers_95_percent_of_n_plus_one() {
1251        // ⌈0.95 (n + 1)⌉: the largest of 19 or 20 medians, the 39th of 40.
1252        assert_eq!(tolerance_rank(20), 20);
1253        assert_eq!(tolerance_rank(19), 19);
1254        assert_eq!(tolerance_rank(40), 39);
1255        assert_eq!(tolerance_rank(100), 96);
1256        // Below 19 renders the rank stops at n.
1257        assert_eq!(tolerance_rank(3), 3);
1258    }
1259
1260    #[test]
1261    fn the_tolerance_is_the_leave_one_out_median_at_the_rank() {
1262        // Values 0..5: the medians of each value's distances to the others
1263        // are 2.5, 1.5, 1.5, 1.5, 2.5, and the rank of 5 is 5.
1264        let values: Vec<f64> = (0..5).map(f64::from).collect();
1265        let row = SpreadRow::of_values("x", "dB", &values, 1e-9).unwrap();
1266        assert_eq!(row.tolerance, 2.5);
1267        // Held out, the end values lie 2.5 from the other four, whose
1268        // largest median is 2: only the middle three lie within.
1269        assert_eq!(row.held_out_within, 3);
1270        // Adjacent values lie 1 apart; the median of all ten pairs is 2.
1271        assert_eq!(row.adjacent_ratio, Some(0.5));
1272    }
1273
1274    #[test]
1275    fn a_drift_over_the_draws_shows_in_both_diagnostics() {
1276        // The same 20 values, once rising in draw order and once
1277        // interleaved, so the spread and the tolerance stay alike.
1278        let rising: Vec<f64> = (0..20).map(f64::from).collect();
1279        let interleaved: Vec<f64> = (0..20)
1280            .map(|i| f64::from(if i % 2 == 0 { i / 2 } else { 19 - i / 2 }))
1281            .collect();
1282        let drift = SpreadRow::of_values("x", "dB", &rising, 1e-9).unwrap();
1283        let mixed = SpreadRow::of_values("x", "dB", &interleaved, 1e-9).unwrap();
1284        assert_eq!(
1285            (drift.spread, drift.tolerance),
1286            (mixed.spread, mixed.tolerance)
1287        );
1288        let drift_r = drift.lag1_autocorrelation.unwrap();
1289        let mixed_r = mixed.lag1_autocorrelation.unwrap();
1290        assert!(drift_r > 0.8, "{drift_r}");
1291        assert!(mixed_r < -0.8, "{mixed_r}");
1292        assert!(drift.adjacent_ratio.unwrap() < 0.2, "{drift:?}");
1293        assert!(mixed.adjacent_ratio.unwrap() > 1.5, "{mixed:?}");
1294        assert_eq!(lag1_autocorrelation(&[1.0; 5]), None);
1295    }
1296
1297    #[test]
1298    fn a_value_that_every_render_repeats_has_no_autocorrelation() {
1299        // The mean of 20 copies of 4.2 is not exactly 4.2, so the variance
1300        // of the deviations is not 0.
1301        assert_eq!(lag1_autocorrelation(&[4.2; 20]), None);
1302        assert_eq!(lag1_autocorrelation(&[0.1; 3]), None);
1303        let row = SpreadRow::of_values("x", "s", &[4.2; 20], 1e-9).unwrap();
1304        assert_eq!(row.lag1_autocorrelation, None);
1305    }
1306
1307    #[test]
1308    fn values_one_step_apart_give_the_autocorrelation_of_their_pattern() {
1309        // Two values that alternate deviate from their mean by ±c, so the
1310        // lag-1 autocorrelation is -19 c² / (20 c²) = -0.95 however small c
1311        // is. A mean of the raw values rounds by as much as c.
1312        let values: Vec<f64> = (0..20)
1313            .map(|i| if i % 2 == 0 { 4.2 } else { 4.2_f64.next_up() })
1314            .collect();
1315        let r = lag1_autocorrelation(&values).unwrap();
1316        assert!((r + 0.95).abs() < 1e-12, "{r}");
1317    }
1318
1319    #[test]
1320    fn a_floor_of_zero_gives_no_row() {
1321        assert!(SpreadRow::of_values("x", "ratio", &[0.0; 4], 0.0).is_none());
1322        let zero = || serde_json::json!({ "stem": { "flatness": { "value": 0.0 } } });
1323        let rows = layer_rows(&[zero(), zero(), zero()]).unwrap();
1324        assert!(rows[0].floor > 0.0, "{rows:?}");
1325        assert!(rows[0].limit() > 0.0, "{rows:?}");
1326    }
1327
1328    #[test]
1329    fn too_few_layer_reports_fail_like_too_few_clips() {
1330        let report = || serde_json::json!({ "stem": { "rms_dbfs": { "value": -16.0 } } });
1331        let error = layer_rows(&[report(), report()]).unwrap_err();
1332        assert!(matches!(error, SpreadError::Runs(2)), "{error}");
1333        assert!(layer_rows(&[report(), report(), report()]).is_ok());
1334    }
1335
1336    #[test]
1337    fn the_peak_to_rms_distance_ignores_a_gain() {
1338        let base = metrics(0.25, 0.5, 0);
1339        let other = metrics(0.3, 0.4, 0);
1340        let distance = ClipOutput::PeakToRmsDb.distance(&base, &other);
1341        // ±6 dB, near half and twice the level.
1342        for gain in [0.501_187_233_627_272_2, 1.995_262_314_968_879_5] {
1343            let louder = metrics(0.25 * gain, 0.5, 0);
1344            assert!(
1345                (ClipOutput::PeakToRmsDb.distance(&louder, &other) - distance).abs() < 1e-9,
1346                "{gain}"
1347            );
1348            assert!(
1349                ClipOutput::PeakToRmsDb.distance(&louder, &base) < 1e-9,
1350                "{gain}"
1351            );
1352            assert!(ClipOutput::RmsDb.distance(&louder, &base) > 5.9, "{gain}");
1353        }
1354    }
1355
1356    #[test]
1357    fn a_fitted_comparison_refuses_a_spread_without_a_shape_row() {
1358        let clips: Vec<ClipMetrics> = (0..3).map(|_| metrics(0.5, 0.2, 0)).collect();
1359        let mut rows = clip_rows(&clips).unwrap();
1360        rows.retain(|row| row.output != "envelope_distance_db");
1361        let error = compare_fitted(&tone(0.4, 0.2, 0), "the recording", &clips, &rows).unwrap_err();
1362        assert!(
1363            matches!(error, SpreadError::MissingRow("envelope_distance_db")),
1364            "{error}"
1365        );
1366    }
1367
1368    #[test]
1369    fn too_few_clips_or_a_missing_row_fail_the_comparison() {
1370        let clips: Vec<ClipMetrics> = (0..2).map(|_| metrics(0.5, 0.2, 0)).collect();
1371        let error = clip_rows(&clips).unwrap_err();
1372        assert!(matches!(error, SpreadError::Runs(2)), "{error}");
1373        let clips: Vec<ClipMetrics> = (0..3).map(|_| metrics(0.5, 0.2, 0)).collect();
1374        let mut rows = clip_rows(&clips).unwrap();
1375        assert_eq!(rows.len(), ClipOutput::ALL.len());
1376        rows.remove(1);
1377        let error = compare(&clips[0], &clips, &rows).unwrap_err();
1378        assert!(
1379            matches!(error, SpreadError::MissingRow("peak_db")),
1380            "{error}"
1381        );
1382    }
1383
1384    #[test]
1385    fn a_repeated_output_compares_against_the_floor() {
1386        let row = SpreadRow::of_values("x", "dB", &[1.0; 4], LEVEL_FLOOR_DB).unwrap();
1387        assert_eq!((row.spread, row.tolerance), (0.0, 0.0));
1388        assert_eq!(row.adjacent_ratio, None);
1389        assert_eq!(row.limit(), LEVEL_FLOOR_DB);
1390        let one_sample = ClipOutput::DurationS.floor(SampleRate::new(48_000).unwrap());
1391        assert!((one_sample - 1.0 / 48_000.0).abs() < 1e-15);
1392    }
1393
1394    #[test]
1395    fn a_clip_at_another_sample_rate_does_not_compare() {
1396        let renders: Vec<ClipMetrics> = (0..3).map(|_| metrics(0.5, 0.2, 0)).collect();
1397        let rows = clip_rows(&renders).unwrap();
1398        let samples = vec![0.5; 4800];
1399        let other = ClipMetrics::of(
1400            &Audio::new(SampleRate::new(48_000).unwrap(), vec![samples]).unwrap(),
1401            "c",
1402        )
1403        .unwrap();
1404        let error = compare(&other, &renders, &rows).unwrap_err();
1405        assert!(matches!(error, SpreadError::SampleRate { .. }), "{error}");
1406    }
1407}