SDL: audio: Separate channel maps out of SDL_AudioSpec.

From 4755055bc3a89b967e903d231bf632526c549488 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Wed, 10 Jul 2024 00:10:37 -0400
Subject: [PATCH] audio: Separate channel maps out of SDL_AudioSpec.

---
 include/SDL3/SDL_audio.h          | 195 +++++++++++++++++++++++++++---
 src/audio/SDL_audio.c             |  50 ++++++--
 src/audio/SDL_audiocvt.c          | 188 +++++++++++++++++++++-------
 src/audio/SDL_audioqueue.c        |  38 ++++--
 src/audio/SDL_audioqueue.h        |   8 +-
 src/audio/SDL_sysaudio.h          |  24 +++-
 src/audio/alsa/SDL_alsa_audio.c   |  46 ++++---
 src/dynapi/SDL_dynapi.sym         |   5 +
 src/dynapi/SDL_dynapi_overrides.h |   5 +
 src/dynapi/SDL_dynapi_procs.h     |   5 +
 test/loopwave.c                   |   1 +
 test/testffmpeg.c                 |   4 +-
 12 files changed, 455 insertions(+), 114 deletions(-)

diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h
index d8879e2aa9003..92251c92faea7 100644
--- a/include/SDL3/SDL_audio.h
+++ b/include/SDL3/SDL_audio.h
@@ -43,7 +43,12 @@
  * if you aren't reading from a file) as a basic means to load sound data into
  * your program.
  *
- * ## Channel layouts as SDL expects them
+ * ## Channel layouts
+ *
+ * Audio data passing through SDL is uncompressed PCM data, interleaved.
+ * One can provide their own decompression through an MP3, etc, decoder, but
+ * SDL does not provide this directly. Each interleaved channel of data is
+ * meant to be in a specific order.
  *
  * Abbreviations:
  *
@@ -76,7 +81,7 @@
  * 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
+ * SDL_AudioStream can also be provided channel maps to change this ordering
  * to whatever is necessary, in other audio processing scenarios.
  */
 
@@ -301,18 +306,6 @@ 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.
  *
@@ -325,8 +318,6 @@ 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;
 
 /**
@@ -560,6 +551,29 @@ extern SDL_DECLSPEC const char *SDLCALL SDL_GetAudioDeviceName(SDL_AudioDeviceID
  */
 extern SDL_DECLSPEC int SDLCALL SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SDL_AudioSpec *spec, int *sample_frames);
 
+/**
+ * Get the current channel map of an audio device.
+ *
+ * Channel maps are optional; most things do not need them, instead passing
+ * data in the [order that SDL expects](CategoryAudio#channel-layouts).
+ *
+ * Audio devices usually have no remapping applied. This is represented by
+ * returning NULL, and does not signify an error.
+ *
+ * The returned array follows the SDL_GetStringRule (even though, strictly
+ * speaking, it isn't a string, it has the same memory manangement rules).
+ *
+ * \param devid the instance ID of the device to query.
+ * \param count On output, set to number of channels in the map. Can be NULL.
+ * \returns an array of the current channel mapping, with as many elements as the current output spec's channels, or NULL if default.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_SetAudioStreamInputChannelMap
+ */
+extern SDL_DECLSPEC const int * SDLCALL SDL_GetAudioDeviceChannelMap(SDL_AudioDeviceID devid, int *count);
 
 /**
  * Open a specific audio device.
@@ -1081,6 +1095,151 @@ extern SDL_DECLSPEC float SDLCALL SDL_GetAudioStreamGain(SDL_AudioStream *stream
  */
 extern SDL_DECLSPEC int SDLCALL SDL_SetAudioStreamGain(SDL_AudioStream *stream, float gain);
 
+/**
+ * Get the current input channel map of an audio stream.
+ *
+ * Channel maps are optional; most things do not need them, instead passing
+ * data in the [order that SDL expects](CategoryAudio#channel-layouts).
+ *
+ * Audio streams default to no remapping applied. This is represented by
+ * returning NULL, and does not signify an error.
+ *
+ * The returned array follows the SDL_GetStringRule (even though, strictly
+ * speaking, it isn't a string, it has the same memory manangement rules).
+ *
+ * \param stream the SDL_AudioStream to query.
+ * \param count On output, set to number of channels in the map. Can be NULL.
+ * \returns an array of the current channel mapping, with as many elements as the current output spec's channels, or NULL if default.
+ *
+ * \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_SetAudioStreamInputChannelMap
+ */
+extern SDL_DECLSPEC const int * SDLCALL SDL_GetAudioStreamInputChannelMap(SDL_AudioStream *stream, int *count);
+
+/**
+ * Get the current output channel map of an audio stream.
+ *
+ * Channel maps are optional; most things do not need them, instead passing
+ * data in the [order that SDL expects](CategoryAudio#channel-layouts).
+ *
+ * Audio streams default to no remapping applied. This is represented by
+ * returning NULL, and does not signify an error.
+ *
+ * The returned array follows the SDL_GetStringRule (even though, strictly
+ * speaking, it isn't a string, it has the same memory manangement rules).
+ *
+ * \param stream the SDL_AudioStream to query.
+ * \param count On output, set to number of channels in the map. Can be NULL.
+ * \returns an array of the current channel mapping, with as many elements as the current output spec's channels, or NULL if default.
+ *
+ * \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_SetAudioStreamInputChannelMap
+ */
+extern SDL_DECLSPEC const int * SDLCALL SDL_GetAudioStreamOutputChannelMap(SDL_AudioStream *stream, int *count);
+
+/**
+ * Set the current input channel map of an audio stream.
+ *
+ * Channel maps are optional; most things do not need them, instead passing
+ * data in the [order that SDL expects](CategoryAudio#channel-layouts).
+ *
+ * The input channel map reorders data that is added to a stream via
+ * SDL_PutAudioStreamData. Future calls to SDL_PutAudioStreamData
+ * must provide data in the new channel order.
+ *
+ * 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.
+ *
+ * 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
+ * the end of a sound file in one order to a stream, change orders for the
+ * next sound file, and start putting that new data while the previous sound
+ * file is still queued, and everything will still play back correctly.
+ *
+ * Audio streams default to no remapping applied. Passing a NULL channel map
+ * is legal, and turns off remapping.
+ *
+ * SDL will copy the channel map; the caller does not have to save this array
+ * after this call.
+ *
+ * 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 channel map.
+ *
+ * \param stream the SDL_AudioStream to change.
+ * \param chmap the new channel map, NULL to reset to default.
+ * \param count The number of channels in the map.
+ * \returns 0 on success, -1 on error.
+ *
+ * \threadsafety It is safe to call this function from any thread, as it holds
+ *               a stream-specific mutex while running. Don't change the
+ *               stream's format to have a different number of channels from a
+ *               a different thread at the same time, though!
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_SetAudioStreamInputChannelMap
+ */
+extern SDL_DECLSPEC int SDLCALL SDL_SetAudioStreamInputChannelMap(SDL_AudioStream *stream, const int *chmap, int count);
+
+/**
+ * Set the current output channel map of an audio stream.
+ *
+ * Channel maps are optional; most things do not need them, instead passing
+ * data in the [order that SDL expects](CategoryAudio#channel-layouts).
+ *
+ * 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
+ * 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.
+ *
+ * The output channel map can be changed at any time, as output remapping is
+ * applied during SDL_GetAudioStreamData.
+ *
+ * Audio streams default to no remapping applied. Passing a NULL channel map
+ * is legal, and turns off remapping.
+ *
+ * SDL will copy the channel map; the caller does not have to save this array
+ * after this call.
+ *
+ * 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 channel map.
+ *
+ * \param stream the SDL_AudioStream to change.
+ * \param chmap the new channel map, NULL to reset to default.
+ * \param count The number of channels in the map.
+ * \returns 0 on success, -1 on error.
+ *
+ * \threadsafety It is safe to call this function from any thread, as it holds
+ *               a stream-specific mutex while running. Don't change the
+ *               stream's format to have a different number of channels from a
+ *               a different thread at the same time, though!
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_SetAudioStreamInputChannelMap
+ */
+extern SDL_DECLSPEC int SDLCALL SDL_SetAudioStreamOutputChannelMap(SDL_AudioStream *stream, const int *chmap, int count);
 
 /**
  * Add data to the stream.
@@ -1505,7 +1664,7 @@ extern SDL_DECLSPEC void SDLCALL SDL_DestroyAudioStream(SDL_AudioStream *stream)
  * Also unlike other functions, the audio device begins paused. This is to map
  * more closely to SDL2-style behavior, since there is no extra step here to
  * bind a stream to begin audio flowing. The audio device should be resumed
- * with `SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(stream));`
+ * with `SDL_ResumeAudioStreamDevice(stream);`
  *
  * This function works with both playback and recording devices.
  *
@@ -1547,7 +1706,7 @@ extern SDL_DECLSPEC void SDLCALL SDL_DestroyAudioStream(SDL_AudioStream *stream)
  * \since This function is available since SDL 3.0.0.
  *
  * \sa SDL_GetAudioStreamDevice
- * \sa SDL_ResumeAudioDevice
+ * \sa SDL_ResumeAudioStreamDevice
  */
 extern SDL_DECLSPEC SDL_AudioStream *SDLCALL SDL_OpenAudioDeviceStream(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec, SDL_AudioStreamCallback callback, void *userdata);
 
diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index 4a3c7281f6aca..9d63bcfaaf54c 100644
--- a/src/audio/SDL_audio.c
+++ b/src/audio/SDL_audio.c
@@ -167,6 +167,16 @@ static int GetDefaultSampleFramesFromFreq(const int freq)
     }
 }
 
+int *SDL_ChannelMapDup(const int *origchmap, int channels)
+{
+    const size_t chmaplen = sizeof (*origchmap) * channels;
+    int *chmap = (int *) SDL_malloc(chmaplen);
+    if (chmap) {
+        SDL_memcpy(chmap, origchmap, chmaplen);
+    }
+    return chmap;
+}
+
 void OnAudioStreamCreated(SDL_AudioStream *stream)
 {
     SDL_assert(stream != NULL);
@@ -243,17 +253,18 @@ static void UpdateAudioStreamFormatsPhysical(SDL_AudioDevice *device)
                 // SDL_SetAudioStreamFormat does a ton of validation just to memcpy an audiospec.
                 SDL_LockMutex(stream->lock);
                 SDL_copyp(&stream->dst_spec, &spec);
+                SDL_SetAudioStreamOutputChannelMap(stream, device->chmap, spec.channels);  // this should be fast for normal cases, though!
                 SDL_UnlockMutex(stream->lock);
             }
         }
     }
 }
 
-SDL_bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b)
+SDL_bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b, const int *channel_map_a, const int *channel_map_b)
 {
-    if ((a->format != b->format) || (a->channels != b->channels) || (a->freq != b->freq) || (a->use_channel_map != b->use_channel_map)) {
+    if ((a->format != b->format) || (a->channels != b->channels) || (a->freq != b->freq) || ((channel_map_a != NULL) != (channel_map_b != NULL))) {
         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)) {
+    } else if (channel_map_a && (SDL_memcmp(channel_map_a, channel_map_b, sizeof (*channel_map_a) * a->channels) != 0)) {
         return SDL_FALSE;
     }
     return SDL_TRUE;
@@ -533,6 +544,7 @@ static void DestroyPhysicalAudioDevice(SDL_AudioDevice *device)
     SDL_DestroyMutex(device->lock);
     SDL_DestroyCondition(device->close_cond);
     SDL_free(device->work_buffer);
+    SDL_FreeLater(device->chmap);  // this pointer is handed to the app during SDL_GetAudioDeviceChannelMap
     SDL_FreeLater(device->name);  // this pointer is handed to the app during SDL_GetAudioDeviceName
     SDL_free(device);
 }
@@ -648,7 +660,6 @@ SDL_AudioDevice *SDL_AddAudioDevice(SDL_bool recording, const char *name, const
         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;
@@ -1101,7 +1112,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(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec));
+            SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec, stream->dst_chmap, device->chmap));
 
             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.
@@ -1137,7 +1148,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(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec));
+                    SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec, stream->dst_chmap, device->chmap));
 
                     /* 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.
@@ -1448,6 +1459,25 @@ int SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SDL_AudioSpec *spec, int *
     return retval;
 }
 
+const int *SDL_GetAudioDeviceChannelMap(SDL_AudioDeviceID devid, int *count)
+{
+    const int *retval = NULL;
+    int channels = 0;
+    SDL_AudioDevice *device = ObtainPhysicalAudioDeviceDefaultAllowed(devid);
+    if (device) {
+        retval = device->chmap;
+        channels = device->spec.channels;
+    }
+    ReleaseAudioDevice(device);
+
+    if (count) {
+        *count = channels;
+    }
+
+    return retval;
+}
+
+
 // this is awkward, but this makes sure we can release the device lock
 //  so the device thread can terminate but also not have two things
 //  race to close or open the device while the lock is unprotected.
@@ -1618,7 +1648,6 @@ 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.
 
@@ -1906,6 +1935,7 @@ int SDL_BindAudioStreams(SDL_AudioDeviceID devid, SDL_AudioStream **streams, int
                     if (logdev->postmix) {
                         stream->src_spec.format = SDL_AUDIO_F32;
                     }
+                    SDL_SetAudioStreamInputChannelMap(stream, device->chmap, device->spec.channels);  // this should be fast for normal cases, though!
                 }
 
                 SDL_UnlockMutex(stream->lock);
@@ -2208,7 +2238,8 @@ void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device)
         }
 
         if (needs_migration) {
-            const SDL_bool spec_changed = !SDL_AudioSpecsEqual(&current_default_device->spec, &new_default_device->spec);
+            // we don't currently report channel map changes, so we'll leave them as NULL for now.
+            const SDL_bool spec_changed = !SDL_AudioSpecsEqual(&current_default_device->spec, &new_default_device->spec, NULL, NULL);
             SDL_LogicalAudioDevice *next = NULL;
             for (SDL_LogicalAudioDevice *logdev = current_default_device->logical_devices; logdev; logdev = next) {
                 next = logdev->next;
@@ -2288,7 +2319,8 @@ int SDL_AudioDeviceFormatChangedAlreadyLocked(SDL_AudioDevice *device, const SDL
 {
     const int orig_work_buffer_size = device->work_buffer_size;
 
-    if (SDL_AudioSpecsEqual(&device->spec, newspec) && (new_sample_frames == device->sample_frames)) {
+    // we don't currently have any place where channel maps change from under you, but we can check that if necessary later.
+    if (SDL_AudioSpecsEqual(&device->spec, newspec, NULL, NULL) && (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 29cd63dc6d4db..3da7543ceb805 100644
--- a/src/audio/SDL_audiocvt.c
+++ b/src/audio/SDL_audiocvt.c
@@ -124,11 +124,12 @@ static SDL_bool SDL_IsSupportedChannelCount(const int channels)
     return ((channels >= 1) && (channels <= 8));
 }
 
-SDL_bool SDL_ChannelMapIsBogus(const Uint8 *map, int channels)
+SDL_bool SDL_ChannelMapIsBogus(const int *chmap, int channels)
 {
-    if (map) {
+    if (chmap) {
         for (int i = 0; i < channels; i++) {
-            if (map[i] >= ((Uint8) channels)) {
+            const int mapping = chmap[i];
+            if ((mapping < 0) || (mapping >= channels)) {
                 return SDL_TRUE;
             }
         }
@@ -136,11 +137,11 @@ SDL_bool SDL_ChannelMapIsBogus(const Uint8 *map, int channels)
     return SDL_FALSE;
 }
 
-SDL_bool SDL_ChannelMapIsDefault(const Uint8 *map, int channels)
+SDL_bool SDL_ChannelMapIsDefault(const int *chmap, int channels)
 {
-    if (map) {
+    if (chmap) {
         for (int i = 0; i < channels; i++) {
-            if (map[i] != i) {
+            if (chmap[i] != i) {
                 return SDL_FALSE;
             }
         }
@@ -149,7 +150,7 @@ SDL_bool SDL_ChannelMapIsDefault(const Uint8 *map, 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 Uint8 *map, int bitsize)
+static void SwizzleAudio(const int num_frames, void *dst, const void *src, int channels, const int *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. */ \
@@ -161,16 +162,18 @@ static void SwizzleAudio(const int num_frames, void *dst, const void *src, int c
                 } \
             } \
         } else { \
-            Uint##bits tmp[SDL_MAX_CHANNEL_MAP_SIZE]; \
-            SDL_zeroa(tmp); \
-            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]; \
+            SDL_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]]; \
+                    } \
+                    for (int ch = 0; ch < channels; ch++) { \
+                        tdst[ch] = tmp[ch]; \
+                    } \
                 } \
+                SDL_small_free(tmp, isstack); \
             } \
         } \
     }
@@ -199,8 +202,8 @@ static void SwizzleAudio(const int num_frames, void *dst, const void *src, int c
 // 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,
+                  const void *src, SDL_AudioFormat src_format, int src_channels, const int *src_map,
+                  void *dst, SDL_AudioFormat dst_format, int dst_channels, const int *dst_map,
                   void* scratch, float gain)
 {
     SDL_assert(src != NULL);
@@ -374,9 +377,9 @@ static Sint64 GetAudioStreamResampleRate(SDL_AudioStream* stream, int src_freq,
     return resample_rate;
 }
 
-static int UpdateAudioStreamInputSpec(SDL_AudioStream *stream, const SDL_AudioSpec *spec)
+static int UpdateAudioStreamInputSpec(SDL_AudioStream *stream, const SDL_AudioSpec *spec, const int *chmap)
 {
-    if (SDL_AudioSpecsEqual(&stream->input_spec, spec)) {
+    if (SDL_AudioSpecsEqual(&stream->input_spec, spec, stream->input_chmap, chmap)) {
         return 0;
     }
 
@@ -384,6 +387,14 @@ static int UpdateAudioStreamInputSpec(SDL_AudioStream *stream, const SDL_AudioSp
         return -1;
     }
 
+    if (!chmap) {
+        stream->input_chmap = NULL;
+    } else {
+        const size_t chmaplen = sizeof (*chmap) * spec->channels;
+        stream->input_chmap = stream->input_chmap_storage;
+        SDL_memcpy(stream->input_chmap, chmap, chmaplen);
+    }
+
     SDL_copyp(&stream->input_spec, spec);
 
     return 0;
@@ -524,8 +535,6 @@ 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");
         }
     }
 
@@ -540,8 +549,6 @@ 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");
         }
     }
 
@@ -557,27 +564,114 @@ 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 (src_spec->channels != stream->src_spec.channels) {
+            SDL_FreeLater(stream->src_chmap);  // this pointer is handed to the app during SDL_GetAudioStreamInputChannelMap
+            stream->src_chmap = NULL;
         }
+        SDL_copyp(&stream->src_spec, src_spec);
     }
 
     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.
+        if (dst_spec->channels != stream->dst_spec.channels) {
+            SDL_FreeLater(stream->dst_chmap);  // this pointer is handed to the app during SDL_GetAudioStreamInputChannelMap
+            stream->dst_chmap = NULL;
         }
+        SDL_copyp(&stream->dst_spec, dst_spec);
     }
 
-    // !!! 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;
 }
 
+static int SetAudioStreamChannelMap(SDL_AudioStream *stream, const SDL_AudioSpec *spec, int **stream_chmap, const int *chmap, int channels, SDL_bool isinput)
+{
+    if (!stream) {
+        return SDL_InvalidParamError("stream");
+    }
+
+    int retval = 0;
+
+    SDL_LockMutex(stream->lock);
+
+    if (channels != spec->channels) {
+        retval = SDL_SetError("Wrong number of channels");
+    } else if (!*stream_chmap && !chmap) {
+        // already at default, we're good.
+    } else if (*stream_chmap && chmap && (SDL_memcmp(*stream_chmap, chmap, sizeof (*chmap) * channels) == 0)) {
+        // already have this map, don't allocate/copy it again.
+    } else if (SDL_ChannelMapIsBogus(chmap, channels)) {
+        retval = SDL_SetError("Invalid channel mapping");
+    } else if (stream->bound_device && (!!isinput == !!stream->bound_device->physical_device->recording)) {
+        // quietly refuse to change the format of the end currently bound to a device.
+    } else {
+        if (SDL_ChannelMapIsDefault(chmap, channels)) {
+            chmap = NULL;  // just apply a default mapping.
+        }
+        if (chmap) {
+            int *dupmap = SDL_ChannelMapDup(chmap, channels);
+            if (!dupmap) {
+                retval = SDL_SetError("Invalid channel mapping");
+            } else {
+                SDL_FreeLater(*stream_chmap);  // this pointer is handed to the app during SDL_GetAudioStreamInputChannelMap
+                *stream_chmap = dupmap;
+            }
+        } else {
+            SDL_FreeLater(*stream_chmap);  // this pointer is handed to the app during SDL_GetAudioStreamInputChannelMap
+            *stream_chmap = NULL;
+        }
+    }
+
+    SDL_UnlockMutex(stream->lock);
+    return retval;
+}
+
+int SDL_SetAudioStreamInputChannelMap(SDL_AudioStream *stream, const int *chmap, int channels)
+{
+    return SetAudioStreamChannelMap(stream, &stream->src_spec, &stream->src_chmap, chmap, channels, SDL_TRUE);
+}
+
+int SDL_SetAudioStreamOutputChannelMap(SDL_AudioStream *stream, const int *chmap, int channels)
+{
+    return SetAudioStreamChannelMap(stream, &stream->dst_spec, &stream->dst_chmap, chmap, channels, SDL_FALSE);
+}
+
+const int *SDL_GetAudioStreamInputChannelMap(SDL_AudioStream *stream, int *count)
+{
+    const int *retval = NULL;
+    int channels = 0;
+    if (stream) {
+        SDL_LockMutex(stream->lock);
+        retval = stream->src_chmap;
+        channels = stream->src_spec.channels;
+        SDL_UnlockMutex(stream->lock);
+    }
+
+    if (count) {
+        *count = channels;
+    }
+
+    return retval;
+}
+
+const int *SDL_GetAudioStreamOutputChannelMap(SDL_AudioStream *stream, int *count)
+{
+    const int *retval = NULL;
+    int channels = 0;
+    if (stream) {
+        SDL_LockMutex(stream->lock);
+        retval = stream->dst_chmap;
+        channels = stream->dst_spec.channels;
+        SDL_UnlockMutex(stream->lock);
+    }
+
+    if (count) {
+        *count = channels;
+    }
+
+    return retval;
+}
+
 float SDL_GetAudioStreamFrequencyRatio(SDL_AudioStream *stream)
 {
     if (!stream) {
@@ -676,7 +770,7 @@ static int PutAudioStreamBuffer(SDL_AudioStream *stream, const void *buf, int le
     SDL_AudioTrack* track = NULL;
 
     if (callback) {
-        track = SDL_CreateAudioTrack(stream->queue, &stream->src_spec, (Uint8 *)buf, len, len, callback, userdata);
+        track = SDL_CreateAudioTrack(stream->queue, &stream->src_spec, stream->src_chmap, (Uint8 *)buf, len, len, callback, userdata);
 
         if (!track) {
             SDL_UnlockMutex(stream->lock);
@@ -691,7 +785,7 @@ static int PutAudioStreamBuffer(SDL_AudioStream *stream, const void *buf, int le
     if (track) {
         SDL_AddTrackToAudioQueue(stream->queue, track);
     } else {
-        retval = SDL_WriteToAudioQueue(stream->queue, &stream->src_spec, (const Uint8 *)buf, len);
+        retval = SDL_WriteToAudioQueue(stream->queue, &stream->src_spec, stream->src_chmap, (const Uint8 *)buf, len);
     }
 
     if (retval == 0) {
@@ -782,16 +876,21 @@ static Uint8 *EnsureAudioStreamWorkBufferSize(SDL_AudioStream *stream, size_t ne
 }
 
 static Sint64 NextAudioStreamIter(SDL_AudioStream* stream, void** inout_iter,
-    Sint64* inout_resample_offset, SDL_AudioSpec* out_spec, SDL_bool* out_flushed)
+    Sint64* inout_resample_offset, SDL

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