8a77001016
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.
84 lines
2.6 KiB
C
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;
|
|
}
|