Skip to main content

max / audiofiles

Hold the bundled layer behind an off-by-default feature The layer as generated is drums only: clap, cymbal, hihat, kick, snare, tom, percussion, and nothing else. Asked about a bass or a vocal it answers with the nearest drum it knows. Nothing measures how often that is right, either -- the accuracy bench scores BPM and key, and class accuracy went away with the retired sample-class label -- so "good enough to ship" currently has no meter. A default classifier that is confidently wrong on a user's library is worse than no default: it puts wrong tags on their samples with the app's authority behind it. So the whole path stays built and tested, and stops at the point of being shipped. `bundled-layer` gates the module, so a default build compiles neither the import nor the 1.1 MiB blob, and audiofiles-browser forwards the feature to gate the DirectBackend::new call. Verified both ways: 678 core tests with it off, 685 with it on, and "Official drum classifier" appears in the rlib only with the feature. ConfigKey::OfficialLayerImported stays declared unconditionally. The registry is the sync contract and unknown keys fail closed on import, so gating the row would have a default build silently drop the key from a peer built with the feature on. The remove-confirmation branch keys on `source == BUNDLED_SOURCE` rather than on `kind == "official"`. kind is free text out of an imported file's manifest, so any .afcl can call itself official, and that one does come back from its file -- telling its owner otherwise would be wrong. The constant moves to afcl.rs, which is compiled either way. docs/ml_classifier.md was stale in both directions: it said no .afcl exists in the repo and ExportOptions has no kind field, both untrue since 46717b0. It now records what exists, that it is off, and the two things that have to land before it flips.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-06 14:32 UTC
Signed with PGP, not checked
Commit: 6047f3b46efc8c0005fd15f9c7867e67d737a856
Parent: 6d51212
11 files changed, +94 insertions, -30 deletions
M Cargo.lock +4 -4
@@ -7297,10 +7297,6 @@
7297 7297 "winnow 1.0.4",
7298 7298 ]
7299 7299
7300 - [[patch.unused]]
7301 - name = "supernote-push"
7302 - version = "0.1.0"
7303 -
7304 7300 [[patch.unused]]
7305 7301 name = "kberg"
7306 7302 version = "0.1.0"
@@ -7309,6 +7305,10 @@
7309 7305 name = "painhours"
7310 7306 version = "0.1.0"
7311 7307
7308 + [[patch.unused]]
7309 + name = "supernote-push"
7310 + version = "0.1.0"
7311 +
7312 7312 [[patch.unused]]
7313 7313 name = "docengine"
7314 7314 version = "0.4.0"
@@ -127,27 +127,44 @@
127 127 Today: no model and no third-party data. Every number the classifier uses is either
128 128 computed from the user's audio at analysis time or written by hand as a threshold.
129 129
130 - That is changing, and the change is deliberate. Layer A ships empty and Layer B needs the
131 - user's own labels, so a new library starts with nothing to match against. The intended fix
132 - is a bundled official classifier layer: a `kind = "official"` `.afcl` built from the
133 - Reverb Drum Machines corpus, imported on first run and weighted below anything the user
130 + Layer A ships empty and Layer B needs the user's own labels, so a new library starts with
131 + nothing to match against. The intended fix is a bundled official classifier layer: a
132 + `kind = "official"` `.afcl`, imported on first run and weighted below anything the user
134 133 labels themselves.
135 134
136 - That layer will put data derived from a third-party dataset in the binary. The terms it
137 - rests on:
135 + That layer puts data derived from a third-party dataset in the binary. The terms it rests
136 + on:
138 137
139 138 - The corpus is CC-BY 4.0, which permits derivatives.
140 139 - Attribution travels with the artifact in `AfclManifest.license_note`, which the UI
141 - surfaces.
140 + surfaces. It credits the dataset the labels came from (`training_source` in the corpus
141 + manifest), not everything the corpus holds.
142 142 - An `.afcl` carries no audio. Each exemplar is the 35-number feature vector, 26 of which
143 143 are MFCC means and variances, plus its labels. The vector is not reversible to audio.
144 144
145 - The corpus is drum-machine one-shots, so an official layer built from it has no exemplars
146 - for bass, vocal, pad, synth, texture, ambience, or foley. Those categories fall back to
147 - the user's own labels.
145 + ### Status: built, not shipped
148 146
149 - No `.afcl` is in the repo yet. The import side is complete and gated on `feat_version`;
150 - nothing produces an official layer, and `ExportOptions` has no `kind` field to mark one.
147 + The whole path exists and is tested. `scripts/corpus.py` lays out the labelled corpus,
148 + `cargo run --release -p audiofiles-bench -- afcl` turns it into a layer,
149 + `crates/audiofiles-core/assets/official.afcl` is the checked-in artifact, and
150 + `analysis::official` embeds it and imports it once per vault.
151 +
152 + It is behind the **`bundled-layer` cargo feature, which is off**, so no shipped build
153 + carries it. Two things have to land before that flips:
154 +
155 + 1. **The layer is drums only.** Built from Reverb Drum Machines one-shots, so it has
156 + exemplars for clap, cymbal, hihat, kick, snare, tom and percussion and none for bass,
157 + vocal, pad, synth, texture, ambience or foley. It would answer for a bass with the
158 + nearest drum it knows. NSynth (CC-BY 4.0, ground-truth `instrument_family_str`) is the
159 + candidate source for the pitched families; it is 16 kHz mono against the drums' 44.1
160 + kHz, and five of the 35 features are Nyquist-bounded, so the two cannot simply be mixed
161 + without the classifier learning sample rate as a proxy for instrument.
162 + 2. **Nothing measures whether the layer is any good.** The accuracy bench scores BPM and
163 + key only; class accuracy went away with the retired sample-class label. A default
164 + classifier that is confidently wrong is worse than no default, so shipping needs a
165 + held-out measurement and a threshold agreed before the number is read.
166 +
167 + Both are tracked in the audiofiles GoingsOn project.
151 168
152 169 ## Feature vector
153 170
@@ -7,6 +7,9 @@
7 7 [features]
8 8 default = ["device-profiles"]
9 9 device-profiles = ["dep:audiofiles-rhai", "dep:rayon"]
10 + # Seed a fresh vault with the bundled official classifier layer. Off; see
11 + # audiofiles-core's feature of the same name for why.
12 + bundled-layer = ["audiofiles-core/bundled-layer"]
10 13
11 14 [dependencies]
12 15 audiofiles-core = { workspace = true }
@@ -7,6 +7,13 @@
7 7 [features]
8 8 default = ["analysis"]
9 9 analysis = ["dep:bs1770", "dep:realfft"]
10 + # The bundled official classifier layer. OFF: the layer is drums-only and
11 + # unmeasured, and a default classifier that is wrong on a user's library is worse
12 + # than none, because it puts wrong tags on their samples with the app's authority
13 + # behind it. Off also means the 1.1 MiB embedded blob is not compiled in.
14 + # Turn back on when the corpus is broadened and the layer measures well enough to
15 + # ship; see the audiofiles GoingsOn project.
16 + bundled-layer = []
10 17
11 18 [dependencies]
12 19 rusqlite = { workspace = true }
@@ -110,6 +110,12 @@
110 110 /// removed it on. The value is the version rather than a bare flag so a
111 111 /// later `FEATURE_VERSION` bump can tell a stale bundled layer from a
112 112 /// current one without a second key.
113 + ///
114 + /// Only written by builds with the `bundled-layer` feature, which is off, so
115 + /// nothing writes it today. Declared unconditionally anyway: this registry is
116 + /// the sync contract, and unknown keys fail closed on import, so gating the
117 + /// row behind the feature would have a default build silently drop this key
118 + /// from a peer that has the feature on.
113 119 OfficialLayerImported => "official_layer_imported": Synced,
114 120
115 121 // --- Local: local paths and safety gates. Never sync these. ---
@@ -7,13 +7,14 @@
7 7 /// on-disk SQLite database. Each test gets full isolation.
8 8 fn make_state() -> (BrowserState, tempfile::TempDir) {
9 9 let dir = tempfile::TempDir::new().unwrap();
10 - // Skip the bundled official layer. A real fresh vault imports it (that is
11 - // what `bundled_official_layer_is_imported_on_first_run` covers), but here it
12 - // would put 1,049 exemplars and a second layer into every test library, so
13 - // every layer-count and exemplar-count assertion would be measuring the
14 - // bundle rather than what the test set up. Pre-setting the marker on the
15 - // vault's database is what `official::ensure_imported` reads, so opening it
16 - // once before the backend does is enough.
10 + // Skip the bundled official layer. A `bundled-layer` build imports it into
11 + // every fresh vault (that is what `bundled_official_layer_is_imported_on_first_run`
12 + // covers), which here would put 1,049 exemplars and a second layer into every
13 + // test library, so every layer-count and exemplar-count assertion would be
14 + // measuring the bundle rather than what the test set up. Pre-setting the
15 + // marker on the vault's database is what `official::ensure_imported` reads,
16 + // so opening it once before the backend does is enough. Unconditional so the
17 + // suite behaves identically with the feature on or off.
17 18 {
18 19 let db = audiofiles_core::db::Database::open(dir.path().join("audiofiles.db")).unwrap();
19 20 db.set_config(
@@ -28,6 +29,7 @@
28 29 }
29 30
30 31 /// A state whose vault gets the real first-run treatment, bundled layer included.
32 + #[cfg(feature = "bundled-layer")]
31 33 fn make_state_first_run() -> (BrowserState, tempfile::TempDir) {
32 34 let dir = tempfile::TempDir::new().unwrap();
33 35 let shared = Arc::new(SharedState::new());
@@ -2260,6 +2262,7 @@
2260 2262 }
2261 2263
2262 2264 #[test]
2265 + #[cfg(feature = "bundled-layer")]
2263 2266 fn bundled_official_layer_is_imported_on_first_run() {
2264 2267 // Covers the wiring rather than the mechanism (`analysis::official` has
2265 2268 // the mechanism's tests): opening a fresh vault is what triggers it, and
@@ -1067,7 +1067,13 @@
1067 1067 // user to re-import one would send them looking for
1068 1068 // something that does not exist. It is imported once and
1069 1069 // stays removed on every device.
1070 - let warning = if layer.kind == "official" {
1070 + //
1071 + // Keyed on `source`, not on `kind == "official"`: kind is
1072 + // free text read straight out of an imported file's
1073 + // manifest, so a third-party .afcl can declare itself
1074 + // official, and that one DOES come back from its file.
1075 + // `source` is set by us at import and identifies the bundle.
1076 + let warning = if layer.source.as_deref() == Some("bundled") {
1071 1077 "Removing deletes this layer's exemplars. The bundled layer is added \
1072 1078 once, so this will not come back."
1073 1079 } else {
@@ -41,6 +41,16 @@
41 41 /// imported exemplars fill gaps and break ties without overriding the user's labels.
42 42 pub const DEFAULT_IMPORT_WEIGHT: f64 = 0.5;
43 43
44 + /// `source` recorded on the bundled layer's row, and the only reliable way to tell it
45 + /// from a file the user imported.
46 + ///
47 + /// Not a path: nothing on disk corresponds to it. Lives here rather than beside the
48 + /// bundled layer itself ([`super::official`]) because that module is behind the
49 + /// `bundled-layer` feature while readers of this value are not, and because `kind` is
50 + /// the wrong test for the same question: it is free text out of an imported file's
51 + /// manifest, so any `.afcl` can call itself `official`.
52 + pub const BUNDLED_SOURCE: &str = "bundled";
53 +
44 54 // File format
45 55
46 56 /// `.afcl` manifest. `feat_version` MUST match the importing app's [`FEATURE_VERSION`].
@@ -17,6 +17,7 @@
17 17 #[cfg(feature = "analysis")]
18 18 pub mod loudness;
19 19 pub mod mfcc;
20 + #[cfg(feature = "bundled-layer")]
20 21 pub mod official;
21 22 pub mod spectral;
22 23 pub mod suggest;