mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
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:
Generated
+29
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>,
|
||||
/// 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<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
|
||||
/// 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::<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
|
||||
/// 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;
|
||||
@@ -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::<crate::analysis_cache::AnalysisCache>() else {
|
||||
let Some(cache) = app.try_state::<psysonic_analysis::analysis_cache::AnalysisCache>() 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,
|
||||
@@ -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};
|
||||
+3
-3
@@ -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::<crate::analysis_cache::AnalysisCache>()
|
||||
.try_state::<psysonic_analysis::analysis_cache::AnalysisCache>()
|
||||
.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 {}: {}",
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+9
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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::<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(); }
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user