From e2f9a2081a53114ff2135a5a94a024e0c8fcd257 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 2 Mar 2026 15:52:45 -0500
Subject: [PATCH] wav: Attempt to remap channels if expected speaker layout
doesn't match SDL's.
Reference Issue #804.
---
src/decoder_wav.c | 195 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 195 insertions(+)
diff --git a/src/decoder_wav.c b/src/decoder_wav.c
index 97cb7ca5..2a0c2ea5 100644
--- a/src/decoder_wav.c
+++ b/src/decoder_wav.c
@@ -64,6 +64,21 @@
#define WAVE_MONO 1
#define WAVE_STEREO 2
+
+// channel mask bits in WAV file
+#define WAV_SPEAKER_FRONT_LEFT (1 << 0)
+#define WAV_SPEAKER_FRONT_RIGHT (1 << 1)
+#define WAV_SPEAKER_FRONT_CENTER (1 << 2)
+#define WAV_SPEAKER_LOW_FREQUENCY (1 << 3)
+#define WAV_SPEAKER_BACK_LEFT (1 << 4)
+#define WAV_SPEAKER_BACK_RIGHT (1 << 5)
+#define WAV_SPEAKER_FRONT_LEFT_OF_CENTER (1 << 6)
+#define WAV_SPEAKER_FRONT_RIGHT_OF_CENTER (1 << 7)
+#define WAV_SPEAKER_BACK_CENTER (1 << 8)
+#define WAV_SPEAKER_SIDE_LEFT (1 << 9)
+#define WAV_SPEAKER_SIDE_RIGHT (1 << 10)
+
+
#pragma pack(push, 1)
typedef struct {
// Not saved in the chunk we read:
@@ -199,6 +214,7 @@ typedef struct WAV_AudioData
WAVLoopPoint *loops;
unsigned int num_seekblocks;
WAVSeekBlock *seekblocks;
+ Uint32 channelmask;
} WAV_AudioData;
struct WAV_TrackData
@@ -209,6 +225,8 @@ struct WAV_TrackData
const WAVSeekBlock *seekblock; // current seekblock we're decoding.
Uint32 current_iteration; // current loop iteration in seekblock
Uint32 current_iteration_frames; // current framecount into seekblock.
+ int channels;
+ bool must_set_channel_map;
};
static bool IsADPCM(const Uint16 encoding)
@@ -807,6 +825,22 @@ static int FetchFloat64LE(WAV_TrackData *tdata, Uint8 *buffer, int buflen)
return length / 2;
}
+static Uint32 StandardSDLWavChannelMask(int channels)
+{
+ switch (channels) {
+ case 1: return WAV_SPEAKER_FRONT_CENTER;
+ case 2: return WAV_SPEAKER_FRONT_LEFT|WAV_SPEAKER_FRONT_RIGHT;
+ case 3: return WAV_SPEAKER_FRONT_LEFT|WAV_SPEAKER_FRONT_RIGHT|WAV_SPEAKER_LOW_FREQUENCY;
+ case 4: return WAV_SPEAKER_FRONT_LEFT|WAV_SPEAKER_FRONT_RIGHT|WAV_SPEAKER_BACK_LEFT|WAV_SPEAKER_BACK_RIGHT;
+ case 5: return WAV_SPEAKER_FRONT_LEFT|WAV_SPEAKER_FRONT_RIGHT|WAV_SPEAKER_BACK_LEFT|WAV_SPEAKER_BACK_RIGHT|WAV_SPEAKER_LOW_FREQUENCY;
+ case 6: return WAV_SPEAKER_FRONT_LEFT|WAV_SPEAKER_FRONT_RIGHT|WAV_SPEAKER_FRONT_CENTER|WAV_SPEAKER_BACK_LEFT|WAV_SPEAKER_BACK_RIGHT|WAV_SPEAKER_LOW_FREQUENCY;
+ case 7: return WAV_SPEAKER_FRONT_LEFT|WAV_SPEAKER_FRONT_RIGHT|WAV_SPEAKER_FRONT_CENTER|WAV_SPEAKER_BACK_CENTER|WAV_SPEAKER_SIDE_LEFT|WAV_SPEAKER_SIDE_RIGHT|WAV_SPEAKER_LOW_FREQUENCY;
+ case 8: return WAV_SPEAKER_FRONT_LEFT|WAV_SPEAKER_FRONT_RIGHT|WAV_SPEAKER_FRONT_CENTER|WAV_SPEAKER_BACK_LEFT|WAV_SPEAKER_BACK_RIGHT|WAV_SPEAKER_SIDE_LEFT|WAV_SPEAKER_SIDE_RIGHT|WAV_SPEAKER_LOW_FREQUENCY;
+ default: return (channels < 32) ? (Uint32)((1 << channels) - 1) : (Uint32) 0xFFFFFFFF; // mark all available channels as used by default.
+ }
+ SDL_assert(!"shouldn't hit this.");
+ return 0;
+}
static bool ParseFMT(WAV_AudioData *adata, SDL_IOStream *io, SDL_AudioSpec *spec, Uint32 chunk_length)
{
@@ -838,6 +872,9 @@ static bool ParseFMT(WAV_AudioData *adata, SDL_IOStream *io, SDL_AudioSpec *spec
return SDL_SetError("Wave format chunk too small");
}
adata->encoding = (Uint16)SDL_Swap32LE(fmt.subencoding);
+ adata->channelmask = SDL_Swap32LE(fmt.channelsmask);
+ } else {
+ adata->channelmask = StandardSDLWavChannelMask(fmt.format.channels);
}
// Decode the audio data format
@@ -1350,6 +1387,156 @@ static bool SDLCALL WAV_init_audio(SDL_IOStream *io, SDL_AudioSpec *spec, SDL_Pr
return true;
}
+static int SpeakerBitToSDLChannelIndex(int channels, Uint32 newbit)
+{
+ switch (channels) {
+ case 1:
+ return 0; // always speaker 0, I dunno.
+
+ case 2:
+ switch (newbit) {
+ case WAV_SPEAKER_FRONT_LEFT: return 0;
+ case WAV_SPEAKER_FRONT_RIGHT: return 1;
+ default: break;
+ }
+ return -1;
+
+ case 3:
+ switch (newbit) {
+ case WAV_SPEAKER_FRONT_LEFT: return 0;
+ case WAV_SPEAKER_FRONT_RIGHT: return 1;
+ case WAV_SPEAKER_LOW_FREQUENCY: return 2;
+ default: break;
+ }
+ return -1;
+
+ case 4:
+ switch (newbit) {
+ case WAV_SPEAKER_FRONT_LEFT: return 0;
+ case WAV_SPEAKER_FRONT_RIGHT: return 1;
+ case WAV_SPEAKER_BACK_LEFT: return 2;
+ case WAV_SPEAKER_BACK_RIGHT: return 3;
+ default: break;
+ }
+ return -1;
+
+ case 5:
+ switch (newbit) {
+ case WAV_SPEAKER_FRONT_LEFT: return 0;
+ case WAV_SPEAKER_FRONT_RIGHT: return 1;
+ case WAV_SPEAKER_LOW_FREQUENCY: return 2;
+ case WAV_SPEAKER_BACK_LEFT: return 3;
+ case WAV_SPEAKER_BACK_RIGHT: return 4;
+ default: break;
+ }
+ return -1;
+
+ case 6:
+ switch (newbit) {
+ case WAV_SPEAKER_FRONT_LEFT: return 0;
+ case WAV_SPEAKER_FRONT_RIGHT: return 1;
+ case WAV_SPEAKER_FRONT_CENTER: return 2;
+ case WAV_SPEAKER_LOW_FREQUENCY: return 3;
+ case WAV_SPEAKER_BACK_LEFT: return 4;
+ case WAV_SPEAKER_BACK_RIGHT: return 5;
+ default: break;
+ }
+ return -1;
+
+ case 7:
+ switch (newbit) {
+ case WAV_SPEAKER_FRONT_LEFT: return 0;
+ case WAV_SPEAKER_FRONT_RIGHT: return 1;
+ case WAV_SPEAKER_FRONT_CENTER: return 2;
+ case WAV_SPEAKER_LOW_FREQUENCY: return 3;
+ case WAV_SPEAKER_BACK_CENTER: return 4;
+ case WAV_SPEAKER_SIDE_LEFT: return 5;
+ case WAV_SPEAKER_SIDE_RIGHT: return 6;
+ default: break;
+ }
+ return -1;
+
+ case 8:
+ switch (newbit) {
+ case WAV_SPEAKER_FRONT_LEFT: return 0;
+ case WAV_SPEAKER_FRONT_RIGHT: return 1;
+ case WAV_SPEAKER_FRONT_CENTER: return 2;
+ case WAV_SPEAKER_LOW_FREQUENCY: return 3;
+ case WAV_SPEAKER_BACK_LEFT: return 4;
+ case WAV_SPEAKER_BACK_RIGHT: return 5;
+ case WAV_SPEAKER_SIDE_LEFT: return 6;
+ case WAV_SPEAKER_SIDE_RIGHT: return 7;
+ default: break;
+ }
+ return -1;
+
+ default: break;
+ }
+
+ return -1;
+}
+
+
+static void SetAudioStreamChannelMapForWav(SDL_AudioStream *stream, int channels, Uint32 channelmask)
+{
+ // WAV files can provide whatever channels they want, setting the provided channels in a bitmask,
+ // but they have to provide the data for those channels a specific order.
+ // Generally this lines up with SDL, but we need to make sure that unexpected channels in the
+ // bitmask are handled.
+
+ if (channels == 1) {
+ return; // don't remap mono stream (what would you remap it to!?), in case something reports front-center vs front-left or whatever.
+ }
+
+ Uint32 standardmap = StandardSDLWavChannelMask(channels);
+ if (channelmask == standardmap) {
+ return; // no remapping needed!
+ }
+
+ int chmap[32];
+ channels = SDL_min(channels, SDL_arraysize(chmap));
+
+ int current_channel = 0;
+ for (int i = 0; i < 32; i++) {
+ const Uint32 channelbit = channelmask & (1 << i);
+
+ if (channelbit) { // wav file uses this speaker?
+ int remapping = -1; // drop it by default.
+ if (standardmap & (1 << i)) { // SDL offers this same speaker.
+ remapping = SpeakerBitToSDLChannelIndex(channels, channelbit);
+ } else {
+ // see if we can bump this channel into something unused that we _do_ have (like, side_left can become back_left, better than nothing).
+ Uint32 newbit = 0; // no remapping by default.
+ switch (channelbit) {
+ case WAV_SPEAKER_BACK_LEFT: if (!(standardmap & WAV_SPEAKER_SIDE_LEFT)) { newbit = WAV_SPEAKER_SIDE_LEFT; } break;
+ case WAV_SPEAKER_BACK_RIGHT: if (!(standardmap & WAV_SPEAKER_SIDE_RIGHT)) { newbit = WAV_SPEAKER_SIDE_RIGHT; } break;
+ case WAV_SPEAKER_FRONT_LEFT_OF_CENTER: if (!(standardmap & WAV_SPEAKER_FRONT_LEFT)) { newbit = WAV_SPEAKER_FRONT_LEFT; } break;
+ case WAV_SPEAKER_FRONT_RIGHT_OF_CENTER: if (!(standardmap & WAV_SPEAKER_FRONT_RIGHT)) { newbit = WAV_SPEAKER_FRONT_RIGHT; } break;
+ case WAV_SPEAKER_SIDE_LEFT: if (!(standardmap & WAV_SPEAKER_BACK_LEFT)) { newbit = WAV_SPEAKER_BACK_LEFT; } break;
+ case WAV_SPEAKER_SIDE_RIGHT: if (!(standardmap & WAV_SPEAKER_BACK_RIGHT)) { newbit = WAV_SPEAKER_BACK_RIGHT; } break;
+ default: break;
+ }
+ if (newbit) {
+ remapping = SpeakerBitToSDLChannelIndex(channels, newbit);
+ standardmap |= newbit; // mark it as used so we don't try to use it for a later missing speaker.
+ }
+ }
+
+ chmap[current_channel++] = remapping;
+ if (current_channel >= channels) {
+ break; // we got them all.
+ }
+ }
+ }
+
+ while (current_channel < channels) {
+ chmap[current_channel++] = -1; // dump anything that wasn't set up for some reason.
+ }
+
+ SDL_SetAudioStreamInputChannelMap(stream, chmap, channels);
+}
+
+
static bool SDLCALL WAV_init_track(void *audio_userdata, SDL_IOStream *io, const SDL_AudioSpec *spec, SDL_PropertiesID props, void **track_userdata)
{
const WAV_AudioData *adata = (const WAV_AudioData *) audio_userdata;
@@ -1363,6 +1550,8 @@ static bool SDLCALL WAV_init_track(void *audio_userdata, SDL_IOStream *io, const
tdata->seekblock = &adata->seekblocks[0];
tdata->current_iteration = 0;
tdata->current_iteration_frames = 0;
+ tdata->must_set_channel_map = true; // !!! FIXME: why aren't we passing the AudioStream in during init_track, so we don't have to do this check?
+ tdata->channels = spec->channels; // !!! FIXME: why aren't we passing the AudioStream in during init_track, so we don't have to do this check?
ADPCM_DecoderState *state = &tdata->adpcm_state;
state->info = &adata->adpcm_info;
@@ -1410,6 +1599,12 @@ static bool SDLCALL WAV_decode(void *track_userdata, SDL_AudioStream *stream)
WAV_TrackData *tdata = (WAV_TrackData *) track_userdata;
const WAVSeekBlock *seekblock = tdata->seekblock;
+ // !!! FIXME: why aren't we passing the AudioStream in during init_track, so we don't have to do this check?
+ if (tdata->must_set_channel_map) {
+ SetAudioStreamChannelMapForWav(stream, tdata->channels, tdata->adata->channelmask);
+ tdata->must_set_channel_map = false;
+ }
+
// see if we are at the end of a loop, etc.
SDL_assert(tdata->current_iteration_frames <= seekblock->num_frames);
while (tdata->current_iteration_frames == seekblock->num_frames) {