Skip to main content

max / audiofiles

6.7 KB · 182 lines History Blame Raw
1 //! The 35-feature vector: assembly and versioning.
2 //!
3 //! [`FeatureInput`] collects the cheap DSP measurements (9 spectral/waveform
4 //! scalars) plus MFCC means and variances (26) and flattens them into the vector
5 //! persisted to `sample_features`. That vector is the foundation every tag layer
6 //! reads: user-authored rules, exemplar k-NN, the distilled head, and `.afcl`
7 //! bundles.
8 //!
9 //! This module used to also hold `SampleClass` and the priority-ordered threshold
10 //! tree that produced one. Both are gone (see `docs/ml_classifier.md`): the tree
11 //! measured 33.4% strict accuracy with two of its seven drum classes unreachable
12 //! by any rule, and the measurements in the wiki note `af-browse-axes` found the
13 //! reason it could not be tuned into working. Instrument identity is not in these
14 //! features. Coarse family structure is, and one unfitted threshold reads it at
15 //! 92.4%, so browsing moved to continuous measured axes and instrument names to
16 //! filename rules. Nothing computes a single categorical label any more.
17
18 use super::mfcc::MfccFeatures;
19 use super::spectral::SpectralFeatures;
20
21 /// Number of features in the feature vector.
22 pub const NUM_FEATURES: usize = 35;
23
24 /// Version stamp for the feature-extraction layout/params. Bump whenever the
25 /// 35-feature vector's composition changes so persisted vectors (and shared
26 /// `.afcl` bundles) can be invalidated/recomputed rather than silently mixed.
27 ///
28 /// v2: MFCCs switched to power-spectrum input + orthonormal DCT-II scaling, and
29 /// degenerate low mel bands now use their center bin instead of a constant dead
30 /// value (the 26 MFCC dims of the vector changed numerically). Existing libraries
31 /// recompute these via the normal feature backfill on next launch.
32 ///
33 /// v3: `onset_strength` switched to *normalised* spectral flux (loudness-invariant),
34 /// so that dim of the vector changed scale. Same backfill-on-next-launch path.
35 ///
36 /// v4: mel filterbank bin edges are now forced strictly increasing, so adjacent
37 /// low-frequency filters no longer collapse onto the same center bin (which had
38 /// fed duplicated energies into the DCT). The MFCC dims changed numerically;
39 /// backfill-on-next-launch as before.
40 ///
41 /// v5: the spectral centroid, flatness, rolloff, bandwidth and centroid variance
42 /// are now energy-weighted across frames rather than plain frame means. The old
43 /// mean counted a near-silent frame as heavily as the loudest one, so a one-shot's
44 /// long quiet tail (spectrally bright: noise floor, dither, reverb) dominated the
45 /// result. Measured over 60 kicks, the plain mean put 51 above a 3000 Hz centroid
46 /// and the weighted mean none. Five dims of the vector changed scale;
47 /// backfill-on-next-launch as before.
48 pub const FEATURE_VERSION: u32 = 5;
49
50 /// Input bundle for the feature vector: every measurement that lands in it.
51 pub struct FeatureInput {
52 pub duration: f64,
53 pub centroid: f64,
54 pub flatness: f64,
55 pub zcr: f64,
56 pub onset_strength: f64,
57 pub bandwidth: f64,
58 pub centroid_variance: f64,
59 pub crest_factor: f64,
60 pub attack_time: f64,
61 pub mfcc_means: [f64; 13],
62 pub mfcc_variances: [f64; 13],
63 }
64
65 impl FeatureInput {
66 /// Build from spectral features + waveform measurements (no MFCCs).
67 pub fn new(
68 features: &SpectralFeatures,
69 duration: f64,
70 crest_factor: f64,
71 attack_time: f64,
72 ) -> Self {
73 Self::with_mfccs(
74 features,
75 duration,
76 crest_factor,
77 attack_time,
78 &MfccFeatures::default(),
79 )
80 }
81
82 /// Build from spectral features + waveform measurements + MFCCs.
83 pub fn with_mfccs(
84 features: &SpectralFeatures,
85 duration: f64,
86 crest_factor: f64,
87 attack_time: f64,
88 mfccs: &MfccFeatures,
89 ) -> Self {
90 Self {
91 duration,
92 centroid: features.centroid,
93 flatness: features.flatness,
94 zcr: features.zero_crossing_rate,
95 onset_strength: features.onset_strength,
96 bandwidth: features.bandwidth,
97 centroid_variance: features.centroid_variance,
98 crest_factor,
99 attack_time,
100 mfcc_means: mfccs.means,
101 mfcc_variances: mfccs.variances,
102 }
103 }
104
105 /// Flatten to the persisted 35-element vector.
106 ///
107 /// Layout: [0-8] scalar features, [9-21] MFCC means, [22-34] MFCC variances.
108 /// The layout is what `FEATURE_VERSION` stamps, so changing it needs a bump.
109 pub fn to_feature_array(&self) -> [f64; NUM_FEATURES] {
110 let mut arr = [0.0; NUM_FEATURES];
111 arr[0] = self.duration;
112 arr[1] = self.centroid;
113 arr[2] = self.flatness;
114 arr[3] = self.zcr;
115 arr[4] = self.onset_strength;
116 arr[5] = self.bandwidth;
117 arr[6] = self.centroid_variance;
118 arr[7] = self.crest_factor;
119 arr[8] = self.attack_time;
120 arr[9..22].copy_from_slice(&self.mfcc_means);
121 arr[22..35].copy_from_slice(&self.mfcc_variances);
122 arr
123 }
124 }
125
126 #[cfg(test)]
127 mod tests {
128 use super::*;
129
130 #[test]
131 #[allow(
132 clippy::float_cmp,
133 reason = "exact equality on deterministic test values"
134 )]
135 fn feature_array_layout() {
136 let input = FeatureInput {
137 duration: 1.0,
138 centroid: 2.0,
139 flatness: 3.0,
140 zcr: 4.0,
141 onset_strength: 5.0,
142 bandwidth: 6.0,
143 centroid_variance: 7.0,
144 crest_factor: 8.0,
145 attack_time: 9.0,
146 mfcc_means: [10.0; 13],
147 mfcc_variances: [20.0; 13],
148 };
149 let arr = input.to_feature_array();
150 assert_eq!(arr[0], 1.0); // duration
151 assert_eq!(arr[1], 2.0); // centroid
152 assert_eq!(arr[8], 9.0); // attack_time
153 assert_eq!(arr[9], 10.0); // mfcc_mean[0]
154 assert_eq!(arr[21], 10.0); // mfcc_mean[12]
155 assert_eq!(arr[22], 20.0); // mfcc_var[0]
156 assert_eq!(arr[34], 20.0); // mfcc_var[12]
157 }
158
159 #[test]
160 #[allow(
161 clippy::float_cmp,
162 reason = "exact equality on deterministic test values"
163 )]
164 fn new_defaults_mfccs_to_zero() {
165 // `new` is the no-MFCC path (spectral stage ran, MFCC stage did not). The
166 // 26 MFCC dims must be present and zeroed rather than absent, so the
167 // vector is always NUM_FEATURES long and stays k-NN-safe.
168 let features = SpectralFeatures {
169 centroid: 600.0,
170 flatness: 0.15,
171 rolloff: 1200.0,
172 zero_crossing_rate: 0.04,
173 onset_strength: 50.0,
174 ..Default::default()
175 };
176 let arr = FeatureInput::new(&features, 0.3, 5.0, 0.003).to_feature_array();
177 assert_eq!(arr.len(), NUM_FEATURES);
178 assert!(arr[9..].iter().all(|v| *v == 0.0));
179 assert_eq!(arr[1], 600.0);
180 }
181 }
182