mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
refactor(audio): lift PlayInput → BuiltSource dispatch into helper
Pull the 60-LOC match in audio_play that turned a PlayInput into a fully-wrapped rodio source out of audio_play and into play_input::build_source_from_play_input. Returns a small PlaybackSource struct holding both the BuiltSource and a `is_seekable` flag (only the Streaming variant is non-seekable). Behaviour preserved verbatim — same build_source / build_streaming_source calls, same target_rate=0 (no app-level resampling), same spawn_blocking decoder build for Seekable+Streaming, same error path (`audio:error` emit + propagate). audio/commands.rs: 642 → 600 LOC. The remaining audio_play body now reads top-to-bottom as: ghost guard → preview clear → gapless pre-chain check → bump generation → reuse-bytes prep → URL pin → format hint → **select_play_input** → gen check → loudness/RG via gain_inputs → crossfade prep → **build_source_from_play_input** → stream rate switching → sink construction + prefill → swap sink → progress task.
This commit is contained in:
@@ -10,11 +10,13 @@ use rodio::Player;
|
|||||||
use rodio::Source;
|
use rodio::Source;
|
||||||
use tauri::{AppHandle, Emitter, State};
|
use tauri::{AppHandle, Emitter, State};
|
||||||
|
|
||||||
use super::decode::{build_source, build_streaming_source, SizedDecoder};
|
use super::decode::build_source;
|
||||||
use super::engine::{audio_http_client, AudioEngine};
|
use super::engine::{audio_http_client, AudioEngine};
|
||||||
use super::helpers::*;
|
use super::helpers::*;
|
||||||
use super::ipc::{maybe_emit_normalization_state, NormalizationStatePayload};
|
use super::ipc::{maybe_emit_normalization_state, NormalizationStatePayload};
|
||||||
use super::play_input::{select_play_input, url_format_hint, PlayInput, PlayInputContext};
|
use super::play_input::{
|
||||||
|
build_source_from_play_input, select_play_input, url_format_hint, PlayInputContext,
|
||||||
|
};
|
||||||
use super::preview::preview_clear_for_new_main_playback;
|
use super::preview::preview_clear_for_new_main_playback;
|
||||||
use super::progress_task::spawn_progress_task;
|
use super::progress_task::spawn_progress_task;
|
||||||
use super::state::{ChainedInfo, PreloadedTrack};
|
use super::state::{ChainedInfo, PreloadedTrack};
|
||||||
@@ -216,65 +218,22 @@ pub async fn audio_play(
|
|||||||
let done_flag = Arc::new(AtomicBool::new(false));
|
let done_flag = Arc::new(AtomicBool::new(false));
|
||||||
// Reset sample counter for the new track.
|
// Reset sample counter for the new track.
|
||||||
state.samples_played.store(0, Ordering::Relaxed);
|
state.samples_played.store(0, Ordering::Relaxed);
|
||||||
// Always 0 — no application-level resampling. Rodio handles conversion to
|
let playback_source = build_source_from_play_input(
|
||||||
// the output device rate internally; we let every track play at its native rate.
|
play_input,
|
||||||
let target_rate: u32 = 0;
|
&state,
|
||||||
let mut new_is_seekable = true;
|
format_hint.as_deref(),
|
||||||
let built = match play_input {
|
done_flag.clone(),
|
||||||
PlayInput::Bytes(data) => build_source(
|
fade_in_dur,
|
||||||
data,
|
hi_res_enabled,
|
||||||
duration_hint,
|
duration_hint,
|
||||||
state.eq_gains.clone(),
|
)
|
||||||
state.eq_enabled.clone(),
|
.await
|
||||||
state.eq_pre_gain.clone(),
|
.map_err(|e| {
|
||||||
done_flag.clone(),
|
app.emit("audio:error", &e).ok();
|
||||||
fade_in_dur,
|
e
|
||||||
state.samples_played.clone(),
|
})?;
|
||||||
target_rate,
|
state.current_is_seekable.store(playback_source.is_seekable, Ordering::SeqCst);
|
||||||
format_hint.as_deref(),
|
let built = playback_source.built;
|
||||||
hi_res_enabled,
|
|
||||||
),
|
|
||||||
PlayInput::SeekableMedia { reader, format_hint, tag } => {
|
|
||||||
let decoder = tokio::task::spawn_blocking(move || {
|
|
||||||
SizedDecoder::new_streaming(reader, format_hint.as_deref(), tag)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())??;
|
|
||||||
|
|
||||||
build_streaming_source(
|
|
||||||
decoder,
|
|
||||||
duration_hint,
|
|
||||||
state.eq_gains.clone(),
|
|
||||||
state.eq_enabled.clone(),
|
|
||||||
state.eq_pre_gain.clone(),
|
|
||||||
done_flag.clone(),
|
|
||||||
fade_in_dur,
|
|
||||||
state.samples_played.clone(),
|
|
||||||
target_rate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
PlayInput::Streaming { reader, format_hint } => {
|
|
||||||
new_is_seekable = false;
|
|
||||||
let decoder = tokio::task::spawn_blocking(move || {
|
|
||||||
SizedDecoder::new_streaming(Box::new(reader), format_hint.as_deref(), "track-stream")
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())??;
|
|
||||||
|
|
||||||
build_streaming_source(
|
|
||||||
decoder,
|
|
||||||
duration_hint,
|
|
||||||
state.eq_gains.clone(),
|
|
||||||
state.eq_enabled.clone(),
|
|
||||||
state.eq_pre_gain.clone(),
|
|
||||||
done_flag.clone(),
|
|
||||||
fade_in_dur,
|
|
||||||
state.samples_played.clone(),
|
|
||||||
target_rate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.map_err(|e| { app.emit("audio:error", &e).ok(); e })?;
|
|
||||||
state.current_is_seekable.store(new_is_seekable, Ordering::SeqCst);
|
|
||||||
let source = built.source;
|
let source = built.source;
|
||||||
let duration_secs = built.duration_secs;
|
let duration_secs = built.duration_secs;
|
||||||
let output_rate = built.output_rate;
|
let output_rate = built.output_rate;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use ringbuf::{HeapCons, HeapRb};
|
|||||||
use symphonia::core::io::MediaSource;
|
use symphonia::core::io::MediaSource;
|
||||||
use tauri::{AppHandle, Emitter, Manager, State};
|
use tauri::{AppHandle, Emitter, Manager, State};
|
||||||
|
|
||||||
|
use super::decode::{build_source, build_streaming_source, BuiltSource, SizedDecoder};
|
||||||
use super::engine::{audio_http_client, AudioEngine};
|
use super::engine::{audio_http_client, AudioEngine};
|
||||||
use super::helpers::{
|
use super::helpers::{
|
||||||
content_type_to_hint, fetch_data, format_hint_from_content_disposition,
|
content_type_to_hint, fetch_data, format_hint_from_content_disposition,
|
||||||
@@ -383,3 +384,82 @@ pub(super) fn url_format_hint(url: &str) -> Option<String> {
|
|||||||
})
|
})
|
||||||
.map(|s| s.to_lowercase())
|
.map(|s| s.to_lowercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Output of `build_source_from_play_input`: the wrapped rodio source plus
|
||||||
|
/// whether the chosen source path is seekable (only the Streaming variant
|
||||||
|
/// is not).
|
||||||
|
pub(super) struct PlaybackSource {
|
||||||
|
pub(super) built: BuiltSource,
|
||||||
|
pub(super) is_seekable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatch [`PlayInput`] → fully wrapped rodio source. For Bytes the full
|
||||||
|
/// in-memory pipeline (incl. iTunSMPB scan); for SeekableMedia / Streaming
|
||||||
|
/// the streaming variant runs the decoder build on a blocking thread.
|
||||||
|
pub(super) async fn build_source_from_play_input(
|
||||||
|
play_input: PlayInput,
|
||||||
|
state: &State<'_, AudioEngine>,
|
||||||
|
format_hint: Option<&str>,
|
||||||
|
done_flag: Arc<AtomicBool>,
|
||||||
|
fade_in_dur: Duration,
|
||||||
|
hi_res_enabled: bool,
|
||||||
|
duration_hint: f64,
|
||||||
|
) -> Result<PlaybackSource, String> {
|
||||||
|
// Always 0 — no application-level resampling. Rodio handles conversion to
|
||||||
|
// the output device rate internally; we let every track play at its native rate.
|
||||||
|
let target_rate: u32 = 0;
|
||||||
|
let mut is_seekable = true;
|
||||||
|
let built = match play_input {
|
||||||
|
PlayInput::Bytes(data) => build_source(
|
||||||
|
data,
|
||||||
|
duration_hint,
|
||||||
|
state.eq_gains.clone(),
|
||||||
|
state.eq_enabled.clone(),
|
||||||
|
state.eq_pre_gain.clone(),
|
||||||
|
done_flag,
|
||||||
|
fade_in_dur,
|
||||||
|
state.samples_played.clone(),
|
||||||
|
target_rate,
|
||||||
|
format_hint,
|
||||||
|
hi_res_enabled,
|
||||||
|
),
|
||||||
|
PlayInput::SeekableMedia { reader, format_hint: media_hint, tag } => {
|
||||||
|
let decoder = tokio::task::spawn_blocking(move || {
|
||||||
|
SizedDecoder::new_streaming(reader, media_hint.as_deref(), tag)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())??;
|
||||||
|
build_streaming_source(
|
||||||
|
decoder,
|
||||||
|
duration_hint,
|
||||||
|
state.eq_gains.clone(),
|
||||||
|
state.eq_enabled.clone(),
|
||||||
|
state.eq_pre_gain.clone(),
|
||||||
|
done_flag,
|
||||||
|
fade_in_dur,
|
||||||
|
state.samples_played.clone(),
|
||||||
|
target_rate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PlayInput::Streaming { reader, format_hint: stream_hint } => {
|
||||||
|
is_seekable = false;
|
||||||
|
let decoder = tokio::task::spawn_blocking(move || {
|
||||||
|
SizedDecoder::new_streaming(Box::new(reader), stream_hint.as_deref(), "track-stream")
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())??;
|
||||||
|
build_streaming_source(
|
||||||
|
decoder,
|
||||||
|
duration_hint,
|
||||||
|
state.eq_gains.clone(),
|
||||||
|
state.eq_enabled.clone(),
|
||||||
|
state.eq_pre_gain.clone(),
|
||||||
|
done_flag,
|
||||||
|
fade_in_dur,
|
||||||
|
state.samples_played.clone(),
|
||||||
|
target_rate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
Ok(PlaybackSource { built, is_seekable })
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user