From 608f706a954cb856c8ae2f18d3a6a0339ff7b0ae Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Sun, 6 Apr 2025 14:31:47 -0400 Subject: [PATCH] audio: Added SDL_SetAudioIterationCallbacks(). --- include/SDL3/SDL_audio.h | 78 +++++++++++++++++++++++++++++++ src/audio/SDL_audio.c | 41 +++++++++++++++- src/audio/SDL_sysaudio.h | 7 +++ src/dynapi/SDL_dynapi.sym | 1 + src/dynapi/SDL_dynapi_overrides.h | 1 + src/dynapi/SDL_dynapi_procs.h | 1 + 6 files changed, 127 insertions(+), 2 deletions(-) diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h index 0213f0cea2..4bb7564035 100644 --- a/include/SDL3/SDL_audio.h +++ b/include/SDL3/SDL_audio.h @@ -1931,6 +1931,84 @@ extern SDL_DECLSPEC void SDLCALL SDL_DestroyAudioStream(SDL_AudioStream *stream) */ extern SDL_DECLSPEC SDL_AudioStream * SDLCALL SDL_OpenAudioDeviceStream(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec, SDL_AudioStreamCallback callback, void *userdata); +/** + * A callback that fires around an audio device's processing work. + * + * This callback fires when a logical audio device is about to start + * accessing its bound audio streams, and fires again when it has + * finished accessing them. It covers the range of one "iteration" of + * the audio device. + * + * It can be useful to use this callback to update state that must + * apply to all bound audio streams atomically: to make sure state + * changes don't happen while half of the streams are already processed + * for the latest audio buffer. + * + * This callback should run as quickly as possible and not block for any + * significant time, as this callback delays submission of data to the audio + * device, which can cause audio playback problems. This callback delays all + * audio processing across a single physical audio device: all its logical + * devices and all bound audio streams. Use it carefully. + * + * \param userdata a pointer provided by the app through + * SDL_SetAudioPostmixCallback, for its own use. + * \param devid the audio device this callback is running for. + * \param start true if this is the start of the iteration, false if the end. + * + * \threadsafety This will run from a background thread owned by SDL. The + * application is responsible for locking resources the callback + * touches that need to be protected. + * + * \since This datatype is available since SDL 3.4.0. + * + * \sa SDL_SetAudioIterationCallbacks + */ +typedef void (SDLCALL *SDL_AudioIterationCallback)(void *userdata, SDL_AudioDeviceID devid, bool start); + +/** + * Set callbacks that fire around a new iteration of audio device processing. + * + * Two callbacks are provided here: one that runs when a device is about to + * process its bound audio streams, and another that runs when the device has + * finished processing them. + * + * These callbacks can run at any time, and from any thread; if you need to + * serialize access to your app's data, you should provide and use a mutex or + * other synchronization device. + * + * Generally these callbacks are used to apply state that applies to multiple + * bound audio streams, with a guarantee that the audio device's thread isn't + * halfway through processing them. Generally a finer-grained lock through + * SDL_LockAudioStream() is more appropriate. + * + * The callbacks are extremely time-sensitive; the callback should do the + * least amount of work possible and return as quickly as it can. The longer + * the callback runs, the higher the risk of audio dropouts or other problems. + * + * This function will block until the audio device is in between iterations, + * so any existing callback that might be running will finish before this + * function sets the new callback and returns. + * + * Physical devices do not accept these callbacks, only logical devices + * created through SDL_OpenAudioDevice() can be. + * + * Setting a NULL callback function disables any previously-set callback. + * Either callback may be NULL, and the same callback is permitted to be used + * for both. + * + * \param devid the ID of an opened audio device. + * \param start a callback function to be called at the start of an iteration. Can be NULL. + * \param end a callback function to be called at the end of an iteration. Can be NULL. + * \param userdata app-controlled pointer passed to callback. Can be NULL. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.4.0. + */ +extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioIterationCallbacks(SDL_AudioDeviceID devid, SDL_AudioIterationCallback start, SDL_AudioIterationCallback end, void *userdata); + /** * A callback that fires when data is about to be fed to an audio device. * diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index a3a115c48d..f11066c21b 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -1147,7 +1147,20 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) // We should have updated this elsewhere if the format changed! SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec, NULL, NULL)); - const int br = SDL_GetAtomicInt(&logdev->paused) ? 0 : SDL_GetAudioStreamDataAdjustGain(stream, device_buffer, buffer_size, logdev->gain); + int br = 0; + + if (!SDL_GetAtomicInt(&logdev->paused)) { + if (logdev->iteration_start) { + logdev->iteration_start(logdev->iteration_userdata, logdev->instance_id, true); + } + + br = SDL_GetAudioStreamDataAdjustGain(stream, device_buffer, buffer_size, logdev->gain); + + if (logdev->iteration_end) { + logdev->iteration_end(logdev->iteration_userdata, logdev->instance_id, false); + } + } + if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow. failed = true; SDL_memset(device_buffer, device->silence_value, buffer_size); // just supply silence to the device before we die. @@ -1185,6 +1198,10 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) SDL_memset(mix_buffer, '\0', work_buffer_size); // start with silence. } + if (logdev->iteration_start) { + logdev->iteration_start(logdev->iteration_userdata, logdev->instance_id, true); + } + for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) { // We should have updated this elsewhere if the format changed! SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec, NULL, NULL)); @@ -1207,6 +1224,10 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) } } + if (logdev->iteration_end) { + logdev->iteration_end(logdev->iteration_userdata, logdev->instance_id, false); + } + if (postmix) { SDL_assert(mix_buffer == device->postmix_buffer); postmix(logdev->postmix_userdata, &outspec, mix_buffer, work_buffer_size); @@ -1902,8 +1923,9 @@ bool SDL_SetAudioPostmixCallback(SDL_AudioDeviceID devid, SDL_AudioPostmixCallba { SDL_AudioDevice *device = NULL; SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); - bool result = true; + bool result = false; if (logdev) { + result = true; if (callback && !device->postmix_buffer) { device->postmix_buffer = (float *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), device->work_buffer_size); if (!device->postmix_buffer) { @@ -1922,6 +1944,21 @@ bool SDL_SetAudioPostmixCallback(SDL_AudioDeviceID devid, SDL_AudioPostmixCallba return result; } +bool SDL_SetAudioIterationCallbacks(SDL_AudioDeviceID devid, SDL_AudioIterationCallback iter_start, SDL_AudioIterationCallback iter_end, void *userdata) +{ + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); + bool result = false; + if (logdev) { + logdev->iteration_start = iter_start; + logdev->iteration_end = iter_end; + logdev->iteration_userdata = userdata; + result = true; + } + ReleaseAudioDevice(device); + return result; +} + bool SDL_BindAudioStreams(SDL_AudioDeviceID devid, SDL_AudioStream * const *streams, int num_streams) { const bool islogical = !(devid & (1<<1)); diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index 4a88bd2302..3e2936cbae 100644 --- a/src/audio/SDL_sysaudio.h +++ b/src/audio/SDL_sysaudio.h @@ -264,6 +264,13 @@ struct SDL_LogicalAudioDevice // true if device was opened with SDL_OpenAudioDeviceStream (so it forbids binding changes, etc). bool simplified; + // If non-NULL, callback into the app that alerts it to start/end of device iteration. + SDL_AudioIterationCallback iteration_start; + SDL_AudioIterationCallback iteration_end; + + // App-supplied pointer for iteration callbacks. + void *iteration_userdata; + // If non-NULL, callback into the app that lets them access the final postmix buffer. SDL_AudioPostmixCallback postmix; diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 93d6771cd8..b602a1a9ca 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1251,6 +1251,7 @@ SDL3_0.0.0 { SDL_GetGPUDeviceProperties; SDL_CreateGPURenderer; SDL_PutAudioStreamPlanarData; + SDL_SetAudioIterationCallbacks; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 2808e409b6..bfbce4fa75 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1276,3 +1276,4 @@ #define SDL_GetGPUDeviceProperties SDL_GetGPUDeviceProperties_REAL #define SDL_CreateGPURenderer SDL_CreateGPURenderer_REAL #define SDL_PutAudioStreamPlanarData SDL_PutAudioStreamPlanarData_REAL +#define SDL_SetAudioIterationCallbacks SDL_SetAudioIterationCallbacks_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 86005529a1..b29428f58b 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1284,3 +1284,4 @@ SDL_DYNAPI_PROC(bool,SDL_GetRenderTextureAddressMode,(SDL_Renderer *a,SDL_Textur SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetGPUDeviceProperties,(SDL_GPUDevice *a),(a),return) SDL_DYNAPI_PROC(SDL_Renderer*,SDL_CreateGPURenderer,(SDL_Window *a,SDL_GPUShaderFormat b,SDL_GPUDevice **c),(a,b,c),return) SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamPlanarData,(SDL_AudioStream *a,const void * const*b,int c),(a,b,c),return) +SDL_DYNAPI_PROC(bool,SDL_SetAudioIterationCallbacks,(SDL_AudioDeviceID a,SDL_AudioIterationCallback b,SDL_AudioIterationCallback c,void *d),(a,b,c,d),return)