diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h index 7f1fd42cd8..5490be5c27 100644 --- a/include/SDL3/SDL_audio.h +++ b/include/SDL3/SDL_audio.h @@ -141,6 +141,14 @@ typedef Uint16 SDL_AudioFormat; /* @} *//* Audio flags */ +/** + * SDL Audio Device instance IDs. + */ +typedef Uint32 SDL_AudioDeviceID; + +#define SDL_AUDIO_DEVICE_DEFAULT_OUTPUT ((SDL_AudioDeviceID) 0xFFFFFFFF) +#define SDL_AUDIO_DEVICE_DEFAULT_CAPTURE ((SDL_AudioDeviceID) 0xFFFFFFFE) + typedef struct SDL_AudioSpec { SDL_AudioFormat format; /**< Audio data format */ @@ -237,11 +245,6 @@ extern DECLSPEC const char *SDLCALL SDL_GetAudioDriver(int index); */ extern DECLSPEC const char *SDLCALL SDL_GetCurrentAudioDriver(void); -/** - * SDL Audio Device instance IDs. - */ -typedef Uint32 SDL_AudioDeviceID; - /** * Get a list of currently-connected audio output devices. @@ -326,8 +329,6 @@ extern DECLSPEC int SDLCALL SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SD /** * Open a specific audio device. * - * Passing in a `devid` name of zero requests the most reasonable default. - * * You can open both output and capture devices through this function. * Output devices will take data from bound audio streams, mix it, and * send it to the hardware. Capture devices will feed any bound audio @@ -336,7 +337,22 @@ extern DECLSPEC int SDLCALL SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SD * An opened audio device starts out with no audio streams bound. To * start audio playing, bind a stream and supply audio data to it. Unlike * SDL2, there is no audio callback; you only bind audio streams and - * make sure they have data flowing into them. + * make sure they have data flowing into them (although, as an optional + * feature, each audio stream may have its own callback, which can be + * used to simulate SDL2's semantics). + * + * If you don't care about opening a specific device, pass a `devid` + * of either `SDL_AUDIO_DEVICE_DEFAULT_OUTPUT` or + * `SDL_AUDIO_DEVICE_DEFAULT_CAPTURE`. In this case, SDL will try to + * pick the most reasonable default, and may also switch between + * physical devices seamlessly later, if the most reasonable default + * changes during the lifetime of this opened device (user changed + * the default in the OS's system preferences, the default got + * unplugged so the system jumped to a new default, the user plugged + * in headphones on a mobile device, etc). Unless you have a good + * reason to choose a specific device, this is probably what you want. + * Requesting the default will also allow the user to specify + * preferences with hints/environment variables. * * You may request a specific format for the audio device, but there is * no promise the device will honor that request for several reasons. As @@ -346,28 +362,30 @@ extern DECLSPEC int SDLCALL SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SD * tell you the preferred format for the device before opening and the * actual format the device is using after opening. * - * It's legal to open the same device ID more than once; in the end, you must - * close it the same number of times. This allows libraries to open a device - * separate from the main app and bind its own streams without conflicting. + * It's legal to open the same device ID more than once; each successful + * open will generate a new logical SDL_AudioDeviceID that is managed + * separately from others on the same physical device. This allows + * libraries to open a device separately from the main app and bind its own + * streams without conflicting. * - * This function returns the opened device ID on success, so that if you - * open a device of 0, you'll have a real ID to bind streams to, but this - * does not generate new instance IDs. Unlike SDL2, these IDs are assigned - * to each unique device on the system, open or not, so if you request a - * specific device, you'll get that same device ID back. + * This function returns the opened device ID on success. This is a new, + * unique SDL_AudioDeviceID that represents a logical device. * * Some backends might offer arbitrary devices (for example, a networked * audio protocol that can connect to an arbitrary server). For these, as - * a change from SDL2, you should open a device ID of zero and use an SDL + * a change from SDL2, you should open a default device ID and use an SDL * hint to specify the target if you care, or otherwise let the backend * figure out a reasonable default. Most backends don't offer anything like * this, and often this would be an end user setting an environment * variable for their custom need, and not something an application should * specifically manage. * - * \param devid the device instance id to open. 0 requests the most - * reasonable default device. - * \param spec the requested device configuration + * When done with an audio device, possibly at the end of the app's life, + * one should call SDL_CloseAudioDevice() on the returned device id. + * + * \param devid the device instance id to open, or SDL_AUDIO_DEVICE_DEFAULT_OUTPUT or + * SDL_AUDIO_DEVICE_DEFAULT_CAPTURE for the most reasonable default device. + * \param spec the requested device configuration. Can be NULL to use reasonable defaults. * \returns The device ID on success, 0 on error; call SDL_GetError() for more information. * * \since This function is available since SDL 3.0.0. @@ -384,15 +402,13 @@ extern DECLSPEC SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(SDL_AudioDeviceID * Close a previously-opened audio device. * * The application should close open audio devices once they are no longer - * needed. Audio devices can be opened multiple times; when they are closed - * an equal number of times, its resources are freed, any bound streams are - * unbound, and any audio will stop playing. + * needed. * * This function may block briefly while pending audio data is played by the * hardware, so that applications don't drop the last buffer of data they - * supplied. + * supplied if terminating immediately afterwards. * - * \param devid an audio device previously opened with SDL_OpenAudioDevice() + * \param devid an audio device id previously returned by SDL_OpenAudioDevice() * * \since This function is available since SDL 3.0.0. * diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index 060980ee77..96fe7fa7de 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -121,37 +121,55 @@ const char *SDL_GetCurrentAudioDriver(void) /* device management and hotplug... */ -/* SDL_AudioDevice, in SDL3, represents a piece of hardware, whether it is in use or not, so these objects exist as long as +/* SDL_AudioDevice, in SDL3, represents a piece of physical hardware, whether it is in use or not, so these objects exist as long as the system-level device is available. - Devices get destroyed for three reasons: + Physical devices get destroyed for three reasons: - They were lost to the system (a USB cable is kicked out, etc). - They failed for some other unlikely reason at the API level (which is _also_ probably a USB cable being kicked out). - We are shutting down, so all allocated resources are being freed. They are _not_ destroyed because we are done using them (when we "close" a playing device). */ -static void CloseAudioDevice(SDL_AudioDevice *device); +static void ClosePhysicalAudioDevice(SDL_AudioDevice *device); -/* this must not be called while `device` is still in a device list, or while a device's audio thread is still running (except if the thread calls this while shutting down). */ -static void DestroyAudioDevice(SDL_AudioDevice *device) +// the loop in assign_audio_device_instance_id relies on this being true. +SDL_COMPILE_TIME_ASSERT(check_lowest_audio_default_value, SDL_AUDIO_DEVICE_DEFAULT_CAPTURE < SDL_AUDIO_DEVICE_DEFAULT_OUTPUT); + +static SDL_AudioDeviceID assign_audio_device_instance_id(SDL_bool iscapture, SDL_bool islogical) { - SDL_AudioStream *stream; + /* Assign an instance id! Start at 2, in case there are things from the SDL2 era that still think 1 is a special value. + There's no reasonable scenario where this rolls over, but just in case, we wrap it in a loop. + Also, make sure we don't assign SDL_AUDIO_DEVICE_DEFAULT_OUTPUT, etc. */ + + // The bottom two bits of the instance id tells you if it's an output device (1<<0), and if it's a physical device (1<<1). Make sure these are right. + const SDL_AudioDeviceID required_mask = (iscapture ? 0 : (1<<0)) | (islogical ? 0 : (1<<1)); + + SDL_AudioDeviceID instance_id; + do { + instance_id = (SDL_AudioDeviceID) (SDL_AtomicIncRef(¤t_audio.last_device_instance_id) + 1); + } while ( (instance_id < 2) || (instance_id >= SDL_AUDIO_DEVICE_DEFAULT_CAPTURE) || ((instance_id & 0x3) != required_mask) ); + return instance_id; +} + +// this assumes you hold the _physical_ device lock for this logical device! This will not unlock the lock or close the physical device! +static void DestroyLogicalAudioDevice(SDL_LogicalAudioDevice *logdev) +{ + // remove ourselves from the physical device's list of logical devices. + if (logdev->next) { + logdev->next->prev = logdev->prev; + } + if (logdev->prev) { + logdev->prev->next = logdev->next; + } + if (logdev->physical_device->logical_devices == logdev) { + logdev->physical_device->logical_devices = logdev->next; + } + + // unbind any still-bound streams... SDL_AudioStream *next; - - if (!device) { - return; - } - - if (SDL_AtomicGet(&device->refcount) > 0) { - SDL_AtomicSet(&device->refcount, 0); /* it's going down NOW. */ - CloseAudioDevice(device); - } - - /* unbind any still-bound streams... */ - SDL_LockMutex(device->lock); - for (stream = device->bound_streams; stream != NULL; stream = next) { + for (SDL_AudioStream *stream = logdev->bound_streams; stream != NULL; stream = next) { SDL_LockMutex(stream->lock); next = stream->next_binding; stream->next_binding = NULL; @@ -159,8 +177,27 @@ static void DestroyAudioDevice(SDL_AudioDevice *device) stream->bound_device = NULL; SDL_UnlockMutex(stream->lock); } + + SDL_free(logdev); +} + +// this must not be called while `device` is still in a device list, or while a device's audio thread is still running (except if the thread calls this while shutting down). */ +static void DestroyPhysicalAudioDevice(SDL_AudioDevice *device) +{ + if (!device) { + return; + } + + // Destroy any logical devices that still exist... + SDL_LockMutex(device->lock); + while (device->logical_devices != NULL) { + DestroyLogicalAudioDevice(device->logical_devices); + } SDL_UnlockMutex(device->lock); + // it's safe to not hold the lock for this (we can't anyhow, or the audio thread won't quit), because we shouldn't be in the device list at this point. + ClosePhysicalAudioDevice(device); + current_audio.impl.FreeDeviceHandle(device->handle); SDL_DestroyMutex(device->lock); @@ -169,7 +206,7 @@ static void DestroyAudioDevice(SDL_AudioDevice *device) SDL_free(device); } -static SDL_AudioDevice *CreateAudioDevice(const char *name, SDL_bool iscapture, const SDL_AudioSpec *spec, void *handle, SDL_AudioDevice **devices, SDL_AtomicInt *device_count) +static SDL_AudioDevice *CreatePhysicalAudioDevice(const char *name, SDL_bool iscapture, const SDL_AudioSpec *spec, void *handle, SDL_AudioDevice **devices, SDL_AtomicInt *device_count) { SDL_AudioDevice *device; @@ -208,12 +245,7 @@ static SDL_AudioDevice *CreateAudioDevice(const char *name, SDL_bool iscapture, device->handle = handle; device->prev = NULL; - /* Assign an instance id! Start at 2, in case there are things from the SDL2 era that still think 1 is a special value. */ - /* There's no reasonable scenario where this rolls over, but just in case, we wrap it in a loop. */ - /* Also, make sure capture instance IDs are even, and output IDs are odd, so we know what kind of device it is from just the ID. */ - do { - device->instance_id = (SDL_AudioDeviceID) (SDL_AtomicIncRef(¤t_audio.last_device_instance_id) + 1); - } while ( ((iscapture && (device->instance_id & 1)) || (!iscapture && ((device->instance_id & 1) == 0))) || (device->instance_id < 2) ); + device->instance_id = assign_audio_device_instance_id(iscapture, /*islogical=*/SDL_FALSE); SDL_LockRWLockForWriting(current_audio.device_list_lock); device->next = *devices; @@ -227,12 +259,12 @@ static SDL_AudioDevice *CreateAudioDevice(const char *name, SDL_bool iscapture, static SDL_AudioDevice *CreateAudioCaptureDevice(const char *name, const SDL_AudioSpec *spec, void *handle) { SDL_assert(current_audio.impl.HasCaptureSupport); - return CreateAudioDevice(name, SDL_TRUE, spec, handle, ¤t_audio.capture_devices, ¤t_audio.capture_device_count); + return CreatePhysicalAudioDevice(name, SDL_TRUE, spec, handle, ¤t_audio.capture_devices, ¤t_audio.capture_device_count); } static SDL_AudioDevice *CreateAudioOutputDevice(const char *name, const SDL_AudioSpec *spec, void *handle) { - return CreateAudioDevice(name, SDL_FALSE, spec, handle, ¤t_audio.output_devices, ¤t_audio.output_device_count); + return CreatePhysicalAudioDevice(name, SDL_FALSE, spec, handle, ¤t_audio.output_devices, ¤t_audio.output_device_count); } /* The audio backends call this when a new device is plugged in. */ @@ -317,7 +349,7 @@ void SDL_AudioDeviceDisconnected(SDL_AudioDevice *device) } if (should_destroy) { - DestroyAudioDevice(device); + DestroyPhysicalAudioDevice(device); } } @@ -512,7 +544,6 @@ int SDL_InitAudio(const char *driver_name) void SDL_QuitAudio(void) { SDL_AudioDevice *devices = NULL; - SDL_AudioDevice *i; if (!current_audio.name) { /* not initialized?! */ return; @@ -521,7 +552,7 @@ void SDL_QuitAudio(void) /* merge device lists so we don't have to duplicate work below. */ SDL_LockRWLockForWriting(current_audio.device_list_lock); SDL_AtomicSet(¤t_audio.shutting_down, 1); - for (i = current_audio.output_devices; i != NULL; i = i->next) { + for (SDL_AudioDevice *i = current_audio.output_devices; i != NULL; i = i->next) { devices = i; } if (!devices) { @@ -536,12 +567,12 @@ void SDL_QuitAudio(void) SDL_UnlockRWLock(current_audio.device_list_lock); /* mark all devices for shutdown so all threads can begin to terminate. */ - for (i = devices; i != NULL; i = i->next) { + for (SDL_AudioDevice *i = devices; i != NULL; i = i->next) { SDL_AtomicSet(&i->shutdown, 1); } /* now wait on any audio threads. */ - for (i = devices; i != NULL; i = i->next) { + for (SDL_AudioDevice *i = devices; i != NULL; i = i->next) { if (i->thread) { SDL_assert(!SDL_AtomicGet(&i->condemned)); /* these shouldn't have been in the device list still, and thread should have detached. */ SDL_WaitThread(i->thread, NULL); @@ -550,9 +581,9 @@ void SDL_QuitAudio(void) } while (devices) { - i = devices->next; - DestroyAudioDevice(devices); - devices = i; + SDL_AudioDevice *next = devices->next; + DestroyPhysicalAudioDevice(devices); + devices = next; } /* Free the driver data */ @@ -586,7 +617,6 @@ void SDL_OutputAudioThreadSetup(SDL_AudioDevice *device) SDL_bool SDL_OutputAudioThreadIterate(SDL_AudioDevice *device) { SDL_bool retval = SDL_TRUE; - SDL_AudioStream *stream; int buffer_size = device->buffer_size; Uint8 *mix_buffer; @@ -605,22 +635,26 @@ SDL_bool SDL_OutputAudioThreadIterate(SDL_AudioDevice *device) } else { SDL_assert(buffer_size <= device->buffer_size); /* you can ask for less, but not more. */ SDL_memset(mix_buffer, device->silence_value, buffer_size); /* start with silence. */ - for (stream = device->bound_streams; stream != NULL; stream = stream->next_binding) { - /* this will hold a lock on `stream` while getting. We don't explicitly lock the streams for iterating here because the binding linked list can only change while the device lock is held. */ - /* (we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind the same stream to different devices at the same time, though.) */ - const int br = SDL_GetAudioStreamData(stream, device->work_buffer, buffer_size); - if (br < 0) { - /* oh crud, we probably ran out of memory. This is possibly an overreaction to kill the audio device, but it's likely the whole thing is going down in a moment anyhow. */ - retval = SDL_FALSE; - break; - } else if (br > 0) { /* it's okay if we get less than requested, we mix what we have. */ - if (SDL_MixAudioFormat(mix_buffer, device->work_buffer, device->spec.format, br, SDL_MIX_MAXVOLUME) < 0) { /* !!! FIXME: allow streams to specify gain? */ - SDL_assert(!"We probably ended up with some totally unexpected audio format here"); - retval = SDL_FALSE; /* uh...? */ + for (SDL_LogicalAudioDevice *logdev = device->logical_devices; logdev != NULL; logdev = logdev->next) { + for (SDL_AudioStream *stream = logdev->bound_streams; stream != NULL; stream = stream->next_binding) { + /* this will hold a lock on `stream` while getting. We don't explicitly lock the streams for iterating here because the binding linked list can only change while the device lock is held. */ + /* (we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind the same stream to different devices at the same time, though.) */ + const int br = SDL_GetAudioStreamData(stream, device->work_buffer, buffer_size); + if (br < 0) { + /* oh crud, we probably ran out of memory. This is possibly an overreaction to kill the audio device, but it's likely the whole thing is going down in a moment anyhow. */ + retval = SDL_FALSE; break; + } else if (br > 0) { /* it's okay if we get less than requested, we mix what we have. */ + // !!! FIXME: this needs to mix to float32 or int32, so we don't clip. + if (SDL_MixAudioFormat(mix_buffer, device->work_buffer, device->spec.format, br, SDL_MIX_MAXVOLUME) < 0) { /* !!! FIXME: allow streams to specify gain? */ + SDL_assert(!"We probably ended up with some totally unexpected audio format here"); + retval = SDL_FALSE; /* uh...? */ + break; + } } } } + /* !!! FIXME: have PlayDevice return a value and do disconnects in here with it. */ current_audio.impl.PlayDevice(device, buffer_size); /* this SHOULD NOT BLOCK, as we are holding a lock right now. Block in WaitDevice! */ } @@ -644,7 +678,7 @@ void SDL_OutputAudioThreadShutdown(SDL_AudioDevice *device) if (SDL_AtomicGet(&device->condemned)) { SDL_DetachThread(device->thread); /* no one is waiting for us, just detach ourselves. */ device->thread = NULL; - DestroyAudioDevice(device); + DestroyPhysicalAudioDevice(device); } } @@ -692,21 +726,22 @@ SDL_bool SDL_CaptureAudioThreadIterate(SDL_AudioDevice *device) if (SDL_AtomicGet(&device->shutdown)) { retval = SDL_FALSE; /* we're done, shut it down. */ - } else if (device->bound_streams == NULL) { + } else if (device->logical_devices == NULL) { current_audio.impl.FlushCapture(device); /* nothing wants data, dump anything pending. */ } else { const int rc = current_audio.impl.CaptureFromDevice(device, device->work_buffer, device->buffer_size); if (rc < 0) { /* uhoh, device failed for some reason! */ retval = SDL_FALSE; } else if (rc > 0) { /* queue the new data to each bound stream. */ - SDL_AudioStream *stream; - for (stream = device->bound_streams; stream != NULL; stream = stream->next_binding) { - /* this will hold a lock on `stream` while putting. We don't explicitly lock the streams for iterating here because the binding linked list can only change while the device lock is held. */ - /* (we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind the same stream to different devices at the same time, though.) */ - if (SDL_PutAudioStreamData(stream, device->work_buffer, rc) < 0) { - /* oh crud, we probably ran out of memory. This is possibly an overreaction to kill the audio device, but it's likely the whole thing is going down in a moment anyhow. */ - retval = SDL_FALSE; - break; + for (SDL_LogicalAudioDevice *logdev = device->logical_devices; logdev != NULL; logdev = logdev->next) { + for (SDL_AudioStream *stream = logdev->bound_streams; stream != NULL; stream = stream->next_binding) { + /* this will hold a lock on `stream` while putting. We don't explicitly lock the streams for iterating here because the binding linked list can only change while the device lock is held. */ + /* (we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind the same stream to different devices at the same time, though.) */ + if (SDL_PutAudioStreamData(stream, device->work_buffer, rc) < 0) { + /* oh crud, we probably ran out of memory. This is possibly an overreaction to kill the audio device, but it's likely the whole thing is going down in a moment anyhow. */ + retval = SDL_FALSE; + break; + } } } } @@ -727,7 +762,7 @@ void SDL_CaptureAudioThreadShutdown(SDL_AudioDevice *device) current_audio.impl.FlushCapture(device); current_audio.impl.ThreadDeinit(device); if (SDL_AtomicGet(&device->condemned)) { - DestroyAudioDevice(device); + DestroyPhysicalAudioDevice(device); } } @@ -791,19 +826,69 @@ SDL_AudioDeviceID *SDL_GetAudioCaptureDevices(int *count) return GetAudioDevices(count, ¤t_audio.capture_devices, ¤t_audio.capture_device_count); } - -/* this finds the device object associated with `devid` and locks it for use. */ -static SDL_AudioDevice *ObtainAudioDevice(SDL_AudioDeviceID devid) +// If found, this locks _the physical device_ this logical device is associated with, before returning. +static SDL_LogicalAudioDevice *ObtainLogicalAudioDevice(SDL_AudioDeviceID devid) { - /* capture instance ids are even and output devices are odd, so we know which list to iterate from the devid. */ - const SDL_bool iscapture = (devid & 1) ? SDL_FALSE : SDL_TRUE; - SDL_AudioDevice *dev = NULL; + if (!SDL_GetCurrentAudioDriver()) { + SDL_SetError("Audio subsystem is not initialized"); + return NULL; + } + + SDL_LogicalAudioDevice *logdev = NULL; + + const SDL_bool islogical = (devid & (1<<1)) ? SDL_FALSE : SDL_TRUE; + if (islogical) { // don't bother looking if it's not a logical device id value. + const SDL_bool iscapture = (devid & (1<<0)) ? SDL_FALSE : SDL_TRUE; + + SDL_LockRWLockForReading(current_audio.device_list_lock); + + for (SDL_AudioDevice *device = iscapture ? current_audio.capture_devices : current_audio.output_devices; device != NULL; device = device->next) { + SDL_LockMutex(device->lock); // caller must unlock if we choose a logical device from this guy. + SDL_assert(!SDL_AtomicGet(&device->condemned)); // shouldn't be in the list if pending deletion. + for (logdev = device->logical_devices; logdev != NULL; logdev = logdev->next) { + if (logdev->instance_id == devid) { + break; // found it! + } + } + if (logdev != NULL) { + break; + } + SDL_UnlockMutex(device->lock); // give up this lock and try the next physical device. + } + + SDL_UnlockRWLock(current_audio.device_list_lock); + } + + if (!logdev) { + SDL_SetError("Invalid audio device instance ID"); + } + + return logdev; +} + +/* this finds the physical device associated with `devid` and locks it for use. + Note that a logical device instance id will return its associated physical device! */ +static SDL_AudioDevice *ObtainPhysicalAudioDevice(SDL_AudioDeviceID devid) +{ + // bit #1 of devid is set for physical devices and unset for logical. + const SDL_bool islogical = (devid & (1<<1)) ? SDL_FALSE : SDL_TRUE; + if (islogical) { + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid); + if (logdev) { + return logdev->physical_device; + } + return NULL; + } if (!SDL_GetCurrentAudioDriver()) { SDL_SetError("Audio subsystem is not initialized"); return NULL; } + // bit #0 of devid is set for output devices and unset for capture. + const SDL_bool iscapture = (devid & (1<<0)) ? SDL_FALSE : SDL_TRUE; + SDL_AudioDevice *dev = NULL; + SDL_LockRWLockForReading(current_audio.device_list_lock); for (dev = iscapture ? current_audio.capture_devices : current_audio.output_devices; dev != NULL; dev = dev->next) { @@ -823,7 +908,7 @@ static SDL_AudioDevice *ObtainAudioDevice(SDL_AudioDeviceID devid) return dev; } -SDL_AudioDevice *SDL_ObtainAudioDeviceByHandle(void *handle) +SDL_AudioDevice *SDL_ObtainPhysicalAudioDeviceByHandle(void *handle) { SDL_AudioDevice *dev = NULL; @@ -863,7 +948,7 @@ SDL_AudioDevice *SDL_ObtainAudioDeviceByHandle(void *handle) char *SDL_GetAudioDeviceName(SDL_AudioDeviceID devid) { char *retval; - SDL_AudioDevice *device = ObtainAudioDevice(devid); + SDL_AudioDevice *device = ObtainPhysicalAudioDevice(devid); if (!device) { return NULL; } @@ -884,7 +969,7 @@ int SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SDL_AudioSpec *spec) return SDL_InvalidParamError("spec"); } - SDL_AudioDevice *device = ObtainAudioDevice(devid); + SDL_AudioDevice *device = ObtainPhysicalAudioDevice(devid); if (!device) { return -1; } @@ -896,7 +981,7 @@ int SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SDL_AudioSpec *spec) } /* this expects the device lock to be held. */ -static void CloseAudioDevice(SDL_AudioDevice *device) +static void ClosePhysicalAudioDevice(SDL_AudioDevice *device) { if (device->thread != NULL) { SDL_AtomicSet(&device->shutdown, 1); @@ -905,7 +990,10 @@ static void CloseAudioDevice(SDL_AudioDevice *device) SDL_AtomicSet(&device->shutdown, 0); } - current_audio.impl.CloseDevice(device); + if (device->is_opened) { + current_audio.impl.CloseDevice(device); + device->is_opened = SDL_FALSE; + } if (device->work_buffer) { SDL_aligned_free(device->work_buffer); @@ -919,16 +1007,17 @@ static void CloseAudioDevice(SDL_AudioDevice *device) void SDL_CloseAudioDevice(SDL_AudioDeviceID devid) { - SDL_AudioDevice *device = ObtainAudioDevice(devid); - if (device) { /* if NULL, maybe it was already lost? */ - if (SDL_AtomicDecRef(&device->refcount)) { + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid); + if (logdev) { /* if NULL, maybe it was already lost? */ + SDL_AudioDevice *device = logdev->physical_device; + DestroyLogicalAudioDevice(logdev); + + if (device->logical_devices == NULL) { // no more logical devices? Close the physical device, too. + // !!! FIXME: we _need_ to release this lock, but doing so can cause a race condition if someone opens a device while we're closing it. SDL_UnlockMutex(device->lock); /* can't hold the lock or the audio thread will deadlock while we WaitThread it. */ - CloseAudioDevice(device); + ClosePhysicalAudioDevice(device); } else { - if (SDL_AtomicGet(&device->refcount) < 0) { - SDL_AtomicSet(&device->refcount, 0); /* something closed more than it should, force this back to zero. Best we can do. */ - } - SDL_UnlockMutex(device->lock); /* can't hold the lock or the audio thread will deadlock while we WaitThread it. */ + SDL_UnlockMutex(device->lock); // we're set, let everything go again. } } } @@ -1008,9 +1097,9 @@ void SDL_UpdatedAudioDeviceFormat(SDL_AudioDevice *device) } /* this expects the device lock to be held. */ -static int OpenAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec *inspec) +static int OpenPhysicalAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec *inspec) { - SDL_assert(SDL_AtomicGet(&device->refcount) == 1); + SDL_assert(device->logical_devices == NULL); SDL_AudioSpec spec; SDL_memcpy(&spec, inspec, sizeof (SDL_AudioSpec)); @@ -1026,8 +1115,9 @@ static int OpenAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec *inspec) device->sample_frames = GetDefaultSampleFramesFromFreq(device->spec.freq); SDL_UpdatedAudioDeviceFormat(device); /* start this off sane. */ + device->is_opened = SDL_TRUE; // mark this true even if impl.OpenDevice fails, so we know to clean up. if (current_audio.impl.OpenDevice(device) < 0) { - CloseAudioDevice(device); /* clean up anything the backend left half-initialized. */ + ClosePhysicalAudioDevice(device); /* clean up anything the backend left half-initialized. */ return -1; } @@ -1036,20 +1126,20 @@ static int OpenAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec *inspec) /* Allocate a scratch audio buffer */ device->work_buffer = (Uint8 *)SDL_aligned_alloc(SDL_SIMDGetAlignment(), device->buffer_size); if (device->work_buffer == NULL) { - CloseAudioDevice(device); + ClosePhysicalAudioDevice(device); return SDL_OutOfMemory(); } /* Start the audio thread if necessary */ if (!current_audio.impl.ProvidesOwnCallbackThread) { - const size_t stacksize = 64 * 1024; /* We only need a little space, so make the stack tiny. We may adjust this as necessary later. */ + const size_t stacksize = 0; /* just take the system default, since audio streams might have callbacks. */ char threadname[64]; (void)SDL_snprintf(threadname, sizeof(threadname), "SDLAudio%c%d", (device->iscapture) ? 'C' : 'P', (int) device->instance_id); device->thread = SDL_CreateThreadInternal(device->iscapture ? CaptureAudioThread : OutputAudioThread, threadname, stacksize, device); if (device->thread == NULL) { - CloseAudioDevice(device); + ClosePhysicalAudioDevice(device); return SDL_SetError("Couldn't create audio thread"); } } @@ -1059,14 +1149,43 @@ static int OpenAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec *inspec) SDL_AudioDeviceID SDL_OpenAudioDevice(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec) { - SDL_AudioDevice *device = ObtainAudioDevice(devid); /* !!! FIXME: need to choose default device for devid==0 */ - int retval = 0; + SDL_bool is_default = SDL_FALSE; + if (devid == SDL_AUDIO_DEVICE_DEFAULT_OUTPUT) { + // !!! FIXME: needs to be hooked up. + //devid = current_audio.default_output_device_id; + is_default = SDL_TRUE; + } else if (devid == SDL_AUDIO_DEVICE_DEFAULT_CAPTURE) { + // !!! FIXME: needs to be hooked up. + //devid = current_audio.default_capture_device_id; + is_default = SDL_TRUE; + } + SDL_AudioDeviceID retval = 0; + + // this will let you use a logical device to make a new logical device on the parent physical device. Could be useful? + SDL_AudioDevice *device = ObtainPhysicalAudioDevice(devid); if (device) { - retval = device->instance_id; - if (SDL_AtomicIncRef(&device->refcount) == 0) { - if (OpenAudioDevice(device, spec) == -1) { - retval = 0; + SDL_LogicalAudioDevice *logdev = (SDL_LogicalAudioDevice *) SDL_calloc(1, sizeof (SDL_LogicalAudioDevice)); + if (!logdev) { + SDL_OutOfMemory(); + } else { + if (device->logical_devices == NULL) { // first thing using this physical device? Open at the OS level... + if (OpenPhysicalAudioDevice(device, spec) == -1) { + SDL_free(logdev); + logdev = NULL; + } + } + + if (logdev != NULL) { + SDL_AtomicSet(&logdev->paused, 0); + retval = logdev->instance_id = assign_audio_device_instance_id(device->iscapture, /*islogical=*/SDL_TRUE); + logdev->physical_device = device; + logdev->is_default = is_default; + logdev->next = device->logical_devices; + if (device->logical_devices) { + device->logical_devices->prev = logdev; + } + device->logical_devices = logdev; } } SDL_UnlockMutex(device->lock); @@ -1077,27 +1196,29 @@ SDL_AudioDeviceID SDL_OpenAudioDevice(SDL_AudioDeviceID devid, const SDL_AudioSp int SDL_BindAudioStreams(SDL_AudioDeviceID devid, SDL_AudioStream **streams, int num_streams) { - SDL_AudioDevice *device; - int retval = 0; - int i; + const SDL_bool islogical = (devid & (1<<1)) ? SDL_FALSE : SDL_TRUE; + SDL_LogicalAudioDevice *logdev; if (num_streams == 0) { - return 0; /* nothing to do */ + return 0; // nothing to do } else if (num_streams < 0) { return SDL_InvalidParamError("num_streams"); } else if (streams == NULL) { return SDL_InvalidParamError("streams"); - } else if ((device = ObtainAudioDevice(devid)) == NULL) { - return -1; /* ObtainAudioDevice set the error message. */ - } else if (SDL_AtomicGet(&device->refcount) == 0) { - SDL_UnlockMutex(device->lock); - return SDL_SetError("Device is not opened"); + } else if (!islogical) { + return SDL_SetError("Audio streams are bound to device ids from SDL_OpenAudioDevice, not raw physical devices"); + } else if ((logdev = ObtainLogicalAudioDevice(devid)) == NULL) { + return -1; // ObtainLogicalAudioDevice set the error message. } - SDL_assert(!device->bound_streams || (device->bound_streams->prev_binding == NULL)); + // make sure start of list is sane. + SDL_assert(!logdev->bound_streams || (logdev->bound_streams->prev_binding == NULL)); - /* lock all the streams upfront, so we can verify they aren't bound elsewhere and add them all in one block, as this is intended to add everything or nothing. */ - for (i = 0; i < num_streams; i++) { + SDL_AudioDevice *device = logdev->physical_device; + int retval = 0; + + // lock all the streams upfront, so we can verify they aren't bound elsewhere and add them all in one block, as this is intended to add everything or nothing. + for (int i = 0; i < num_streams; i++) { SDL_AudioStream *stream = streams[i]; if (stream == NULL) { retval = SDL_SetError("Stream #%d is NULL", i); @@ -1119,26 +1240,27 @@ int SDL_BindAudioStreams(SDL_AudioDeviceID devid, SDL_AudioStream **streams, int } if (retval == 0) { - /* Now that everything is verified, chain everything together. */ - for (i = 0; i < num_streams; i++) { + // Now that everything is verified, chain everything together. + const SDL_bool iscapture = device->iscapture; + for (int i = 0; i < num_streams; i++) { SDL_AudioStream *stream = streams[i]; SDL_AudioSpec src_spec, dst_spec; - /* set the proper end of the stream to the device's format. */ + // set the proper end of the stream to the device's format. SDL_GetAudioStreamFormat(stream, &src_spec, &dst_spec); - if (device->iscapture) { + if (iscapture) { SDL_SetAudioStreamFormat(stream, &device->spec, &dst_spec); } else { SDL_SetAudioStreamFormat(stream, &src_spec, &device->spec); } - stream->bound_device = device; + stream->bound_device = logdev; stream->prev_binding = NULL; - stream->next_binding = device->bound_streams; - if (device->bound_streams) { - device->bound_streams->prev_binding = stream; + stream->next_binding = logdev->bound_streams; + if (logdev->bound_streams) { + logdev->bound_streams->prev_binding = stream; } - device->bound_streams = stream; + logdev->bound_streams = stream; SDL_UnlockMutex(stream->lock); } @@ -1168,15 +1290,13 @@ void SDL_UnbindAudioStreams(SDL_AudioStream **streams, int num_streams) } while (SDL_TRUE) { - SDL_AudioDevice *bounddev; - SDL_LockMutex(stream->lock); /* lock to check this and then release it, in case the device isn't locked yet. */ - bounddev = stream->bound_device; + SDL_LogicalAudioDevice *bounddev = stream->bound_device; SDL_UnlockMutex(stream->lock); /* lock in correct order. */ if (bounddev) { - SDL_LockMutex(bounddev->lock); /* this requires recursive mutexes, since we're likely locking the same device multiple times. */ + SDL_LockMutex(bounddev->physical_device->lock); /* this requires recursive mutexes, since we're likely locking the same device multiple times. */ } SDL_LockMutex(stream->lock); @@ -1185,7 +1305,7 @@ void SDL_UnbindAudioStreams(SDL_AudioStream **streams, int num_streams) } else { SDL_UnlockMutex(stream->lock); /* it changed bindings! Try again. */ if (bounddev) { - SDL_UnlockMutex(bounddev->lock); + SDL_UnlockMutex(bounddev->physical_device->lock); } } } @@ -1213,11 +1333,11 @@ void SDL_UnbindAudioStreams(SDL_AudioStream **streams, int num_streams) for (i = 0; i < num_streams; i++) { SDL_AudioStream *stream = streams[i]; if (stream && stream->bound_device) { - SDL_AudioDevice *dev = stream->bound_device; + SDL_LogicalAudioDevice *logdev = stream->bound_device; stream->bound_device = NULL; SDL_UnlockMutex(stream->lock); - if (dev) { - SDL_UnlockMutex(dev->lock); + if (logdev) { + SDL_UnlockMutex(logdev->physical_device->lock); } } } @@ -1231,11 +1351,17 @@ void SDL_UnbindAudioStream(SDL_AudioStream *stream) SDL_AudioStream *SDL_CreateAndBindAudioStream(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec) { + const SDL_bool islogical = (devid & (1<<1)) ? SDL_FALSE : SDL_TRUE; + if (!islogical) { + SDL_SetError("Audio streams are bound to device ids from SDL_OpenAudioDevice, not raw physical devices"); + return NULL; + } + SDL_AudioStream *stream = NULL; - SDL_AudioDevice *device = ObtainAudioDevice(devid); - if (device) { - const SDL_bool iscapture = (devid & 1) ? SDL_FALSE : SDL_TRUE; /* capture instance ids are even and output devices are odd */ - if (iscapture) { + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid); + if (logdev) { + SDL_AudioDevice *device = logdev->physical_device; + if (device->iscapture) { stream = SDL_CreateAudioStream(&device->spec, spec); } else { stream = SDL_CreateAudioStream(spec, &device->spec); diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index 9674151d8a..ccc1af9745 100644 --- a/src/audio/SDL_sysaudio.h +++ b/src/audio/SDL_sysaudio.h @@ -55,8 +55,8 @@ extern void (*SDL_Convert_F32_to_S32)(Sint32 *dst, const float *src, int num_sam #define DEFAULT_AUDIO_FREQUENCY 44100 -/* The SDL audio driver */ typedef struct SDL_AudioDevice SDL_AudioDevice; +typedef struct SDL_LogicalAudioDevice SDL_LogicalAudioDevice; /* Used by src/SDL.c to initialize a particular audio driver. */ extern int SDL_InitAudio(const char *driver_name); @@ -80,7 +80,7 @@ extern SDL_AudioDevice *SDL_AddAudioDevice(const SDL_bool iscapture, const char extern void SDL_AudioDeviceDisconnected(SDL_AudioDevice *device); /* Find the SDL_AudioDevice associated with the handle supplied to SDL_AddAudioDevice. NULL if not found. Locks the device! You must unlock!! */ -extern SDL_AudioDevice *SDL_ObtainAudioDeviceByHandle(void *handle); +extern SDL_AudioDevice *SDL_ObtainPhysicalAudioDeviceByHandle(void *handle); /* Backends should call this if they change the device format, channels, freq, or sample_frames to keep other state correct. */ extern void SDL_UpdatedAudioDeviceFormat(SDL_AudioDevice *device); @@ -169,11 +169,37 @@ struct SDL_AudioStream int pre_resample_channels; int packetlen; - SDL_AudioDevice *bound_device; + SDL_LogicalAudioDevice *bound_device; SDL_AudioStream *next_binding; SDL_AudioStream *prev_binding; }; +/* Logical devices are an abstraction in SDL3; you can open the same physical + device multiple times, and each will result in an object with its own set + of bound audio streams, etc, even though internally these are all processed + as a group when mixing the final output for the physical device. */ +struct SDL_LogicalAudioDevice +{ + /* the unique instance ID of this device. */ + SDL_AudioDeviceID instance_id; + + /* The physical device associated with this opened device. */ + SDL_AudioDevice *physical_device; + + /* If whole logical device is paused (process no streams bound to this device). */ + SDL_AtomicInt paused; + + /* double-linked list of all audio streams currently bound to this opened device. */ + SDL_AudioStream *bound_streams; + + /* SDL_TRUE if this was opened as a default device. */ + SDL_bool is_default; + + /* double-linked list of opened devices on the same physical device. */ + SDL_LogicalAudioDevice *next; + SDL_LogicalAudioDevice *prev; +}; + struct SDL_AudioDevice { /* A mutex for locking access to this struct */ @@ -216,16 +242,16 @@ struct SDL_AudioDevice /* A thread to feed the audio device */ SDL_Thread *thread; + /* SDL_TRUE if this physical device is currently opened by the backend. */ + SDL_bool is_opened; + /* Data private to this driver */ struct SDL_PrivateAudioData *hidden; - /* Each device open increases the refcount. We actually close the system device when this hits zero again. */ - SDL_AtomicInt refcount; + /* All logical devices associated with this physical device. */ + SDL_LogicalAudioDevice *logical_devices; - /* double-linked list of all audio streams currently bound to this device. */ - SDL_AudioStream *bound_streams; - - /* double-linked list of all devices. */ + /* double-linked list of all physical devices. */ struct SDL_AudioDevice *prev; struct SDL_AudioDevice *next; }; diff --git a/src/audio/pulseaudio/SDL_pulseaudio.c b/src/audio/pulseaudio/SDL_pulseaudio.c index 514357e378..16ff3cb906 100644 --- a/src/audio/pulseaudio/SDL_pulseaudio.c +++ b/src/audio/pulseaudio/SDL_pulseaudio.c @@ -824,7 +824,7 @@ static void HotplugCallback(pa_context *c, pa_subscription_event_type_t t, uint3 PULSEAUDIO_pa_operation_unref(PULSEAUDIO_pa_context_get_source_info_by_index(pulseaudio_context, idx, SourceInfoCallback, (void *)((intptr_t)added))); } else if (removed && (sink || source)) { /* removes we can handle just with the device index. */ - SDL_AudioDevice *device = SDL_ObtainAudioDeviceByHandle((void *)((intptr_t)idx + 1)); /* !!! FIXME: maybe just have a "disconnect by handle" function instead. */ + SDL_AudioDevice *device = SDL_ObtainPhysicalAudioDeviceByHandle((void *)((intptr_t)idx + 1)); /* !!! FIXME: maybe just have a "disconnect by handle" function instead. */ if (device) { SDL_UnlockMutex(device->lock); /* AudioDeviceDisconnected will relock and verify it's still in the list, but in case this is destroyed, unlock now. */ SDL_AudioDeviceDisconnected(device); diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index e4da9c8d37..0745fd025f 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -884,6 +884,7 @@ SDL3_0.0.0 { SDL_MixAudioFormat; SDL_ConvertAudioSamples; SDL_GetSilenceValueForFormat; + SDL_LoadWAV; # 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 029c9b0f28..4964e77629 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -910,3 +910,4 @@ #define SDL_MixAudioFormat SDL_MixAudioFormat_REAL #define SDL_ConvertAudioSamples SDL_ConvertAudioSamples_REAL #define SDL_GetSilenceValueForFormat SDL_GetSilenceValueForFormat_REAL +#define SDL_LoadWAV SDL_LoadWAV_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 034a630e02..b51ac769c1 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -954,3 +954,4 @@ SDL_DYNAPI_PROC(int,SDL_LoadWAV_RW,(SDL_RWops *a, int b, SDL_AudioSpec *c, Uint8 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,(const SDL_AudioSpec *a, const Uint8 *b, int c, const SDL_AudioSpec *d, Uint8 **e, int *f),(a,b,c,d,e,f),return) SDL_DYNAPI_PROC(int,SDL_GetSilenceValueForFormat,(SDL_AudioFormat a),(a),return) +SDL_DYNAPI_PROC(int,SDL_LoadWAV,(const char *a, SDL_AudioSpec *b, Uint8 **c, Uint32 *d),(a,b,c,d),return)