SDL_AudioDeviceEvent: can't determine which removed (SDL2)

My program needs to know which audio devices are available on the system at any given time, so I was pleased to see that SDL captures SDL_AUDIODEVICEADDED and SDL_AUDIODEVICEREMOVED events, but I’m having trouble getting the information I need from them.

The which member of the SDL_AudioDeviceEvent struct provides the id (SDL_AudioDeviceID) of a device that has been removed if the event is SDL_AUDIODEVICEREMOVED. But as far as I can tell, the only way to get a device’s ID is to open it. It is not practical for me to open every available audio device merely to obtain its ID, because that’s very slow for some devices. SDL must know the id of a device before it has been opened, because it captures SDL_AUDIODEVICEREMOVED events for available devices that have never been opened.

How can I obtain the ID of an audio device without opening it, or otherwise determine which device (by name or index) has been removed during an SDL_AUDIODEVICEREMOVED event?

In general the idea is that if you have opened the device you will have kept the returned ID, if you have not opened the device then it should not otherwise affect your code (and event.adevice.which on removal will equal 0 if you have not opened the removed device).
A list of currently available devices is always maintained by SDL, so they don’t expect you to keep your own records of added and removed devices in order to keep track.
You only need to keep track of the devices that you have open so that you can close them if they match event.adevice.which on device removal.

Accessing the list of available devices:

// print out all audio devices

#include <SDL2/SDL.h>
#include <vector>

int main()
{
        SDL_Init(SDL_INIT_EVERYTHING);

        int countSpeakers = SDL_GetNumAudioDevices(0);
        SDL_Log("___ SPEAKERS DETECTED:");
        for(int i = 0; i < countSpeakers; i ++)
        {
                SDL_Log(SDL_GetAudioDeviceName(i, 0));
        }

        int countRecorders = SDL_GetNumAudioDevices(1);
        SDL_Log("___ RECORDERS DETECTED:");
        for(int i = 0; i < countRecorders; i ++)
        {
                SDL_Log(SDL_GetAudioDeviceName(i, 1));
        }
        SDL_Quit();
}

Thanks @anon914446, I always appreciate your thorough responses on mine and other posts on this forum.

In general the idea is that if you have opened the device you will have kept the returned ID, if you have not opened the device then it should not otherwise affect your code

this may be true most of the time, but not for my application. I’m writing a DAW, and the user must be able to see a list of available devices when, for example, selecting the input for a given audio track:

For a variety of reasons (one of which you helped me on!), those capture devices are only opened when they are being used to record audio. The data structures in my program containing the audio device information cannot be simply destroyed and recreated, because they are referenced all over the place. If SDL_AudioEvent provided an index or a device name, I could check those lists for the removed device and modify them accordingly. Since it does not, as far as I can tell right now, my best option is to do another call to SDL_GetNumAudioDevices and check each of the subsequent calls to SDL_GetAudioDeviceName against the existing list to determine which was removed, and then do the modification. That is ok, but requires some extra work (n^2 string comparisons instead of n) and is a somewhat ugly solution if SDL actually knows which device was removed (even if it has never been opened).

1 Like

… Just remember not to get too concerned about bigO notation on user driven events, your user can only unplug so many cables in a second. You could iterate the list a couple of thousand times in a frame without causing any kind of a detectable delay in audio processing.

I usually have my own struct/class that pairs the name with the opened device ID. When the Device Added Event occurs then I open the device and add it as a new object in the list. When Remove Event occurs then I do scroll through the list to close and delete the correct device.
Do you have a reason not to open the devices when they are made available? (Open but not playing)?
→ Looking back to that link you posted, when you open your airpods for both playback and recording it was forcing them to play as mono instead of stereo. That is a good reason not to open both by default.
→ I’ve never notice a delay on my system from opening audio devices, How slow is it to open all your devices? Is it slower on a Bluetooth device?

I don’t usually have to deal with more than 4 to 6 audio IO devices at a time, but I don’t think opening the device adds much of a power draw.

P.S.

If you are writing a DAW, then I think you will like SDL3’s Audio Streams API if you have time to try it out.

1 Like

Just remember not to get too concerned about bigO notation on user driven events, your user can only unplug so many cables in a second

This is true, the time complexity is not a real performance issue, it just gives me irrational displeasure haha. The only case that I was actually concerned about is a race condition that can occur if a device is removed but is actively used for recording on another thread. I would hypothetically like to halt recording (and related operations) if and only if the device removed is the one actively recording, and I’d have a small window in which to make that determination where the difference could count. But realistically it probably makes most sense to halt all recording anytime any device is removed, as soon as possible.

Looking back to that link you posted, when you open your airpods for both playback and recording it was forcing them to play as mono instead of stereo. That is a good reason not to open both by default.

Yeah exactly. Only a problem I’ve encountered with airpods, but I use them often, as do others, so it’s a consideration.

I’ve never notice a delay on my system from opening audio devices, How slow is it to open all your devices? Is it slower on a Bluetooth device?

The big one is this apple (shakes fist) feature where your macbook can use your iphone as a recording device. It takes forever to open, and then it kind of hijacks your phone screen, too. Not something I ever intend to use, but it seems to be on by default, and any other users who have a similar setup would encounter the same thing upon starting up the program.

If you are writing a DAW, then I think you will like SDL3’s Audio Streams API if you have time to try it out.

I really do want to check out all the work that’s been done on SDL3, audio stuff in particular. Unfortunately I already completed most of my interfacing with SDL’s audio system, and it’s all in SDL2 with the old callback method. So it’s hard to justify the cutover right now. But I’ll get there eventually :slight_smile:

1 Like

I just want to confirm we’re at a solution to the problem, by creating a struct to envelope the data that you need. If you’re working in pure C (not C++) then you might want to create some helper functions as well to clean up the open and remove process.

typedef struct audioData
{
    SDL_AudioDeviceID devID = -1;
    char * nameStr = NULL;
    bool DeviceIsOpen = false;
} audioData;

// ... down in the event loop

switch(event.type)
{
    case SDL_AUDIODEVICEREMOVED:
    // if event.adevice.which is one of the objects.devID in your list, 
    // if it's open then close it 
    // Now remove this object from your list

    // if event.adevice.which == 0 an un-opened device has been removed,
    // now it's a check of your list of objects.nameStr against the list that SDL maintains.

    // if C++ were an option then I might suggest using an unordered_set or an unordered_map
    // as the storage container.
    break;
    case SDL_AUDIODEVICEADDED:
    // add this object to your list. Present it to your user and give them
    // the option to activate it or not. On activation you will receive the devID

    // this event will be called on startup for each device that's already
    // plugged in, so your list will be complete just by handling this event.
    break;
}

Yes that’s all correct. I ran into a few gotchas while implementing and testing:

  • A device can be opened multiple times without being closed. Each subsequent call to SDL_OpenAudioDevice will return a new, distinct ID, even though it is the same device.
  • The ID in SDL_AudioDeviceEvent.which will be the first one the device was opened with.
  • Therefore, if you (accidentally) open a device multiple times and reset the ID of a persistent data structure each time, you will not be able to identify the correct closed device.
  • When a device that is currently open is disconnected (as reported by an SDL_AUDIODEVICEREMOVED event) you must close it; otherwise, subsequent removals of same device (after being re-added) may not generate events at all. This might be considered a bug, but it’s a good idea to close those devices no matter what.