mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +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",
|
"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"
|
||||||
|
|||||||
@@ -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};
|
||||||
+3
-3
@@ -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 {}: {}",
|
||||||
+2
-2
@@ -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
|
||||||
+9
-9
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -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,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,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
|
||||||
|
|||||||
Reference in New Issue
Block a user