SDL: audio: Add channel remapping to SDL_AudioSpec and SDL_AudioStream.

From 16e7fdc4f2a76515bafa594c6c19ca3897fc396c Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Wed, 3 Jul 2024 03:19:00 -0400
Subject: [PATCH] audio: Add channel remapping to SDL_AudioSpec and
 SDL_AudioStream.

Fixes #8367.
---
 include/SDL3/SDL_audio.h              |  56 ++++++++--
 src/audio/SDL_audio.c                 |  33 ++++--
 src/audio/SDL_audiocvt.c              | 149 ++++++++++++++++++--------
 src/audio/SDL_audioqueue.c            |  17 ++-
 src/audio/SDL_audioqueue.h            |   2 +-
 src/audio/SDL_sysaudio.h              |  16 ++-
 src/audio/alsa/SDL_alsa_audio.c       | 144 ++++++-------------------
 src/audio/pulseaudio/SDL_pulseaudio.c |   1 +
 src/audio/qnx/SDL_qsa_audio.c         |   1 +
 test/testaudiostreamdynamicresample.c |   1 +
 test/testautomation_audio.c           |  23 +++-
 test/testffmpeg.c                     |   4 +-
 12 files changed, 254 insertions(+), 193 deletions(-)

diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h
index 3f8b9d918db86..f2e7c0916bd59 100644
--- a/include/SDL3/SDL_audio.h
+++ b/include/SDL3/SDL_audio.h
@@ -43,20 +43,41 @@
  * if you aren't reading from a file) as a basic means to load sound data into
  * your program.
  *
- * For multi-channel audio, data is interleaved (one sample for each channel,
- * then repeat). The SDL channel order is:
- *
- * - Stereo: FL, FR
- * - 2.1 surround: FL, FR, LFE
- * - Quad: FL, FR, BL, BR
- * - 4.1 surround: FL, FR, LFE, BL, BR
- * - 5.1 surround: FL, FR, FC, LFE, SL, SR (last two can also be BL BR)
- * - 6.1 surround: FL, FR, FC, LFE, BC, SL, SR
- * - 7.1 surround: FL, FR, FC, LFE, BL, BR, SL, SR
+ * ## Channel layouts as SDL expects them
+ *
+ * Abbreviations:
+ *
+ * - FRONT = single mono speaker
+ * - FL = front left speaker
+ * - FR = front right speaker
+ * - FC = front center speaker
+ * - BL = back left speaker
+ * - BR = back right speaker
+ * - SR = surround right speaker
+ * - SL = surround left speaker
+ * - BC = back center speaker
+ * - LFE = low-frequency speaker
+ *
+ * These are listed in the order they are laid out in
+ * memory, so "FL, FR" means "the front left speaker is
+ * laid out in memory first, then the front right, then
+ * it repeats for the next audio frame".
+ *
+ * - 1 channel (mono) layout: FRONT
+ * - 2 channels (stereo) layout: FL, FR
+ * - 3 channels (2.1) layout: FL, FR, LFE
+ * - 4 channels (quad) layout: FL, FR, BL, BR
+ * - 5 channels (4.1) layout: FL, FR, LFE, BL, BR
+ * - 6 channels (5.1) layout: FL, FR, FC, LFE, BL, BR  (last two can also be BL, BR)
+ * - 7 channels (6.1) layout: FL, FR, FC, LFE, BC, SL, SR
+ * - 8 channels (7.1) layout: FL, FR, FC, LFE, BL, BR, SL, SR
  *
  * This is the same order as DirectSound expects, but applied to all
  * platforms; SDL will swizzle the channels as necessary if a platform expects
  * something different.
+ *
+ * SDL_AudioStream can also be provided a channel map to change this ordering
+ * to whatever is necessary, in other audio processing scenarios.
  */
 
 #ifndef SDL_audio_h_
@@ -280,6 +301,18 @@ typedef Uint32 SDL_AudioDeviceID;
  */
 #define SDL_AUDIO_DEVICE_DEFAULT_RECORDING ((SDL_AudioDeviceID) 0xFFFFFFFE)
 
+/**
+ * Maximum channels that an SDL_AudioSpec channel map can handle.
+ *
+ * This is (currently) double the number of channels that SDL supports,
+ * to allow for future expansion while maintaining binary compatibility.
+ *
+ * \since This macro is available since SDL 3.0.0.
+ *
+ * \sa SDL_AudioSpec
+ */
+#define SDL_MAX_CHANNEL_MAP_SIZE 16
+
 /**
  * Format specifier for audio data.
  *
@@ -292,6 +325,8 @@ typedef struct SDL_AudioSpec
     SDL_AudioFormat format;     /**< Audio data format */
     int channels;               /**< Number of channels: 1 mono, 2 stereo, etc */
     int freq;                   /**< sample rate: sample frames per second */
+    SDL_bool use_channel_map;   /**< If SDL_FALSE, ignore `channel_map` and use default order. */
+    Uint8 channel_map[SDL_MAX_CHANNEL_MAP_SIZE];      /**< `channels` items of channel order. */
 } SDL_AudioSpec;
 
 /**
@@ -318,6 +353,7 @@ typedef struct SDL_AudioSpec
  *   when it doesn't have the complete buffer available.
  * - It can handle incoming data in any variable size.
  * - It can handle input/output format changes on the fly.
+ * - It can remap audio channels between inputs and outputs.
  * - You push data as you have it, and pull it when you need it
  * - It can also function as a basic audio data queue even if you just have
  *   sound that needs to pass from one place to another.
diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index 5194858bf700a..0c1edacdae3ac 100644
--- a/src/audio/SDL_audio.c
+++ b/src/audio/SDL_audio.c
@@ -249,6 +249,16 @@ static void UpdateAudioStreamFormatsPhysical(SDL_AudioDevice *device)
     }
 }
 
+SDL_bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b)
+{
+    if ((a->format != b->format) || (a->channels != b->channels) || (a->freq != b->freq) || (a->use_channel_map != b->use_channel_map)) {
+        return SDL_FALSE;
+    } else if (a->use_channel_map && (SDL_memcmp(a->channel_map, b->channel_map, sizeof (a->channel_map[0]) * a->channels) != 0)) {
+        return SDL_FALSE;
+    }
+    return SDL_TRUE;
+}
+
 
 // Zombie device implementation...
 
@@ -632,11 +642,13 @@ SDL_AudioDevice *SDL_AddAudioDevice(SDL_bool recording, const char *name, const
     const int default_freq = recording ? DEFAULT_AUDIO_RECORDING_FREQUENCY : DEFAULT_AUDIO_PLAYBACK_FREQUENCY;
 
     SDL_AudioSpec spec;
+    SDL_zero(spec);
     if (!inspec) {
         spec.format = default_format;
         spec.channels = default_channels;
         spec.freq = default_freq;
     } else {
+        SDL_assert(!inspec->use_channel_map);  // backends shouldn't set a channel map here! Set it when opening the device!
         spec.format = (inspec->format != 0) ? inspec->format : default_format;
         spec.channels = (inspec->channels != 0) ? inspec->channels : default_channels;
         spec.freq = (inspec->freq != 0) ? inspec->freq : default_freq;
@@ -1089,7 +1101,7 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
             SDL_AudioStream *stream = logdev->bound_streams;
 
             // We should have updated this elsewhere if the format changed!
-            SDL_assert(AUDIO_SPECS_EQUAL(stream->dst_spec, device->spec));
+            SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec));
 
             const int br = SDL_AtomicGet(&logdev->paused) ? 0 : SDL_GetAudioStreamData(stream, device_buffer, buffer_size);
             if (br < 0) {  // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow.
@@ -1106,9 +1118,8 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
 
             SDL_assert(work_buffer_size <= device->work_buffer_size);
 
+            SDL_copyp(&outspec, &device->spec);
             outspec.format = SDL_AUDIO_F32;
-            outspec.channels = device->spec.channels;
-            outspec.freq = device->spec.freq;
 
             SDL_memset(final_mix_buffer, '\0', work_buffer_size);  // start with silence.
 
@@ -1126,7 +1137,7 @@ SDL_bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
 
                 for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
                     // We should have updated this elsewhere if the format changed!
-                    SDL_assert(AUDIO_SPECS_EQUAL(stream->dst_spec, outspec));
+                    SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec));
 
                     /* this will hold a lock on `stream` while getting. We don't explicitly lock the streams
                        for iterating here because the binding linked list can only change while the device lock is held.
@@ -1150,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, device_buffer, device->spec.format, device->spec.channels, device->work_buffer);
-                ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, device->work_buffer, device->spec.format, device->spec.channels, 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);
+                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);
                 SDL_memcpy(device_buffer, device->work_buffer, buffer_size);
             }
         }
@@ -1242,13 +1253,12 @@ SDL_bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device)
                 if (logdev->postmix) {
                     // move to float format.
                     SDL_AudioSpec outspec;
+                    SDL_copyp(&outspec, &device->spec);
                     outspec.format = SDL_AUDIO_F32;
-                    outspec.channels = device->spec.channels;
-                    outspec.freq = device->spec.freq;
                     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, device->postmix_buffer, SDL_AUDIO_F32, outspec.channels, NULL);
+                    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);
                 }
 
@@ -1606,6 +1616,7 @@ static int OpenPhysicalAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec
     device->spec.format = (SDL_AUDIO_BITSIZE(device->default_spec.format) >= SDL_AUDIO_BITSIZE(spec.format)) ? device->default_spec.format : spec.format;
     device->spec.freq = SDL_max(device->default_spec.freq, spec.freq);
     device->spec.channels = SDL_max(device->default_spec.channels, spec.channels);
+    device->spec.use_channel_map = SDL_FALSE;  // all initial channel map requests are denied, since we might have to change channel counts.
     device->sample_frames = GetDefaultSampleFramesFromFreq(device->spec.freq);
     SDL_UpdatedAudioDeviceFormat(device);  // start this off sane.
 
@@ -2155,7 +2166,7 @@ void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device)
         }
 
         if (needs_migration) {
-            const SDL_bool spec_changed = !AUDIO_SPECS_EQUAL(current_default_device->spec, new_default_device->spec);
+            const SDL_bool spec_changed = !SDL_AudioSpecsEqual(&current_default_device->spec, &new_default_device->spec);
             SDL_LogicalAudioDevice *next = NULL;
             for (SDL_LogicalAudioDevice *logdev = current_default_device->logical_devices; logdev; logdev = next) {
                 next = logdev->next;
@@ -2235,7 +2246,7 @@ int SDL_AudioDeviceFormatChangedAlreadyLocked(SDL_AudioDevice *device, const SDL
 {
     const int orig_work_buffer_size = device->work_buffer_size;
 
-    if (AUDIO_SPECS_EQUAL(device->spec, *newspec) && new_sample_frames == device->sample_frames) {
+    if (SDL_AudioSpecsEqual(&device->spec, newspec) && (new_sample_frames == device->sample_frames)) {
         return 0;  // we're already in that format.
     }
 
diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c
index 638f570962100..6b7a9e566e0ab 100644
--- a/src/audio/SDL_audiocvt.c
+++ b/src/audio/SDL_audiocvt.c
@@ -29,39 +29,6 @@
 #define SDL_INT_MAX ((int)(~0u>>1))
 #endif
 
-/*
- * CHANNEL LAYOUTS AS SDL EXPECTS THEM:
- *
- * (Even if the platform expects something else later, that
- * SDL will swizzle between the app and the platform).
- *
- * Abbreviations:
- * - FRONT=single mono speaker
- * - FL=front left speaker
- * - FR=front right speaker
- * - FC=front center speaker
- * - BL=back left speaker
- * - BR=back right speaker
- * - SR=surround right speaker
- * - SL=surround left speaker
- * - BC=back center speaker
- * - LFE=low-frequency speaker
- *
- * These are listed in the order they are laid out in
- * memory, so "FL+FR" means "the front left speaker is
- * laid out in memory first, then the front right, then
- * it repeats for the next audio frame".
- *
- * 1 channel (mono) layout: FRONT
- * 2 channels (stereo) layout: FL+FR
- * 3 channels (2.1) layout: FL+FR+LFE
- * 4 channels (quad) layout: FL+FR+BL+BR
- * 5 channels (4.1) layout: FL+FR+LFE+BL+BR
- * 6 channels (5.1) layout: FL+FR+FC+LFE+BL+BR
- * 7 channels (6.1) layout: FL+FR+FC+LFE+BC+SL+SR
- * 8 channels (7.1) layout: FL+FR+FC+LFE+BL+BR+SL+SR
- */
-
 #ifdef SDL_SSE3_INTRINSICS
 // Convert from stereo to mono. Average left and right.
 static void SDL_TARGETING("sse3") SDL_ConvertStereoToMono_SSE3(float *dst, const float *src, int num_frames)
@@ -157,6 +124,68 @@ static SDL_bool SDL_IsSupportedChannelCount(const int channels)
     return ((channels >= 1) && (channels <= 8));
 }
 
+SDL_bool SDL_ChannelMapIsBogus(const Uint8 *map, int channels)
+{
+    if (map) {
+        for (int i = 0; i < channels; i++) {
+            if (map[i] >= ((Uint8) channels)) {
+                return SDL_TRUE;
+            }
+        }
+    }
+    return SDL_FALSE;
+}
+
+SDL_bool SDL_ChannelMapIsDefault(const Uint8 *map, int channels)
+{
+    if (map) {
+        for (int i = 0; i < channels; i++) {
+            if (map[i] != i) {
+                return SDL_FALSE;
+            }
+        }
+    }
+    return SDL_TRUE;
+}
+
+// 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 Uint8 *map, int bitsize)
+{
+    #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]]; \
+                } \
+            } \
+        } else { \
+            Uint##bits tmp[SDL_MAX_CHANNEL_MAP_SIZE]; \
+            SDL_assert(SDL_arraysize(tmp) >= channels); \
+            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]; \
+                } \
+            } \
+        } \
+    }
+
+    switch (bitsize) {
+        case 8: CHANNEL_SWIZZLE(8); break;
+        case 16: CHANNEL_SWIZZLE(16); break;
+        case 32: CHANNEL_SWIZZLE(32); break;
+        // we don't currently have int64 or double audio datatypes, so no `case 64` for now.
+        default: SDL_assert(!"Unsupported audio datatype size"); break;
+    }
+
+    #undef CHANNEL_SWIZZLE
+}
+
+
 // This does type and channel conversions _but not resampling_ (resampling happens in SDL_AudioStream).
 // This does not check parameter validity, (beyond asserts), it expects you did that already!
 // All of this has to function as if src==dst==scratch (conversion in-place), but as a convenience
@@ -164,8 +193,10 @@ static SDL_bool SDL_IsSupportedChannelCount(const int channels)
 //
 // 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.
-void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, int src_channels,
-                  void *dst, SDL_AudioFormat dst_format, int dst_channels, void* scratch)
+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)
 {
     SDL_assert(src != NULL);
     SDL_assert(dst != NULL);
@@ -188,11 +219,13 @@ void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, i
     const int dst_sample_frame_size = (dst_bitsize / 8) * dst_channels;
 
     /* Type conversion goes like this now:
+        - swizzle through source channel map to "standard" layout.
         - byteswap to CPU native format first if necessary.
         - convert to native Float32 if necessary.
         - change channel count if necessary.
         - convert to final data format.
         - byteswap back to foreign format if necessary.
+        - swizzle through dest channel map from "standard" layout.
 
        The expectation is we can process data faster in float32
        (possibly with SIMD), and making several passes over the same
@@ -201,11 +234,20 @@ void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, i
        (script-generated) custom converters for every data type and
        it was a bloat on SDL compile times and final library size. */
 
+    // 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);
+        src = buf;
+    }
+
     // see if we can skip float conversion entirely.
     if (src_channels == dst_channels) {
         if (src_format == dst_format) {
             // nothing to do, we're already in the right format, just copy it over if necessary.
-            if (src != dst) {
+            if (dst_map) {
+                SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_bitsize);
+            } else if (src != dst) {
                 SDL_memcpy(dst, src, num_frames * dst_sample_frame_size);
             }
             return;
@@ -213,7 +255,11 @@ void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, i
 
         // just a byteswap needed?
         if ((src_format ^ dst_format) == SDL_AUDIO_MASK_BIG_ENDIAN) {
-            ConvertAudioSwapEndian(dst, src, num_frames * src_channels, src_bitsize);
+            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);
+                src = dst;
+            }
+            ConvertAudioSwapEndian(dst, src, num_frames * dst_channels, dst_bitsize);
             return;  // all done.
         }
     }
@@ -275,6 +321,10 @@ void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, i
     }
 
     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);
+    }
 }
 
 // Calculate the largest frame size needed to convert between the two formats.
@@ -304,7 +354,7 @@ static Sint64 GetAudioStreamResampleRate(SDL_AudioStream* stream, int src_freq,
 
 static int UpdateAudioStreamInputSpec(SDL_AudioStream *stream, const SDL_AudioSpec *spec)
 {
-    if (AUDIO_SPECS_EQUAL(stream->input_spec, *spec)) {
+    if (SDL_AudioSpecsEqual(&stream->input_spec, spec)) {
         return 0;
     }
 
@@ -451,6 +501,8 @@ int SDL_SetAudioStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *src_s
             return SDL_SetError("Source rate is too low");
         } else if (src_spec->freq > max_freq) {
             return SDL_SetError("Source rate is too high");
+        } else if (src_spec->use_channel_map && SDL_ChannelMapIsBogus(src_spec->channel_map, src_spec->channels)) {
+            return SDL_SetError("Source channel map is invalid");
         }
     }
 
@@ -465,6 +517,8 @@ int SDL_SetAudioStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *src_s
             return SDL_SetError("Destination rate is too low");
         } else if (dst_spec->freq > max_freq) {
             return SDL_SetError("Destination rate is too high");
+        } else if (dst_spec->use_channel_map && SDL_ChannelMapIsBogus(dst_spec->channel_map, dst_spec->channels)) {
+            return SDL_SetError("Destination channel map is invalid");
         }
     }
 
@@ -481,12 +535,21 @@ int SDL_SetAudioStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *src_s
 
     if (src_spec) {
         SDL_copyp(&stream->src_spec, src_spec);
+        if (src_spec->use_channel_map && SDL_ChannelMapIsDefault(src_spec->channel_map, src_spec->channels)) {
+            stream->src_spec.use_channel_map = SDL_FALSE;  // turn off the channel map, as this is just unnecessary work.
+        }
     }
 
     if (dst_spec) {
         SDL_copyp(&stream->dst_spec, dst_spec);
+        if (dst_spec->use_channel_map && !stream->src_spec.use_channel_map && SDL_ChannelMapIsDefault(dst_spec->channel_map, dst_spec->channels)) {
+            stream->dst_spec.use_channel_map = SDL_FALSE;  // turn off the channel map, as this is just unnecessary work.
+        }
     }
 
+    // !!! FIXME: decide if the source and dest channel maps would swizzle us back to the starting order and just turn them both off.
+    // !!! FIXME:  (but in this case, you can only do it if the channel count isn't changing, because source order is important to that.)
+
     SDL_UnlockMutex(stream->lock);
 
     return 0;
@@ -766,6 +829,7 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
 
     const SDL_AudioFormat dst_format = dst_spec->format;
     const int dst_channels = dst_spec->channels;
+    const Uint8 *dst_map = dst_spec->use_channel_map ? dst_spec->channel_map : NULL;
 
     const int max_frame_size = CalculateMaxFrameSize(src_format, src_channels, dst_format, dst_channels);
     const Sint64 resample_rate = GetAudioStreamResampleRate(stream, src_spec->freq, stream->resample_offset);
@@ -789,7 +853,7 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
             }
         }
 
-        if (SDL_ReadFromAudioQueue(stream->queue, buf, dst_format, dst_channels, 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) != buf) {
             return SDL_SetError("Not enough data in queue");
         }
 
@@ -854,8 +918,9 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
         return -1;
     }
 
+    // (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, resample_format, resample_channels, NULL,
         padding_frames, input_frames, padding_frames, work_buffer);
 
     if (!input_buffer) {
@@ -872,8 +937,8 @@ static int GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int ou
                   (float*) resample_buffer, output_frames,
                   resample_rate, &stream->resample_offset);
 
-    // Convert to the final format, if necessary
-    ConvertAudio(output_frames, resample_buffer, resample_format, resample_channels, buf, dst_format, dst_channels, work_buffer);
+    // 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);
 
     return 0;
 }
diff --git a/src/audio/SDL_audioqueue.c b/src/audio/SDL_audioqueue.c
index 53ddddb17027d..41c8af3b456c7 100644
--- a/src/audio/SDL_audioqueue.c
+++ b/src/audio/SDL_audioqueue.c
@@ -23,8 +23,6 @@
 #include "SDL_audioqueue.h"
 #include "SDL_sysaudio.h"
 
-#define AUDIO_SPECS_EQUAL(x, y) (((x).format == (y).format) && ((x).channels == (y).channels) && ((x).freq == (y).freq))
-
 typedef struct SDL_MemoryPool SDL_MemoryPool;
 
 struct SDL_MemoryPool
@@ -285,7 +283,7 @@ void SDL_AddTrackToAudioQueue(SDL_AudioQueue *queue, SDL_AudioTrack *track)
 
     if (tail) {
         // If the spec has changed, make sure to flush the previous track
-        if (!AUDIO_SPECS_EQUAL(tail->spec, track->spec)) {
+        if (!SDL_AudioSpecsEqual(&tail->spec, &track->spec)) {
             FlushAudioTrack(tail);
         }
 
@@ -319,7 +317,7 @@ int SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, cons
     SDL_AudioTrack *track = queue->tail;
 
     if (track) {
-        if (!AUDIO_SPECS_EQUAL(track->spec, *spec)) {
+        if (!SDL_AudioSpecsEqual(&track->spec, spec)) {
             FlushAudioTrack(track);
         }
     } else {
@@ -514,7 +512,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,
+                                    Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
                                     int past_frames, int present_frames, int future_frames,
                                     Uint8 *scratch)
 {
@@ -526,6 +524,7 @@ const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
 
     SDL_AudioFormat src_format = track->spec.format;
     int src_channels = track->spec.channels;
+    const Uint8 *src_map = track->spec.use_channel_map ? track->spec.channel_map : NULL;
 
     size_t src_frame_size = SDL_AUDIO_BYTESIZE(src_format) * src_channels;
     size_t dst_frame_size = SDL_AUDIO_BYTESIZE(dst_format) * dst_channels;
@@ -553,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, dst, dst_format, dst_channels, scratch);
+                         src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
             ptr = dst;
         }
 
@@ -571,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, dst, dst_format, dst_channels, scratch);
+        ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
         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, dst, dst_format, dst_channels, scratch);
+        ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
         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, dst, dst_format, dst_channels, scratch);
+        ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch);
         dst += dst_future_bytes;
         scratch += dst_future_bytes;
     }
diff --git a/src/audio/SDL_audioqueue.h b/src/audio/SDL_audioqueue.h
index 54c2ba72ad104..26675ce295761 100644
--- a/src/audio/SDL_audioqueue.h
+++ b/src/audio/SDL_audioqueue.h
@@ -67,7 +67,7 @@ void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue);
 size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_AudioSpec *out_spec, SDL_bool *out_flushed);
 
 const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
-                                    Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels,
+                                    Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const Uint8 *dst_map,
                                     int past_frames, int present_frames, int future_frames,
                                     Uint8 *scratch);
 
diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h
index 4c3d2e6d062ad..9973be9bf115c 100644
--- a/src/audio/SDL_sysaudio.h
+++ b/src/audio/SDL_sysaudio.h
@@ -48,8 +48,6 @@
 #define DEFAULT_AUDIO_RECORDING_CHANNELS 1
 #define DEFAULT_AUDIO_RECORDING_FREQUENCY 44100
 
-#define AUDIO_SPECS_EQUAL(x, y) (((x).format == (y).format) && ((x).channels == (y).channels) && ((x).freq == (y).freq))
-
 typedef struct SDL_AudioDevice SDL_AudioDevice;
 typedef struct SDL_LogicalAudioDevice SDL_LogicalAudioDevice;
 
@@ -113,9 +111,19 @@ extern void ConvertAudioToFloat(float *dst, const void *src, int num_samples, SD
 extern void ConvertAudioFromFloat(void *dst, const float *src, int num_samples, SDL_AudioFormat dst_fmt);
 extern void ConvertAudioSwapEndian(void* dst, const void* src, int num_samples, int bitsize);
 
+extern SDL_bool SDL_ChannelMapIsDefault(const Uint8 *map, int channels);
+extern SDL_bool SDL_ChannelMapIsBogus(const Uint8 *map, int channels);
+
 // this gets used from the audio device threads. It has rules, don't use this if you don't know how to use it!
-extern void ConvertAudio(int num_frames, const void *src, SDL_AudioFormat src_format, int src_channels,
-                         void *dst, SDL_AudioFormat dst_format, int dst_channels, void* scratch);
+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);
+
+// 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);
diff --git a/src/audio/alsa/SDL_alsa_audio.c b/src/audio/alsa/SDL_alsa_audio.c
index 115707f48c45a..07a4849709e30 100644
--- a/src/audio/alsa/SDL_alsa_audio.c
+++ b/src/audio/alsa/SDL_alsa_audio.c
@@ -242,108 +242,22 @@ static const char *get_audio_device(void *handle, const int channels)
     return dev->name;
 }
 
-// !!! FIXME: is there a channel swizzler in alsalib instead?
+// Swizzle channels to match SDL defaults.
+// These are swizzl

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