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;