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:
Psychotoxical
2026-05-08 15:45:53 +02:00
parent 8f976dc371
commit ed92a77035
2 changed files with 100 additions and 61 deletions
+20 -61
View File
@@ -10,11 +10,13 @@ use rodio::Player;
use rodio::Source;
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::helpers::*;
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::progress_task::spawn_progress_task;
use super::state::{ChainedInfo, PreloadedTrack};
@@ -216,65 +218,22 @@ pub async fn audio_play(
let done_flag = Arc::new(AtomicBool::new(false));
// Reset sample counter for the new track.
state.samples_played.store(0, Ordering::Relaxed);
// 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 new_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.clone(),
fade_in_dur,
state.samples_played.clone(),
target_rate,
format_hint.as_deref(),
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 playback_source = build_source_from_play_input(
play_input,
&state,
format_hint.as_deref(),
done_flag.clone(),
fade_in_dur,
hi_res_enabled,
duration_hint,
)
.await
.map_err(|e| {
app.emit("audio:error", &e).ok();
e
})?;
state.current_is_seekable.store(playback_source.is_seekable, Ordering::SeqCst);
let built = playback_source.built;
let source = built.source;
let duration_secs = built.duration_secs;
let output_rate = built.output_rate;
+80
View File
@@ -11,6 +11,7 @@ use ringbuf::{HeapCons, HeapRb};
use symphonia::core::io::MediaSource;
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::helpers::{
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())
}
/// 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 })
}