diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h index 6b27908c07..b55b548190 100644 --- a/include/SDL3/SDL_audio.h +++ b/include/SDL3/SDL_audio.h @@ -759,6 +759,21 @@ extern SDL_DECLSPEC SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(SDL_AudioDevic */ extern SDL_DECLSPEC bool SDLCALL SDL_IsAudioDevicePhysical(SDL_AudioDeviceID devid); +/** + * Get the physical audio device associated with a logical audio device. + * + * If `devid` is already a physical device, this function returns `devid`. + * If `devid` is an invalid device, it returns 0. + * + * \param devid the device ID to query. + * \returns the physical device ID, or 0 on error. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.5.0. + */ +extern SDL_DECLSPEC SDL_AudioDeviceID SDLCALL SDL_GetPhysicalAudioDevice(SDL_AudioDeviceID devid); + /** * Determine if an audio device is a playback device (instead of recording). * @@ -862,8 +877,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 +900,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. diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index 0a3827571f..704a13f25b 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -242,6 +242,7 @@ typedef enum SDL_EventType SDL_EVENT_AUDIO_DEVICE_ADDED = 0x1100, /**< A new audio device is available */ SDL_EVENT_AUDIO_DEVICE_REMOVED, /**< An audio device has been removed. */ SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED, /**< An audio device's format has been changed by the system. */ + SDL_EVENT_AUDIO_DEVICE_GAIN_CHANGED, /**< An audio device's gain has been changed by the system. */ /* Sensor events */ SDL_EVENT_SENSOR_UPDATE = 0x1200, /**< A sensor was updated */ @@ -741,7 +742,7 @@ typedef struct SDL_GamepadCapSenseEvent */ typedef struct SDL_AudioDeviceEvent { - SDL_EventType type; /**< SDL_EVENT_AUDIO_DEVICE_ADDED, or SDL_EVENT_AUDIO_DEVICE_REMOVED, or SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED */ + SDL_EventType type; /**< SDL_EVENT_AUDIO_DEVICE_ADDED, or SDL_EVENT_AUDIO_DEVICE_REMOVED, or SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED, or SDL_EVENT_AUDIO_DEVICE_GAIN_CHANGED */ Uint32 reserved; Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ SDL_AudioDeviceID which; /**< SDL_AudioDeviceID for the device being added or removed or changing */ diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index d642016541..2999da962d 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -538,6 +538,20 @@ static SDL_AudioDevice *ObtainPhysicalAudioDeviceDefaultAllowed(SDL_AudioDeviceI return NULL; } +SDL_AudioDeviceID SDL_GetPhysicalAudioDevice(SDL_AudioDeviceID devid) +{ + if (SDL_IsAudioDevicePhysical(devid)) { + return devid; + } + + SDL_AudioDeviceID result = 0; + SDL_AudioDevice *device = ObtainPhysicalAudioDeviceDefaultAllowed(devid); + if (device) { + result = device->instance_id; + ReleaseAudioDevice(device); + } + return result; +} // this assumes you hold the _physical_ device lock for this logical device! This will not unlock the lock or close the physical device! // It also will not unref the physical device, since we might be shutting down; SDL_CloseAudioDevice handles the unref. static void DestroyLogicalAudioDevice(SDL_LogicalAudioDevice *logdev) @@ -820,6 +834,56 @@ static void SDLCALL SDL_AudioDeviceDisconnected_OnMainThread(void *userdata) UnrefPhysicalAudioDevice(device); } + +static void SDLCALL SDL_AudioDeviceGainChanged_OnMainThread(void *userdata) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userdata; + SDL_assert(device != NULL); + + SDL_PendingAudioDeviceEvent pending; + pending.next = NULL; + SDL_PendingAudioDeviceEvent *pending_tail = &pending; + + ObtainPhysicalAudioDeviceObj(device); + + const SDL_AudioDeviceID devid = device->instance_id; + + if (!devid) { + ReleaseAudioDevice(device); + UnrefPhysicalAudioDevice(device); + return; + } + + SDL_PendingAudioDeviceEvent *p = (SDL_PendingAudioDeviceEvent *)SDL_malloc(sizeof(SDL_PendingAudioDeviceEvent)); + if (p) { + p->type = SDL_EVENT_AUDIO_DEVICE_GAIN_CHANGED; + p->devid = devid; + p->next = NULL; + pending_tail->next = p; + pending_tail = p; + } + + ReleaseAudioDevice(device); + + if (pending.next) { + SDL_LockRWLockForWriting(current_audio.subsystem_rwlock); + SDL_PendingAudioDeviceEvent *tail = current_audio.pending_events_tail; + SDL_assert(tail->next == NULL); + tail->next = pending.next; + current_audio.pending_events_tail = pending_tail; + SDL_UnlockRWLock(current_audio.subsystem_rwlock); + } + UnrefPhysicalAudioDevice(device); +} + +void SDL_AudioDeviceGainChanged(SDL_AudioDevice *device) +{ + if (device) { + RefPhysicalAudioDevice(device); + SDL_RunOnMainThread(SDL_AudioDeviceGainChanged_OnMainThread, device, false); + } +} + void SDL_AudioDeviceDisconnected(SDL_AudioDevice *device) { // lots of risk of various audio backends deadlocking because they're calling @@ -844,6 +908,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 +963,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 +2028,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 +2058,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; diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index 858354a9aa..6c6ec87db9 100644 --- a/src/audio/SDL_sysaudio.h +++ b/src/audio/SDL_sysaudio.h @@ -73,6 +73,9 @@ extern SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, con This can happen due to i/o errors, or a device being unplugged, etc. */ extern void SDL_AudioDeviceDisconnected(SDL_AudioDevice *device); +// Backends can notify applications of physical device gain changes +extern void SDL_AudioDeviceGainChanged(SDL_AudioDevice *device); + // Backends should call this if the system default device changes. extern void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device); @@ -162,12 +165,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; diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c index 2161a5a386..f9b09dda8b 100644 --- a/src/audio/pipewire/SDL_pipewire.c +++ b/src/audio/pipewire/SDL_pipewire.c @@ -27,6 +27,7 @@ #include #include +#include #include /* @@ -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,22 +1094,25 @@ 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); } +static void stream_control_info_callback(void *data, uint32_t id, const struct pw_stream_control *control) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) data; + if (id == SPA_PROP_channelVolumes && control && control->n_values > 0 && control->values) { + const float gain = control->values[0]; + const float old_gain = device->hidden->current_gain; + if (old_gain != gain) { + device->hidden->current_gain = gain; + if (old_gain >= 0) { + SDL_AudioDeviceGainChanged(device); + } + } + } +} + static void stream_state_changed_callback(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) { SDL_AudioDevice *device = (SDL_AudioDevice *) data; @@ -1113,10 +1128,12 @@ static void stream_state_changed_callback(void *data, enum pw_stream_state old, static const struct pw_stream_events stream_output_events = { PW_VERSION_STREAM_EVENTS, .state_changed = stream_state_changed_callback, + .control_info = stream_control_info_callback, .add_buffer = stream_add_buffer_callback, .process = output_callback }; static const struct pw_stream_events stream_input_events = { PW_VERSION_STREAM_EVENTS, .state_changed = stream_state_changed_callback, + .control_info = stream_control_info_callback, .add_buffer = stream_add_buffer_callback, .process = input_callback }; @@ -1202,6 +1219,7 @@ static bool PIPEWIRE_OpenDevice(SDL_AudioDevice *device) } priv = SDL_calloc(1, sizeof(struct SDL_PrivateAudioData)); + priv->current_gain = -1.0f; device->hidden = priv; if (!priv) { return false; @@ -1355,6 +1373,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 +1437,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; } diff --git a/src/audio/pipewire/SDL_pipewire.h b/src/audio/pipewire/SDL_pipewire.h index 0e98ec4ee1..eec355e5e9 100644 --- a/src/audio/pipewire/SDL_pipewire.h +++ b/src/audio/pipewire/SDL_pipewire.h @@ -38,6 +38,8 @@ struct SDL_PrivateAudioData // Set in GetDeviceBuf, filled in AudioThreadIterate, queued in PlayDevice struct pw_buffer *pw_buf; + + float current_gain; }; #endif // SDL_pipewire_h_ diff --git a/src/dynapi/SDL_dynapi.exports b/src/dynapi/SDL_dynapi.exports index 9864557071..fb3faeeac3 100644 --- a/src/dynapi/SDL_dynapi.exports +++ b/src/dynapi/SDL_dynapi.exports @@ -1290,3 +1290,4 @@ _SDL_LoadJPG _SDL_HasSVE2 _SDL_GamepadHasCapSense _SDL_GetGamepadCapSense +_SDL_GetPhysicalAudioDevice diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 3958a52aa6..0f2d6d4b6a 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1291,6 +1291,7 @@ SDL3_0.0.0 { SDL_HasSVE2; SDL_GamepadHasCapSense; SDL_GetGamepadCapSense; + SDL_GetPhysicalAudioDevice; # 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 b54d32ae6d..f90d41dd6d 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1317,3 +1317,4 @@ #define SDL_HasSVE2 SDL_HasSVE2_REAL #define SDL_GamepadHasCapSense SDL_GamepadHasCapSense_REAL #define SDL_GetGamepadCapSense SDL_GetGamepadCapSense_REAL +#define SDL_GetPhysicalAudioDevice SDL_GetPhysicalAudioDevice_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 4f8ac0ba0c..74a5b933b0 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1325,3 +1325,4 @@ SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadJPG,(const char *a),(a),return) SDL_DYNAPI_PROC(bool,SDL_HasSVE2,(void),(),return) SDL_DYNAPI_PROC(bool,SDL_GamepadHasCapSense,(SDL_Gamepad *a,SDL_GamepadCapSenseType b),(a,b),return) SDL_DYNAPI_PROC(bool,SDL_GetGamepadCapSense,(SDL_Gamepad *a,SDL_GamepadCapSenseType b),(a,b),return) +SDL_DYNAPI_PROC(SDL_AudioDeviceID,SDL_GetPhysicalAudioDevice,(SDL_AudioDeviceID a),(a),return)