* fix(audio): fix Opus/Ogg seek crash by keeping random-access sources seekable through probe
Scrubbing the seekbar on Opus/Ogg files (then pressing Stop) crashed the whole
app. symphonia 0.6's Ogg demuxer records the physical stream's byte range only
when the source is seekable during the probe, but ProbeSeekGate hid seekability
there — so phys_byte_range_end stayed None and the first seek hit
Option::unwrap() on None on the cpal audio thread. That poisoned the engine
mutexes and aborted the process at the non-unwinding cpal FFI boundary (the
"crash on Stop" was a downstream symptom).
- Keep Ogg/Opus seekable through the probe on random-access sources (local
files, in-memory) so the demuxer computes its seek bounds and seeking works
for real. Progressive ranged-HTTP keeps the gate to avoid forcing a full
download before playback starts.
- Contain any demuxer unwind inside SizedDecoder::try_seek (catch_unwind) so a
panic on the audio thread can no longer poison engine state — covers streamed
Ogg (still gated) and any future demuxer panic.
- Thread a random_access flag through PlayInput::SeekableMedia and new_streaming.
* docs(changelog): add 1.48.1 entry for the Opus/Ogg seek crash fix
Also note the Opus seek crash fix in WHATS_NEW.md (1.48.1).
(cherry picked from commit 8bfde08199)
* fix(stream): defer M4A probe until moov tail or fast-start prefix
Ranged moov-at-end M4A started Symphonia format probe as soon as ~384 KiB
linear data arrived, before the parallel tail prefetch filled the moov
atom — probe hit end of stream and skipped the track. Wait for tail_ready
or detect fast-start moov in the prefix; do not arm playback from linear
bytes alone when tail prefetch is active.
* fix(isomp4): skip EOF-spanning mdat after moov-at-end is parsed
Second-pass header scan with moov already loaded still tried to read
through mdat→EOF on RangedHttpSource holes, causing format probe
end of stream. Re-check play generation after moov wait.
* fix(isomp4): fix AtomIterator overread after seek in patched demuxer
`AtomIterator::new_root(reader, len)` treats `len` as bytes available
from the current `reader.pos()`, not the absolute file length. After
`mss.seek(resume_at)` we were passing the absolute `total_len`, so the
iterator thought there were `total_len` bytes left and tried to read
past EOF on the next iteration, returning "end of stream".
Fix: pass `total_len.map(|tl| tl.saturating_sub(resume_at))` (remaining
bytes from the new position) in both branches:
- `moov.is_none()` (moov-at-end layout, seek to moov offset)
- `moov.is_some()` (fast-start layout, skip bounded mdat body)
This caused Symphonia to fail probing moov-at-end M4A files read from
local disk (hot-cache) and from in-memory buffers — every decode attempt
returned "end of stream", analysis fell back to `byte_envelope_no_ebu`
(no EBU R128 loudness), and rodio produced distorted audio.
Also in this commit:
- `resolve_playback_format_hint()` helper to resolve hint from URL,
stream suffix, Content-Disposition, or byte sniff
- ISO-BMFF diagnostic helpers (`isobmff_buffer_looks_complete`,
`log_isobmff_buffer_diagnostic`, `mp4_suspect_zero_holes`)
- Probe-fallback path for ranged-stream failures now uses these helpers
to decide whether to refetch or wait for the in-flight download
* chore(audio): remove redundant hint recomputation and add missing blank line
`bytes_hint_for_wait` in the ranged-stream fallback path was an exact
duplicate of `effective_hint` already in scope — reuse the existing binding.
Also add missing blank line after `wait_for_ranged_mp4_probe_ready` in mod.rs.
* chore(release): CHANGELOG and credits for PR #757
Add Fixed entry for M4A moov-at-end probe fix and credits line in
settingsCredits.ts under existing cucadmuh contributions.
* fix(audio): extract BuildSourceArgs to fix clippy::too_many_arguments
`build_playback_source_with_probe_fallback` had 12 parameters, exceeding
the clippy limit of 7. Group url/gen/hints/fade/hi-res/duration into
`BuildSourceArgs` so the function signature stays at 4 arguments.
* feat(playback): stream buffering UI, ranged M4A tail prefetch, demuxer fix
Defer seekbar/progress until HTTP stream is armed for both legacy and
RangedHttpSource; show buffering overlay on cover art. Add MP4 tail
prefetch and Symphonia isomp4 bounded-mdat/moov-at-EOF probing so
moov-at-end M4A can start without reading the full mdat.
* feat(hot-cache): spill large ranged streams to disk for promote
When a ranged HTTP download completes above the 64 MiB RAM promote cap,
write the existing buffer once to app-data stream-spill/ and register it
for hot-cache promote (rename) and replay via fetch_data. Analysis seeds
from the spill file up to the local-file cap (512 MiB).
* fix(ui): stream buffering — grayscale cover and static clock icon
Desaturate player and queue cover art while isPlaybackBuffering; keep a
non-animated clock overlay for visibility without the spinning animation.
* fix(playback): review follow-up — tests, i18n, spill cleanup, changelog
Clippy and test layout fixes; stream spill orphan cleanup on startup;
buffering flag guard in progress handler; bufferingStream in all player
locales; CHANGELOG and contributor credits for stream/M4A work.
* docs: attribute stream buffering and M4A streaming to PR #737
* test(audio): avoid create_engine in stream spill unit test
CI runners have no audio output device; test spill take/consume via
the Mutex slot only, matching install_stream_completed_spill tests.
Moves all audio playback code (Symphonia decode, rodio output, HTTP
streaming, gapless, previews, and the seven stream/ source-type
submodules from the prior split) out of the top crate into a new
psysonic-audio crate.
crates/psysonic-audio/ new lib crate, depends on
psysonic-core + psysonic-analysis
src/{engine,helpers,decode,…}.rs flattened layout (no more
extra audio/ namespace level)
src/stream/ seven submodules from M0
src/lib.rs re-exports macros from
psysonic-core and the public
API surface
The audio↔analysis edges identified in the dep survey are now real
crate deps (audio depends on analysis directly: AnalysisCache reads,
recommended_gain_for_target, submit_analysis_cpu_seed). Only the
analysis→audio back-edge goes through the PlaybackQueryHandle port
registered in M2.
Cross-crate ref migrations applied via batch sed:
crate::audio::* → crate::* (intra-crate)
crate::analysis_cache::* → psysonic_analysis::analysis_cache::*
crate::submit_analysis_cpu_seed → psysonic_analysis::analysis_runtime::*
crate::subsonic_wire_user_agent → psysonic_core::user_agent::*
Top crate keeps `crate::audio::*` paths working via
`pub use psysonic_audio as audio;` — lib_commands/cli callers untouched.
`stop_audio_engine` (mac process-exit cleanup) moved into the audio
crate as `pub fn stop_audio_engine` since it reaches AudioEngine
internals; tray.rs now re-exports the moved fn.
Two small visibility promotions in engine.rs:
pub(crate) fn analysis_track_id_is_current_playback → pub
pub(crate) fn ranged_loudness_backfill_should_defer → pub
Behaviour preserving. Cargo check + clippy --workspace clean.