diff --git a/docs/README-migration.md b/docs/README-migration.md index 0a3c0fd5e..1e8265c52 100644 --- a/docs/README-migration.md +++ b/docs/README-migration.md @@ -53,11 +53,11 @@ The following structures have been renamed: ## SDL_audio.h -The audio subsystem in SDL3 is dramatically different than SDL2. There is no longer an audio callback; instead you bind SDL_AudioStreams to devices. +The audio subsystem in SDL3 is dramatically different than SDL2. The primary way to play audio is no longer an audio callback; instead you bind SDL_AudioStreams to devices. The SDL 1.2 audio compatibility API has also been removed, as it was a simplified version of the audio callback interface. -If your app depends on the callback method, you can use the single-header library at https://github.com/libsdl-org/SDL3_audio_callback (to be written!) to simulate it on top of SDL3's new API. +If your app depends on the callback method, there is a similar approach you can take. But first, this is the new approach: In SDL2, you might have done something like this to play audio: @@ -84,7 +84,6 @@ in SDL3: ```c /* ...somewhere near startup... */ - my_desired_audio_format.callback = MyAudioCallback; /* etc */ SDL_AudioDeviceID my_audio_device = SDL_OpenAudioDevice(0, SDL_AUDIO_S16, 2, 44100); SDL_AudioSteam *stream = SDL_CreateAndBindAudioStream(my_audio_device, SDL_AUDIO_S16, 2, 44100); @@ -93,6 +92,23 @@ in SDL3: SDL_PutAudioStreamData(stream, buf, buflen); ``` +If you absolutely require the callback method, SDL_AudioStreams can use a callback whenever more data is to be read from them, which can be used to simulate SDL2 semantics: + +```c + void SDLCALL MyAudioCallback(SDL_AudioStream *stream, int len, void *userdata) + { + /* calculate a little more audio here, maybe using `userdata`, write it to `stream` */ + SDL_PutAudioStreamData(stream, newdata, len); + } + + /* ...somewhere near startup... */ + SDL_AudioDeviceID my_audio_device = SDL_OpenAudioDevice(0, SDL_AUDIO_S16, 2, 44100); + SDL_AudioSteam *stream = SDL_CreateAndBindAudioStream(my_audio_device, SDL_AUDIO_S16, 2, 44100); + SDL_SetAudioStreamGetCallback(stream, MyAudioCallback); + + /* MyAudioCallback will be called whenever the device requests more audio data. */ +``` + SDL_AudioInit() and SDL_AudioQuit() have been removed. Instead you can call SDL_InitSubSystem() and SDL_QuitSubSystem() with SDL_INIT_AUDIO, which will properly refcount the subsystems. You can choose a specific audio driver using SDL_AUDIO_DRIVER hint. The `SDL_AUDIO_ALLOW_*` symbols have been removed; now one may request the format they desire from the audio device, but ultimately SDL_AudioStream will manage the difference. One can use SDL_GetAudioDeviceFormat() to see what the final format is, if any "allowed" changes should be accomodated by the app. diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h index 43ecc1ea1..1e073cdb2 100644 --- a/include/SDL3/SDL_audio.h +++ b/include/SDL3/SDL_audio.h @@ -713,6 +713,171 @@ extern DECLSPEC int SDLCALL SDL_FlushAudioStream(SDL_AudioStream *stream); */ extern DECLSPEC int SDLCALL SDL_ClearAudioStream(SDL_AudioStream *stream); +/** + * Lock an audio stream for serialized access. + * + * Each SDL_AudioStream has an internal mutex it uses to + * protect its data structures from threading conflicts. This function + * allows an app to lock that mutex, which could be useful if + * registering callbacks on this stream. + * + * One does not need to lock a stream to use in it most cases, + * as the stream manages this lock internally. However, this lock + * is held during callbacks, which may run from arbitrary threads + * at any time, so if an app needs to protect shared data during + * those callbacks, locking the stream guarantees that the + * callback is not running while the lock is held. + * + * As this is just a wrapper over SDL_LockMutex for an internal + * lock, it has all the same attributes (recursive locks are + * allowed, etc). + * + * \param stream The audio stream to lock. + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \threadsafety It is safe to call this function from any thread. + * + * \sa SDL_UnlockAudioStream + * \sa SDL_SetAudioStreamPutCallback + * \sa SDL_SetAudioStreamGetCallback + */ +extern DECLSPEC int SDLCALL SDL_LockAudioStream(SDL_AudioStream *stream); + + +/** + * Unlock an audio stream for serialized access. + * + * This unlocks an audio stream after a call to SDL_LockAudioStream. + * + * \param stream The audio stream to unlock. + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \threadsafety You should only call this from the same thread that + * previously called SDL_LockAudioStream. + * + * \sa SDL_LockAudioStream + * \sa SDL_SetAudioStreamPutCallback + * \sa SDL_SetAudioStreamGetCallback + */ +extern DECLSPEC int SDLCALL SDL_UnlockAudioStream(SDL_AudioStream *stream); + +/** + * A callback that fires when data passes through an SDL_AudioStream. + * + * Apps can (optionally) register a callback with an audio stream that + * is called when data is added with SDL_PutAudioStreamData, or requested + * with SDL_GetAudioStreamData. These callbacks may run from any + * thread, so if you need to protect shared data, you should use + * SDL_LockAudioStream to serialize access; this lock will be held by + * before your callback is called, so your callback does not need to + * manage the lock explicitly. + * + * \param stream The SDL audio stream associated with this callback. + * \param approx_request The _approximate_ amout of data, in bytes, that is requested. + * This might be slightly overestimated due to buffering or + * resampling, and may change from call to call anyhow. + * \param userdata An opaque pointer provided by the app for their personal use. + */ +typedef void (SDLCALL *SDL_AudioStreamRequestCallback)(SDL_AudioStream *stream, int approx_request, void *userdata); + +/** + * Set a callback that runs when data is requested from an audio stream. + * + * This callback is called _before_ data is obtained from the stream, + * giving the callback the chance to add more on-demand. + * + * The callback can (optionally) call SDL_PutAudioStreamData() to add + * more audio to the stream during this call; if needed, the request + * that triggered this callback will obtain the new data immediately. + * + * The callback's `approx_request` argument is roughly how many bytes + * of _unconverted_ data (in the stream's input format) is needed by + * the caller, although this may overestimate a little for safety. + * This takes into account how much is already in the stream and only + * asks for any extra necessary to resolve the request, which means + * the callback may be asked for zero bytes, and a different amount + * on each call. + * + * The callback is not required to supply exact amounts; it is allowed + * to supply too much or too little or none at all. The caller will + * get what's available, up to the amount they requested, regardless + * of this callback's outcome. + * + * Clearing or flushing an audio stream does not call this callback. + * + * This function obtains the stream's lock, which means any existing + * callback (get or put) in progress will finish running before setting + * the new callback. + * + * Setting a NULL function turns off the callback. + * + * \param stream the audio stream to set the new callback on. + * \param callback the new callback function to call when data is added to the stream. + * \param userdata an opaque pointer provided to the callback for its own personal use. + * \returns 0 on success, -1 on error. This only fails if `stream` is NULL. + * + * \since This function is available since SDL 3.0.0. + * + * \threadsafety It is safe to call this function from any thread. + * + * \sa SDL_SetAudioStreamPutCallback + */ +extern DECLSPEC int SDLCALL SDL_SetAudioStreamGetCallback(SDL_AudioStream *stream, SDL_AudioStreamRequestCallback callback, void *userdata); + +/** + * Set a callback that runs when data is added to an audio stream. + * + * This callback is called _after_ the data is added to the stream, + * giving the callback the chance to obtain it immediately. + * + * The callback can (optionally) call SDL_GetAudioStreamData() to + * obtain audio from the stream during this call. + * + * The callback's `approx_request` argument is how many bytes + * of _converted_ data (in the stream's output format) was provided + * by the caller, although this may underestimate a little for safety. + * This value might be less than what is currently available in the + * stream, if data was already there, and might be less than the + * caller provided if the stream needs to keep a buffer to aid in + * resampling. Which means the callback may be provided with zero + * bytes, and a different amount on each call. + * + * The callback may call SDL_GetAudioStreamAvailable to see the + * total amount currently available to read from the stream, instead + * of the total provided by the current call. + * + * The callback is not required to obtain all data. It is allowed + * to read less or none at all. Anything not read now simply remains + * in the stream for later access. + * + * Clearing or flushing an audio stream does not call this callback. + * + * This function obtains the stream's lock, which means any existing + * callback (get or put) in progress will finish running before setting + * the new callback. + * + * Setting a NULL function turns off the callback. + * + * \param stream the audio stream to set the new callback on. + * \param callback the new callback function to call when data is added to the stream. + * \param userdata an opaque pointer provided to the callback for its own personal use. + * \returns 0 on success, -1 on error. This only fails if `stream` is NULL. + * + * \since This function is available since SDL 3.0.0. + * + * \threadsafety It is safe to call this function from any thread. + * + * \sa SDL_SetAudioStreamGetCallback + */ +extern DECLSPEC int SDLCALL SDL_SetAudioStreamPutCallback(SDL_AudioStream *stream, SDL_AudioStreamRequestCallback callback, void *userdata); + + /** * Free an audio stream * diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c index f5d7b9c31..eec84107a 100644 --- a/src/audio/SDL_audiocvt.c +++ b/src/audio/SDL_audiocvt.c @@ -629,6 +629,40 @@ SDL_AudioStream *SDL_CreateAudioStream(SDL_AudioFormat src_format, return retval; } +int SDL_SetAudioStreamGetCallback(SDL_AudioStream *stream, SDL_AudioStreamRequestCallback callback, void *userdata) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + SDL_LockMutex(stream->lock); + stream->get_callback = callback; + stream->get_callback_userdata = userdata; + SDL_UnlockMutex(stream->lock); + return 0; +} + +int SDL_SetAudioStreamPutCallback(SDL_AudioStream *stream, SDL_AudioStreamRequestCallback callback, void *userdata) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + SDL_LockMutex(stream->lock); + stream->put_callback = callback; + stream->put_callback_userdata = userdata; + SDL_UnlockMutex(stream->lock); + return 0; +} + +int SDL_LockAudioStream(SDL_AudioStream *stream) +{ + return stream ? SDL_LockMutex(stream->lock) : SDL_InvalidParamError("stream"); +} + +int SDL_UnlockAudioStream(SDL_AudioStream *stream) +{ + return stream ? SDL_UnlockMutex(stream->lock) : SDL_InvalidParamError("stream"); +} + int SDL_GetAudioStreamFormat(SDL_AudioStream *stream, SDL_AudioFormat *src_format, int *src_channels, int *src_rate, SDL_AudioFormat *dst_format, int *dst_channels, int *dst_rate) { if (!stream) { @@ -696,6 +730,8 @@ int SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len) SDL_LockMutex(stream->lock); + const int prev_available = stream->put_callback ? SDL_GetAudioStreamAvailable(stream) : 0; + if ((len % stream->src_sample_frame_size) != 0) { SDL_UnlockMutex(stream->lock); return SDL_SetError("Can't add partial sample frames"); @@ -704,6 +740,11 @@ int SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len) /* just queue the data, we convert/resample when dequeueing. */ retval = SDL_WriteToDataQueue(stream->queue, buf, len); stream->flushed = SDL_FALSE; + + if (stream->put_callback) { + stream->put_callback(stream, SDL_GetAudioStreamAvailable(stream) - prev_available, stream->put_callback_userdata); + } + SDL_UnlockMutex(stream->lock); return retval; @@ -978,6 +1019,23 @@ int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len) len -= len % stream->dst_sample_frame_size; /* chop off any fractional sample frame. */ + // give the callback a chance to fill in more stream data if it wants. + if (stream->get_callback) { + int approx_request = len / stream->dst_sample_frame_size; /* start with sample frames desired */ + if (stream->src_rate != stream->dst_rate) { + /* calculate difference in dataset size after resampling. Use a Uint64 so the multiplication doesn't overflow. */ + approx_request = (size_t) ((((Uint64) approx_request) * stream->src_rate) / stream->dst_rate); + if (!stream->flushed) { /* do we need to fill the future buffer to accomodate this, too? */ + approx_request += stream->future_buffer_filled_frames - stream->resampler_padding_frames; + } + } + + approx_request *= stream->src_sample_frame_size; // convert sample frames to bytes. + const int already_have = SDL_GetAudioStreamAvailable(stream); + approx_request -= SDL_min(approx_request, already_have); // we definitely have this much output already packed in. + stream->get_callback(stream, approx_request, stream->get_callback_userdata); + } + /* we convert in chunks, so we don't end up allocating a massive work buffer, etc. */ while (len > 0) { /* didn't ask for a whole sample frame, nothing to do */ const int chunk_size = 1024 * 1024; /* !!! FIXME: a megabyte might be overly-aggressive. */ diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index 7fa95c812..95350d8f3 100644 --- a/src/audio/SDL_sysaudio.h +++ b/src/audio/SDL_sysaudio.h @@ -137,6 +137,11 @@ struct SDL_AudioStream SDL_DataQueue *queue; SDL_Mutex *lock; /* this is just a copy of `queue`'s mutex. We share a lock. */ + SDL_AudioStreamRequestCallback get_callback; + void *get_callback_userdata; + SDL_AudioStreamRequestCallback put_callback; + void *put_callback_userdata; + Uint8 *work_buffer; /* used for scratch space during data conversion/resampling. */ Uint8 *history_buffer; /* history for left padding and future sample rate changes. */ Uint8 *future_buffer; /* stuff that left the queue for the right padding and will be next read's data. */ diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 7fca72a59..4cd53c2d6 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -879,6 +879,10 @@ SDL3_0.0.0 { SDL_MixAudioFormat; SDL_ConvertAudioSamples; SDL_GetSilenceValueForFormat; + SDL_LockAudioStream; + SDL_UnlockAudioStream; + SDL_SetAudioStreamGetCallback; + SDL_SetAudioStreamPutCallback; # 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 7bb20c97c..6d623744b 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -905,3 +905,7 @@ #define SDL_MixAudioFormat SDL_MixAudioFormat_REAL #define SDL_ConvertAudioSamples SDL_ConvertAudioSamples_REAL #define SDL_GetSilenceValueForFormat SDL_GetSilenceValueForFormat_REAL +#define SDL_LockAudioStream SDL_LockAudioStream_REAL +#define SDL_UnlockAudioStream SDL_UnlockAudioStream_REAL +#define SDL_SetAudioStreamGetCallback SDL_SetAudioStreamGetCallback_REAL +#define SDL_SetAudioStreamPutCallback SDL_SetAudioStreamPutCallback_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index a93431596..359d4552a 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -950,3 +950,7 @@ SDL_DYNAPI_PROC(int,SDL_LoadWAV_RW,(SDL_RWops *a, int b, SDL_AudioFormat *c, int SDL_DYNAPI_PROC(int,SDL_MixAudioFormat,(Uint8 *a, const Uint8 *b, SDL_AudioFormat c, Uint32 d, int e),(a,b,c,d,e),return) SDL_DYNAPI_PROC(int,SDL_ConvertAudioSamples,(SDL_AudioFormat a, int b, int c, const Uint8 *d, int e, SDL_AudioFormat f, int g, int h, Uint8 **i, int *j),(a,b,c,d,e,f,g,h,i,j),return) SDL_DYNAPI_PROC(int,SDL_GetSilenceValueForFormat,(SDL_AudioFormat a),(a),return) +SDL_DYNAPI_PROC(int,SDL_LockAudioStream,(SDL_AudioStream *a),(a),return) +SDL_DYNAPI_PROC(int,SDL_UnlockAudioStream,(SDL_AudioStream *a),(a),return) +SDL_DYNAPI_PROC(int,SDL_SetAudioStreamGetCallback,(SDL_AudioStream *a, SDL_AudioStreamRequestCallback b, void *c),(a,b,c),return) +SDL_DYNAPI_PROC(int,SDL_SetAudioStreamPutCallback,(SDL_AudioStream *a, SDL_AudioStreamRequestCallback b, void *c),(a,b,c),return) diff --git a/test/loopwave.c b/test/loopwave.c index 871fc8eec..127b3f4ce 100644 --- a/test/loopwave.c +++ b/test/loopwave.c @@ -33,11 +33,36 @@ static struct int freq; Uint8 *sound; /* Pointer to wave data */ Uint32 soundlen; /* Length of wave data */ + Uint32 soundpos; } wave; static SDL_AudioDeviceID device; static SDL_AudioStream *stream; +static void SDLCALL +fillerup(SDL_AudioStream *stream, int len, void *unused) +{ + Uint8 *waveptr; + int waveleft; + + /*SDL_Log("CALLBACK WANTS %d MORE BYTES!", len);*/ + + /* Set up the pointers */ + waveptr = wave.sound + wave.soundpos; + waveleft = wave.soundlen - wave.soundpos; + + /* Go! */ + while (waveleft <= len) { + SDL_PutAudioStreamData(stream, waveptr, waveleft); + len -= waveleft; + waveptr = wave.sound; + waveleft = wave.soundlen; + wave.soundpos = 0; + } + SDL_PutAudioStreamData(stream, waveptr, len); + wave.soundpos += len; +} + /* Call this instead of exit(), so we can clean up SDL: atexit() is evil. */ static void quit(int rc) @@ -78,6 +103,8 @@ open_audio(void) SDL_free(wave.sound); quit(2); } + + SDL_SetAudioStreamGetCallback(stream, fillerup, NULL); } #ifndef __EMSCRIPTEN__ @@ -91,12 +118,6 @@ static void reopen_audio(void) static int done = 0; -static void fillerup(void) -{ - if (SDL_GetAudioStreamAvailable(stream) < (wave.soundlen / 2)) { - SDL_PutAudioStreamData(stream, wave.sound, wave.soundlen); - } -} #ifdef __EMSCRIPTEN__ @@ -192,7 +213,6 @@ int main(int argc, char *argv[]) } } - fillerup(); SDL_Delay(100); } #endif