SDL: audio: Add gain support to audio streams and logical audio devices.

From 2a8f1e11caf31790e1fc0efb1edc541366266085 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 1 Jul 2024 15:08:20 -0400
Subject: [PATCH] audio: Add gain support to audio streams and logical audio
 devices.

Fixes #10028.
---
 include/SDL3/SDL_audio.h          | 105 ++++++++++++++++++++++++++++++
 src/audio/SDL_audio.c             |  60 ++++++++++++++---
 src/audio/SDL_audiocvt.c          |  78 +++++++++++++++++++---
 src/audio/SDL_audioqueue.c        |  10 +--
 src/audio/SDL_audioqueue.h        |   2 +-
 src/audio/SDL_sysaudio.h          |  10 ++-
 src/dynapi/SDL_dynapi.sym         |   4 ++
 src/dynapi/SDL_dynapi_overrides.h |   4 ++
 src/dynapi/SDL_dynapi_procs.h     |   4 ++
 test/testaudio.c                  |  35 +++++++++-
 10 files changed, 286 insertions(+), 26 deletions(-)

diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h
index c3bf5e94f7796..8ee76d166aa93 100644
--- a/include/SDL3/SDL_audio.h
+++ b/include/SDL3/SDL_audio.h
@@ -718,6 +718,62 @@ extern SDL_DECLSPEC int SDLCALL SDL_ResumeAudioDevice(SDL_AudioDeviceID dev);
  */
 extern SDL_DECLSPEC SDL_bool SDLCALL SDL_AudioDevicePaused(SDL_AudioDeviceID dev);
 
+/**
+ * Get the gain of an audio device.
+ *
+ * The gain of a device is its volume; a larger gain means a louder output,
+ * with a gain of zero being silence.
+ *
+ * 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.
+ *
+ * \param devid the audio device to query.
+ * \returns the gain of the device, or -1.0f on error.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_SetAudioDeviceGain
+ */
+extern SDL_DECLSPEC float SDLCALL SDL_GetAudioDeviceGain(SDL_AudioDeviceID devid);
+
+/**
+ * Change the gain of an audio device.
+ *
+ * The gain of a device is its volume; a larger gain means a louder output,
+ * with a gain of zero being silence.
+ *
+ * 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 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.
+ *
+ * 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.
+ * \returns 0 on success, or -1 on error.
+ *
+ * \threadsafety It is safe to call this function from any thread, as it holds
+ *               a stream-specific mutex while running.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_GetAudioDeviceGain
+ */
+extern SDL_DECLSPEC int SDLCALL SDL_SetAudioDeviceGain(SDL_AudioDeviceID devid, float gain);
+
 /**
  * Close a previously-opened audio device.
  *
@@ -981,6 +1037,51 @@ extern SDL_DECLSPEC float SDLCALL SDL_GetAudioStreamFrequencyRatio(SDL_AudioStre
  */
 extern SDL_DECLSPEC int SDLCALL SDL_SetAudioStreamFrequencyRatio(SDL_AudioStream *stream, float ratio);
 
+/**
+ * Get the gain of an audio stream.
+ *
+ * The gain of a stream is its volume; a larger gain means a louder output,
+ * with a gain of zero being silence.
+ *
+ * Audio streams default to a gain of 1.0f (no change in output).
+ *
+ * \param stream the SDL_AudioStream to query.
+ * \returns the gain of the stream, or -1.0f on error.
+ *
+ * \threadsafety It is safe to call this function from any thread, as it holds
+ *               a stream-specific mutex while running.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_SetAudioStreamGain
+ */
+extern SDL_DECLSPEC float SDLCALL SDL_GetAudioStreamGain(SDL_AudioStream *stream);
+
+/**
+ * Change the gain of an audio stream.
+ *
+ * The gain of a stream is its volume; a larger gain means a louder output,
+ * with a gain of zero being silence.
+ *
+ * Audio streams default to a gain of 1.0f (no change in output).
+ *
+ * This is applied during SDL_GetAudioStreamData, and can be continuously
+ * changed to create various effects.
+ *
+ * \param stream the stream on which the gain is being changed.
+ * \param gain the gain. 1.0f is no change, 0.0f is silence.
+ * \returns 0 on success, or -1 on error.
+ *
+ * \threadsafety It is safe to call this function from any thread, as it holds
+ *               a stream-specific mutex while running.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_GetAudioStreamGain
+ */
+extern SDL_DECLSPEC int SDLCALL SDL_SetAudioStreamGain(SDL_AudioStream *stream, float gain);
+
+
 /**
  * Add data to the stream.
  *
@@ -1465,6 +1566,10 @@ extern SDL_DECLSPEC SDL_AudioStream *SDLCALL SDL_OpenAudioDeviceStream(SDL_Audio
  * changed. However, this only covers frequency and channel count; data is
  * always provided here in SDL_AUDIO_F32 format.
  *
+ * The postmix callback runs _after_ logical device gain and audiostream gain
+ * have been applied, which is to say you can make the output data louder
+ * at this point than the gain settings would suggest.
+ *
  * \param userdata a pointer provided by the app through
  *                 SDL_SetAudioPostmixCallback, for its own use.
  * \param spec the current format of audio that is to be submitted to the
diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index 0c1edacdae3ac..4a3c7281f6aca 100644
--- a/src/audio/SDL_audio.c
+++ b/src/audio/SDL_audio.c
@@ -1103,7 +1103,7 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
             // We should have updated this elsewhere if the format changed!
             SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec));
 
-            const int br = SDL_AtomicGet(&logdev->paused) ? 0 : SDL_GetAudioStreamData(stream, device_buffer, buffer_size);
+            const int br = SDL_AtomicGet(&logdev->paused) ? 0 : SDL_GetAudioStreamDataAdjustGain(stream, device_buffer, buffer_size, logdev->gain);
             if (br < 0) {  // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow.
                 failed = SDL_TRUE;
                 SDL_memset(device_buffer, device->silence_value, buffer_size);  // just supply silence to the device before we die.
@@ -1143,7 +1143,7 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
                        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, work_buffer_size);
+                    const int br = SDL_GetAudioStreamDataAdjustGain(stream, device->work_buffer, work_buffer_size, logdev->gain);
                     if (br < 0) {  // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow.
                         failed = SDL_TRUE;
                         break;
@@ -1161,8 +1161,8 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
 
             if (((Uint8 *) final_mix_buffer) != device_buffer) {
                 // !!! FIXME: we can't promise the device buf is aligned/padded for SIMD.
-                //ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device_buffer, device->spec.format, device->spec.channels, NULL, NULL);
-                ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device->work_buffer, device->spec.format, device->spec.channels, NULL, NULL);
+                //ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device_buffer, device->spec.format, device->spec.channels, NULL, NULL, 1.0f);
+                ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device->work_buffer, device->spec.format, device->spec.channels, NULL, NULL, 1.0f);
                 SDL_memcpy(device_buffer, device->work_buffer, buffer_size);
             }
         }
@@ -1250,7 +1250,7 @@ SDL_bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device)
                 void *output_buffer = device->work_buffer;
 
                 // I don't know why someone would want a postmix on a recording device, but we offer it for API consistency.
-                if (logdev->postmix) {
+                if (logdev->postmix || (logdev->gain != 1.0f)) {
                     // move to float format.
                     SDL_AudioSpec outspec;
                     SDL_copyp(&outspec, &device->spec);
@@ -1258,13 +1258,15 @@ SDL_bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device)
                     output_buffer = device->postmix_buffer;
                     const int frames = br / SDL_AUDIO_FRAMESIZE(device->spec);
                     br = frames * SDL_AUDIO_FRAMESIZE(outspec);
-                    ConvertAudio(frames, device->work_buffer, device->spec.format, outspec.channels, NULL, device->postmix_buffer, SDL_AUDIO_F32, outspec.channels, NULL, NULL);
-                    logdev->postmix(logdev->postmix_userdata, &outspec, device->postmix_buffer, br);
+                    ConvertAudio(frames, device->work_buffer, device->spec.format, outspec.channels, NULL, device->postmix_buffer, SDL_AUDIO_F32, outspec.channels, NULL, NULL, logdev->gain);
+                    if (logdev->postmix) {
+                        logdev->postmix(logdev->postmix_userdata, &outspec, device->postmix_buffer, br);
+                    }
                 }
 
                 for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
                     // We should have updated this elsewhere if the format changed!
-                    SDL_assert(stream->src_spec.format == (logdev->postmix ? SDL_AUDIO_F32 : device->spec.format));
+                    SDL_assert(stream->src_spec.format == ((logdev->postmix || (logdev->gain != 1.0f)) ? SDL_AUDIO_F32 : device->spec.format));
                     SDL_assert(stream->src_spec.channels == device->spec.channels);
                     SDL_assert(stream->src_spec.freq == device->spec.freq);
 
@@ -1695,6 +1697,7 @@ SDL_AudioDeviceID SDL_OpenAudioDevice(SDL_AudioDeviceID devid, const SDL_AudioSp
             SDL_AtomicSet(&logdev->paused, 0);
             retval = logdev->instance_id = AssignAudioDeviceInstanceId(device->recording, /*islogical=*/SDL_TRUE);
             logdev->physical_device = device;
+            logdev->gain = 1.0f;
             logdev->opened_as_default = wants_default;
             logdev->next = device->logical_devices;
             if (device->logical_devices) {
@@ -1752,6 +1755,44 @@ SDL_bool SDL_AudioDevicePaused(SDL_AudioDeviceID devid)
     return retval;
 }
 
+float SDL_GetAudioDeviceGain(SDL_AudioDeviceID devid)
+{
+    SDL_AudioDevice *device = NULL;
+    SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device);
+    const float retval = logdev ? logdev->gain : -1.0f;
+    ReleaseAudioDevice(device);
+    return retval;
+}
+
+int SDL_SetAudioDeviceGain(SDL_AudioDeviceID devid, float gain)
+{
+    if (gain < 0.0f) {
+        return SDL_InvalidParamError("gain");
+    }
+
+    SDL_AudioDevice *device = NULL;
+    SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device);
+    int retval = -1;
+    if (logdev) {
+        logdev->gain = gain;
+        if (device->recording) {
+            const SDL_bool need_float32 = (logdev->postmix || logdev->gain != 1.0f);
+            for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
+                // set the proper end of the stream to the device's format.
+                // SDL_SetAudioStreamFormat does a ton of validation just to memcpy an audiospec.
+                SDL_LockMutex(stream->lock);
+                stream->src_spec.format = need_float32 ? SDL_AUDIO_F32 : device->spec.format;
+                SDL_UnlockMutex(stream->lock);
+            }
+        }
+
+        UpdateAudioStreamFormatsPhysical(device);
+        retval = 0;
+    }
+    ReleaseAudioDevice(device);
+    return retval;
+}
+
 int SDL_SetAudioPostmixCallback(SDL_AudioDeviceID devid, SDL_AudioPostmixCallback callback, void *userdata)
 {
     SDL_AudioDevice *device = NULL;
@@ -1770,11 +1811,12 @@ int SDL_SetAudioPostmixCallback(SDL_AudioDeviceID devid, SDL_AudioPostmixCallbac
             logdev->postmix_userdata = userdata;
 
             if (device->recording) {
+                const SDL_bool need_float32 = (callback || logdev->gain != 1.0f);
                 for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
                     // set the proper end of the stream to the device's format.
                     // SDL_SetAudioStreamFormat does a ton of validation just to memcpy an audiospec.
                     SDL_LockMutex(stream->lock);
-                    stream->src_spec.format = callback ? SDL_AUDIO_F32 : device->spec.format;
+                    stream->src_spec.format = need_float32 ? SDL_AUDIO_F32 : device->spec.format;
                     SDL_UnlockMutex(stream->lock);
                 }
             }
diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c
index e05c88ee650b1..29cd63dc6d4db 100644
--- a/src/audio/SDL_audiocvt.c
+++ b/src/audio/SDL_audiocvt.c
@@ -194,10 +194,14 @@ static void SwizzleAudio(const int num_frames, void *dst, const void *src, int c
 //
 // The scratch buffer must be able to store `num_frames * CalculateMaxSampleFrameSize(src_format, src_channels, dst_format, dst_channels)` bytes.
 // If the scratch buffer is NULL, this restriction applies to the output buffer instead.
+//
+// Since this is a convenient point that audio goes through even if it doesn't need format conversion,
+// we also handle gain adjustment here, so we don't have to make another pass over the data later.
+// Strictly speaking, this is also a "conversion".  :)
 void ConvertAudio(int num_frames,
                   const void *src, SDL_AudioFormat src_format, int src_channels, const Uint8 *src_map,
                   void *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
-                  void* scratch)
+                  void* scratch, float gain)
 {
     SDL_assert(src != NULL);
     SDL_assert(dst != NULL);
@@ -243,7 +247,7 @@ void ConvertAudio(int num_frames,
     }
 
     // see if we can skip float conversion entirely.
-    if (src_channels == dst_channels) {
+    if ((src_channels == dst_channels) && (gain == 1.0f)) {
         if (src_format == dst_format) {
             // nothing to do, we're already in the right format, just copy it over if necessary.
             if (dst_map) {
@@ -280,6 +284,23 @@ void ConvertAudio(int num_frames,
         src = buf;
     }
 
+    // Gain adjustment
+    if (gain != 1.0f) {
+        float *buf = (float *)(dstconvert ? scratch : dst);
+        const int total_samples = num_frames * src_channels;
+        if (src == buf) {
+            for (int i = 0; i < total_samples; i++) {
+                buf[i] *= gain;
+            }
+        } else {
+            float *fsrc = (float *)src;
+            for (int i = 0; i < total_samples; i++) {
+                buf[i] = fsrc[i] * gain;
+            }
+        }
+        src = buf;
+    }
+
     // Channel conversion
 
     if (channelconvert) {
@@ -379,6 +400,7 @@ SDL_AudioStream *SDL_CreateAudioStream(const SDL_AudioSpec *src_spec, const SDL_
     }
 
     retval->freq_ratio = 1.0f;
+    retval->gain = 1.0f;
     retval->queue = SDL_CreateAudioQueue(8192);
 
     if (!retval->queue) {
@@ -593,6 +615,35 @@ int SDL_SetAudioStreamFrequencyRatio(SDL_AudioStream *stream, float freq_ratio)
     return 0;
 }
 
+float SDL_GetAudioStreamGain(SDL_AudioStream *stream)
+{
+    if (!stream) {
+        SDL_InvalidParamError("stream");
+        return -1.0f;
+    }
+
+    SDL_LockMutex(stream->lock);
+    const float gain = stream->gain;
+    SDL_UnlockMutex(stream->lock);
+
+    return gain;
+}
+
+int SDL_SetAudioStreamGain(SDL_AudioStream *stream, float gain)
+{
+    if (!stream) {
+        return SDL_InvalidParamError("stream");
+    } else if (gain < 0.0f) {
+        return SDL_InvalidParamError("gain");
+    }
+
+    SDL_LockMutex(stream->lock);
+    stream->gain = gain;
+    SDL_UnlockMutex(stream->lock);
+
+    return 0;
+}
+
 static int CheckAudioStreamIsFullySetup(SDL_AudioStream *stream)
 {
     if (stream->src_spec.format == 0) {
@@ -820,7 +871,7 @@ static Sint64 GetAudioStreamHead(SDL_AudioStream* stream, SDL_AudioSpec* out_spe
 
 // You must hold stream->lock and validate your parameters before calling this!
 // Enough input data MUST be available!
-static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int output_frames)
+static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int output_frames, float gain)
 {
     const SDL_AudioSpec* src_spec = &stream->input_spec;
     const SDL_AudioSpec* dst_spec = &stream->dst_spec;
@@ -854,7 +905,7 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
             }
         }
 
-        if (SDL_ReadFromAudioQueue(stream->queue, buf, dst_format, dst_channels, dst_map, 0, output_frames, 0, work_buffer) != buf) {
+        if (SDL_ReadFromAudioQueue(stream->queue, buf, dst_format, dst_channels, dst_map, 0, output_frames, 0, work_buffer, gain) != buf) {
             return SDL_SetError("Not enough data in queue");
         }
 
@@ -919,10 +970,15 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
         return -1;
     }
 
+    // adjust gain either before resampling or after, depending on which point has less
+    // samples to process.
+    const float preresample_gain = (input_frames > output_frames) ? 1.0f : gain;
+    const float postresample_gain = (input_frames > output_frames) ? gain : 1.0f;
+
     // (dst channel map is NULL because we'll do the final swizzle on ConvertAudio after resample.)
     const Uint8* input_buffer = SDL_ReadFromAudioQueue(stream->queue,
         NULL, resample_format, resample_channels, NULL,
-        padding_frames, input_frames, padding_frames, work_buffer);
+        padding_frames, input_frames, padding_frames, work_buffer, preresample_gain);
 
     if (!input_buffer) {
         return SDL_SetError("Not enough data in queue (resample)");
@@ -939,13 +995,13 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
                   resample_rate, &stream->resample_offset);
 
     // Convert to the final format, if necessary (src channel map is NULL because SDL_ReadFromAudioQueue already handled this).
-    ConvertAudio(output_frames, resample_buffer, resample_format, resample_channels, NULL, buf, dst_format, dst_channels, dst_map, work_buffer);
+    ConvertAudio(output_frames, resample_buffer, resample_format, resample_channels, NULL, buf, dst_format, dst_channels, dst_map, work_buffer, postresample_gain);
 
     return 0;
 }
 
 // get converted/resampled data from the stream
-int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
+int SDL_GetAudioStreamDataAdjustGain(SDL_AudioStream *stream, void *voidbuf, int len, float extra_gain)
 {
     Uint8 *buf = (Uint8 *) voidbuf;
 
@@ -970,6 +1026,7 @@ int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
         return -1;
     }
 
+    const float gain = stream->gain * extra_gain;
     const int dst_frame_size = SDL_AUDIO_FRAMESIZE(stream->dst_spec);
 
     len -= len % dst_frame_size;  // chop off any fractional sample frame.
@@ -1029,7 +1086,7 @@ int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
         output_frames = SDL_min(output_frames, chunk_size);
         output_frames = (int) SDL_min(output_frames, available_frames);
 
-        if (GetAudioStreamDataInternal(stream, &buf[total], output_frames) != 0) {
+        if (GetAudioStreamDataInternal(stream, &buf[total], output_frames, gain) != 0) {
             total = total ? total : -1;
             break;
         }
@@ -1046,6 +1103,11 @@ int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
     return total;
 }
 
+int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len)
+{
+    return SDL_GetAudioStreamDataAdjustGain(stream, voidbuf, len, 1.0f);
+}
+
 // number of converted/resampled bytes available for output
 int SDL_GetAudioStreamAvailable(SDL_AudioStream *stream)
 {
diff --git a/src/audio/SDL_audioqueue.c b/src/audio/SDL_audioqueue.c
index 41c8af3b456c7..e9110b5ac05f2 100644
--- a/src/audio/SDL_audioqueue.c
+++ b/src/audio/SDL_audioqueue.c
@@ -514,7 +514,7 @@ static const Uint8 *PeekIntoAudioQueueFuture(SDL_AudioQueue *queue, Uint8 *data,
 const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
                                     Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
                                     int past_frames, int present_frames, int future_frames,
-                                    Uint8 *scratch)
+                                    Uint8 *scratch, float gain)
 {
     SDL_AudioTrack *track = queue->head;
 
@@ -552,7 +552,7 @@ const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
         // Do we still need to copy/convert the data?
         if (dst) {
             ConvertAudio(past_frames + present_frames + future_frames, ptr,
-                         src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
+                         src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
             ptr = dst;
         }
 
@@ -570,19 +570,19 @@ const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
     Uint8 *ptr = dst;
 
     if (src_past_bytes) {
-        ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
+        ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
         dst += dst_past_bytes;
         scratch += dst_past_bytes;
     }
 
     if (src_present_bytes) {
-        ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
+        ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
         dst += dst_present_bytes;
         scratch += dst_present_bytes;
     }
 
     if (src_future_bytes) {
-        ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
+        ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain);
         dst += dst_future_bytes;
         scratch += dst_future_bytes;
     }
diff --git a/src/audio/SDL_audioqueue.h b/src/audio/SDL_audioqueue.h
index 26675ce295761..46dce2d19b659 100644
--- a/src/audio/SDL_audioqueue.h
+++ b/src/audio/SDL_audioqueue.h
@@ -69,7 +69,7 @@ size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_Audi
 const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
                                     Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
                                     int past_frames, int present_frames, int future_frames,
-                                    Uint8 *scratch);
+                                    Uint8 *scratch, float gain);
 
 // Get the total number of bytes currently queued
 size_t SDL_GetAudioQueueQueued(SDL_AudioQueue *queue);
diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h
index 9973be9bf115c..b5c2335e2ca07 100644
--- a/src/audio/SDL_sysaudio.h
+++ b/src/audio/SDL_sysaudio.h
@@ -118,17 +118,19 @@ extern SDL_bool SDL_ChannelMapIsBogus(const Uint8 *map, int channels);
 extern void ConvertAudio(int num_frames,
                          const void *src, SDL_AudioFormat src_format, int src_channels, const Uint8 *src_map,
                          void *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
-                         void* scratch);
+                         void* scratch, float gain);
 
 // Compare two SDL_AudioSpecs, return SDL_TRUE if they match exactly.
 // Using SDL_memcmp directly isn't safe, since potential padding (and unused parts of the channel map) might not be initialized.
 extern SDL_bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b);
 
-
 // Special case to let something in SDL_audiocvt.c access something in SDL_audio.c. Don't use this.
 extern void OnAudioStreamCreated(SDL_AudioStream *stream);
 extern void OnAudioStreamDestroy(SDL_AudioStream *stream);
 
+// This just lets audio playback apply logical device gain at the same time as audiostream gain, so it's one multiplication instead of thousands.
+extern int SDL_GetAudioStreamDataAdjustGain(SDL_AudioStream *stream, void *voidbuf, int len, float extra_gain);
+
 typedef struct SDL_AudioDriverImpl
 {
     void (*DetectDevices)(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording);
@@ -196,6 +198,7 @@ struct SDL_AudioStream
     SDL_AudioSpec src_spec;
     SDL_AudioSpec dst_spec;
     float freq_ratio;
+    float gain;
 
     struct SDL_AudioQueue* queue;
 
@@ -230,6 +233,9 @@ struct SDL_LogicalAudioDevice
     // If whole logical device is paused (process no streams bound to this device).
     SDL_AtomicInt paused;
 
+    // Volume of the device output.
+    float gain;
+
     // double-linked list of all audio streams currently bound to this opened device.
     SDL_AudioStream *bound_streams;
 
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 40d7177a8ae7a..1f5f8ca5429b7 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -167,6 +167,7 @@ SDL3_0.0.0 {
     SDL_GetAssertionHandler;
     SDL_GetAssertionReport;
     SDL_GetAudioDeviceFormat;
+    SDL_GetAudioDeviceGain;
     SDL_GetAudioDeviceName;
     SDL_GetAudioDriver;
     SDL_GetAudioPlaybackDevices;
@@ -176,6 +177,7 @@ SDL3_0.0.0 {
     SDL_GetAudioStreamDevice;
     SDL_GetAudioStreamFormat;
     SDL_GetAudioStreamFrequencyRatio;
+    SDL_GetAudioStreamGain;
     SDL_GetAudioStreamProperties;
     SDL_GetAudioStreamQueued;
     SDL_GetBasePath;
@@ -682,9 +684,11 @@ SDL3_0.0.0 {
     SDL_SendJoystickEffect;
     SDL_SendJoystickVirtualSensorData;
     SDL_SetAssertionHandler;
+    SDL_SetAudioDeviceGain;
     SDL_SetAudioPostmixCallback;
     SDL_SetAudioStreamFormat;
     SDL_SetAudioStreamFrequencyRatio;
+    SDL_SetAudioStreamGain;
     SDL_SetAudioStreamGetCallback;
     SDL_SetAudioStreamPutCallback;
     SDL_SetBooleanProperty;
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 76f69d781b6ff..36fec9b6485b1 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -191,6 +191,7 @@
 #define SDL_GetAndroidSDKVersion SDL_GetAndroidSDKVersion_REAL
 #define SDL_GetAssertionHandler SDL_GetAssertionHandler_REAL
 #define SDL_GetAssertionReport SDL_GetAssertionReport_REAL
+#define SDL_GetAudioDeviceGain SDL_GetAudioDeviceGain_REAL
 #define SDL_GetAudioDeviceFormat SDL_GetAudioDeviceFormat_REAL
 #define SDL_GetAudioDeviceName SDL_GetAudioDeviceName_REAL
 #define SDL_GetAudioDriver SDL_GetAudioDriver_REAL
@@ -201,6 +202,7 @@
 #define SDL_GetAudioStreamDevice SDL_GetAudioStreamDevice_REAL
 #define SDL_GetAudioStreamFormat SDL_GetAudioStreamFormat_REAL
 #define SDL_GetAudioStreamFrequencyRatio SDL_GetAudioStreamFrequencyRatio_REAL
+#define SDL_GetAudioStreamGain SDL_GetAudioStreamGain_REAL
 #define SDL_GetAudioStreamProperties SDL_GetAudioStreamProperties_REAL
 #define SDL_GetAudioStreamQueued SDL_GetAudioStreamQueued_REAL
 #define SDL_GetBasePath SDL_GetBasePath_REAL
@@ -707,9 +709,11 @@
 #define SDL_SendJoystickEffect SDL_SendJoystickEffect_REAL
 #define SDL_SendJoystickVirtualSensorData SDL_SendJoystickVirtualSensorData_REAL
 #define SDL_SetAssertionHandler SDL_SetAssertionHandler_REAL
+#define SDL_SetAudioDeviceGain SDL_SetAudioDeviceGain_REAL
 #define SDL_SetAudioPostmixCallback SDL_SetAudioPostmixCallback_REAL
 #define SDL_SetAudioStreamFormat SDL_SetAudioStreamFormat_REAL
 #define SDL_SetAudioStreamFrequencyRatio SDL_SetAudioStreamFrequencyRatio_REAL
+#define SDL_SetAudioStreamGain SDL_SetAudioStreamGain_REAL
 #define SDL_SetAudioStreamGetCallback SDL_SetAudioStreamGetCallback_REAL
 #define SDL_SetAudioStreamPutCallback SDL_SetAudioStreamPutCallback_REAL
 #define SDL_SetBooleanProperty SDL_SetBooleanProperty_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 50613fd993636..16c54b365896a 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -212,6 +212,7 @@ SDL_DYNAPI_PROC(int,SDL_GetAndroidSDKVersion,(void),(),return)
 SDL_DYNAPI_PROC(SDL_AssertionHandler,SDL_GetAssertionHandler,(void **a),(a),return)
 SDL_DYNAPI_PROC(const SDL_AssertData*,SDL_GetAssertionReport,(void),(),return)
 SDL_DYNAPI_PROC(int,SDL_GetAudioDeviceFormat,(SDL_AudioDeviceID a, SDL_AudioSpec *b, int *c),(a,b,c),return)
+SDL_DYNAPI_PROC(float,SDL_GetAudioDeviceGain,(SDL_AudioDeviceID a),(a),return)
 SDL_DYNAPI_PROC(const char*,SDL_GetAudioDeviceName,(SDL_AudioDeviceID a),(a),return)
 SDL_DYNAPI_PROC(const char*,SDL_GetAudioDriver,(int a),(a),return)
 SDL_DYNAPI_PROC(SDL_AudioDeviceID*,SDL_GetAudioPlaybackDevices,(int *a),(a),return)
@@ -221,6 +222,7 @@ SDL_DYNAPI_PROC(int,SDL_GetAudioStreamData,(SDL_AudioStream *a, void *b, int c),
 SDL_DYNAPI_PROC(SDL_AudioDeviceID,SDL_GetAudioStreamDevice,(SDL_AudioStream *a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_GetAudioStreamFormat,(SDL_AudioStream *a, SDL_AudioSpec *b, SDL_AudioSpec *c),(a,b,c),return)
 SDL_DYNAPI_PROC(float,SDL_GetAudioStreamFrequencyRatio,(SDL_AudioStream *a),(a),return)
+SDL_DYNAPI_PROC(float,SDL_GetAudioStreamGain,(SDL_AudioStream *a),(a),return)
 SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetAudioStreamProperties,(SDL_AudioStream *a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_GetAudioStreamQueued,(SDL_AudioStream *a),(a),return)
 SDL_DYNAPI_PROC(char*,SDL_GetBasePath,(void),(),return)
@@ -718,9 +720,11 @@ SDL_DYNAPI_PROC(int,SDL_SendGamepadEffect,(SDL_Gamepad *a, const void *b, int c)
 SDL_DYNAPI_PROC(int,SDL_SendJoystickEffect,(SDL_Joystick *a, const void 

(Patch may be truncated, please check the link at the top of this post.)