From ed92a770351fba39d29699e68906e0733245a87c Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 8 May 2026 15:45:53 +0200 Subject: [PATCH] =?UTF-8?q?refactor(audio):=20lift=20PlayInput=20=E2=86=92?= =?UTF-8?q?=20BuiltSource=20dispatch=20into=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src-tauri/src/audio/commands.rs | 81 ++++++++----------------------- src-tauri/src/audio/play_input.rs | 80 ++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 61 deletions(-) diff --git a/src-tauri/src/audio/commands.rs b/src-tauri/src/audio/commands.rs index 6aefe874..9b02a4cb 100644 --- a/src-tauri/src/audio/commands.rs +++ b/src-tauri/src/audio/commands.rs @@ -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; diff --git a/src-tauri/src/audio/play_input.rs b/src-tauri/src/audio/play_input.rs index 614158e5..3fd9265d 100644 --- a/src-tauri/src/audio/play_input.rs +++ b/src-tauri/src/audio/play_input.rs @@ -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 { }) .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, + fade_in_dur: Duration, + hi_res_enabled: bool, + duration_hint: f64, +) -> Result { + // 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 }) +}