audio: allow backends to set the volume themselves for physical devices

this allows system per-app volume sliders to work as expected
This commit is contained in:
Ian Monroe 2026-05-29 22:30:35 -07:00
parent dc8b189491
commit 37515a9fea
No known key found for this signature in database
GPG key ID: AA9CC4703A3BB60A
4 changed files with 111 additions and 34 deletions

View file

@ -862,8 +862,8 @@ extern SDL_DECLSPEC bool SDLCALL SDL_AudioDevicePaused(SDL_AudioDeviceID devid);
*
* Audio devices default to a gain of 1.0f (no change in output).
*
* Physical devices may not have their gain changed, only logical devices, and
* this function will always return -1.0f when used on physical devices.
* Physical device gain support depends on the backend. -1.0f will be returned
* if it is not supported.
*
* \param devid the audio device to query.
* \returns the gain of the device or -1.0f on failure; call SDL_GetError()
@ -885,18 +885,16 @@ extern SDL_DECLSPEC float SDLCALL SDL_GetAudioDeviceGain(SDL_AudioDeviceID devid
*
* Audio devices default to a gain of 1.0f (no change in output).
*
* Physical devices may not have their gain changed, only logical devices, and
* this function will always return false when used on physical devices. While
* it might seem attractive to adjust several logical devices at once in this
* way, it would allow an app or library to interfere with another portion of
* the program's otherwise-isolated devices.
* Support for gain changes of physical devices depends on the backend. It will
* return false if it is not supported; likely you should fall back to changing
* the logical device gain in that case.
*
* This is applied, along with any per-audiostream gain, during playback to
* the hardware, and can be continuously changed to create various effects. On
* recording devices, this will adjust the gain before passing the data into
* an audiostream; that recording audiostream can then adjust its gain further
* when outputting the data elsewhere, if it likes, but that second gain is
* not applied until the data leaves the audiostream again.
* For logical devices this is applied, along with any per-audiostream gain,
* during playback to the hardware, and can be continuously changed to create
* various effects. On recording devices, this will adjust the gain before
* passing the data into an audiostream; that recording audiostream can then
* adjust its gain further when outputting the data elsewhere, if it likes, but
* that second gain is not applied until the data leaves the audiostream again.
*
* \param devid the audio device on which to change gain.
* \param gain the gain. 1.0f is no change, 0.0f is silence.

View file

@ -844,6 +844,8 @@ static void SDL_AudioCloseDevice_Default(SDL_AudioDevice *device) { /* no-op. */
static void SDL_AudioDeinitializeStart_Default(void) { /* no-op. */ }
static void SDL_AudioDeinitialize_Default(void) { /* no-op. */ }
static void SDL_AudioFreeDeviceHandle_Default(SDL_AudioDevice *device) { /* no-op. */ }
static bool SDL_AudioSetDeviceGain_Default(SDL_AudioDevice *device, float gain) { return false; /* no-op. */ }
static float SDL_AudioGetDeviceGain_Default(SDL_AudioDevice *device) { return -1.0f; /* no-op. */ }
static void SDL_AudioThreadInit_Default(SDL_AudioDevice *device)
{
@ -897,6 +899,8 @@ static void CompleteAudioEntryPoints(void)
FILL_STUB(FreeDeviceHandle);
FILL_STUB(DeinitializeStart);
FILL_STUB(Deinitialize);
FILL_STUB(SetDeviceGain);
FILL_STUB(GetDeviceGain);
#undef FILL_STUB
}
@ -1960,9 +1964,27 @@ bool SDL_AudioDevicePaused(SDL_AudioDeviceID devid)
float SDL_GetAudioDeviceGain(SDL_AudioDeviceID devid)
{
float result = -1.0f;
SDL_AudioDevice *device = NULL;
SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device);
const float result = logdev ? logdev->gain : -1.0f;
if (!SDL_IsAudioDeviceLogical(devid)) {
if (!current_audio.impl.HasBackendVolumeControl) {
return result;
}
device = ObtainPhysicalAudioDeviceDefaultAllowed(devid);
if (!device) {
return result;
}
float backend_gain = current_audio.impl.GetDeviceGain(device);
if (backend_gain >= 0.0f) {
result = backend_gain;
}
} else {
SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device);
if (logdev) {
result = logdev->gain;
}
}
ReleaseAudioDevice(device);
return result;
}
@ -1972,14 +1994,25 @@ bool SDL_SetAudioDeviceGain(SDL_AudioDeviceID devid, float gain)
CHECK_PARAM(gain < 0.0f) {
return SDL_InvalidParamError("gain");
}
SDL_AudioDevice *device = NULL;
SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device);
bool result = false;
if (logdev) {
logdev->gain = gain;
UpdateAudioStreamFormatsPhysical(device);
result = true;
SDL_AudioDevice *device = NULL;
if (!SDL_IsAudioDeviceLogical(devid)) {
if (!current_audio.impl.HasBackendVolumeControl) {
return result;
}
device = ObtainPhysicalAudioDeviceDefaultAllowed(devid);
if (!device) {
return result;
}
result = current_audio.impl.SetDeviceGain(device, gain);
} else {
SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device);
if (logdev) {
logdev->gain = gain;
result = true;
UpdateAudioStreamFormatsPhysical(device);
}
}
ReleaseAudioDevice(device);
return result;

View file

@ -162,12 +162,15 @@ typedef struct SDL_AudioDriverImpl
void (*FreeDeviceHandle)(SDL_AudioDevice *device); // SDL is done with this device; free the handle from SDL_AddAudioDevice()
void (*DeinitializeStart)(void); // SDL calls this, then starts destroying objects, then calls Deinitialize. This is a good place to stop hotplug detection.
void (*Deinitialize)(void);
bool (*SetDeviceGain)(SDL_AudioDevice *device, float gain); // Tell the backend that a device's gain has changed
float (*GetDeviceGain)(SDL_AudioDevice *device); // Retrieve the current hardware/server gain
// Some flags to push duplicate code into the core and reduce #ifdefs.
bool ProvidesOwnCallbackThread; // !!! FIXME: rename this, it's not a callback thread anymore.
bool HasRecordingSupport;
bool OnlyHasDefaultPlaybackDevice;
bool OnlyHasDefaultRecordingDevice; // !!! FIXME: is there ever a time where you'd have a default playback and not a default recording (or vice versa)?
bool HasBackendVolumeControl;
} SDL_AudioDriverImpl;

View file

@ -27,6 +27,7 @@
#include <pipewire/extensions/metadata.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>
#include <spa/utils/json.h>
/*
@ -91,6 +92,8 @@ static struct pw_properties *(*PIPEWIRE_pw_properties_new)(const char *, ...)SPA
static int (*PIPEWIRE_pw_properties_set)(struct pw_properties *, const char *, const char *);
static int (*PIPEWIRE_pw_properties_setf)(struct pw_properties *, const char *, const char *, ...) SPA_PRINTF_FUNC(3, 4);
static int (*PIPEWIRE_pw_stream_update_properties)(struct pw_stream *, const struct spa_dict *);
static int (*PIPEWIRE_pw_stream_set_control)(struct pw_stream *, uint32_t, uint32_t, float *, ...);
static const struct pw_stream_control *(*PIPEWIRE_pw_stream_get_control)(struct pw_stream *, uint32_t);
#ifdef SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC
@ -185,6 +188,8 @@ static bool load_pipewire_syms(void)
SDL_PIPEWIRE_SYM(pw_properties_set);
SDL_PIPEWIRE_SYM(pw_properties_setf);
SDL_PIPEWIRE_SYM(pw_stream_update_properties);
SDL_PIPEWIRE_SYM(pw_stream_set_control);
SDL_PIPEWIRE_SYM(pw_stream_get_control);
return true;
}
@ -991,6 +996,13 @@ static Uint8 *PIPEWIRE_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size)
return NULL;
}
const int maxsize = (int) spa_buf->datas[0].maxsize;
if (maxsize < *buffer_size) {
*buffer_size = maxsize;
device->buffer_size = maxsize;
device->sample_frames = maxsize / device->hidden->stride;
}
device->hidden->pw_buf = pw_buf;
return (Uint8 *) spa_buf->datas[0].data;
}
@ -1082,18 +1094,6 @@ static void input_callback(void *data)
static void stream_add_buffer_callback(void *data, struct pw_buffer *buffer)
{
SDL_AudioDevice *device = (SDL_AudioDevice *) data;
if (device->recording == false) {
/* Clamp the output spec samples and size to the max size of the Pipewire buffer.
If they exceed the maximum size of the Pipewire buffer, double buffering will be used. */
if (device->buffer_size > buffer->buffer->datas[0].maxsize) {
SDL_LockMutex(device->lock);
device->sample_frames = buffer->buffer->datas[0].maxsize / device->hidden->stride;
device->buffer_size = buffer->buffer->datas[0].maxsize;
SDL_UnlockMutex(device->lock);
}
}
device->hidden->stream_init_status |= PW_READY_FLAG_BUFFER_ADDED;
PIPEWIRE_pw_thread_loop_signal(device->hidden->loop, false);
}
@ -1355,6 +1355,46 @@ static void PIPEWIRE_Deinitialize(void)
}
}
static bool PIPEWIRE_SetDeviceGain(SDL_AudioDevice *device, float gain)
{
if (!device || !device->hidden || !device->hidden->stream || !device->hidden->loop) {
return false;
}
float values[SPA_AUDIO_MAX_CHANNELS];
int channels = device->spec.channels;
if (channels > SPA_AUDIO_MAX_CHANNELS) {
channels = SPA_AUDIO_MAX_CHANNELS;
}
for (int i = 0; i < channels; i++) {
values[i] = gain;
}
PIPEWIRE_pw_thread_loop_lock(device->hidden->loop);
PIPEWIRE_pw_stream_set_control(device->hidden->stream, SPA_PROP_channelVolumes, channels, values, 0);
PIPEWIRE_pw_thread_loop_unlock(device->hidden->loop);
return true;
}
static float PIPEWIRE_GetDeviceGain(SDL_AudioDevice *device)
{
float gain = -1.0f;
if (!device || !device->hidden || !device->hidden->stream || !device->hidden->loop) {
return gain;
}
PIPEWIRE_pw_thread_loop_lock(device->hidden->loop);
const struct pw_stream_control *control = PIPEWIRE_pw_stream_get_control(device->hidden->stream, SPA_PROP_channelVolumes);
if (control && control->n_values > 0 && control->values) {
gain = control->values[0];
}
PIPEWIRE_pw_thread_loop_unlock(device->hidden->loop);
return gain;
}
static bool PipewireInitialize(SDL_AudioDriverImpl *impl)
{
if (!pipewire_initialized) {
@ -1379,9 +1419,12 @@ static bool PipewireInitialize(SDL_AudioDriverImpl *impl)
impl->RecordDevice = PIPEWIRE_RecordDevice;
impl->FlushRecording = PIPEWIRE_FlushRecording;
impl->CloseDevice = PIPEWIRE_CloseDevice;
impl->GetDeviceGain = PIPEWIRE_GetDeviceGain;
impl->HasRecordingSupport = true;
impl->ProvidesOwnCallbackThread = true;
impl->HasBackendVolumeControl = true;
impl->SetDeviceGain = PIPEWIRE_SetDeviceGain;
return true;
}