mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
fix(audio): VLC-style frame dropping for corrupt MP3s, silence logs in release
Replace the fixed DECODE_MAX_RETRIES (3) loop with a consecutive-error counter that tolerates up to 100 bad frames before giving up — matching how VLC handles files with a handful of invalid main_data offset frames. Frame-drop logs are wrapped in #[cfg(debug_assertions)] so production builds stay silent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+58
-16
@@ -890,7 +890,13 @@ impl MediaSource for SizedCursorSource {
|
|||||||
// Implements Iterator<Item = i16> + Source — identical interface to
|
// Implements Iterator<Item = i16> + Source — identical interface to
|
||||||
// rodio::Decoder, so the rest of the source chain is unchanged.
|
// rodio::Decoder, so the rest of the source chain is unchanged.
|
||||||
|
|
||||||
|
/// Max retries for IO/packet-read errors (fatal — network drop, truncated file).
|
||||||
const DECODE_MAX_RETRIES: usize = 3;
|
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;
|
||||||
|
|
||||||
struct SizedDecoder {
|
struct SizedDecoder {
|
||||||
decoder: Box<dyn symphonia::core::codecs::Decoder>,
|
decoder: Box<dyn symphonia::core::codecs::Decoder>,
|
||||||
@@ -899,6 +905,9 @@ struct SizedDecoder {
|
|||||||
total_duration: Option<Time>,
|
total_duration: Option<Time>,
|
||||||
buffer: SampleBuffer<i16>,
|
buffer: SampleBuffer<i16>,
|
||||||
spec: SignalSpec,
|
spec: SignalSpec,
|
||||||
|
/// Counts consecutive DecodeErrors in the hot-path. Reset to 0 on every
|
||||||
|
/// successfully decoded frame. Used to detect fully undecodable streams.
|
||||||
|
consecutive_decode_errors: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizedDecoder {
|
impl SizedDecoder {
|
||||||
@@ -983,6 +992,8 @@ impl SizedDecoder {
|
|||||||
let mut format = probed.format;
|
let mut format = probed.format;
|
||||||
|
|
||||||
// Decode the first packet to initialise spec + buffer.
|
// Decode the first packet to initialise spec + buffer.
|
||||||
|
// DecodeErrors (e.g. "invalid main_data offset") are non-fatal: drop the
|
||||||
|
// frame and try the next packet up to MAX_CONSECUTIVE_DECODE_ERRORS times.
|
||||||
let mut decode_errors: usize = 0;
|
let mut decode_errors: usize = 0;
|
||||||
let decoded = loop {
|
let decoded = loop {
|
||||||
let packet = match format.next_packet() {
|
let packet = match format.next_packet() {
|
||||||
@@ -1002,10 +1013,10 @@ impl SizedDecoder {
|
|||||||
match decoder.decode(&packet) {
|
match decoder.decode(&packet) {
|
||||||
Ok(decoded) => break decoded,
|
Ok(decoded) => break decoded,
|
||||||
Err(symphonia::core::errors::Error::DecodeError(ref msg)) => {
|
Err(symphonia::core::errors::Error::DecodeError(ref msg)) => {
|
||||||
eprintln!("[psysonic] decode error (retry {decode_errors}): {msg}");
|
|
||||||
decode_errors += 1;
|
decode_errors += 1;
|
||||||
if decode_errors > DECODE_MAX_RETRIES {
|
eprintln!("[psysonic] init: dropped corrupt frame #{decode_errors}: {msg}");
|
||||||
return Err("too many decode errors — file may be corrupt".into());
|
if decode_errors >= MAX_CONSECUTIVE_DECODE_ERRORS {
|
||||||
|
return Err("too many consecutive decode errors during init — file may be corrupt".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -1025,6 +1036,7 @@ impl SizedDecoder {
|
|||||||
total_duration,
|
total_duration,
|
||||||
buffer,
|
buffer,
|
||||||
spec,
|
spec,
|
||||||
|
consecutive_decode_errors: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1061,16 +1073,19 @@ impl SizedDecoder {
|
|||||||
if packet.track_id() != track_id { continue; }
|
if packet.track_id() != track_id { continue; }
|
||||||
match decoder.decode(&packet) {
|
match decoder.decode(&packet) {
|
||||||
Ok(d) => break d,
|
Ok(d) => break d,
|
||||||
Err(symphonia::core::errors::Error::DecodeError(_)) => {
|
Err(symphonia::core::errors::Error::DecodeError(ref msg)) => {
|
||||||
errors += 1;
|
errors += 1;
|
||||||
if errors > DECODE_MAX_RETRIES { return Err("radio: too many decode errors".into()); }
|
eprintln!("[psysonic] radio init: dropped corrupt frame #{errors}: {msg}");
|
||||||
|
if errors >= MAX_CONSECUTIVE_DECODE_ERRORS {
|
||||||
|
return Err("radio: too many consecutive decode errors".into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => return Err(format!("radio: decode error: {e}")),
|
Err(e) => return Err(format!("radio: decode error: {e}")),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let spec = decoded.spec().to_owned();
|
let spec = decoded.spec().to_owned();
|
||||||
let buffer = Self::make_buffer(decoded, &spec);
|
let buffer = Self::make_buffer(decoded, &spec);
|
||||||
Ok(SizedDecoder { decoder, current_frame_offset: 0, format, total_duration, buffer, spec })
|
Ok(SizedDecoder { decoder, current_frame_offset: 0, format, total_duration, buffer, spec, consecutive_decode_errors: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -1120,18 +1135,45 @@ impl Iterator for SizedDecoder {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn next(&mut self) -> Option<i16> {
|
fn next(&mut self) -> Option<i16> {
|
||||||
if self.current_frame_offset >= self.buffer.len() {
|
if self.current_frame_offset >= self.buffer.len() {
|
||||||
let packet = self.format.next_packet().ok()?;
|
// Loop until a decodable packet is found or the stream ends.
|
||||||
let mut decoded = self.decoder.decode(&packet);
|
// DecodeErrors (e.g. MP3 "invalid main_data offset") are non-fatal:
|
||||||
for _ in 0..DECODE_MAX_RETRIES {
|
// drop the frame and advance to the next packet. IO errors and a
|
||||||
if decoded.is_err() {
|
// clean end-of-stream both terminate the iterator normally.
|
||||||
let p = self.format.next_packet().ok()?;
|
loop {
|
||||||
decoded = self.decoder.decode(&p);
|
let packet = self.format.next_packet().ok()?;
|
||||||
|
match self.decoder.decode(&packet) {
|
||||||
|
Ok(decoded) => {
|
||||||
|
self.consecutive_decode_errors = 0;
|
||||||
|
decoded.spec().clone_into(&mut self.spec);
|
||||||
|
self.buffer = Self::make_buffer(decoded, &self.spec);
|
||||||
|
self.current_frame_offset = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(symphonia::core::errors::Error::DecodeError(ref msg)) => {
|
||||||
|
self.consecutive_decode_errors += 1;
|
||||||
|
// Log sparingly: first drop, then every 10th to avoid spam.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if self.consecutive_decode_errors == 1
|
||||||
|
|| self.consecutive_decode_errors % 10 == 0
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"[psysonic] dropped corrupt frame #{}: {msg}",
|
||||||
|
self.consecutive_decode_errors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.consecutive_decode_errors >= MAX_CONSECUTIVE_DECODE_ERRORS {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
eprintln!(
|
||||||
|
"[psysonic] {MAX_CONSECUTIVE_DECODE_ERRORS} consecutive decode \
|
||||||
|
failures — stream appears unrecoverable, stopping"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// continue → fetch next packet
|
||||||
|
}
|
||||||
|
Err(_) => return None, // IO error or fatal codec error → end of stream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let decoded = decoded.ok()?;
|
|
||||||
decoded.spec().clone_into(&mut self.spec);
|
|
||||||
self.buffer = Self::make_buffer(decoded, &self.spec);
|
|
||||||
self.current_frame_offset = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let sample = *self.buffer.samples().get(self.current_frame_offset)?;
|
let sample = *self.buffer.samples().get(self.current_frame_offset)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user