Fix audio loop gaps: same-tick re-buffer + Linux lead-margin refill
Two independent gap sources, both confirmed fixed on Linux: 1. audiostream.c's Update() used if/else-if, so a loop restart (clearing BUFFERED) couldn't re-trigger Buffer() until the *next* frame's Update() noticed - up to one frame of dead air on every loop, on every platform. Restructured to two sequential ifs so a loop falls straight through into re-buffering in the same call. 2. audioStreamLinuxIsFinished only reported true once SDL's queue was completely empty - meaning silence had already started by the time "empty" could be observed, guaranteeing a gap by construction. Changed it to report ready-to-refill once the queue drops to a 4096-frame lead margin (matching the SDL device's own internal buffer size) instead of waiting for zero. SDL_QueueAudio only ever appends to a FIFO, so refilling that early never causes overlap - it just means the device's callback never runs dry. Confirmed looping gaplessly on Linux.
This commit is contained in:
@@ -91,13 +91,12 @@ errorret_t audioStreamUpdate(audiostream_t *stream) {
|
||||
// start of the buffer (loopTo is not yet honored) since replaying the
|
||||
// exact same stream->data/dataSize needs no platform-specific changes;
|
||||
// an arbitrary loopTo offset would need slicing that buffer instead.
|
||||
// Checked in this order (finished-check before needs-buffering) so that a
|
||||
// loop restart falls straight through into re-buffering within this same
|
||||
// call, instead of leaving BUFFERED cleared for the caller to notice and
|
||||
// act on next frame - that extra frame of latency was audible as a gap
|
||||
// at every loop boundary.
|
||||
if(
|
||||
(stream->state & AUDIO_STREAM_STATE_PLAYING) &&
|
||||
!(stream->state & AUDIO_STREAM_STATE_BUFFERED)
|
||||
) {
|
||||
errorChain(audioStreamPlatformBuffer(stream));
|
||||
stream->state |= AUDIO_STREAM_STATE_BUFFERED;
|
||||
} else if(
|
||||
(stream->state & AUDIO_STREAM_STATE_PLAYING) &&
|
||||
(stream->state & AUDIO_STREAM_STATE_BUFFERED) &&
|
||||
audioStreamPlatformIsFinished(stream)
|
||||
@@ -117,6 +116,14 @@ errorret_t audioStreamUpdate(audiostream_t *stream) {
|
||||
}
|
||||
}
|
||||
|
||||
if(
|
||||
(stream->state & AUDIO_STREAM_STATE_PLAYING) &&
|
||||
!(stream->state & AUDIO_STREAM_STATE_BUFFERED)
|
||||
) {
|
||||
errorChain(audioStreamPlatformBuffer(stream));
|
||||
stream->state |= AUDIO_STREAM_STATE_BUFFERED;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
// How many frames of lead time to keep queued ahead of playback. Matches
|
||||
// SDL_AudioSpec.samples below - the device's own internal buffer size - so
|
||||
// this is "never let the queue run drier than one SDL-internal buffer."
|
||||
#define AUDIO_LINUX_LEAD_FRAMES 4096
|
||||
|
||||
errorret_t audioStreamLinuxInit(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
@@ -18,7 +23,7 @@ errorret_t audioStreamLinuxInit(audiostream_t *stream) {
|
||||
desired.freq = (int) stream->pcm.sampleRate;
|
||||
desired.format = AUDIO_S16SYS;
|
||||
desired.channels = stream->pcm.channels;
|
||||
desired.samples = 4096;
|
||||
desired.samples = AUDIO_LINUX_LEAD_FRAMES;
|
||||
|
||||
stream->platform.device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0);
|
||||
if(stream->platform.device == 0) {
|
||||
@@ -63,5 +68,16 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
||||
bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
|
||||
assertNotNull(stream, "Stream cannot be NULL.");
|
||||
|
||||
return SDL_GetQueuedAudioSize(stream->platform.device) == 0;
|
||||
// Reports "finished" (ready to be re-buffered) with AUDIO_LINUX_LEAD_FRAMES
|
||||
// of margin still queued, rather than waiting for the queue to actually
|
||||
// run dry. SDL_QueueAudio only ever appends to a FIFO, so queuing the next
|
||||
// loop this early never causes overlap - it just avoids ever going silent
|
||||
// while our once-per-frame Update() notices and catches up. Waiting for
|
||||
// truly empty (as this used to) guarantees a gap by definition: silence
|
||||
// has already started by the time "empty" can be observed.
|
||||
const Uint32 leadBytes = (Uint32) (
|
||||
AUDIO_LINUX_LEAD_FRAMES * stream->pcm.channels * sizeof(int16_t)
|
||||
);
|
||||
|
||||
return SDL_GetQueuedAudioSize(stream->platform.device) <= leadBytes;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user