Repository: libsdl-org/SDL
Pull request #12872: audio: Added SDL_PutAudioStreamSeparatedData.
Author: login: icculus
Created: 2025-04-22T07:00:51Z | Merged: 2025-04-23T03:30:50Z

Fixes #12846.

I'm not in love with the new function name, but I couldn't think of anything better.

I resisted the urge to plumb the separated channel arrays all the way into SDL_AudioTrack. I decided it would add complexity for not much performance gain, and just interleaving the data upfront was both easier and cleaner.

There's an obvious FIXME for SIMD code. My suspicion is an SSE unpack (or ARM NEON zip) implementation would be a massive speed boost over the generic for-loop, at least for stereo data, but that can be done later, or never, as appropriate.


CC'ing @0x1F9F1, in case any of this is interesting.

Linked issue #12846: audio: Add a separated-channel version of SDL_PutAudioStreamData()
Several audio decoders (libFLAC, libvorbis, etc) want to provide their decoded PCM output as arrays of channels, so to get the usual PCM stream, you have to interleave them:

```c
// a given sample is at `pcm_channels[channel_num][frame_num]`.
const float first_stereo_sample_frame[] = { pcm_channels[0][0], pcm_channels[1][0] };
const float second_stereo_sample_frame[] = { pcm_channels[0][1], pcm_channels[1][1] };
const float third_stereo_sample_frame[] = { pcm_channels[0][2], pcm_channels[1][2] };
```

It might be nice if SDL_AudioStream could accept input as these original separate arrays and manage interleaving into a single PCM buffer itself.

This allows some garbage code to be removed from SDL_mixer, but also allows us to have a central point where we can manage interleaving ([possibly using SIMD](https://www.felixcloutier.com/x86/unpckhps)), and maybe move the newly-allocated interleaved buffer directly into an SDL_AudioStream track without an extra copy.

Something like:

```c
bool SDL_PutAudioStreamDataSeparateChannels(SDL_AudioStream *stream, const void **channels, int len);
```

Not (currently) interested in an equivalent for _getting_ data; this just makes it easier to get non-interleaved data into the existing system. Once it goes into the stream, it gets interleaved.

Diff:
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 58406f6221bd4..6efc021546f96 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -141,6 +141,7 @@ add_sdl_example_executable(audio-simple-playback SOURCES audio/01-simple-playbac
 add_sdl_example_executable(audio-simple-playback-callback SOURCES audio/02-simple-playback-callback/simple-playback-callback.c)
 add_sdl_example_executable(audio-load-wav SOURCES audio/03-load-wav/load-wav.c DATAFILES ${CMAKE_CURRENT_SOURCE_DIR}/../test/sample.wav)
 add_sdl_example_executable(audio-multiple-streams SOURCES audio/04-multiple-streams/multiple-streams.c DATAFILES ${CMAKE_CURRENT_SOURCE_DIR}/../test/sample.wav ${CMAKE_CURRENT_SOURCE_DIR}/../test/sword.wav)
+add_sdl_example_executable(audio-planar-data SOURCES audio/05-planar-data/planar-data.c)
 add_sdl_example_executable(input-joystick-polling SOURCES input/01-joystick-polling/joystick-polling.c)
 add_sdl_example_executable(input-joystick-events SOURCES input/02-joystick-events/joystick-events.c)
 add_sdl_example_executable(camera-read-and-draw SOURCES camera/01-read-and-draw/read-and-draw.c)
diff --git a/examples/audio/05-planar-data/README.txt b/examples/audio/05-planar-data/README.txt
new file mode 100644
index 0000000000000..c7bd0ab5e862b
--- /dev/null
+++ b/examples/audio/05-planar-data/README.txt
@@ -0,0 +1,7 @@
+This example code draws two clickable buttons. Each causes a sound to play,
+fed to either the left or right audio channel through separate (planar)
+arrays.
+
+Planar audio can feed both channels at the same time from different arrays,
+as well, but this example only uses one channel at a time for clarity. A
+NULL array will supply silence for that channel.
diff --git a/examples/audio/05-planar-data/onmouseover.webp b/examples/audio/05-planar-data/onmouseover.webp
new file mode 100644
index 0000000000000..90880c9e45448
Binary files /dev/null and b/examples/audio/05-planar-data/onmouseover.webp differ
diff --git a/examples/audio/05-planar-data/planar-data.c b/examples/audio/05-planar-data/planar-data.c
new file mode 100644
index 0000000000000..ab67999ab92bc
--- /dev/null
+++ b/examples/audio/05-planar-data/planar-data.c
@@ -0,0 +1,366 @@
+/*
+ * This example code draws two clickable buttons. Each causes a sound to play,
+ * fed to either the left or right audio channel through separate ("planar")
+ * arrays.
+ *
+ * This code is public domain. Feel free to use it for any purpose!
+ */
+
+#define SDL_MAIN_USE_CALLBACKS 1  /* use the callbacks instead of main() */
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+/* We will use this renderer to draw into this window every frame. */
+static SDL_Window *window = NULL;
+static SDL_Renderer *renderer = NULL;
+static SDL_AudioStream *stream = NULL;
+
+/* location of buttons on the screen. */
+static const SDL_FRect rect_left_button = { 100, 170, 100, 100 };
+static const SDL_FRect rect_right_button = { 440, 170, 100, 100 };
+
+/* -1 if we're currently playing left, 1 if playing right, 0 if not playing. */
+static int playing_sound = 0;
+
+/* Raw audio data. These arrays are at the end of the source file. */
+static const Uint8 left[1870];
+static const Uint8 right[1777];
+
+
+/* This function runs once at startup. */
+SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
+{
+    const SDL_AudioSpec spec = { SDL_AUDIO_U8, 2, 4000 };  /* Uint8 data, stereo, 4000Hz. */
+
+    SDL_SetAppMetadata("Example Audio Planar Data", "1.0", "com.example.audio-planar-data");
+
+    if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
+        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
+        return SDL_APP_FAILURE;
+    }
+
+    if (!SDL_CreateWindowAndRenderer("examples/audio/planar-data", 640, 480, 0, &window, &renderer)) {
+        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
+        return SDL_APP_FAILURE;
+    }
+
+    stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL);
+    if (!stream) {
+        SDL_Log("Couldn't open audio device stream: %s", SDL_GetError());
+        return SDL_APP_FAILURE;
+    }
+
+    SDL_ResumeAudioStreamDevice(stream);  /* SDL_OpenAudioDeviceStream starts the device paused. Resume it! */
+
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs when a new event (mouse input, keypresses, etc) occurs. */
+SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
+{
+    if (event->type == SDL_EVENT_QUIT) {
+        return SDL_APP_SUCCESS;  /* end the program, reporting success to the OS. */
+    } else if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
+        if (playing_sound == 0) {  /* nothing currently playing? */
+            const SDL_FPoint point = { event->button.x, event->button.y };
+            if (SDL_PointInRectFloat(&point, &rect_left_button)) {  /* clicked left button? */
+                const Uint8 *planes[] = { left, NULL };  /* specify NULL to say "this specific channel is silent" */
+                SDL_PutAudioStreamPlanarData(stream, (const void * const *) planes, SDL_arraysize(left));
+                SDL_FlushAudioStream(stream);  /* that's all we're playing until it completes. */
+                playing_sound = -1;  /* left is playing */
+            } else if (SDL_PointInRectFloat(&point, &rect_right_button)) {  /* clicked right button? */
+                const Uint8 *planes[] = { NULL, right };  /* specify NULL to say "this specific channel is silent" */
+                SDL_PutAudioStreamPlanarData(stream, (const void * const *) planes, SDL_arraysize(right));
+                SDL_FlushAudioStream(stream);  /* that's all we're playing until it completes. */
+                playing_sound = 1;  /* right is playing */
+            }
+        }
+    }
+
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+static void render_button(const SDL_FRect *rect, const char *str, int button_value)
+{
+    float x, y;
+
+    if (playing_sound == button_value) {
+        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);  /* green while playing */
+    } else {
+        SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);  /* blue while not playing */
+    }
+
+    SDL_RenderFillRect(renderer, rect);
+    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+
+    x = rect->x + ((rect->w - (SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE * SDL_strlen(str))) / 2.0f);
+    y = rect->y + ((rect->h - SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE) / 2.0f);
+    SDL_RenderDebugText(renderer, x, y, str);
+}
+
+/* This function runs once per frame, and is the heart of the program. */
+SDL_AppResult SDL_AppIterate(void *appstate)
+{
+    if (playing_sound) {
+        if (SDL_GetAudioStreamQueued(stream) == 0) {  /* sound is done? We can play a new sound now. */
+            playing_sound = 0;
+        }
+    }
+
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+    SDL_RenderClear(renderer);
+
+    render_button(&rect_left_button, "LEFT", -1);
+    render_button(&rect_right_button, "RIGHT", 1);
+
+    SDL_RenderPresent(renderer);
+
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs once at shutdown. */
+void SDL_AppQuit(void *appstate, SDL_AppResult result)
+{
+    SDL_DestroyAudioStream(stream);
+    /* SDL will clean up the window/renderer for us. */
+}
+
+
+
+/* This is the audio data, as raw PCM samples (Uint8, 1 channel, 4000Hz) packed into C byte arrays for convenience. */
+
+static const Uint8 left[1870] = {
+  0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x80, 0x81, 0x80, 0x81, 0x82, 0x82,
+  0x83, 0x83, 0x83, 0x83, 0x83, 0x82, 0x82, 0x81, 0x80, 0x80, 0x80, 0x7f, 0x7e, 0x7e, 0x7e, 0x7d,
+  0x7b, 0x7b, 0x7b, 0x7b, 0x7c, 0x7d, 0x7d, 0x7e, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x85, 0x84,
+  0x84, 0x83, 0x81, 0x7f, 0x7d, 0x7c, 0x7a, 0x7a, 0x7a, 0x77, 0x77, 0x77, 0x76, 0x76, 0x76, 0x77,
+  0x78, 0x7d, 0x82, 0x89, 0x8e, 0x92, 0x95, 0x95, 0x91, 0x8b, 0x84, 0x7d, 0x77, 0x73, 0x72, 0x72,
+  0x74, 0x75, 0x75, 0x75, 0x76, 0x74, 0x73, 0x73, 0x74, 0x79, 0x81, 0x89, 0x8f, 0x96, 0x9b, 0x9c,
+  0x98, 0x91, 0x88, 0x7e, 0x77, 0x74, 0x73, 0x74, 0x77, 0x7b, 0x7c, 0x7a, 0x77, 0x73, 0x6d, 0x69,
+  0x68, 0x6a, 0x73, 0x7f, 0x87, 0x8e, 0x99, 0xa1, 0x9e, 0x97, 0x90, 0x86, 0x7c, 0x76, 0x77, 0x7b,
+  0x80, 0x89, 0x91, 0x93, 0x91, 0x8e, 0x87, 0x7c, 0x71, 0x6b, 0x65, 0x60, 0x5d, 0x5f, 0x60, 0x61,
+  0x6b, 0x7b, 0x84, 0x8d, 0xa0, 0xae, 0xae, 0xa8, 0xa1, 0x94, 0x81, 0x73, 0x6f, 0x70, 0x74, 0x7e,
+  0x8d, 0x95, 0x97, 0x98, 0x92, 0x83, 0x72, 0x69, 0x61, 0x5a, 0x56, 0x59, 0x5d, 0x5f, 0x65, 0x75,
+  0x82, 0x87, 0x95, 0xaa, 0xb4, 0xb0, 0xaa, 0xa0, 0x8d, 0x77, 0x6c, 0x6c, 0x6d, 0x72, 0x81, 0x91,
+  0x98, 0x9a, 0x9a, 0x8f, 0x7a, 0x6a, 0x61, 0x58, 0x4f, 0x50, 0x57, 0x5b, 0x61, 0x74, 0x85, 0x8a,
+  0x96, 0xab, 0xb4, 0xae, 0xa5, 0x9c, 0x88, 0x71, 0x67, 0x69, 0x6c, 0x73, 0x85, 0x96, 0x9d, 0xa1,
+  0xa3, 0x96, 0x7f, 0x6e, 0x63, 0x56, 0x4c, 0x4d, 0x52, 0x53, 0x58, 0x6b, 0x80, 0x86, 0x92, 0xaa,
+  0xb8, 0xb4, 0xac, 0xa5, 0x90, 0x75, 0x69, 0x6a, 0x6c, 0x73, 0x86, 0x98, 0x9c, 0xa2, 0xa7, 0x99,
+  0x7f, 0x6e, 0x61, 0x54, 0x4c, 0x4b, 0x4d, 0x4f, 0x54, 0x66, 0x7c, 0x85, 0x90, 0xa9, 0xbc, 0xba,
+  0xb4, 0xac, 0x95, 0x78, 0x69, 0x67, 0x67, 0x71, 0x86, 0x99, 0x9d, 0xa4, 0xab, 0x9b, 0x7f, 0x6e,
+  0x5f, 0x50, 0x4b, 0x4e, 0x4e, 0x4e, 0x54, 0x60, 0x77, 0x86, 0x8e, 0xa4, 0xbb, 0xbf, 0xb9, 0xb3,
+  0x9e, 0x7d, 0x68, 0x65, 0x63, 0x6b, 0x84, 0x9a, 0x9d, 0xa3, 0xb0, 0x9f, 0x83, 0x71, 0x5f, 0x4d,
+  0x4c, 0x51, 0x51, 0x51, 0x56, 0x5a, 0x64, 0x7d, 0x90, 0x99, 0xad, 0xc3, 0xc2, 0xb5, 0xaa, 0x92,
+  0x71, 0x62, 0x65, 0x6a, 0x78, 0x92, 0xa2, 0xa1, 0xa7, 0xa8, 0x91, 0x78, 0x66, 0x55, 0x4a, 0x50,
+  0x54, 0x50, 0x50, 0x58, 0x5a, 0x65, 0x8b, 0x9b, 0x9b, 0xb7, 0xc9, 0xb3, 0xa6, 0xa2, 0x7d, 0x5a,
+  0x66, 0x6f, 0x70, 0x94, 0xa2, 0x90, 0x9b, 0xa5, 0x8f, 0x82, 0x77, 0x5c, 0x58, 0x60, 0x50, 0x46,
+  0x56, 0x49, 0x3a, 0x54, 0x97, 0xbe, 0xa9, 0xb0, 0xad, 0x91, 0xa7, 0xb3, 0x83, 0x6f, 0x6c, 0x5b,
+  0x71, 0x91, 0x9c, 0xac, 0x98, 0x78, 0x8a, 0xa6, 0xad, 0x9e, 0x72, 0x4d, 0x4e, 0x4f, 0x4e, 0x4a,
+  0x48, 0x46, 0x42, 0x4e, 0x99, 0xd5, 0xae, 0xb0, 0xb1, 0x8a, 0xb3, 0xbd, 0x82, 0x6b, 0x53, 0x56,
+  0x8b, 0x97, 0xa7, 0xaf, 0x74, 0x6b, 0x92, 0xaf, 0xc1, 0x8f, 0x55, 0x47, 0x4e, 0x60, 0x5e, 0x45,
+  0x4a, 0x4f, 0x3a, 0x44, 0x9f, 0xdf, 0xac, 0xa8, 0x93, 0x79, 0xbf, 0xc3, 0x92, 0x67, 0x36, 0x5a,
+  0x90, 0x9b, 0xb6, 0xa1, 0x6b, 0x68, 0x8d, 0xc3, 0xca, 0x83, 0x4f, 0x3d, 0x53, 0x72, 0x63, 0x46,
+  0x44, 0x55, 0x4f, 0x4c, 0x78, 0xcb, 0xbb, 0x93, 0x99, 0x79, 0xad, 0xd0, 0x9f, 0x70, 0x37, 0x4f,
+  0x90, 0x9e, 0xaf, 0x94, 0x73, 0x71, 0x89, 0xc0, 0xc0, 0x8f, 0x5b, 0x45, 0x62, 0x79, 0x6f, 0x5b,
+  0x46, 0x56, 0x54, 0x53, 0x59, 0x90, 0xd8, 0x95, 0x8c, 0x8c, 0x88, 0xd6, 0xb8, 0x83, 0x4c, 0x2f,
+  0x80, 0xa2, 0xaa, 0x9c, 0x69, 0x74, 0x80, 0xb0, 0xc6, 0x99, 0x78, 0x54, 0x69, 0x80, 0x7c, 0x69,
+  0x4b, 0x4e, 0x57, 0x4e, 0x4c, 0x5f, 0xae, 0xc3, 0x82, 0x86, 0x83, 0xac, 0xd9, 0xa3, 0x6a, 0x31,
+  0x50, 0xa0, 0xad, 0xa6, 0x6d, 0x59, 0x7f, 0x9e, 0xc8, 0xaf, 0x81, 0x74, 0x70, 0x8b, 0x83, 0x76,
+  0x58, 0x50, 0x56, 0x59, 0x58, 0x49, 0x62, 0x7c, 0xce, 0x99, 0x71, 0x9c, 0x8d, 0xd4, 0xb1, 0x6c,
+  0x4f, 0x37, 0x95, 0xab, 0x9b, 0x7f, 0x4b, 0x82, 0xa2, 0xba, 0xb5, 0x7b, 0x7d, 0x7d, 0x8d, 0x8b,
+  0x71, 0x62, 0x54, 0x5b, 0x4e, 0x5d, 0x4c, 0x5e, 0x57, 0x9c, 0xd4, 0x67, 0x94, 0x83, 0xa2, 0xd8,
+  0x83, 0x70, 0x2e, 0x59, 0xb5, 0x9d, 0xa1, 0x51, 0x55, 0x97, 0xad, 0xcb, 0x86, 0x77, 0x78, 0x95,
+  0xa1, 0x76, 0x6d, 0x58, 0x67, 0x5b, 0x4f, 0x66, 0x55, 0x67, 0x4e, 0x67, 0xd9, 0x88, 0x89, 0x86,
+  0x6f, 0xcd, 0x9b, 0x89, 0x4e, 0x39, 0x9f, 0xa0, 0xa9, 0x7a, 0x47, 0x88, 0x99, 0xbe, 0xac, 0x6b,
+  0x88, 0x87, 0xaf, 0x9a, 0x67, 0x71, 0x63, 0x74, 0x62, 0x55, 0x5c, 0x5e, 0x65, 0x5c, 0x54, 0xb1,
+  0xb0, 0x79, 0x8d, 0x6f, 0xac, 0xb7, 0x8e, 0x73, 0x44, 0x7b, 0xa1, 0x99, 0x90, 0x5a, 0x70, 0x97,
+  0xa0, 0xb4, 0x89, 0x83, 0x8e, 0x96, 0xa3, 0x7e, 0x6f, 0x6c, 0x6a, 0x6b, 0x5b, 0x5a, 0x61, 0x5e,
+  0x5d, 0x63, 0x66, 0xa0, 0xa6, 0x7c, 0x8d, 0x83, 0xa4, 0xad, 0x88, 0x7b, 0x58, 0x75, 0x95, 0x91,
+  0x92, 0x70, 0x75, 0x93, 0x9c, 0xab, 0x92, 0x84, 0x8d, 0x91, 0x96, 0x81, 0x70, 0x6b, 0x6c, 0x68,
+  0x62, 0x59, 0x5e, 0x69, 0x5a, 0x5a, 0x68, 0x5f, 0xa2, 0xb0, 0x6d, 0x87, 0x7e, 0xa0, 0xba, 0x89,
+  0x78, 0x53, 0x73, 0xa6, 0x9b, 0x95, 0x6c, 0x65, 0x8e, 0x9a, 0xab, 0x97, 0x7b, 0x85, 0x8e, 0x9a,
+  0x91, 0x71, 0x6b, 0x68, 0x65, 0x6e, 0x58, 0x5d, 0x70, 0x5d, 0x6d, 0x67, 0x5e, 0x80, 0x78, 0x94,
+  0x98, 0x7c, 0x96, 0x90, 0xa1, 0xa5, 0x82, 0x7f, 0x70, 0x7e, 0x94, 0x87, 0x87, 0x80, 0x88, 0x92,
+  0x8e, 0x96, 0x8c, 0x89, 0x84, 0x73, 0x72, 0x6f, 0x71, 0x6d, 0x5e, 0x61, 0x6a, 0x70, 0x77, 0x6f,
+  0x6d, 0x79, 0x76, 0x7f, 0x77, 0x75, 0x7e, 0x90, 0xa8, 0x8c, 0x85, 0x98, 0x9b, 0xa7, 0x93, 0x79,
+  0x78, 0x79, 0x91, 0x94, 0x87, 0x86, 0x85, 0x86, 0x8b, 0x89, 0x82, 0x7c, 0x74, 0x6d, 0x6c, 0x75,
+  0x75, 0x6f, 0x64, 0x69, 0x74, 0x7e, 0x83, 0x76, 0x75, 0x85, 0x8a, 0x89, 0x88, 0x78, 0x81, 0x88,
+  0x83, 0x85, 0x7e, 0x80, 0x88, 0x89, 0x8c, 0x8d, 0x8a, 0x8b, 0x88, 0x88, 0x89, 0x85, 0x81, 0x81,
+  0x7e, 0x7c, 0x7c, 0x77, 0x7d, 0x76, 0x6f, 0x7d, 0x7f, 0x78, 0x73, 0x76, 0x83, 0x84, 0x80, 0x7f,
+  0x82, 0x86, 0x80, 0x81, 0x83, 0x81, 0x81, 0x7e, 0x7d, 0x7b, 0x83, 0x8b, 0x85, 0x7a, 0x76, 0x83,
+  0x87, 0x82, 0x7d, 0x76, 0x7b, 0x80, 0x83, 0x81, 0x7a, 0x79, 0x7d, 0x82, 0x81, 0x82, 0x82, 0x83,
+  0x86, 0x80, 0x80, 0x81, 0x7e, 0x80, 0x7d, 0x7a, 0x7e, 0x81, 0x7e, 0x7e, 0x80, 0x7f, 0x81, 0x82,
+  0x80, 0x81, 0x82, 0x7f, 0x7f, 0x7d, 0x7c, 0x7f, 0x7b, 0x7b, 0x7d, 0x7a, 0x7a, 0x7e, 0x7e, 0x7c,
+  0x7c, 0x7f, 0x80, 0x7f, 0x80, 0x82, 0x81, 0x81, 0x80, 0x7e, 0x80, 0x7f, 0x81, 0x7b, 0x7c, 0x7f,
+  0x7f, 0x81, 0x7f, 0x7f, 0x80, 0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x83, 0x7e, 0x7f, 0x85, 0x81, 0x83,
+  0x84, 0x80, 0x84, 0x81, 0x81, 0x83, 0x81, 0x83, 0x80, 0x84, 0x80, 0x80, 0x85, 0x80, 0x81, 0x7f,
+  0x82, 0x82, 0x81, 0x81, 0x80, 0x81, 0x80, 0x87, 0x81, 0x7c, 0x80, 0x7f, 0x80, 0x7d, 0x7c, 0x7d,
+  0x80, 0x80, 0x80, 0x82, 0x7d, 0x81, 0x82, 0x7e, 0x82, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, 0x82,
+  0x7f, 0x80, 0x7f, 0x7f, 0x81, 0x7f, 0x80, 0x7e, 0x81, 0x80, 0x7e, 0x80, 0x7e, 0x7f, 0x80, 0x80,
+  0x82, 0x7f, 0x83, 0x83, 0x80, 0x80, 0x7f, 0x7f, 0x7f, 0x7e, 0x7e, 0x7f, 0x80, 0x80, 0x80, 0x80,
+  0x80, 0x81, 0x80, 0x7f, 0x7f, 0x7f, 0x7e, 0x7f, 0x7e, 0x7d, 0x7e, 0x7d, 0x7c, 0x7d, 0x7c, 0x7c,
+  0x7d, 0x7c, 0x7d, 0x7e, 0x7f, 0x7e, 0x7e, 0x7f, 0x7d, 0x7f, 0x7f, 0x80, 0x7f, 0x7e, 0x7f, 0x80,
+  0x7e, 0x80, 0x7e, 0x7e, 0x80, 0x7e, 0x80, 0x7e, 0x7f, 0x7e, 0x7d, 0x7f, 0x7d, 0x7d, 0x7d, 0x7d,
+  0x7d, 0x7d, 0x7e, 0x7f, 0x7f, 0x7d, 0x7e, 0x7f, 0x7e, 0x80, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x80,
+  0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x81, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x7f, 0x80, 0x7f,
+  0x7f, 0x80, 0x80, 0x7f, 0x80, 0x80, 0x7f, 0x7f, 0x80, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x80,
+  0x81, 0x80, 0x82, 0x83, 0x81, 0x82, 0x81, 0x82, 0x82, 0x82, 0x81, 0x81, 0x83, 0x82, 0x82, 0x82,
+  0x81, 0x83, 0x82, 0x81, 0x81, 0x80, 0x80, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7e, 0x80, 0x7d, 0x80,
+  0x81, 0x7e, 0x7f, 0x7f, 0x80, 0x7f, 0x7f, 0x80, 0x80, 0x80, 0x81, 0x81, 0x80, 0x81, 0x81, 0x80,
+  0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x7f, 0x7f, 0x7e, 0x7f, 0x81, 0x80, 0x7f, 0x81, 0x81, 0x82, 0x81,
+  0x80, 0x82, 0x82, 0x80, 0x81, 0x81, 0x80, 0x80, 0x7e, 0x7d, 0x7f, 0x7e, 0x81, 0x81, 0x7e, 0x7f,
+  0x82, 0x7f, 0x7d, 0x7f, 0x7d, 0x81, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x80,
+  0x7f, 0x7e, 0x7f, 0x7f, 0x7e, 0x7c, 0x7d, 0x7e, 0x7d, 0x7d, 0x7e, 0x7d, 0x7e, 0x7c, 0x7e, 0x7e,
+  0x7c, 0x7e, 0x7d, 0x7e, 0x7e, 0x7e, 0x7d, 0x7d, 0x7c, 0x7b, 0x7c, 0x7b, 0x7b, 0x7b, 0x7b, 0x7b,
+  0x7b, 0x7c, 0x7c, 0x7d, 0x7c, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d, 0x7c, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d,
+  0x7d, 0x7d, 0x7d, 0x7d, 0x7e, 0x7d, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f,
+  0x7f, 0x7f, 0x80, 0x80, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+  0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x81, 0x81, 0x81,
+  0x81, 0x81, 0x81, 0x81, 0x82, 0x82, 0x82, 0x82, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x81,
+  0x81, 0x81, 0x80, 0x80, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x82, 0x82, 0x82, 0x82, 0x82,
+  0x81, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+  0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x7f,
+  0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+  0x80, 0x80, 0x7f, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e,
+  0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f,
+  0x7f, 0x7f, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x7f,
+  0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x81, 0x80, 0x83, 0x80, 0x80, 0x80, 0x84, 0x84,
+  0x7b, 0x7e, 0x80, 0x80, 0x7e, 0x80, 0x7e, 0x7f, 0x81, 0x81, 0x80, 0x7f, 0x80, 0x7f, 0x7e, 0x7e,
+  0x7f, 0x80, 0x80, 0x7f, 0x81, 0x82, 0x80, 0x80, 0x81, 0x81, 0x80, 0x81, 0x7f, 0x80, 0x80, 0x81,
+  0x81, 0x81, 0x81, 0x84, 0x83, 0x7f, 0x7f, 0x80, 0x80, 0x7f, 0x81, 0x7e, 0x7e, 0x7f, 0x81, 0x7f,
+  0x7f, 0x80, 0x7f, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x81, 0x82, 0x82, 0x80, 0x7f, 0x80,
+  0x7f, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7e, 0x7f, 0x7f, 0x7f, 0x7d, 0x7e, 0x7e, 0x7f, 0x80,
+  0x80, 0x80, 0x80, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x81, 0x80, 0x80, 0x81, 0x7f, 0x80,
+  0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x80, 0x7f, 0x7e, 0x7e, 0x7f, 0x7f, 0x7e, 0x7e, 0x7e, 0x7f,
+  0x7e, 0x7d, 0x7d, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7e, 0x80, 0x7e, 0x7f, 0x7f,
+  0x7e, 0x7f, 0x7e, 0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x7f, 0x7e, 0x7f, 0x7f,
+  0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x7f, 0x7f, 0x81, 0x80, 0x7f, 0x80, 0x80, 0x82,
+  0x81, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x81, 0x81, 0x80, 0x81, 0x80, 0x82, 0x7f,
+  0x7f, 0x7e, 0x7e, 0x80, 0x7e, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x81, 0x80,
+  0x81, 0x80, 0x80, 0x81, 0x80, 0x83, 0x80, 0x80, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x7e, 0x80, 0x7f,
+  0x7f, 0x80, 0x7f, 0x82, 0x80, 0x81, 0x7f, 0x7e, 0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x80,
+  0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x7f, 0x80, 0x7f, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+  0x80, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x80, 0x81, 0x80, 0x80, 0x80, 0x81, 0x81, 0x7f, 0x7f, 0x7f,
+  0x7f, 0x7e, 0x7e, 0x7f, 0x7f, 0x80, 0x80, 0x80, 0x7f, 0x80, 0x81, 0x7f, 0x80, 0x7e, 0x7f, 0x7f,
+  0x7e, 0x80, 0x7e, 0x7f, 0x7f, 0x7f, 0x7f, 0x7e, 0x7e, 0x7d, 0x7f, 0x7e, 0x7f, 0x7f, 0x7f, 0x80,
+  0x7f, 0x80, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x81, 0x81, 0x81, 0x80, 0x80, 0x7f, 0x7f, 0x80, 0x7f,
+  0x7f, 0x7f, 0x81, 0x81, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x80, 0x7f, 0x7f,
+  0x7f, 0x80, 0x80, 0x7f, 0x80, 0x80, 0x81, 0x80, 0x80, 0x7f, 0x7e, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e,
+  0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x80, 0x7f, 0x80, 0x7e, 0x7f, 0x7e
+};
+
+static const Uint8 right[1777]  = {
+  0x7f, 0x7e, 0x7e, 0x7f, 0x80, 0x80, 0x80, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x82, 0x83, 0x83, 0x83,
+  0x82, 0x81, 0x81, 0x80, 0x7f, 0x7e, 0x7c, 0x7b, 0x7a, 0x7a, 0x79, 0x79, 0x79, 0x7a, 0x7a, 0x7b,
+  0x7c, 0x7e, 0x80, 0x82, 0x84, 0x86, 0x88, 0x89, 0x89, 0x89, 0x88, 0x87, 0x84, 0x82, 0x80, 0x7e,
+  0x7c, 0x7b, 0x7a, 0x7a, 0x79, 0x78, 0x77, 0x75, 0x76, 0x77, 0x78, 0x78, 0x78, 0x7b, 0x81, 0x87,
+  0x8c, 0x8e, 0x90, 0x92, 0x91, 0x8d, 0x87, 0x81, 0x7d, 0x7b, 0x7a, 0x79, 0x79, 0x7a, 0x79, 0x78,
+  0x75, 0x74, 0x75, 0x75, 0x75, 0x76, 0x76, 0x76, 0x76, 0x7b, 0x83, 0x88, 0x8b, 0x8f, 0x95, 0x98,
+  0x95, 0x8d, 0x86, 0x83, 0x80, 0x7e, 0x7c, 0x7c, 0x7e, 0x7e, 0x7c, 0x79, 0x78, 0x76, 0x75, 0x72,
+  0x73, 0x74, 0x72, 0x6f, 0x6d, 0x72, 0x7e, 0x87, 0x8b, 0x90, 0x98, 0x9f, 0x9b, 0x91, 0x85, 0x7f,
+  0x7b, 0x78, 0x79, 0x7f, 0x87, 0x8b, 0x8a, 0x89, 0x89, 0x86, 0x81, 0x79, 0x75, 0x74, 0x73, 0x73,
+  0x6f, 0x6d, 0x6e, 0x6e, 0x6e, 0x6f, 0x72, 0x77, 0x82, 0x8f, 0x95, 0x99, 0x9c, 0x9e, 0x99, 0x8c,
+  0x7f, 0x74, 0x71, 0x70, 0x74, 0x7e, 0x8a, 0x92, 0x91, 0x8f, 0x8f, 0x8d, 0x85, 0x7c, 0x76, 0x75,
+  0x76, 0x75, 0x71, 0x6d, 0x6b, 0x68, 0x64, 0x64, 0x66, 0x6e, 0x83, 0x8f, 0x93, 0x9b, 0xa3, 0xa4,
+  0x97, 0x86, 0x76, 0x6f, 0x6d, 0x6e, 0x78, 0x87, 0x94, 0x98, 0x96, 0x94, 0x91, 0x89, 0x7e, 0x74,
+  0x6f, 0x70, 0x74, 0x72, 0x6e, 0x6b, 0x67, 0x62, 0x60, 0x60, 0x69, 0x84, 0x91, 0x95, 0xa1, 0xae,
+  0xb0, 0x9b, 0x84, 0x74, 0x6a, 0x65, 0x67, 0x78, 0x8b, 0x98, 0x9f, 0x9e, 0x9a, 0x90, 0x86, 0x7c,
+  0x71, 0x6a, 0x6c, 0x73, 0x74, 0x6d, 0x69, 0x65, 0x5e, 0x5c, 0x60, 0x6f, 0x8b, 0x95, 0x9b, 0xac,
+  0xb3, 0xa5, 0x89, 0x7a, 0x6b, 0x5c, 0x5f, 0x70, 0x88, 0x97, 0xa5, 0xac, 0xa1, 0x95, 0x8e, 0x86,
+  0x76, 0x6a, 0x6b, 0x72, 0x72, 0x6c, 0x67, 0x5e, 0x55, 0x52, 0x56, 0x78, 0x9c, 0x91, 0x9c, 0xbc,
+  0xb8, 0x98, 0x83, 0x7f, 0x5e, 0x4c, 0x6c, 0x83, 0x8a, 0x9a, 0xb7, 0xae, 0x8a, 0x8f, 0x93, 0x79,
+  0x69, 0x76, 0x76, 0x69, 0x70, 0x70, 0x5b, 0x50, 0x51, 0x57, 0x52, 0x77, 0xb2, 0x90, 0x95, 0xc8,
+  0xb1, 0x8d, 0x89, 0x8a, 0x55, 0x4e, 0x87, 0x7f, 0x82, 0xb3, 0xb9, 0x8f, 0x8c, 0x9d, 0x79, 0x71,
+  0x80, 0x6a, 0x61, 0x7b, 0x70, 0x51, 0x63, 0x62, 0x3e, 0x50, 0x61, 0x9a, 0xad, 0x7e, 0xba, 0xb5,
+  0x94, 0x9f, 0x93, 0x75, 0x4b, 0x7b, 0x79, 0x6c, 0xab, 0xaf, 0x9f, 0x93, 0x8e, 0x7a, 0x7f, 0x89,
+  0x6a, 0x6e, 0x71, 0x66, 0x5e, 0x63, 0x5c, 0x53, 0x53, 0x50, 0x5a, 0xb8, 0xbd, 0x6d, 0xc3, 0xb2,
+  0x8a, 0xa7, 0xa1, 0x70, 0x4c, 0x88, 0x63, 0x7d, 0xb1, 0xa1, 0xa6, 0x8e, 0x6a, 0x7c, 0x95, 0x8b,
+  0x84, 0x72, 0x5c, 0x5c, 0x67, 0x64, 0x61, 0x56, 0x65, 0x52, 0x44, 0x80, 0xda, 0x8a, 0x88, 0xc9,
+  0x89, 0x96, 0xb1, 0x92, 0x4a, 0x6f, 0x6d, 0x78, 0xa5, 0xa7, 0xa0, 0x98, 0x66, 0x6e, 0xa6, 0x9d,
+  0x95, 0x70, 0x52, 0x57, 0x73, 0x69, 0x72, 0x5a, 0x55, 0x52, 0x50, 0x3d, 0xb8, 0xdb, 0x5d, 0xa9,
+  0xab, 0x82, 0xad, 0xc3, 0x65, 0x4c, 0x6c, 0x6d, 0x98, 0xac, 0x9f, 0x97, 0x74, 0x5a, 0xa0, 0xb1,
+  0x9e, 0x7e, 0x52, 0x54, 0x74, 0x71, 0x6a, 0x6a, 0x5a, 0x53, 0x4b, 0x46, 0x5e, 0xe5, 0xaa, 0x62,
+  0xab, 0x8f, 0x97, 0xcb, 0xa5, 0x4b, 0x4f, 0x67, 0x88, 0xa6, 0xa4, 0x98, 0x84, 0x61, 0x80, 0xb7,
+  0xb4, 0x98, 0x64, 0x4e, 0x64, 0x77, 0x72, 0x72, 0x55, 0x54, 0x4e, 0x52, 0x3c, 0x96, 0xf0, 0x69,
+  0x7f, 0xa2, 0x80, 0xc1, 0xc8, 0x75, 0x46, 0x4d, 0x74, 0xa4, 0x9e, 0x95, 0x8a, 0x6a, 0x73, 0xa6,
+  0xb7, 0xb4, 0x81, 0x60, 0x5e, 0x71, 0x7c, 0x74, 0x6b, 0x54, 0x54, 0x48, 0x4f, 0x44, 0xc3, 0xcb,
+  0x5b, 0x9b, 0x86, 0x99, 0xd4, 0xa3, 0x71, 0x3e, 0x4b, 0x91, 0x99, 0x9d, 0x95, 0x70, 0x72, 0x85,
+  0xb0, 0xbd, 0xa5, 0x7e, 0x67, 0x67, 0x78, 0x77, 0x6e, 0x63, 0x53, 0x5b, 0x39, 0x50, 0x48, 0xb5,
+  0xc2, 0x6a, 0xa5, 0x77, 0xa8, 0xbd, 0x98, 0x89, 0x3a, 0x60, 0x83, 0x85, 0xa9, 0x87, 0x87, 0x74,
+  0x77, 0xac, 0xa9, 0xb9, 0x8a, 0x71, 0x6b, 0x6d, 0x81, 0x6d, 0x66, 0x51, 0x60, 0x3c, 0x50, 0x4a,
+  0x91, 0xbf, 0x83, 0xae, 0x7a, 0xa4, 0xa1, 0x97, 0x92, 0x4b, 0x73, 0x68, 0x86, 0x8e, 0x8c, 0x95,
+  0x79, 0x83, 0x86, 0xa2, 0xab, 0xa6, 0x8d, 0x79, 0x6a, 0x75, 0x68, 0x74, 0x56, 0x5c, 0x4e, 0x4c,
+  0x49, 0x5d, 0xb1, 0x88, 0xb9, 0x8d, 0xa4, 0x90, 0x94, 0x8b, 0x66, 0x72, 0x69, 0x83, 0x7c, 0x91,
+  0x82, 0x89, 0x79, 0x87, 0x8a, 0xa1, 0x9f, 0xa5, 0x95, 0x8d, 0x7a, 0x6f, 0x6f, 0x61, 0x62, 0x58,
+  0x5f, 0x52, 0x52, 0x4f, 0x80, 0x90, 0xa1, 0xa6, 0xa3, 0x9c, 0x90, 0x86, 0x74, 0x6d, 0x6c, 0x7a,
+  0x83, 0x8a, 0x8c, 0x88, 0x7f, 0x80, 0x82, 0x8f, 0x99, 0x9e, 0xa3, 0x9a, 0x93, 0x84, 0x73, 0x68,
+  0x5d, 0x5e, 0x5d, 0x5f, 0x5e, 0x5d, 0x52, 0x6a, 0x7d, 0x8d, 0x9f, 0xa6, 0xac, 0xa0, 0x95, 0x7d,
+  0x6e, 0x64, 0x6a, 0x76, 0x81, 0x8e, 0x98, 0x94, 0x8e, 0x84, 0x84, 0x84, 0x86, 0x91, 0x98, 0x9d,
+  0x9a, 0x8c, 0x7b, 0x67, 0x5c, 0x58, 0x58, 0x5e, 0x5e, 0x64, 0x60, 0x67, 0x75, 0x7f, 0x8e, 0x99,
+  0xa2, 0xa5, 0xa2, 0x99, 0x87, 0x74, 0x6a, 0x67, 0x6e, 0x7b, 0x87, 0x96, 0x97, 0x97, 0x94, 0x8e,
+  0x8c, 0x8a, 0x8b, 0x8a, 0x8a, 0x88, 0x84, 0x7b, 0x72, 0x66, 0x5e, 0x58, 0x57, 0x5a, 0x60, 0x64,
+  0x6c, 0x78, 0x81, 0x8c, 0x96, 0x9d, 0x9f, 0xa1, 0x99, 0x8f, 0x80, 0x76, 0x70, 0x6c, 0x70, 0x76,
+  0x81, 0x8a, 0x93, 0x97, 0x98, 0x94, 0x91, 0x8c, 0x8a, 0x87, 0x81, 0x7c, 0x74, 0x71, 0x6b, 0x68,
+  0x65, 0x62, 0x60, 0x61, 0x63, 0x67, 0x6c, 0x77, 0x82, 0x8a, 0x96, 0x9c, 0xa4, 0xa5, 0xa0, 0x95,
+  0x86, 0x7b, 0x71, 0x6e, 0x6e, 0x73, 0x79, 0x82, 0x8b, 0x94, 0x99, 0x98, 0x95, 0x8e, 0x88, 0x81,
+  0x7b, 0x77, 0x73, 0x6f, 0x6c, 0x6a, 0x68, 0x67, 0x66, 0x69, 0x69, 0x6e, 0x70, 0x77, 0x81, 0x88,
+  0x91, 0x97, 0x9f, 0xa1, 0xa2, 0x9b, 0x92, 0x82, 0x77, 0x6c, 0x6a, 0x6b, 0x71, 0x79, 0x83, 0x8d,
+  0x93, 0x97, 0x96, 0x95, 0x8f, 0x8b, 0x84, 0x7d, 0x76, 0x71, 0x6a, 0x68, 0x67, 0x68, 0x6b, 0x6d,
+  0x71, 0x73, 0x76, 0x79, 0x7e, 0x83, 0x8a, 0x8f, 0x94, 0x97, 0x97, 0x95, 0x8f, 0x87, 0x7f, 0x79,
+  0x76, 0x76, 0x78, 0x7a, 0x7e, 0x81, 0x86, 0x8a, 0x8c, 0x8e, 0x8d, 0x8a, 0x86, 0x80, 0x7c, 0x78,
+  0x74, 0x71, 0x70, 0x70, 0x72, 0x74, 0x76, 0x78, 0x7a, 0x7c, 0x7e, 0x80, 0x82, 0x82, 0x81, 0x80,
+  0x7e, 0x7f, 0x81, 0x84, 0x88, 0x8b, 0x8d, 0x8d, 0x8b, 0x87, 0x83, 0x7d, 0x7a, 0x77, 0x78, 0x7a,
+  0x7e, 0x81, 0x83, 0x84, 0x84, 0x84, 0x82, 0x80, 0x7f, 0x7d, 0x7b, 0x7a, 0x78, 0x78, 0x77, 0x78,
+  0x78, 0x79, 0x7c, 0x7e, 0x81, 0x83, 0x83, 0x84, 0x83, 0x82, 0x81, 0x80, 0x80, 0x7f, 0x7f, 0x80,
+  0x80, 0x81, 0x82, 0x83, 0x84, 0x84, 0x84, 0x83, 0x83, 0x81, 0x80, 0x7f, 0x7f, 0x80, 0x80, 0x80,
+  0x7f, 0x7e, 0x7d, 0x7c, 0x7c, 0x7c, 0x7d, 0x7d, 0x7f, 0x80, 0x80, 0x81, 0x80, 0x7f, 0x7e, 0x7d,
+  0x7d, 0x7d, 0x7e, 0x80, 0x80, 0x81, 0x82, 0x82, 0x81, 0x81, 0x81, 0x81, 0x80, 0x80, 0x81, 0x80,
+  0x81, 0x80, 0x7f, 0x7f, 0x80, 0x80, 0x80, 0x81, 0x80, 0x80, 0x7e, 0x7d, 0x7d, 0x7e, 0x7e, 0x7f,
+  0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x80, 0x80, 0x80, 0x81, 0x81,
+  0x81, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f,
+  0x7f, 0x7f, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x82, 0x82, 0x82, 0x81, 0x80,
+  0x7f, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7e, 0x7f, 0x7f, 0x7f, 0x7f,
+  0x80, 0x80, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x7e, 0x7f, 0x7e, 0x7f, 0x7f, 0x7f,
+  0x80, 0x80, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x82, 0x82, 0x82, 0x82,
+  0x81, 0x81, 0x80, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x80, 0x80, 0x80, 0x80, 0x80,
+  0x80, 0x81, 0x80, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x7e, 0x7e, 0x7e,
+  0x7e, 0x7d, 0x7e, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x82, 0x82, 0x81, 0x81, 0x80, 0x7f, 0x7f, 0x7f,
+  0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x80, 0x80,
+  0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x80,
+  0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f, 0x80, 0x80, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
+  0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x80, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x7f, 0x80,
+  0x80, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x7f, 0x87, 0x83, 0x7d, 0x81, 0x80, 0x7e, 0x81, 0x7b,
+  0x7d, 0x84, 0x7f, 0x81, 0x83, 0x82, 0x7f, 0x80, 0x7c, 0x7b, 0x7d, 0x80, 0x80, 0x80, 0x80, 0x80,
+  0x80, 0x7e, 0x7f, 0x7e, 0x7f, 0x80, 0x80, 0x81, 0x82, 0x82, 0x82, 0x82, 0x80, 0x80, 0x7f, 0x7f,
+  0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x82, 0x80, 0x7f, 0x80, 0x81, 0x80, 0x81, 0x7f,
+  0x83, 0x85, 0x7f, 0x80, 0x84, 0x83, 0x7d, 0x7c, 0x7d, 0x80, 0x7d, 0x7d, 0x7e, 0x7e, 0x7d, 0x83,
+  0x81, 0x7d, 0x7d, 0x81, 0x7f, 0x7c, 0x7c, 0x7c, 0x7d, 0x7c, 0x83, 0x80, 0x84, 0x84, 0x82, 0x7d,
+  0x7f, 0x7d, 0x7c, 0x7e, 0x7e, 0x7f, 0x81, 0x84, 0x82, 0x81, 0x7e, 0x7f, 0x7f, 0x7f, 0x7e, 0x80,
+  0x81, 0x80, 0x7f, 0x80, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x80, 0x80, 0x80, 0x80, 0x81, 0x81,
+  0x7f, 0x7f, 0x7f, 0x7f, 0x81, 0x80, 0x82, 0x81, 0x83, 0x81, 0x82, 0x80, 0x80, 0x7f, 0x7f, 0x80,
+  0x7d, 0x80, 0x7e, 0x81, 0x7f, 0x81, 0x7f, 0x80, 0x7f, 0x7f, 0x7e, 0x7d, 0x81, 0x80, 0x82, 0x7f,
+  0x81, 0x7f, 0x7f, 0x7e, 0x7e, 0x7f, 0x7e, 0x80, 0x7f, 0x81, 0x7f, 0x81, 0x81, 0x81, 0x81, 0x81,
+  0x82, 0x81, 0x81, 0x80, 0x81, 0x7f, 0x80, 0x7f, 0x7f, 0x7e, 0x7e, 0x7f, 0x7e, 0x7f, 0x7f, 0x7e,
+  0x7f, 0x7e, 0x7f, 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7e, 0x80, 0x7f, 0x82, 0x80, 0x81, 0x81, 0x80,
+  0x81, 0x80, 0x81, 0x80, 0x81, 0x80, 0x81, 0x80, 0x80, 0x7e, 0x7f, 0x7e, 0x7d, 0x7e, 0x7e, 0x7f,
+  0x7f, 0x7f, 0x7e, 0x7d, 0x80, 0x7e, 0x7f, 0x7f, 0x80, 0x7f, 0x7f, 0x81, 0x7e, 0x81, 0x81, 0x83,
+  0x80, 0x81, 0x80, 0x80, 0x81, 0x7f, 0x80, 0x80, 0x80, 0x81, 0x7f, 0x80, 0x7f, 0x80, 0x7d, 0x80,
+  0x7e, 0x7d, 0x80, 0x80, 0x83, 0x7f, 0x83, 0x7e, 0x83, 0x7f, 0x80, 0x7f, 0x7e, 0x81, 0x7f, 0x7f,
+  0x80, 0x80, 0x81, 0x7e, 0x7f, 0x7f, 0x80, 0x81, 0x80, 0x83, 0x7f, 0x82, 0x7f, 0x82, 0x7f, 0x80,
+  0x80, 0x7e, 0x7f, 0x7d, 0x7e, 0x7d, 0x7e, 0x7e, 0x7f, 0x7e, 0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x81,
+  0x82, 0x80, 0x82, 0x80, 0x82, 0x80, 0x7f, 0x7e, 0x7f, 0x7e, 0x7f, 0x80, 0x7e, 0x80, 0x80, 0x81,
+  0x7f, 0x7f, 0x7e, 0x80, 0x7d, 0x7e, 0x7e, 0x7f, 0x80, 0x7f, 0x80, 0x7e, 0x81, 0x7e, 0x81, 0x7f,
+  0x80, 0x7f, 0x80, 0x81, 0x7f, 0x80, 0x7e, 0x81, 0x7e, 0x80, 0x7d, 0x80, 0x80, 0x80, 0x81, 0x80,
+  0x82, 0x7e, 0x83, 0x7d, 0x80, 0x7c, 0x7d, 0x7e, 0x7c, 0x7e, 0x7d, 0x7e, 0x7f, 0x7e, 0x7e, 0x80,
+  0x7e, 0x81, 0x7e, 0x81, 0x7f, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x81, 0x83, 0x80, 0x81, 0x80,
+  0x80, 0x80, 0x80, 0x7f, 0x80, 0x7e, 0x7f, 0x7f, 0x81, 0x7f, 0x80, 0x80, 0x7f, 0x7e, 0x80, 0x80,
+  0x81, 0x82, 0x81, 0x82, 0x81, 0x81, 0x81, 0x82, 0x80, 0x80, 0x7e, 0x82, 0x80, 0x84, 0x81, 0x80,
+  0x7f, 0x81, 0x80, 0x7f, 0x80, 0x7d, 0x80, 0x7d, 0x81, 0x7f, 0x81, 0x80, 0x81, 0x81, 0x80, 0x80,
+  0x7e, 0x80, 0x7f, 0x81, 0x7f, 0x81, 0x81, 0x81, 0x7f, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+  0x81, 0x80, 0x80, 0x7e, 0x81, 0x7f, 0x7f, 0x7e, 0x7e, 0x7f, 0x7f, 0x80, 0x7f, 0x7f, 0x7e, 0x81,
+  0x7e, 0x7f, 0x80, 0x80, 0x7f, 0x7f, 0x7f, 0x7f, 0x81, 0x7f, 0x80, 0x7f, 0x80, 0x80, 0x7f, 0x80,
+  0x80, 0x80, 0x7f, 0x7f, 0x7f, 0x80, 0x7f, 0x7e, 0x7d, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
+  0x7f, 0x7e, 0x7f, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x7f, 0x80, 0x80, 0x80, 0x80, 0x81,
+  0x80
+};
+
diff --git a/examples/audio/05-planar-data/thumbnail.png b/examples/audio/05-planar-data/thumbnail.png
new file mode 100644
index 0000000000000..3a040dfaef38a
Binary files /dev/null and b/examples/audio/05-planar-data/thumbnail.png differ
diff --git a/include/SDL3/SDL_audio.h b/include/SDL3/SDL_audio.h
index c6acf885f42a8..b8999488b8023 100644
--- a/include/SDL3/SDL_audio.h
+++ b/include/SDL3/SDL_audio.h
@@ -1414,6 +1414,47 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamOutputChannelMap(SDL_AudioStr
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);
 
+/**
+ * Add data to the stream with each channel in a separate array.
+ *
+ * This data must match the format/channels/samplerate specified in the latest
+ * call to SDL_SetAudioStreamFormat, or the format specified when creating the
+ * stream if it hasn't been changed.
+ *
+ * The data will be interleaved and queued. Note that SDL_AudioStream only
+ * operates on interleaved data, so this is simply a convenience function for
+ * easily queueing data from sources that provide separate arrays. There is no
+ * equivalent function to retrieve planar data.
+ *
+ * The arrays in `channel_buffers` are ordered as they are to be interleaved;
+ * the first array will be the first sample in the interleaved data. Any
+ * individual array may be NULL; in this case, silence will be interleaved for
+ * that channel.
+ *
+ * Note that `num_samples` is the number of _samples per array_. This can also
+ * be thought of as the number of _sample frames_ to be queued. A value of 1
+ * with stereo arrays will queue two samples to the stream. This is different
+ * than SDL_PutAudioStreamData, which wants the size of a single array in bytes.
+ *
+ * \param stream the stream the audio data is being added to.
+ * \param channel_buffers a pointer to an array of arrays, one array per channel.
+ * \param num_samples the number of _samples_ per array to write to the stream.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety It is safe to call this function from any thread, but if the
+ *               stream has a callback set, the caller might need to manage
+ *               extra locking.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_ClearAudioStream
+ * \sa SDL_FlushAudioStream
+ * \sa SDL_GetAudioStreamData
+ * \sa SDL_GetAudioStreamQueued
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamPlanarData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples);
+
 /**
  * Get converted/resampled data from the stream.
  *
diff --git a/src/audio/SDL_audiocvt.c b/src/audio/SDL_audiocvt.c
index f751b0e580bdb..085308a73c6ad 100644
--- a/src/audio/SDL_audiocvt.c
+++ b/src/audio/SDL_audiocvt.c
@@ -768,64 +768,70 @@ bool SDL_SetAudioStreamGain(SDL_AudioStream *stream, float gain)
 
 static bool CheckAudioStreamIsFullySetup(SDL_AudioStream *stream)
 {
-    if (stream->src_spec.format == 0) {
+    if (stream->src_spec.format == SDL_AUDIO_UNKNOWN) {
         return SDL_SetError("Stream has no source format");
-    } else if (stream->dst_spec.format == 0) {
+    } else if (stream->dst_spec.format == SDL_AUDIO_UNKNOWN) {
         return SDL_SetError("Stream has no destination format");
     }
 
     return true;
 }
 
-static bool PutAudioStreamBuffer(SDL_AudioStream *stream, const void *buf, int len, SDL_ReleaseAudioBufferCallback callback, void* userdata)
+// you MUST hold `stream->lock` when calling this, and validate your parameters!
+static bool PutAudioStreamBufferInternal(SDL_AudioStream *stream, const SDL_AudioSpec *spec, const int *chmap, const void *buf, int len, SDL_ReleaseAudioBufferCallback callback, void* userdata)
 {
-#if DEBUG_AUDIOSTREAM
-    SDL_Log("AUDIOSTREAM: wants to put %d bytes", len);
-#endif
-
-    SDL_LockMutex(stream->lock);
-
-    if (!CheckAudioStreamIsFullySetup(stream)) {
-        SDL_UnlockMutex(stream->lock);
-        return false;
-    }
-
-    if ((len % SDL_AUDIO_FRAMESIZE(stream->src_spec)) != 0) {
-        SDL_UnlockMutex(stream->lock);
-        return SDL_SetError("Can't add partial sample frames");
-    }
-
     SDL_AudioTrack* track = NULL;
 
     if (callback) {
-        track = SDL_CreateAudioTrack(stream->queue, &stream->src_spec, stream->src_chmap, (Uint8 *)buf, len, len, callback, userdata);
-
+        track = SDL_CreateAudioTrack(stream->queue, spec, chmap, (Uint8 *)buf, len, len, callback, userdata);
         if (!track) {
-            SDL_UnlockMutex(stream->lock);
             return false;
         }
     }
 
     const int prev_available = stream->put_callback ? SDL_GetAudioStreamAvailable(stream) : 0;
 
-    bool result = true;
+    bool retval = true;
 
     if (track) {
         SDL_AddTrackToAudioQueue(stream->queue, track);
     } else {
-        result = SDL_WriteToAudioQueue(stream->queue, &stream->src_spec, stream->src_chmap, (const Uint8 *)buf, len);
+        retval = SDL_WriteToAudioQueue(stream->queue, spec, chmap, (const Uint8 *)buf, len);
     }
 
-    if (result) {
+    if (retval) {
         if (stream->put_callback) {
             const int newavail = SDL_GetAudioStreamAvailable(stream) - prev_available;
             stream->put_callback(stream->put_callback_userdata, stream, newavail, newavail);
         }
     }
 
+    return retval;
+}
+
+static bool PutAudioStreamBuffer(SDL_AudioStream *stream, const void *buf, int len, SDL_ReleaseAudioBufferCallback callback, void* userdata)
+{
+#if DEBUG_AUDIOSTREAM
+    SDL_Log("AUDIOSTREAM: wants to put %d bytes", len);
+#endif
+
+    SDL_LockMutex(stream->lock);
+
+    if (!CheckAudioStreamIsFullySetup(stream)) {
+        SDL_UnlockMutex(stream->lock);
+        return false;
+    }
+
+    if ((len % SDL_AUDIO_FRAMESIZE(stream->src_spec)) != 0) {
+        SDL_UnlockMutex(stream->lock);
+        return SDL_SetError("Can't add partial sample frames");
+    }
+
+    const bool retval = PutAudioStreamBufferInternal(stream, &stream->src_spec, stream->src_chmap, buf, len, callback, userdata);
+
     SDL_UnlockMutex(stream->lock);
 
-    return result;
+    return retval;
 }
 
 static void SDLCALL FreeAllocatedAudioBuffer(void *userdata, const void *buf, int len)
@@ -857,9 +863,8 @@ bool SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len)
         }
 
         SDL_memcpy(data, buf, len);
-        buf = data;
 
-        bool ret = PutAudioStreamBuffer(stream, buf, len, FreeAllocatedAudioBuffer, NULL);
+        bool ret = PutAudioStreamBuffer(stream, data, len, FreeAllocatedAudioBuffer, NULL);
         if (!ret) {
             SDL_free(data);
         }
@@ -869,6 +874,144 @@ bool SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len)
     return PutAudioStreamBuffer(stream, buf, len, NULL, NULL);
 }
 
+
+#define GENERIC_INTERLEAVE_FUNCTION(bits) \
+    static void InterleaveAudioChannelsGeneric##bits(void *output, const void * const *channel_buffers, const int channels, int num_samples) { \
+        Uint##bits *dst = (Uint##bits *) output; \
+        const Uint##bits * const *srcs = (const Uint##bits * const *) channel_buffers; \
+        for (int frame = 0; frame < num_samples; frame++) { \
+            for (int channel = 0; channel < channels; channel++) { \
+                *(dst++) = srcs[channel][frame]; \
+            } \
+        } \
+    }
+
+GENERIC_INTERLEAVE_FUNCTION(8)
+GENERIC_INTERLEAVE_FUNCTION(16)
+GENERIC_INTERLEAVE_FUNCTION(32)
+//GENERIC_INTERLEAVE_FUNCTION(64)   (we don't have any 64-bit audio data types at the moment.)
+#undef GENERIC_INTERLEAVE_FUNCTION
+
+#define GENERIC_INTERLEAVE_WITH_NULLS_FUNCTION(bits) \
+    static void InterleaveAudioChannelsWithNullsGeneric##bits(void *output, const void * const *channel_buffers, const int channels, int num_samples, const int isilence) { \
+        const Uint##bits silence = (Uint##bits) isilence; \
+        Uint##bits *dst = (Uint##bits *) output; \
+        const Uint##bits * const *srcs = (const Uint##bits * const *) channel_buffers; \
+        for (int frame = 0; frame < num_samples; frame++) { \
+            for (int channel = 0; channel < channels; channel++) { \
+                *(dst++) = srcs[channel] ? srcs[channel][frame] : silence; \
+            } \
+        } \
+    }
+
+GENERIC_INTERLEAVE_WITH_NULLS_FUNCTION(8)
+GENERIC_INTERLEAVE_WITH_NULLS_FUNCTION(16)
+GENERIC_INTERLEAVE_WITH_NULLS_FUNCTION(32)
+//GENERIC_INTERLEAVE_WITH_NULLS_FUNCTION(64)   (we don't have any 64-bit audio data types at the moment.)
+#undef GENERIC_INTERLEAVE_WITH_NULLS_FUNCTION
+
+static void InterleaveAudioChannels(void *output, const void * const *channel_buffers, int num_samples, const SDL_AudioSpec *spec)
+{
+    const int channels = spec->channels;
+
+    bool have_null_channel = false;
+    for (int i = 0; i < channels; i++) {
+        if (channel_buffers[i] == NULL) {
+            have_null_channel = true;
+            break;
+        }
+    }
+
+    if (have_null_channel) {
+        const int silence = SDL_GetSilenceValueForFormat(spec->format);
+        switch (SDL_AUDIO_BITSIZE(spec->format)) {
+            case 8: InterleaveAudioChannelsWithNullsGeneric8(output, channel_buffers, channels, num_samples, silence); break;
+            case 16: InterleaveAudioChannelsWithNullsGeneric16(output, channel_buffers, channels, num_samples, silence); break;
+            case 32: InterleaveAudioChannelsWithNullsGeneric32(output, channel_buffers, channels, num_samples, silence); break;
+            //case 64: InterleaveAudioChannelsGeneric64(output, channel_buffers, channels, num_samples); break;  (we don't have any 64-bit audio data types at the moment.)
+            default: SDL_assert(!"Missing needed generic audio interleave function!"); SDL_memset(output, 0, SDL_AUDIO_FRAMESIZE(*spec) * num_samples); break;
+        }
+    } else {
+        // !!! FIXME: it would be possible to do this really well in SIMD for stereo data, using unpack (intel) or zip (arm) instructions, etc.
+        switch (SDL_AUDIO_BITSIZE(spec->format)) {
+            case 8: InterleaveAudioChannelsGeneric8(output, channel_buffers, channels, num_samples); break;
+            case 16: InterleaveAudioChannelsGeneric16(output, channel_buffers, channels, num_samples); break;
+            case 32: InterleaveAudioChannelsGeneric32(output, channel_buffers, channels, num_samples); break;
+            //case 64: InterleaveAudioChannelsGeneric64(output, channel_buffers, channels, num_samples); break;  (we don't have any 64-bit audio data types at the moment.)
+            default: SDL_assert(!"Missing needed generic audio interleave function!"); SDL_memset(output, 0, SDL_AUDIO_FRAMESIZE(*spec) * num_samples); break;
+        }
+    }
+}
+
+bool SDL_PutAudioStreamPlanarData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples)
+{
+    if (!stream) {
+        return SDL_InvalidParamError("stream");
+    } else if (!channel_buffers) {
+        return SDL_InvalidParamError("channel_buffers");
+    } else if (num_samples < 0) {
+        return SDL_InvalidParamError("num_samples");
+    } else if (num_samples == 0) {
+        return true; // nothing to do.
+    }
+
+    // we do the interleaving up front without the lock held, so the audio device doesn't starve while we work.
+    //  but we _do_ need to know the current input spec.
+    SDL_AudioSpec spec;
+    int chmap_copy[SDL_MAX_CHANNELMAP_CHANNELS];
+    int *chmap = NULL;
+    SDL_LockMutex(stream->lock);
+    if (!CheckAudioStreamIsFullySetup(stream)) {
+        SDL_UnlockMutex(stream->lock);
+        return false;
+    }
+    SDL_copyp(&spec, &stream->src_spec);
+    if (stream->src_chmap) {
+        chmap = chmap_copy;
+        SDL_memcpy(chmap, stream->src_chmap, sizeof (*chmap) * spec.channels);
+    }
+    SDL_UnlockMutex(stream->lock);
+
+    if (spec.channels == 1) {  // nothing to interleave, just use the usual function.
+        return SDL_PutAudioStreamData(stream, channel_buffers[0], SDL_AUDIO_FRAMESIZE(spec) * num_samples);
+    }
+
+    bool retval = false;
+
+    const int len = SDL_AUDIO_FRAMESIZE(spec) * num_samples;
+    #if DEBUG_AUDIOSTREAM
+    SDL_Log("AUDIOSTREAM: wants to put %d bytes of separated data", len);
+    #endif
+
+    // Is the data small enough to just interleave it on the stack and put it through the normal interface?
+    #define INTERLEAVE_STACK_SIZE 1024
+    Uint8 stackbuf[INTERLEAVE_STACK_SIZE];
+    void *data = stackbuf;
+    SDL_ReleaseAudioBufferCallback callback = NULL;
+
+    if (len > INTERLEAVE_STACK_SIZE) {
+        // too big for the stack? Just SDL_malloc a block and interleave into that. To avoid the extra copy, we'll just set it as a
+        //  new track in the queue (the distinction is specifying a callback to PutAudioStreamBufferInternal, to release the buffer).
+        data = SDL_malloc(len);
+        if (!data) {
+            return false;
+        }
+        callback = FreeAllocatedAudioBuffer;
+    }
+
+    InterleaveAudioChannels(data, channel_buffers, num_samples, &spec);
+
+    // it's okay if the stream format changed on another thread while we didn't hold the lock; PutAudioStreamBufferInternal will notice
+    //  and set up a new track with the right format, and the next SDL_PutAudioStreamData will notice that stream->src_spec doesn't
+    //  match the new track and set up a new one again. It's a bad idea to change the format on another thread while putting here,
+    //  but everything _will_ work out with the format that was (presumably) expected.
+    SDL_LockMutex(stream->lock);
+    retval = PutAudioStreamBufferInternal(stream, &spec, chmap, data, len, callback, NULL);
+    SDL_UnlockMutex(stream->lock);
+
+    return retval;
+}
+
 bool SDL_FlushAudioStream(SDL_AudioStream *stream)
 {
     if (!stream) {
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 44f9d0e062a36..93d6771cd8c80 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1250,6 +1250,7 @@ SDL3_0.0.0 {
     SDL_GetRenderTextureAddressMode;
     SDL_GetGPUDeviceProperties;
     SDL_CreateGPURenderer;
+    SDL_PutAudioStreamPlanarData;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index a12f3fbf24b4a..2808e409b6124 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1275,3 +1275,4 @@
 #define SDL_GetRenderTextureAddressMode SDL_GetRenderTextureAddressMode_REAL
 #define SDL_GetGPUDeviceProperties SDL_GetGPUDeviceProperties_REAL
 #define SDL_CreateGPURenderer SDL_CreateGPURenderer_REAL
+#define SDL_PutAudioStreamPlanarData SDL_PutAudioStreamPlanarData_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index d7988ac2b0944..4b642c9103319 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1283,3 +1283,4 @@ SDL_DYNAPI_PROC(bool,SDL_SetRenderTextureAddressMode,(SDL_Renderer *a,SDL_Textur
 SDL_DYNAPI_PROC(bool,SDL_GetRenderTextureAddressMode,(SDL_Renderer *a,SDL_TextureAddressMode *b,SDL_TextureAddressMode *c),(a,b,c),return)
 SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetGPUDeviceProperties,(SDL_GPUDevice *a),(a),return)
 SDL_DYNAPI_PROC(SDL_Renderer*,SDL_CreateGPURenderer,(SDL_Window *a,SDL_GPUShaderFormat b,SDL_GPUDevice **c),(a,b,c),return)
+SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamPlanarData,(SDL_AudioStream *a,const void * const*b,int c),(a,b,c),return)


Reviews:
user: login: slouken
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T17:09:16Z
user: login: icculus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T17:31:14Z
user: login: slouken
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T17:33:55Z
user: login: slouken
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T17:34:39Z
user: login: icculus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T17:41:07Z
user: login: slouken
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T18:02:04Z
user: login: icculus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T18:22:48Z

Review comments:
diff_hunk: @@ -1414,6 +1414,45 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamOutputChannelMap(SDL_AudioStr
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);
 
+/**
+ * Add data to the stream with each channel in a separate array.
+ *
+ * This data must match the format/channels/samplerate specified in the latest
+ * call to SDL_SetAudioStreamFormat, or the format specified when creating the
+ * stream if it hasn't been changed.
+ *
+ * The data will be interleaved and queued. Note that SDL_AudioStream only
+ * operates on interleaved data, so this is simply a convenience function for
+ * easily queueing data from sources that provide separate arrays. There is no
+ * equivalent function to retrieve separated data.
+ *
+ * The arrays in `channel_buffers` are ordered as they are to be interleaved;
+ * the first array will be the first sample in the interleaved data.
+ *
+ * Note that `num_samples` is the number of _samples per array_. This can also
+ * be thought of as the number of _sample frames_ to be queued. A value of 1
+ * with stereo arrays will queue two samples to the stream. This is different
+ * than SDL_PutAudioStreamData, which wants the size of a single array in bytes.
+ *
+ * \param stream the stream the audio data is being added to.
+ * \param channel_buffers a pointer to an array of arrays, one array per channel.
+ * \param num_samples the number of _samples_ per array to write to the stream.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety It is safe to call this function from any thread, but if the
+ *               stream has a callback set, the caller might need to manage
+ *               extra locking.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_ClearAudioStream
+ * \sa SDL_FlushAudioStream
+ * \sa SDL_GetAudioStreamData
+ * \sa SDL_GetAudioStreamQueued
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamSeparatedData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples);
path: include/SDL3/SDL_audio.h
user: login: slouken
body: This should probably include the number of channels, for a sanity check.
created_at: 2025-04-22T17:09:16Z
author_association: COLLABORATOR
diff_hunk: @@ -1414,6 +1414,45 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamOutputChannelMap(SDL_AudioStr
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);
 
+/**
+ * Add data to the stream with each channel in a separate array.
+ *
+ * This data must match the format/channels/samplerate specified in the latest
+ * call to SDL_SetAudioStreamFormat, or the format specified when creating the
+ * stream if it hasn't been changed.
+ *
+ * The data will be interleaved and queued. Note that SDL_AudioStream only
+ * operates on interleaved data, so this is simply a convenience function for
+ * easily queueing data from sources that provide separate arrays. There is no
+ * equivalent function to retrieve separated data.
+ *
+ * The arrays in `channel_buffers` are ordered as they are to be interleaved;
+ * the first array will be the first sample in the interleaved data.
+ *
+ * Note that `num_samples` is the number of _samples per array_. This can also
+ * be thought of as the number of _sample frames_ to be queued. A value of 1
+ * with stereo arrays will queue two samples to the stream. This is different
+ * than SDL_PutAudioStreamData, which wants the size of a single array in bytes.
+ *
+ * \param stream the stream the audio data is being added to.
+ * \param channel_buffers a pointer to an array of arrays, one array per channel.
+ * \param num_samples the number of _samples_ per array to write to the stream.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety It is safe to call this function from any thread, but if the
+ *               stream has a callback set, the caller might need to manage
+ *               extra locking.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_ClearAudioStream
+ * \sa SDL_FlushAudioStream
+ * \sa SDL_GetAudioStreamData
+ * \sa SDL_GetAudioStreamQueued
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamSeparatedData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples);
path: include/SDL3/SDL_audio.h
user: login: icculus
body: It gets it from the current input spec, the same way it does when you add interleaved data. Giving it interleaved data in the wrong format would _also_ be a disaster, so I'm inclined to trust the programmer here.
created_at: 2025-04-22T17:31:13Z
author_association: COLLABORATOR
diff_hunk: @@ -1414,6 +1414,45 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamOutputChannelMap(SDL_AudioStr
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);
 
+/**
+ * Add data to the stream with each channel in a separate array.
+ *
+ * This data must match the format/channels/samplerate specified in the latest
+ * call to SDL_SetAudioStreamFormat, or the format specified when creating the
+ * stream if it hasn't been changed.
+ *
+ * The data will be interleaved and queued. Note that SDL_AudioStream only
+ * operates on interleaved data, so this is simply a convenience function for
+ * easily queueing data from sources that provide separate arrays. There is no
+ * equivalent function to retrieve separated data.
+ *
+ * The arrays in `channel_buffers` are ordered as they are to be interleaved;
+ * the first array will be the first sample in the interleaved data.
+ *
+ * Note that `num_samples` is the number of _samples per array_. This can also
+ * be thought of as the number of _sample frames_ to be queued. A value of 1
+ * with stereo arrays will queue two samples to the stream. This is different
+ * than SDL_PutAudioStreamData, which wants the size of a single array in bytes.
+ *
+ * \param stream the stream the audio data is being added to.
+ * \param channel_buffers a pointer to an array of arrays, one array per channel.
+ * \param num_samples the number of _samples_ per array to write to the stream.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety It is safe to call this function from any thread, but if the
+ *               stream has a callback set, the caller might need to manage
+ *               extra locking.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_ClearAudioStream
+ * \sa SDL_FlushAudioStream
+ * \sa SDL_GetAudioStreamData
+ * \sa SDL_GetAudioStreamQueued
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamSeparatedData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples);
path: include/SDL3/SDL_audio.h
user: login: slouken
body: Fair enough.
created_at: 2025-04-22T17:33:55Z
author_association: COLLABORATOR
diff_hunk: @@ -1414,6 +1414,45 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamOutputChannelMap(SDL_AudioStr
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);
 
+/**
+ * Add data to the stream with each channel in a separate array.
+ *
+ * This data must match the format/channels/samplerate specified in the latest
+ * call to SDL_SetAudioStreamFormat, or the format specified when creating the
+ * stream if it hasn't been changed.
+ *
+ * The data will be interleaved and queued. Note that SDL_AudioStream only
+ * operates on interleaved data, so this is simply a convenience function for
+ * easily queueing data from sources that provide separate arrays. There is no
+ * equivalent function to retrieve separated data.
+ *
+ * The arrays in `channel_buffers` are ordered as they are to be interleaved;
+ * the first array will be the first sample in the interleaved data.
+ *
+ * Note that `num_samples` is the number of _samples per array_. This can also
+ * be thought of as the number of _sample frames_ to be queued. A value of 1
+ * with stereo arrays will queue two samples to the stream. This is different
+ * than SDL_PutAudioStreamData, which wants the size of a single array in bytes.
+ *
+ * \param stream the stream the audio data is being added to.
+ * \param channel_buffers a pointer to an array of arrays, one array per channel.
+ * \param num_samples the number of _samples_ per array to write to the stream.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety It is safe to call this function from any thread, but if the
+ *               stream has a callback set, the caller might need to manage
+ *               extra locking.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_ClearAudioStream
+ * \sa SDL_FlushAudioStream
+ * \sa SDL_GetAudioStreamData
+ * \sa SDL_GetAudioStreamQueued
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamSeparatedData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples);
path: include/SDL3/SDL_audio.h
user: login: slouken
body: Can we provide a small code example for this? I can easily see people getting this wrong, given the unusual parameter type.
created_at: 2025-04-22T17:34:39Z
author_association: COLLABORATOR
diff_hunk: @@ -1414,6 +1414,45 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetAudioStreamOutputChannelMap(SDL_AudioStr
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);
 
+/**
+ * Add data to the stream with each channel in a separate array.
+ *
+ * This data must match the format/channels/samplerate specified in the latest
+ * call to SDL_SetAudioStreamFormat, or the format specified when creating the
+ * stream if it hasn't been changed.
+ *
+ * The data will be interleaved and queued. Note that SDL_AudioStream only
+ * operates on interleaved data, so this is simply a convenience function for
+ * easily queueing data from sources that provide separate arrays. There is no
+ * equivalent function to retrieve separated data.
+ *
+ * The arrays in `channel_buffers` are ordered as they are to be interleaved;
+ * the first array will be the first sample in the interleaved data.
+ *
+ * Note that `num_samples` is the number of _samples per array_. This can also
+ * be thought of as the number of _sample frames_ to be queued. A value of 1
+ * with stereo arrays will queue two samples to the stream. This is different
+ * than SDL_PutAudioStreamData, which wants the size of a single array in bytes.
+ *
+ * \param stream the stream the audio data is being added to.
+ * \param channel_buffers a pointer to an array of arrays, one array per channel.
+ * \param num_samples the number of _samples_ per array to write to the stream.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety It is safe to call this function from any thread, but if the
+ *               stream has a callback set, the caller might need to manage
+ *               extra locking.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_ClearAudioStream
+ * \sa SDL_FlushAudioStream
+ * \sa SDL_GetAudioStreamData
+ * \sa SDL_GetAudioStreamQueued
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_PutAudioStreamSeparatedData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples);
path: include/SDL3/SDL_audio.h
user: login: icculus
body: Yeah, definitely.
created_at: 2025-04-22T17:41:07Z
author_association: COLLABORATOR
diff_hunk: @@ -869,6 +874,108 @@ bool SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len)
     return PutAudioStreamBuffer(stream, buf, len, NULL, NULL);
 }
 
+
+#define GENERIC_INTERLEAVE_FUNCTION(bits) \
+    static void InterleaveAudioChannelsGeneric##bits(void *output, const void * const *channel_buffers, const int channels, int num_samples) { \
+        Uint##bits *dst = (Uint##bits *) output; \
+        const Uint##bits * const *srcs = (const Uint##bits * const *) channel_buffers; \
+        for (int frame = 0; frame < num_samples; frame++) { \
+            for (int channel = 0; channel < channels; channel++) { \
+                *(dst++) = srcs[channel][frame]; \
+            } \
+        } \
+    }
+
+GENERIC_INTERLEAVE_FUNCTION(8)
+GENERIC_INTERLEAVE_FUNCTION(16)
+GENERIC_INTERLEAVE_FUNCTION(32)
+//GENERIC_INTERLEAVE_FUNCTION(64)   (we don't have any 64-bit audio data types at the moment.)
+#undef GENERIC_INTERLEAVE_FUNCTION
+
+static void InterleaveAudioChannels(void *output, const void * const *channel_buffers, int num_samples, const SDL_AudioSpec *spec)
+{
+    const int channels = spec->channels;
+
+    // !!! FIXME: it would be possible to do this really well in SIMD for stereo data, using unpack (intel) or zip (arm) instructions, etc.
+
+    switch (SDL_AUDIO_BITSIZE(spec->format)) {
+        case 8: InterleaveAudioChannelsGeneric8(output, channel_buffers, channels, num_samples); break;
+        case 16: InterleaveAudioChannelsGeneric16(output, channel_buffers, channels, num_samples); break;
+        case 32: InterleaveAudioChannelsGeneric32(output, channel_buffers, channels, num_samples); break;
+        //case 64: InterleaveAudioChannelsGeneric64(output, channel_buffers, channels, num_samples); break;  (we don't have any 64-bit audio data types at the moment.)
+        default: SDL_assert(!"Missing needed generic audio interleave function!"); SDL_memset(output, 0, SDL_AUDIO_FRAMESIZE(*spec) * num_samples); break;
+    }
+}
+
+bool SDL_PutAudioStreamSeparatedData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples)
+{
+    if (!stream) {
+        return SDL_InvalidParamError("stream");
+    } else if (!channel_buffers) {
+        return SDL_InvalidParamError("channel_buffers");
+    } else if (num_samples < 0) {
+        return SDL_InvalidParamError("num_samples");
+    } else if (num_samples == 0) {
+        return true; // nothing to do.
+    }
+
+    // we do the interleaving up front without the lock held, so the audio device doesn't starve while we work.
+    //  but we _do_ need to know the current input spec.
+    SDL_AudioSpec spec;
+    int chmap_copy[SDL_MAX_CHANNELMAP_CHANNELS];
+    int *chmap = NULL;
+    SDL_LockMutex(stream->lock);
+    if (!CheckAudioStreamIsFullySetup(stream)) {
+        SDL_UnlockMutex(stream->lock);
+        return false;
+    }
+    SDL_copyp(&spec, &stream->src_spec);
+    if (stream->src_chmap) {
+        chmap = chmap_copy;
+        SDL_memcpy(chmap, stream->src_chmap, sizeof (*chmap) * spec.channels);
+    }
+    SDL_UnlockMutex(stream->lock);
+
+    if (spec.channels == 1) {  // nothing to interleave, just use the usual function.
+        return SDL_PutAudioStreamData(stream, channel_buffers[0], SDL_AUDIO_FRAMESIZE(spec) * num_samples);
+    }
+
+    bool retval = false;
+
+    const int len = SDL_AUDIO_FRAMESIZE(spec) * num_samples;
+    #if DEBUG_AUDIOSTREAM
+    SDL_Log("AUDIOSTREAM: wants to put %d bytes of separated data", len);
+    #endif
+
+    // Is the data small enough to just interleave it on the stack and put it through the normal interface?
path: src/audio/SDL_audiocvt.c
user: login: slouken
body: Any reason not to use SDL_small_alloc()?
created_at: 2025-04-22T18:02:04Z
author_association: COLLABORATOR
diff_hunk: @@ -869,6 +874,108 @@ bool SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len)
     return PutAudioStreamBuffer(stream, buf, len, NULL, NULL);
 }
 
+
+#define GENERIC_INTERLEAVE_FUNCTION(bits) \
+    static void InterleaveAudioChannelsGeneric##bits(void *output, const void * const *channel_buffers, const int channels, int num_samples) { \
+        Uint##bits *dst = (Uint##bits *) output; \
+        const Uint##bits * const *srcs = (const Uint##bits * const *) channel_buffers; \
+        for (int frame = 0; frame < num_samples; frame++) { \
+            for (int channel = 0; channel < channels; channel++) { \
+                *(dst++) = srcs[channel][frame]; \
+            } \
+        } \
+    }
+
+GENERIC_INTERLEAVE_FUNCTION(8)
+GENERIC_INTERLEAVE_FUNCTION(16)
+GENERIC_INTERLEAVE_FUNCTION(32)
+//GENERIC_INTERLEAVE_FUNCTION(64)   (we don't have any 64-bit audio data types at the moment.)
+#undef GENERIC_INTERLEAVE_FUNCTION
+
+static void InterleaveAudioChannels(void *output, const void * const *channel_buffers, int num_samples, const SDL_AudioSpec *spec)
+{
+    const int channels = spec->channels;
+
+    // !!! FIXME: it would be possible to do this really well in SIMD for stereo data, using unpack (intel) or zip (arm) instructions, etc.
+
+    switch (SDL_AUDIO_BITSIZE(spec->format)) {
+        case 8: InterleaveAudioChannelsGeneric8(output, channel_buffers, channels, num_samples); break;
+        case 16: InterleaveAudioChannelsGeneric16(output, channel_buffers, channels, num_samples); break;
+        case 32: InterleaveAudioChannelsGeneric32(output, channel_buffers, channels, num_samples); break;
+        //case 64: InterleaveAudioChannelsGeneric64(output, channel_buffers, channels, num_samples); break;  (we don't have any 64-bit audio data types at the moment.)
+        default: SDL_assert(!"Missing needed generic audio interleave function!"); SDL_memset(output, 0, SDL_AUDIO_FRAMESIZE(*spec) * num_samples); break;
+    }
+}
+
+bool SDL_PutAudioStreamSeparatedData(SDL_AudioStream *stream, const void * const *channel_buffers, int num_samples)
+{
+    if (!stream) {
+        return SDL_InvalidParamError("stream");
+    } else if (!channel_buffers) {
+        return SDL_InvalidParamError("channel_buffers");
+    } else if (num_samples < 0) {
+        return SDL_InvalidParamError("num_samples");
+    } else if (num_samples == 0) {
+        return true; // nothing to do.
+    }
+
+    // we do the interleaving up front without the lock held, so the audio device doesn't starve while we work.
+    //  but we _do_ need to know the current input spec.
+    SDL_AudioSpec spec;
+    int chmap_copy[SDL_MAX_CHANNELMAP_CHANNELS];
+    int *chmap = NULL;
+    SDL_LockMutex(stream->lock);
+    if (!CheckAudioStreamIsFullySetup(stream)) {
+        SDL_UnlockMutex(stream->lock);
+        return false;
+    }
+    SDL_copyp(&spec, &stream->src_spec);
+    if (stream->src_chmap) {
+        chmap = chmap_copy;
+        SDL_memcpy(chmap, stream->src_chmap, sizeof (*chmap) * spec.channels);
+    }
+    SDL_UnlockMutex(stream->lock);
+
+    if (spec.channels == 1) {  // nothing to interleave, just use the usual function.
+        return SDL_PutAudioStreamData(stream, channel_buffers[0], SDL_AUDIO_FRAMESIZE(spec) * num_samples);
+    }
+
+    bool retval = false;
+
+    const int len = SDL_AUDIO_FRAMESIZE(spec) * num_samples;
+    #if DEBUG_AUDIOSTREAM
+    SDL_Log("AUDIOSTREAM: wants to put %d bytes of separated data", len);
+    #endif
+
+    // Is the data small enough to just interleave it on the stack and put it through the normal interface?
path: src/audio/SDL_audiocvt.c
user: login: icculus
body: Because it only allows 128 bytes of stack, and we'd need to SDL_small_free it afterwards (or break the abstraction). Here, we want to keep the malloc'd version, which gets reused directly as the backing for an SDL_AudioTrack.
created_at: 2025-04-22T18:22:48Z
author_association: COLLABORATOR

PR comments:
user: login: icculus
created_at: 2025-04-22T07:04:01Z
body: Also, I just plugged this into SDL_remixer, and it's _so much nicer_ to be like, "here, libvorbisfile handed me a bunch of arrays, figure this out for me."  :)
author_association: COLLABORATOR
user: login: maia-s
created_at: 2025-04-22T12:07:18Z
body: > I'm not in love with the new function name, but I couldn't think of anything better.

Some suggestions:
- `SDL_PutAudioStreamSeparateData` (removed a d)
- `SDL_PutAudioStreamChannelData`
- `SDL_PutAudioStreamPerChannelData`
- `SDL_PutAudioStreamSplitData`

author_association: CONTRIBUTOR
user: login: slouken
created_at: 2025-04-22T14:28:20Z
body: This is typically called planar audio data, so maybe `SDL_PutAudioStreamPlanarData`?
author_association: COLLABORATOR
user: login: slouken
created_at: 2025-04-22T14:34:46Z
body: Also, I’m going to play devil’s advocate on both sides here.

On one side, it’s pretty easy to interleave the data, especially since the chunks you’re getting may not be the size you want to put into the stream, so maybe we don’t need this?

On the other side, you might have audio data coming from different sources, for example the DualSense controller has four channels of audio, two stereo audio and two haptic feedback, so maybe it makes sense to have a function to send each channel separately?

(I don’t necessarily think we should do either of these, just raising them for discussion)

author_association: COLLABORATOR
user: login: icculus
created_at: 2025-04-22T17:40:21Z
body: > This is typically called planar audio data, so maybe SDL_PutAudioStreamPlanarData?

That was my first impulse, but I thought that was a term for image data...apparently it is not.

[ffmpeg uses "planar"](https://ffmpeg.org/doxygen/3.1/group__lavu__sampfmts.html#ga82caf838259d95cc6c4fd87633bb0e19).

[PortAudio uses "non-interleaved"](https://portaudio.com/docs/v19-doxydocs/portaudio_8h.html#a1d79f0b88812cef5364b276dbbc898b5).

[ChatGPT thinks it's "planar"](https://chatgpt.com/share/6807d43c-7ed0-800d-bfa8-30673700001a). (and because AI is terrible, it had to add a wrong comment about PortAudio and libsndio at the end.)

So I'm fine with Planar.
author_association: COLLABORATOR
user: login: icculus
created_at: 2025-04-22T17:56:44Z
body: > On one side, it’s pretty easy to interleave the data, especially since the chunks you’re getting may not be the size you want to put into the stream, so maybe we don’t need this?

It needs to be interleaved one way or the other, regardless of size. If the app doesn't want to interleave it all because the chunk is too large, they can hold their arrays and offset into them to interleave more, later. They would have to anyhow. But SDL_AudioStream is designed to absorb data and use it as necessary, so they can often just shove the whole chunk in there anyhow.

As for "easy to interleave" ...this is what I had in SDL_remixer for this:

```c
SDL_assert(amount <= SDL_arraysize(samples));
const int channels = d->current_channels;
if (channels == 1) {  // one channel, just copy it right through.
    SDL_PutAudioStreamData(stream, samples, amount * sizeof (float));
} else {  // multiple channels, we need to interleave them.
    // We have `amount` sample frames, which is <= SDL_arraysize(samples), but at most,
    //  `samples` is `channels` multiples too small to hold the interleaved data, so
    //  interleave it and put it to the audio stream in chunks.
    const int max_frames = SDL_arraysize(samples) / channels;  // most sample frames per chunk.
    const int num_chunks = amount / max_frames;
    const int chunksize = max_frames * channels * sizeof (float);

    int frame = 0;
    for (int chunk = 0; chunk < num_chunks; chunk++) {
        float *fptr = samples;
        for (int i = 0; i < max_frames; i++) {
            for (int channel = 0; channel < channels; channel++) {
                *(fptr++) = pcm_channels[channel][frame];
            }
            frame++;
        }
        SDL_PutAudioStreamData(stream, samples, chunksize);
    }

    // put the last bit.
    SDL_assert(frame <= amount);
    SDL_assert(((amount - frame) * channels) <= SDL_arraysize(samples));

    const int remaining = amount - frame;
    if (remaining > 0) {
        float *fptr = samples;
        for (int i = 0; i < remaining; i++) {
            for (int channel = 0; channel < channels; channel++) {
                *(fptr++) = pcm_channels[channel][frame];
            }
            frame++;
        }
        SDL_PutAudioStreamData(stream, samples, remaining * channels * sizeof (float));
    }

    SDL_assert(frame == amount);
}
```

(Note that libvorbisfile only gives you float data; this gets more complicated if data comes in various formats.)

libvorbisfile gives you planar arrays of data, and we tapdance to interleave it in a little stack space. We were able to avoid that tapdance inside SDL because we were going to malloc anyhow to store it in the audiostream. So we can just allocate it upfront and use that buffer all the way through instead of allocating, interleaving, allocating again, copying, freeing, freeing.

Here's the vorbis code, in my working copy, with the new API:

```c
SDL_PutAudioStreamSeparatedData(stream, (const void * const *) pcm_channels, amount);
```

author_association: COLLABORATOR
user: login: icculus
created_at: 2025-04-22T18:00:50Z
body: > On the other side, you might have audio data coming from different sources, for example the DualSense controller has four channels of audio, two stereo audio and two haptic feedback, so maybe it makes sense to have a function to send each channel separately?

A good point. Another thing I ran into is libFLAC gives you 3 channel audio in FL-FR-FC format, as opposed to the usual 2.1 audio of FL-FR-LFE, so you have to move up to 5.1 before you get anything with a front-center channel, and here we can just give it a single silent array to blank out those unneeded channels.

(This is a strong argument for the channel mask stuff we've talked about, too, but that's a longer discussion.)
author_association: COLLABORATOR
user: login: icculus
created_at: 2025-04-22T20:47:21Z
body: Example code in action: https://icculus.org/~icculus/emscripten/sdl3-planar-audio/
author_association: COLLABORATOR
user: login: slouken
created_at: 2025-04-22T21:15:48Z
body: > Example code in action: https://icculus.org/~icculus/emscripten/sdl3-planar-audio/

Looks good!
author_association: COLLABORATOR
user: login: icculus
created_at: 2025-04-22T23:52:24Z
body: Okay, this has been rebased, so it's ready to merge if the builders turn green.
author_association: COLLABORATOR
user: login: icculus
created_at: 2025-04-23T01:18:39Z
body: Builders are green! Last call before I click the Big Green Button!
author_association: COLLABORATOR
user: login: icculus
created_at: 2025-04-25T18:11:39Z
body: Thinking on this a little more, Sam said:

> This should probably include the number of channels, for a sanity check.

And I pushed back, but on further consideration, this actually makes some sense in that an app could be free to specify _less_ channels than are needed and SDL could treat the missing range as NULL arrays, instead of the app having to supply them. This would actually be usable today in SDL_remixer's libFLAC decoder. I'll make this tweak before 3.4.0 ships this API.

author_association: COLLABORATOR