Files
dusk/src/dusklinux/audio/audiostreamlinux.c
T
YourWishes 8a77001016 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.
2026-08-31 12:33:33 -05:00

84 lines
2.6 KiB
C

/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "audiostreamlinux.h"
#include "audio/audiostream.h"
#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.");
SDL_AudioSpec desired;
memoryZero(&desired, sizeof(SDL_AudioSpec));
desired.freq = (int) stream->pcm.sampleRate;
desired.format = AUDIO_S16SYS;
desired.channels = stream->pcm.channels;
desired.samples = AUDIO_LINUX_LEAD_FRAMES;
stream->platform.device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0);
if(stream->platform.device == 0) {
errorThrow("Failed to open SDL2 audio device: %s", SDL_GetError());
}
errorOk();
}
errorret_t audioStreamLinuxDispose(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
SDL_CloseAudioDevice(stream->platform.device);
errorOk();
}
errorret_t audioStreamLinuxBuffer(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
uint8_t *mixed = memoryAllocate(stream->dataSize);
memoryZero(mixed, stream->dataSize);
SDL_MixAudioFormat(
mixed, stream->data, AUDIO_S16SYS, (Uint32) stream->dataSize,
(stream->volume * SDL_MIX_MAXVOLUME) / 0xFF
);
int queued = SDL_QueueAudio(
stream->platform.device, mixed, (Uint32) stream->dataSize
);
memoryFree(mixed);
if(queued != 0) {
errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError());
}
SDL_PauseAudioDevice(stream->platform.device, 0);
errorOk();
}
bool_t audioStreamLinuxIsFinished(audiostream_t *stream) {
assertNotNull(stream, "Stream cannot be NULL.");
// 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;
}