Skip to main content

max / audiofiles

Apply rustfmt across the crate Formatting only, no behavior change. cargo check --all-targets passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-21 23:46 UTC
Commit: 49d14a242d094dd8e30de2a4f809d5416d057906
Parent: 9f67ff4
142 files changed, +7928 insertions, -4102 deletions
@@ -3,7 +3,7 @@
3 3 use audiofiles_browser::ui::theme;
4 4 use eframe::egui;
5 5
6 - use super::{AudioFilesApp, AppScreen, SYNC_SERVER_URL};
6 + use super::{AppScreen, AudioFilesApp, SYNC_SERVER_URL};
7 7
8 8 impl AudioFilesApp {
9 9 /// Draw the license activation screen.
@@ -103,8 +103,15 @@ impl AudioFilesApp {
103 103
104 104 let can_activate = !self.activating && !self.license_key_input.trim().is_empty();
105 105 ui.horizontal(|ui| {
106 - let button_text = if self.activating { "Activating\u{2026}" } else { "Activate" };
107 - if ui.add_enabled(can_activate, egui::Button::new(button_text)).clicked() {
106 + let button_text = if self.activating {
107 + "Activating\u{2026}"
108 + } else {
109 + "Activate"
110 + };
111 + if ui
112 + .add_enabled(can_activate, egui::Button::new(button_text))
113 + .clicked()
114 + {
108 115 self.start_activation();
109 116 }
110 117 if self.activating {
@@ -139,10 +146,7 @@ impl AudioFilesApp {
139 146 }
140 147
141 148 ui.add_space(theme::space::MD);
142 - ui.hyperlink_to(
143 - "Get a license key",
144 - "https://makenot.work/store/audiofiles",
145 - );
149 + ui.hyperlink_to("Get a license key", "https://makenot.work/store/audiofiles");
146 150
147 151 ui.add_space(theme::space::XL);
148 152 ui.horizontal(|ui| {
@@ -196,16 +200,15 @@ impl AudioFilesApp {
196 200 browser.settings.license_key_masked = Some(mask_key(&cache.key_code));
197 201 browser.settings.trial_days_remaining = None;
198 202 } else if let Some(ref trial) = self.trial_state {
199 - browser.settings.trial_days_remaining = Some(super::license::trial_days_remaining(trial));
203 + browser.settings.trial_days_remaining =
204 + Some(super::license::trial_days_remaining(trial));
200 205 }
201 206 let mid = &self.machine_id;
202 - browser.settings.machine_id = Some(
203 - if mid.len() > 12 {
204 - format!("{}...{}", &mid[..8], &mid[mid.len()-4..])
205 - } else {
206 - mid.clone()
207 - }
208 - );
207 + browser.settings.machine_id = Some(if mid.len() > 12 {
208 + format!("{}...{}", &mid[..8], &mid[mid.len() - 4..])
209 + } else {
210 + mid.clone()
211 + });
209 212 }
210 213 }
211 214
@@ -289,8 +292,8 @@ pub(crate) fn trial_button_label(trial: Option<&super::license::TrialState>) ->
289 292
290 293 #[cfg(test)]
291 294 mod tests {
292 - use super::*;
293 295 use super::super::license::{ActivationError, TrialState};
296 + use super::*;
294 297
295 298 fn trial_starting_days_ago(days: i64) -> TrialState {
296 299 let start = chrono::Utc::now() - chrono::Duration::days(days);
@@ -304,12 +307,18 @@ mod tests {
304 307
305 308 #[test]
306 309 fn network_error_offers_retry() {
307 - assert_eq!(recovery_affordance(&ActivationError::Network), RecoveryAffordance::Retry);
310 + assert_eq!(
311 + recovery_affordance(&ActivationError::Network),
312 + RecoveryAffordance::Retry
313 + );
308 314 }
309 315
310 316 #[test]
311 317 fn server_error_offers_retry() {
312 - assert_eq!(recovery_affordance(&ActivationError::Server(503)), RecoveryAffordance::Retry);
318 + assert_eq!(
319 + recovery_affordance(&ActivationError::Server(503)),
320 + RecoveryAffordance::Retry
321 + );
313 322 }
314 323
315 324 #[test]
@@ -133,7 +133,10 @@ mod tests {
133 133 fn read_key_file_trims_whitespace() {
134 134 let dir = tempfile::tempdir().unwrap();
135 135 std::fs::write(dir.path().join("sync_api_key"), " key-with-spaces \n").unwrap();
136 - assert_eq!(read_key_file(dir.path()), Some("key-with-spaces".to_string()));
136 + assert_eq!(
137 + read_key_file(dir.path()),
138 + Some("key-with-spaces".to_string())
139 + );
137 140 }
138 141
139 142 #[test]
@@ -5,11 +5,11 @@ use std::sync::Arc;
5 5 use audiofiles_browser::instrument::render_voices;
6 6 use audiofiles_browser::preview::PreviewPlayback;
7 7 use audiofiles_browser::state::SharedState;
8 - use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
9 8 use cpal::Stream;
9 + use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
10 10 use parking_lot::Mutex;
11 - use tracing::instrument;
12 11 use thiserror::Error;
12 + use tracing::instrument;
13 13
14 14 /// Errors from audio output stream setup.
15 15 #[derive(Error, Debug)]
@@ -33,9 +33,7 @@ pub enum AudioError {
33 33 #[instrument(skip_all)]
34 34 pub fn start_output_stream(shared: Arc<SharedState>) -> Result<(Stream, u32, String), AudioError> {
35 35 let host = cpal::default_host();
36 - let device = host
37 - .default_output_device()
38 - .ok_or(AudioError::NoDevice)?;
36 + let device = host.default_output_device().ok_or(AudioError::NoDevice)?;
39 37
40 38 let device_name = device
41 39 .description()
@@ -98,47 +96,46 @@ fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>(
98 96 // recovers instead of going permanently silent.
99 97 let err_shared = Arc::clone(&shared);
100 98
101 - let stream = device
102 - .build_output_stream(
103 - config,
104 - move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
105 - let num_samples = data.len();
106 -
107 - // Backstop for an unexpectedly large block (pre-sized above to
108 - // avoid allocating here on the steady-state path).
109 - if mix_buf.len() < num_samples {
110 - mix_buf.resize(num_samples, 0.0);
111 - }
112 - let buf = &mut mix_buf[..num_samples];
113 -
114 - // Zero the mix buffer
115 - for s in buf.iter_mut() {
116 - *s = 0.0;
117 - }
118 -
119 - // Fill preview audio
120 - fill_preview(&shared.preview, buf, channels, device_sample_rate);
121 -
122 - // Fill instrument audio (additive)
123 - if let Some(mut inst) = shared.instrument.try_lock() {
124 - render_voices(&mut inst, buf, channels, device_sample_rate);
125 - }
126 -
127 - // Convert f32 mix → output format with clamp
128 - for (out, &mix) in data.iter_mut().zip(buf.iter()) {
129 - *out = T::from_sample(mix.clamp(-1.0, 1.0));
130 - }
131 - },
132 - move |err| {
133 - tracing::error!("audio stream error: {err}");
134 - // Signal the app loop to rebuild the stream. Lock-free store only
135 - // — this runs in cpal's error context, never take a lock here.
136 - err_shared
137 - .device_lost
138 - .store(true, std::sync::atomic::Ordering::Relaxed);
139 - },
140 - None,
141 - )?;
99 + let stream = device.build_output_stream(
100 + config,
101 + move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
102 + let num_samples = data.len();
103 +
104 + // Backstop for an unexpectedly large block (pre-sized above to
105 + // avoid allocating here on the steady-state path).
106 + if mix_buf.len() < num_samples {
107 + mix_buf.resize(num_samples, 0.0);
108 + }
109 + let buf = &mut mix_buf[..num_samples];
110 +
111 + // Zero the mix buffer
112 + for s in buf.iter_mut() {
113 + *s = 0.0;
114 + }
115 +
116 + // Fill preview audio
117 + fill_preview(&shared.preview, buf, channels, device_sample_rate);
118 +
119 + // Fill instrument audio (additive)
120 + if let Some(mut inst) = shared.instrument.try_lock() {
121 + render_voices(&mut inst, buf, channels, device_sample_rate);
122 + }
123 +
124 + // Convert f32 mix → output format with clamp
125 + for (out, &mix) in data.iter_mut().zip(buf.iter()) {
126 + *out = T::from_sample(mix.clamp(-1.0, 1.0));
127 + }
128 + },
129 + move |err| {
130 + tracing::error!("audio stream error: {err}");
131 + // Signal the app loop to rebuild the stream. Lock-free store only
132 + // — this runs in cpal's error context, never take a lock here.
133 + err_shared
134 + .device_lost
135 + .store(true, std::sync::atomic::Ordering::Relaxed);
136 + },
137 + None,
138 + )?;
142 139 Ok(stream)
143 140 }
144 141
@@ -327,10 +324,7 @@ mod tests {
327 324
328 325 #[test]
329 326 fn fill_cpal_resamples_96k_to_48k() {
330 - let playback = make_playback(
331 - vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5],
332 - 96000, true,
333 - );
327 + let playback = make_playback(vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5], 96000, true);
334 328 let mut data = vec![0.0f32; 4];
335 329
336 330 fill_cpal_output(&playback, &mut data, 2, 48000);
@@ -98,8 +98,7 @@ pub fn load_license(data_dir: &Path) -> LicenseStatus {
98 98 pub fn save_license(data_dir: &Path, cache: &LicenseCache) -> io::Result<()> {
99 99 let path = data_dir.join("license.json");
100 100 let tmp = data_dir.join("license.json.tmp");
101 - let json = serde_json::to_string_pretty(cache)
102 - .map_err(io::Error::other)?;
101 + let json = serde_json::to_string_pretty(cache).map_err(io::Error::other)?;
103 102 std::fs::write(&tmp, &json)?;
104 103 std::fs::rename(&tmp, &path)
105 104 }
@@ -134,10 +133,22 @@ pub enum ActivationError {
134 133 impl std::fmt::Display for ActivationError {
135 134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 135 match self {
137 - Self::Network => write!(f, "Couldn't reach the activation server. Check your connection and try again."),
138 - Self::Server(code) => write!(f, "The activation server returned an error ({code}). Try again in a few minutes."),
139 - Self::InvalidKey => write!(f, "We didn't recognise that key. Double-check spelling, or get a new one."),
140 - Self::MachineLimit => write!(f, "This key is already in use on another machine. Deactivate it there first."),
136 + Self::Network => write!(
137 + f,
138 + "Couldn't reach the activation server. Check your connection and try again."
139 + ),
140 + Self::Server(code) => write!(
141 + f,
142 + "The activation server returned an error ({code}). Try again in a few minutes."
143 + ),
144 + Self::InvalidKey => write!(
145 + f,
146 + "We didn't recognise that key. Double-check spelling, or get a new one."
147 + ),
148 + Self::MachineLimit => write!(
149 + f,
150 + "This key is already in use on another machine. Deactivate it there first."
151 + ),
141 152 Self::Other(msg) => write!(f, "{msg}"),
142 153 }
143 154 }
@@ -151,7 +162,8 @@ fn classify_server_error(msg: &str) -> ActivationError {
151 162 let lower = msg.to_lowercase();
152 163 if lower.contains("machine") || lower.contains("already activated") || lower.contains("limit") {
153 164 ActivationError::MachineLimit
154 - } else if lower.contains("invalid") || lower.contains("not found") || lower.contains("unknown") {
165 + } else if lower.contains("invalid") || lower.contains("not found") || lower.contains("unknown")
166 + {
155 167 ActivationError::InvalidKey
156 168 } else {
157 169 ActivationError::Other(msg.to_string())
@@ -163,7 +175,11 @@ fn classify_server_error(msg: &str) -> ActivationError {
163 175 /// Sends the key and machine ID to the server for validation. On success the
164 176 /// server records an activation slot; on failure the returned error is
165 177 /// classified for per-class UI messaging.
166 - pub async fn activate_key(server_url: &str, key: &str, machine_id: &str) -> Result<(), ActivationError> {
178 + pub async fn activate_key(
179 + server_url: &str,
180 + key: &str,
181 + machine_id: &str,
182 + ) -> Result<(), ActivationError> {
167 183 let client = reqwest::Client::builder()
168 184 .timeout(std::time::Duration::from_secs(15))
169 185 .build()
@@ -176,18 +192,13 @@ pub async fn activate_key(server_url: &str, key: &str, machine_id: &str) -> Resu
176 192 label: None,
177 193 };
178 194
179 - let resp = client
180 - .post(&url)
181 - .json(&body)
182 - .send()
183 - .await
184 - .map_err(|e| {
185 - if e.is_timeout() || e.is_connect() || e.is_request() {
186 - ActivationError::Network
187 - } else {
188 - ActivationError::Other(format!("Network error: {e}"))
189 - }
190 - })?;
195 + let resp = client.post(&url).json(&body).send().await.map_err(|e| {
196 + if e.is_timeout() || e.is_connect() || e.is_request() {
197 + ActivationError::Network
198 + } else {
199 + ActivationError::Other(format!("Network error: {e}"))
200 + }
201 + })?;
191 202
192 203 if !resp.status().is_success() {
193 204 return Err(ActivationError::Server(resp.status().as_u16()));
@@ -201,7 +212,9 @@ pub async fn activate_key(server_url: &str, key: &str, machine_id: &str) -> Resu
201 212 if parsed.valid {
202 213 Ok(())
203 214 } else {
204 - let msg = parsed.error.unwrap_or_else(|| "Invalid license key".to_string());
215 + let msg = parsed
216 + .error
217 + .unwrap_or_else(|| "Invalid license key".to_string());
205 218 Err(classify_server_error(&msg))
206 219 }
207 220 }
@@ -222,7 +235,11 @@ pub enum DeactivationError {
222 235 }
223 236
224 237 /// Deactivate a license key (best-effort, fire-and-forget).
225 - pub async fn deactivate_key(server_url: &str, key: &str, machine_id: &str) -> Result<(), DeactivationError> {
238 + pub async fn deactivate_key(
239 + server_url: &str,
240 + key: &str,
241 + machine_id: &str,
242 + ) -> Result<(), DeactivationError> {
226 243 let client = reqwest::Client::builder()
227 244 .timeout(std::time::Duration::from_secs(15))
228 245 .build()
@@ -369,14 +386,20 @@ mod tests {
369 386 #[test]
370 387 fn load_missing_returns_unlicensed() {
371 388 let dir = tempfile::tempdir().unwrap();
372 - assert!(matches!(load_license(dir.path()), LicenseStatus::Unlicensed));
389 + assert!(matches!(
390 + load_license(dir.path()),
391 + LicenseStatus::Unlicensed
392 + ));
373 393 }
374 394
375 395 #[test]
376 396 fn load_corrupt_returns_unlicensed() {
377 397 let dir = tempfile::tempdir().unwrap();
378 398 std::fs::write(dir.path().join("license.json"), "not json{{{").unwrap();
379 - assert!(matches!(load_license(dir.path()), LicenseStatus::Unlicensed));
399 + assert!(matches!(
400 + load_license(dir.path()),
401 + LicenseStatus::Unlicensed
402 + ));
380 403 }
381 404
382 405 #[test]
@@ -114,7 +114,9 @@ fn main() -> eframe::Result<()> {
114 114 // rebuilt on device loss (see AudioFilesApp::poll_audio_device).
115 115 let initial_stream = match audio::start_output_stream(shared.clone()) {
116 116 Ok((stream, device_rate, device_name)) => {
117 - shared.device_sample_rate.store(device_rate, std::sync::atomic::Ordering::Relaxed);
117 + shared
118 + .device_sample_rate
119 + .store(device_rate, std::sync::atomic::Ordering::Relaxed);
118 120 *shared.preview_device_name.lock() = Some(device_name);
119 121 Some(stream)
120 122 }
@@ -155,7 +157,13 @@ fn main() -> eframe::Result<()> {
155 157 Box::new(move |cc| {
156 158 audiofiles_browser::ui::theme::setup_fonts(&cc.egui_ctx);
157 159 Ok(Box::new(AudioFilesApp::new(
158 - config_dir, shared, app_tray, update_checker, prefs, runtime, gtk_ok,
160 + config_dir,
161 + shared,
162 + app_tray,
163 + update_checker,
164 + prefs,
165 + runtime,
166 + gtk_ok,
159 167 initial_stream,
160 168 )))
161 169 }),
@@ -165,13 +173,10 @@ fn main() -> eframe::Result<()> {
165 173 // ── API key persistence ──
166 174
167 175 /// Create a SyncManager from a saved or env-provided API key.
168 - fn create_sync_manager(
169 - data_dir: &Path,
170 - runtime: &tokio::runtime::Handle,
171 - ) -> Option<SyncManager> {
176 + fn create_sync_manager(data_dir: &Path, runtime: &tokio::runtime::Handle) -> Option<SyncManager> {
172 177 let api_key = api_key::load_api_key(data_dir)?;
173 - let server_url = std::env::var("AF_SYNC_SERVER_URL")
174 - .unwrap_or_else(|_| SYNC_SERVER_URL.to_string());
178 + let server_url =
179 + std::env::var("AF_SYNC_SERVER_URL").unwrap_or_else(|_| SYNC_SERVER_URL.to_string());
175 180 let config = SyncKitConfig {
176 181 server_url,
177 182 api_key,
@@ -208,7 +213,8 @@ fn resolve_initial_screen(
208 213 license_status: &license::LicenseStatus,
209 214 has_trial: bool,
210 215 ) -> AppScreen {
211 - let licensed_or_trial = matches!(license_status, license::LicenseStatus::Licensed(_)) || has_trial;
216 + let licensed_or_trial =
217 + matches!(license_status, license::LicenseStatus::Licensed(_)) || has_trial;
212 218 match (vault_registry, licensed_or_trial) {
213 219 (Some(_), true) => AppScreen::Browser,
214 220 (Some(_), false) => AppScreen::Activation,
@@ -293,10 +299,13 @@ impl AudioFilesApp {
293 299 }
294 300 };
295 301
296 - let has_active_trial = trial_state.as_ref().is_some_and(|t| license::trial_days_remaining(t) > 0);
302 + let has_active_trial = trial_state
303 + .as_ref()
304 + .is_some_and(|t| license::trial_days_remaining(t) > 0);
297 305 let screen = resolve_initial_screen(&vault_registry, &license_status, has_active_trial);
298 306
299 - let licensed_or_trial = matches!(&license_status, license::LicenseStatus::Licensed(_)) || has_active_trial;
307 + let licensed_or_trial =
308 + matches!(&license_status, license::LicenseStatus::Licensed(_)) || has_active_trial;
300 309
301 310 let (data_dir, browser, error, sync_manager, license_cache) =
302 311 match (&vault_registry, &license_status) {
@@ -305,7 +314,11 @@ impl AudioFilesApp {
305 314 let data_dir = reg.active.clone();
306 315 let _ = std::fs::create_dir_all(&data_dir);
307 316 let sync_manager = create_sync_manager(&data_dir, runtime.handle());
308 - let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_setup::vault_name_for_path(reg, &data_dir));
317 + let (browser, error) = init_browser(
318 + &data_dir,
319 + shared.clone(),
320 + &vault_setup::vault_name_for_path(reg, &data_dir),
321 + );
309 322 (data_dir, browser, error, sync_manager, Some(cache.clone()))
310 323 }
311 324 // Registry exists, unlicensed but in trial → open the active vault
@@ -313,7 +326,11 @@ impl AudioFilesApp {
313 326 let data_dir = reg.active.clone();
314 327 let _ = std::fs::create_dir_all(&data_dir);
315 328 let sync_manager = create_sync_manager(&data_dir, runtime.handle());
316 - let (browser, error) = init_browser(&data_dir, shared.clone(), &vault_setup::vault_name_for_path(reg, &data_dir));
329 + let (browser, error) = init_browser(
330 + &data_dir,
331 + shared.clone(),
332 + &vault_setup::vault_name_for_path(reg, &data_dir),
333 + );
317 334 (data_dir, browser, error, sync_manager, None)
318 335 }
319 336 // Registry exists but unlicensed (deactivated and reactivated)
@@ -394,7 +411,9 @@ impl AudioFilesApp {
394 411 self.audio_stream = None;
395 412 match audio::start_output_stream(self.shared.clone()) {
396 413 Ok((stream, rate, name)) => {
397 - self.shared.device_sample_rate.store(rate, Ordering::Relaxed);
414 + self.shared
415 + .device_sample_rate
416 + .store(rate, Ordering::Relaxed);
398 417 *self.shared.preview_device_name.lock() = Some(name);
399 418 self.shared.device_lost.store(false, Ordering::Relaxed);
400 419 self.audio_stream = Some(stream);
@@ -412,7 +431,9 @@ impl AudioFilesApp {
412 431 fn activate_browser(&mut self) {
413 432 let _ = std::fs::create_dir_all(&self.data_dir);
414 433 self.sync_manager = create_sync_manager(&self.data_dir, self._runtime.handle());
415 - let vault_name = self.vault_registry.as_ref()
434 + let vault_name = self
435 + .vault_registry
436 + .as_ref()
416 437 .map(|r| vault_setup::vault_name_for_path(r, &self.data_dir))
417 438 .unwrap_or_else(|| "Library".to_string());
418 439 let (browser, error) = init_browser(&self.data_dir, self.shared.clone(), &vault_name);
@@ -450,8 +471,14 @@ impl AudioFilesApp {
450 471
451 472 /// Create a BrowserState, returning (Some(browser), None) on success or
452 473 /// (None, Some(error)) on failure.
453 - fn init_browser(data_dir: &Path, shared: Arc<SharedState>, vault_name: &str) -> (Option<BrowserState>, Option<String>) {
454 - let sample_rate = shared.device_sample_rate.load(std::sync::atomic::Ordering::Relaxed) as f32;
474 + fn init_browser(
475 + data_dir: &Path,
476 + shared: Arc<SharedState>,
477 + vault_name: &str,
478 + ) -> (Option<BrowserState>, Option<String>) {
479 + let sample_rate = shared
480 + .device_sample_rate
481 + .load(std::sync::atomic::Ordering::Relaxed) as f32;
455 482 match BrowserState::new(data_dir, shared, sample_rate, vault_name) {
456 483 Ok(mut browser) => {
457 484 // CLI args / OS "Open with": route through the background import
@@ -592,114 +619,121 @@ impl AudioFilesApp {
592 619 let ctx = &ctx;
593 620 // Poll tray menu events
594 621 if let Some(ref tray) = self.tray
595 - && let Some(action) = tray.poll() {
596 - match action {
597 - tray::TrayAction::ShowWindow => {
598 - ctx.send_viewport_cmd(ViewportCommand::Focus);
599 - }
600 - tray::TrayAction::TogglePlayback => {
601 - if let Some(ref mut browser) = self.browser {
602 - browser.toggle_preview();
603 - }
604 - }
605 - tray::TrayAction::Quit => {
606 - ctx.send_viewport_cmd(ViewportCommand::Close);
622 + && let Some(action) = tray.poll()
623 + {
624 + match action {
625 + tray::TrayAction::ShowWindow => {
626 + ctx.send_viewport_cmd(ViewportCommand::Focus);
627 + }
628 + tray::TrayAction::TogglePlayback => {
629 + if let Some(ref mut browser) = self.browser {
630 + browser.toggle_preview();
607 631 }
608 632 }
633 + tray::TrayAction::Quit => {
634 + ctx.send_viewport_cmd(ViewportCommand::Close);
635 + }
609 636 }
637 + }
610 638
611 639 // Update tray tooltip based on playback state
612 640 if let Some(ref tray) = self.tray
613 - && let Some(ref browser) = self.browser {
614 - let playing = browser.shared.preview.lock().playing;
615 - if playing {
616 - tray.set_tooltip(&browser.status);
617 - } else {
618 - tray.set_tooltip("audiofiles");
619 - }
641 + && let Some(ref browser) = self.browser
642 + {
643 + let playing = browser.shared.preview.lock().playing;
644 + if playing {
645 + tray.set_tooltip(&browser.status);
646 + } else {
647 + tray.set_tooltip("audiofiles");
620 648 }
649 + }
621 650
622 651 // Check if sync pulled remote changes → refresh browser contents
623 652 if let Some(ref sync) = self.sync_manager
624 - && sync.status().needs_refresh {
625 - if let Some(ref mut browser) = self.browser {
626 - browser.refresh_vfs_list();
627 - browser.refresh_contents();
628 - }
629 - sync.clear_needs_refresh();
653 + && sync.status().needs_refresh
654 + {
655 + if let Some(ref mut browser) = self.browser {
656 + browser.refresh_vfs_list();
657 + browser.refresh_contents();
630 658 }
659 + sync.clear_needs_refresh();
660 + }
631 661
632 662 // ── Sync setup actions (before draw, so UI sees results this frame) ──
633 663 // ── Vault actions ──
634 664 if let Some(ref mut browser) = self.browser
635 - && let Some(action) = browser.settings.pending_action.take() {
636 - use audiofiles_browser::state::VaultAction;
637 - match action {
638 - VaultAction::SwitchVault(path) => {
639 - self.switch_vault(path);
640 - return;
641 - }
642 - VaultAction::CreateVault { name, path, loose_files } => {
643 - let switch_path = path.clone();
644 - if self.with_vault_registry(|reg| vault::create_vault(reg, &name, &path)) {
645 - self.switch_vault(switch_path);
646 - if loose_files
647 - && let Some(ref mut browser) = self.browser {
648 - let _ = browser.backend.set_config("loose_files", "1");
649 - browser.settings.is_loose_files = true;
650 - }
651 - return;
665 + && let Some(action) = browser.settings.pending_action.take()
666 + {
667 + use audiofiles_browser::state::VaultAction;
668 + match action {
669 + VaultAction::SwitchVault(path) => {
670 + self.switch_vault(path);
671 + return;
672 + }
673 + VaultAction::CreateVault {
674 + name,
675 + path,
676 + loose_files,
677 + } => {
678 + let switch_path = path.clone();
679 + if self.with_vault_registry(|reg| vault::create_vault(reg, &name, &path)) {
680 + self.switch_vault(switch_path);
681 + if loose_files && let Some(ref mut browser) = self.browser {
682 + let _ = browser.backend.set_config("loose_files", "1");
683 + browser.settings.is_loose_files = true;
652 684 }
685 + return;
653 686 }
654 - VaultAction::AddExistingVault { name, path } => {
655 - self.with_vault_registry(|reg| vault::add_existing_vault(reg, &name, &path));
656 - }
657 - VaultAction::RemoveVault(path) => {
658 - self.with_vault_registry(|reg| vault::remove_vault(reg, &path));
659 - }
660 - VaultAction::RenameVault { path, new_name } => {
661 - self.with_vault_registry(|reg| vault::rename_vault(reg, &path, &new_name));
662 - }
663 - VaultAction::RelocateVault { old_path, new_path } => {
664 - // If we're repointing the active vault, switch to the new
665 - // path so the open browser picks up the new location.
666 - let was_active = self
667 - .vault_registry
668 - .as_ref()
669 - .map(|r| r.active == old_path)
670 - .unwrap_or(false);
671 - let ok = self.with_vault_registry(|reg| {
672 - vault::relocate_vault(reg, &old_path, &new_path)
673 - });
674 - if ok && was_active {
675 - self.switch_vault(new_path);
676 - return;
677 - }
687 + }
688 + VaultAction::AddExistingVault { name, path } => {
689 + self.with_vault_registry(|reg| vault::add_existing_vault(reg, &name, &path));
690 + }
691 + VaultAction::RemoveVault(path) => {
692 + self.with_vault_registry(|reg| vault::remove_vault(reg, &path));
693 + }
694 + VaultAction::RenameVault { path, new_name } => {
695 + self.with_vault_registry(|reg| vault::rename_vault(reg, &path, &new_name));
696 + }
697 + VaultAction::RelocateVault { old_path, new_path } => {
698 + // If we're repointing the active vault, switch to the new
699 + // path so the open browser picks up the new location.
700 + let was_active = self
701 + .vault_registry
702 + .as_ref()
703 + .map(|r| r.active == old_path)
704 + .unwrap_or(false);
705 + let ok = self.with_vault_registry(|reg| {
706 + vault::relocate_vault(reg, &old_path, &new_path)
707 + });
708 + if ok && was_active {
709 + self.switch_vault(new_path);
710 + return;
678 711 }
679 - VaultAction::ScanStorage => {
680 - // storage_stats() is a single indexed COUNT/SUM — fast
681 - // enough to run inline; no in-progress flag needed (the
682 - // old one was set and cleared in the same frame, so its
683 - // spinner could never render).
684 - match browser.backend.storage_stats() {
685 - Ok(stats) => {
686 - browser.settings.storage_cache = Some(stats);
687 - browser.settings.storage_cache_at = Some(
688 - std::time::SystemTime::now()
689 - .duration_since(std::time::UNIX_EPOCH)
690 - .map(|d| d.as_secs() as i64)
691 - .unwrap_or(0),
692 - );
693 - }
694 - Err(e) => browser.status = format!("Storage scan failed: {e}"),
712 + }
713 + VaultAction::ScanStorage => {
714 + // storage_stats() is a single indexed COUNT/SUM — fast
715 + // enough to run inline; no in-progress flag needed (the
716 + // old one was set and cleared in the same frame, so its
717 + // spinner could never render).
718 + match browser.backend.storage_stats() {
719 + Ok(stats) => {
720 + browser.settings.storage_cache = Some(stats);
721 + browser.settings.storage_cache_at = Some(
722 + std::time::SystemTime::now()
723 + .duration_since(std::time::UNIX_EPOCH)
724 + .map(|d| d.as_secs() as i64)
725 + .unwrap_or(0),
726 + );
695 727 }
728 + Err(e) => browser.status = format!("Storage scan failed: {e}"),
696 729 }
697 - VaultAction::DeactivateLicense => {
698 - self.deactivate();
699 - return;
700 - }
730 + }
731 + VaultAction::DeactivateLicense => {
732 + self.deactivate();
733 + return;
701 734 }
702 735 }
736 + }
703 737
704 738 // ── VFS Mirror: sync if dirty ──
705 739 if let Some(ref mut browser) = self.browser {
@@ -715,24 +749,24 @@ impl AudioFilesApp {
715 749 MidiAction::RefreshPorts => {
716 750 browser.midi_state.available_ports = midi::list_input_ports();
717 751 }
718 - MidiAction::Connect(idx) => {
719 - match midi::connect(idx, self.shared.clone()) {
720 - Ok(conn) => {
721 - let name = browser.midi_state.available_ports
722 - .get(idx)
723 - .cloned()
724 - .unwrap_or_else(|| format!("Port {idx}"));
725 - browser.midi_state.connected_port = Some(idx);
726 - browser.midi_state.connected_port_name = Some(name);
727 - self.midi_connection = Some(conn);
728 - }
729 - Err(e) => {
730 - tracing::error!("MIDI connect failed: {e}");
731 - browser.midi_state.connected_port = None;
732 - browser.midi_state.connected_port_name = None;
733 - }
752 + MidiAction::Connect(idx) => match midi::connect(idx, self.shared.clone()) {
753 + Ok(conn) => {
754 + let name = browser
755 + .midi_state
756 + .available_ports
757 + .get(idx)
758 + .cloned()
759 + .unwrap_or_else(|| format!("Port {idx}"));
760 + browser.midi_state.connected_port = Some(idx);
761 + browser.midi_state.connected_port_name = Some(name);
762 + self.midi_connection = Some(conn);
734 763 }
735 - }
764 + Err(e) => {
765 + tracing::error!("MIDI connect failed: {e}");
766 + browser.midi_state.connected_port = None;
767 + browser.midi_state.connected_port_name = None;
768 + }
769 + },
736 770 MidiAction::Disconnect => {
737 771 self.midi_connection = None;
738 772 browser.midi_state.connected_port = None;
@@ -937,10 +971,7 @@ impl AudioFilesApp {
937 971 ui.separator();
938 972 ui.add_space(8.0);
939 973 ui.strong("Network");
940 - ui.checkbox(
941 - &mut updated_check_pref,
942 - "Check makenot.work for updates",
943 - );
974 + ui.checkbox(&mut updated_check_pref, "Check makenot.work for updates");
944 975 ui.label(
945 976 egui::RichText::new(
946 977 "When enabled, the app contacts makenot.work on launch and every 6 hours \
@@ -1037,13 +1068,19 @@ mod tests {
1037 1068 let dir = tempfile::tempdir().unwrap();
1038 1069 let reg = Some(make_registry(dir.path()));
1039 1070 let status = license::LicenseStatus::Licensed(make_license_cache());
1040 - assert_eq!(resolve_initial_screen(&reg, &status, false), AppScreen::Browser);
1071 + assert_eq!(
1072 + resolve_initial_screen(&reg, &status, false),
1073 + AppScreen::Browser
1074 + );
1041 1075 }
1042 1076
1043 1077 #[test]
1044 1078 fn initial_screen_licensed_without_registry() {
1045 1079 let status = license::LicenseStatus::Licensed(make_license_cache());
1046 - assert_eq!(resolve_initial_screen(&None, &status, false), AppScreen::VaultSetup);
1080 + assert_eq!(
1081 + resolve_initial_screen(&None, &status, false),
1082 + AppScreen::VaultSetup
1083 + );
1047 1084 }
1048 1085
1049 1086 #[test]
@@ -1051,13 +1088,19 @@ mod tests {
1051 1088 let dir = tempfile::tempdir().unwrap();
1052 1089 let reg = Some(make_registry(dir.path()));
1053 1090 let status = license::LicenseStatus::Unlicensed;
1054 - assert_eq!(resolve_initial_screen(&reg, &status, false), AppScreen::Activation);
1091 + assert_eq!(
1092 + resolve_initial_screen(&reg, &status, false),
1093 + AppScreen::Activation
1094 + );
1055 1095 }
1056 1096
1057 1097 #[test]
1058 1098 fn initial_screen_unlicensed_without_registry() {
1059 1099 let status = license::LicenseStatus::Unlicensed;
1060 - assert_eq!(resolve_initial_screen(&None, &status, false), AppScreen::Activation);
1100 + assert_eq!(
1101 + resolve_initial_screen(&None, &status, false),
1102 + AppScreen::Activation
1103 + );
1061 1104 }
1062 1105
1063 1106 #[test]
@@ -1065,13 +1108,19 @@ mod tests {
1065 1108 let dir = tempfile::tempdir().unwrap();
1066 1109 let reg = Some(make_registry(dir.path()));
1067 1110 let status = license::LicenseStatus::Unlicensed;
1068 - assert_eq!(resolve_initial_screen(&reg, &status, true), AppScreen::Browser);
1111 + assert_eq!(
1112 + resolve_initial_screen(&reg, &status, true),
1113 + AppScreen::Browser
1114 + );
1069 1115 }
1070 1116
1071 1117 #[test]
1072 1118 fn initial_screen_trial_without_registry() {
1073 1119 let status = license::LicenseStatus::Unlicensed;
1074 - assert_eq!(resolve_initial_screen(&None, &status, true), AppScreen::VaultSetup);
1120 + assert_eq!(
1121 + resolve_initial_screen(&None, &status, true),
1122 + AppScreen::VaultSetup
1123 + );
1075 1124 }
1076 1125
1077 1126 // ── License migration ──
@@ -1080,7 +1129,11 @@ mod tests {
1080 1129 fn migrate_license_copies_files() {
1081 1130 let src = tempfile::tempdir().unwrap();
1082 1131 let dst = tempfile::tempdir().unwrap();
1083 - std::fs::write(src.path().join("license.json"), r#"{"key_code":"k","machine_id":"m","activated_at":"t"}"#).unwrap();
1132 + std::fs::write(
1133 + src.path().join("license.json"),
1134 + r#"{"key_code":"k","machine_id":"m","activated_at":"t"}"#,
1135 + )
1136 + .unwrap();
1084 1137 std::fs::write(src.path().join("machine_id"), "mid-123").unwrap();
1085 1138
1086 1139 vault_setup::migrate_license_to_config(dst.path(), src.path());
@@ -1154,13 +1207,19 @@ mod tests {
1154 1207 fn vault_name_for_path_found() {
1155 1208 let dir = tempfile::tempdir().unwrap();
1156 1209 let reg = make_registry(dir.path());
1157 - assert_eq!(vault_setup::vault_name_for_path(&reg, dir.path()), "Library");
1210 + assert_eq!(
1211 + vault_setup::vault_name_for_path(&reg, dir.path()),
1212 + "Library"
1213 + );
1158 1214 }
1159 1215
1160 1216 #[test]
1161 1217 fn vault_name_for_path_not_found() {
1162 1218 let dir = tempfile::tempdir().unwrap();
1163 1219 let reg = make_registry(dir.path());
1164 - assert_eq!(vault_setup::vault_name_for_path(&reg, Path::new("/nonexistent")), "Library");
1220 + assert_eq!(
1221 + vault_setup::vault_name_for_path(&reg, Path::new("/nonexistent")),
1222 + "Library"
1223 + );
1165 1224 }
1166 1225 }
@@ -91,35 +91,33 @@ pub fn list_input_ports() -> Vec<String> {
91 91 pub fn connect(port_index: usize, shared: Arc<SharedState>) -> Result<MidiConnection, MidiError> {
92 92 let midi_in = MidiInput::new("audiofiles-input")?;
93 93 let ports = midi_in.ports();
94 - let port = ports
95 - .get(port_index)
96 - .ok_or(MidiError::PortOutOfRange { idx: port_index, count: ports.len() })?;
94 + let port = ports.get(port_index).ok_or(MidiError::PortOutOfRange {
95 + idx: port_index,
96 + count: ports.len(),
97 + })?;
97 98
98 99 let shared_cb = shared.clone();
99 - let conn = midi_in
100 - .connect(
101 - port,
102 - "audiofiles-midi-in",
103 - move |_timestamp, message, _| {
104 - match parse_note_message(message) {
105 - NoteAction::On { note, velocity } => {
106 - shared_cb.instrument.lock().note_on(note, velocity);
107 - let event = MidiNoteEvent {
108 - note,
109 - velocity,
110 - note_name: note_name(note),
111 - timestamp: Instant::now(),
112 - };
113 - push_note_event_capped(&mut shared_cb.midi_recent_notes.lock(), event);
114 - }
115 - NoteAction::Off { note } => {
116 - shared_cb.instrument.lock().note_off(note);
117 - }
118 - NoteAction::Ignore => {}
119 - }
120 - },
121 - (),
122 - )?;
100 + let conn = midi_in.connect(
101 + port,
102 + "audiofiles-midi-in",
103 + move |_timestamp, message, _| match parse_note_message(message) {
104 + NoteAction::On { note, velocity } => {
105 + shared_cb.instrument.lock().note_on(note, velocity);
106 + let event = MidiNoteEvent {
107 + note,
108 + velocity,
109 + note_name: note_name(note),
110 + timestamp: Instant::now(),
111 + };
112 + push_note_event_capped(&mut shared_cb.midi_recent_notes.lock(), event);
113 + }
114 + NoteAction::Off { note } => {
115 + shared_cb.instrument.lock().note_off(note);
116 + }
117 + NoteAction::Ignore => {}
118 + },
119 + (),
120 + )?;
123 121
124 122 Ok(MidiConnection { _conn: conn })
125 123 }
@@ -143,7 +141,10 @@ mod tests {
143 141 fn parse_note_on() {
144 142 assert_eq!(
145 143 parse_note_message(&[0x90, 60, 100]),
146 - NoteAction::On { note: 60, velocity: 100 }
144 + NoteAction::On {
145 + note: 60,
146 + velocity: 100
147 + }
147 148 );
148 149 }
149 150
@@ -169,7 +170,10 @@ mod tests {
169 170 // Low nibble is the channel; note-on on channel 15 (0x9F) still parses.
170 171 assert_eq!(
171 172 parse_note_message(&[0x9F, 72, 1]),
172 - NoteAction::On { note: 72, velocity: 1 }
173 + NoteAction::On {
174 + note: 72,
175 + velocity: 1
176 + }
173 177 );
174 178 // Note-off on channel 7 (0x87).
175 179 assert_eq!(
@@ -199,7 +203,10 @@ mod tests {
199 203 // A message longer than 3 bytes still parses off the first three.
200 204 assert_eq!(
201 205 parse_note_message(&[0x90, 64, 80, 0xFF, 0xFF]),
202 - NoteAction::On { note: 64, velocity: 80 }
206 + NoteAction::On {
207 + note: 64,
208 + velocity: 80
209 + }
203 210 );
204 211 }
205 212
@@ -208,11 +215,17 @@ mod tests {
208 215 // Full note range 0..=127.
209 216 assert_eq!(
210 217 parse_note_message(&[0x90, 0, 1]),
211 - NoteAction::On { note: 0, velocity: 1 }
218 + NoteAction::On {
219 + note: 0,
220 + velocity: 1
221 + }
212 222 );
213 223 assert_eq!(
214 224 parse_note_message(&[0x90, 127, 127]),
215 - NoteAction::On { note: 127, velocity: 127 }
225 + NoteAction::On {
226 + note: 127,
227 + velocity: 127
228 + }
216 229 );
217 230 }
218 231
@@ -80,7 +80,9 @@ mod tests {
80 80 #[test]
81 81 fn save_then_load_roundtrip() {
82 82 let dir = tempfile::tempdir().unwrap();
83 - let p = Preferences { check_for_updates: false };
83 + let p = Preferences {
84 + check_for_updates: false,
85 + };
84 86 p.save(dir.path());
85 87 let loaded = Preferences::load(dir.path());
86 88 assert!(!loaded.check_for_updates);
@@ -182,11 +182,10 @@ impl UpdateChecker {
182 182
183 183 /// Verify that a download URL points to a trusted domain.
184 184 pub fn is_trusted_download_url(url: &str) -> bool {
185 - const TRUSTED_PREFIXES: &[&str] = &[
186 - "https://makenot.work/",
187 - "https://dist.makenot.work/",
188 - ];
189 - TRUSTED_PREFIXES.iter().any(|prefix| url.starts_with(prefix))
185 + const TRUSTED_PREFIXES: &[&str] = &["https://makenot.work/", "https://dist.makenot.work/"];
186 + TRUSTED_PREFIXES
187 + .iter()
188 + .any(|prefix| url.starts_with(prefix))
190 189 }
191 190
192 191 /// Check the MNW OTA endpoint once.
@@ -239,15 +238,17 @@ async fn check_once(status: &Arc<Mutex<UpdateStatus>>) {
239 238 Ok(body) => match serde_json::from_slice::<UpdateResponse>(&body) {
240 239 Ok(mut update) => {
241 240 if let Ok(remote) = Version::parse(&update.version)
242 - && remote > current && is_trusted_download_url(&update.url) {
243 - tracing::info!("Update available: v{}", update.version);
244 - truncate_chars(&mut update.notes, MAX_NOTES_LEN);
245 - let mut s = status.lock();
246 - s.available = true;
247 - s.version = update.version;
248 - s.notes = update.notes;
249 - s.download_url = update.url;
250 - }
241 + && remote > current
242 + && is_trusted_download_url(&update.url)
243 + {
244 + tracing::info!("Update available: v{}", update.version);
245 + truncate_chars(&mut update.notes, MAX_NOTES_LEN);
246 + let mut s = status.lock();
247 + s.available = true;
248 + s.version = update.version;
249 + s.notes = update.notes;
250 + s.download_url = update.url;
251 + }
251 252 }
252 253 Err(e) => {
253 254 tracing::warn!("Failed to parse update response: {e}");
@@ -429,7 +430,6 @@ mod tests {
429 430
430 431 #[test]
431 432 fn current_version_is_valid_semver() {
432 - Version::parse(CURRENT_VERSION)
433 - .expect("CURRENT_VERSION should be valid semver");
433 + Version::parse(CURRENT_VERSION).expect("CURRENT_VERSION should be valid semver");
434 434 }
435 435 }
@@ -75,9 +75,10 @@ impl AudioFilesApp {
75 75 ui.add_space(theme::space::SM);
76 76 ui.horizontal(|ui| {
77 77 if ui.button("Choose different location...").clicked()
78 - && let Some(path) = rfd::FileDialog::new().pick_folder() {
79 - self.vault_setup_path = Some(path);
80 - }
78 + && let Some(path) = rfd::FileDialog::new().pick_folder()
79 + {
80 + self.vault_setup_path = Some(path);
81 + }
81 82 if is_custom && ui.button("Use default").clicked() {
82 83 self.vault_setup_path = None;
83 84 }
@@ -164,7 +165,9 @@ impl AudioFilesApp {
164 165 where
165 166 F: FnOnce(&mut VaultRegistry) -> Result<(), vault::VaultError>,
166 167 {
167 - let Some(ref mut reg) = self.vault_registry else { return false };
168 + let Some(ref mut reg) = self.vault_registry else {
169 + return false;
170 + };
168 171 if let Err(e) = op(reg) {
169 172 if let Some(ref mut b) = self.browser {
170 173 b.status = format!("Vault error: {e}");
@@ -185,7 +188,13 @@ impl AudioFilesApp {
185 188 browser.settings.list = reg
186 189 .vaults
187 190 .iter()
188 - .map(|v| (v.name.clone(), v.path.clone(), vault::is_vault_reachable(&v.path)))
191 + .map(|v| {
192 + (
193 + v.name.clone(),
194 + v.path.clone(),
195 + vault::is_vault_reachable(&v.path),
196 + )
197 + })
189 198 .collect();
190 199 browser.settings.name = vault_name_for_path(reg, &self.data_dir);
191 200 }
@@ -207,7 +216,11 @@ pub(crate) fn migrate_license_to_config(config_dir: &Path, default_vault: &Path)
207 216 if let Err(e) = std::fs::copy(&src, &dst) {
208 217 tracing::warn!("Failed to migrate {filename} to config_dir: {e}");
209 218 } else {
210 - tracing::info!("Migrated {filename} from {} to {}", src.display(), dst.display());
219 + tracing::info!(
220 + "Migrated {filename} from {} to {}",
221 + src.display(),
222 + dst.display()
223 + );
211 224 }
212 225 }
213 226 }
@@ -55,8 +55,7 @@ fn time_stages(path: &Path) -> Option<(StageTiming, f64, u32)> {
55 55
56 56 // Spectral
57 57 let t = Instant::now();
58 - let (features, magnitude_frames) =
59 - spectral::compute_spectral_features_with_frames(capped, sr);
58 + let (features, magnitude_frames) = spectral::compute_spectral_features_with_frames(capped, sr);
60 59 let spectral_ms = t.elapsed().as_secs_f64() * 1000.0;
61 60
62 61 // MFCC
@@ -149,10 +148,10 @@ fn classify_file(path: &Path) -> Option<(SampleClass, f64)> {
149 148
150 149 let crest = basic::crest_factor(&decoded.samples);
151 150 let attack = basic::attack_time(&decoded.samples, sr);
152 - let (features, magnitude_frames) =
153 - spectral::compute_spectral_features_with_frames(capped, sr);
151 + let (features, magnitude_frames) = spectral::compute_spectral_features_with_frames(capped, sr);
154 152 let mfcc_features = mfcc::compute_mfccs(&magnitude_frames, sr, 1024);
155 - let input = ClassifyInput::with_mfccs(&features, decoded.duration, crest, attack, &mfcc_features);
153 + let input =
154 + ClassifyInput::with_mfccs(&features, decoded.duration, crest, attack, &mfcc_features);
156 155 let result = classify::classify_ml(&input);
157 156 Some((result.class, result.confidence))
158 157 }
@@ -175,9 +174,10 @@ fn collect_audio_files(dir: &Path, limit: Option<usize>) -> Vec<PathBuf> {
175 174 }
176 175 for entry in walkdir(dir) {
177 176 if let Some(lim) = limit
178 - && files.len() >= lim {
179 - break;
180 - }
177 + && files.len() >= lim
178 + {
179 + break;
180 + }
181 181 files.push(entry);
182 182 }
183 183 files
@@ -241,14 +241,46 @@ fn main() {
241 241 // Collect samples from different sources for timing
242 242 let timing_sources: Vec<(&str, PathBuf, Option<usize>)> = vec![
243 243 ("drum one-shots (WAV)", training_dir.join("kick"), Some(200)),
244 - ("drum one-shots (WAV)", training_dir.join("snare"), Some(200)),
245 - ("drum one-shots (WAV)", training_dir.join("hihat"), Some(200)),
246 - ("philharmonia (MP3)", test_suite_dir.join("formats/mp3"), Some(50)),
247 - ("AIFF samples", test_suite_dir.join("formats/aiff"), Some(10)),
248 - ("FLAC samples", test_suite_dir.join("formats/flac"), Some(10)),
249 - ("ambient loops (WAV)", test_suite_dir.join("genres/ambient"), Some(50)),
250 - ("synth loops (WAV)", test_suite_dir.join("genres/synth"), Some(50)),
251 - ("guitar loops (WAV)", test_suite_dir.join("genres/guitar"), Some(50)),
244 + (
245 + "drum one-shots (WAV)",
246 + training_dir.join("snare"),
247 + Some(200),
248 + ),
249 + (
250 + "drum one-shots (WAV)",
251 + training_dir.join("hihat"),
252 + Some(200),
253 + ),
254 + (
255 + "philharmonia (MP3)",
256 + test_suite_dir.join("formats/mp3"),
257 + Some(50),
258 + ),
259 + (
260 + "AIFF samples",
261 + test_suite_dir.join("formats/aiff"),
262 + Some(10),
263 + ),
264 + (
265 + "FLAC samples",
266 + test_suite_dir.join("formats/flac"),
267 + Some(10),
268 + ),
269 + (
270 + "ambient loops (WAV)",
271 + test_suite_dir.join("genres/ambient"),
272 + Some(50),
273 + ),
274 + (
275 + "synth loops (WAV)",
276 + test_suite_dir.join("genres/synth"),
277 + Some(50),
278 + ),
279 + (
280 + "guitar loops (WAV)",
281 + test_suite_dir.join("genres/guitar"),
282 + Some(50),
283 + ),
252 284 ];
253 285
254 286 let mut all_timings: Vec<(StageTiming, f64, u32, String)> = Vec::new();
@@ -260,9 +292,7 @@ fn main() {
260 292 }
261 293 let results: Vec<_> = files
262 294 .par_iter()
263 - .filter_map(|f| {
264 - time_stages(f).map(|(t, dur, sr)| (t, dur, sr, label.to_string()))
265 - })
295 + .filter_map(|f| time_stages(f).map(|(t, dur, sr)| (t, dur, sr, label.to_string())))
266 296 .collect();
267 297 all_timings.extend(results);
268 298 }
@@ -313,7 +343,10 @@ fn main() {
313 343 );
314 344 }
315 345
316 - println!(" {:<16} {:>8} {:>8} {:>8} {:>8} {:>8}", "Stage", "Mean", "P50", "P95", "P99", "Max");
346 + println!(
347 + " {:<16} {:>8} {:>8} {:>8} {:>8} {:>8}",
348 + "Stage", "Mean", "P50", "P95", "P99", "Max"
349 + );
317 350 println!(" {}", "─".repeat(58));
318 351 stats_line("Decode", &mut decode);
319 352 stats_line("Loudness+LUFS", &mut loud);
@@ -333,7 +366,10 @@ fn main() {
333 366 let realtime_ratio = avg_dur * 1000.0 / avg_total;
334 367 println!(" Avg sample duration: {:.2}s", avg_dur);
335 368 println!(" Avg analysis time: {:.1}ms", avg_total);
336 - println!(" Real-time ratio: {:.0}× (analysis is {:.0}× faster than real-time)", realtime_ratio, realtime_ratio);
369 + println!(
370 + " Real-time ratio: {:.0}× (analysis is {:.0}× faster than real-time)",
371 + realtime_ratio, realtime_ratio
372 + );
337 373 println!();
338 374
339 375 // ─────────────────────────────────────────────────────────────
@@ -349,7 +385,10 @@ fn main() {
349 385 ("FLAC", test_suite_dir.join("formats/flac")),
350 386 ];
351 387
352 - println!(" {:<8} {:>6} {:>10} {:>10} {:>10}", "Format", "Files", "Mean(ms)", "P95(ms)", "Max(ms)");
388 + println!(
389 + " {:<8} {:>6} {:>10} {:>10} {:>10}",
390 + "Format", "Files", "Mean(ms)", "P95(ms)", "Max(ms)"
391 + );
353 392 println!(" {}", "─".repeat(50));
354 393
355 394 for (fmt, dir) in &format_dirs {
@@ -370,7 +409,10 @@ fn main() {
370 409 let mean = decode_times.iter().sum::<f64>() / count as f64;
371 410 let p95 = percentile(&mut decode_times, 95.0);
372 411 let max = percentile(&mut decode_times, 100.0);
373 - println!(" {:<8} {:>6} {:>10.2} {:>10.2} {:>10.2}", fmt, count, mean, p95, max);
412 + println!(
413 + " {:<8} {:>6} {:>10.2} {:>10.2} {:>10.2}",
414 + fmt, count, mean, p95, max
415 + );
374 416 }
375 417 println!();
376 418
@@ -411,7 +453,10 @@ fn main() {
411 453 println!(" Files: {} ({} succeeded)", tp_count, tp_ok);
412 454 println!(" Wall time: {:.1}s", tp_elapsed);
413 455 println!(" Throughput: {:.1} files/sec", tp_rate);
414 - println!(" Avg/file: {:.1}ms", tp_elapsed * 1000.0 / tp_ok as f64);
456 + println!(
457 + " Avg/file: {:.1}ms",
458 + tp_elapsed * 1000.0 / tp_ok as f64
459 + );
415 460 println!();
416 461
417 462 // Single-threaded throughput for comparison
@@ -482,7 +527,10 @@ fn main() {
482 527 // Model size
483 528 let model_path = project_root.join("crates/audiofiles-core/models/layer2_drum.json");
484 529 if let Ok(meta) = std::fs::metadata(&model_path) {
485 - println!(" RF model (layer2_drum.json): {:.1} MB on disk, embedded at compile time", meta.len() as f64 / 1_048_576.0);
530 + println!(
531 + " RF model (layer2_drum.json): {:.1} MB on disk, embedded at compile time",
532 + meta.len() as f64 / 1_048_576.0
533 + );
486 534 }
487 535 println!();
488 536
@@ -492,7 +540,15 @@ fn main() {
492 540 println!("━━━ 5. CLASSIFICATION ACCURACY ━━━");
493 541 println!();
494 542
495 - let class_dirs = ["kick", "snare", "hihat", "cymbal", "clap", "tom", "percussion"];
543 + let class_dirs = [
544 + "kick",
545 + "snare",
546 + "hihat",
547 + "cymbal",
548 + "clap",
549 + "tom",
550 + "percussion",
551 + ];
496 552 let max_per_class = 300; // enough for statistical significance, not too slow
497 553
498 554 let mut all_results: Vec<ClassifyResult> = Vec::new();
@@ -525,7 +581,10 @@ fn main() {
525 581 }
526 582
527 583 let total_classified = all_results.len();
528 - println!(" Evaluated {} samples (up to {} per class)", total_classified, max_per_class);
584 + println!(
585 + " Evaluated {} samples (up to {} per class)",
586 + total_classified, max_per_class
587 + );
529 588 println!();
530 589
531 590 // Strict accuracy: predicted class == expected class exactly
@@ -543,13 +602,22 @@ fn main() {
543 602 let drum_acc = drum_correct as f64 / total_classified as f64 * 100.0;
544 603
545 604 println!(" Overall:");
546 - println!(" Strict accuracy (exact class match): {:.1}% ({}/{})", strict_acc, strict_correct, total_classified);
547 - println!(" Layer 1 accuracy (drum detection): {:.1}% ({}/{})", drum_acc, drum_correct, total_classified);
605 + println!(
606 + " Strict accuracy (exact class match): {:.1}% ({}/{})",
607 + strict_acc, strict_correct, total_classified
608 + );
609 + println!(
610 + " Layer 1 accuracy (drum detection): {:.1}% ({}/{})",
611 + drum_acc, drum_correct, total_classified
612 + );
548 613 println!();
549 614
550 615 // Per-class breakdown
551 616 println!(" Per-class (strict):");
552 - println!(" {:<12} {:>6} {:>8} {:>10} {:>10}", "Expected", "N", "Correct", "Accuracy", "Avg Conf");
617 + println!(
618 + " {:<12} {:>6} {:>8} {:>10} {:>10}",
619 + "Expected", "N", "Correct", "Accuracy", "Avg Conf"
620 + );
553 621 println!(" {}", "─".repeat(52));
554 622
555 623 for dir_name in &class_dirs {
@@ -565,7 +633,10 @@ fn main() {
565 633 continue;
566 634 }
567 635 let n = class_results.len();
568 - let correct = class_results.iter().filter(|r| r.predicted == r.expected).count();
636 + let correct = class_results
637 + .iter()
638 + .filter(|r| r.predicted == r.expected)
639 + .count();
569 640 let acc = correct as f64 / n as f64 * 100.0;
570 641 let avg_conf = class_results.iter().map(|r| r.confidence).sum::<f64>() / n as f64;
571 642 println!(
@@ -605,7 +676,10 @@ fn main() {
605 676 }
606 677 print!(" {:>12}", class_names[i]);
607 678 for pred in &class_ids {
608 - let count = class_results.iter().filter(|r| r.predicted == *pred).count();
679 + let count = class_results
680 + .iter()
681 + .filter(|r| r.predicted == *pred)
682 + .count();
609 683 print!(" {:>7}", count);
610 684 }
611 685 let other = class_results
@@ -622,7 +696,11 @@ fn main() {
622 696 .filter(|r| !is_drum_class(r.predicted))
623 697 .collect();
624 698 if !non_drum.is_empty() {
625 - println!(" Samples classified as non-drum: {} ({:.1}%)", non_drum.len(), non_drum.len() as f64 / total_classified as f64 * 100.0);
699 + println!(
700 + " Samples classified as non-drum: {} ({:.1}%)",
701 + non_drum.len(),
702 + non_drum.len() as f64 / total_classified as f64 * 100.0
703 + );
626 704 let mut non_drum_classes: HashMap<&str, usize> = HashMap::new();
627 705 for r in &non_drum {
628 706 *non_drum_classes.entry(r.predicted.as_str()).or_default() += 1;
@@ -643,9 +721,18 @@ fn main() {
643 721
644 722 let edge_cases = [
645 723 ("silent.wav", test_suite_dir.join("edge-cases/silent.wav")),
646 - ("truncated.wav", test_suite_dir.join("edge-cases/truncated.wav")),
647 - ("café-crème.wav", test_suite_dir.join("edge-cases/unicode-name/café-crème.wav")),
648 - ("日本語テスト.wav", test_suite_dir.join("edge-cases/unicode-name/日本語テスト.wav")),
724 + (
725 + "truncated.wav",
726 + test_suite_dir.join("edge-cases/truncated.wav"),
727 + ),
728 + (
729 + "café-crème.wav",
730 + test_suite_dir.join("edge-cases/unicode-name/café-crème.wav"),
731 + ),
732 + (
733 + "日本語テスト.wav",
734 + test_suite_dir.join("edge-cases/unicode-name/日本語テスト.wav"),
735 + ),
649 736 ];
650 737
651 738 for (name, path) in &edge_cases {
@@ -708,7 +795,10 @@ fn main() {
708 795 println!();
709 796
710 797 println!("═══════════════════════════════════════════════════════════════");
711 - println!(" Benchmark complete. {} files analyzed.", n + total_classified);
798 + println!(
799 + " Benchmark complete. {} files analyzed.",
800 + n + total_classified
801 + );
712 802 println!("═══════════════════════════════════════════════════════════════");
713 803 }
714 804
@@ -778,7 +868,9 @@ fn bench_similarity_scaling() {
778 868 for _ in 0..runs {
779 869 let mut best: Vec<f64> = data
780 870 .iter()
781 - .map(|(_, fv)| audiofiles_core::similarity::feature_distance(probe_fv, fv, &weights))
871 + .map(|(_, fv)| {
872 + audiofiles_core::similarity::feature_distance(probe_fv, fv, &weights)
873 + })
782 874 .collect();
783 875 best.sort_by(|a, b| a.total_cmp(b));
784 876 let _ = &best[..best.len().min(20)];
@@ -866,8 +958,7 @@ fn bench_persistence_scaling() {
866 958 // offset the hashes so they don't collide with the batched rows
867 959 insert_analysis_row(&db, size + i);
868 960 }
869 - let per_row_ms = t.elapsed().as_secs_f64() * 1000.0
870 - * (size as f64 / per_row_n as f64); // extrapolate to `size` rows
961 + let per_row_ms = t.elapsed().as_secs_f64() * 1000.0 * (size as f64 / per_row_n as f64); // extrapolate to `size` rows
871 962
872 963 // Load from DB (I/O) then build the index (CPU) — the full cold-start cost.
873 964 let t = Instant::now();
@@ -4,7 +4,6 @@
4 4 use super::*;
5 5
6 6 impl AnalysisBackend for DirectBackend {
7 -
8 7 // --- Analysis ---
9 8
10 9 fn get_analysis(&self, hash: &str) -> BackendResult<Option<AnalysisResult>> {
@@ -45,9 +44,7 @@ impl AnalysisBackend for DirectBackend {
45 44 }
46 45 }
47 46
48 -
49 47 impl RulesBackend for DirectBackend {
50 -
51 48 fn list_rules(&self) -> BackendResult<Vec<audiofiles_core::rules::Rule>> {
52 49 let db = self.db.lock();
53 50 Ok(audiofiles_core::rules::list_rules(&db)?)
@@ -95,9 +92,7 @@ impl RulesBackend for DirectBackend {
95 92 }
96 93 }
97 94
98 -
99 95 impl ConfigBackend for DirectBackend {
100 -
101 96 // --- Config ---
102 97
103 98 fn get_config(&self, key: &str) -> BackendResult<Option<String>> {
@@ -144,4 +139,3 @@ impl ConfigBackend for DirectBackend {
144 139 Ok(vfs::get_vfs_sync_files(&db, id)?)
145 140 }
146 141 }
147 -
@@ -4,7 +4,6 @@
4 4 use super::*;
5 5
6 6 impl SearchBackend for DirectBackend {
7 -
8 7 // --- Search ---
9 8
10 9 fn search_in_folder(
@@ -23,9 +22,7 @@ impl SearchBackend for DirectBackend {
23 22 }
24 23 }
25 24
26 -
27 25 impl ClassifierBackend for DirectBackend {
28 -
29 26 fn ml_auto_apply_all(&self, k: usize) -> BackendResult<usize> {
30 27 use audiofiles_core::analysis::{exemplar, trained_head};
31 28 let db = self.db.lock();
@@ -41,20 +38,26 @@ impl ClassifierBackend for DirectBackend {
41 38
42 39 fn train_classifier_head(&self) -> BackendResult<Option<TrainedHeadInfo>> {
43 40 let db = self.db.lock();
44 - Ok(audiofiles_core::analysis::trained_head::train_and_save(&db)?.map(|h| TrainedHeadInfo {
45 - class_count: h.classes.len(),
46 - exemplar_count: h.exemplar_count,
47 - trained_at: h.trained_at,
48 - }))
41 + Ok(
42 + audiofiles_core::analysis::trained_head::train_and_save(&db)?.map(|h| {
43 + TrainedHeadInfo {
44 + class_count: h.classes.len(),
45 + exemplar_count: h.exemplar_count,
46 + trained_at: h.trained_at,
47 + }
48 + }),
49 + )
49 50 }
50 51
51 52 fn classifier_head_info(&self) -> BackendResult<Option<TrainedHeadInfo>> {
52 53 let db = self.db.lock();
53 - Ok(audiofiles_core::analysis::trained_head::load(&db)?.map(|h| TrainedHeadInfo {
54 - class_count: h.classes.len(),
55 - exemplar_count: h.exemplar_count,
56 - trained_at: h.trained_at,
57 - }))
54 + Ok(
55 + audiofiles_core::analysis::trained_head::load(&db)?.map(|h| TrainedHeadInfo {
56 + class_count: h.classes.len(),
57 + exemplar_count: h.exemplar_count,
58 + trained_at: h.trained_at,
59 + }),
60 + )
58 61 }
59 62
60 63 fn clear_classifier_head(&self) -> BackendResult<()> {
@@ -77,10 +80,10 @@ impl ClassifierBackend for DirectBackend {
77 80 // the render thread. The UI gates to one active job, so at most one stale
78 81 // job is reaping at a time.
79 82 if let Some(old) = self.classifier_worker.lock().take() {
80 - let _ = audiofiles_core::worker_runtime::spawn_detached(
81 - "classifier-reaper",
82 - move || drop(old),
83 - );
83 + let _ =
84 + audiofiles_core::worker_runtime::spawn_detached("classifier-reaper", move || {
85 + drop(old)
86 + });
84 87 }
85 88 let handle = crate::classifier_worker::spawn_classifier_job(db_path, job)
86 89 .map_err(|e| BackendError::Other(format!("failed to spawn classifier worker: {e}")))?;
@@ -94,7 +97,9 @@ impl ClassifierBackend for DirectBackend {
94 97 opts: &audiofiles_core::analysis::afcl::ExportOptions,
95 98 ) -> BackendResult<()> {
96 99 let db = self.db.lock();
97 - Ok(audiofiles_core::analysis::afcl::export_to_path(&db, path, opts)?)
100 + Ok(audiofiles_core::analysis::afcl::export_to_path(
101 + &db, path, opts,
102 + )?)
98 103 }
99 104
100 105 fn import_afcl(
@@ -102,7 +107,9 @@ impl ClassifierBackend for DirectBackend {
102 107 path: &Path,
103 108 ) -> BackendResult<audiofiles_core::analysis::afcl::ImportSummary> {
104 109 let db = self.db.lock();
105 - Ok(audiofiles_core::analysis::afcl::import_from_path(&db, path)?)
110 + Ok(audiofiles_core::analysis::afcl::import_from_path(
111 + &db, path,
112 + )?)
106 113 }
107 114
108 115 fn list_classifier_layers(
@@ -119,12 +126,16 @@ impl ClassifierBackend for DirectBackend {
119 126
120 127 fn set_classifier_layer_enabled(&self, id: &str, enabled: bool) -> BackendResult<()> {
121 128 let db = self.db.lock();
122 - Ok(audiofiles_core::analysis::afcl::set_layer_enabled(&db, id, enabled)?)
129 + Ok(audiofiles_core::analysis::afcl::set_layer_enabled(
130 + &db, id, enabled,
131 + )?)
123 132 }
124 133
125 134 fn set_classifier_layer_weight(&self, id: &str, weight: f64) -> BackendResult<()> {
126 135 let db = self.db.lock();
127 - Ok(audiofiles_core::analysis::afcl::set_layer_weight(&db, id, weight)?)
136 + Ok(audiofiles_core::analysis::afcl::set_layer_weight(
137 + &db, id, weight,
138 + )?)
128 139 }
129 140
130 141 fn accept_ml_tag(&self, hash: &str, tag: &str) -> BackendResult<()> {
@@ -146,7 +157,9 @@ impl ClassifierBackend for DirectBackend {
146 157 #[instrument(skip(self))]
147 158 fn set_tag_policy(&self, tag: &str, review: f64, auto: f64) -> BackendResult<()> {
148 159 let db = self.db.lock();
149 - Ok(audiofiles_core::analysis::exemplar::set_policy(&db, tag, review, auto)?)
160 + Ok(audiofiles_core::analysis::exemplar::set_policy(
161 + &db, tag, review, auto,
162 + )?)
150 163 }
151 164
152 165 fn list_tag_policies(
@@ -166,19 +179,21 @@ impl ClassifierBackend for DirectBackend {
166 179
167 180 fn apply_cluster_tag(&self, members: &[String], tag: &str) -> BackendResult<usize> {
168 181 let db = self.db.lock();
169 - Ok(audiofiles_core::analysis::cluster::apply_cluster_tag(&db, members, tag)?)
182 + Ok(audiofiles_core::analysis::cluster::apply_cluster_tag(
183 + &db, members, tag,
184 + )?)
170 185 }
171 186
172 - fn harvest_folder_labels(
173 - &self,
174 - ) -> BackendResult<Vec<audiofiles_core::harvest::FolderLabel>> {
187 + fn harvest_folder_labels(&self) -> BackendResult<Vec<audiofiles_core::harvest::FolderLabel>> {
175 188 let db = self.db.lock();
176 189 Ok(audiofiles_core::harvest::harvest_folder_labels(&db)?)
177 190 }
178 191
179 192 fn apply_harvested_tag(&self, hashes: &[String], tag: &str) -> BackendResult<usize> {
180 193 let db = self.db.lock();
181 - Ok(audiofiles_core::harvest::apply_harvested_tag(&db, hashes, tag)?)
194 + Ok(audiofiles_core::harvest::apply_harvested_tag(
195 + &db, hashes, tag,
196 + )?)
182 197 }
183 198
184 199 fn remove_tags_by_source(&self, source: &str) -> BackendResult<usize> {
@@ -188,13 +203,13 @@ impl ClassifierBackend for DirectBackend {
188 203
189 204 fn get_waveform(&self, hash: &str) -> BackendResult<Option<WaveformData>> {
190 205 let db = self.db.lock();
191 - Ok(audiofiles_core::analysis::waveform::load_waveform(&db, hash))
206 + Ok(audiofiles_core::analysis::waveform::load_waveform(
207 + &db, hash,
208 + ))
192 209 }
193 210 }
194 211
195 -
196 212 impl SimilarityBackend for DirectBackend {
197 -
198 213 // --- Similarity ---
199 214
200 215 fn start_find_similar(&self, hash: &str, limit: usize) -> BackendResult<()> {
@@ -211,7 +226,9 @@ impl SimilarityBackend for DirectBackend {
211 226 // spin the repaint-while-busy guard forever.
212 227 if !delivered {
213 228 *guard = None;
214 - return Err(BackendError::Other("search worker is not running".to_string()));
229 + return Err(BackendError::Other(
230 + "search worker is not running".to_string(),
231 + ));
215 232 }
216 233 Ok(())
217 234 }
@@ -227,9 +244,10 @@ impl SimilarityBackend for DirectBackend {
227 244 });
228 245 if !delivered {
229 246 *guard = None;
230 - return Err(BackendError::Other("search worker is not running".to_string()));
247 + return Err(BackendError::Other(
248 + "search worker is not running".to_string(),
249 + ));
231 250 }
232 251 Ok(())
233 252 }
234 253 }
235 -
@@ -4,7 +4,6 @@
4 4 use super::*;
5 5
6 6 impl ForgeBackend for DirectBackend {
7 -
8 7 // --- Sample Forge ---
9 8
10 9 #[instrument(skip_all)]
@@ -34,7 +33,9 @@ impl ForgeBackend for DirectBackend {
34 33 // caller doesn't set forge.busy and wedge the repaint guard / busy-guard.
35 34 if !delivered {
36 35 *guard = None;
37 - return Err(BackendError::Other("forge worker is not running".to_string()));
36 + return Err(BackendError::Other(
37 + "forge worker is not running".to_string(),
38 + ));
38 39 }
39 40 Ok(())
40 41 }
@@ -123,11 +124,9 @@ impl ForgeBackend for DirectBackend {
123 124 {
124 125 let mut guard = self.forge_worker.lock();
125 126 if guard.is_none() {
126 - *guard = Some(
127 - audiofiles_core::forge::spawn_forge_worker().map_err(|e| {
128 - BackendError::Other(format!("failed to spawn forge worker: {e}"))
129 - })?,
130 - );
127 + *guard = Some(audiofiles_core::forge::spawn_forge_worker().map_err(|e| {
128 + BackendError::Other(format!("failed to spawn forge worker: {e}"))
129 + })?);
131 130 }
132 131 let handle = guard.as_ref().expect("forge worker present");
133 132 let _ = handle.send(ForgeCommand::Cancel);
@@ -138,7 +137,9 @@ impl ForgeBackend for DirectBackend {
138 137 });
139 138 if !delivered {
140 139 *guard = None;
141 - return Err(BackendError::Other("forge worker is not running".to_string()));
140 + return Err(BackendError::Other(
141 + "forge worker is not running".to_string(),
142 + ));
142 143 }
143 144 }
144 145 *self.forge_pending.lock() = Some(ForgePending::Chop { vfs_id, parent_id });
@@ -175,11 +176,9 @@ impl ForgeBackend for DirectBackend {
175 176 {
176 177 let mut guard = self.forge_worker.lock();
177 178 if guard.is_none() {
178 - *guard = Some(
179 - audiofiles_core::forge::spawn_forge_worker().map_err(|e| {
180 - BackendError::Other(format!("failed to spawn forge worker: {e}"))
181 - })?,
182 - );
179 + *guard = Some(audiofiles_core::forge::spawn_forge_worker().map_err(|e| {
180 + BackendError::Other(format!("failed to spawn forge worker: {e}"))
181 + })?);
183 182 }
184 183 let handle = guard.as_ref().expect("forge worker present");
185 184 let _ = handle.send(ForgeCommand::Cancel);
@@ -191,7 +190,9 @@ impl ForgeBackend for DirectBackend {
191 190 });
192 191 if !delivered {
193 192 *guard = None;
194 - return Err(BackendError::Other("forge worker is not running".to_string()));
193 + return Err(BackendError::Other(
194 + "forge worker is not running".to_string(),
195 + ));
195 196 }
196 197 }
197 198 *self.forge_pending.lock() = Some(ForgePending::Conform {
@@ -203,4 +204,3 @@ impl ForgeBackend for DirectBackend {
203 204 Ok(())
204 205 }
205 206 }
206 -
@@ -67,7 +67,13 @@ impl VfsBackend for DirectBackend {
67 67 sample_hash: &str,
68 68 ) -> BackendResult<NodeId> {
69 69 let db = self.db.lock();
70 - Ok(vfs::create_sample_link(&db, vfs_id, parent_id, name, sample_hash)?)
70 + Ok(vfs::create_sample_link(
71 + &db,
72 + vfs_id,
73 + parent_id,
74 + name,
75 + sample_hash,
76 + )?)
71 77 }
72 78
73 79 fn get_node(&self, id: NodeId) -> BackendResult<VfsNode> {
@@ -124,9 +130,7 @@ impl VfsBackend for DirectBackend {
124 130 }
125 131 }
126 132
127 -
128 133 impl TagBackend for DirectBackend {
129 -
130 134 // --- Tags ---
131 135
132 136 #[instrument(skip(self))]
@@ -179,9 +183,7 @@ impl TagBackend for DirectBackend {
179 183 }
180 184 }
181 185
182 -
183 186 impl CollectionBackend for DirectBackend {
184 -
185 187 // --- Collections ---
186 188
187 189 fn list_collections(&self) -> BackendResult<Vec<Collection>> {
@@ -190,12 +192,20 @@ impl CollectionBackend for DirectBackend {
190 192 }
191 193
192 194 #[instrument(skip(self))]
193 - fn create_collection(&self, name: &str, description: Option<&str>) -> BackendResult<CollectionId> {
195 + fn create_collection(
196 + &self,
197 + name: &str,
198 + description: Option<&str>,
199 + ) -> BackendResult<CollectionId> {
194 200 let db = self.db.lock();
195 201 Ok(collections::create_collection(&db, name, description)?)
196 202 }
197 203
198 - fn create_dynamic_collection(&self, name: &str, filter: &SearchFilter) -> BackendResult<CollectionId> {
204 + fn create_dynamic_collection(
205 + &self,
206 + name: &str,
207 + filter: &SearchFilter,
208 + ) -> BackendResult<CollectionId> {
199 209 let db = self.db.lock();
200 210 Ok(collections::create_dynamic_collection(&db, name, filter)?)
201 211 }
@@ -211,14 +221,30 @@ impl CollectionBackend for DirectBackend {
211 221 Ok(collections::delete_collection(&db, id)?)
212 222 }
213 223
214 - fn add_to_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()> {
224 + fn add_to_collection(
225 + &self,
226 + collection_id: CollectionId,
227 + sample_hash: &str,
228 + ) -> BackendResult<()> {
215 229 let db = self.db.lock();
216 - Ok(collections::add_to_collection(&db, collection_id, sample_hash)?)
230 + Ok(collections::add_to_collection(
231 + &db,
232 + collection_id,
233 + sample_hash,
234 + )?)
217 235 }
218 236
219 - fn remove_from_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()> {
237 + fn remove_from_collection(
238 + &self,
239 + collection_id: CollectionId,
240 + sample_hash: &str,
241 + ) -> BackendResult<()> {
220 242 let db = self.db.lock();
221 - Ok(collections::remove_from_collection(&db, collection_id, sample_hash)?)
243 + Ok(collections::remove_from_collection(
244 + &db,
245 + collection_id,
246 + sample_hash,
247 + )?)
222 248 }
223 249
224 250 fn list_collection_members(&self, collection_id: CollectionId) -> BackendResult<Vec<String>> {
@@ -231,4 +257,3 @@ impl CollectionBackend for DirectBackend {
231 257 Ok(collections::get_sample_collections(&db, sample_hash)?)
232 258 }
233 259 }
234 -
@@ -8,31 +8,32 @@ use std::path::{Path, PathBuf};
8 8
9 9 use tracing::instrument;
10 10
11 + use audiofiles_core::analysis::AnalysisResult;
11 12 use audiofiles_core::analysis::config::AnalysisConfig;
12 13 use audiofiles_core::analysis::waveform::WaveformData;
13 - use audiofiles_core::analysis::AnalysisResult;
14 + use audiofiles_core::collections::Collection;
14 15 use audiofiles_core::db::Database;
15 16 use audiofiles_core::edit::EditOperation;
16 17 use audiofiles_core::edit::worker::{EditCommand, EditEvent, EditWorkerHandle};
17 - use audiofiles_core::export::profile::DeviceProfileSummary;
18 18 use audiofiles_core::export::ExportItem;
19 + use audiofiles_core::export::profile::DeviceProfileSummary;
19 20 use audiofiles_core::forge::{ChopMethod, ConformResult, ConformTarget};
20 21 use audiofiles_core::search::SearchFilter;
21 - use audiofiles_core::collections::Collection;
22 22 use audiofiles_core::store::SampleStore;
23 23 use audiofiles_core::vfs::{self, Vfs, VfsNode, VfsNodeWithAnalysis};
24 - use audiofiles_core::{collections, search, tags, CollectionId, NodeId, VfsId};
24 + use audiofiles_core::{CollectionId, NodeId, VfsId, collections, search, tags};
25 25
26 26 use super::search_worker::{self, SearchCommand, SearchEvent};
27 27 use parking_lot::Mutex;
28 28
29 29 use super::{
30 - Backend, VfsBackend, TagBackend, SearchBackend, CollectionBackend, AnalysisBackend, RulesBackend, ClassifierBackend, SimilarityBackend, StoreBackend, ExportBackend, ForgeBackend, ConfigBackend, OperationsBackend,
31 - BackendError, BackendEvent, BackendResult, ExportConfigDesc, ExportItemDesc,
32 - ImportStrategyDesc, ImportedFolderDesc, TrainedHeadInfo,
30 + AnalysisBackend, Backend, BackendError, BackendEvent, BackendResult, ClassifierBackend,
31 + CollectionBackend, ConfigBackend, ExportBackend, ExportConfigDesc, ExportItemDesc,
32 + ForgeBackend, ImportStrategyDesc, ImportedFolderDesc, OperationsBackend, RulesBackend,
33 + SearchBackend, SimilarityBackend, StoreBackend, TagBackend, TrainedHeadInfo, VfsBackend,
33 34 };
34 35
35 - use super::forge::{ForgePending, ForgeImportOutcome, forge_import_chop, forge_import_conform};
36 + use super::forge::{ForgeImportOutcome, ForgePending, forge_import_chop, forge_import_conform};
36 37
37 38 use crate::cleanup::{CleanupCommand, CleanupHandle};
38 39 use crate::export::{ExportCommand, ExportHandle};
@@ -117,7 +118,9 @@ impl DirectBackend {
117 118 // should never trip — but if it does, the conform/M8/MPC wedge
118 119 // would silently show "No device profiles". Log so the empty
119 120 // registry is diagnosable instead of mysterious.
120 - tracing::error!("Failed to load device-profile registry, falling back to empty: {e}");
121 + tracing::error!(
122 + "Failed to load device-profile registry, falling back to empty: {e}"
123 + );
121 124 audiofiles_rhai::registry::PluginRegistry::new()
122 125 }),
123 126 }
@@ -146,16 +149,18 @@ impl DirectBackend {
146 149 db_path,
147 150 self.store.root().to_path_buf(),
148 151 )
149 - .map_err(|e| {
150 - BackendError::Other(format!("failed to spawn loose-files worker: {e}"))
151 - })?;
152 + .map_err(|e| BackendError::Other(format!("failed to spawn loose-files worker: {e}")))?;
152 153 *guard = Some(handle);
153 154 }
154 155 // If the cached worker died (e.g. its DB open failed inside the thread),
155 156 // send returns false. Drop the dead handle so the next call respawns, and
156 157 // report Err so the caller never sets a busy flag for a command that no
157 158 // worker will ever answer (which would wedge the repaint-while-busy guard).
158 - if !guard.as_ref().expect("loose-files worker present").send(cmd) {
159 + if !guard
160 + .as_ref()
161 + .expect("loose-files worker present")
162 + .send(cmd)
163 + {
159 164 *guard = None;
160 165 return Err(BackendError::Other(
161 166 "loose-files worker is not running".to_string(),
@@ -219,9 +224,10 @@ impl DirectBackend {
219 224
220 225 // Format: if Original, set to profile's first supported format
221 226 if config.format == ExportFormat::Original
222 - && let Some(fmt) = profile.audio.formats.first() {
223 - config.format = fmt.clone();
224 - }
227 + && let Some(fmt) = profile.audio.formats.first()
228 + {
229 + config.format = fmt.clone();
230 + }
225 231
226 232 // Sample rate: if not set, use profile's first rate
227 233 if config.sample_rate.is_none() {
@@ -252,7 +258,10 @@ impl DirectBackend {
252 258 // to avoid holding the DB lock during potentially slow Rhai execution.
253 259 let mut infos: Vec<_> = {
254 260 let db = self.db.lock();
255 - items.iter().map(|item| super::sample_info::build_sample_info(&db, &self.store, item)).collect()
261 + items
262 + .iter()
263 + .map(|item| super::sample_info::build_sample_info(&db, &self.store, item))
264 + .collect()
256 265 };
257 266 // Probe bit depth off the DB lock and in parallel: N independent
258 267 // file-header reads that previously ran serially *while holding the
@@ -265,7 +274,10 @@ impl DirectBackend {
265 274 .collect();
266 275 let depths: Vec<u16> = paths
267 276 .into_par_iter()
268 - .map(|p| p.and_then(|p| super::sample_info::probe_bit_depth(&p)).unwrap_or(0))
277 + .map(|p| {
278 + p.and_then(|p| super::sample_info::probe_bit_depth(&p))
279 + .unwrap_or(0)
280 + })
269 281 .collect();
270 282 for (info, depth) in infos.iter_mut().zip(depths) {
271 283 info.bit_depth = depth;
@@ -292,7 +304,11 @@ impl DirectBackend {
292 304 };
293 305 }
294 306 let mut ki = 0;
295 - items.retain(|_| { let k = keep[ki]; ki += 1; k });
307 + items.retain(|_| {
308 + let k = keep[ki];
309 + ki += 1;
310 + k
311 + });
296 312 }
297 313
298 314 // transform_filename hook: pre-compute output names with custom naming logic
@@ -302,11 +318,8 @@ impl DirectBackend {
302 318 .as_ref()
303 319 .and_then(|p| audiofiles_core::rename::RenamePattern::parse(p).ok());
304 320
305 - let mut names = audiofiles_core::export::resolve_output_names(
306 - items,
307 - config,
308 - pattern.as_ref(),
309 - );
321 + let mut names =
322 + audiofiles_core::export::resolve_output_names(items, config, pattern.as_ref());
310 323
311 324 let device_name = profile.name.clone();
312 325 let destination = config.destination.display().to_string();
@@ -332,7 +345,8 @@ impl DirectBackend {
332 345 // to prevent traversal. resolve_output_names re-sanitizes
333 346 // overrides too, so this is a first layer, not the only one.
334 347 let safe_stem = new_stem.replace(['/', '\\', '\0'], "_");
335 - let safe_stem = if safe_stem.is_empty() || safe_stem == ".." || safe_stem == "." {
348 + let safe_stem = if safe_stem.is_empty() || safe_stem == ".." || safe_stem == "."
349 + {
336 350 "untitled".to_string()
337 351 } else {
338 352 safe_stem
@@ -355,12 +369,12 @@ use audiofiles_core::util::split_name_ext;
355 369
356 370 impl Backend for DirectBackend {}
357 371
358 - mod library;
359 372 mod analysis;
360 - mod store;
361 373 mod classify;
362 374 mod forge;
375 + mod library;
363 376 mod operations;
377 + mod store;
364 378
365 379 #[cfg(test)]
366 380 mod tests;
@@ -5,7 +5,6 @@
5 5 use super::*;
6 6
7 7 impl OperationsBackend for DirectBackend {
8 -
9 8 // --- VFS Mirror ---
10 9
11 10 fn sync_vfs_mirror(&self, mirror_root: &Path) -> BackendResult<(usize, usize, usize)> {
@@ -15,16 +14,16 @@ impl OperationsBackend for DirectBackend {
15 14 store_root: self.store.root().to_path_buf(),
16 15 };
17 16 let stats = audiofiles_core::vfs_mirror::sync_mirror(&db, &config)?;
18 - Ok((stats.dirs_created, stats.links_created, stats.entries_removed))
17 + Ok((
18 + stats.dirs_created,
19 + stats.links_created,
20 + stats.entries_removed,
21 + ))
19 22 }
20 23
21 24 // --- Long-running operations ---
22 25
23 - fn start_import(
24 - &self,
25 - source: &Path,
26 - strategy: ImportStrategyDesc,
27 - ) -> BackendResult<()> {
26 + fn start_import(&self, source: &Path, strategy: ImportStrategyDesc) -> BackendResult<()> {
28 27 let import_strategy = resolve_import_strategy(strategy);
29 28 let handle = self.respawn_import_worker()?;
30 29 handle.send(ImportCommand::ImportDirectory {
@@ -60,9 +59,9 @@ impl OperationsBackend for DirectBackend {
60 59 sample_hashes
61 60 .into_iter()
62 61 .filter_map(|(hash, ext)| {
63 - let path = audiofiles_core::store::resolve_file_path(
64 - &self.store, &db, &hash, &ext,
65 - ).ok()?;
62 + let path =
63 + audiofiles_core::store::resolve_file_path(&self.store, &db, &hash, &ext)
64 + .ok()?;
66 65 if path.exists() {
67 66 Some((hash, ext, path))
68 67 } else {
@@ -210,11 +209,7 @@ impl OperationsBackend for DirectBackend {
210 209 Ok(())
211 210 }
212 211
213 - fn delete_edit_history(
214 - &self,
215 - source_hash: &str,
216 - result_hash: &str,
217 - ) -> BackendResult<()> {
212 + fn delete_edit_history(&self, source_hash: &str, result_hash: &str) -> BackendResult<()> {
218 213 let db = self.db.lock();
219 214 db.conn()
220 215 .execute(
@@ -233,11 +228,16 @@ impl OperationsBackend for DirectBackend {
233 228
234 229 fn storage_stats(&self) -> BackendResult<crate::backend::StorageStats> {
235 230 let db = self.db.lock();
236 - let (sample_count, total_bytes) = db.storage_stats()
231 + let (sample_count, total_bytes) = db
232 + .storage_stats()
237 233 .map_err(|e| BackendError::Other(e.to_string()))?;
238 234 let db_path = self.data_dir.join("audiofiles.db");
239 235 let db_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
240 - Ok(crate::backend::StorageStats { sample_count, total_bytes, db_bytes })
236 + Ok(crate::backend::StorageStats {
237 + sample_count,
238 + total_bytes,
239 + db_bytes,
240 + })
241 241 }
242 242
243 243 fn vfs_storage_stats(&self, vfs_id: audiofiles_core::VfsId) -> BackendResult<(u64, u64)> {
@@ -493,7 +493,12 @@ impl OperationsBackend for DirectBackend {
493 493 },
494 494 move || {
495 495 forge_import_conform(
496 - &data_dir, &root, vfs_id, parent_id, sample_rate, bit_depth,
496 + &data_dir,
497 + &root,
498 + vfs_id,
499 + parent_id,
500 + sample_rate,
501 + bit_depth,
497 502 staging,
498 503 )
499 504 },
@@ -530,13 +535,15 @@ impl OperationsBackend for DirectBackend {
530 535 ForgeImportOutcome::Chop { slice_count } => {
531 536 events.push(BackendEvent::ForgeChopComplete { slice_count })
532 537 }
533 - ForgeImportOutcome::Conform { sample_rate, bit_depth, overshoot } => {
534 - events.push(BackendEvent::ForgeConformComplete {
535 - sample_rate,
536 - bit_depth,
537 - overshoot,
538 - })
539 - }
538 + ForgeImportOutcome::Conform {
539 + sample_rate,
540 + bit_depth,
541 + overshoot,
542 + } => events.push(BackendEvent::ForgeConformComplete {
543 + sample_rate,
544 + bit_depth,
545 + overshoot,
546 + }),
540 547 ForgeImportOutcome::Error { error } => {
541 548 events.push(BackendEvent::ForgeError { error })
542 549 }
@@ -590,4 +597,3 @@ impl OperationsBackend for DirectBackend {
590 597 events
591 598 }
592 599 }
593 -
@@ -4,7 +4,6 @@
4 4 use super::*;
5 5
6 6 impl StoreBackend for DirectBackend {
7 -
8 7 // --- Store ---
9 8
10 9 fn import_file(&self, path: &Path) -> BackendResult<String> {
@@ -14,7 +13,12 @@ impl StoreBackend for DirectBackend {
14 13
15 14 fn sample_path(&self, hash: &str, ext: &str) -> BackendResult<PathBuf> {
16 15 let db = self.db.lock();
17 - Ok(audiofiles_core::store::resolve_file_path(&self.store, &db, hash, ext)?)
16 + Ok(audiofiles_core::store::resolve_file_path(
17 + &self.store,
18 + &db,
19 + hash,
20 + ext,
21 + )?)
18 22 }
19 23
20 24 fn sample_extension(&self, hash: &str) -> BackendResult<String> {
@@ -73,7 +77,12 @@ impl StoreBackend for DirectBackend {
73 77
74 78 fn relocate_sample(&self, hash: &str, new_path: &Path) -> BackendResult<()> {
75 79 let db = self.db.lock();
76 - Ok(audiofiles_core::store::relocate_sample(&self.store, &db, hash, new_path)?)
80 + Ok(audiofiles_core::store::relocate_sample(
81 + &self.store,
82 + &db,
83 + hash,
84 + new_path,
85 + )?)
77 86 }
78 87
79 88 fn check_vault_integrity(&self) -> BackendResult<(usize, usize)> {
@@ -91,7 +100,10 @@ impl StoreBackend for DirectBackend {
91 100 search_root: &std::path::Path,
92 101 ) -> BackendResult<(usize, usize)> {
93 102 let db = self.db.lock();
94 - Ok(audiofiles_core::store::relocate_missing_loose_files(&db, search_root)?)
103 + Ok(audiofiles_core::store::relocate_missing_loose_files(
104 + &db,
105 + search_root,
106 + )?)
95 107 }
96 108
97 109 fn start_loose_files_check(&self) -> BackendResult<()> {
@@ -113,9 +125,7 @@ impl StoreBackend for DirectBackend {
113 125 }
114 126 }
115 127
116 -
117 128 impl ExportBackend for DirectBackend {
118 -
119 129 // --- Export ---
120 130
121 131 fn collect_export_items(
@@ -124,7 +134,9 @@ impl ExportBackend for DirectBackend {
124 134 parent_id: Option<NodeId>,
125 135 ) -> BackendResult<Vec<ExportItem>> {
126 136 let db = self.db.lock();
127 - Ok(audiofiles_core::export::collect_export_items(&db, vfs_id, parent_id)?)
137 + Ok(audiofiles_core::export::collect_export_items(
138 + &db, vfs_id, parent_id,
139 + )?)
128 140 }
129 141
130 142 fn enrich_export_with_tags(&self, items: &mut [ExportItem]) -> BackendResult<()> {
@@ -165,4 +177,3 @@ impl ExportBackend for DirectBackend {
165 177 }
166 178 }
167 179 }
168 -
@@ -143,7 +143,9 @@ fn list_all_directories_works() {
143 143 let backend = setup();
144 144 let vfs_id = backend.create_vfs("Test").unwrap();
145 145 let drums = backend.create_directory(vfs_id, None, "Drums").unwrap();
146 - backend.create_directory(vfs_id, Some(drums), "Kicks").unwrap();
146 + backend
147 + .create_directory(vfs_id, Some(drums), "Kicks")
148 + .unwrap();
147 149
148 150 let dirs = backend.list_all_directories(vfs_id).unwrap();
149 151 assert_eq!(dirs.len(), 2);
@@ -173,7 +175,10 @@ fn write_float_wav(path: &Path, channels: u16, sample_rate: u32, samples: &[f32]
173 175 for &s in samples {
174 176 buf.extend_from_slice(&s.to_le_bytes());
175 177 }
176 - std::fs::File::create(path).unwrap().write_all(&buf).unwrap();
178 + std::fs::File::create(path)
179 + .unwrap()
180 + .write_all(&buf)
181 + .unwrap();
177 182 }
178 183
179 184 #[test]
@@ -212,7 +217,14 @@ fn chop_sample_creates_slice_folder() {
212 217 assert_eq!(marks.last().copied(), Some(1.0));
213 218
214 219 let count = backend
215 - .chop_sample(vfs_id, &hash, "wav", "loop.wav", None, &ChopMethod::EqualDivisions(4))
220 + .chop_sample(
221 + vfs_id,
222 + &hash,
223 + "wav",
224 + "loop.wav",
225 + None,
226 + &ChopMethod::EqualDivisions(4),
227 + )
216 228 .unwrap();
217 229 assert_eq!(count, 4);
218 230
@@ -242,7 +254,14 @@ fn start_chop_runs_on_worker_and_imports_on_poll() {
242 254
243 255 // Dispatch to the worker; returns immediately (no UI block).
244 256 backend
245 - .start_chop(vfs_id, &hash, "wav", "loop.wav", None, &ChopMethod::EqualDivisions(4))
257 + .start_chop(
258 + vfs_id,
259 + &hash,
260 + "wav",
261 + "loop.wav",
262 + None,
263 + &ChopMethod::EqualDivisions(4),
264 + )
246 265 .unwrap();
247 266
248 267 // Poll until the chop completes (worker does CPU; poll imports).
@@ -274,9 +293,7 @@ fn start_chop_runs_on_worker_and_imports_on_poll() {
274 293 fn device_conform_target_resolves_bundled() {
275 294 let backend = setup();
276 295 // A bundled mono device resolves to a mono target.
277 - let target = backend
278 - .device_conform_target("SP-404 MKII", 48000)
279 - .unwrap();
296 + let target = backend.device_conform_target("SP-404 MKII", 48000).unwrap();
280 297 assert!(target.is_some(), "SP-404 MKII should resolve to a target");
281 298 }
282 299
@@ -29,13 +29,17 @@ pub(super) enum ForgePending {
29 29 /// writes) runs on its own WAL connection so it never holds the GUI's `db.lock()`;
30 30 /// `poll_workers` drains this and emits the matching `BackendEvent` on a later frame.
31 31 pub(super) enum ForgeImportOutcome {
32 - Chop { slice_count: usize },
32 + Chop {
33 + slice_count: usize,
34 + },
33 35 Conform {
34 36 sample_rate: u32,
35 37 bit_depth: u16,
36 38 overshoot: Option<super::ForgeOvershoot>,
37 39 },
38 - Error { error: String },
40 + Error {
41 + error: String,
42 + },
39 43 }
40 44
41 45 /// Open a fresh WAL connection + store and import chop slices. Runs on a job
@@ -50,15 +54,27 @@ pub(super) fn forge_import_chop(
50 54 ) -> ForgeImportOutcome {
51 55 let db = match Database::open(data_dir.join("audiofiles.db")) {
52 56 Ok(d) => d,
53 - Err(e) => return ForgeImportOutcome::Error { error: e.to_string() },
57 + Err(e) => {
58 + return ForgeImportOutcome::Error {
59 + error: e.to_string(),
60 + };
61 + }
54 62 };
55 63 let store = match SampleStore::new(root) {
56 64 Ok(s) => s,
57 - Err(e) => return ForgeImportOutcome::Error { error: e.to_string() },
65 + Err(e) => {
66 + return ForgeImportOutcome::Error {
67 + error: e.to_string(),
68 + };
69 + }
58 70 };
59 71 match audiofiles_core::forge::import_chop(&store, &db, vfs_id, parent_id, staging) {
60 - Ok(r) => ForgeImportOutcome::Chop { slice_count: r.slice_count },
61 - Err(e) => ForgeImportOutcome::Error { error: e.to_string() },
72 + Ok(r) => ForgeImportOutcome::Chop {
73 + slice_count: r.slice_count,
74 + },
75 + Err(e) => ForgeImportOutcome::Error {
76 + error: e.to_string(),
77 + },
62 78 }
63 79 }
64 80
@@ -77,24 +93,43 @@ pub(super) fn forge_import_conform(
77 93 ) -> ForgeImportOutcome {
78 94 let db = match Database::open(data_dir.join("audiofiles.db")) {
79 95 Ok(d) => d,
80 - Err(e) => return ForgeImportOutcome::Error { error: e.to_string() },
96 + Err(e) => {
97 + return ForgeImportOutcome::Error {
98 + error: e.to_string(),
99 + };
100 + }
81 101 };
82 102 let store = match SampleStore::new(root) {
83 103 Ok(s) => s,
84 - Err(e) => return ForgeImportOutcome::Error { error: e.to_string() },
104 + Err(e) => {
105 + return ForgeImportOutcome::Error {
106 + error: e.to_string(),
107 + };
108 + }
85 109 };
86 110 match audiofiles_core::forge::import_conform(&store, &db, vfs_id, parent_id, staging) {
87 111 Ok(r) => {
88 112 let overshoot = r.overshoot.map(|o| match o.action {
89 113 audiofiles_core::forge::OvershootAction::Trimmed { gain_db } => {
90 - super::ForgeOvershoot::Trimmed { peak_dbfs: o.peak_dbfs, gain_db }
114 + super::ForgeOvershoot::Trimmed {
115 + peak_dbfs: o.peak_dbfs,
116 + gain_db,
117 + }
91 118 }
92 119 audiofiles_core::forge::OvershootAction::Flagged => {
93 - super::ForgeOvershoot::Flagged { peak_dbfs: o.peak_dbfs }
120 + super::ForgeOvershoot::Flagged {
121 + peak_dbfs: o.peak_dbfs,
122 + }
94 123 }
95 124 });
96 - ForgeImportOutcome::Conform { sample_rate, bit_depth, overshoot }
125 + ForgeImportOutcome::Conform {
126 + sample_rate,
127 + bit_depth,
128 + overshoot,
129 + }
97 130 }
98 - Err(e) => ForgeImportOutcome::Error { error: e.to_string() },
131 + Err(e) => ForgeImportOutcome::Error {
132 + error: e.to_string(),
133 + },
99 134 }
100 135 }
@@ -8,22 +8,22 @@
8 8 //! - `DaemonBackend` (Phase D) — forwards calls to the daemon over Unix socket via JSON-RPC.
9 9
10 10 pub mod direct;
11 - pub mod search_worker;
12 11 mod forge;
13 12 mod sample_info;
13 + pub mod search_worker;
14 14
15 15 use std::path::{Path, PathBuf};
16 16
17 + use audiofiles_core::analysis::AnalysisResult;
17 18 use audiofiles_core::analysis::config::AnalysisConfig;
18 19 use audiofiles_core::analysis::suggest::TagSuggestion;
19 20 use audiofiles_core::analysis::waveform::WaveformData;
20 - use audiofiles_core::analysis::AnalysisResult;
21 + use audiofiles_core::collections::Collection;
21 22 use audiofiles_core::edit::EditOperation;
22 23 use audiofiles_core::export::profile::DeviceProfileSummary;
23 24 use audiofiles_core::export::{ExportConfig, ExportItem};
24 25 use audiofiles_core::forge::{ChopMethod, ConformResult, ConformTarget};
25 26 use audiofiles_core::search::SearchFilter;
26 - use audiofiles_core::collections::Collection;
27 27 use audiofiles_core::store::TombstonedSample;
28 28 use audiofiles_core::vfs::{Vfs, VfsNode, VfsNodeWithAnalysis};
29 29 use audiofiles_core::{CollectionId, NodeId, VfsId};
@@ -207,9 +207,17 @@ pub struct ImportedFolderDesc {
207 207 /// Serializable description of an import strategy (for IPC).
208 208 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
209 209 pub enum ImportStrategyDesc {
210 - Flat { vfs_id: VfsId, parent_id: Option<NodeId> },
211 - NewVfs { vfs_name: String },
212 - MergeIntoVfs { vfs_id: VfsId, parent_id: Option<NodeId> },
210 + Flat {
211 + vfs_id: VfsId,
212 + parent_id: Option<NodeId>,
213 + },
214 + NewVfs {
215 + vfs_name: String,
216 + },
217 + MergeIntoVfs {
218 + vfs_id: VfsId,
219 + parent_id: Option<NodeId>,
220 + },
213 221 }
214 222
215 223 /// Serializable description of an export item (for IPC).
@@ -257,7 +265,10 @@ pub enum ClassifierJob {
257 265 /// Cluster the library into `k` groups.
258 266 Cluster { k: usize },
259 267 /// Export the user's classifier data to a `.afcl` file.
260 - Export { path: std::path::PathBuf, opts: audiofiles_core::analysis::afcl::ExportOptions },
268 + Export {
269 + path: std::path::PathBuf,
270 + opts: audiofiles_core::analysis::afcl::ExportOptions,
271 + },
261 272 /// k-NN tag suggestions for a single sample. Building the exemplar index is
262 273 /// O(library), so the detail-panel "Suggest tags" action runs it here rather
263 274 /// than under the DB lock on the GUI thread.
@@ -273,7 +284,10 @@ pub enum ClassifierJobResult {
273 284 Exported(std::path::PathBuf),
274 285 /// Suggestions for the sample identified by `hash`. The hash lets the GUI
275 286 /// drop the result if the selection changed while the job ran.
276 - Suggested { hash: String, outcome: audiofiles_core::analysis::exemplar::MlOutcome },
287 + Suggested {
288 + hash: String,
289 + outcome: audiofiles_core::analysis::exemplar::MlOutcome,
290 + },
277 291 }
278 292
279 293 /// The core abstraction separating UI from data access.
@@ -361,7 +375,6 @@ pub trait VfsBackend {
361 375
362 376 /// Sample tagging: per-sample and bulk tag mutation and lookup.
363 377 pub trait TagBackend {
364 -
365 378 // --- Tags ---
366 379
367 380 /// Add a tag to a sample.
@@ -394,7 +407,6 @@ pub trait TagBackend {
394 407
395 408 /// Text/filter search over the library.
396 409 pub trait SearchBackend {
397 -
398 410 // --- Search ---
399 411
400 412 /// Search within a specific VFS folder.
@@ -411,7 +423,6 @@ pub trait SearchBackend {
411 423
412 424 /// Collections and smart (dynamic) folders.
413 425 pub trait CollectionBackend {
414 -
415 426 // --- Smart folders ---
416 427
417 428 // --- Collections ---
@@ -420,10 +431,18 @@ pub trait CollectionBackend {
420 431 fn list_collections(&self) -> BackendResult<Vec<Collection>>;
421 432
422 433 /// Create a manual collection. Returns the new ID.
423 - fn create_collection(&self, name: &str, description: Option<&str>) -> BackendResult<CollectionId>;
434 + fn create_collection(
435 + &self,
436 + name: &str,
437 + description: Option<&str>,
438 + ) -> BackendResult<CollectionId>;
424 439
425 440 /// Create a dynamic collection (saved search). Returns the new ID.
426 - fn create_dynamic_collection(&self, name: &str, filter: &SearchFilter) -> BackendResult<CollectionId>;
441 + fn create_dynamic_collection(
442 + &self,
443 + name: &str,
444 + filter: &SearchFilter,
445 + ) -> BackendResult<CollectionId>;
427 446
428 447 /// Rename a collection.
429 448 fn rename_collection(&self, id: CollectionId, new_name: &str) -> BackendResult<()>;
@@ -432,10 +451,18 @@ pub trait CollectionBackend {
432 451 fn delete_collection(&self, id: CollectionId) -> BackendResult<()>;
433 452
434 453 /// Add a sample to a collection (no-op if already present).
435 - fn add_to_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()>;
454 + fn add_to_collection(
455 + &self,
456 + collection_id: CollectionId,
457 + sample_hash: &str,
458 + ) -> BackendResult<()>;
436 459
437 460 /// Remove a sample from a collection.
438 - fn remove_from_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()>;
461 + fn remove_from_collection(
462 + &self,
463 + collection_id: CollectionId,
464 + sample_hash: &str,
465 + ) -> BackendResult<()>;
439 466
440 467 /// List sample hashes in a collection.
441 468 fn list_collection_members(&self, collection_id: CollectionId) -> BackendResult<Vec<String>>;
@@ -446,7 +473,6 @@ pub trait CollectionBackend {
446 473
447 474 /// Feature analysis: read/persist results and rule application over them.
448 475 pub trait AnalysisBackend {
449 -
450 476 // --- Analysis ---
451 477
452 478 /// Get the full analysis result for a sample, if it exists.
@@ -473,7 +499,6 @@ pub trait AnalysisBackend {
473 499
474 500 /// Tag rules (classifier Layer A).
475 501 pub trait RulesBackend {
476 -
477 502 // --- Tag rules (classifier Layer A) ---
478 503
479 504 /// All tag rules in evaluation (priority) order.
@@ -510,7 +535,6 @@ pub trait RulesBackend {
510 535
511 536 /// Auto-tagging (Layer B): k-NN suggestions, thresholds, the trained head, .afcl sharing, clustering, and harvesting.
512 537 pub trait ClassifierBackend {
513 -
514 538 // --- Auto-tagging: k-NN suggestions, thresholds, bootstrap (Layer B) ---
515 539
516 540 // Single-sample k-NN suggestions run on the classifier worker
@@ -598,8 +622,7 @@ pub trait ClassifierBackend {
598 622 fn apply_cluster_tag(&self, members: &[String], tag: &str) -> BackendResult<usize>;
599 623
600 624 /// Folder-derived tag candidates from the VFS structure.
601 - fn harvest_folder_labels(&self)
602 - -> BackendResult<Vec<audiofiles_core::harvest::FolderLabel>>;
625 + fn harvest_folder_labels(&self) -> BackendResult<Vec<audiofiles_core::harvest::FolderLabel>>;
603 626
604 627 /// Apply a harvested tag to the given samples (`source = 'harvest'`).
605 628 fn apply_harvested_tag(&self, hashes: &[String], tag: &str) -> BackendResult<usize>;
@@ -613,7 +636,6 @@ pub trait ClassifierBackend {
613 636
614 637 /// Similarity and near-duplicate search.
615 638 pub trait SimilarityBackend {
616 -
617 639 // --- Similarity ---
618 640
619 641 /// Start a similarity search for `hash`. The VP-tree build + query run on the
@@ -628,7 +650,6 @@ pub trait SimilarityBackend {
628 650
629 651 /// Content-addressed store: import, sample paths, trash, and loose-file maintenance.
630 652 pub trait StoreBackend {
631 -
632 653 // --- Store ---
633 654
634 655 /// Import a file into the content-addressed store. Returns the hash.
@@ -719,7 +740,6 @@ pub trait StoreBackend {
719 740
720 741 /// Export item collection and device profiles.
721 742 pub trait ExportBackend {
722 -
723 743 // --- Export ---
724 744
725 745 /// Collect export items from a VFS subtree.
@@ -749,7 +769,6 @@ pub trait ExportBackend {
749 769
750 770 /// Sample Forge: chop and conform.
751 771 pub trait ForgeBackend {
752 -
753 772 // --- Sample Forge ---
754 773
755 774 /// Start computing slice-boundary positions (normalized 0..1 fractions of
@@ -814,7 +833,6 @@ pub trait ForgeBackend {
814 833
815 834 /// App config key/value and per-VFS sync toggles.
816 835 pub trait ConfigBackend {
817 -
818 836 // --- Config ---
819 837
820 838 /// Get a user config value by key.
@@ -835,7 +853,6 @@ pub trait ConfigBackend {
835 853
836 854 /// VFS mirror sync plus the long-running background workers (import/analysis/export/edit/cleanup), edit history, storage stats, and event polling.
837 855 pub trait OperationsBackend {
838 -
839 856 // --- VFS Mirror ---
840 857
841 858 /// Synchronise the VFS mirror directory with the current VFS state.
@@ -845,11 +862,7 @@ pub trait OperationsBackend {
845 862 // --- Long-running operations ---
846 863
847 864 /// Start a folder import in the background.
848 - fn start_import(
849 - &self,
850 - source: &Path,
851 - strategy: ImportStrategyDesc,
852 - ) -> BackendResult<()>;
865 + fn start_import(&self, source: &Path, strategy: ImportStrategyDesc) -> BackendResult<()>;
853 866
854 867 /// Start a background import of an explicit list of files (loose drops, CLI
855 868 /// args, OS "Open with"). Runs on the same worker as [`start_import`] so the
@@ -906,11 +919,7 @@ pub trait OperationsBackend {
906 919 /// Delete the most recent `edit_history` row matching this (source, result)
907 920 /// pair. Used by `BrowserState::undo_last_edit` to reverse a previously
908 921 /// recorded edit when the user clicks the inline Undo affordance.
909 - fn delete_edit_history(
910 - &self,
911 - source_hash: &str,
912 - result_hash: &str,
913 - ) -> BackendResult<()>;
922 + fn delete_edit_history(&self, source_hash: &str, result_hash: &str) -> BackendResult<()>;
914 923
915 924 /// Get aggregate storage statistics for the current vault.
916 925 fn storage_stats(&self) -> BackendResult<StorageStats>;
@@ -975,11 +984,16 @@ mod tests {
975 984
976 985 #[test]
977 986 fn import_strategy_desc_variants() {
978 - let flat = ImportStrategyDesc::Flat { vfs_id: VfsId::from(1), parent_id: None };
987 + let flat = ImportStrategyDesc::Flat {
988 + vfs_id: VfsId::from(1),
989 + parent_id: None,
990 + };
979 991 let json = serde_json::to_string(&flat).unwrap();
980 992 assert!(json.contains("Flat"));
981 993
982 - let new_vfs = ImportStrategyDesc::NewVfs { vfs_name: "Test".to_string() };
994 + let new_vfs = ImportStrategyDesc::NewVfs {
995 + vfs_name: "Test".to_string(),
996 + };
983 997 let json = serde_json::to_string(&new_vfs).unwrap();
984 998 assert!(json.contains("Test"));
985 999 }