SDL_mixer: - Remap channels from vorbis and opus sources to match SDL channels.

From 03306cf667646dd207eee28e773e3ec9543b283f Mon Sep 17 00:00:00 2001
From: "Daniel K. O. (dkosmari)" <[EMAIL REDACTED]>
Date: Mon, 19 Jan 2026 21:26:13 -0300
Subject: [PATCH] - Remap channels from vorbis and opus sources to match SDL
 channels. - Don't discard decoded samples when stream configuration changes.

---
 CMakeLists.txt              |   1 +
 src/codecs/music_ogg.c      |  30 ++++--
 src/codecs/music_opus.c     |  30 ++++--
 src/codecs/remap_channels.c | 210 ++++++++++++++++++++++++++++++++++++
 src/codecs/remap_channels.h |  29 +++++
 5 files changed, 281 insertions(+), 19 deletions(-)
 create mode 100644 src/codecs/remap_channels.c
 create mode 100644 src/codecs/remap_channels.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index e56c18188..1a962e874 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -233,6 +233,7 @@ add_library(SDL2_mixer
     src/codecs/music_wav.c
     src/codecs/music_wavpack.c
     src/codecs/music_xmp.c
+    src/codecs/remap_channels.c
     src/effect_position.c
     src/effect_stereoreverse.c
     src/effects_internal.c
diff --git a/src/codecs/music_ogg.c b/src/codecs/music_ogg.c
index a5ebd9569..0338ac8cb 100644
--- a/src/codecs/music_ogg.c
+++ b/src/codecs/music_ogg.c
@@ -26,6 +26,7 @@
 #include "SDL_loadso.h"
 
 #include "music_ogg.h"
+#include "remap_channels.h"
 #include "utils.h"
 
 #define OV_EXCLUDE_STATIC_CALLBACKS
@@ -195,6 +196,7 @@ static void OGG_Delete(void *context);
 static int OGG_UpdateSection(OGG_music *music)
 {
     vorbis_info *vi;
+    int new_buffer_size;
 
     vi = vorbis.ov_info(&music->vf, -1);
     if (!vi) {
@@ -206,11 +208,6 @@ static int OGG_UpdateSection(OGG_music *music)
     }
     SDL_memcpy(&music->vi, vi, sizeof(*vi));
 
-    if (music->buffer) {
-        SDL_free(music->buffer);
-        music->buffer = NULL;
-    }
-
     if (music->stream) {
         SDL_FreeAudioStream(music->stream);
         music->stream = NULL;
@@ -219,13 +216,24 @@ static int OGG_UpdateSection(OGG_music *music)
     music->stream = SDL_NewAudioStream(AUDIO_S16SYS, (Uint8)vi->channels, (int)vi->rate,
                                        music_spec.format, music_spec.channels, music_spec.freq);
     if (!music->stream) {
+        SDL_free(music->buffer);
+        music->buffer      = NULL;
+        music->buffer_size = 0;
         return -1;
     }
 
-    music->buffer_size = music_spec.samples * (int)sizeof(Sint16) * vi->channels;
-    music->buffer = (char *)SDL_malloc((size_t)music->buffer_size);
-    if (!music->buffer) {
-        return -1;
+    /* Note: never shrink the buffer, we just decoded data in there. */
+    new_buffer_size = music_spec.samples * (int)sizeof(Sint16) * vi->channels;
+    if (new_buffer_size > music->buffer_size) {
+        char *new_buffer = (char *)SDL_realloc(music->buffer, new_buffer_size);
+        if (!new_buffer) {
+            SDL_free(music->buffer);
+            music->buffer      = NULL;
+            music->buffer_size = 0;
+            return -1;
+        }
+        music->buffer      = new_buffer;
+        music->buffer_size = new_buffer_size;
     }
     return 0;
 }
@@ -387,7 +395,7 @@ static int OGG_GetSome(void *context, void *data, int bytes, SDL_bool *done)
 #ifdef OGG_USE_TREMOR
     amount = (int)vorbis.ov_read(&music->vf, music->buffer, music->buffer_size, &section);
 #else
-    amount = (int)vorbis.ov_read(&music->vf, music->buffer, music->buffer_size, SDL_BYTEORDER == SDL_BIG_ENDIAN, 2, 1, &section);
+    amount = (int)vorbis.ov_read(&music->vf, music->buffer, music->buffer_size, SDL_BYTEORDER == SDL_BIG_ENDIAN, (int)sizeof(Sint16), 1, &section);
 #endif
     if (amount < 0) {
         return set_ov_error("ov_read", amount);
@@ -400,6 +408,8 @@ static int OGG_GetSome(void *context, void *data, int bytes, SDL_bool *done)
         }
     }
 
+    remap_channels_vorbis((Sint16 *)music->buffer, amount / (int)sizeof(Sint16), music->vi.channels);
+
     pcmPos = vorbis.ov_pcm_tell(&music->vf);
     if (music->loop && (music->play_count != 1) && (pcmPos >= music->loop_end)) {
         amount -= (int)((pcmPos - music->loop_end) * music->vi.channels) * (int)sizeof(Sint16);
diff --git a/src/codecs/music_opus.c b/src/codecs/music_opus.c
index a367fbaa3..648d50cb6 100644
--- a/src/codecs/music_opus.c
+++ b/src/codecs/music_opus.c
@@ -26,6 +26,7 @@
 #include "SDL_loadso.h"
 
 #include "music_opus.h"
+#include "remap_channels.h"
 #include "utils.h"
 
 #ifdef OPUSFILE_HEADER
@@ -169,6 +170,7 @@ static void OPUS_Delete(void*);
 static int OPUS_UpdateSection(OPUS_music *music)
 {
     const OpusHead *op_info;
+    int new_buffer_size;
 
     op_info = opus.op_head(music->of, -1);
     if (!op_info) {
@@ -180,11 +182,6 @@ static int OPUS_UpdateSection(OPUS_music *music)
     }
     music->op_info = op_info;
 
-    if (music->buffer) {
-        SDL_free(music->buffer);
-        music->buffer = NULL;
-    }
-
     if (music->stream) {
         SDL_FreeAudioStream(music->stream);
         music->stream = NULL;
@@ -193,13 +190,24 @@ static int OPUS_UpdateSection(OPUS_music *music)
     music->stream = SDL_NewAudioStream(AUDIO_S16SYS, (Uint8)op_info->channel_count, 48000,
                                        music_spec.format, music_spec.channels, music_spec.freq);
     if (!music->stream) {
+        SDL_free(music->buffer);
+        music->buffer      = NULL;
+        music->buffer_size = 0;
         return -1;
     }
 
-    music->buffer_size = (int)music_spec.samples * (int)sizeof(opus_int16) * op_info->channel_count;
-    music->buffer = (char *)SDL_malloc((size_t)music->buffer_size);
-    if (!music->buffer) {
-        return -1;
+    /* Note: never shrink the buffer, we just decoded data in there. */
+    new_buffer_size = (int)music_spec.samples * (int)sizeof(opus_int16) * op_info->channel_count;
+    if (new_buffer_size > music->buffer_size) {
+        char *new_buffer = (char *)SDL_realloc(music->buffer, (size_t)new_buffer_size);
+        if (!new_buffer) {
+            SDL_free(music->buffer);
+            music->buffer      = NULL;
+            music->buffer_size = 0;
+            return -1;
+        }
+        music->buffer      = new_buffer;
+        music->buffer_size = new_buffer_size;
     }
     return 0;
 }
@@ -378,6 +386,10 @@ static int OPUS_GetSome(void *context, void *data, int bytes, SDL_bool *done)
         }
     }
 
+    if (music->op_info->mapping_family == 1) {
+        remap_channels_vorbis((Sint16 *)music->buffer, samples * music->op_info->channel_count, music->op_info->channel_count);
+    }
+
     pcmPos = opus.op_pcm_tell(music->of);
     if (music->loop && (music->play_count != 1) && (pcmPos >= music->loop_end)) {
         samples -= (int)((pcmPos - music->loop_end) * music->op_info->channel_count) * (int)sizeof(Sint16);
diff --git a/src/codecs/remap_channels.c b/src/codecs/remap_channels.c
new file mode 100644
index 000000000..552b06c1d
--- /dev/null
+++ b/src/codecs/remap_channels.c
@@ -0,0 +1,210 @@
+/*
+  SDL_mixer:  An audio mixer library based on the SDL library
+  Copyright (C) 2026 Daniel K. O. <github.com/dkosmari>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+/*
+ * Comparison of channel orders:
+ *
+ * |  Num. | chan. | SDL | FLAC | MS/USB | Vorbis |
+ * |------:|------:|-----|------|--------|--------|
+ * |     2 |     1 | FL  | FL   | FL     | FL     |
+ * |       |     2 | FR  | FR   | FR     | FR     |
+ * |-------|-------|-----|------|--------|--------|
+ * |     3 |     1 | FL  | FL   | FL     | FL     |
+ * |       |     2 | FR  | FR   | FR     | FC     |
+ * |       |     3 | LFE | FC   | FC/LFE | FR     |
+ * |-------|-------|-----|------|--------|--------|
+ * |     4 |     1 | FL  | FL   | FL     | FL     |
+ * |       |     2 | FR  | FR   | FR     | FR     |
+ * |       |     3 | RL  | RL   | RL     | RL     |
+ * |       |     4 | RR  | RR   | RR     | RR     |
+ * |-------|-------|-----|------|--------|--------|
+ * |     5 |     1 | FL  | FL   | FL     | FL     |
+ * | (5.0) |     2 | FR  | FR   | FR     | FC     |
+ * |       |     3 | LFE | FC   | FC/LFE | FR     |
+ * |       |     4 | RL  | RL   | RL     | RL     |
+ * |       |     5 | RR  | RR   | RR     | RR     |
+ * |-------|-------|-----|------|--------|--------|
+ * |     6 |     1 | FL  | FL   | FL     | FL     |
+ * | (5.1) |     2 | FR  | FR   | FR     | FC     |
+ * |       |     3 | FC  | FC   | FC     | FR     |
+ * |       |     4 | LFE | LFE  | LFE    | RL     |
+ * |       |     5 | RL  | RL   | RR     | RR     |
+ * |       |     6 | RR  | RR   | RR     | LFE    |
+ * |-------|-------|-----|------|--------|--------|
+ * |     7 |     1 | FL  | FL   | FL     | FL     |
+ * |       |     2 | FR  | FR   | FR     | FC     |
+ * |       |     3 | FC  | FC   | FC     | FR     |
+ * |       |     4 | LFE | LFE  | LFE    | SL     |
+ * |       |     5 | RC  | RC   | RC     | SR     |
+ * |       |     6 | SL  | SL   | SL     | RC     |
+ * |       |     7 | SR  | SR   | SR     | LFE    |
+ * |-------|-------|-----|------|--------|--------|
+ * |     8 |     1 | FL  | FL   | FL     | FL     |
+ * | (7.1) |     2 | FR  | FR   | FR     | FC     |
+ * |       |     3 | FC  | FC   | FC     | FR     |
+ * |       |     4 | LFE | LFE  | LFE    | SL     |
+ * |       |     5 | RL  | RL   | RL     | SR     |
+ * |       |     6 | RR  | RR   | RR     | RL     |
+ * |       |     7 | SL  | SL   | SL     | RR     |
+ * |       |     8 | SR  | SR   | SR     | LFE    |
+ *
+ *
+ * **Note:** USB/MS use a bitmask to indicate which channels are present. The only
+ * requirement is that they must appear in a fixed order, if present:
+ *
+ *   - FL (Front Center)
+ *   - FR (Front Right)
+ *   - FC (Front Center)
+ *   - LFE (Low Frequency Enhancement)
+ *   - BL (Back Left) aka RL (Rear LEft)
+ *   - BR (Back Right) aka RR (Rear Right)
+ *   - FLC (Front Left of Center)
+ *   - FRC (Front Right of Center)
+ *   - BC (Back Center) aka RC (Rear Center)
+ *   - SL (Side Left)
+ *   - SR (Side Right)
+ *   - TC (Top Center)
+ *   - TFL (Top Front Left)
+ *   - TFC (Top Front Center)
+ *   - TFR (Top Front Right)
+ *   - TBL (Top Back Left)
+ *   - TBC (Top Back Center)
+ *   - TBR (Top Back Right)
+ *
+ *
+ * **Note:** WavPack documentation claims that ALL MS/USB channels (up to the max) must be
+ * present. So to contain all the 7.1 channels, a .wv file must have 11 channels, with
+ * silent FLC, FRC, BC/RC channels.
+ *
+ *
+ * Sources:
+ *   - Vorbis: https://www.rfc-editor.org/rfc/rfc7845.html#section-5.1.1.2
+ *   - SDL: https://github.com/libsdl-org/SDL/blob/main/include/SDL3/SDL_audio.h
+ *   - FLAC: https://www.rfc-editor.org/rfc/rfc9639.html#name-channels-bits
+ *   - WavPack (WV): https://www.wavpack.com/wavpack_doc.html
+ *   - USB: https://www.usb.org/sites/default/files/audio10.pdf (see "3.7.2.3 Audio Channel Cluster Format")
+ *   - MS: https://learn.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible
+ */
+
+#include "remap_channels.h"
+
+
+static void remap_channels_vorbis_3(Sint16 *samples, int num_samples)
+{
+    /* Note: this isn't perfect, because we map FC to LFE */
+    int i;
+    for (i = 0; i < num_samples; i += 3) {
+        Sint16 FC = samples[i + 1];
+        Sint16 FR = samples[i + 2];
+        samples[i + 1] = FR;
+        samples[i + 2] = FC;
+    }
+}
+
+static void remap_channels_vorbis_5(Sint16 *samples, int num_samples)
+{
+    /* Note: this isn't perfect, because we map FC to LFE. */
+    int i;
+    for (i = 0; i < num_samples; i += 5) {
+        Sint16 FC = samples[i + 1];
+        Sint16 FR = samples[i + 2];
+        samples[i + 1] = FR;
+        samples[i + 2] = FC;
+    }
+}
+
+static void remap_channels_vorbis_5_1(Sint16 *samples, int num_samples)
+{
+    int i;
+    for (i = 0; i < num_samples; i += 6) {
+        Sint16 FC  = samples[i + 1];
+        Sint16 FR  = samples[i + 2];
+        Sint16 RL  = samples[i + 3];
+        Sint16 RR  = samples[i + 4];
+        Sint16 LFE = samples[i + 5];
+        samples[i + 1] = FR;
+        samples[i + 2] = FC;
+        samples[i + 3] = LFE;
+        samples[i + 4] = RL;
+        samples[i + 5] = RR;
+    }
+}
+
+static void remap_channels_vorbis_7(Sint16 *samples, int num_samples)
+{
+    int i = 0;
+    for (i = 0; i < num_samples; i += 7) {
+        Sint16 FC  = samples[i + 1];
+        Sint16 FR  = samples[i + 2];
+        Sint16 SL  = samples[i + 3];
+        Sint16 SR  = samples[i + 4];
+        Sint16 RC  = samples[i + 5];
+        Sint16 LFE = samples[i + 6];
+        samples[i + 1] = FR;
+        samples[i + 2] = FC;
+        samples[i + 3] = LFE;
+        samples[i + 4] = RC;
+        samples[i + 5] = SL;
+        samples[i + 6] = SR;
+    }
+}
+
+static void remap_channels_vorbis_7_1(Sint16 *samples, int num_samples)
+{
+    int i = 0;
+    for (i = 0; i < num_samples; i += 8) {
+        Sint16 FC  = samples[i + 1];
+        Sint16 FR  = samples[i + 2];
+        Sint16 SL  = samples[i + 3];
+        Sint16 SR  = samples[i + 4];
+        Sint16 RL  = samples[i + 5];
+        Sint16 RR  = samples[i + 6];
+        Sint16 LFE = samples[i + 7];
+        samples[i + 1] = FR;
+        samples[i + 2] = FC;
+        samples[i + 3] = LFE;
+        samples[i + 4] = RL;
+        samples[i + 5] = RR;
+        samples[i + 6] = SL;
+        samples[i + 7] = SR;
+    }
+}
+
+void remap_channels_vorbis(Sint16 *samples, int num_samples, int num_channels)
+{
+    switch (num_channels) {
+    case 3:
+        remap_channels_vorbis_3(samples, num_samples);
+        break;
+    case 5:
+        remap_channels_vorbis_5(samples, num_samples);
+        break;
+    case 6:
+        remap_channels_vorbis_5_1(samples, num_samples);
+        break;
+    case 7:
+        remap_channels_vorbis_7(samples, num_samples);
+        break;
+    case 8:
+        remap_channels_vorbis_7_1(samples, num_samples);
+        break;
+    }
+}
diff --git a/src/codecs/remap_channels.h b/src/codecs/remap_channels.h
new file mode 100644
index 000000000..235364871
--- /dev/null
+++ b/src/codecs/remap_channels.h
@@ -0,0 +1,29 @@
+/*
+  SDL_mixer:  An audio mixer library based on the SDL library
+  Copyright (C) 2026 Daniel K. O. <github.com/dkosmari>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef REMAP_CHANNELS_H_
+#define REMAP_CHANNELS_H_
+
+#include "SDL_types.h"
+
+extern void remap_channels_vorbis(Sint16 *samples, int num_samples, int num_channels);
+
+#endif /* CHANNEL_REMAP_H_ */