From edaab8ad9f93c2e25a54aa68817480dd6c2f7877 Mon Sep 17 00:00:00 2001 From: Brick <6098371+0x1F9F1@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:22:29 +0100 Subject: [PATCH] Refactored audio conversion to reduce copying More of the logic has been moved into SDL_AudioQueue, allowing data to be converted directly from the input buffer. --- include/SDL3/SDL_audio.h | 4 +- src/audio/SDL_audiocvt.c | 214 ++++----- src/audio/SDL_audioqueue.c | 625 ++++++++++++++++---------- src/audio/SDL_audioqueue.h | 26 +- src/audio/SDL_sysaudio.h | 4 - test/testaudiostreamdynamicresample.c | 9 +- test/testautomation_audio.c | 5 - 7 files changed, 478 insertions(+), 409 deletions(-) diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h index e77afcbfc..8569cb3f1 100644 --- a/include/SDL3/SDL_audio.h +++ b/include/SDL3/SDL_audio.h @@ -789,13 +789,13 @@ extern DECLSPEC float SDLCALL SDL_GetAudioStreamFrequencyRatio(SDL_AudioStream * extern DECLSPEC int SDLCALL SDL_SetAudioStreamFrequencyRatio(SDL_AudioStream *stream, float ratio); /** - * Add data to be converted/resampled to the stream. + * Add data to the stream. * * This data must match the format/channels/samplerate specified in the latest * call to SDL_SetAudioStreamFormat, or the format specified when creating the * stream if it hasn't been changed. * - * Note that this call simply queues unconverted data for later. This is + * Note that this call simply copies the unconverted data for later. This is * different than SDL2, where data was converted during the Put call and the * Get call would just dequeue the previously-converted data. * diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c index e51a7daba..a7cc96dd6 100644 --- a/src/audio/SDL_audiocvt.c +++ b/src/audio/SDL_audiocvt.c @@ -308,20 +308,10 @@ static int UpdateAudioStreamInputSpec(SDL_AudioStream *stream, const SDL_AudioSp return 0; } - const size_t history_buffer_allocation = SDL_GetResamplerHistoryFrames() * SDL_AUDIO_FRAMESIZE(*spec); - Uint8 *history_buffer = stream->history_buffer; - - if (stream->history_buffer_allocation < history_buffer_allocation) { - history_buffer = (Uint8 *) SDL_aligned_alloc(SDL_SIMDGetAlignment(), history_buffer_allocation); - if (!history_buffer) { - return -1; - } - SDL_aligned_free(stream->history_buffer); - stream->history_buffer = history_buffer; - stream->history_buffer_allocation = history_buffer_allocation; + if (SDL_ResetAudioQueueHistory(stream->queue, SDL_GetResamplerHistoryFrames()) != 0) { + return -1; } - SDL_memset(history_buffer, SDL_GetSilenceValueForFormat(spec->format), history_buffer_allocation); SDL_copyp(&stream->input_spec, spec); return 0; @@ -338,7 +328,7 @@ SDL_AudioStream *SDL_CreateAudioStream(const SDL_AudioSpec *src_spec, const SDL_ } retval->freq_ratio = 1.0f; - retval->queue = SDL_CreateAudioQueue(4096); + retval->queue = SDL_CreateAudioQueue(8192); if (!retval->queue) { SDL_free(retval); @@ -550,22 +540,12 @@ static int CheckAudioStreamIsFullySetup(SDL_AudioStream *stream) return 0; } -int SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len) +static int PutAudioStreamBuffer(SDL_AudioStream *stream, const void *buf, int len, SDL_ReleaseAudioBufferCallback callback, void* userdata) { #if DEBUG_AUDIOSTREAM SDL_Log("AUDIOSTREAM: wants to put %d bytes", len); #endif - if (!stream) { - return SDL_InvalidParamError("stream"); - } else if (!buf) { - return SDL_InvalidParamError("buf"); - } else if (len < 0) { - return SDL_InvalidParamError("len"); - } else if (len == 0) { - return 0; // nothing to do. - } - SDL_LockMutex(stream->lock); if (CheckAudioStreamIsFullySetup(stream) != 0) { @@ -580,24 +560,13 @@ int SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len) SDL_AudioTrack* track = NULL; - // When copying in large amounts of data, try and do as much work as possible - // outside of the stream lock, otherwise the output device is likely to be starved. - const int large_input_thresh = 1024 * 1024; - - if (len >= large_input_thresh) { - SDL_AudioSpec src_spec; - SDL_copyp(&src_spec, &stream->src_spec); - - SDL_UnlockMutex(stream->lock); - - size_t chunk_size = SDL_GetAudioQueueChunkSize(stream->queue); - track = SDL_CreateChunkedAudioTrack(&src_spec, (const Uint8 *)buf, len, chunk_size); + if (callback) { + track = SDL_CreateAudioTrack(stream->queue, &stream->src_spec, (Uint8 *)buf, len, len, callback, userdata); if (!track) { + SDL_UnlockMutex(stream->lock); return -1; } - - SDL_LockMutex(stream->lock); } const int prev_available = stream->put_callback ? SDL_GetAudioStreamAvailable(stream) : 0; @@ -611,7 +580,6 @@ int SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len) } if (retval == 0) { - stream->total_bytes_queued += len; if (stream->put_callback) { const int newavail = SDL_GetAudioStreamAvailable(stream) - prev_available; stream->put_callback(stream->put_callback_userdata, stream, newavail, newavail); @@ -623,6 +591,49 @@ int SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len) return retval; } +static void SDLCALL FreeAllocatedAudioBuffer(void *userdata, const void *buf, int len) +{ + SDL_free((void*) buf); +} + +int SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } else if (!buf) { + return SDL_InvalidParamError("buf"); + } else if (len < 0) { + return SDL_InvalidParamError("len"); + } else if (len == 0) { + return 0; // nothing to do. + } + + // When copying in large amounts of data, try and do as much work as possible + // outside of the stream lock, otherwise the output device is likely to be starved. + const int large_input_thresh = 64 * 1024; + + if (len >= large_input_thresh) { + void* data = SDL_malloc(len); + + if (!data) { + return -1; + } + + SDL_memcpy(data, buf, len); + buf = data; + + int ret = PutAudioStreamBuffer(stream, buf, len, FreeAllocatedAudioBuffer, NULL); + + if (ret < 0) { + SDL_free(data); + } + + return ret; + } + + return PutAudioStreamBuffer(stream, buf, len, NULL, NULL); +} + int SDL_FlushAudioStream(SDL_AudioStream *stream) { if (!stream) { @@ -655,31 +666,6 @@ static Uint8 *EnsureAudioStreamWorkBufferSize(SDL_AudioStream *stream, size_t ne return ptr; } -static void UpdateAudioStreamHistoryBuffer(SDL_AudioStream* stream, - Uint8* input_buffer, int input_bytes, Uint8* left_padding, int padding_bytes) -{ - const int history_buffer_frames = SDL_GetResamplerHistoryFrames(); - - // Even if we aren't currently resampling, we always need to update the history buffer - Uint8 *history_buffer = stream->history_buffer; - int history_bytes = history_buffer_frames * SDL_AUDIO_FRAMESIZE(stream->input_spec); - - if (left_padding) { - // Fill in the left padding using the history buffer - SDL_assert(padding_bytes <= history_bytes); - SDL_memcpy(left_padding, history_buffer + history_bytes - padding_bytes, padding_bytes); - } - - // Update the history buffer using the new input data - if (input_bytes >= history_bytes) { - SDL_memcpy(history_buffer, input_buffer + (input_bytes - history_bytes), history_bytes); - } else { - int preserve_bytes = history_bytes - input_bytes; - SDL_memmove(history_buffer, history_buffer + input_bytes, preserve_bytes); - SDL_memcpy(history_buffer + preserve_bytes, input_buffer, input_bytes); - } -} - static Sint64 NextAudioStreamIter(SDL_AudioStream* stream, void** inout_iter, Sint64* inout_resample_offset, SDL_AudioSpec* out_spec, SDL_bool* out_flushed) { @@ -777,7 +763,6 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou const SDL_AudioFormat src_format = src_spec->format; const int src_channels = src_spec->channels; - const int src_frame_size = SDL_AUDIO_FRAMESIZE(*src_spec); const SDL_AudioFormat dst_format = dst_spec->format; const int dst_channels = dst_spec->channels; @@ -793,34 +778,19 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou // Not resampling? It's an easy conversion (and maybe not even that!) if (resample_rate == 0) { - Uint8* input_buffer = NULL; + Uint8* work_buffer = NULL; - // If no conversion is happening, read straight into the output buffer. - // Note, this is just to avoid extra copies. - // Some other formats may fit directly into the output buffer, but i'd rather process data in a SIMD-aligned buffer. - if ((src_format == dst_format) && (src_channels == dst_channels)) { - input_buffer = (Uint8 *)buf; - } else { - input_buffer = EnsureAudioStreamWorkBufferSize(stream, output_frames * max_frame_size); + // Ensure we have enough scratch space for any conversions + if ((src_format != dst_format) || (src_channels != dst_channels)) { + work_buffer = EnsureAudioStreamWorkBufferSize(stream, output_frames * max_frame_size); - if (!input_buffer) { + if (!work_buffer) { return -1; } } - const int input_bytes = output_frames * src_frame_size; - if (SDL_ReadFromAudioQueue(stream->queue, input_buffer, input_bytes) != 0) { - SDL_assert(!"Not enough data in queue (read)"); - } - - stream->total_bytes_queued -= input_bytes; - - // Even if we aren't currently resampling, we always need to update the history buffer - UpdateAudioStreamHistoryBuffer(stream, input_buffer, input_bytes, NULL, 0); - - // Convert the data, if necessary - if (buf != input_buffer) { - ConvertAudio(output_frames, input_buffer, src_format, src_channels, buf, dst_format, dst_channels, input_buffer); + if (SDL_ReadFromAudioQueue(stream->queue, buf, dst_format, dst_channels, 0, output_frames, 0, work_buffer) != buf) { + return SDL_SetError("Not enough data in queue"); } return 0; @@ -832,9 +802,10 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou // can require a different number of input_frames, depending on the resample_offset. // Infact, input_frames can sometimes even be zero when upsampling. const int input_frames = (int) SDL_GetResamplerInputFrames(output_frames, resample_rate, stream->resample_offset); - const int input_bytes = input_frames * src_frame_size; - const int resampler_padding_frames = SDL_GetResamplerPaddingFrames(resample_rate); + const int padding_frames = SDL_GetResamplerPaddingFrames(resample_rate); + + const SDL_AudioFormat resample_format = SDL_AUDIO_F32; // If increasing channels, do it after resampling, since we'd just // do more work to resample duplicate channels. If we're decreasing, do @@ -843,7 +814,7 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou const int resample_channels = SDL_min(src_channels, dst_channels); // The size of the frame used when resampling - const int resample_frame_size = resample_channels * sizeof(float); + const int resample_frame_size = SDL_AUDIO_BYTESIZE(resample_format) * resample_channels; // The main portion of the work_buffer can be used to store 3 things: // src_sample_frame_size * (left_padding+input_buffer+right_padding) @@ -854,14 +825,14 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou // resample_frame_size * output_frames // // Note, ConvertAudio requires (num_frames * max_sample_frame_size) of scratch space - const int work_buffer_frames = input_frames + (resampler_padding_frames * 2); + const int work_buffer_frames = input_frames + (padding_frames * 2); int work_buffer_capacity = work_buffer_frames * max_frame_size; int resample_buffer_offset = -1; // Check if we can resample directly into the output buffer. // Note, this is just to avoid extra copies. // Some other formats may fit directly into the output buffer, but i'd rather process data in a SIMD-aligned buffer. - if ((dst_format != SDL_AUDIO_F32) || (dst_channels != resample_channels)) { + if ((dst_format != resample_format) || (dst_channels != resample_channels)) { // Allocate space for converting the resampled output to the destination format int resample_convert_bytes = output_frames * max_frame_size; work_buffer_capacity = SDL_max(work_buffer_capacity, resample_convert_bytes); @@ -883,45 +854,15 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou return -1; } - const int padding_bytes = resampler_padding_frames * src_frame_size; + const Uint8* input_buffer = SDL_ReadFromAudioQueue(stream->queue, + NULL, resample_format, resample_channels, + padding_frames, input_frames, padding_frames, work_buffer); - Uint8* work_buffer_tail = work_buffer; - - // Split the work_buffer into [left_padding][input_buffer][right_padding] - Uint8* left_padding = work_buffer_tail; - work_buffer_tail += padding_bytes; - - Uint8* input_buffer = work_buffer_tail; - work_buffer_tail += input_bytes; - - Uint8* right_padding = work_buffer_tail; - work_buffer_tail += padding_bytes; - - SDL_assert((work_buffer_tail - work_buffer) <= work_buffer_capacity); - - // Now read unconverted data from the queue into the work buffer to fulfill the request. - if (SDL_ReadFromAudioQueue(stream->queue, input_buffer, input_bytes) != 0) { - SDL_assert(!"Not enough data in queue (resample read)"); - } - stream->total_bytes_queued -= input_bytes; - - // Update the history buffer and fill in the left padding - UpdateAudioStreamHistoryBuffer(stream, input_buffer, input_bytes, left_padding, padding_bytes); - - // Fill in the right padding by peeking into the input queue (missing data is filled with silence) - if (SDL_PeekIntoAudioQueue(stream->queue, right_padding, padding_bytes) != 0) { - SDL_assert(!"Not enough data in queue (resample peek)"); + if (!input_buffer) { + return SDL_SetError("Not enough data in queue (resample)"); } - SDL_assert(work_buffer_frames == input_frames + (resampler_padding_frames * 2)); - - // Resampling! get the work buffer to float32 format, etc, in-place. - ConvertAudio(work_buffer_frames, work_buffer, src_format, src_channels, work_buffer, SDL_AUDIO_F32, resample_channels, NULL); - - // Update the work_buffer pointers based on the new frame size - input_buffer = work_buffer + ((input_buffer - work_buffer) / src_frame_size * resample_frame_size); - work_buffer_tail = work_buffer + ((work_buffer_tail - work_buffer) / src_frame_size * resample_frame_size); - SDL_assert((work_buffer_tail - work_buffer) <= work_buffer_capacity); + input_buffer += padding_frames * resample_frame_size; // Decide where the resampled output goes void* resample_buffer = (resample_buffer_offset != -1) ? (work_buffer + resample_buffer_offset) : buf; @@ -932,9 +873,7 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou resample_rate, &stream->resample_offset); // Convert to the final format, if necessary - if (buf != resample_buffer) { - ConvertAudio(output_frames, resample_buffer, SDL_AUDIO_F32, resample_channels, buf, dst_format, dst_channels, work_buffer); - } + ConvertAudio(output_frames, resample_buffer, resample_format, resample_channels, buf, dst_format, dst_channels, work_buffer); return 0; } @@ -1074,7 +1013,9 @@ int SDL_GetAudioStreamQueued(SDL_AudioStream *stream) } SDL_LockMutex(stream->lock); - const Uint64 total = stream->total_bytes_queued; + + size_t total = SDL_GetAudioQueueQueued(stream->queue); + SDL_UnlockMutex(stream->lock); // if this overflows an int, just clamp it to a maximum. @@ -1092,7 +1033,6 @@ int SDL_ClearAudioStream(SDL_AudioStream *stream) SDL_ClearAudioQueue(stream->queue); SDL_zero(stream->input_spec); stream->resample_offset = 0; - stream->total_bytes_queued = 0; SDL_UnlockMutex(stream->lock); return 0; @@ -1118,7 +1058,6 @@ void SDL_DestroyAudioStream(SDL_AudioStream *stream) SDL_UnbindAudioStream(stream); } - SDL_aligned_free(stream->history_buffer); SDL_aligned_free(stream->work_buffer); SDL_DestroyAudioQueue(stream->queue); SDL_DestroyMutex(stream->lock); @@ -1126,6 +1065,11 @@ void SDL_DestroyAudioStream(SDL_AudioStream *stream) SDL_free(stream); } +static void SDLCALL DontFreeThisAudioBuffer(void *userdata, const void *buf, int len) +{ + // We don't own the buffer, but know it will outlive the stream +} + int SDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data, int src_len, const SDL_AudioSpec *dst_spec, Uint8 **dst_data, int *dst_len) { @@ -1153,7 +1097,7 @@ int SDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data SDL_AudioStream *stream = SDL_CreateAudioStream(src_spec, dst_spec); if (stream) { - if ((SDL_PutAudioStreamData(stream, src_data, src_len) == 0) && (SDL_FlushAudioStream(stream) == 0)) { + if ((PutAudioStreamBuffer(stream, src_data, src_len, DontFreeThisAudioBuffer, NULL) == 0) && (SDL_FlushAudioStream(stream) == 0)) { dstlen = SDL_GetAudioStreamAvailable(stream); if (dstlen >= 0) { dst = (Uint8 *)SDL_malloc(dstlen); diff --git a/src/audio/SDL_audioqueue.c b/src/audio/SDL_audioqueue.c index 923873676..520b7d724 100644 --- a/src/audio/SDL_audioqueue.c +++ b/src/audio/SDL_audioqueue.c @@ -21,253 +21,131 @@ #include "SDL_internal.h" #include "SDL_audioqueue.h" +#include "SDL_sysaudio.h" #define AUDIO_SPECS_EQUAL(x, y) (((x).format == (y).format) && ((x).channels == (y).channels) && ((x).freq == (y).freq)) +typedef struct SDL_MemoryPool SDL_MemoryPool; + +struct SDL_MemoryPool +{ + void *free_blocks; + size_t block_size; + size_t num_free; + size_t max_free; +}; + struct SDL_AudioTrack { SDL_AudioSpec spec; SDL_bool flushed; SDL_AudioTrack *next; - size_t (*avail)(void *ctx); - int (*write)(void *ctx, const Uint8 *buf, size_t len); - size_t (*read)(void *ctx, Uint8 *buf, size_t len, SDL_bool advance); - void (*destroy)(void *ctx); + void *userdata; + SDL_ReleaseAudioBufferCallback callback; + + Uint8 *data; + size_t head; + size_t tail; + size_t capacity; }; struct SDL_AudioQueue { SDL_AudioTrack *head; SDL_AudioTrack *tail; - size_t chunk_size; + + Uint8 *history_buffer; + size_t history_length; + size_t history_capacity; + + SDL_MemoryPool track_pool; + SDL_MemoryPool chunk_pool; }; -typedef struct SDL_AudioChunk SDL_AudioChunk; - -struct SDL_AudioChunk +// Allocate a new block, avoiding checking for ones already in the pool +static void *AllocNewMemoryPoolBlock(const SDL_MemoryPool *pool) { - SDL_AudioChunk *next; - size_t head; - size_t tail; - Uint8 data[SDL_VARIABLE_LENGTH_ARRAY]; -}; - -typedef struct SDL_ChunkedAudioTrack -{ - SDL_AudioTrack track; - - size_t chunk_size; - - SDL_AudioChunk *head; - SDL_AudioChunk *tail; - size_t queued_bytes; - - SDL_AudioChunk *free_chunks; - size_t num_free_chunks; -} SDL_ChunkedAudioTrack; - -static void DestroyAudioChunk(SDL_AudioChunk *chunk) -{ - SDL_free(chunk); + return SDL_malloc(pool->block_size); } -static void DestroyAudioChunks(SDL_AudioChunk *chunk) +// Allocate a new block, first checking if there are any in the pool +static void *AllocMemoryPoolBlock(SDL_MemoryPool *pool) { - while (chunk) { - SDL_AudioChunk *next = chunk->next; - DestroyAudioChunk(chunk); - chunk = next; - } -} - -static void ResetAudioChunk(SDL_AudioChunk *chunk) -{ - chunk->next = NULL; - chunk->head = 0; - chunk->tail = 0; -} - -static SDL_AudioChunk *CreateAudioChunk(size_t chunk_size) -{ - SDL_AudioChunk *chunk = (SDL_AudioChunk *)SDL_malloc(sizeof(*chunk) + chunk_size); - - if (!chunk) { - return NULL; + if (pool->num_free == 0) { + return AllocNewMemoryPoolBlock(pool); } - ResetAudioChunk(chunk); - - return chunk; + void *block = pool->free_blocks; + pool->free_blocks = *(void **)block; + --pool->num_free; + return block; } -static void DestroyAudioTrackChunk(SDL_ChunkedAudioTrack *track, SDL_AudioChunk *chunk) +// Free a block, or add it to the pool if there's room +static void FreeMemoryPoolBlock(SDL_MemoryPool *pool, void *block) { - // Keeping a list of free chunks reduces memory allocations, - // But also increases the amount of work to perform when freeing the track. - const size_t max_free_bytes = 64 * 1024; - - if (track->chunk_size * track->num_free_chunks < max_free_bytes) { - chunk->next = track->free_chunks; - track->free_chunks = chunk; - ++track->num_free_chunks; + if (pool->num_free < pool->max_free) { + *(void **)block = pool->free_blocks; + pool->free_blocks = block; + ++pool->num_free; } else { - DestroyAudioChunk(chunk); + SDL_free(block); } } -static SDL_AudioChunk *CreateAudioTrackChunk(SDL_ChunkedAudioTrack *track) +// Destroy a pool and all of its blocks +static void DestroyMemoryPool(SDL_MemoryPool *pool) { - if (track->num_free_chunks > 0) { - SDL_AudioChunk *chunk = track->free_chunks; + void *block = pool->free_blocks; + pool->free_blocks = NULL; + pool->num_free = 0; - track->free_chunks = chunk->next; - --track->num_free_chunks; - - ResetAudioChunk(chunk); - - return chunk; + while (block) { + void *next = *(void **)block; + SDL_free(block); + block = next; } - - return CreateAudioChunk(track->chunk_size); } -static size_t AvailChunkedAudioTrack(void *ctx) +// Keeping a list of free chunks reduces memory allocations, +// But also increases the amount of work to perform when freeing the track. +static void InitMemoryPool(SDL_MemoryPool *pool, size_t block_size, size_t max_free) { - SDL_ChunkedAudioTrack *track = (SDL_ChunkedAudioTrack *)ctx; + SDL_zerop(pool); - return track->queued_bytes; + SDL_assert(block_size >= sizeof(void *)); + pool->block_size = block_size; + pool->max_free = max_free; } -static int WriteToChunkedAudioTrack(void *ctx, const Uint8 *data, size_t len) +// Allocates a number of blocks and adds them to the pool +static int ReserveMemoryPoolBlocks(SDL_MemoryPool *pool, size_t num_blocks) { - SDL_ChunkedAudioTrack *track = (SDL_ChunkedAudioTrack *)ctx; + for (; num_blocks; --num_blocks) { + void *block = AllocNewMemoryPoolBlock(pool); - SDL_AudioChunk *chunk = track->tail; - - // Handle the first chunk - if (!chunk) { - chunk = CreateAudioTrackChunk(track); - - if (!chunk) { - return -1; + if (block == NULL) { + return SDL_OutOfMemory(); } - SDL_assert((track->head == NULL) && (track->tail == NULL) && (track->queued_bytes == 0)); - track->head = chunk; - track->tail = chunk; + *(void **)block = pool->free_blocks; + pool->free_blocks = block; + ++pool->num_free; } - size_t total = 0; - size_t old_tail = chunk->tail; - size_t chunk_size = track->chunk_size; - - while (chunk) { - size_t to_write = chunk_size - chunk->tail; - to_write = SDL_min(to_write, len - total); - SDL_memcpy(&chunk->data[chunk->tail], &data[total], to_write); - total += to_write; - - chunk->tail += to_write; - - if (total == len) { - break; - } - - SDL_AudioChunk *next = CreateAudioTrackChunk(track); - chunk->next = next; - chunk = next; - } - - // Roll back the changes if we couldn't write all the data - if (!chunk) { - chunk = track->tail; - - SDL_AudioChunk *next = chunk->next; - chunk->next = NULL; - chunk->tail = old_tail; - - DestroyAudioChunks(next); - - return -1; - } - - track->tail = chunk; - track->queued_bytes += total; - return 0; } -static size_t ReadFromChunkedAudioTrack(void *ctx, Uint8 *data, size_t len, SDL_bool advance) +void SDL_DestroyAudioQueue(SDL_AudioQueue *queue) { - SDL_ChunkedAudioTrack *track = (SDL_ChunkedAudioTrack *)ctx; - SDL_AudioChunk *chunk = track->head; + SDL_ClearAudioQueue(queue); - size_t total = 0; - size_t head = 0; + DestroyMemoryPool(&queue->track_pool); + DestroyMemoryPool(&queue->chunk_pool); + SDL_aligned_free(queue->history_buffer); - while (chunk) { - head = chunk->head; - - size_t to_read = chunk->tail - head; - to_read = SDL_min(to_read, len - total); - SDL_memcpy(&data[total], &chunk->data[head], to_read); - total += to_read; - - SDL_AudioChunk *next = chunk->next; - - if (total == len) { - head += to_read; - break; - } - - if (advance) { - DestroyAudioTrackChunk(track, chunk); - } - - chunk = next; - } - - if (advance) { - if (chunk) { - chunk->head = head; - track->head = chunk; - } else { - track->head = NULL; - track->tail = NULL; - } - - track->queued_bytes -= total; - } - - return total; -} - -static void DestroyChunkedAudioTrack(void *ctx) -{ - SDL_ChunkedAudioTrack *track = (SDL_ChunkedAudioTrack *)ctx; - DestroyAudioChunks(track->head); - DestroyAudioChunks(track->free_chunks); - SDL_free(track); -} - -static SDL_AudioTrack *CreateChunkedAudioTrack(const SDL_AudioSpec *spec, size_t chunk_size) -{ - SDL_ChunkedAudioTrack *track = (SDL_ChunkedAudioTrack *)SDL_calloc(1, sizeof(*track)); - - if (!track) { - return NULL; - } - - SDL_copyp(&track->track.spec, spec); - track->track.avail = AvailChunkedAudioTrack; - track->track.write = WriteToChunkedAudioTrack; - track->track.read = ReadFromChunkedAudioTrack; - track->track.destroy = DestroyChunkedAudioTrack; - - track->chunk_size = chunk_size; - - return &track->track; + SDL_free(queue); } SDL_AudioQueue *SDL_CreateAudioQueue(size_t chunk_size) @@ -278,35 +156,42 @@ SDL_AudioQueue *SDL_CreateAudioQueue(size_t chunk_size) return NULL; } - queue->chunk_size = chunk_size; + InitMemoryPool(&queue->track_pool, sizeof(SDL_AudioTrack), 8); + InitMemoryPool(&queue->chunk_pool, chunk_size, 4); + + if (ReserveMemoryPoolBlocks(&queue->track_pool, 2) != 0) { + SDL_DestroyAudioQueue(queue); + return NULL; + } return queue; } -void SDL_DestroyAudioQueue(SDL_AudioQueue *queue) +static void DestroyAudioTrack(SDL_AudioQueue *queue, SDL_AudioTrack *track) { - SDL_ClearAudioQueue(queue); + track->callback(track->userdata, track->data, (int)track->capacity); - SDL_free(queue); + FreeMemoryPoolBlock(&queue->track_pool, track); } void SDL_ClearAudioQueue(SDL_AudioQueue *queue) { SDL_AudioTrack *track = queue->head; + queue->head = NULL; queue->tail = NULL; + queue->history_length = 0; while (track) { SDL_AudioTrack *next = track->next; - track->destroy(track); + DestroyAudioTrack(queue, track); track = next; } } -static void SDL_FlushAudioTrack(SDL_AudioTrack *track) +static void FlushAudioTrack(SDL_AudioTrack *track) { track->flushed = SDL_TRUE; - track->write = NULL; } void SDL_FlushAudioQueue(SDL_AudioQueue *queue) @@ -314,7 +199,7 @@ void SDL_FlushAudioQueue(SDL_AudioQueue *queue) SDL_AudioTrack *track = queue->tail; if (track) { - SDL_FlushAudioTrack(track); + FlushAudioTrack(track); } } @@ -326,7 +211,7 @@ void SDL_PopAudioQueueHead(SDL_AudioQueue *queue) SDL_bool flushed = track->flushed; SDL_AudioTrack *next = track->next; - track->destroy(track); + DestroyAudioTrack(queue, track); track = next; if (flushed) { @@ -335,27 +220,59 @@ void SDL_PopAudioQueueHead(SDL_AudioQueue *queue) } queue->head = track; + queue->history_length = 0; if (!track) { queue->tail = NULL; } } -size_t SDL_GetAudioQueueChunkSize(SDL_AudioQueue *queue) +SDL_AudioTrack *SDL_CreateAudioTrack( + SDL_AudioQueue *queue, const SDL_AudioSpec *spec, + Uint8 *data, size_t len, size_t capacity, + SDL_ReleaseAudioBufferCallback callback, void *userdata) { - return queue->chunk_size; -} - -SDL_AudioTrack *SDL_CreateChunkedAudioTrack(const SDL_AudioSpec *spec, const Uint8 *data, size_t len, size_t chunk_size) -{ - SDL_AudioTrack *track = CreateChunkedAudioTrack(spec, chunk_size); + SDL_AudioTrack *track = AllocMemoryPoolBlock(&queue->track_pool); if (!track) { return NULL; } - if (track->write(track, data, len) != 0) { - track->destroy(track); + SDL_zerop(track); + SDL_copyp(&track->spec, spec); + + track->userdata = userdata; + track->callback = callback; + track->data = data; + track->head = 0; + track->tail = len; + track->capacity = capacity; + + return track; +} + +static void SDLCALL FreeChunkedAudioBuffer(void *userdata, const void *buf, int len) +{ + SDL_AudioQueue *queue = userdata; + + FreeMemoryPoolBlock(&queue->chunk_pool, (void *)buf); +} + +static SDL_AudioTrack *CreateChunkedAudioTrack(SDL_AudioQueue *queue, const SDL_AudioSpec *spec) +{ + void *chunk = AllocMemoryPoolBlock(&queue->chunk_pool); + + if (!chunk) { + return NULL; + } + + size_t capacity = queue->chunk_pool.block_size; + capacity -= capacity % SDL_AUDIO_FRAMESIZE(*spec); + + SDL_AudioTrack *track = SDL_CreateAudioTrack(queue, spec, chunk, 0, capacity, FreeChunkedAudioBuffer, queue); + + if (!track) { + FreeMemoryPoolBlock(&queue->chunk_pool, chunk); return NULL; } @@ -369,7 +286,7 @@ void SDL_AddTrackToAudioQueue(SDL_AudioQueue *queue, SDL_AudioTrack *track) if (tail) { // If the spec has changed, make sure to flush the previous track if (!AUDIO_SPECS_EQUAL(tail->spec, track->spec)) { - SDL_FlushAudioTrack(tail); + FlushAudioTrack(tail); } tail->next = track; @@ -380,6 +297,19 @@ void SDL_AddTrackToAudioQueue(SDL_AudioQueue *queue, SDL_AudioTrack *track) queue->tail = track; } +static size_t WriteToAudioTrack(SDL_AudioTrack *track, const Uint8 *data, size_t len) +{ + if (track->flushed || track->tail >= track->capacity) { + return 0; + } + + len = SDL_min(len, track->capacity - track->tail); + SDL_memcpy(&track->data[track->tail], data, len); + track->tail += len; + + return len; +} + int SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const Uint8 *data, size_t len) { if (len == 0) { @@ -388,29 +318,43 @@ int SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, cons SDL_AudioTrack *track = queue->tail; - if ((track) && !AUDIO_SPECS_EQUAL(track->spec, *spec)) { - SDL_FlushAudioTrack(track); + if (track) { + if (!AUDIO_SPECS_EQUAL(track->spec, *spec)) { + FlushAudioTrack(track); + } + } else { + SDL_assert(!queue->head); + track = CreateChunkedAudioTrack(queue, spec); + + if (!track) { + return -1; + } + + queue->head = track; + queue->tail = track; } - if ((!track) || (!track->write)) { - SDL_AudioTrack *new_track = CreateChunkedAudioTrack(spec, queue->chunk_size); + for (;;) { + size_t written = WriteToAudioTrack(track, data, len); + data += written; + len -= written; + + if (len == 0) { + break; + } + + SDL_AudioTrack *new_track = CreateChunkedAudioTrack(queue, spec); if (!new_track) { return -1; } - if (track) { - track->next = new_track; - } else { - queue->head = new_track; - } - + track->next = new_track; queue->tail = new_track; - track = new_track; } - return track->write(track, data, len); + return 0; } void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue) @@ -432,7 +376,7 @@ size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_Audi SDL_AudioTrack *track = iter; iter = iter->next; - size_t avail = track->avail(track); + size_t avail = track->tail - track->head; if (avail >= SDL_SIZE_MAX - queued_bytes) { queued_bytes = SDL_SIZE_MAX; @@ -454,61 +398,244 @@ size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_Audi return queued_bytes; } -int SDL_ReadFromAudioQueue(SDL_AudioQueue *queue, Uint8 *data, size_t len) +static const Uint8 *PeekIntoAudioQueuePast(SDL_AudioQueue *queue, Uint8 *data, size_t len) { - size_t total = 0; SDL_AudioTrack *track = queue->head; - for (;;) { - if (!track) { - return SDL_SetError("Reading past end of queue"); - } + if (track->head >= len) { + return &track->data[track->head - len]; + } - total += track->read(track, &data[total], len - total, SDL_TRUE); + size_t past = len - track->head; + + if (past > queue->history_length) { + return NULL; + } + + SDL_memcpy(data, &queue->history_buffer[queue->history_length - past], past); + SDL_memcpy(&data[past], track->data, track->head); + + return data; +} + +static void UpdateAudioQueueHistory(SDL_AudioQueue *queue, + const Uint8 *data, size_t len) +{ + Uint8 *history_buffer = queue->history_buffer; + size_t history_bytes = queue->history_length; + + if (len >= history_bytes) { + SDL_memcpy(history_buffer, &data[len - history_bytes], history_bytes); + } else { + size_t preserve = history_bytes - len; + SDL_memmove(history_buffer, &history_buffer[len], preserve); + SDL_memcpy(&history_buffer[preserve], data, len); + } +} + +static const Uint8 *ReadFromAudioQueue(SDL_AudioQueue *queue, Uint8 *data, size_t len) +{ + SDL_AudioTrack *track = queue->head; + + if (track->tail - track->head >= len) { + const Uint8 *ptr = &track->data[track->head]; + track->head += len; + return ptr; + } + + size_t total = 0; + + for (;;) { + size_t avail = SDL_min(len - total, track->tail - track->head); + SDL_memcpy(&data[total], &track->data[track->head], avail); + track->head += avail; + total += avail; if (total == len) { - return 0; + break; } if (track->flushed) { - return SDL_SetError("Reading past end of flushed track"); + SDL_SetError("Reading past end of flushed track"); + return NULL; } SDL_AudioTrack *next = track->next; if (!next) { - return SDL_SetError("Reading past end of incomplete track"); + SDL_SetError("Reading past end of incomplete track"); + return NULL; } + UpdateAudioQueueHistory(queue, track->data, track->tail); + queue->head = next; - - track->destroy(track); + DestroyAudioTrack(queue, track); track = next; } + + return data; } -int SDL_PeekIntoAudioQueue(SDL_AudioQueue *queue, Uint8 *data, size_t len) +static const Uint8 *PeekIntoAudioQueueFuture(SDL_AudioQueue *queue, Uint8 *data, size_t len) { - size_t total = 0; SDL_AudioTrack *track = queue->head; - for (;;) { - if (!track) { - return SDL_SetError("Peeking past end of queue"); - } + if (track->tail - track->head >= len) { + return &track->data[track->head]; + } - total += track->read(track, &data[total], len - total, SDL_FALSE); + size_t total = 0; + + for (;;) { + size_t avail = SDL_min(len - total, track->tail - track->head); + SDL_memcpy(&data[total], &track->data[track->head], avail); + total += avail; if (total == len) { - return 0; + break; } if (track->flushed) { // If we have run out of data, fill the rest with silence. SDL_memset(&data[total], SDL_GetSilenceValueForFormat(track->spec.format), len - total); - return 0; + break; } track = track->next; + + if (!track) { + SDL_SetError("Peeking past end of incomplete track"); + return NULL; + } } + + return data; +} + +const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue, + Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, + int past_frames, int present_frames, int future_frames, + Uint8 *scratch) +{ + SDL_AudioTrack *track = queue->head; + + if (!track) { + return NULL; + } + + SDL_AudioFormat src_format = track->spec.format; + SDL_AudioFormat src_channels = track->spec.channels; + + size_t src_frame_size = SDL_AUDIO_BYTESIZE(src_format) * src_channels; + size_t dst_frame_size = SDL_AUDIO_BYTESIZE(dst_format) * dst_channels; + + size_t src_past_bytes = past_frames * src_frame_size; + size_t src_present_bytes = present_frames * src_frame_size; + size_t src_future_bytes = future_frames * src_frame_size; + + size_t dst_past_bytes = past_frames * dst_frame_size; + size_t dst_present_bytes = present_frames * dst_frame_size; + size_t dst_future_bytes = future_frames * dst_frame_size; + + SDL_bool convert = (src_format != dst_format) || (src_channels != dst_channels); + + if (convert && !dst) { + // The user didn't ask for the data to be copied, but we need to convert it, so store it in the scratch buffer + dst = scratch; + } + + // Can we get all of the data straight from this track? + if ((track->head >= src_past_bytes) && ((track->tail - track->head) >= (src_present_bytes + src_future_bytes))) { + const Uint8 *ptr = &track->data[track->head - src_past_bytes]; + track->head += src_present_bytes; + + // Do we still need to copy/convert the data? + if (dst) { + ConvertAudio(past_frames + present_frames + future_frames, ptr, + src_format, src_channels, dst, dst_format, dst_channels, scratch); + ptr = dst; + } + + return ptr; + } + + if (!dst) { + // The user didn't ask for the data to be copied, but we need to, so store it in the scratch buffer + dst = scratch; + } else if (!convert) { + // We are only copying, not converting, so copy straight into the dst buffer + scratch = dst; + } + + Uint8 *ptr = dst; + + if (src_past_bytes) { + ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch); + dst += dst_past_bytes; + scratch += dst_past_bytes; + } + + if (src_present_bytes) { + ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch); + dst += dst_present_bytes; + scratch += dst_present_bytes; + } + + if (src_future_bytes) { + ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch); + dst += dst_future_bytes; + scratch += dst_future_bytes; + } + + return ptr; +} + +size_t SDL_GetAudioQueueQueued(SDL_AudioQueue *queue) +{ + size_t total = 0; + void *iter = SDL_BeginAudioQueueIter(queue); + + while (iter) { + SDL_AudioSpec src_spec; + SDL_bool flushed; + + size_t avail = SDL_NextAudioQueueIter(queue, &iter, &src_spec, &flushed); + + if (avail >= SDL_SIZE_MAX - total) { + total = SDL_SIZE_MAX; + break; + } + + total += avail; + } + + return total; +} + +int SDL_ResetAudioQueueHistory(SDL_AudioQueue *queue, int num_frames) +{ + SDL_AudioTrack *track = queue->head; + + if (!track) { + return -1; + } + + size_t length = num_frames * SDL_AUDIO_FRAMESIZE(track->spec); + Uint8 *history_buffer = queue->history_buffer; + + if (queue->history_capacity < length) { + history_buffer = SDL_aligned_alloc(SDL_SIMDGetAlignment(), length); + if (!history_buffer) { + return -1; + } + SDL_aligned_free(queue->history_buffer); + queue->history_buffer = history_buffer; + queue->history_capacity = length; + } + + queue->history_length = length; + SDL_memset(history_buffer, SDL_GetSilenceValueForFormat(track->spec.format), length); + + return 0; } diff --git a/src/audio/SDL_audioqueue.h b/src/audio/SDL_audioqueue.h index 9b18a5c5e..54c2ba72a 100644 --- a/src/audio/SDL_audioqueue.h +++ b/src/audio/SDL_audioqueue.h @@ -25,6 +25,8 @@ // Internal functions used by SDL_AudioStream for queueing audio. +typedef void(SDLCALL *SDL_ReleaseAudioBufferCallback)(void *userdata, const void *buffer, int buflen); + typedef struct SDL_AudioQueue SDL_AudioQueue; typedef struct SDL_AudioTrack SDL_AudioTrack; @@ -44,16 +46,14 @@ void SDL_FlushAudioQueue(SDL_AudioQueue *queue); // REQUIRES: The head track must exist, and must have been flushed void SDL_PopAudioQueueHead(SDL_AudioQueue *queue); -// Get the chunk size, mostly for use with SDL_CreateChunkedAudioTrack -// This can be called from any thread -size_t SDL_GetAudioQueueChunkSize(SDL_AudioQueue *queue); - // Write data to the end of queue // REQUIRES: If the spec has changed, the last track must have been flushed int SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const Uint8 *data, size_t len); -// Create a track without needing to hold any locks -SDL_AudioTrack *SDL_CreateChunkedAudioTrack(const SDL_AudioSpec *spec, const Uint8 *data, size_t len, size_t chunk_size); +// Create a track where the input data is owned by the caller +SDL_AudioTrack *SDL_CreateAudioTrack(SDL_AudioQueue *queue, + const SDL_AudioSpec *spec, Uint8 *data, size_t len, size_t capacity, + SDL_ReleaseAudioBufferCallback callback, void *userdata); // Add a track to the end of the queue // REQUIRES: `track != NULL` @@ -66,12 +66,14 @@ void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue); // REQUIRES: `*inout_iter != NULL` (a valid iterator) size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_AudioSpec *out_spec, SDL_bool *out_flushed); -// Read data from the start of the queue -// REQUIRES: There must be enough data in the queue -int SDL_ReadFromAudioQueue(SDL_AudioQueue *queue, Uint8 *data, size_t len); +const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue, + Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, + int past_frames, int present_frames, int future_frames, + Uint8 *scratch); -// Peek into the start of the queue -// REQUIRES: There must be enough data in the queue, unless it has been flushed, in which case missing data is filled with silence. -int SDL_PeekIntoAudioQueue(SDL_AudioQueue *queue, Uint8 *data, size_t len); +// Get the total number of bytes currently queued +size_t SDL_GetAudioQueueQueued(SDL_AudioQueue *queue); + +int SDL_ResetAudioQueueHistory(SDL_AudioQueue *queue, int num_frames); #endif // SDL_audioqueue_h_ diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index d2e243d8c..d94358f76 100644 --- a/src/audio/SDL_sysaudio.h +++ b/src/audio/SDL_sysaudio.h @@ -190,7 +190,6 @@ struct SDL_AudioStream float freq_ratio; struct SDL_AudioQueue* queue; - Uint64 total_bytes_queued; SDL_AudioSpec input_spec; // The spec of input data currently being processed Sint64 resample_offset; @@ -198,9 +197,6 @@ struct SDL_AudioStream Uint8 *work_buffer; // used for scratch space during data conversion/resampling. size_t work_buffer_allocation; - Uint8 *history_buffer; // history for left padding and future sample rate changes. - size_t history_buffer_allocation; - SDL_bool simplified; // SDL_TRUE if created via SDL_OpenAudioDeviceStream SDL_LogicalAudioDevice *bound_device; diff --git a/test/testaudiostreamdynamicresample.c b/test/testaudiostreamdynamicresample.c index 06c2ba9d1..1db69ecf3 100644 --- a/test/testaudiostreamdynamicresample.c +++ b/test/testaudiostreamdynamicresample.c @@ -10,8 +10,6 @@ freely. */ -/* !!! FIXME: this code is not up to standards for SDL3 test apps. Someone should improve this. */ - #include #include #include @@ -117,6 +115,7 @@ static void queue_audio() SDL_Log("Converting audio from %i to %i", spec.freq, new_spec.freq); + /* You shouldn't actually use SDL_ConvertAudioSamples like this (just put the data straight into the stream and let it handle conversion) */ retval = retval ? retval : SDL_ConvertAudioSamples(&spec, audio_buf, audio_len, &new_spec, &new_data, &new_len); retval = retval ? retval : SDL_SetAudioStreamFormat(stream, &new_spec, NULL); retval = retval ? retval : SDL_PutAudioStreamData(stream, new_data, new_len); @@ -207,6 +206,7 @@ static void loop(void) SDL_Event e; SDL_FPoint p; SDL_AudioSpec src_spec, dst_spec; + int queued_bytes = 0; int available_bytes = 0; float available_seconds = 0; @@ -294,6 +294,8 @@ static void loop(void) } } + queued_bytes = SDL_GetAudioStreamQueued(stream); + for (i = 0; i < state->num_windows; i++) { int draw_y = 0; SDL_Renderer* rend = state->renderers[i]; @@ -326,6 +328,9 @@ static void loop(void) draw_textf(rend, 0, draw_y, "Available: %4.2f (%i bytes)", available_seconds, available_bytes); draw_y += FONT_LINE_HEIGHT; + draw_textf(rend, 0, draw_y, "Queued: %i bytes", queued_bytes); + draw_y += FONT_LINE_HEIGHT; + SDL_LockAudioStream(stream); draw_textf(rend, 0, draw_y, "Get Callback: %i/%i bytes, %2i ms ago", diff --git a/test/testautomation_audio.c b/test/testautomation_audio.c index cc0ffd9fe..4747fbb88 100644 --- a/test/testautomation_audio.c +++ b/test/testautomation_audio.c @@ -821,11 +821,6 @@ static double sine_wave_sample(const Sint64 idx, const Sint64 rate, const Sint64 return SDL_sin(((double)(idx * freq % rate)) / ((double)rate) * (SDL_PI_D * 2) + phase); } -static void free_audio_buffer(void* userdata, const void* buf, int len) -{ - SDL_free((void*) buf); -} - /* Split the data into randomly sized chunks */ static int put_audio_data_split(SDL_AudioStream* stream, const void* buf, int len) {