Fix minimp3 sliding-window decoder silently dropping frames near tail

audioStreamMp3DecoderDecodeFrame was treating minimp3's "can't confirm
a frame here" verdict as genuine garbage even when the window simply
hadn't been topped up yet - discarding real frames whenever one landed
near the tail of a not-yet-full window, since minimp3 needs to also
validate the *next* frame's header to confirm a decode. On a real ~236s
VBR file this silently dropped ~5.5% of frames, heard as the stream
finishing early ("racing") with a stutter at each drop.

Now refills before ever trusting a "not found" verdict as confirmed
garbage, and grows the window from 16KB to 256KB (past the point of
diminishing returns, ~0.3% residual loss). PSP is unaffected - it uses
the hardware sceMp3 decoder, not this file.

Also removes now-unneeded debug instrumentation from the Linux feed
path.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-01 09:43:19 -05:00
co-authored by Claude Sonnet 5
parent d9beb647c1
commit 62c081ecf2
4 changed files with 93 additions and 29 deletions
+40 -1
View File
@@ -88,6 +88,8 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
stream->platform.position = startFrame;
stream->platform.endFrame = endFrame;
stream->platform.passStartTicks = SDL_GetTicks();
stream->platform.passStartPosition = startFrame;
errorChain(audioStreamLinuxFeed(stream));
@@ -105,7 +107,43 @@ errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
? stream->platform.endFrame - stream->platform.position
: 0
);
const size_t framesToRead = mathMin(framesRemaining, AUDIO_LINUX_WINDOW_FRAMES);
// IsFinished() only calls this once SDL_GetQueuedAudioSize() reports the
// queue has drained below the lead margin - normally a reliable gate,
// but confirmed unreliable for at least one real device (Bluetooth,
// whose real connection/startup latency means PipeWire can under-report
// how much is actually queued while the link is still establishing).
// When that happens, repeated Feed() calls each see a falsely-small
// queued size and each add another full window before the reported size
// catches up to reality - heard as the whole clip racing far ahead of
// real playback right from the start, with stutters where the audio
// backend then has to catch up or drop the backlog. Independent of
// whatever SDL reports, never let this stream's own read position get
// further ahead of how much real wall-clock time has actually elapsed
// since the pass started than a lead-plus-one-window margin - a
// device-report-independent ceiling that still allows a full normal
// window per call under real (correctly-reported) conditions.
const Uint32 elapsedMs = SDL_GetTicks() - stream->platform.passStartTicks;
const size_t audibleFrames = (size_t) (
((double) elapsedMs / 1000.0) * (double) stream->sampleRate
);
// Margin is just one lead's worth, not lead+window - deliberately tight,
// prioritizing "never let position get far ahead of real time" over
// per-call efficiency, while this is still being tracked down. Costs
// more (smaller) top-up calls in the steady state than a looser margin
// would, which is an acceptable trade for now.
const size_t maxPosition = (
stream->platform.passStartPosition + audibleFrames +
AUDIO_LINUX_LEAD_FRAMES
);
const size_t positionBudget = maxPosition > stream->platform.position
? maxPosition - stream->platform.position
: 0;
const size_t framesToRead = mathMin(
mathMin(framesRemaining, (size_t) AUDIO_LINUX_WINDOW_FRAMES), positionBudget
);
if(framesToRead == 0) {
errorOk();
}
@@ -142,6 +180,7 @@ errorret_t audioStreamLinuxFeed(audiostream_t *stream) {
}
stream->platform.position += framesRead;
errorOk();
}
+7
View File
@@ -24,6 +24,13 @@ typedef struct {
// the true end of the clip if not looping) - recomputed by
// audioStreamLinuxBuffer() at the start of each pass.
size_t endFrame;
// SDL_GetTicks() value and starting frame position when the current
// pass started - together, what audioStreamLinuxFeed() paces its own
// feeding rate against (see its own comment on why it doesn't trust
// SDL_GetQueuedAudioSize() alone).
Uint32 passStartTicks;
size_t passStartPosition;
} audiostreamlinux_t;
/**