refactor: extract psysonic-audio crate (M3/7)

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.
This commit is contained in:
Psychotoxical
2026-05-09 13:33:44 +02:00
parent ff456dd823
commit 41e75663f1
35 changed files with 126 additions and 54 deletions
+29
View File
@@ -3609,6 +3609,7 @@ dependencies = [
"lofty", "lofty",
"md5", "md5",
"psysonic-analysis", "psysonic-analysis",
"psysonic-audio",
"psysonic-core", "psysonic-core",
"reqwest", "reqwest",
"ringbuf", "ringbuf",
@@ -3656,6 +3657,34 @@ dependencies = [
"tokio", "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]] [[package]]
name = "psysonic-core" name = "psysonic-core"
version = "1.46.0-dev" version = "1.46.0-dev"
+1
View File
@@ -32,6 +32,7 @@ tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
psysonic-core = { path = "crates/psysonic-core" } psysonic-core = { path = "crates/psysonic-core" }
psysonic-analysis = { path = "crates/psysonic-analysis" } psysonic-analysis = { path = "crates/psysonic-analysis" }
psysonic-audio = { path = "crates/psysonic-audio" }
tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-single-instance = "2" tauri-plugin-single-instance = "2"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
@@ -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",
] }
@@ -65,7 +65,7 @@ pub struct AudioEngine {
pub gapless_switch_at: Arc<AtomicU64>, pub gapless_switch_at: Arc<AtomicU64>,
/// Active radio session state. None for regular (non-radio) tracks. /// Active radio session state. None for regular (non-radio) tracks.
/// Dropping the value aborts the HTTP download task via RadioLiveState::Drop. /// Dropping the value aborts the HTTP download task via RadioLiveState::Drop.
pub(crate) radio_state: Mutex<Option<crate::audio::stream::RadioLiveState>>, pub(crate) radio_state: Mutex<Option<crate::stream::RadioLiveState>>,
/// URL last committed to `AudioCurrent` — used so `audio_update_replay_gain` can /// URL last committed to `AudioCurrent` — used so `audio_update_replay_gain` can
/// resolve LUFS / startup trim when the frontend passes `loudnessGainDb: null` /// resolve LUFS / startup trim when the frontend passes `loudnessGainDb: null`
/// (otherwise `compute_gain` would treat that as unity gain and playback "jumps"). /// (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() reqwest::Client::builder()
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
.use_rustls_tls() .use_rustls_tls()
.user_agent(crate::subsonic_wire_user_agent()) .user_agent(psysonic_core::user_agent::subsonic_wire_user_agent())
.build() .build()
.unwrap_or_default(), .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 /// `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. /// 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(); let tid = track_id.trim();
if tid.is_empty() { if tid.is_empty() {
return false; 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) 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::<AudioEngine>();
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 /// Subsonic id pinned for the playing source (`audio_play`). Used to prioritize
/// HTTP loudness backfill for the track the user is listening to. /// 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(); let needle = track_id.trim();
if needle.is_empty() { if needle.is_empty() {
return false; return false;
@@ -8,8 +8,8 @@ use rodio::Player;
use serde::Serialize; use serde::Serialize;
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
use crate::audio::engine::AudioEngine; use crate::engine::AudioEngine;
use crate::audio::ipc::{ use crate::ipc::{
partial_loudness_should_emit, PartialLoudnessPayload, PARTIAL_LOUDNESS_DELTA_THRESHOLD_DB, 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; return None;
}; };
let Some(cache) = app.try_state::<crate::analysis_cache::AnalysisCache>() else { let Some(cache) = app.try_state::<psysonic_analysis::analysis_cache::AnalysisCache>() else {
if opts.log_soft_misses { if opts.log_soft_misses {
crate::app_deprintln!( crate::app_deprintln!(
"[normalization] resolve_loudness_gain source=no-analysis-cache track_id={}", "[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) { match cache.get_latest_loudness_for_track(&track_id) {
Ok(Some(row)) if row.integrated_lufs.is_finite() => { 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.integrated_lufs,
row.true_peak, row.true_peak,
target_lufs as f64, 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 { 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); 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. // `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, LOUDNESS_PLACEHOLDER_INTEGRATED_LUFS,
0.0, 0.0,
f64::from(target_lufs), f64::from(target_lufs),
@@ -551,7 +551,7 @@ pub(crate) async fn fetch_data(
return Ok(Some(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 status = response.status();
let ct = response.headers() let ct = response.headers()
.get(reqwest::header::CONTENT_TYPE) .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 { let Some(track_id) = cache_track_id.map(str::trim).filter(|s| !s.is_empty()) else {
return; 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; return;
} }
let track_id = track_id.to_string(); let track_id = track_id.to_string();
@@ -616,12 +616,12 @@ pub(crate) fn spawn_analysis_seed_from_in_memory_bytes(
track_id, track_id,
bytes.len() as f64 / (1024.0 * 1024.0) 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 { tokio::spawn(async move {
if gen_arc.load(Ordering::SeqCst) != gen { if gen_arc.load(Ordering::SeqCst) != gen {
return; 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!( crate::app_eprintln!(
"[analysis] in-memory play path seed failed for {}: {}", "[analysis] in-memory play path seed failed for {}: {}",
track_id, track_id,
@@ -1,7 +1,10 @@
//! Audio playback: Symphonia decode, rodio output, HTTP radio/streaming, gapless, previews. //! `psysonic-audio` — Symphonia decode, rodio output, HTTP radio/streaming,
//! //! gapless, previews. Submodules (`sources`, `decode`, `stream`, `commands`, …)
//! Implementation is split into submodules (`sources`, `decode`, `stream`, `commands`, …) //! preserve the historical single `audio.rs` partitioning.
//! for navigation; behavior matches the historical single `audio.rs` file.
// 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; pub mod autoeq_commands;
mod codec; mod codec;
@@ -51,4 +54,4 @@ pub fn register_post_sleep_audio_recovery(app: tauri::AppHandle) {
let _ = app; 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};
@@ -140,7 +140,7 @@ fn open_local_file_input(
); );
if let Some(seed_id) = ctx.cache_id_for_tasks { if let Some(seed_id) = ctx.cache_id_for_tasks {
let skip_cpu_seed = app let skip_cpu_seed = app
.try_state::<crate::analysis_cache::AnalysisCache>() .try_state::<psysonic_analysis::analysis_cache::AnalysisCache>()
.map(|c| c.cpu_seed_redundant_for_track(seed_id).unwrap_or(false)) .map(|c| c.cpu_seed_redundant_for_track(seed_id).unwrap_or(false))
.unwrap_or(false); .unwrap_or(false);
if !skip_cpu_seed { if !skip_cpu_seed {
@@ -174,9 +174,9 @@ fn open_local_file_input(
seed_id, seed_id,
data.len() as f64 / (1024.0 * 1024.0) 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) = 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!( crate::app_eprintln!(
"[analysis] local-file seed failed for {}: {}", "[analysis] local-file seed failed for {}: {}",
@@ -55,8 +55,8 @@ pub async fn audio_preload(
track_id, track_id,
data.len() as f64 / (1024.0 * 1024.0) data.len() as f64 / (1024.0 * 1024.0)
); );
let high = crate::audio::engine::analysis_track_id_is_current_playback(&state, &track_id); let high = crate::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 { 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); crate::app_eprintln!("[analysis] preload seed failed for {}: {}", track_id, e);
} }
} }
@@ -144,7 +144,7 @@ pub async fn audio_preview_play(
let preview_http = reqwest::Client::builder() let preview_http = reqwest::Client::builder()
.timeout(Duration::from_secs(300)) .timeout(Duration::from_secs(300))
.use_rustls_tls() .use_rustls_tls()
.user_agent(crate::subsonic_wire_user_agent()) .user_agent(psysonic_core::user_agent::subsonic_wire_user_agent())
.build() .build()
.unwrap_or_else(|_| audio_http_client(&state)); .unwrap_or_else(|_| audio_http_client(&state));
let bytes = preview_http let bytes = preview_http
@@ -279,9 +279,9 @@ pub(crate) async fn ranged_download_task(
} }
downloaded += n; downloaded += n;
downloaded_to.store(downloaded, Ordering::SeqCst); 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 && 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(); last_partial_loudness_emit = Instant::now();
if normalization_engine.load(Ordering::Relaxed) == 2 { 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)) let start_db = f32::from_bits(loudness_pre_analysis_attenuation_db.load(Ordering::Relaxed))
.clamp(-24.0, 0.0); .clamp(-24.0, 0.0);
if let Some(provisional_db) = 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()); let track_key = crate::helpers::playback_identity(&url).unwrap_or_else(|| url.clone());
if crate::audio::ipc::partial_loudness_should_emit(&track_key, provisional_db) { if crate::ipc::partial_loudness_should_emit(&track_key, provisional_db) {
let _ = app.emit( let _ = app.emit(
"analysis:loudness-partial", "analysis:loudness-partial",
crate::audio::ipc::PartialLoudnessPayload { crate::ipc::PartialLoudnessPayload {
track_id: crate::audio::helpers::playback_identity(&url), track_id: crate::helpers::playback_identity(&url),
gain_db: provisional_db, gain_db: provisional_db,
target_lufs, target_lufs,
is_partial: true, is_partial: true,
@@ -368,8 +368,8 @@ pub(crate) async fn ranged_download_task(
); );
} }
if let Some(track_id) = cache_track_id { if let Some(track_id) = cache_track_id {
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(), data.clone(), high).await { 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); crate::app_eprintln!("[analysis] ranged seed failed for {}: {}", track_id, e);
} }
} }
@@ -132,7 +132,7 @@ pub(crate) async fn track_download_task(
} }
} }
if !capture_over_limit 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(); last_partial_loudness_emit = Instant::now();
if normalization_engine.load(Ordering::Relaxed) == 2 { 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), loudness_pre_analysis_attenuation_db.load(Ordering::Relaxed),
) )
.clamp(-24.0, 0.0); .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; offset += pushed;
@@ -160,9 +160,9 @@ pub(crate) async fn track_download_task(
track_id, track_id,
capture.len() as f64 / (1024.0 * 1024.0) 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) = 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); crate::app_eprintln!("[analysis] track seed failed for {}: {}", track_id, e);
} }
+1 -4
View File
@@ -1,7 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod audio;
pub mod cli; pub mod cli;
mod discord; mod discord;
mod lib_commands; 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, default_subsonic_wire_user_agent, runtime_subsonic_wire_user_agent, subsonic_wire_user_agent,
}; };
pub use psysonic_analysis::{analysis_cache, analysis_runtime}; pub use psysonic_analysis::{analysis_cache, analysis_runtime};
// `crate::submit_analysis_cpu_seed` shorthand kept so the audio modules don't pub use psysonic_audio as audio;
// have to spell out `psysonic_analysis::analysis_runtime::...` until M3.
pub(crate) use psysonic_analysis::analysis_runtime::submit_analysis_cpu_seed;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod taskbar_win; mod taskbar_win;
mod tray_runtime; mod tray_runtime;
+1 -13
View File
@@ -1,12 +1,9 @@
use std::sync::atomic::Ordering;
use tauri::menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem}; use tauri::menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem};
use tauri::tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent}; use tauri::tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent};
use tauri::{Emitter, Manager}; use tauri::{Emitter, Manager};
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
use tauri::tray::MouseButtonState; use tauri::tray::MouseButtonState;
use crate::audio;
use crate::tray_runtime::{ use crate::tray_runtime::{
tray_state_icon, TrayMenuItems, TrayMenuItemsState, TrayMenuLabels, TrayMenuLabelsState, tray_state_icon, TrayMenuItems, TrayMenuItemsState, TrayMenuLabels, TrayMenuLabelsState,
TrayPlaybackState, TrayState, TrayTooltip, TrayPlaybackState, TrayState, TrayTooltip,
@@ -355,16 +352,7 @@ pub(crate) fn toggle_tray_icon(
Ok(()) Ok(())
} }
/// Stops the Rust audio engine cleanly (mirrors the logic in `audio_stop`). pub(crate) use crate::audio::stop_audio_engine;
/// Called before process exit on macOS to ensure audio stops immediately.
pub(crate) fn stop_audio_engine(app: &tauri::AppHandle) {
let engine = app.state::<audio::AudioEngine>();
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(); }
}
/// Returns `true` if running under a tiling window manager (Hyprland, Sway, i3, /// Returns `true` if running under a tiling window manager (Hyprland, Sway, i3,
/// bspwm, AwesomeWM, Openbox, etc.). Detection is based on environment variables /// bspwm, AwesomeWM, Openbox, etc.). Detection is based on environment variables