Skip to content

Commit 9e60a89

Browse files
committed
audio: Allow streams to change the device-side channels maps.
Fixes #11881.
1 parent b2793a2 commit 9e60a89

File tree

4 files changed

+67
-10
lines changed

4 files changed

+67
-10
lines changed

include/SDL3/SDL_audio.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,7 @@ extern SDL_DECLSPEC void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID devid);
942942
* Binding a stream to a device will set its output format for playback
943943
* devices, and its input format for recording devices, so they match the
944944
* device's settings. The caller is welcome to change the other end of the
945-
* stream's format at any time.
945+
* stream's format at any time with SDL_SetAudioStreamFormat().
946946
*
947947
* \param devid an audio device to bind a stream to.
948948
* \param streams an array of audio streams to bind.
@@ -1104,6 +1104,12 @@ extern SDL_DECLSPEC bool SDLCALL SDL_GetAudioStreamFormat(SDL_AudioStream *strea
11041104
* next sound file, and start putting that new data while the previous sound
11051105
* file is still queued, and everything will still play back correctly.
11061106
*
1107+
* If a stream is bound to a device, then the format of the side of the stream
1108+
* bound to a device cannot be changed (src_spec for recording devices,
1109+
* dst_spec for playback devices). Attempts to make a change to this side
1110+
* will be ignored, but this will not report an error. The other side's format
1111+
* can be changed.
1112+
*
11071113
* \param stream the stream the format is being changed.
11081114
* \param src_spec the new format of the audio input; if NULL, it is not
11091115
* changed.
@@ -1298,6 +1304,11 @@ extern SDL_DECLSPEC int * SDLCALL SDL_GetAudioStreamOutputChannelMap(SDL_AudioSt
12981304
* race condition hasn't changed the format while this call is setting the
12991305
* channel map.
13001306
*
1307+
* Unlike attempting to change the stream's format, the input channel map on a
1308+
* stream bound to a recording device is permitted to change at any time; any
1309+
* data added to the stream from the device after this call will have the new
1310+
* mapping, but previously-added data will still have the prior mapping.
1311+
*
13011312
* \param stream the SDL_AudioStream to change.
13021313
* \param chmap the new channel map, NULL to reset to default.
13031314
* \param count The number of channels in the map.
@@ -1349,6 +1360,13 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamInputChannelMap(SDL_AudioStre
13491360
* race condition hasn't changed the format while this call is setting the
13501361
* channel map.
13511362
*
1363+
* Unlike attempting to change the stream's format, the output channel map on
1364+
* a stream bound to a recording device is permitted to change at any time;
1365+
* any data added to the stream after this call will have the new mapping, but
1366+
* previously-added data will still have the prior mapping. When the channel
1367+
* map doesn't match the hardware's channel layout, SDL will convert the data
1368+
* before feeding it to the device for playback.
1369+
*
13521370
* \param stream the SDL_AudioStream to change.
13531371
* \param chmap the new channel map, NULL to reset to default.
13541372
* \param count The number of channels in the map.

src/audio/SDL_audio.c

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,18 @@ bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b, const i
281281
return true;
282282
}
283283

284+
bool SDL_AudioChannelMapsEqual(int channels, const int *channel_map_a, const int *channel_map_b)
285+
{
286+
if (channel_map_a == channel_map_b) {
287+
return true;
288+
} else if ((channel_map_a != NULL) != (channel_map_b != NULL)) {
289+
return false;
290+
} else if (channel_map_a && (SDL_memcmp(channel_map_a, channel_map_b, sizeof (*channel_map_a) * channels) != 0)) {
291+
return false;
292+
}
293+
return true;
294+
}
295+
284296

285297
// Zombie device implementation...
286298

@@ -1134,7 +1146,7 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
11341146
SDL_AudioStream *stream = logdev->bound_streams;
11351147

11361148
// We should have updated this elsewhere if the format changed!
1137-
SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec, stream->dst_chmap, device->chmap));
1149+
SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec, NULL, NULL));
11381150

11391151
const int br = SDL_GetAtomicInt(&logdev->paused) ? 0 : SDL_GetAudioStreamDataAdjustGain(stream, device_buffer, buffer_size, logdev->gain);
11401152
if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow.
@@ -1143,6 +1155,12 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
11431155
} else if (br < buffer_size) {
11441156
SDL_memset(device_buffer + br, device->silence_value, buffer_size - br); // silence whatever we didn't write to.
11451157
}
1158+
1159+
// generally channel maps will line up, but if the audio stream's chmap has been explicitly changed, do a final swizzle to device layout.
1160+
if ((br > 0) && (!SDL_AudioChannelMapsEqual(device->spec.channels, stream->dst_chmap, device->chmap))) {
1161+
ConvertAudio(br / SDL_AUDIO_FRAMESIZE(device->spec), device_buffer, device->spec.format, device->spec.channels, NULL,
1162+
device_buffer, device->spec.format, device->spec.channels, device->chmap, NULL, 1.0f);
1163+
}
11461164
} else { // need to actually mix (or silence the buffer)
11471165
float *final_mix_buffer = (float *) ((device->spec.format == SDL_AUDIO_F32) ? device_buffer : device->mix_buffer);
11481166
const int needed_samples = buffer_size / SDL_AUDIO_BYTESIZE(device->spec.format);
@@ -1170,7 +1188,7 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
11701188

11711189
for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) {
11721190
// We should have updated this elsewhere if the format changed!
1173-
SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec, stream->dst_chmap, device->chmap));
1191+
SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec, NULL, NULL));
11741192

11751193
/* this will hold a lock on `stream` while getting. We don't explicitly lock the streams
11761194
for iterating here because the binding linked list can only change while the device lock is held.
@@ -1181,6 +1199,11 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device)
11811199
failed = true;
11821200
break;
11831201
} else if (br > 0) { // it's okay if we get less than requested, we mix what we have.
1202+
// generally channel maps will line up, but if the audio stream's chmap has been explicitly changed, do a final swizzle to device layout.
1203+
if (!SDL_AudioChannelMapsEqual(device->spec.channels, stream->dst_chmap, device->chmap)) {
1204+
ConvertAudio(br / SDL_AUDIO_FRAMESIZE(device->spec), device->work_buffer, device->spec.format, device->spec.channels, NULL,
1205+
device->work_buffer, device->spec.format, device->spec.channels, device->chmap, NULL, 1.0f);
1206+
}
11841207
MixFloat32Audio(mix_buffer, (float *) device->work_buffer, br);
11851208
}
11861209
}
@@ -1303,11 +1326,20 @@ bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device)
13031326
SDL_assert(stream->src_spec.channels == device->spec.channels);
13041327
SDL_assert(stream->src_spec.freq == device->spec.freq);
13051328

1329+
void *final_buf = output_buffer;
1330+
1331+
// generally channel maps will line up, but if the audio stream's chmap has been explicitly changed, do a final swizzle to stream layout.
1332+
if (!SDL_AudioChannelMapsEqual(device->spec.channels, stream->src_chmap, device->chmap)) {
1333+
final_buf = device->mix_buffer; // this is otherwise unused on recording devices, so it makes convenient scratch space here.
1334+
ConvertAudio(br / SDL_AUDIO_FRAMESIZE(device->spec), output_buffer, device->spec.format, device->spec.channels, NULL,
1335+
final_buf, device->spec.format, device->spec.channels, stream->src_chmap, NULL, 1.0f);
1336+
}
1337+
13061338
/* this will hold a lock on `stream` while putting. We don't explicitly lock the streams
13071339
for iterating here because the binding linked list can only change while the device lock is held.
13081340
(we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind
13091341
the same stream to different devices at the same time, though.) */
1310-
if (!SDL_PutAudioStreamData(stream, output_buffer, br)) {
1342+
if (!SDL_PutAudioStreamData(stream, final_buf, br)) {
13111343
// oh crud, we probably ran out of memory. This is possibly an overreaction to kill the audio device, but it's likely the whole thing is going down in a moment anyhow.
13121344
failed = true;
13131345
break;

src/audio/SDL_audiocvt.c

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ static void SwizzleAudio(const int num_frames, void *dst, const void *src, int c
154154
{
155155
const int bitsize = (int) SDL_AUDIO_BITSIZE(fmt);
156156

157-
bool has_null_mappings = false;
157+
bool has_null_mappings = false; // !!! FIXME: calculate this when setting the channel map instead.
158158
for (int i = 0; i < channels; i++) {
159159
if (map[i] == -1) {
160160
has_null_mappings = true;
@@ -257,6 +257,11 @@ void ConvertAudio(int num_frames,
257257
const int dst_bitsize = (int) SDL_AUDIO_BITSIZE(dst_format);
258258
const int dst_sample_frame_size = (dst_bitsize / 8) * dst_channels;
259259

260+
const bool chmaps_match = (src_channels == dst_channels) && SDL_AudioChannelMapsEqual(src_channels, src_map, dst_map);
261+
if (chmaps_match) {
262+
src_map = dst_map = NULL; // NULL both these out so we don't do any unnecessary swizzling.
263+
}
264+
260265
/* Type conversion goes like this now:
261266
- swizzle through source channel map to "standard" layout.
262267
- byteswap to CPU native format first if necessary.
@@ -635,8 +640,6 @@ bool SetAudioStreamChannelMap(SDL_AudioStream *stream, const SDL_AudioSpec *spec
635640
// already have this map, don't allocate/copy it again.
636641
} else if (SDL_ChannelMapIsBogus(chmap, channels)) {
637642
result = SDL_SetError("Invalid channel mapping");
638-
} else if ((isinput != -1) && stream->bound_device && (!!isinput == !!stream->bound_device->physical_device->recording)) {
639-
// quietly refuse to change the format of the end currently bound to a device.
640643
} else {
641644
if (SDL_ChannelMapIsDefault(chmap, channels)) {
642645
chmap = NULL; // just apply a default mapping.
@@ -661,12 +664,12 @@ bool SetAudioStreamChannelMap(SDL_AudioStream *stream, const SDL_AudioSpec *spec
661664

662665
bool SDL_SetAudioStreamInputChannelMap(SDL_AudioStream *stream, const int *chmap, int channels)
663666
{
664-
return SetAudioStreamChannelMap(stream, &stream->src_spec, &stream->src_chmap, chmap, channels, true);
667+
return SetAudioStreamChannelMap(stream, &stream->src_spec, &stream->src_chmap, chmap, channels, 1);
665668
}
666669

667670
bool SDL_SetAudioStreamOutputChannelMap(SDL_AudioStream *stream, const int *chmap, int channels)
668671
{
669-
return SetAudioStreamChannelMap(stream, &stream->dst_spec, &stream->dst_chmap, chmap, channels, false);
672+
return SetAudioStreamChannelMap(stream, &stream->dst_spec, &stream->dst_chmap, chmap, channels, 0);
670673
}
671674

672675
int *SDL_GetAudioStreamInputChannelMap(SDL_AudioStream *stream, int *count)

src/audio/SDL_sysaudio.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,13 @@ extern void ConvertAudio(int num_frames,
125125

126126
// Compare two SDL_AudioSpecs, return true if they match exactly.
127127
// Using SDL_memcmp directly isn't safe, since potential padding might not be initialized.
128-
// either channel maps can be NULL for the default (and both should be if you don't care about them).
128+
// either channel map can be NULL for the default (and both should be if you don't care about them).
129129
extern bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b, const int *channel_map_a, const int *channel_map_b);
130130

131+
// See if two channel maps match
132+
// either channel map can be NULL for the default (and both should be if you don't care about them).
133+
extern bool SDL_AudioChannelMapsEqual(int channels, const int *channel_map_a, const int *channel_map_b);
134+
131135
// allocate+copy a channel map.
132136
extern int *SDL_ChannelMapDup(const int *origchmap, int channels);
133137

0 commit comments

Comments
 (0)