diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 14e48cf11..2ea479e1d 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -292,6 +292,28 @@ extern "C" { */ #define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME" +/** + * Specify an application icon name for an audio device. + * + * Some audio backends (such as Pulseaudio and Pipewire) allow you to set an + * XDG icon name for your application. Among other things, this icon might show + * up in a system control panel that lets the user adjust the volume on specific + * audio streams instead of using one giant master volume slider. Note that this + * is unrelated to the icon used by the windowing system, which may be set with + * SDL_SetWindowIcon (or via desktop file on Wayland). + * + * Setting this to "" or leaving it unset will have SDL use a reasonable + * default, "applications-games", which is likely to be installed. + * See https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html + * and https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html + * for the relevant XDG icon specs. + * + * This hint should be set before an audio device is opened. + * + * \since This hint is available since SDL 3.0.0. + */ +#define SDL_HINT_AUDIO_DEVICE_APP_ICON_NAME "SDL_AUDIO_DEVICE_APP_ICON_NAME" + /** * A variable controlling device buffer size. * diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c index 6d9eab728..aab43f161 100644 --- a/src/audio/pipewire/SDL_pipewire.c +++ b/src/audio/pipewire/SDL_pipewire.c @@ -1108,7 +1108,7 @@ static int PIPEWIRE_OpenDevice(SDL_AudioDevice *device) const struct spa_pod *params = NULL; struct SDL_PrivateAudioData *priv; struct pw_properties *props; - const char *app_name, *app_id, *stream_name, *stream_role, *error; + const char *app_name, *icon_name, *app_id, *stream_name, *stream_role, *error; Uint32 node_id = !device->handle ? PW_ID_ANY : PW_HANDLE_TO_ID(device->handle); const SDL_bool iscapture = device->iscapture; int res; @@ -1116,7 +1116,7 @@ static int PIPEWIRE_OpenDevice(SDL_AudioDevice *device) // Clamp the period size to sane values const int min_period = PW_MIN_SAMPLES * SPA_MAX(device->spec.freq / PW_BASE_CLOCK_RATE, 1); - // Get the hints for the application name, stream name and role + // Get the hints for the application name, icon name, stream name and role app_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME); if (!app_name || *app_name == '\0') { app_name = SDL_GetHint(SDL_HINT_APP_NAME); @@ -1125,6 +1125,11 @@ static int PIPEWIRE_OpenDevice(SDL_AudioDevice *device) } } + icon_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_APP_ICON_NAME); + if (!icon_name || *icon_name == '\0') { + icon_name = "applications-games"; + } + // App ID. Default to NULL if not available. app_id = SDL_GetHint(SDL_HINT_APP_ID); @@ -1190,6 +1195,7 @@ static int PIPEWIRE_OpenDevice(SDL_AudioDevice *device) PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, iscapture ? "Capture" : "Playback"); PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_ROLE, stream_role); PIPEWIRE_pw_properties_set(props, PW_KEY_APP_NAME, app_name); + PIPEWIRE_pw_properties_set(props, PW_KEY_APP_ICON_NAME, icon_name); if (app_id) { PIPEWIRE_pw_properties_set(props, PW_KEY_APP_ID, app_id); } diff --git a/src/audio/pulseaudio/SDL_pulseaudio.c b/src/audio/pulseaudio/SDL_pulseaudio.c index 2d0ca1009..1898a19e2 100644 --- a/src/audio/pulseaudio/SDL_pulseaudio.c +++ b/src/audio/pulseaudio/SDL_pulseaudio.c @@ -66,6 +66,9 @@ static const char *(*PULSEAUDIO_pa_get_library_version)(void); static pa_channel_map *(*PULSEAUDIO_pa_channel_map_init_auto)( pa_channel_map *, unsigned, pa_channel_map_def_t); static const char *(*PULSEAUDIO_pa_strerror)(int); +static pa_proplist *(*PULSEAUDIO_pa_proplist_new)(void); +static void (*PULSEAUDIO_pa_proplist_free)(pa_proplist *); +static int (*PULSEAUDIO_pa_proplist_sets)(pa_proplist *, const char *, const char *); static pa_threaded_mainloop *(*PULSEAUDIO_pa_threaded_mainloop_new)(void); static void (*PULSEAUDIO_pa_threaded_mainloop_set_name)(pa_threaded_mainloop *, const char *); @@ -84,8 +87,9 @@ static void (*PULSEAUDIO_pa_operation_set_state_callback)(pa_operation *, pa_ope static void (*PULSEAUDIO_pa_operation_cancel)(pa_operation *); static void (*PULSEAUDIO_pa_operation_unref)(pa_operation *); -static pa_context *(*PULSEAUDIO_pa_context_new)(pa_mainloop_api *, - const char *); +static pa_context *(*PULSEAUDIO_pa_context_new_with_proplist)(pa_mainloop_api *, + const char *, + const pa_proplist *); static void (*PULSEAUDIO_pa_context_set_state_callback)(pa_context *, pa_context_notify_cb_t, void *); static int (*PULSEAUDIO_pa_context_connect)(pa_context *, const char *, pa_context_flags_t, const pa_spawn_api *); @@ -205,7 +209,7 @@ static int load_pulseaudio_syms(void) SDL_PULSEAUDIO_SYM(pa_operation_get_state); SDL_PULSEAUDIO_SYM(pa_operation_cancel); SDL_PULSEAUDIO_SYM(pa_operation_unref); - SDL_PULSEAUDIO_SYM(pa_context_new); + SDL_PULSEAUDIO_SYM(pa_context_new_with_proplist); SDL_PULSEAUDIO_SYM(pa_context_set_state_callback); SDL_PULSEAUDIO_SYM(pa_context_connect); SDL_PULSEAUDIO_SYM(pa_context_get_sink_info_list); @@ -238,6 +242,9 @@ static int load_pulseaudio_syms(void) SDL_PULSEAUDIO_SYM(pa_stream_set_write_callback); SDL_PULSEAUDIO_SYM(pa_stream_set_read_callback); SDL_PULSEAUDIO_SYM(pa_context_get_server_info); + SDL_PULSEAUDIO_SYM(pa_proplist_new); + SDL_PULSEAUDIO_SYM(pa_proplist_free); + SDL_PULSEAUDIO_SYM(pa_proplist_sets); // optional #ifdef SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC @@ -337,6 +344,8 @@ static void PulseContextStateChangeCallback(pa_context *context, void *userdata) static int ConnectToPulseServer(void) { pa_mainloop_api *mainloop_api = NULL; + pa_proplist *proplist = NULL; + const char *icon_name; int state = 0; SDL_assert(pulseaudio_threaded_mainloop == NULL); @@ -362,11 +371,22 @@ static int ConnectToPulseServer(void) mainloop_api = PULSEAUDIO_pa_threaded_mainloop_get_api(pulseaudio_threaded_mainloop); SDL_assert(mainloop_api != NULL); // this never fails, right? - pulseaudio_context = PULSEAUDIO_pa_context_new(mainloop_api, getAppName()); + if (!(proplist = PULSEAUDIO_pa_proplist_new())) { + return SDL_SetError("pa_proplist_new() failed"); + } + + icon_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_APP_ICON_NAME); + if (!icon_name || *icon_name == '\0') { + icon_name = "applications-games"; + } + PULSEAUDIO_pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, icon_name); + + pulseaudio_context = PULSEAUDIO_pa_context_new_with_proplist(mainloop_api, getAppName(), proplist); if (!pulseaudio_context) { - SDL_SetError("pa_context_new() failed"); + SDL_SetError("pa_context_new_with_proplist() failed"); goto failed; } + PULSEAUDIO_pa_proplist_free(proplist); PULSEAUDIO_pa_context_set_state_callback(pulseaudio_context, PulseContextStateChangeCallback, NULL);