auracle_features/vet.rs
1//! The vetting gate: safety layer 1.
2//!
3//! *No candidate is ever played live unvetted.* The standard-phrase render is
4//! inspected **before** normalization; failures are quarantined — never
5//! auditioned, never featurized, and reported to the session layer so
6//! evolution learns to avoid the region (safety layer 2).
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11/// Why a render was quarantined.
12#[derive(Clone, Debug, PartialEq, Error, Serialize, Deserialize)]
13pub enum VetFailure {
14 /// A non-finite sample survived every DSP-level defense.
15 #[error("render contains non-finite samples")]
16 NonFinite,
17 /// Effectively silent (below the RMS floor / loudness gate).
18 #[error("render is effectively silent (rms {rms:.2e})")]
19 Silent {
20 /// Measured RMS.
21 rms: f64,
22 },
23 /// Peak beyond the limiter ceiling plus overshoot headroom — runaway.
24 #[error("render peak {peak:.2} exceeds the safety ceiling")]
25 Overlevel {
26 /// Measured peak.
27 peak: f64,
28 },
29 /// Dominated by DC offset rather than audio.
30 #[error("render is DC-dominated (|mean|/rms = {dc_ratio:.2})")]
31 DcDominated {
32 /// |mean| / RMS ratio.
33 dc_ratio: f64,
34 },
35}
36
37/// Measurements taken on the raw (pre-normalization) render.
38#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
39pub struct VetReport {
40 /// Peak absolute sample.
41 pub peak: f64,
42 /// Whole-phrase RMS.
43 pub rms: f64,
44 /// |mean| / RMS — DC dominance.
45 pub dc_ratio: f64,
46 /// Fraction of samples pinned near the limiter ceiling (heavy limiting
47 /// indicator; informational, not a failure).
48 pub pinned_fraction: f64,
49}
50
51/// Vet thresholds. Defaults are deliberately lenient — the gate exists to
52/// catch pathology, not to encode taste (that's the model's job).
53///
54/// **Re-checked against the v2 palette's drive modules and left unchanged.**
55/// A gate tuned before distortion existed is exactly the kind that starts
56/// quarantining a whole timbre as pathology, so the three thresholds were
57/// measured over the full cross of `{soft, hard, tube} × drive
58/// {0.3, 0.6, 0.85, 1.0} × {saw, square, supersaw}`, plus a stacked
59/// fold → tube drive → resonant ladder chain:
60///
61/// - **peak** never exceeded 2.00 against a 3.5 ceiling. quiver's shapers all
62/// normalize into the ±1 domain and rescale, so the module is bounded at
63/// ±5 V *by construction* however hard it is driven — drive buys harmonics,
64/// not level.
65/// - **|mean|/rms** never exceeded 0.0016 against a 0.6 limit, because
66/// `compile::makes_dc` puts a blocker in front of every tube-mode patch.
67/// Without it the same renders measure 1–8% — still nowhere near the
68/// threshold, which is the point: this gate was never the thing protecting
69/// the feature extractor from that offset.
70/// - **rms** stayed far above the floor; distortion raises level, it cannot
71/// silence a patch.
72///
73/// So no threshold moved. The one that would have needed to, had the module
74/// not been bounded, is `peak_ceiling`.
75#[derive(Clone, Copy, Debug)]
76pub struct VetConfig {
77 /// RMS below this is "silent".
78 pub rms_floor: f64,
79 /// Peak above this is runaway (limiter ceiling 0.8 + generous headroom).
80 pub peak_ceiling: f64,
81 /// |mean|/rms above this is DC-dominated.
82 pub max_dc_ratio: f64,
83}
84
85impl Default for VetConfig {
86 fn default() -> Self {
87 Self {
88 rms_floor: 1e-4,
89 peak_ceiling: 2.0,
90 max_dc_ratio: 0.6,
91 }
92 }
93}
94
95impl VetConfig {
96 /// Defaults with the peak ceiling scaled for the phrase's polyphony.
97 ///
98 /// The default ceiling (2.0) is one limiter-bounded voice (~1.5 peak in
99 /// the ±1.0 float domain) plus overshoot headroom. N gate-synced voices
100 /// legitimately sum toward N× one voice, and that summing is exactly the
101 /// stacking information the chord segment exists to measure — so each
102 /// additional simultaneous voice raises the ceiling by one voice's worth
103 /// (1.5) rather than the gate quarantining honest polyphony as runaway.
104 pub fn for_spec(spec: &crate::phrase::PhraseSpec) -> Self {
105 Self {
106 peak_ceiling: 2.0 + 1.5 * (spec.max_voices() as f64 - 1.0),
107 ..Self::default()
108 }
109 }
110}
111
112/// Inspect a raw render. `Ok(report)` admits the candidate to normalization
113/// and feature extraction; `Err` quarantines it.
114pub fn vet(samples: &[f64], cfg: &VetConfig) -> Result<VetReport, VetFailure> {
115 if samples.is_empty() {
116 return Err(VetFailure::Silent { rms: 0.0 });
117 }
118 if samples.iter().any(|s| !s.is_finite()) {
119 return Err(VetFailure::NonFinite);
120 }
121 let n = samples.len() as f64;
122 let peak = samples.iter().fold(0.0f64, |p, s| p.max(s.abs()));
123 let mean = samples.iter().sum::<f64>() / n;
124 let rms = (samples.iter().map(|s| s * s).sum::<f64>() / n).sqrt();
125 let dc_ratio = mean.abs() / (rms + 1e-30);
126 let pinned_fraction = if peak > 0.0 {
127 samples.iter().filter(|s| s.abs() >= 0.98 * peak).count() as f64 / n
128 } else {
129 0.0
130 };
131
132 let report = VetReport {
133 peak,
134 rms,
135 dc_ratio,
136 pinned_fraction,
137 };
138 if rms < cfg.rms_floor {
139 return Err(VetFailure::Silent { rms });
140 }
141 if peak > cfg.peak_ceiling {
142 return Err(VetFailure::Overlevel { peak });
143 }
144 if dc_ratio > cfg.max_dc_ratio {
145 return Err(VetFailure::DcDominated { dc_ratio });
146 }
147 Ok(report)
148}