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
|
// start of the buffer (loopTo is not yet honored) since replaying the
|
||||||
// exact same stream->data/dataSize needs no platform-specific changes;
|
// exact same stream->data/dataSize needs no platform-specific changes;
|
||||||
// an arbitrary loopTo offset would need slicing that buffer instead.
|
// 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(
|
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_PLAYING) &&
|
||||||
(stream->state & AUDIO_STREAM_STATE_BUFFERED) &&
|
(stream->state & AUDIO_STREAM_STATE_BUFFERED) &&
|
||||||
audioStreamPlatformIsFinished(stream)
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.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) {
|
errorret_t audioStreamLinuxInit(audiostream_t *stream) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
assertNotNull(stream, "Stream cannot be NULL.");
|
||||||
|
|
||||||
@@ -18,7 +23,7 @@ errorret_t audioStreamLinuxInit(audiostream_t *stream) {
|
|||||||
desired.freq = (int) stream->pcm.sampleRate;
|
desired.freq = (int) stream->pcm.sampleRate;
|
||||||
desired.format = AUDIO_S16SYS;
|
desired.format = AUDIO_S16SYS;
|
||||||
desired.channels = stream->pcm.channels;
|
desired.channels = stream->pcm.channels;
|
||||||
desired.samples = 4096;
|
desired.samples = AUDIO_LINUX_LEAD_FRAMES;
|
||||||
|
|
||||||
stream->platform.device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0);
|
stream->platform.device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0);
|
||||||
if(stream->platform.device == 0) {
|
if(stream->platform.device == 0) {
|
||||||
@@ -63,5 +68,16 @@ errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
|
|||||||
bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
|
bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
|
||||||
assertNotNull(stream, "Stream cannot be NULL.");
|
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