From e9421e58e3d8a04428772a0b17157a4f0b4cb205 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 8 May 2026 14:05:34 +0200 Subject: [PATCH] refactor(audio): extract audio_preload into preload_commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull audio_preload (background fetch + analysis seed for the next track in the queue) out of audio/commands.rs into audio/preload_commands.rs (~67 LOC). It's a self-contained fetch-and-cache flow — distinct from audio_chain_preload (which constructs the gapless source chain) and audio_play (which starts playback) so it makes sense to live alongside them rather than inside the same file. audio/commands.rs: 1241 → 1185 LOC. lib.rs invoke_handler updated. --- src-tauri/src/audio/commands.rs | 56 --------------------- src-tauri/src/audio/mod.rs | 1 + src-tauri/src/audio/preload_commands.rs | 67 +++++++++++++++++++++++++ src-tauri/src/lib.rs | 2 +- 4 files changed, 69 insertions(+), 57 deletions(-) create mode 100644 src-tauri/src/audio/preload_commands.rs diff --git a/src-tauri/src/audio/commands.rs b/src-tauri/src/audio/commands.rs index 62f7614b..7a6062cd 100644 --- a/src-tauri/src/audio/commands.rs +++ b/src-tauri/src/audio/commands.rs @@ -1183,59 +1183,3 @@ pub(super) fn spawn_progress_task( -#[tauri::command] -pub async fn audio_preload( - url: String, - duration_hint: f64, - analysis_track_id: Option, - app: AppHandle, - state: State<'_, AudioEngine>, -) -> Result<(), String> { - { - let preloaded = state.preloaded.lock().unwrap(); - if preloaded.as_ref().is_some_and(|p| same_playback_target(&p.url, &url)) { - let _ = app.emit("audio:preload-ready", url.clone()); - return Ok(()); - } - } - // Throttle: wait 8 s before starting the background download so it does not - // compete with the decode + sink-feed work of the just-started current track. - // If the user skips during the wait the generation counter changes and we abort. - let gen_snapshot = state.generation.load(Ordering::Relaxed); - tokio::time::sleep(Duration::from_secs(8)).await; - if state.generation.load(Ordering::Relaxed) != gen_snapshot { - return Ok(()); - } - let data: Vec = if let Some(path) = url.strip_prefix("psysonic-local://") { - tokio::fs::read(path).await.map_err(|e| e.to_string())? - } else { - let response = audio_http_client(&state).get(&url).send().await.map_err(|e| e.to_string())?; - if !response.status().is_success() { - return Ok(()); - } - response.bytes().await.map_err(|e| e.to_string())?.into() - }; - let _ = duration_hint; // kept in API for compatibility - let logical_trim = analysis_track_id - .as_ref() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()); - if let Some(track_id) = analysis_cache_track_id(logical_trim.as_deref(), &url) { - crate::app_deprintln!( - "[stream] audio_preload: bytes ready track_id={} size_mib={:.2} — invoking full-track analysis", - track_id, - data.len() as f64 / (1024.0 * 1024.0) - ); - let high = crate::audio::engine::analysis_track_id_is_current_playback(&state, &track_id); - if let Err(e) = crate::submit_analysis_cpu_seed(app.clone(), track_id.clone(), data.clone(), high).await { - crate::app_eprintln!("[analysis] preload seed failed for {}: {}", track_id, e); - } - } - let url_for_emit = url.clone(); - *state.preloaded.lock().unwrap() = Some(PreloadedTrack { url, data }); - let _ = app.emit("audio:preload-ready", url_for_emit); - Ok(()) -} - - - diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index 23196bdf..1602c565 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -10,6 +10,7 @@ mod decode; mod dev_io; pub mod device_commands; pub mod mix_commands; +pub mod preload_commands; pub mod radio_commands; pub mod transport_commands; mod device_watcher; diff --git a/src-tauri/src/audio/preload_commands.rs b/src-tauri/src/audio/preload_commands.rs new file mode 100644 index 00000000..4a6f66b7 --- /dev/null +++ b/src-tauri/src/audio/preload_commands.rs @@ -0,0 +1,67 @@ +//! Background audio_preload: fetch the next track's bytes ahead of time +//! and seed the analysis cache. Distinct from `audio_chain_preload` +//! (which constructs the gapless source chain) and `audio_play` (which +//! starts playback). All three live in this audio submodule. + +use std::sync::atomic::Ordering; +use std::time::Duration; + +use tauri::{AppHandle, Emitter, State}; + +use super::engine::{audio_http_client, AudioEngine}; +use super::helpers::{analysis_cache_track_id, same_playback_target}; +use super::state::PreloadedTrack; + +#[tauri::command] +pub async fn audio_preload( + url: String, + duration_hint: f64, + analysis_track_id: Option, + app: AppHandle, + state: State<'_, AudioEngine>, +) -> Result<(), String> { + { + let preloaded = state.preloaded.lock().unwrap(); + if preloaded.as_ref().is_some_and(|p| same_playback_target(&p.url, &url)) { + let _ = app.emit("audio:preload-ready", url.clone()); + return Ok(()); + } + } + // Throttle: wait 8 s before starting the background download so it does not + // compete with the decode + sink-feed work of the just-started current track. + // If the user skips during the wait the generation counter changes and we abort. + let gen_snapshot = state.generation.load(Ordering::Relaxed); + tokio::time::sleep(Duration::from_secs(8)).await; + if state.generation.load(Ordering::Relaxed) != gen_snapshot { + return Ok(()); + } + let data: Vec = if let Some(path) = url.strip_prefix("psysonic-local://") { + tokio::fs::read(path).await.map_err(|e| e.to_string())? + } else { + let response = audio_http_client(&state).get(&url).send().await.map_err(|e| e.to_string())?; + if !response.status().is_success() { + return Ok(()); + } + response.bytes().await.map_err(|e| e.to_string())?.into() + }; + let _ = duration_hint; // kept in API for compatibility + let logical_trim = analysis_track_id + .as_ref() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + if let Some(track_id) = analysis_cache_track_id(logical_trim.as_deref(), &url) { + crate::app_deprintln!( + "[stream] audio_preload: bytes ready track_id={} size_mib={:.2} — invoking full-track analysis", + track_id, + data.len() as f64 / (1024.0 * 1024.0) + ); + let high = crate::audio::engine::analysis_track_id_is_current_playback(&state, &track_id); + if let Err(e) = crate::submit_analysis_cpu_seed(app.clone(), track_id.clone(), data.clone(), high).await { + crate::app_eprintln!("[analysis] preload seed failed for {}: {}", track_id, e); + } + } + let url_for_emit = url.clone(); + *state.preloaded.lock().unwrap() = Some(PreloadedTrack { url, data }); + let _ = app.emit("audio:preload-ready", url_for_emit); + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6060d2b7..790cd397 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -397,7 +397,7 @@ pub fn run() { audio::mix_commands::audio_set_eq, audio::autoeq_commands::autoeq_entries, audio::autoeq_commands::autoeq_fetch_profile, - audio::commands::audio_preload, + audio::preload_commands::audio_preload, audio::radio_commands::audio_play_radio, audio::preview::audio_preview_play, audio::preview::audio_preview_stop,