//! Symphonia `SizedDecoder`, gapless trim, and `build_source` / `build_streaming_source`. use std::io::{Cursor, Read, Seek}; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64}; use std::sync::Arc; use std::time::Duration; use rodio::source::UniformSourceIterator; use rodio::Source; use symphonia::core::{ audio::{AudioBufferRef, SampleBuffer, SignalSpec}, codecs::{DecoderOptions, CODEC_TYPE_NULL}, formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions}, meta::MetadataOptions, probe::Hint, units::{self, Time}, }; use super::codec::{psysonic_codec_registry, try_make_radio_decoder}; use super::sources::*; // ─── SizedCursorSource — correct byte_len for seekable in-memory sources ────── // // rodio's internal ReadSeekSource wraps Cursor> but hardcodes // byte_len() → None. This tells symphonia "stream length unknown", which // prevents the FLAC demuxer from seeking (it validates seek offsets against // the total stream length from byte_len). MP3 is unaffected because its // demuxer uses Xing/LAME headers instead. // // This wrapper provides the actual byte length, fixing seek for all formats. pub(crate) struct SizedCursorSource { inner: Cursor>, len: u64, } impl Read for SizedCursorSource { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.inner.read(buf) } } impl Seek for SizedCursorSource { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.inner.seek(pos) } } impl MediaSource for SizedCursorSource { fn is_seekable(&self) -> bool { true } fn byte_len(&self) -> Option { Some(self.len) } } // ─── SizedDecoder — symphonia decoder with correct byte_len ─────────────────── // // Replaces rodio::Decoder::new() which wraps the source in ReadSeekSource // (byte_len = None). This constructs the symphonia pipeline directly, // providing the correct byte_len via SizedCursorSource. // // Implements Iterator + Source — identical interface to // rodio::Decoder, so the rest of the source chain is unchanged. /// Debug logging: codec parameters in human-readable form to verify whether /// playback is genuinely lossless. pub(crate) fn log_codec_resolution( tag: &str, params: &symphonia::core::codecs::CodecParameters, container_hint: Option<&str>, ) { let codec_name = symphonia::default::get_codecs() .get_codec(params.codec) .map(|d| d.short_name) .unwrap_or("?"); let rate = params.sample_rate.map(|r| format!("{} Hz", r)).unwrap_or_else(|| "? Hz".into()); let bits = params.bits_per_sample .or(params.bits_per_coded_sample) .map(|b| format!("{}-bit", b)) .unwrap_or_else(|| "?-bit".into()); let ch = params.channels .map(|c| format!("{}ch", c.count())) .unwrap_or_else(|| "?ch".into()); let lossless = codec_name.starts_with("pcm") || matches!( codec_name, "flac" | "alac" | "wavpack" | "monkeys-audio" | "tta" | "shorten" ); let kind = if lossless { "LOSSLESS" } else { "lossy" }; crate::app_deprintln!( "[stream] {tag}: codec={codec_name} ({kind}) {bits} {rate} {ch} container={}", container_hint.unwrap_or("?") ); } /// Max retries for IO/packet-read errors (fatal — network drop, truncated file). const DECODE_MAX_RETRIES: usize = 3; /// Max *consecutive* DecodeErrors before giving up on a file. /// Non-fatal errors like "invalid main_data offset" are silently dropped up to /// this limit so a handful of corrupt MP3 frames never aborts an otherwise /// playable track (VLC-style frame dropping). const MAX_CONSECUTIVE_DECODE_ERRORS: usize = 100; pub(crate) struct SizedDecoder { decoder: Box, current_frame_offset: usize, format: Box, total_duration: Option