From 41e75663f126f4ee93d135eb9f70e5005d597f28 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sat, 9 May 2026 13:33:44 +0200 Subject: [PATCH] refactor: extract psysonic-audio crate (M3/7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves all audio playback code (Symphonia decode, rodio output, HTTP streaming, gapless, previews, and the seven stream/ source-type submodules from the prior split) out of the top crate into a new psysonic-audio crate. crates/psysonic-audio/ new lib crate, depends on psysonic-core + psysonic-analysis src/{engine,helpers,decode,…}.rs flattened layout (no more extra audio/ namespace level) src/stream/ seven submodules from M0 src/lib.rs re-exports macros from psysonic-core and the public API surface The audio↔analysis edges identified in the dep survey are now real crate deps (audio depends on analysis directly: AnalysisCache reads, recommended_gain_for_target, submit_analysis_cpu_seed). Only the analysis→audio back-edge goes through the PlaybackQueryHandle port registered in M2. Cross-crate ref migrations applied via batch sed: crate::audio::* → crate::* (intra-crate) crate::analysis_cache::* → psysonic_analysis::analysis_cache::* crate::submit_analysis_cpu_seed → psysonic_analysis::analysis_runtime::* crate::subsonic_wire_user_agent → psysonic_core::user_agent::* Top crate keeps `crate::audio::*` paths working via `pub use psysonic_audio as audio;` — lib_commands/cli callers untouched. `stop_audio_engine` (mac process-exit cleanup) moved into the audio crate as `pub fn stop_audio_engine` since it reaches AudioEngine internals; tray.rs now re-exports the moved fn. Two small visibility promotions in engine.rs: pub(crate) fn analysis_track_id_is_current_playback → pub pub(crate) fn ranged_loudness_backfill_should_defer → pub Behaviour preserving. Cargo check + clippy --workspace clean. --- src-tauri/Cargo.lock | 29 +++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/crates/psysonic-audio/Cargo.toml | 41 +++++++++++++++++++ .../psysonic-audio/src}/autoeq_commands.rs | 0 .../psysonic-audio/src}/codec.rs | 0 .../psysonic-audio/src}/commands.rs | 0 .../psysonic-audio/src}/decode.rs | 0 .../psysonic-audio/src}/dev_io.rs | 0 .../psysonic-audio/src}/device_commands.rs | 0 .../psysonic-audio/src}/device_watcher.rs | 0 .../psysonic-audio/src}/engine.rs | 21 ++++++++-- .../psysonic-audio/src}/helpers.rs | 18 ++++---- .../psysonic-audio/src}/ipc.rs | 0 .../psysonic-audio/src/lib.rs} | 13 +++--- .../psysonic-audio/src}/mix_commands.rs | 0 .../psysonic-audio/src}/play_input.rs | 6 +-- .../psysonic-audio/src}/power_notify_linux.rs | 0 .../psysonic-audio/src}/power_notify_win.rs | 0 .../psysonic-audio/src}/power_resume.rs | 0 .../psysonic-audio/src}/preload_commands.rs | 4 +- .../psysonic-audio/src}/preview.rs | 2 +- .../psysonic-audio/src}/progress_task.rs | 0 .../psysonic-audio/src}/radio_commands.rs | 0 .../psysonic-audio/src}/sources.rs | 0 .../psysonic-audio/src}/state.rs | 0 .../psysonic-audio/src}/stream/icy.rs | 0 .../psysonic-audio/src}/stream/local_file.rs | 0 .../psysonic-audio/src}/stream/mod.rs | 0 .../psysonic-audio/src}/stream/radio.rs | 0 .../psysonic-audio/src}/stream/ranged_http.rs | 18 ++++---- .../psysonic-audio/src}/stream/reader.rs | 0 .../src}/stream/track_stream.rs | 8 ++-- .../psysonic-audio/src}/transport_commands.rs | 0 src-tauri/src/lib.rs | 5 +-- src-tauri/src/lib_commands/sync/tray.rs | 14 +------ 35 files changed, 126 insertions(+), 54 deletions(-) create mode 100644 src-tauri/crates/psysonic-audio/Cargo.toml rename src-tauri/{src/audio => crates/psysonic-audio/src}/autoeq_commands.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/codec.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/commands.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/decode.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/dev_io.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/device_commands.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/device_watcher.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/engine.rs (95%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/helpers.rs (97%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/ipc.rs (100%) rename src-tauri/{src/audio/mod.rs => crates/psysonic-audio/src/lib.rs} (73%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/mix_commands.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/play_input.rs (98%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/power_notify_linux.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/power_notify_win.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/power_resume.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/preload_commands.rs (91%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/preview.rs (99%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/progress_task.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/radio_commands.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/sources.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/state.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/stream/icy.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/stream/local_file.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/stream/mod.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/stream/radio.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/stream/ranged_http.rs (94%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/stream/reader.rs (100%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/stream/track_stream.rs (94%) rename src-tauri/{src/audio => crates/psysonic-audio/src}/transport_commands.rs (100%) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index aa31bd23..50c88e87 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3609,6 +3609,7 @@ dependencies = [ "lofty", "md5", "psysonic-analysis", + "psysonic-audio", "psysonic-core", "reqwest", "ringbuf", @@ -3656,6 +3657,34 @@ dependencies = [ "tokio", ] +[[package]] +name = "psysonic-audio" +version = "1.46.0-dev" +dependencies = [ + "biquad", + "dasp_sample", + "futures-util", + "id3", + "libc", + "lofty", + "md5", + "psysonic-analysis", + "psysonic-core", + "reqwest", + "ringbuf", + "rodio", + "serde", + "serde_json", + "symphonia", + "symphonia-adapter-libopus", + "tauri", + "thread-priority", + "tokio", + "url", + "windows 0.62.2", + "zbus 5.15.0", +] + [[package]] name = "psysonic-core" version = "1.46.0-dev" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b67f5db7..60ffda8c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ tauri-build = { version = "2", features = [] } [dependencies] psysonic-core = { path = "crates/psysonic-core" } psysonic-analysis = { path = "crates/psysonic-analysis" } +psysonic-audio = { path = "crates/psysonic-audio" } tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri-plugin-single-instance = "2" tauri-plugin-shell = "2" diff --git a/src-tauri/crates/psysonic-audio/Cargo.toml b/src-tauri/crates/psysonic-audio/Cargo.toml new file mode 100644 index 00000000..c662c2b9 --- /dev/null +++ b/src-tauri/crates/psysonic-audio/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "psysonic-audio" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +psysonic-core = { path = "../psysonic-core" } +psysonic-analysis = { path = "../psysonic-analysis" } + +tauri = { version = "2" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt", "time", "sync"] } +reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "rustls", "blocking"] } +futures-util = "0.3" +rodio = { version = "0.22", default-features = false, features = ["playback", "symphonia-all"] } +symphonia = { version = "0.5", default-features = false, features = ["flac", "mp3", "pcm", "aac", "alac", "isomp4", "vorbis", "ogg", "wav", "adpcm"] } +symphonia-adapter-libopus = "0.2.9" +ringbuf = "0.5" +biquad = "0.6" +dasp_sample = "0.11.0" +md5 = "0.8" +url = "2" +thread-priority = "3" +lofty = "0.24" +id3 = "1.16.4" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(target_os = "linux")'.dependencies] +zbus = { version = "5.15", default-features = false, features = ["blocking-api"] } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_System_Com", + "Win32_System_Threading", +] } diff --git a/src-tauri/src/audio/autoeq_commands.rs b/src-tauri/crates/psysonic-audio/src/autoeq_commands.rs similarity index 100% rename from src-tauri/src/audio/autoeq_commands.rs rename to src-tauri/crates/psysonic-audio/src/autoeq_commands.rs diff --git a/src-tauri/src/audio/codec.rs b/src-tauri/crates/psysonic-audio/src/codec.rs similarity index 100% rename from src-tauri/src/audio/codec.rs rename to src-tauri/crates/psysonic-audio/src/codec.rs diff --git a/src-tauri/src/audio/commands.rs b/src-tauri/crates/psysonic-audio/src/commands.rs similarity index 100% rename from src-tauri/src/audio/commands.rs rename to src-tauri/crates/psysonic-audio/src/commands.rs diff --git a/src-tauri/src/audio/decode.rs b/src-tauri/crates/psysonic-audio/src/decode.rs similarity index 100% rename from src-tauri/src/audio/decode.rs rename to src-tauri/crates/psysonic-audio/src/decode.rs diff --git a/src-tauri/src/audio/dev_io.rs b/src-tauri/crates/psysonic-audio/src/dev_io.rs similarity index 100% rename from src-tauri/src/audio/dev_io.rs rename to src-tauri/crates/psysonic-audio/src/dev_io.rs diff --git a/src-tauri/src/audio/device_commands.rs b/src-tauri/crates/psysonic-audio/src/device_commands.rs similarity index 100% rename from src-tauri/src/audio/device_commands.rs rename to src-tauri/crates/psysonic-audio/src/device_commands.rs diff --git a/src-tauri/src/audio/device_watcher.rs b/src-tauri/crates/psysonic-audio/src/device_watcher.rs similarity index 100% rename from src-tauri/src/audio/device_watcher.rs rename to src-tauri/crates/psysonic-audio/src/device_watcher.rs diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/crates/psysonic-audio/src/engine.rs similarity index 95% rename from src-tauri/src/audio/engine.rs rename to src-tauri/crates/psysonic-audio/src/engine.rs index a554c05f..b69b00c1 100644 --- a/src-tauri/src/audio/engine.rs +++ b/src-tauri/crates/psysonic-audio/src/engine.rs @@ -65,7 +65,7 @@ pub struct AudioEngine { pub gapless_switch_at: Arc, /// Active radio session state. None for regular (non-radio) tracks. /// Dropping the value aborts the HTTP download task via RadioLiveState::Drop. - pub(crate) radio_state: Mutex>, + pub(crate) radio_state: Mutex>, /// URL last committed to `AudioCurrent` — used so `audio_update_replay_gain` can /// resolve LUFS / startup trim when the frontend passes `loudnessGainDb: null` /// (otherwise `compute_gain` would treat that as unity gain and playback "jumps"). @@ -345,7 +345,7 @@ pub fn create_engine() -> (AudioEngine, std::thread::JoinHandle<()>) { reqwest::Client::builder() .timeout(Duration::from_secs(30)) .use_rustls_tls() - .user_agent(crate::subsonic_wire_user_agent()) + .user_agent(psysonic_core::user_agent::subsonic_wire_user_agent()) .build() .unwrap_or_default(), )), @@ -381,7 +381,7 @@ pub fn create_engine() -> (AudioEngine, std::thread::JoinHandle<()>) { } /// `analysis_enqueue_seed_from_url` should bail while this track's ranged HTTP buffer /// is still filling — playback will seed on completion with the same bytes. -pub(crate) fn ranged_loudness_backfill_should_defer(engine: &AudioEngine, track_id: &str) -> bool { +pub fn ranged_loudness_backfill_should_defer(engine: &AudioEngine, track_id: &str) -> bool { let tid = track_id.trim(); if tid.is_empty() { return false; @@ -392,9 +392,22 @@ pub(crate) fn ranged_loudness_backfill_should_defer(engine: &AudioEngine, track_ matches!(&*g, Some((t, _)) if t.as_str() == tid) } +/// Stops the Rust audio engine cleanly (mirrors the logic in `audio_stop`). +/// Called before process exit on macOS to ensure audio stops immediately. +pub fn stop_audio_engine(app: &tauri::AppHandle) { + use std::sync::atomic::Ordering; + use tauri::Manager; + let engine = app.state::(); + engine.generation.fetch_add(1, Ordering::SeqCst); + *engine.chained_info.lock().unwrap() = None; + drop(engine.radio_state.lock().unwrap().take()); + let mut cur = engine.current.lock().unwrap(); + if let Some(sink) = cur.sink.take() { sink.stop(); } +} + /// Subsonic id pinned for the playing source (`audio_play`). Used to prioritize /// HTTP loudness backfill for the track the user is listening to. -pub(crate) fn analysis_track_id_is_current_playback(engine: &AudioEngine, track_id: &str) -> bool { +pub fn analysis_track_id_is_current_playback(engine: &AudioEngine, track_id: &str) -> bool { let needle = track_id.trim(); if needle.is_empty() { return false; diff --git a/src-tauri/src/audio/helpers.rs b/src-tauri/crates/psysonic-audio/src/helpers.rs similarity index 97% rename from src-tauri/src/audio/helpers.rs rename to src-tauri/crates/psysonic-audio/src/helpers.rs index 40a2a915..aa6f2485 100644 --- a/src-tauri/src/audio/helpers.rs +++ b/src-tauri/crates/psysonic-audio/src/helpers.rs @@ -8,8 +8,8 @@ use rodio::Player; use serde::Serialize; use tauri::{AppHandle, Emitter, Manager}; -use crate::audio::engine::AudioEngine; -use crate::audio::ipc::{ +use crate::engine::AudioEngine; +use crate::ipc::{ partial_loudness_should_emit, PartialLoudnessPayload, PARTIAL_LOUDNESS_DELTA_THRESHOLD_DB, }; @@ -336,7 +336,7 @@ pub(crate) fn resolve_loudness_gain_from_cache_impl( } return None; }; - let Some(cache) = app.try_state::() else { + let Some(cache) = app.try_state::() else { if opts.log_soft_misses { crate::app_deprintln!( "[normalization] resolve_loudness_gain source=no-analysis-cache track_id={}", @@ -351,7 +351,7 @@ pub(crate) fn resolve_loudness_gain_from_cache_impl( } match cache.get_latest_loudness_for_track(&track_id) { Ok(Some(row)) if row.integrated_lufs.is_finite() => { - let recommended = crate::analysis_cache::recommended_gain_for_target( + let recommended = psysonic_analysis::analysis_cache::recommended_gain_for_target( row.integrated_lufs, row.true_peak, target_lufs as f64, @@ -404,7 +404,7 @@ const LOUDNESS_PLACEHOLDER_INTEGRATED_LUFS: f64 = -14.0; pub(crate) fn loudness_gain_placeholder_until_cache(target_lufs: f32, pre_analysis_attenuation_db: f32) -> f32 { let pre = pre_analysis_attenuation_db.clamp(-24.0, 0.0).min(0.0); // `true_peak = 0.0` skips the headroom cap until integrated measurement exists. - let pivot = crate::analysis_cache::recommended_gain_for_target( + let pivot = psysonic_analysis::analysis_cache::recommended_gain_for_target( LOUDNESS_PLACEHOLDER_INTEGRATED_LUFS, 0.0, f64::from(target_lufs), @@ -551,7 +551,7 @@ pub(crate) async fn fetch_data( return Ok(Some(data)); } - let response = crate::audio::engine::audio_http_client(&state).get(url).send().await.map_err(|e| e.to_string())?; + let response = crate::engine::audio_http_client(&state).get(url).send().await.map_err(|e| e.to_string())?; let status = response.status(); let ct = response.headers() .get(reqwest::header::CONTENT_TYPE) @@ -604,7 +604,7 @@ pub(crate) fn spawn_analysis_seed_from_in_memory_bytes( let Some(track_id) = cache_track_id.map(str::trim).filter(|s| !s.is_empty()) else { return; }; - if bytes.is_empty() || bytes.len() > crate::audio::stream::TRACK_STREAM_PROMOTE_MAX_BYTES { + if bytes.is_empty() || bytes.len() > crate::stream::TRACK_STREAM_PROMOTE_MAX_BYTES { return; } let track_id = track_id.to_string(); @@ -616,12 +616,12 @@ pub(crate) fn spawn_analysis_seed_from_in_memory_bytes( track_id, bytes.len() as f64 / (1024.0 * 1024.0) ); - let high = crate::audio::engine::analysis_seed_high_priority_for_track(&app, &track_id); + let high = crate::engine::analysis_seed_high_priority_for_track(&app, &track_id); tokio::spawn(async move { if gen_arc.load(Ordering::SeqCst) != gen { return; } - if let Err(e) = crate::submit_analysis_cpu_seed(app.clone(), track_id.clone(), bytes, high).await { + if let Err(e) = psysonic_analysis::analysis_runtime::submit_analysis_cpu_seed(app.clone(), track_id.clone(), bytes, high).await { crate::app_eprintln!( "[analysis] in-memory play path seed failed for {}: {}", track_id, diff --git a/src-tauri/src/audio/ipc.rs b/src-tauri/crates/psysonic-audio/src/ipc.rs similarity index 100% rename from src-tauri/src/audio/ipc.rs rename to src-tauri/crates/psysonic-audio/src/ipc.rs diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/crates/psysonic-audio/src/lib.rs similarity index 73% rename from src-tauri/src/audio/mod.rs rename to src-tauri/crates/psysonic-audio/src/lib.rs index f03fc935..26f7b4db 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/crates/psysonic-audio/src/lib.rs @@ -1,7 +1,10 @@ -//! Audio playback: Symphonia decode, rodio output, HTTP radio/streaming, gapless, previews. -//! -//! Implementation is split into submodules (`sources`, `decode`, `stream`, `commands`, …) -//! for navigation; behavior matches the historical single `audio.rs` file. +//! `psysonic-audio` — Symphonia decode, rodio output, HTTP radio/streaming, +//! gapless, previews. Submodules (`sources`, `decode`, `stream`, `commands`, …) +//! preserve the historical single `audio.rs` partitioning. + +// Re-export the logging facade so submodules can keep using +// `crate::app_eprintln!()` / `crate::app_deprintln!()`. +pub use psysonic_core::{app_deprintln, app_eprintln, logging}; pub mod autoeq_commands; mod codec; @@ -51,4 +54,4 @@ pub fn register_post_sleep_audio_recovery(app: tauri::AppHandle) { let _ = app; } -pub(crate) use engine::{analysis_track_id_is_current_playback, ranged_loudness_backfill_should_defer}; +pub use engine::{analysis_track_id_is_current_playback, ranged_loudness_backfill_should_defer, stop_audio_engine}; diff --git a/src-tauri/src/audio/mix_commands.rs b/src-tauri/crates/psysonic-audio/src/mix_commands.rs similarity index 100% rename from src-tauri/src/audio/mix_commands.rs rename to src-tauri/crates/psysonic-audio/src/mix_commands.rs diff --git a/src-tauri/src/audio/play_input.rs b/src-tauri/crates/psysonic-audio/src/play_input.rs similarity index 98% rename from src-tauri/src/audio/play_input.rs rename to src-tauri/crates/psysonic-audio/src/play_input.rs index 2a562780..ea9dfa87 100644 --- a/src-tauri/src/audio/play_input.rs +++ b/src-tauri/crates/psysonic-audio/src/play_input.rs @@ -140,7 +140,7 @@ fn open_local_file_input( ); if let Some(seed_id) = ctx.cache_id_for_tasks { let skip_cpu_seed = app - .try_state::() + .try_state::() .map(|c| c.cpu_seed_redundant_for_track(seed_id).unwrap_or(false)) .unwrap_or(false); if !skip_cpu_seed { @@ -174,9 +174,9 @@ fn open_local_file_input( seed_id, data.len() as f64 / (1024.0 * 1024.0) ); - let high = crate::audio::engine::analysis_seed_high_priority_for_track(&app_seed, &seed_id); + let high = crate::engine::analysis_seed_high_priority_for_track(&app_seed, &seed_id); if let Err(e) = - crate::submit_analysis_cpu_seed(app_seed.clone(), seed_id.clone(), data, high).await + psysonic_analysis::analysis_runtime::submit_analysis_cpu_seed(app_seed.clone(), seed_id.clone(), data, high).await { crate::app_eprintln!( "[analysis] local-file seed failed for {}: {}", diff --git a/src-tauri/src/audio/power_notify_linux.rs b/src-tauri/crates/psysonic-audio/src/power_notify_linux.rs similarity index 100% rename from src-tauri/src/audio/power_notify_linux.rs rename to src-tauri/crates/psysonic-audio/src/power_notify_linux.rs diff --git a/src-tauri/src/audio/power_notify_win.rs b/src-tauri/crates/psysonic-audio/src/power_notify_win.rs similarity index 100% rename from src-tauri/src/audio/power_notify_win.rs rename to src-tauri/crates/psysonic-audio/src/power_notify_win.rs diff --git a/src-tauri/src/audio/power_resume.rs b/src-tauri/crates/psysonic-audio/src/power_resume.rs similarity index 100% rename from src-tauri/src/audio/power_resume.rs rename to src-tauri/crates/psysonic-audio/src/power_resume.rs diff --git a/src-tauri/src/audio/preload_commands.rs b/src-tauri/crates/psysonic-audio/src/preload_commands.rs similarity index 91% rename from src-tauri/src/audio/preload_commands.rs rename to src-tauri/crates/psysonic-audio/src/preload_commands.rs index 4a6f66b7..097f2198 100644 --- a/src-tauri/src/audio/preload_commands.rs +++ b/src-tauri/crates/psysonic-audio/src/preload_commands.rs @@ -55,8 +55,8 @@ pub async fn audio_preload( 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 { + let high = crate::engine::analysis_track_id_is_current_playback(&state, &track_id); + if let Err(e) = psysonic_analysis::analysis_runtime::submit_analysis_cpu_seed(app.clone(), track_id.clone(), data.clone(), high).await { crate::app_eprintln!("[analysis] preload seed failed for {}: {}", track_id, e); } } diff --git a/src-tauri/src/audio/preview.rs b/src-tauri/crates/psysonic-audio/src/preview.rs similarity index 99% rename from src-tauri/src/audio/preview.rs rename to src-tauri/crates/psysonic-audio/src/preview.rs index 82ea29b9..b52014b8 100644 --- a/src-tauri/src/audio/preview.rs +++ b/src-tauri/crates/psysonic-audio/src/preview.rs @@ -144,7 +144,7 @@ pub async fn audio_preview_play( let preview_http = reqwest::Client::builder() .timeout(Duration::from_secs(300)) .use_rustls_tls() - .user_agent(crate::subsonic_wire_user_agent()) + .user_agent(psysonic_core::user_agent::subsonic_wire_user_agent()) .build() .unwrap_or_else(|_| audio_http_client(&state)); let bytes = preview_http diff --git a/src-tauri/src/audio/progress_task.rs b/src-tauri/crates/psysonic-audio/src/progress_task.rs similarity index 100% rename from src-tauri/src/audio/progress_task.rs rename to src-tauri/crates/psysonic-audio/src/progress_task.rs diff --git a/src-tauri/src/audio/radio_commands.rs b/src-tauri/crates/psysonic-audio/src/radio_commands.rs similarity index 100% rename from src-tauri/src/audio/radio_commands.rs rename to src-tauri/crates/psysonic-audio/src/radio_commands.rs diff --git a/src-tauri/src/audio/sources.rs b/src-tauri/crates/psysonic-audio/src/sources.rs similarity index 100% rename from src-tauri/src/audio/sources.rs rename to src-tauri/crates/psysonic-audio/src/sources.rs diff --git a/src-tauri/src/audio/state.rs b/src-tauri/crates/psysonic-audio/src/state.rs similarity index 100% rename from src-tauri/src/audio/state.rs rename to src-tauri/crates/psysonic-audio/src/state.rs diff --git a/src-tauri/src/audio/stream/icy.rs b/src-tauri/crates/psysonic-audio/src/stream/icy.rs similarity index 100% rename from src-tauri/src/audio/stream/icy.rs rename to src-tauri/crates/psysonic-audio/src/stream/icy.rs diff --git a/src-tauri/src/audio/stream/local_file.rs b/src-tauri/crates/psysonic-audio/src/stream/local_file.rs similarity index 100% rename from src-tauri/src/audio/stream/local_file.rs rename to src-tauri/crates/psysonic-audio/src/stream/local_file.rs diff --git a/src-tauri/src/audio/stream/mod.rs b/src-tauri/crates/psysonic-audio/src/stream/mod.rs similarity index 100% rename from src-tauri/src/audio/stream/mod.rs rename to src-tauri/crates/psysonic-audio/src/stream/mod.rs diff --git a/src-tauri/src/audio/stream/radio.rs b/src-tauri/crates/psysonic-audio/src/stream/radio.rs similarity index 100% rename from src-tauri/src/audio/stream/radio.rs rename to src-tauri/crates/psysonic-audio/src/stream/radio.rs diff --git a/src-tauri/src/audio/stream/ranged_http.rs b/src-tauri/crates/psysonic-audio/src/stream/ranged_http.rs similarity index 94% rename from src-tauri/src/audio/stream/ranged_http.rs rename to src-tauri/crates/psysonic-audio/src/stream/ranged_http.rs index 3de8ea73..041f542d 100644 --- a/src-tauri/src/audio/stream/ranged_http.rs +++ b/src-tauri/crates/psysonic-audio/src/stream/ranged_http.rs @@ -279,9 +279,9 @@ pub(crate) async fn ranged_download_task( } downloaded += n; downloaded_to.store(downloaded, Ordering::SeqCst); - if downloaded >= crate::audio::helpers::PARTIAL_LOUDNESS_MIN_BYTES + if downloaded >= crate::helpers::PARTIAL_LOUDNESS_MIN_BYTES && total_size > 0 - && last_partial_loudness_emit.elapsed() >= Duration::from_millis(crate::audio::helpers::PARTIAL_LOUDNESS_EMIT_INTERVAL_MS) + && last_partial_loudness_emit.elapsed() >= Duration::from_millis(crate::helpers::PARTIAL_LOUDNESS_EMIT_INTERVAL_MS) { last_partial_loudness_emit = Instant::now(); if normalization_engine.load(Ordering::Relaxed) == 2 { @@ -289,14 +289,14 @@ pub(crate) async fn ranged_download_task( let start_db = f32::from_bits(loudness_pre_analysis_attenuation_db.load(Ordering::Relaxed)) .clamp(-24.0, 0.0); if let Some(provisional_db) = - crate::audio::helpers::provisional_loudness_gain_from_progress(downloaded, total_size, target_lufs, start_db) + crate::helpers::provisional_loudness_gain_from_progress(downloaded, total_size, target_lufs, start_db) { - let track_key = crate::audio::helpers::playback_identity(&url).unwrap_or_else(|| url.clone()); - if crate::audio::ipc::partial_loudness_should_emit(&track_key, provisional_db) { + let track_key = crate::helpers::playback_identity(&url).unwrap_or_else(|| url.clone()); + if crate::ipc::partial_loudness_should_emit(&track_key, provisional_db) { let _ = app.emit( "analysis:loudness-partial", - crate::audio::ipc::PartialLoudnessPayload { - track_id: crate::audio::helpers::playback_identity(&url), + crate::ipc::PartialLoudnessPayload { + track_id: crate::helpers::playback_identity(&url), gain_db: provisional_db, target_lufs, is_partial: true, @@ -368,8 +368,8 @@ pub(crate) async fn ranged_download_task( ); } if let Some(track_id) = cache_track_id { - let high = crate::audio::engine::analysis_seed_high_priority_for_track(&app, &track_id); - if let Err(e) = crate::submit_analysis_cpu_seed(app.clone(), track_id.clone(), data.clone(), high).await { + let high = crate::engine::analysis_seed_high_priority_for_track(&app, &track_id); + if let Err(e) = psysonic_analysis::analysis_runtime::submit_analysis_cpu_seed(app.clone(), track_id.clone(), data.clone(), high).await { crate::app_eprintln!("[analysis] ranged seed failed for {}: {}", track_id, e); } } diff --git a/src-tauri/src/audio/stream/reader.rs b/src-tauri/crates/psysonic-audio/src/stream/reader.rs similarity index 100% rename from src-tauri/src/audio/stream/reader.rs rename to src-tauri/crates/psysonic-audio/src/stream/reader.rs diff --git a/src-tauri/src/audio/stream/track_stream.rs b/src-tauri/crates/psysonic-audio/src/stream/track_stream.rs similarity index 94% rename from src-tauri/src/audio/stream/track_stream.rs rename to src-tauri/crates/psysonic-audio/src/stream/track_stream.rs index 1dcfe3db..e5dd5dd2 100644 --- a/src-tauri/src/audio/stream/track_stream.rs +++ b/src-tauri/crates/psysonic-audio/src/stream/track_stream.rs @@ -132,7 +132,7 @@ pub(crate) async fn track_download_task( } } if !capture_over_limit - && last_partial_loudness_emit.elapsed() >= Duration::from_millis(crate::audio::helpers::PARTIAL_LOUDNESS_EMIT_INTERVAL_MS) + && last_partial_loudness_emit.elapsed() >= Duration::from_millis(crate::helpers::PARTIAL_LOUDNESS_EMIT_INTERVAL_MS) { last_partial_loudness_emit = Instant::now(); if normalization_engine.load(Ordering::Relaxed) == 2 { @@ -141,7 +141,7 @@ pub(crate) async fn track_download_task( loudness_pre_analysis_attenuation_db.load(Ordering::Relaxed), ) .clamp(-24.0, 0.0); - crate::audio::helpers::emit_partial_loudness_from_bytes(&app, &url, &capture, target_lufs, pre_db); + crate::helpers::emit_partial_loudness_from_bytes(&app, &url, &capture, target_lufs, pre_db); } } offset += pushed; @@ -160,9 +160,9 @@ pub(crate) async fn track_download_task( track_id, capture.len() as f64 / (1024.0 * 1024.0) ); - let high = crate::audio::engine::analysis_seed_high_priority_for_track(&app, &track_id); + let high = crate::engine::analysis_seed_high_priority_for_track(&app, &track_id); if let Err(e) = - crate::submit_analysis_cpu_seed(app.clone(), track_id.clone(), capture.clone(), high).await + psysonic_analysis::analysis_runtime::submit_analysis_cpu_seed(app.clone(), track_id.clone(), capture.clone(), high).await { crate::app_eprintln!("[analysis] track seed failed for {}: {}", track_id, e); } diff --git a/src-tauri/src/audio/transport_commands.rs b/src-tauri/crates/psysonic-audio/src/transport_commands.rs similarity index 100% rename from src-tauri/src/audio/transport_commands.rs rename to src-tauri/crates/psysonic-audio/src/transport_commands.rs diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f117d51e..09d7bbbe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,6 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -mod audio; pub mod cli; mod discord; mod lib_commands; @@ -12,9 +11,7 @@ pub use psysonic_core::user_agent::{ default_subsonic_wire_user_agent, runtime_subsonic_wire_user_agent, subsonic_wire_user_agent, }; pub use psysonic_analysis::{analysis_cache, analysis_runtime}; -// `crate::submit_analysis_cpu_seed` shorthand kept so the audio modules don't -// have to spell out `psysonic_analysis::analysis_runtime::...` until M3. -pub(crate) use psysonic_analysis::analysis_runtime::submit_analysis_cpu_seed; +pub use psysonic_audio as audio; #[cfg(target_os = "windows")] mod taskbar_win; mod tray_runtime; diff --git a/src-tauri/src/lib_commands/sync/tray.rs b/src-tauri/src/lib_commands/sync/tray.rs index 82f76d5f..efdd3978 100644 --- a/src-tauri/src/lib_commands/sync/tray.rs +++ b/src-tauri/src/lib_commands/sync/tray.rs @@ -1,12 +1,9 @@ -use std::sync::atomic::Ordering; - use tauri::menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem}; use tauri::tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent}; use tauri::{Emitter, Manager}; #[cfg(not(target_os = "windows"))] use tauri::tray::MouseButtonState; -use crate::audio; use crate::tray_runtime::{ tray_state_icon, TrayMenuItems, TrayMenuItemsState, TrayMenuLabels, TrayMenuLabelsState, TrayPlaybackState, TrayState, TrayTooltip, @@ -355,16 +352,7 @@ pub(crate) fn toggle_tray_icon( Ok(()) } -/// Stops the Rust audio engine cleanly (mirrors the logic in `audio_stop`). -/// Called before process exit on macOS to ensure audio stops immediately. -pub(crate) fn stop_audio_engine(app: &tauri::AppHandle) { - let engine = app.state::(); - engine.generation.fetch_add(1, Ordering::SeqCst); - *engine.chained_info.lock().unwrap() = None; - drop(engine.radio_state.lock().unwrap().take()); - let mut cur = engine.current.lock().unwrap(); - if let Some(sink) = cur.sink.take() { sink.stop(); } -} +pub(crate) use crate::audio::stop_audio_engine; /// Returns `true` if running under a tiling window manager (Hyprland, Sway, i3, /// bspwm, AwesomeWM, Openbox, etc.). Detection is based on environment variables