SDL: audio: Allow channel maps to specify -1 to mute a channel.

From f5ad66ef4b81b8af7c984d429babcdf924eeb759 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Wed, 20 Nov 2024 23:34:28 -0500
Subject: [PATCH] audio: Allow channel maps to specify -1 to mute a channel.

Fixes #11373.
---
 include/SDL3/SDL_audio.h | 22 +++++++++-----
 src/audio/SDL_audiocvt.c | 63 ++++++++++++++++++++++++++++++----------
 2 files changed, 62 insertions(+), 23 deletions(-)

diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h
index e8447e07600a9..3f14d4acfc7af 100644
--- a/include/SDL3/SDL_audio.h
+++ b/include/SDL3/SDL_audio.h
@@ -1189,8 +1189,12 @@ extern SDL_DECLSPEC int * SDLCALL SDL_GetAudioStreamOutputChannelMap(SDL_AudioSt
  * channel that it should be remapped to. To reverse a stereo signal's left
  * and right values, you'd have an array of `{ 1, 0 }`. It is legal to remap
  * multiple channels to the same thing, so `{ 1, 1 }` would duplicate the
- * right channel to both channels of a stereo signal. You cannot change the
- * number of channels through a channel map, just reorder them.
+ * right channel to both channels of a stereo signal. An element in the
+ * channel map set to -1 instead of a valid channel will mute that channel,
+ * setting it to a silence value.
+ *
+ * You cannot change the number of channels through a channel map, just
+ * reorder/mute them.
  *
  * Data that was previously queued in the stream will still be operated on in
  * the order that was current when it was added, which is to say you can put
@@ -1206,7 +1210,7 @@ extern SDL_DECLSPEC int * SDLCALL SDL_GetAudioStreamOutputChannelMap(SDL_AudioSt
  *
  * If `count` is not equal to the current number of channels in the audio
  * stream's format, this will fail. This is a safety measure to make sure a a
- * race condition hasn't changed the format while you this call is setting the
+ * race condition hasn't changed the format while this call is setting the
  * channel map.
  *
  * \param stream the SDL_AudioStream to change.
@@ -1235,12 +1239,16 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamInputChannelMap(SDL_AudioStre
  * The output channel map reorders data that leaving a stream via
  * SDL_GetAudioStreamData.
  *
- * Each item in the array represents an output channel, and its value is the
+ * Each item in the array represents an input channel, and its value is the
  * channel that it should be remapped to. To reverse a stereo signal's left
  * and right values, you'd have an array of `{ 1, 0 }`. It is legal to remap
  * multiple channels to the same thing, so `{ 1, 1 }` would duplicate the
- * right channel to both channels of a stereo signal. You cannot change the
- * number of channels through a channel map, just reorder them.
+ * right channel to both channels of a stereo signal. An element in the
+ * channel map set to -1 instead of a valid channel will mute that channel,
+ * setting it to a silence value.
+ *
+ * You cannot change the number of channels through a channel map, just
+ * reorder/mute them.
  *
  * The output channel map can be changed at any time, as output remapping is
  * applied during SDL_GetAudioStreamData.
@@ -1253,7 +1261,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamInputChannelMap(SDL_AudioStre
  *
  * If `count` is not equal to the current number of channels in the audio
  * stream's format, this will fail. This is a safety measure to make sure a a
- * race condition hasn't changed the format while you this call is setting the
+ * race condition hasn't changed the format while this call is setting the
  * channel map.
  *
  * \param stream the SDL_AudioStream to change.
diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c
index 644df39d5d4e7..4f3750342c46d 100644
--- a/src/audio/SDL_audiocvt.c
+++ b/src/audio/SDL_audiocvt.c
@@ -129,7 +129,7 @@ bool SDL_ChannelMapIsBogus(const int *chmap, int channels)
     if (chmap) {
         for (int i = 0; i < channels; i++) {
             const int mapping = chmap[i];
-            if ((mapping < 0) || (mapping >= channels)) {
+            if ((mapping < -1) || (mapping >= channels)) {
                 return true;
             }
         }
@@ -150,27 +150,60 @@ bool SDL_ChannelMapIsDefault(const int *chmap, int channels)
 }
 
 // Swizzle audio channels. src and dst can be the same pointer. It does not change the buffer size.
-static void SwizzleAudio(const int num_frames, void *dst, const void *src, int channels, const int *map, int bitsize)
+static void SwizzleAudio(const int num_frames, void *dst, const void *src, int channels, const int *map, SDL_AudioFormat fmt)
 {
+    const int bitsize = (int) SDL_AUDIO_BITSIZE(fmt);
+
+    bool has_null_mappings = false;
+    for (int i = 0; i < channels; i++) {
+        if (map[i] == -1) {
+            has_null_mappings = true;
+            break;
+        }
+    }
+
     #define CHANNEL_SWIZZLE(bits) { \
         Uint##bits *tdst = (Uint##bits *) dst; /* treat as UintX; we only care about moving bits and not the type here. */ \
         const Uint##bits *tsrc = (const Uint##bits *) src; \
         if (src != dst) {  /* don't need to copy to a temporary frame first. */ \
-            for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \
-                for (int ch = 0; ch < channels; ch++) { \
-                    tdst[ch] = tsrc[map[ch]]; \
+            if (has_null_mappings) { \
+                const Uint##bits silence = (Uint##bits) SDL_GetSilenceValueForFormat(fmt); \
+                for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \
+                    for (int ch = 0; ch < channels; ch++) { \
+                        const int m = map[ch]; \
+                        tdst[ch] = (m == -1) ? silence : tsrc[m]; \
+                    } \
+                } \
+            } else { \
+                for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \
+                    for (int ch = 0; ch < channels; ch++) { \
+                        tdst[ch] = tsrc[map[ch]]; \
+                    } \
                 } \
             } \
         } else { \
             bool isstack; \
             Uint##bits *tmp = (Uint##bits *) SDL_small_alloc(int, channels, &isstack); /* !!! FIXME: allocate this when setting the channel map instead. */ \
             if (tmp) { \
-                for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \
-                    for (int ch = 0; ch < channels; ch++) { \
-                        tmp[ch] = tsrc[map[ch]]; \
+                if (has_null_mappings) { \
+                    const Uint##bits silence = (Uint##bits) SDL_GetSilenceValueForFormat(fmt); \
+                    for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \
+                        for (int ch = 0; ch < channels; ch++) { \
+                            const int m = map[ch]; \
+                            tmp[ch] = (m == -1) ? silence : tsrc[m]; \
+                        } \
+                        for (int ch = 0; ch < channels; ch++) { \
+                            tdst[ch] = tmp[ch]; \
+                        } \
                     } \
-                    for (int ch = 0; ch < channels; ch++) { \
-                        tdst[ch] = tmp[ch]; \
+                } else { \
+                    for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \
+                        for (int ch = 0; ch < channels; ch++) { \
+                            tmp[ch] = tsrc[map[ch]]; \
+                        } \
+                        for (int ch = 0; ch < channels; ch++) { \
+                            tdst[ch] = tmp[ch]; \
+                        } \
                     } \
                 } \
                 SDL_small_free(tmp, isstack); \
@@ -221,9 +254,7 @@ void ConvertAudio(int num_frames,
     SDL_Log("SDL_AUDIO_CONVERT: Convert format %04x->%04x, channels %u->%u", src_format, dst_format, src_channels, dst_channels);
 #endif
 
-    const int src_bitsize = (int) SDL_AUDIO_BITSIZE(src_format);
     const int dst_bitsize = (int) SDL_AUDIO_BITSIZE(dst_format);
-
     const int dst_sample_frame_size = (dst_bitsize / 8) * dst_channels;
 
     /* Type conversion goes like this now:
@@ -245,7 +276,7 @@ void ConvertAudio(int num_frames,
     // swizzle input to "standard" format if necessary.
     if (src_map) {
         void* buf = scratch ? scratch : dst;  // use scratch if available, since it has to be big enough to hold src, unless it's NULL, then dst has to be.
-        SwizzleAudio(num_frames, buf, src, src_channels, src_map, src_bitsize);
+        SwizzleAudio(num_frames, buf, src, src_channels, src_map, src_format);
         src = buf;
     }
 
@@ -254,7 +285,7 @@ void ConvertAudio(int num_frames,
         if (src_format == dst_format) {
             // nothing to do, we're already in the right format, just copy it over if necessary.
             if (dst_map) {
-                SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_bitsize);
+                SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_format);
             } else if (src != dst) {
                 SDL_memcpy(dst, src, num_frames * dst_sample_frame_size);
             }
@@ -264,7 +295,7 @@ void ConvertAudio(int num_frames,
         // just a byteswap needed?
         if ((src_format ^ dst_format) == SDL_AUDIO_MASK_BIG_ENDIAN) {
             if (dst_map) {  // do this first, in case we duplicate channels, we can avoid an extra copy if src != dst.
-                SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_bitsize);
+                SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_format);
                 src = dst;
             }
             ConvertAudioSwapEndian(dst, src, num_frames * dst_channels, dst_bitsize);
@@ -348,7 +379,7 @@ void ConvertAudio(int num_frames,
     SDL_assert(src == dst);  // if we got here, we _had_ to have done _something_. Otherwise, we should have memcpy'd!
 
     if (dst_map) {
-        SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_bitsize);
+        SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_format);
     }
 }