Skip to main content

max / audiofiles

Clean up docs: remove completed archive, condense todo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-13 21:58 UTC
Commit: dd4f6b1ebaea6d8c90f19e7e2a5f4353d7afdd6f
Parent: 718c4ad
3 files changed, +8 insertions, -601 deletions
@@ -1,454 +0,0 @@
1 - # audiofiles — Completed Work
2 -
3 - Archived completed sections from todo.md. All items below are done.
4 -
5 - ## Phase 0: Project Setup
6 -
7 - ### Done
8 - - [x] Init Cargo workspace (audiofiles-core, audiofiles-plugin, audiofiles-app, xtask)
9 - - [x] audiofiles-core: pure library crate, no async, no UI (rusqlite, sha2, thiserror)
10 - - [x] audiofiles-plugin: nih-plug CLAP/VST3 plugin crate with egui editor placeholder
11 - - [x] audiofiles-app: standalone binary (placeholder only)
12 - - [x] Set up SQLite with rusqlite + migrations (8 tables, 7 indexes, PRAGMA user_version tracking)
13 - - [x] xtask bundler for CLAP/VST3 packaging (`cargo xtask bundle audiofiles-plugin`)
14 - - [x] 4 tests: schema creation, migration versioning, idempotency, FK enforcement
15 - - [x] CI — `.build.yml` for builds.sr.ht (check, test, clippy, audit)
16 -
17 - ## Phase 1: Core Library — Sample Store & VFS
18 -
19 - ### Done
20 - - [x] CoreError type + Result alias + helpers (error.rs)
21 - - [x] Content-addressed store: import file → SHA-256 → copy to `{hash}.{ext}` (store.rs)
22 - - [x] Deduplication: skip copy when hash already exists
23 - - [x] Sample remove: delete file + row, CASCADE handles refs
24 - - [x] VFS CRUD: create/delete/rename VFS roots (vfs.rs)
25 - - [x] VFS node CRUD: create/delete/rename/move directories and sample links
26 - - [x] VFS node query: list children (dirs first), breadcrumb trail
27 - - [x] Root-level name conflict detection (SQLite NULL UNIQUE workaround)
28 - - [x] Tag CRUD: add/remove/query tags on samples (tags.rs)
29 - - [x] Tag query: find_by_tag, list_tag_names, list_tag_values
30 - - [x] 23 unit tests (4 db + 4 store + 10 vfs + 5 tags)
31 - - [x] Ratatui TUI app for interactive testing (app.rs, ui.rs, main.rs)
32 - - [x] TUI: vim-style nav, import files/dirs, mkdir, tag, rename, delete, cycle VFS
33 - - [x] Collections CRUD: create/delete collections, add/remove members (schema + 11 files implemented)
34 -
35 - ## Phase 2: CLAP Plugin Shell
36 -
37 - ### Done
38 - - [x] Plugin struct with SharedState bridge between audio + GUI threads
39 - - [x] BrowserState: DB, Store, VFS navigation ported from TUI app (state.rs)
40 - - [x] Audio preview: symphonia decode to interleaved stereo f32 (preview.rs)
41 - - [x] Preview playback: fill_output() with try_lock for audio thread safety
42 - - [x] egui editor: single-panel file browser (breadcrumb, file list, footer) (editor.rs)
43 - - [x] VFS selector dropdown (ComboBox to switch VFS roots)
44 - - [x] Breadcrumb navigation (clickable path segments)
45 - - [x] Keyboard navigation: Up/Down/J/K, Enter/Right enter dir, Backspace/Left go up, Space toggle preview
46 - - [x] Play buttons per sample row + double-click to preview
47 - - [x] Copy-path-to-clipboard button (ctx.copy_text)
48 - - [x] Plugin state persistence: db-path persisted via nih-plug #[persist]
49 - - [x] Default data dir via `dirs` crate (platform data_dir/audiofiles)
50 - - [x] CLAP + VST3 bundle builds successfully
51 -
52 - ### Done (UX Overhaul)
53 - - [x] Three-panel layout (sidebar + columnar file list + detail panel)
54 - - [x] Sortable columns: BPM, key, duration, classification, name
55 - - [x] Detail panel with waveform + metadata + tag management
56 - - [x] Tag editor in detail panel (add/remove tags on selected sample)
57 - - [x] Search bar with folder/global scope toggle
58 - - [x] Dark theme applied via egui Visuals
59 -
60 - ## Phase 3: Import Pipeline
61 -
62 - Store is always flat (content-addressed blobs by SHA-256 hash). VFS controls how samples appear to the user.
63 -
64 - ### Done
65 - - [x] Folder import: background worker walks directory, hashes + copies + creates VFS nodes (import.rs)
66 - - [x] Progress reporting: pre-walk file count → progress bar, per-file updates, cancel support
67 - - [x] Import dialog in plugin UI: "Import Folder" button in breadcrumb bar, rfd folder picker, progress screen
68 - - [x] Drag-and-drop import: directories use background worker, single files remain synchronous
69 - - [x] Analysis flow wired into import: auto-transitions to ConfigureAnalysis after import completes
70 -
71 - ### Done (Phase 3 completion)
72 - - [x] Import options UI: flat / new VFS / merge into existing VFS — ConfigureImport screen with radio buttons, VFS name editor, merge VFS picker (editor.rs, state.rs)
73 - - [x] Post-import tag prompt: TagFolders screen with per-folder tag input, real-time validation, Apply/Skip buttons (editor.rs, state.rs)
74 - - [x] Duplicate detection feedback: ImportFileResult enum, duplicates counter through worker, status shows "X duplicates skipped" (import.rs, state.rs)
75 - - [x] ImportStrategy enum (Flat/NewVfs/MergeIntoVfs), ImportedFolder struct, import_directory_flat(), import_structured() (import.rs)
76 - - [x] Drag-and-drop directories use MergeIntoVfs strategy (main.rs), "Import Folder" button shows config screen
77 - - [x] 84 tests pass, full workspace compiles cleanly
78 -
79 - ## Phase 4: Audio Analysis
80 -
81 - ### Done
82 - - [x] Analysis module structure (analysis/ with 10 submodules)
83 - - [x] Symphonia → mono f32 decode for analysis (analysis/decode.rs)
84 - - [x] Basic loudness: peak_db, rms_db (analysis/basic.rs, 4 tests)
85 - - [x] LUFS loudness via bs1770 ITU-R BS.1770-4 (analysis/loudness.rs, 2 tests)
86 - - [x] Spectral features via realfft STFT: centroid, flatness, rolloff, ZCR, onset strength (analysis/spectral.rs, 4 tests)
87 - - [x] BPM + key detection via stratum-dsp analyze_audio (analysis/bpm.rs, 2 tests)
88 - - [x] Rule-based classification: 12 classes (Kick, Snare, HiHat, Cymbal, Percussion, Bass, Vocal, Synth, Pad, Fx, Noise, Music) (analysis/classify.rs, 6 tests)
89 - - [x] Loop detection: cross-correlation + beat alignment heuristic (analysis/loop_detect.rs, 4 tests)
90 - - [x] Tag suggestion engine: AnalysisResult → Vec<TagSuggestion> with confidence + reason (analysis/suggest.rs, 7 tests)
91 - - [x] AnalysisConfig: 7 toggleable analyses, all default true (analysis/config.rs)
92 - - [x] Background worker thread: mpsc channels, progress reporting, cancellation (analysis/worker.rs)
93 - - [x] analyze_sample() orchestrator + save_analysis() DB writer (analysis/mod.rs)
94 - - [x] DB migration 003: added lufs, spectral_flatness, spectral_rolloff, zero_crossing_rate, classification columns
95 - - [x] Plugin state: ImportMode enum (None/ConfigureAnalysis/Analyzing/ReviewSuggestions), ReviewItem, SuggestionState
96 - - [x] Plugin state: start_analysis_flow, run_analysis, poll_analysis, cancel_analysis, apply_accepted_suggestions
97 - - [x] Plugin UI: configure analysis screen (checkboxes per analysis type)
98 - - [x] Plugin UI: analysis progress screen (progress bar, current file, cancel)
99 - - [x] Plugin UI: review/audit screen (two-panel: sample list + tag suggestions with checkboxes, accept/reject all, apply)
100 - - [x] CLAP + VST3 bundles build
101 -
102 - ### Done (post-Phase 4)
103 - - [x] Wire analysis flow into import pipeline (auto-transition in poll_import Complete handler)
104 -
105 - ### Done (UX Overhaul)
106 - - [x] Waveform data generation: downsampled peak pairs stored as f32 blob in waveform_data table (DB migration 004)
107 - - [x] Waveform rendering in egui detail panel (custom Painter-based widget with click-to-seek)
108 -
109 - ## Phase 5: Search & Filtering
110 -
111 - ### Done (UX Overhaul)
112 - - [x] Multi-dimensional filter query builder: BPM range, key, duration, classification, tags (search.rs)
113 - - [x] Full-text search across sample name (search bar in toolbar, folder/global scope)
114 - - [x] Filter panel UI: BPM range, duration range, 12 classification checkboxes, key selector, tag pills (ui/filter_panel.rs)
115 - - [x] Sort by analysis column: Name, BPM, Key, Duration, Classification (click headers, ascending/descending toggle)
116 - - [x] Multi-select: Cmd+Click toggle, Shift+Click range, Cmd+A select all (Selection struct in state.rs)
117 - - [x] Context menus: right-click sample (Preview, Copy Path, Delete), right-click folder (Open, Delete)
118 - - [x] Enriched file list query: LEFT JOIN vfs_nodes + audio_analysis (list_children_enriched in vfs.rs)
119 -
120 - ### Done (Phase 5 completion)
121 - - [x] Key compatibility filter (compatible_keys() in search.rs, KeyFilterMode::Compatible, filter_panel toggle)
122 - - [x] Smart folders: saved filter queries, stored as JSON in DB (smart_folders.rs, smart_folders table)
123 - - [x] Smart folder sidebar section in plugin UI (sidebar.rs, collapsible section)
124 - - [x] Similarity search: feature vector distance (weighted Euclidean on analysis columns, similarity.rs)
125 - - [x] "Find similar" context menu action on any sample (file_list.rs, state.find_similar())
126 - - [x] Near-duplicate detection: peak envelope fingerprint comparison (fingerprint.rs, DB migration 006, "Find Duplicates" context menu)
127 -
128 - ## Phase 6: Bulk Operations & Polish
129 -
130 - ### Done
131 - - [x] Keyboard shortcuts (j/k navigate, space preview, enter open, / search, Cmd+A select all, Shift+Arrow range select)
132 - - [x] Right-click context menus throughout
133 - - [x] DB migration 005: user_config table, transaction() helper on Database
134 - - [x] Rename pattern engine: {name}, {ext}, {bpm}, {key}, {class}, {duration}, {n}/{nn}/{nnn} tokens, separator collapsing, dedup (rename.rs)
135 - - [x] Bulk core functions: collect_subtree, list_all_directories (recursive CTEs), restore_node, bulk_add_tag, bulk_remove_tag
136 - - [x] Undo system: UndoOp enum (BulkDelete/BulkMove/BulkRename/BulkTagAdd/BulkTagRemove), 50-deep stack, Cmd+Z shortcut, toolbar undo button
137 - - [x] Bulk tag modal: add/remove tag to multi-selection, validation, sample name list (Cmd+T shortcut)
138 - - [x] Bulk move modal: scrollable directory picker with full paths, move to root or subfolder
139 - - [x] Bulk rename modal: pattern input with token chips, live preview table (old→new), error display (F2 shortcut)
140 - - [x] Multi-select context menu: Tag.../Move to.../Rename.../Copy Paths/Delete (right-click when >1 selected)
141 - - [x] Bulk delete with confirmation: DeleteMultiple variant, snapshot subtrees+tags for undo
142 - - [x] Column customization: ColumnConfig with show/hide for Classification, BPM, Key, Duration, Peak dB, Tags; persisted to user_config table
143 - - [x] Merged icon into Name column, eliminated empty vertical gutters in file list
144 - - [x] 21 new tests (10 rename, 5 vfs bulk, 4 tags bulk, 2 db transaction) → 109 total
145 -
146 - ## Phase 7: Export
147 -
148 - Raw filesystem export + Rhai plugin-based device export (community/device-maker extensible profiles).
149 -
150 - Requires: Phase 4 format detection (sample_rate, channels in audio_analysis table)
151 -
152 - ### Done — Raw Export
153 - - [x] Export VFS subtree to real filesystem (preserving directory structure)
154 - - [x] Export flattened with naming pattern
155 - - [x] Export with metadata sidecar (.audiofiles.json per sample)
156 - - [x] Hardlink optimization for same-filesystem export (transparent fallback to copy)
157 - - [x] Export dialog in plugin UI (destination, format, channels, structure, sidecar, progress)
158 - - [x] Single-item Export... in context menus (sample + directory)
159 -
160 - ### Done — Core Export Engine (`audiofiles-core/src/export/`)
161 - - [x] DeviceProfile, AudioConstraints, NamingRules, ExportConstraints types (export/profile.rs)
162 - - [x] ChannelConstraint, NamingCase enums (export/profile.rs)
163 - - [x] DeviceProfileSummary for UI listing (export/profile.rs)
164 - - [x] ExportFormat::Aiff variant + AIFF dispatch in export_single_item (export/mod.rs)
165 - - [x] AIFF encoder: big-endian PCM, 80-bit extended sample rate, 16/24-bit (export/encode_aiff.rs)
166 - - [x] Filename sanitizer: case, separator, stripping, truncation (export/sanitize.rs)
167 - - [x] device_profile field on ExportConfig (export/mod.rs)
168 - - [x] AIFF radio button in export UI (ui/export_screens.rs)
169 -
170 - ### Done — Rhai Plugin Runtime (`crates/audiofiles-rhai/`)
171 - - [x] audiofiles-rhai crate (rhai sync feature, toml, serde, thiserror, audiofiles-core)
172 - - [x] PluginError enum: ManifestParse, ManifestMissing, ManifestInvalid, ScriptCompile, ScriptRuntime, Io (error.rs)
173 - - [x] Sandboxed Rhai engine: 100K ops, 32-level calls, 10K string, no print/debug (engine.rs)
174 - - [x] TOML manifest parsing → DeviceProfile conversion (manifest.rs)
175 - - [x] RhaiSampleInfo + RhaiExportContext wrapper types with exported getter modules (types.rs)
176 - - [x] Host API: pad, truncate, case, replace, strip, format_index, file_stem/ext (host_api.rs)
177 - - [x] CompiledHooks + hook runners: validate_sample, transform_filename, pre/post_export (hooks.rs)
178 - - [x] Filesystem plugin discovery: scan dirs for manifest.toml (loader.rs)
179 - - [x] PluginRegistry: case-insensitive lookup, user-overrides-bundled, sorted listing (registry.rs)
180 - - [x] Bundled plugin embedding via include_str! (bundled.rs)
181 - - [x] create_registry() public API: bundled + user plugins from ~/.config/audiofiles/plugins/user/ (lib.rs)
182 - - [x] Backend trait: list_device_profiles() method
183 - - [x] DirectBackend: PluginRegistry behind device-profiles feature flag
184 - - [x] IPC: device_profiles.list method constant
185 -
186 - ### Done — Bundled Device Plugins (`crates/audiofiles-rhai/plugins/bundled/`)
187 - Each plugin is a `manifest.toml`. All declarative, no Rhai scripts needed.
188 - - [x] Dirtywave M8 (m8/) — 44.1k, 16/24-bit, mono, 127-char lower
189 - - [x] Elektron Digitakt (digitakt/) — 48k, 16-bit, mono, 64-char lower
190 - - [x] Elektron Digitakt II (digitakt_ii/) — 48k, 16-bit, both, 64-char lower
191 - - [x] Roland SP-404 MKII (sp404mkii/) — 44.1/48k, 16/24-bit, both, 12-char upper, 128MB limit
192 - - [x] Akai MPC One/Live (mpc/) — 44.1/48k, 16/24-bit, both, 64-char lower
193 - - [x] Polyend Tracker (polyend_tracker/) — 44.1k, 16-bit, both, 8-char upper
194 - - [x] Synthstrom Deluge (deluge/) — 44.1k, 16/24-bit, both, WAV/AIFF, 64-char original
195 - - [x] 1010music Blackbox (blackbox/) — 48k, 16/24-bit, both, 64-char lower
196 - - [x] Korg Volca Sample 2 (volca_sample_2/) — 31.25k, 16-bit, mono, 100-slot limit
197 - - [x] TE OP-1 / OP-1 Field (op1/) — 44.1k, 16-bit, mono, AIFF, 11-char lower
198 - - [x] Elektron Octatrack (octatrack/) — 44.1k, 16/24-bit, both, WAV/AIFF, 64-char upper
199 - - [x] Elektron Model:Samples (model_samples/) — 48k, 16-bit, mono, 64-char lower
200 - - [x] Novation Circuit Rhythm (circuit_rhythm/) — 44.1/48k, 16-bit, both, 32-char lower
201 - - [x] NI Maschine+ (maschine_plus/) — 44.1/48k, 16/24-bit, both, 128-char original
202 -
203 - ### Done — Pipeline Integration
204 - - [x] run_export() applies device profile constraints (format, rate, depth, channels from profile)
205 - - [x] run_export() applies NamingRules via sanitize_filename() when device profile set
206 - - [x] run_export() runs validate_sample hook (reject incompatible samples)
207 - - [x] run_export() runs transform_filename hook (custom naming beyond NamingRules)
208 - - [x] File size limit enforcement (skip/warn if encoded file exceeds max_file_size_bytes)
209 - - [x] Dedup with device-safe `_2`, `_3` suffixes when sanitization creates collisions
210 - - [x] name_overrides field on ExportConfig for hook-transformed names
211 - - [x] resolve_output_names made pub for backend pre-computation
212 -
213 - ### Done — UI
214 - - [x] Device export dialog in plugin UI (profile picker, destination, preview, progress)
215 - - [x] Device profile picker: ComboBox listing bundled + user profiles with manufacturer metadata
216 - - [x] Custom profile option ("None (manual)" hides profile overrides, shows all manual controls)
217 -
218 - ## Phase 8: Standalone App (Done sections)
219 -
220 - Wraps audiofiles-core in a native window, for users who want to manage outside DAW.
221 -
222 - ### Done
223 - - [x] Created audiofiles-browser shared crate (editor, state, preview extracted from plugin)
224 - - [x] Rewired audiofiles-plugin to use audiofiles-browser (editor.rs, state.rs deleted from plugin)
225 - - [x] audiofiles-app binary: eframe + cpal standalone reusing plugin UI panels
226 - - [x] cpal audio output stream for preview playback (audio.rs)
227 - - [x] Native file drag-and-drop import (egui dropped_files)
228 - - [x] CLI file argument import (open files from command line / OS file associations)
229 - - [x] macOS Info.plist with audio file type associations (.wav, .flac, .mp3, .ogg, .aiff)
230 - - [x] Import logic ported from TUI to BrowserState (import_path, import_single_file, import_directory_recursive)
231 - - [x] Old TUI app replaced (ratatui/crossterm removed)
232 - - [x] 84 tests pass, full workspace compiles cleanly
233 -
234 - ### Done (UX Overhaul)
235 - - [x] Editor.rs refactored from 1006→191 lines (thin dispatcher to 10 UI submodules)
236 - - [x] 14 new UI files: ui/{mod,theme,widgets,toolbar,sidebar,file_list,detail,footer,filter_panel,import_screens,overlays}.rs + waveform.rs
237 - - [x] 109 tests pass (9 UX overhaul + 21 Phase 6 bulk ops)
238 -
239 - ## Pre-Launch Fixes (from audit 2026-02-27)
240 -
241 - ### Done
242 - - [x] `CoreError::RenameInvalid` variant, `RenamePattern::parse()` returns typed error (rename.rs, error.rs)
243 - - [x] `import_single_file()` returns `Result<_, CoreError>` instead of String (browser/import.rs)
244 - - [x] `bulk_add_tag`/`bulk_remove_tag` wrapped in explicit transactions (tags.rs)
245 -
246 - ### Done
247 - - [x] Add tests for `state.rs` orchestration logic (95 tests added: selection, bulk ops, import/analysis, navigation, rename, column config, misc)
248 - - [x] `audio.rs` `start_output_stream()` returns typed `AudioError` enum
249 -
250 - ## Audit Follow-Up (2026-02-28)
251 -
252 - ### Done
253 - - [x] Fix 4 clippy warnings in audiofiles-browser: `drop_non_drop`, `clone_on_copy`, two `collapsible_if`
254 - - [x] Escape SQL LIKE wildcards in search.rs and tags.rs via `escape_like()`
255 - - [x] Wrap each migration step in transactions
256 - - [x] Remove unused import in error.rs test module
257 - - [x] Validate hash parameter in `store.rs::sample_path()` (validate_hash checks 64-char lowercase hex)
258 - - [x] Add tests for `audiofiles-plugin` (8 tests) and `audiofiles-app` (5 tests)
259 - - [x] `contents.clone()` in file_list.rs confirmed as Arc clone (cheap reference count bump)
260 -
261 - ## Audit Follow-Up (2026-02-27)
262 -
263 - ### Done
264 - - [x] Add tests for `browser/preview.rs` (7 tests: mono/stereo/multichannel decode, sample rate, error cases)
265 - - [x] Split `state.rs` into directory module (state/mod.rs, navigation.rs, import_workflow.rs, bulk_ops.rs, tests.rs)
266 -
267 - ## Audit Follow-Up (2026-02-28, third audit)
268 -
269 - ### Done — Clippy
270 - - [x] Fix 3 `items_after_test_module`: moved `save_analysis`, `decode_to_mono`, `worker_loop` above their test modules
271 - - [x] Fix 2 `io_other_error` in test code: use `std::io::Error::other()`
272 - - [x] Fix `identity_op` in test: changed `0 + 1` to `1`
273 - - [x] `too_many_arguments`: added `#[allow(clippy::too_many_arguments)]` on test helper
274 -
275 - ## Audit Follow-Up (2026-03-01, test coverage + error handling)
276 -
277 - ### Done — Test Coverage
278 - - [x] Centralized `split_name_ext` from 3 duplicate locations into `util.rs` + 3 tests (normal, no ext, multiple dots)
279 - - [x] Smart folder error tests: 2 tests (rename nonexistent, corrupt JSON graceful fallback)
280 -
281 - ### Done — Error Handling
282 - - [x] Added `CoreError::Serialization(String)` variant — distinct from `Export` for JSON serialization failures
283 - - [x] Fixed smart_folders.rs: `CoreError::Export` → `CoreError::Serialization` for serde_json errors
284 - - [x] Added `eprintln!` warning for corrupt smart folder JSON (was silent `unwrap_or_default`)
285 - - [x] Error handling audit: no other actionable issues (eprintln is correct for CLAP plugin crate, bundled.rs already returns Result)
286 -
287 - ## Phase 7B: MIDI Instrument Mode (2026-03-01)
288 -
289 - ### Done — Core Types (audiofiles-core/src/instrument.rs)
290 - - [x] InstrumentMode enum (Chromatic, MultiSample)
291 - - [x] AdsrEnvelope struct (5ms attack, 100ms decay, sustain 1.0, 50ms release defaults)
292 - - [x] VelocityTarget enum (Volume)
293 - - [x] KeyZone struct (sample_hash, name, root/low/high note, velocity range)
294 - - [x] InstrumentConfig struct (mode, envelope, velocity_target, max_voices=8)
295 - - [x] key_to_root_note(): parse "A minor" → MIDI note 57, all 12 pitch classes + sharps/flats
296 - - [x] note_name(): MIDI note → "C3", "A#4" (C-1=0 convention, C3=48, C4=60)
297 - - [x] 9 tests
298 -
299 - ### Done — Instrument Playback State (audiofiles-browser/src/instrument.rs)
300 - - [x] EnvelopePhase enum (Idle, Attack, Decay, Sustain, Release)
301 - - [x] Voice struct (active, note, velocity, age, fractional position, zone_index, envelope state)
302 - - [x] LoadedZone struct (PreviewBuffer, root/low/high note, velocity range)
303 - - [x] InstrumentPlayback struct (config, zone_buffers, fixed voice pool, note_counter, active, sample_rate)
304 - - [x] Extended SharedState with instrument: Mutex<InstrumentPlayback>
305 - - [x] Extended BrowserState with instrument_visible, instrument_root_note
306 - - [x] 2 tests
307 -
308 - ### Done — MIDI Voice Engine (audiofiles-plugin/src/instrument.rs)
309 - - [x] fill_instrument(): try_lock, drain MIDI events, render voices, return bool
310 - - [x] NoteOn: zone matching, free voice allocation or oldest-note stealing
311 - - [x] NoteOff: move matching voices to Release phase
312 - - [x] Choke: immediately deactivate matching voices
313 - - [x] Pitch shifting: linear interpolation, rate = 2^(semitone/12) * (source_rate/host_rate)
314 - - [x] ADSR envelope: per-sample advance, linear ramps for A/D/R, constant S
315 - - [x] Velocity: gain = envelope_level * velocity (Volume target)
316 - - [x] Plugin integration: MIDI_INPUT = MidiConfig::Basic, instrument priority over preview
317 - - [x] 8 tests
318 -
319 - ### Done — Sample Loading
320 - - [x] load_chromatic_sample(): decode, derive root from analysis, single zone 0-127
321 - - [x] toggle_instrument(): toggle active/visible
322 - - [x] add_instrument_zone(): decode, append zone, set MultiSample mode
323 - - [x] remove_instrument_zone(): remove zone, kill affected voices, fix indices
324 - - [x] Context menu "Play as Instrument" for samples
325 -
326 - ### Done — Keyboard + Panel UI
327 - - [x] I key toggles instrument panel visibility
328 - - [x] Instrument panel: bottom panel (120px), 3 sections
329 - - [x] Mode selector: Chromatic / Multi-sample radio buttons
330 - - [x] Piano keyboard: 3-octave custom painter, active note highlighting, root note dot
331 - - [x] Click-to-set root note on keyboard, octave +/- navigation
332 - - [x] ADSR sliders: logarithmic for time params, A/D 1ms-5s, S 0-1, R 1ms-10s
333 - - [x] Zone bars: colored horizontal bars below keyboard per zone (multi-sample mode)
334 - - [x] Right-click zone to remove
335 - - [x] Drag-and-drop: file list samples → keyboard creates zone (root +/- 6 semitones)
336 -
337 - ### Files Created
338 - - crates/audiofiles-core/src/instrument.rs
339 - - crates/audiofiles-browser/src/instrument.rs
340 - - crates/audiofiles-plugin/src/instrument.rs
341 - - crates/audiofiles-browser/src/ui/instrument_panel.rs
342 -
343 - ### Files Modified
344 - - crates/audiofiles-core/src/lib.rs
345 - - crates/audiofiles-browser/src/lib.rs
346 - - crates/audiofiles-browser/src/state/mod.rs
347 - - crates/audiofiles-browser/src/editor.rs
348 - - crates/audiofiles-browser/src/ui/mod.rs
349 - - crates/audiofiles-browser/src/ui/file_list.rs
350 - - crates/audiofiles-plugin/src/lib.rs
351 -
352 - ### Test Count
353 - - 410 → 429 (19 new: 9 core, 2 browser, 8 plugin)
354 -
355 - ## Phase 9: Cloud Sync (MNW SyncKit)
356 -
357 - ### Done
358 - - [x] Create `crates/audiofiles-sync/` crate (lib.rs, error.rs, service.rs, auth.rs, scheduler.rs)
359 - - [x] Workspace deps: tokio, uuid, base64, chrono, rand; synckit-client path dep
360 - - [x] Migration 007: `sync_state` table, `sync_changelog` table, `vfs.sync_files` column
361 - - [x] 27 triggers across 9 tables (samples, audio_analysis, vfs, vfs_nodes, tags, collections, collection_members, smart_folders, user_config), all guarded by `applying_remote != '1'`
362 - - [x] `service.rs`: table_columns whitelist, FK-safe UPSERT/DELETE ordering, push/pull with `spawn_blocking` (Connection is !Send), composite PK handling, initial snapshot, cleanup
363 - - [x] `auth.rs`: PKCE helpers (verifier/challenge/state), localhost callback server
364 - - [x] `scheduler.rs`: 60s check interval, exponential backoff (2^n min, max 15), auto-sync guards
365 - - [x] `lib.rs`: `SyncManager` public API, `SyncStatus`/`SyncState` types, interior-mutable command channel
366 - - [x] `audiofiles-app/main.rs`: tokio runtime (2 threads), `SyncManager` from env vars (`AF_SYNC_SERVER_URL`, `AF_SYNC_API_KEY`), session restore, scheduler start, `needs_refresh` polling
367 - - [x] `Vfs` struct: `sync_files: bool` field, `set_vfs_sync_files`/`get_vfs_sync_files` in core + Backend trait + DirectBackend
368 - - [x] `sync_panel.rs`: egui Window overlay with 4 states (Disconnected, Authenticating, NeedsEncryption, Ready)
369 - - [x] Ready state: Sync Now, auto-sync toggle, interval selector (5/15/30/60 min), per-VFS "Sync audio files" checkboxes, Disconnect
370 - - [x] Toolbar "Sync" button, Escape dismisses panel
371 - - [x] CLAP plugin updated: passes `None` for sync manager
372 - - [x] 483 tests passing, 0 clippy warnings, DB v7
373 -
374 - ### Alpha Polish (2026-03-08)
375 - - [x] VFS management UI: context menus (rename/delete) on sidebar libraries, "+" button for new library
376 - - [x] Directory management: "New Folder" and "Rename" in directory context menu
377 - - [x] 4 modal dialogs: VFS create, VFS rename, directory create, directory rename (overlays.rs, editor.rs)
378 -
379 - ### Done — Per-VFS sync_files toggle
380 - - [x] Per-VFS sync_files toggle: wired into blob upload/download queries (sync_files=1 WHERE clause)
381 -
382 - ### Testing — Export Pipeline (Done)
383 - - [x] `resolve_output_names()` tests — 9 tests (no pattern, format changes, patterns, name overrides, dedup after sanitization)
384 - - [x] `run_export()` tests — 5 tests (AIFF format, cancel callback, missing source error, sidecar fields, directory structure)
385 -
386 - ### 9F: Sync Without Blobs (Done)
387 - - [x] Post-pull cloud_only marking: `mark_cloud_only_samples()` scans for samples with no local blob, sets cloud_only=1 with trigger suppression
388 - - [x] VFS nodes referencing cloud-only samples show cloud icon, muted text, no preview/playback/export/instrument
389 - - [x] Analysis results sync independently (audio_analysis in sync whitelist, usable for search/filter without local blob)
390 - - [x] Tags and collections work on cloud-only samples (metadata-only operations)
391 - - [x] `cloud_only` field added to `VfsNodeWithAnalysis`, enriched queries JOIN samples table
392 - - [x] 7 new tests (4 mark_cloud_only, 3 enriched query cloud_only)
393 -
394 - ## Audit Action Items (2026-03-11, sixth audit) — All resolved
395 -
396 - - [x] CRITICAL: Clear applying_remote flag on startup (crash recovery -- silent data loss vector) — cleared at top of perform_sync()
397 - - [x] Migrate browser crate from eprintln! to tracing (25 sites across 8 files, appropriate error/warn/debug levels)
398 - - [x] Split import_screens.rs (668 LOC) into directory module (configure.rs, progress.rs, tagging.rs)
399 - - [x] Fix theme include_str! paths (moved 9 theme TOMLs into audiofiles-browser/themes/, paths now 2 levels)
400 - - [x] Add end-to-end integration test (3 tests: full pipeline import->analyze->search->tag->export, analysis roundtrip verification, multi-sample search with combined filters)
401 -
402 - ## Audit Action Items (2026-03-13, seventh audit — pre-launch skeptical lens)
403 -
404 - - [x] Use SmallVec or pre-allocated buffer for MIDI events in audio path (SmallVec<[NoteEvent; 8]> replaces Vec, zero heap allocation for ≤8 events per buffer)
405 - - [x] Add allowlist validation to `query_sample_field` SQL column interpolation (ALLOWED_FIELDS const, returns CoreError::Internal on disallowed field)
406 - - [x] Add hash re-verification method on content store (`SampleStore::verify_sample` — re-hashes file, compares to expected hash, 3 tests)
407 - - [x] Fix test count: 532 tests (was 528, +4 new: allowlist validation, verify match/corruption/missing)
408 -
409 - ## Brand Rename (2026-03-19)
410 -
411 - ### Done
412 - - [x] Rename "AudioFiles" → "audiofiles" in all user-visible strings (window title, tray tooltip, headings, sync panel, auth callback)
413 - - [x] Rename data directory path `.join("AudioFiles")` → `.join("audiofiles")`
414 - - [x] Rename export default directory "AudioFiles Export" → "audiofiles Export"
415 - - [x] Rename CFBundleName in Info.plist
416 - - [x] Rename .app/.zip in release.sh (`AudioFiles.app` → `audiofiles.app`)
417 - - [x] Rename in all doc comments, tracing messages across 6 crates
418 - - [x] Rename in all project docs (CLAUDE.md, launch.md, audit.md, todo.md, content_seed.md, architecture.md, pitches, strategy docs)
419 - - [x] Rust struct `AudioFilesApp` unchanged (PascalCase convention)
420 - - [x] Crate names `audiofiles-*` unchanged (already lowercase)
421 - - [x] Bundle identifier `com.audiofiles.app` unchanged (already lowercase)
422 - - [x] 560 tests pass, cargo check + clippy clean, zero stale references
423 -
424 - ## Deferred (Done)
425 -
426 - - [x] Resolve PreviewBuffer name collision (ipc/protocol.rs renamed to IpcPreviewBuffer)
427 -
428 - ---
429 -
430 - ## Rust Patterns Audit (2026-03-21)
431 -
432 - - [x] Remove unnecessary `codec_params.clone()` in 3 decode paths
433 - - [x] Stop cloning full UpdateStatus struct every UI frame — extract 3 fields under lock
434 - - [x] Deferred breadcrumb mutation — iterate by ref, apply navigation after loop
435 - - [x] Improve HashMap deduplicate in rename.rs — use get_mut to avoid extra clones
436 - - [x] Eliminate double hashes clone in bulk_ops.rs — move into undo op instead of clone
437 - - [x] Reduce import_workflow hash clones — build SampleHash once, reuse for name fallback
438 - - [x] Tag tree sidebar — dot-separated tags rendered as collapsible folder tree (sidebar.rs)
439 - - [x] Remove Linux Wayland drag-out backend (linux.rs deleted, wayland-client/raw-window-handle deps removed)
440 - - [x] Remove unused smallvec dependency
441 - - [x] Fix `!is_ok()` → `is_err()` in vfs_mirror test
442 -
443 - ---
444 -
445 - ## TagTree Integration (2026-03-21)
446 -
447 - - [x] Add `tagtree` workspace dependency
448 - - [x] Replace local `validate_tag()` with `tagtree::validate_with()` (TagConfig: max_depth 5, max_length 100)
449 - - [x] Replace local `escape_like()` with `tagtree::escape_like()` in tags.rs and search.rs
450 - - [x] Replace `find_by_prefix` LIKE pattern with `tagtree::like_descendant_pattern()`
451 - - [x] Replace `remove_tags_by_prefix` LIKE pattern with `tagtree::like_descendant_pattern()`
452 - - [x] Replace `list_children_tags` with `tagtree::children_at_prefix()`
453 - - [x] Remove local `escape_like()` from util.rs (and its 5 tests)
454 - - [x] All 568 tests pass
@@ -130,7 +130,6 @@ Minimal but appropriate doc set for the project's current stage. No inaccuracies
130 130 | Document | Status | Last Verified | Notes |
131 131 |----------|:------:|:-------------:|-------|
132 132 | docs/todo.md | Current | 2026-04-06 | Active task list (Layer 2 classifiers, UX polish) |
133 - | docs/archive/af_todo_done.md | Current | 2026-04-06 | Completed items archive |
134 133 | docs/architecture.md | Current | 2026-03-28 | System design + 5-crate workspace |
135 134 | docs/competition.md | Current | 2026-03-04 | Competitive analysis |
136 135 | docs/human_testing.md | Current | 2026-03-04 | Manual QA checklist |
M docs/todo.md +8 -146
@@ -1,77 +1,24 @@
1 1 # audiofiles TODO
2 2
3 3 ## Status
4 - Done: All pre-beta phases, multi-vault support, UX polish (17A-17E), classification pipeline rework (7-class retrain, smart skip), Layer 2 classifiers (bass+synth trained, vocal deferred), x86_64 Linux distribution. Active: None. Next: Vocal training data sourcing, Phase 17F, sample forge (phases 10-16).
4 + Done: All pre-beta phases. Active: None. Next: Vocal layer 2, sample forge (phases 10-16).
5 5
6 - v0.3.4. Standalone-only. Audit grade A. Two-layer ML classifier (7 drum + 3 bass + 4 synth sub-classes). Smart skip (4-8x faster imports on drum-heavy libraries). 17 bundled themes. Multi-vault support.
7 -
8 - **Scope:** Pre-beta coding complete. All remaining sections are post-beta.
9 -
10 - Completed work archived in `docs/archive/af_todo_done.md`.
6 + v0.3.4. Audit grade A. 611 tests.
11 7
12 8 ---
13 9
14 - ## Classification Pipeline Rework
15 -
16 - STFT reduced from 2048/512 (window/hop) to 1024/1024 (~4x cheaper). SQLite WAL hardened (busy_timeout, checkpoint-on-open, post-import checkpoint). Layer 2 expanded from 5 to 7 drum classes. Layer 1 leak fixes. Retrained on curated Goldbaby data (87.3% CV accuracy, 7 classes).
17 -
18 - ### Done
19 - - [x] SQLite WAL crash fix: busy_timeout, checkpoint-on-open, post-import checkpoint (db.rs, import.rs)
20 - - [x] STFT window 2048→1024, hop 512→1024 no-overlap (spectral.rs, mod.rs, mfcc.rs, audiofiles-train)
21 - - [x] Organize training data: Goldbaby-focused labeler (organize_training.sh), 2,576 samples across 7 classes
22 - - [x] Expand Layer 2 from 5 to 7 drum classes: added Clap and Tom to SampleClass enum + DRUM_CLASSES
23 - - [x] Fix Layer 1 drum detection leaks: noise/bass/vocal/impact/foley/synth rules tightened with duration/crest guards
24 - - [x] Retrain 7-class RF model: 87.3% CV accuracy, per-class F1 82-95%, model 3.9 MB (classify.rs, audiofiles-train)
25 - - [x] Benchmark binary (audiofiles-bench): per-stage timing, throughput, accuracy, resource usage
26 -
27 - - [x] Smart skip: gate BPM/key/loop on classification result (config.rs, mod.rs, UI checkbox)
10 + ## Classification Pipeline — Remaining
28 11
29 - ### Remaining
30 12 - [ ] Accuracy on non-Goldbaby data — evaluate with archive-org-drum-machines and mixed sources
31 13
32 - ### Layer 2 Classifier Roadmap
33 -
34 - Separate RF model per broad class, filename-labeled training data, embedded at compile time.
35 -
36 - #### Bass Layer 2 (3 classes)
37 - - [x] Extract + label bass training data (NSynth test+valid, 4,465 samples → organize_bass.sh)
38 - - [x] Add SampleClass variants: GuitarBass, SynthBass, SubBass (+ tags, from_str)
39 - - [x] Train layer2_bass.json: 99.4% CV accuracy, 1.0 MB model
40 - - Classes: `guitar-bass` (NSynth guitar acoustic/electronic, pitch < 52), `synth-bass` (NSynth bass electronic), `sub-bass` (NSynth bass synthetic)
41 - - Per-class F1: guitar-bass 99.2%, synth-bass 99.9%, sub-bass 99.2%
42 -
43 - #### Vocal Layer 2 (3 classes) — deferred, needs data
14 + ### Vocal Layer 2 (3 classes) — deferred, needs data
44 15 - [ ] Source vocal-chop data (short rhythmic vocal cuts — VocalSet beatbox, Freesound API)
45 16 - [ ] Source vocal-choir data (layered voices — VocalSet choir exercises, choral libraries)
46 - - [x] Add SampleClass variants: VocalChop, VocalPhrase, VocalChoir (+ tags, from_str)
47 17 - [ ] Train layer2_vocal.json, target 85%+ CV accuracy
48 - - Classes: `vocal-chop` (short, rhythmic), `vocal-phrase` (sung/spoken, sustained), `vocal-choir` (layered voices)
49 - - NSynth has 545 vocal samples (all sustained notes → vocal-phrase only). Needs VocalSet + Freesound for chop/choir.
50 -
51 - #### Synth Layer 2 (4 classes)
52 - - [x] Extract + label synth training data (NSynth test+valid, 7,090 samples → organize_synth.sh)
53 - - [x] Add SampleClass variants: SynthLead, SynthStab, SynthPluck, SynthChord (+ tags, from_str)
54 - - [x] Train layer2_synth.json: 99.5% CV accuracy, 2.9 MB model
55 - - Classes: `synth-lead` (NSynth organ), `synth-stab` (NSynth keyboard acoustic), `synth-pluck` (NSynth mallet + keyboard electronic/synthetic), `synth-chord` (NSynth reed)
56 - - Per-class F1: synth-lead 100%, synth-stab 96.5%, synth-pluck 99.3%, synth-chord 100%
57 -
58 - #### Infrastructure
59 - - [x] Multi-model loading: separate OnceLock per model in classify.rs (4 models: drum, bass, vocal, synth)
60 - - [x] Extend classify_ml() to route Bass→bass RF, Vocal→vocal RF, Synth→synth RF (with empty-model fallback)
61 - - [x] Parameterize training binary: `--target drum|bass|vocal|synth [DATA_PATH]`
62 - - [x] Add 10 SampleClass variants: GuitarBass, SynthBass, SubBass, VocalChop, VocalPhrase, VocalChoir, SynthLead, SynthStab, SynthPluck, SynthChord
63 - - [x] Placeholder model files: layer2_bass.json, layer2_vocal.json, layer2_synth.json (empty trees, graceful fallback)
18 + - Classes: vocal-chop, vocal-phrase, vocal-choir
64 19
65 20 ---
66 21
67 - ## Architecture
68 - - Standalone desktop app (eframe + egui + cpal)
69 - - Core library sync-only (no tokio), SQLite via rusqlite
70 - - Content-addressed flat store (SHA-256), multiple VFS trees
71 - - Multi-vault: each vault is self-contained (audiofiles.db + samples/), registry at config_dir
72 - - Audio: symphonia, stratum-dsp (BPM/key), bs1770 (LUFS), realfft (spectral)
73 - - Rhai scripting for export/import plugins
74 -
75 22 ## Phase 7: Export — Device Reference
76 23
77 24 | Device | Rate | Depth | Channels | Format |
@@ -89,24 +36,14 @@ Separate RF model per broad class, filename-labeled training data, embedded at c
89 36
90 37 ## Phase 8: Distribution
91 38
92 - ### Done
93 - - [x] Linux AppImage packaging (aarch64 native + x86_64 cross-compiled on Astra)
94 - - [x] Linux .deb packaging (aarch64 + amd64)
95 - - [x] Windows MSI + standalone EXE (cargo-xwin cross-compile)
96 - - [x] macOS DMG (signed + notarized)
97 - - [x] Pre-upload checklist in distribution.md (7 required artifacts)
98 -
99 39 ### Remaining
100 40 - [ ] Windows standalone installer (Inno Setup or WiX)
101 41 - [ ] Code-sign Windows binaries
102 42 - [ ] Test standalone on Windows
103 43
104 44 ## Phase 10: Plugin Processing
105 - Apply CLAP plugins to samples as a lightweight host. Destructive or render-to-new-file workflow.
106 -
107 - Research complete (CLAP via clack-host, VST3 deferred).
108 45
109 - ### Implementation
46 + ### Remaining
110 47 - [ ] Add `clack-host` + `clack-extensions` git dependencies (pin commit hash)
111 48 - [ ] Plugin scanner: walk standard CLAP paths, cache descriptors in SQLite
112 49 - [ ] Plugin host engine: load → init → activate → process chunks → deactivate → destroy
@@ -117,18 +54,6 @@ Research complete (CLAP via clack-host, VST3 deferred).
117 54 - [ ] Wet/dry mix control + A/B preview (original vs processed)
118 55
119 56 ## Phase 11: Destructive Edits
120 - Built-in sample transformations without external plugins.
121 -
122 - ### Done
123 - - [x] Trim (start/end sliders, render to new sample)
124 - - [x] Fade in/out (linear, logarithmic, S-curve)
125 - - [x] Normalize (peak or LUFS target)
126 - - [x] Reverse
127 - - [x] Gain adjust (dB slider)
128 - - [x] Edit worker (background thread, progress events)
129 - - [x] Edit history (DB table, source→result tracking)
130 - - [x] Result mode (replace original / create sibling, persisted preference)
131 - - [x] Floating sample editor window (all controls visible, waveform + seek)
132 57
133 58 ### Remaining
134 59 - [ ] DC offset removal
@@ -136,8 +61,6 @@ Built-in sample transformations without external plugins.
136 61 - [ ] Mono-to-stereo / stereo-to-mono conversion
137 62
138 63 ## Phase 12: Chop Engine
139 - Slice samples into pieces for drum kits and one-shots.
140 -
141 64 - [ ] Waveform view with draggable slice markers
142 65 - [ ] Auto-chop by transient detection (reuse existing onset analysis)
143 66 - [ ] Auto-chop by equal divisions (2, 4, 8, 16, 32 slices)
@@ -146,8 +69,6 @@ Slice samples into pieces for drum kits and one-shots.
146 69 - [ ] Batch chop: apply same slice template to multiple samples
147 70
148 71 ## Phase 13: Resample and Time-Stretch
149 - Sample rate conversion and time/pitch manipulation.
150 -
151 72 - [ ] Resample to target rate (rubato already in deps — 22.05, 44.1, 48, 96 kHz)
152 73 - [ ] Bit-depth conversion (8, 16, 24, 32-bit)
153 74 - [ ] Time-stretch without pitch change (rubato or new algorithm)
@@ -155,15 +76,11 @@ Sample rate conversion and time/pitch manipulation.
155 76 - [ ] Varispeed (pitch + time together, tape-style)
156 77
157 78 ## Phase 14: Layer and Stack
158 - Combine multiple samples into composites.
159 -
160 79 - [ ] Layer: mix N samples with per-layer gain/pan, render to new sample
161 80 - [ ] Concatenate: join samples end-to-end with crossfade options
162 81 - [ ] Round-robin export: distribute layers into numbered one-shots for sampler instruments
163 82
164 83 ## Phase 15: Batch Forge
165 - Apply operations to multiple samples at once.
166 -
167 84 - [ ] Batch normalize (peak or LUFS target across selection)
168 85 - [ ] Batch resample (convert selection to target rate/depth)
169 86 - [ ] Batch trim silence (auto-detect and trim leading/trailing silence)
@@ -171,69 +88,14 @@ Apply operations to multiple samples at once.
171 88 - [ ] Batch rename (pattern-based: prefix, suffix, numbering)
172 89
173 90 ## Phase 16: Snapshot History
174 - Non-destructive processing history per sample.
175 -
176 91 - [ ] Store processing chain as metadata (operations + parameters)
177 92 - [ ] Undo to any previous snapshot (content-addressed store makes this free — originals never deleted)
178 93 - [ ] Compare snapshots side-by-side (A/B waveform + playback)
179 94 - [ ] Fork: branch from any snapshot to try different processing paths
180 95
181 - ## Phase 17: UX Polish
182 -
183 - Addresses non-STRONG grades from the UX audit (`_meta/uxaudit.md`, 2026-04-04).
184 -
185 - ### 17A: Aesthetic-Usability — egui Visuals Polish
186 - egui defaults are functional but utilitarian. Small visual tweaks add significant perceived quality.
187 -
188 - - [x] In `theme.rs` `apply_theme()`, set `CornerRadius::same(4)` on all widget visuals
189 - - [x] Increase `style.spacing.item_spacing` to `[8, 5]` (less dense)
190 - - [x] Increase `style.spacing.button_padding` to `[6, 3]`
191 - - [ ] Add theme TOML fields for `rounding` and `item_spacing` so themes can override defaults
192 -
193 - ### 17B: Common Region — Detail Panel Whitespace
194 - Sections in the detail panel run together due to tight egui spacing.
195 -
196 - - [x] Increase spacing between waveform and sample name: 8.0 → 12.0
197 - - [x] Increase spacing between metadata grid and separator: 8.0 → 12.0
198 - - [x] Add separator between tags section and action buttons
199 - - [ ] Group metadata grid in a `ui.group()` with rounded background for visual containment
200 -
201 - ### 17C: Active User — VFS First-Run Explanation
202 - Users unfamiliar with virtual file systems may be confused by the "Library" concept.
203 -
204 - - [x] Add `show_vfs_banner` config flag (persisted via `vfs_explained` key)
205 - - [x] Show dismissible banner below "Libraries" heading explaining VFS concept
206 - - [x] Dismiss persists via `set_config("vfs_explained", "1")`
207 -
208 - ### 17D: Zeigarnik / Goal-Gradient — Analysis Coverage Indicator
209 - Users don't know how much of their library is analyzed or how many samples are untagged.
210 -
211 - - [x] Compute analyzed/total counts from in-memory `contents` (no extra DB query)
212 - - [x] Display in footer: "X/Y analyzed" when incomplete, checkmark when all analyzed
213 - - [x] Display "N untagged" when untagged samples exist
214 -
215 - ### 17E: Fitts's Law — Configurable Row Density
216 - 24px rows are standard for audio tools but tight for general use.
217 -
218 - - [x] Add `row_height` setting (default 24.0, range 20.0-32.0, persisted)
219 - - [x] Slider in Settings: "Row Density" with Compact/Normal/Spacious labels
220 - - [x] Replace hardcoded `24.0` in `file_list.rs` with `state.row_height`
221 -
222 - ### 17F: Hick's Law — Filter Panel Progressive Disclosure
223 - 5 filter dimensions behind a hamburger is good but could be better.
224 -
225 - - [ ] Show active filter count on the hamburger toggle: "Filters (2)" when 2 filters are active
226 - - [ ] Collapse unused filter sections by default, expand only the ones the user has interacted with
227 - - [ ] Remember expanded/collapsed state per session
228 -
229 - (`crates/audiofiles-browser/src/ui/` filter-related code)
230 -
231 - ---
232 -
233 96 ## Shared Code Extraction (Cross-Project)
234 - - [ ] Theme loading: deduplicate TOML theme parser across GO/BB/AF
235 - - [ ] Rhai host functions: deduplicate plugin runtime setup across GO/BB/AF
236 - - [ ] FTS5 query building: extract shared SQLite full-text search utilities
97 + - [ ] Updater UI: extract updater.js from GO/BB into shared module
98 + - [ ] Saved queries: unify GO saved views, BB query feeds, AF smart folders
237 99
238 100 ## Key Paths
239 101 ```