From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- contrib/SDL-3.2.8/src/audio/SDL_audio.c | 2534 ++++++++++++++++++++ contrib/SDL-3.2.8/src/audio/SDL_audio_c.h | 27 + .../src/audio/SDL_audio_channel_converters.h | 1068 +++++++++ contrib/SDL-3.2.8/src/audio/SDL_audiocvt.c | 1381 +++++++++++ contrib/SDL-3.2.8/src/audio/SDL_audiodev.c | 124 + contrib/SDL-3.2.8/src/audio/SDL_audiodev_c.h | 41 + contrib/SDL-3.2.8/src/audio/SDL_audioqueue.c | 652 +++++ contrib/SDL-3.2.8/src/audio/SDL_audioqueue.h | 79 + contrib/SDL-3.2.8/src/audio/SDL_audioresample.c | 706 ++++++ contrib/SDL-3.2.8/src/audio/SDL_audioresample.h | 43 + contrib/SDL-3.2.8/src/audio/SDL_audiotypecvt.c | 925 +++++++ contrib/SDL-3.2.8/src/audio/SDL_mixer.c | 290 +++ contrib/SDL-3.2.8/src/audio/SDL_sysaudio.h | 392 +++ contrib/SDL-3.2.8/src/audio/SDL_wave.c | 2151 +++++++++++++++++ contrib/SDL-3.2.8/src/audio/SDL_wave.h | 151 ++ contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.c | 551 +++++ contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.h | 38 + .../SDL-3.2.8/src/audio/aaudio/SDL_aaudiofuncs.h | 82 + contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.c | 1519 ++++++++++++ contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.h | 41 + .../SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.h | 68 + .../SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.m | 1040 ++++++++ .../src/audio/directsound/SDL_directsound.c | 680 ++++++ .../src/audio/directsound/SDL_directsound.h | 43 + contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.c | 171 ++ contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.h | 36 + contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.c | 303 +++ contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.h | 37 + contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.c | 135 ++ contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.h | 34 + .../src/audio/emscripten/SDL_emscriptenaudio.c | 359 +++ .../src/audio/emscripten/SDL_emscriptenaudio.h | 33 + .../SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.cc | 222 ++ contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.h | 35 + contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.c | 435 ++++ contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.h | 35 + contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.c | 287 +++ contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.h | 40 + .../SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.c | 328 +++ .../SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.h | 44 + .../SDL-3.2.8/src/audio/openslES/SDL_openslES.c | 807 +++++++ .../SDL-3.2.8/src/audio/openslES/SDL_openslES.h | 38 + .../SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c | 1349 +++++++++++ .../SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h | 43 + contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.c | 159 ++ contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.h | 42 + contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.c | 183 ++ contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.h | 41 + .../src/audio/pulseaudio/SDL_pulseaudio.c | 1037 ++++++++ .../src/audio/pulseaudio/SDL_pulseaudio.h | 44 + contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.c | 451 ++++ contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.h | 40 + contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.c | 356 +++ contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.h | 38 + contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.c | 238 ++ contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.h | 41 + contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.c | 963 ++++++++ contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.h | 61 + 58 files changed, 23091 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audio.c create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audio_c.h create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audio_channel_converters.h create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audiocvt.c create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audiodev.c create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audiodev_c.h create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audioqueue.c create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audioqueue.h create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audioresample.c create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audioresample.h create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_audiotypecvt.c create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_mixer.c create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_sysaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_wave.c create mode 100644 contrib/SDL-3.2.8/src/audio/SDL_wave.h create mode 100644 contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudiofuncs.h create mode 100644 contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.c create mode 100644 contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.h create mode 100644 contrib/SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.m create mode 100644 contrib/SDL-3.2.8/src/audio/directsound/SDL_directsound.c create mode 100644 contrib/SDL-3.2.8/src/audio/directsound/SDL_directsound.h create mode 100644 contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/emscripten/SDL_emscriptenaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/emscripten/SDL_emscriptenaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.cc create mode 100644 contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/openslES/SDL_openslES.c create mode 100644 contrib/SDL-3.2.8/src/audio/openslES/SDL_openslES.h create mode 100644 contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c create mode 100644 contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h create mode 100644 contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.c create mode 100644 contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.h create mode 100644 contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.c create mode 100644 contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.h create mode 100644 contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.c create mode 100644 contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.h create mode 100644 contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.c create mode 100644 contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.h (limited to 'contrib/SDL-3.2.8/src/audio') diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audio.c b/contrib/SDL-3.2.8/src/audio/SDL_audio.c new file mode 100644 index 0000000..583a159 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audio.c @@ -0,0 +1,2534 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_audio_c.h" +#include "SDL_sysaudio.h" +#include "../thread/SDL_systhread.h" + +// Available audio drivers +static const AudioBootStrap *const bootstrap[] = { +#ifdef SDL_AUDIO_DRIVER_PRIVATE + &PRIVATEAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_PULSEAUDIO +#ifdef SDL_AUDIO_DRIVER_PIPEWIRE + &PIPEWIRE_PREFERRED_bootstrap, +#endif + &PULSEAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_PIPEWIRE + &PIPEWIRE_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_ALSA + &ALSA_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_SNDIO + &SNDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_NETBSD + &NETBSDAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_WASAPI + &WASAPI_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_DSOUND + &DSOUND_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_HAIKU + &HAIKUAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_COREAUDIO + &COREAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_AAUDIO + &AAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_OPENSLES + &OPENSLES_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_PS2 + &PS2AUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_PSP + &PSPAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_VITA + &VITAAUD_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_N3DS + &N3DSAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_EMSCRIPTEN + &EMSCRIPTENAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_JACK + &JACK_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_OSS + &DSP_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_QNX + &QSAAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_DISK + &DISKAUDIO_bootstrap, +#endif +#ifdef SDL_AUDIO_DRIVER_DUMMY + &DUMMYAUDIO_bootstrap, +#endif + NULL +}; + +static SDL_AudioDriver current_audio; + +// Deduplicated list of audio bootstrap drivers. +static const AudioBootStrap *deduped_bootstrap[SDL_arraysize(bootstrap) - 1]; + +int SDL_GetNumAudioDrivers(void) +{ + static int num_drivers = -1; + + if (num_drivers >= 0) { + return num_drivers; + } + + num_drivers = 0; + + // Build a list of unique audio drivers. + for (int i = 0; bootstrap[i] != NULL; ++i) { + bool duplicate = false; + for (int j = 0; j < i; ++j) { + if (SDL_strcmp(bootstrap[i]->name, bootstrap[j]->name) == 0) { + duplicate = true; + break; + } + } + + if (!duplicate) { + deduped_bootstrap[num_drivers++] = bootstrap[i]; + } + } + + return num_drivers; +} + +const char *SDL_GetAudioDriver(int index) +{ + if (index >= 0 && index < SDL_GetNumAudioDrivers()) { + return deduped_bootstrap[index]->name; + } + SDL_InvalidParamError("index"); + return NULL; +} + +const char *SDL_GetCurrentAudioDriver(void) +{ + return current_audio.name; +} + +int SDL_GetDefaultSampleFramesFromFreq(const int freq) +{ + const char *hint = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES); + if (hint) { + const int val = SDL_atoi(hint); + if (val > 0) { + return val; + } + } + + if (freq <= 22050) { + return 512; + } else if (freq <= 48000) { + return 1024; + } else if (freq <= 96000) { + return 2048; + } else { + return 4096; + } +} + +int *SDL_ChannelMapDup(const int *origchmap, int channels) +{ + const size_t chmaplen = sizeof (*origchmap) * channels; + int *chmap = (int *)SDL_malloc(chmaplen); + if (chmap) { + SDL_memcpy(chmap, origchmap, chmaplen); + } + return chmap; +} + +void OnAudioStreamCreated(SDL_AudioStream *stream) +{ + SDL_assert(stream != NULL); + + // NOTE that you can create an audio stream without initializing the audio subsystem, + // but it will not be automatically destroyed during a later call to SDL_Quit! + // You must explicitly destroy it yourself! + if (current_audio.device_hash_lock) { + // this isn't really part of the "device list" but it's a convenient lock to use here. + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + if (current_audio.existing_streams) { + current_audio.existing_streams->prev = stream; + } + stream->prev = NULL; + stream->next = current_audio.existing_streams; + current_audio.existing_streams = stream; + SDL_UnlockRWLock(current_audio.device_hash_lock); + } +} + +void OnAudioStreamDestroy(SDL_AudioStream *stream) +{ + SDL_assert(stream != NULL); + + // NOTE that you can create an audio stream without initializing the audio subsystem, + // but it will not be automatically destroyed during a later call to SDL_Quit! + // You must explicitly destroy it yourself! + if (current_audio.device_hash_lock) { + // this isn't really part of the "device list" but it's a convenient lock to use here. + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + if (stream->prev) { + stream->prev->next = stream->next; + } + if (stream->next) { + stream->next->prev = stream->prev; + } + if (stream == current_audio.existing_streams) { + current_audio.existing_streams = stream->next; + } + SDL_UnlockRWLock(current_audio.device_hash_lock); + } +} + +// device should be locked when calling this. +static bool AudioDeviceCanUseSimpleCopy(SDL_AudioDevice *device) +{ + SDL_assert(device != NULL); + return ( + device->logical_devices && // there's a logical device + !device->logical_devices->next && // there's only _ONE_ logical device + !device->logical_devices->postmix && // there isn't a postmix callback + device->logical_devices->bound_streams && // there's a bound stream + !device->logical_devices->bound_streams->next_binding // there's only _ONE_ bound stream. + ); +} + +// should hold device->lock before calling. +static void UpdateAudioStreamFormatsPhysical(SDL_AudioDevice *device) +{ + if (!device) { + return; + } + + const bool recording = device->recording; + SDL_AudioSpec spec; + SDL_copyp(&spec, &device->spec); + + const SDL_AudioFormat devformat = spec.format; + + if (!recording) { + const bool simple_copy = AudioDeviceCanUseSimpleCopy(device); + device->simple_copy = simple_copy; + if (!simple_copy) { + spec.format = SDL_AUDIO_F32; // mixing and postbuf operates in float32 format. + } + } + + for (SDL_LogicalAudioDevice *logdev = device->logical_devices; logdev; logdev = logdev->next) { + if (recording) { + const bool need_float32 = (logdev->postmix || logdev->gain != 1.0f); + spec.format = need_float32 ? SDL_AUDIO_F32 : devformat; + } + + for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) { + // set the proper end of the stream to the device's format. + // SDL_SetAudioStreamFormat does a ton of validation just to memcpy an audiospec. + SDL_AudioSpec *streamspec = recording ? &stream->src_spec : &stream->dst_spec; + int **streamchmap = recording ? &stream->src_chmap : &stream->dst_chmap; + SDL_LockMutex(stream->lock); + SDL_copyp(streamspec, &spec); + SetAudioStreamChannelMap(stream, streamspec, streamchmap, device->chmap, device->spec.channels, -1); // this should be fast for normal cases, though! + SDL_UnlockMutex(stream->lock); + } + } +} + +bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b, const int *channel_map_a, const int *channel_map_b) +{ + if ((a->format != b->format) || (a->channels != b->channels) || (a->freq != b->freq) || ((channel_map_a != NULL) != (channel_map_b != NULL))) { + return false; + } else if (channel_map_a && (SDL_memcmp(channel_map_a, channel_map_b, sizeof (*channel_map_a) * a->channels) != 0)) { + return false; + } + return true; +} + +bool SDL_AudioChannelMapsEqual(int channels, const int *channel_map_a, const int *channel_map_b) +{ + if (channel_map_a == channel_map_b) { + return true; + } else if ((channel_map_a != NULL) != (channel_map_b != NULL)) { + return false; + } else if (channel_map_a && (SDL_memcmp(channel_map_a, channel_map_b, sizeof (*channel_map_a) * channels) != 0)) { + return false; + } + return true; +} + + +// Zombie device implementation... + +// These get used when a device is disconnected or fails, so audiostreams don't overflow with data that isn't being +// consumed and apps relying on audio callbacks don't stop making progress. +static bool ZombieWaitDevice(SDL_AudioDevice *device) +{ + if (!SDL_GetAtomicInt(&device->shutdown)) { + const int frames = device->buffer_size / SDL_AUDIO_FRAMESIZE(device->spec); + SDL_Delay((frames * 1000) / device->spec.freq); + } + return true; +} + +static bool ZombiePlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + return true; // no-op, just throw the audio away. +} + +static Uint8 *ZombieGetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->work_buffer; +} + +static int ZombieRecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + // return a full buffer of silence every time. + SDL_memset(buffer, device->silence_value, buflen); + return buflen; +} + +static void ZombieFlushRecording(SDL_AudioDevice *device) +{ + // no-op, this is all imaginary. +} + + + +// device management and hotplug... + + +/* SDL_AudioDevice, in SDL3, represents a piece of physical hardware, whether it is in use or not, so these objects exist as long as + the system-level device is available. + + Physical devices get destroyed for three reasons: + - They were lost to the system (a USB cable is kicked out, etc). + - They failed for some other unlikely reason at the API level (which is _also_ probably a USB cable being kicked out). + - We are shutting down, so all allocated resources are being freed. + + They are _not_ destroyed because we are done using them (when we "close" a playing device). +*/ +static void ClosePhysicalAudioDevice(SDL_AudioDevice *device); + + +SDL_COMPILE_TIME_ASSERT(check_lowest_audio_default_value, SDL_AUDIO_DEVICE_DEFAULT_RECORDING < SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK); + +static SDL_AtomicInt last_device_instance_id; // increments on each device add to provide unique instance IDs +static SDL_AudioDeviceID AssignAudioDeviceInstanceId(bool recording, bool islogical) +{ + /* Assign an instance id! Start at 2, in case there are things from the SDL2 era that still think 1 is a special value. + Also, make sure we don't assign SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, etc. */ + + // The bottom two bits of the instance id tells you if it's an playback device (1<<0), and if it's a physical device (1<<1). + const SDL_AudioDeviceID flags = (recording ? 0 : (1<<0)) | (islogical ? 0 : (1<<1)); + + const SDL_AudioDeviceID instance_id = (((SDL_AudioDeviceID) (SDL_AtomicIncRef(&last_device_instance_id) + 1)) << 2) | flags; + SDL_assert( (instance_id >= 2) && (instance_id < SDL_AUDIO_DEVICE_DEFAULT_RECORDING) ); + return instance_id; +} + +bool SDL_IsAudioDevicePhysical(SDL_AudioDeviceID devid) +{ + return (devid & (1 << 1)) != 0; +} + +bool SDL_IsAudioDevicePlayback(SDL_AudioDeviceID devid) +{ + return (devid & (1 << 0)) != 0; +} + +static void ObtainPhysicalAudioDeviceObj(SDL_AudioDevice *device) SDL_NO_THREAD_SAFETY_ANALYSIS // !!! FIXMEL SDL_ACQUIRE +{ + if (device) { + RefPhysicalAudioDevice(device); + SDL_LockMutex(device->lock); + } +} + +static void ReleaseAudioDevice(SDL_AudioDevice *device) SDL_NO_THREAD_SAFETY_ANALYSIS // !!! FIXME: SDL_RELEASE +{ + if (device) { + SDL_UnlockMutex(device->lock); + UnrefPhysicalAudioDevice(device); + } +} + +// If found, this locks _the physical device_ this logical device is associated with, before returning. +static SDL_LogicalAudioDevice *ObtainLogicalAudioDevice(SDL_AudioDeviceID devid, SDL_AudioDevice **_device) SDL_NO_THREAD_SAFETY_ANALYSIS // !!! FIXME: SDL_ACQUIRE +{ + SDL_assert(_device != NULL); + + if (!SDL_GetCurrentAudioDriver()) { + SDL_SetError("Audio subsystem is not initialized"); + *_device = NULL; + return NULL; + } + + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = NULL; + + // bit #1 of devid is set for physical devices and unset for logical. + const bool islogical = !(devid & (1<<1)); + if (islogical) { // don't bother looking if it's not a logical device id value. + SDL_LockRWLockForReading(current_audio.device_hash_lock); + SDL_FindInHashTable(current_audio.device_hash, (const void *) (uintptr_t) devid, (const void **) &logdev); + if (logdev) { + device = logdev->physical_device; + SDL_assert(device != NULL); + RefPhysicalAudioDevice(device); // reference it, in case the logical device migrates to a new default. + } + SDL_UnlockRWLock(current_audio.device_hash_lock); + + if (logdev) { + // we have to release the device_hash_lock before we take the device lock, to avoid deadlocks, so do a loop + // to make sure the correct physical device gets locked, in case we're in a race with the default changing. + while (true) { + SDL_LockMutex(device->lock); + SDL_AudioDevice *recheck_device = (SDL_AudioDevice *) SDL_GetAtomicPointer((void **) &logdev->physical_device); + if (device == recheck_device) { + break; + } + + // default changed from under us! Try again! + RefPhysicalAudioDevice(recheck_device); + SDL_UnlockMutex(device->lock); + UnrefPhysicalAudioDevice(device); + device = recheck_device; + } + } + } + + if (!logdev) { + SDL_SetError("Invalid audio device instance ID"); + } + + *_device = device; + return logdev; +} + + +/* this finds the physical device associated with `devid` and locks it for use. + Note that a logical device instance id will return its associated physical device! */ +static SDL_AudioDevice *ObtainPhysicalAudioDevice(SDL_AudioDeviceID devid) // !!! FIXME: SDL_ACQUIRE +{ + SDL_AudioDevice *device = NULL; + + // bit #1 of devid is set for physical devices and unset for logical. + const bool islogical = !(devid & (1<<1)); + if (islogical) { + ObtainLogicalAudioDevice(devid, &device); + } else if (!SDL_GetCurrentAudioDriver()) { // (the `islogical` path, above, checks this in ObtainLogicalAudioDevice.) + SDL_SetError("Audio subsystem is not initialized"); + } else { + SDL_LockRWLockForReading(current_audio.device_hash_lock); + SDL_FindInHashTable(current_audio.device_hash, (const void *) (uintptr_t) devid, (const void **) &device); + SDL_UnlockRWLock(current_audio.device_hash_lock); + + if (!device) { + SDL_SetError("Invalid audio device instance ID"); + } else { + ObtainPhysicalAudioDeviceObj(device); + } + } + + return device; +} + +static SDL_AudioDevice *ObtainPhysicalAudioDeviceDefaultAllowed(SDL_AudioDeviceID devid) // !!! FIXME: SDL_ACQUIRE +{ + const bool wants_default = ((devid == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK) || (devid == SDL_AUDIO_DEVICE_DEFAULT_RECORDING)); + if (!wants_default) { + return ObtainPhysicalAudioDevice(devid); + } + + const SDL_AudioDeviceID orig_devid = devid; + + while (true) { + SDL_LockRWLockForReading(current_audio.device_hash_lock); + if (orig_devid == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK) { + devid = current_audio.default_playback_device_id; + } else if (orig_devid == SDL_AUDIO_DEVICE_DEFAULT_RECORDING) { + devid = current_audio.default_recording_device_id; + } + SDL_UnlockRWLock(current_audio.device_hash_lock); + + if (devid == 0) { + SDL_SetError("No default audio device available"); + break; + } + + SDL_AudioDevice *device = ObtainPhysicalAudioDevice(devid); + if (!device) { + break; + } + + // make sure the default didn't change while we were waiting for the lock... + bool got_it = false; + SDL_LockRWLockForReading(current_audio.device_hash_lock); + if ((orig_devid == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK) && (devid == current_audio.default_playback_device_id)) { + got_it = true; + } else if ((orig_devid == SDL_AUDIO_DEVICE_DEFAULT_RECORDING) && (devid == current_audio.default_recording_device_id)) { + got_it = true; + } + SDL_UnlockRWLock(current_audio.device_hash_lock); + + if (got_it) { + return device; + } + + ReleaseAudioDevice(device); // let it go and try again. + } + + return NULL; +} + +// this assumes you hold the _physical_ device lock for this logical device! This will not unlock the lock or close the physical device! +// It also will not unref the physical device, since we might be shutting down; SDL_CloseAudioDevice handles the unref. +static void DestroyLogicalAudioDevice(SDL_LogicalAudioDevice *logdev) +{ + // Remove ourselves from the device_hash hashtable. + if (current_audio.device_hash) { // will be NULL while shutting down. + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + SDL_RemoveFromHashTable(current_audio.device_hash, (const void *) (uintptr_t) logdev->instance_id); + SDL_UnlockRWLock(current_audio.device_hash_lock); + } + + // remove ourselves from the physical device's list of logical devices. + if (logdev->next) { + logdev->next->prev = logdev->prev; + } + if (logdev->prev) { + logdev->prev->next = logdev->next; + } + if (logdev->physical_device->logical_devices == logdev) { + logdev->physical_device->logical_devices = logdev->next; + } + + // unbind any still-bound streams... + SDL_AudioStream *next; + for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = next) { + SDL_LockMutex(stream->lock); + next = stream->next_binding; + stream->next_binding = NULL; + stream->prev_binding = NULL; + stream->bound_device = NULL; + SDL_UnlockMutex(stream->lock); + } + + UpdateAudioStreamFormatsPhysical(logdev->physical_device); + SDL_free(logdev); +} + +// this must not be called while `device` is still in a device list, or while a device's audio thread is still running. +static void DestroyPhysicalAudioDevice(SDL_AudioDevice *device) +{ + if (!device) { + return; + } + + // Destroy any logical devices that still exist... + SDL_LockMutex(device->lock); // don't use ObtainPhysicalAudioDeviceObj because we don't want to change refcounts while destroying. + while (device->logical_devices) { + DestroyLogicalAudioDevice(device->logical_devices); + } + + ClosePhysicalAudioDevice(device); + + current_audio.impl.FreeDeviceHandle(device); + + SDL_UnlockMutex(device->lock); // don't use ReleaseAudioDevice because we don't want to change refcounts while destroying. + + SDL_DestroyMutex(device->lock); + SDL_DestroyCondition(device->close_cond); + SDL_free(device->work_buffer); + SDL_free(device->chmap); + SDL_free(device->name); + SDL_free(device); +} + +// Don't hold the device lock when calling this, as we may destroy the device! +void UnrefPhysicalAudioDevice(SDL_AudioDevice *device) +{ + if (SDL_AtomicDecRef(&device->refcount)) { + // take it out of the device list. + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + if (SDL_RemoveFromHashTable(current_audio.device_hash, (const void *) (uintptr_t) device->instance_id)) { + SDL_AddAtomicInt(device->recording ? ¤t_audio.recording_device_count : ¤t_audio.playback_device_count, -1); + } + SDL_UnlockRWLock(current_audio.device_hash_lock); + DestroyPhysicalAudioDevice(device); // ...and nuke it. + } +} + +void RefPhysicalAudioDevice(SDL_AudioDevice *device) +{ + SDL_AtomicIncRef(&device->refcount); +} + +static SDL_AudioDevice *CreatePhysicalAudioDevice(const char *name, bool recording, const SDL_AudioSpec *spec, void *handle, SDL_AtomicInt *device_count) +{ + SDL_assert(name != NULL); + + SDL_LockRWLockForReading(current_audio.device_hash_lock); + const int shutting_down = SDL_GetAtomicInt(¤t_audio.shutting_down); + SDL_UnlockRWLock(current_audio.device_hash_lock); + if (shutting_down) { + return NULL; // we're shutting down, don't add any devices that are hotplugged at the last possible moment. + } + + SDL_AudioDevice *device = (SDL_AudioDevice *)SDL_calloc(1, sizeof(SDL_AudioDevice)); + if (!device) { + return NULL; + } + + device->name = SDL_strdup(name); + if (!device->name) { + SDL_free(device); + return NULL; + } + + device->lock = SDL_CreateMutex(); + if (!device->lock) { + SDL_free(device->name); + SDL_free(device); + return NULL; + } + + device->close_cond = SDL_CreateCondition(); + if (!device->close_cond) { + SDL_DestroyMutex(device->lock); + SDL_free(device->name); + SDL_free(device); + return NULL; + } + + SDL_SetAtomicInt(&device->shutdown, 0); + SDL_SetAtomicInt(&device->zombie, 0); + device->recording = recording; + SDL_copyp(&device->spec, spec); + SDL_copyp(&device->default_spec, spec); + device->sample_frames = SDL_GetDefaultSampleFramesFromFreq(device->spec.freq); + device->silence_value = SDL_GetSilenceValueForFormat(device->spec.format); + device->handle = handle; + + device->instance_id = AssignAudioDeviceInstanceId(recording, /*islogical=*/false); + + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + if (SDL_InsertIntoHashTable(current_audio.device_hash, (const void *) (uintptr_t) device->instance_id, device, false)) { + SDL_AddAtomicInt(device_count, 1); + } else { + SDL_DestroyCondition(device->close_cond); + SDL_DestroyMutex(device->lock); + SDL_free(device->name); + SDL_free(device); + device = NULL; + } + SDL_UnlockRWLock(current_audio.device_hash_lock); + + RefPhysicalAudioDevice(device); // unref'd on device disconnect. + return device; +} + +static SDL_AudioDevice *CreateAudioRecordingDevice(const char *name, const SDL_AudioSpec *spec, void *handle) +{ + SDL_assert(current_audio.impl.HasRecordingSupport); + return CreatePhysicalAudioDevice(name, true, spec, handle, ¤t_audio.recording_device_count); +} + +static SDL_AudioDevice *CreateAudioPlaybackDevice(const char *name, const SDL_AudioSpec *spec, void *handle) +{ + return CreatePhysicalAudioDevice(name, false, spec, handle, ¤t_audio.playback_device_count); +} + +// The audio backends call this when a new device is plugged in. +SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const SDL_AudioSpec *inspec, void *handle) +{ + // device handles MUST be unique! If the target reuses the same handle for hardware with both recording and playback interfaces, wrap it in a pointer you SDL_malloc'd! + SDL_assert(SDL_FindPhysicalAudioDeviceByHandle(handle) == NULL); + + const SDL_AudioFormat default_format = recording ? DEFAULT_AUDIO_RECORDING_FORMAT : DEFAULT_AUDIO_PLAYBACK_FORMAT; + const int default_channels = recording ? DEFAULT_AUDIO_RECORDING_CHANNELS : DEFAULT_AUDIO_PLAYBACK_CHANNELS; + const int default_freq = recording ? DEFAULT_AUDIO_RECORDING_FREQUENCY : DEFAULT_AUDIO_PLAYBACK_FREQUENCY; + + SDL_AudioSpec spec; + SDL_zero(spec); + if (!inspec) { + spec.format = default_format; + spec.channels = default_channels; + spec.freq = default_freq; + } else { + spec.format = (inspec->format != 0) ? inspec->format : default_format; + spec.channels = (inspec->channels != 0) ? inspec->channels : default_channels; + spec.freq = (inspec->freq != 0) ? inspec->freq : default_freq; + } + + SDL_AudioDevice *device = recording ? CreateAudioRecordingDevice(name, &spec, handle) : CreateAudioPlaybackDevice(name, &spec, handle); + + // Add a device add event to the pending list, to be pushed when the event queue is pumped (away from any of our internal threads). + if (device) { + SDL_PendingAudioDeviceEvent *p = (SDL_PendingAudioDeviceEvent *) SDL_malloc(sizeof (SDL_PendingAudioDeviceEvent)); + if (p) { // if allocation fails, you won't get an event, but we can't help that. + p->type = SDL_EVENT_AUDIO_DEVICE_ADDED; + p->devid = device->instance_id; + p->next = NULL; + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + SDL_assert(current_audio.pending_events_tail != NULL); + SDL_assert(current_audio.pending_events_tail->next == NULL); + current_audio.pending_events_tail->next = p; + current_audio.pending_events_tail = p; + SDL_UnlockRWLock(current_audio.device_hash_lock); + } + } + + return device; +} + +// Called when a device is removed from the system, or it fails unexpectedly, from any thread, possibly even the audio device's thread. +void SDL_AudioDeviceDisconnected(SDL_AudioDevice *device) +{ + if (!device) { + return; + } + + // Save off removal info in a list so we can send events for each, next + // time the event queue pumps, in case something tries to close a device + // from an event filter, as this would risk deadlocks and other disasters + // if done from the device thread. + SDL_PendingAudioDeviceEvent pending; + pending.next = NULL; + SDL_PendingAudioDeviceEvent *pending_tail = &pending; + + ObtainPhysicalAudioDeviceObj(device); + + SDL_LockRWLockForReading(current_audio.device_hash_lock); + const SDL_AudioDeviceID devid = device->instance_id; + const bool is_default_device = ((devid == current_audio.default_playback_device_id) || (devid == current_audio.default_recording_device_id)); + SDL_UnlockRWLock(current_audio.device_hash_lock); + + const bool first_disconnect = SDL_CompareAndSwapAtomicInt(&device->zombie, 0, 1); + if (first_disconnect) { // if already disconnected this device, don't do it twice. + // Swap in "Zombie" versions of the usual platform interfaces, so the device will keep + // making progress until the app closes it. Otherwise, streams might continue to + // accumulate waste data that never drains, apps that depend on audio callbacks to + // progress will freeze, etc. + device->WaitDevice = ZombieWaitDevice; + device->GetDeviceBuf = ZombieGetDeviceBuf; + device->PlayDevice = ZombiePlayDevice; + device->WaitRecordingDevice = ZombieWaitDevice; + device->RecordDevice = ZombieRecordDevice; + device->FlushRecording = ZombieFlushRecording; + + // on default devices, dump any logical devices that explicitly opened this device. Things that opened the system default can stay. + // on non-default devices, dump everything. + // (by "dump" we mean send a REMOVED event; the zombie will keep consuming audio data for these logical devices until explicitly closed.) + for (SDL_LogicalAudioDevice *logdev = device->logical_devices; logdev; logdev = logdev->next) { + if (!is_default_device || !logdev->opened_as_default) { // if opened as a default, leave it on the zombie device for later migration. + SDL_PendingAudioDeviceEvent *p = (SDL_PendingAudioDeviceEvent *) SDL_malloc(sizeof (SDL_PendingAudioDeviceEvent)); + if (p) { // if this failed, no event for you, but you have deeper problems anyhow. + p->type = SDL_EVENT_AUDIO_DEVICE_REMOVED; + p->devid = logdev->instance_id; + p->next = NULL; + pending_tail->next = p; + pending_tail = p; + } + } + } + + SDL_PendingAudioDeviceEvent *p = (SDL_PendingAudioDeviceEvent *) SDL_malloc(sizeof (SDL_PendingAudioDeviceEvent)); + if (p) { // if this failed, no event for you, but you have deeper problems anyhow. + p->type = SDL_EVENT_AUDIO_DEVICE_REMOVED; + p->devid = device->instance_id; + p->next = NULL; + pending_tail->next = p; + pending_tail = p; + } + } + + ReleaseAudioDevice(device); + + if (first_disconnect) { + if (pending.next) { // NULL if event is disabled or disaster struck. + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + SDL_assert(current_audio.pending_events_tail != NULL); + SDL_assert(current_audio.pending_events_tail->next == NULL); + current_audio.pending_events_tail->next = pending.next; + current_audio.pending_events_tail = pending_tail; + SDL_UnlockRWLock(current_audio.device_hash_lock); + } + + UnrefPhysicalAudioDevice(device); + } +} + + +// stubs for audio drivers that don't need a specific entry point... + +static void SDL_AudioThreadDeinit_Default(SDL_AudioDevice *device) { /* no-op. */ } +static bool SDL_AudioWaitDevice_Default(SDL_AudioDevice *device) { return true; /* no-op. */ } +static bool SDL_AudioPlayDevice_Default(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size) { return true; /* no-op. */ } +static bool SDL_AudioWaitRecordingDevice_Default(SDL_AudioDevice *device) { return true; /* no-op. */ } +static void SDL_AudioFlushRecording_Default(SDL_AudioDevice *device) { /* no-op. */ } +static void SDL_AudioCloseDevice_Default(SDL_AudioDevice *device) { /* no-op. */ } +static void SDL_AudioDeinitializeStart_Default(void) { /* no-op. */ } +static void SDL_AudioDeinitialize_Default(void) { /* no-op. */ } +static void SDL_AudioFreeDeviceHandle_Default(SDL_AudioDevice *device) { /* no-op. */ } + +static void SDL_AudioThreadInit_Default(SDL_AudioDevice *device) +{ + SDL_SetCurrentThreadPriority(device->recording ? SDL_THREAD_PRIORITY_HIGH : SDL_THREAD_PRIORITY_TIME_CRITICAL); +} + +static void SDL_AudioDetectDevices_Default(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + // you have to write your own implementation if these assertions fail. + SDL_assert(current_audio.impl.OnlyHasDefaultPlaybackDevice); + SDL_assert(current_audio.impl.OnlyHasDefaultRecordingDevice || !current_audio.impl.HasRecordingSupport); + + *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, (void *)((size_t)0x1)); + if (current_audio.impl.HasRecordingSupport) { + *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, (void *)((size_t)0x2)); + } +} + +static Uint8 *SDL_AudioGetDeviceBuf_Default(SDL_AudioDevice *device, int *buffer_size) +{ + *buffer_size = 0; + return NULL; +} + +static int SDL_AudioRecordDevice_Default(SDL_AudioDevice *device, void *buffer, int buflen) +{ + SDL_Unsupported(); + return -1; +} + +static bool SDL_AudioOpenDevice_Default(SDL_AudioDevice *device) +{ + return SDL_Unsupported(); +} + +// Fill in stub functions for unused driver entry points. This lets us blindly call them without having to check for validity first. +static void CompleteAudioEntryPoints(void) +{ + #define FILL_STUB(x) if (!current_audio.impl.x) { current_audio.impl.x = SDL_Audio##x##_Default; } + FILL_STUB(DetectDevices); + FILL_STUB(OpenDevice); + FILL_STUB(ThreadInit); + FILL_STUB(ThreadDeinit); + FILL_STUB(WaitDevice); + FILL_STUB(PlayDevice); + FILL_STUB(GetDeviceBuf); + FILL_STUB(WaitRecordingDevice); + FILL_STUB(RecordDevice); + FILL_STUB(FlushRecording); + FILL_STUB(CloseDevice); + FILL_STUB(FreeDeviceHandle); + FILL_STUB(DeinitializeStart); + FILL_STUB(Deinitialize); + #undef FILL_STUB +} + +typedef struct FindLowestDeviceIDData +{ + const bool recording; + SDL_AudioDeviceID highest; + SDL_AudioDevice *result; +} FindLowestDeviceIDData; + +static bool SDLCALL FindLowestDeviceID(void *userdata, const SDL_HashTable *table, const void *key, const void *value) +{ + FindLowestDeviceIDData *data = (FindLowestDeviceIDData *) userdata; + const SDL_AudioDeviceID devid = (SDL_AudioDeviceID) (uintptr_t) key; + // bit #0 of devid is set for playback devices and unset for recording. + // bit #1 of devid is set for physical devices and unset for logical. + const bool devid_recording = !(devid & (1 << 0)); + const bool isphysical = !!(devid & (1 << 1)); + if (isphysical && (devid_recording == data->recording) && (devid < data->highest)) { + data->highest = devid; + data->result = (SDL_AudioDevice *) value; + } + return true; // keep iterating. +} + +static SDL_AudioDevice *GetFirstAddedAudioDevice(const bool recording) +{ + const SDL_AudioDeviceID highest = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; // According to AssignAudioDeviceInstanceId, nothing can have a value this large. + + // (Device IDs increase as new devices are added, so the first device added has the lowest SDL_AudioDeviceID value.) + FindLowestDeviceIDData data = { recording, highest, NULL }; + SDL_LockRWLockForReading(current_audio.device_hash_lock); + SDL_IterateHashTable(current_audio.device_hash, FindLowestDeviceID, &data); + SDL_UnlockRWLock(current_audio.device_hash_lock); + return data.result; +} + +static Uint32 SDLCALL HashAudioDeviceID(void *userdata, const void *key) +{ + // shift right 2, to dump the first two bits, since these are flags + // (recording vs playback, logical vs physical) and the rest are unique incrementing integers. + return ((Uint32) ((uintptr_t) key)) >> 2; +} + +// !!! FIXME: the video subsystem does SDL_VideoInit, not SDL_InitVideo. Make this match. +bool SDL_InitAudio(const char *driver_name) +{ + if (SDL_GetCurrentAudioDriver()) { + SDL_QuitAudio(); // shutdown driver if already running. + } + + // make sure device IDs start at 2 (because of SDL2 legacy interface), but don't reset the counter on each init, in case the app is holding an old device ID somewhere. + SDL_CompareAndSwapAtomicInt(&last_device_instance_id, 0, 2); + + SDL_ChooseAudioConverters(); + SDL_SetupAudioResampler(); + + SDL_RWLock *device_hash_lock = SDL_CreateRWLock(); // create this early, so if it fails we don't have to tear down the whole audio subsystem. + if (!device_hash_lock) { + return false; + } + + SDL_HashTable *device_hash = SDL_CreateHashTable(0, false, HashAudioDeviceID, SDL_KeyMatchID, NULL, NULL); + if (!device_hash) { + SDL_DestroyRWLock(device_hash_lock); + return false; + } + + // Select the proper audio driver + if (!driver_name) { + driver_name = SDL_GetHint(SDL_HINT_AUDIO_DRIVER); + } + + bool initialized = false; + bool tried_to_init = false; + + if (driver_name && *driver_name != 0) { + char *driver_name_copy = SDL_strdup(driver_name); + const char *driver_attempt = driver_name_copy; + + if (!driver_name_copy) { + SDL_DestroyRWLock(device_hash_lock); + SDL_DestroyHashTable(device_hash); + return false; + } + + while (driver_attempt && *driver_attempt != 0 && !initialized) { + char *driver_attempt_end = SDL_strchr(driver_attempt, ','); + if (driver_attempt_end) { + *driver_attempt_end = '\0'; + } + + // SDL 1.2 uses the name "dsound", so we'll support both. + if (SDL_strcmp(driver_attempt, "dsound") == 0) { + driver_attempt = "directsound"; + } else if (SDL_strcmp(driver_attempt, "pulse") == 0) { // likewise, "pulse" was renamed to "pulseaudio" + driver_attempt = "pulseaudio"; + } + + for (int i = 0; bootstrap[i]; ++i) { + if (!bootstrap[i]->is_preferred && SDL_strcasecmp(bootstrap[i]->name, driver_attempt) == 0) { + tried_to_init = true; + SDL_zero(current_audio); + current_audio.pending_events_tail = ¤t_audio.pending_events; + current_audio.device_hash_lock = device_hash_lock; + current_audio.device_hash = device_hash; + if (bootstrap[i]->init(¤t_audio.impl)) { + current_audio.name = bootstrap[i]->name; + current_audio.desc = bootstrap[i]->desc; + initialized = true; + break; + } + } + } + + driver_attempt = (driver_attempt_end) ? (driver_attempt_end + 1) : NULL; + } + + SDL_free(driver_name_copy); + } else { + for (int i = 0; (!initialized) && (bootstrap[i]); ++i) { + if (bootstrap[i]->demand_only) { + continue; + } + + tried_to_init = true; + SDL_zero(current_audio); + current_audio.pending_events_tail = ¤t_audio.pending_events; + current_audio.device_hash_lock = device_hash_lock; + current_audio.device_hash = device_hash; + if (bootstrap[i]->init(¤t_audio.impl)) { + current_audio.name = bootstrap[i]->name; + current_audio.desc = bootstrap[i]->desc; + initialized = true; + } + } + } + + if (!initialized) { + // specific drivers will set the error message if they fail, but otherwise we do it here. + if (!tried_to_init) { + if (driver_name) { + SDL_SetError("Audio target '%s' not available", driver_name); + } else { + SDL_SetError("No available audio device"); + } + } + + SDL_DestroyRWLock(device_hash_lock); + SDL_DestroyHashTable(device_hash); + SDL_zero(current_audio); + return false; // No driver was available, so fail. + } + + CompleteAudioEntryPoints(); + + // Make sure we have a list of devices available at startup... + SDL_AudioDevice *default_playback = NULL; + SDL_AudioDevice *default_recording = NULL; + current_audio.impl.DetectDevices(&default_playback, &default_recording); + + // If no default was _ever_ specified, just take the first device we see, if any. + if (!default_playback) { + default_playback = GetFirstAddedAudioDevice(/*recording=*/false); + } + + if (!default_recording) { + default_recording = GetFirstAddedAudioDevice(/*recording=*/true); + } + + if (default_playback) { + current_audio.default_playback_device_id = default_playback->instance_id; + RefPhysicalAudioDevice(default_playback); // extra ref on default devices. + } + + if (default_recording) { + current_audio.default_recording_device_id = default_recording->instance_id; + RefPhysicalAudioDevice(default_recording); // extra ref on default devices. + } + + return true; +} + +static bool SDLCALL DestroyOnePhysicalAudioDevice(void *userdata, const SDL_HashTable *table, const void *key, const void *value) +{ + // bit #1 of devid is set for physical devices and unset for logical. + const SDL_AudioDeviceID devid = (SDL_AudioDeviceID) (uintptr_t) key; + const bool isphysical = !!(devid & (1<<1)); + if (isphysical) { + DestroyPhysicalAudioDevice((SDL_AudioDevice *) value); + } + return true; // keep iterating. +} + +void SDL_QuitAudio(void) +{ + if (!current_audio.name) { // not initialized?! + return; + } + + current_audio.impl.DeinitializeStart(); + + // Destroy any audio streams that still exist... + while (current_audio.existing_streams) { + SDL_DestroyAudioStream(current_audio.existing_streams); + } + + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + SDL_SetAtomicInt(¤t_audio.shutting_down, 1); + SDL_HashTable *device_hash = current_audio.device_hash; + current_audio.device_hash = NULL; + SDL_PendingAudioDeviceEvent *pending_events = current_audio.pending_events.next; + current_audio.pending_events.next = NULL; + SDL_SetAtomicInt(¤t_audio.playback_device_count, 0); + SDL_SetAtomicInt(¤t_audio.recording_device_count, 0); + SDL_UnlockRWLock(current_audio.device_hash_lock); + + SDL_PendingAudioDeviceEvent *pending_next = NULL; + for (SDL_PendingAudioDeviceEvent *i = pending_events; i; i = pending_next) { + pending_next = i->next; + SDL_free(i); + } + + SDL_IterateHashTable(device_hash, DestroyOnePhysicalAudioDevice, NULL); + + // Free the driver data + current_audio.impl.Deinitialize(); + + SDL_DestroyRWLock(current_audio.device_hash_lock); + SDL_DestroyHashTable(device_hash); + + SDL_zero(current_audio); +} + + +void SDL_AudioThreadFinalize(SDL_AudioDevice *device) +{ +} + +static void MixFloat32Audio(float *dst, const float *src, const int buffer_size) +{ + if (!SDL_MixAudio((Uint8 *) dst, (const Uint8 *) src, SDL_AUDIO_F32, buffer_size, 1.0f)) { + SDL_assert(!"This shouldn't happen."); + } +} + + +// Playback device thread. This is split into chunks, so backends that need to control this directly can use the pieces they need without duplicating effort. + +void SDL_PlaybackAudioThreadSetup(SDL_AudioDevice *device) +{ + SDL_assert(!device->recording); + current_audio.impl.ThreadInit(device); +} + +bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) +{ + SDL_assert(!device->recording); + + SDL_LockMutex(device->lock); + + if (SDL_GetAtomicInt(&device->shutdown)) { + SDL_UnlockMutex(device->lock); + return false; // we're done, shut it down. + } + + bool failed = false; + int buffer_size = device->buffer_size; + Uint8 *device_buffer = device->GetDeviceBuf(device, &buffer_size); + if (buffer_size == 0) { + // WASAPI (maybe others, later) does this to say "just abandon this iteration and try again next time." + } else if (!device_buffer) { + failed = true; + } else { + SDL_assert(buffer_size <= device->buffer_size); // you can ask for less, but not more. + SDL_assert(AudioDeviceCanUseSimpleCopy(device) == device->simple_copy); // make sure this hasn't gotten out of sync. + + // can we do a basic copy without silencing/mixing the buffer? This is an extremely likely scenario, so we special-case it. + if (device->simple_copy) { + SDL_LogicalAudioDevice *logdev = device->logical_devices; + SDL_AudioStream *stream = logdev->bound_streams; + + // We should have updated this elsewhere if the format changed! + SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &device->spec, NULL, NULL)); + + const int br = SDL_GetAtomicInt(&logdev->paused) ? 0 : SDL_GetAudioStreamDataAdjustGain(stream, device_buffer, buffer_size, logdev->gain); + if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow. + failed = true; + SDL_memset(device_buffer, device->silence_value, buffer_size); // just supply silence to the device before we die. + } else if (br < buffer_size) { + SDL_memset(device_buffer + br, device->silence_value, buffer_size - br); // silence whatever we didn't write to. + } + + // generally channel maps will line up, but if the audio stream's chmap has been explicitly changed, do a final swizzle to device layout. + if ((br > 0) && (!SDL_AudioChannelMapsEqual(device->spec.channels, stream->dst_chmap, device->chmap))) { + ConvertAudio(br / SDL_AUDIO_FRAMESIZE(device->spec), device_buffer, device->spec.format, device->spec.channels, NULL, + device_buffer, device->spec.format, device->spec.channels, device->chmap, NULL, 1.0f); + } + } else { // need to actually mix (or silence the buffer) + float *final_mix_buffer = (float *) ((device->spec.format == SDL_AUDIO_F32) ? device_buffer : device->mix_buffer); + const int needed_samples = buffer_size / SDL_AUDIO_BYTESIZE(device->spec.format); + const int work_buffer_size = needed_samples * sizeof (float); + SDL_AudioSpec outspec; + + SDL_assert(work_buffer_size <= device->work_buffer_size); + + SDL_copyp(&outspec, &device->spec); + outspec.format = SDL_AUDIO_F32; + + SDL_memset(final_mix_buffer, '\0', work_buffer_size); // start with silence. + + for (SDL_LogicalAudioDevice *logdev = device->logical_devices; logdev; logdev = logdev->next) { + if (SDL_GetAtomicInt(&logdev->paused)) { + continue; // paused? Skip this logical device. + } + + const SDL_AudioPostmixCallback postmix = logdev->postmix; + float *mix_buffer = final_mix_buffer; + if (postmix) { + mix_buffer = device->postmix_buffer; + SDL_memset(mix_buffer, '\0', work_buffer_size); // start with silence. + } + + for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) { + // We should have updated this elsewhere if the format changed! + SDL_assert(SDL_AudioSpecsEqual(&stream->dst_spec, &outspec, NULL, NULL)); + + /* this will hold a lock on `stream` while getting. We don't explicitly lock the streams + for iterating here because the binding linked list can only change while the device lock is held. + (we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind + the same stream to different devices at the same time, though.) */ + const int br = SDL_GetAudioStreamDataAdjustGain(stream, device->work_buffer, work_buffer_size, logdev->gain); + if (br < 0) { // Probably OOM. Kill the audio device; the whole thing is likely dying soon anyhow. + failed = true; + break; + } else if (br > 0) { // it's okay if we get less than requested, we mix what we have. + // generally channel maps will line up, but if the audio stream's chmap has been explicitly changed, do a final swizzle to device layout. + if (!SDL_AudioChannelMapsEqual(device->spec.channels, stream->dst_chmap, device->chmap)) { + ConvertAudio(br / SDL_AUDIO_FRAMESIZE(device->spec), device->work_buffer, device->spec.format, device->spec.channels, NULL, + device->work_buffer, device->spec.format, device->spec.channels, device->chmap, NULL, 1.0f); + } + MixFloat32Audio(mix_buffer, (float *) device->work_buffer, br); + } + } + + if (postmix) { + SDL_assert(mix_buffer == device->postmix_buffer); + postmix(logdev->postmix_userdata, &outspec, mix_buffer, work_buffer_size); + MixFloat32Audio(final_mix_buffer, mix_buffer, work_buffer_size); + } + } + + if (((Uint8 *) final_mix_buffer) != device_buffer) { + // !!! FIXME: we can't promise the device buf is aligned/padded for SIMD. + //ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device_buffer, device->spec.format, device->spec.channels, NULL, NULL, 1.0f); + ConvertAudio(needed_samples / device->spec.channels, final_mix_buffer, SDL_AUDIO_F32, device->spec.channels, NULL, device->work_buffer, device->spec.format, device->spec.channels, NULL, NULL, 1.0f); + SDL_memcpy(device_buffer, device->work_buffer, buffer_size); + } + } + + // PlayDevice SHOULD NOT BLOCK, as we are holding a lock right now. Block in WaitDevice instead! + if (!device->PlayDevice(device, device_buffer, buffer_size)) { + failed = true; + } + } + + SDL_UnlockMutex(device->lock); + + if (failed) { + SDL_AudioDeviceDisconnected(device); // doh. + } + + return true; // always go on if not shutting down, even if device failed. +} + +void SDL_PlaybackAudioThreadShutdown(SDL_AudioDevice *device) +{ + SDL_assert(!device->recording); + const int frames = device->buffer_size / SDL_AUDIO_FRAMESIZE(device->spec); + // Wait for the audio to drain if device didn't die. + if (!SDL_GetAtomicInt(&device->zombie)) { + SDL_Delay(((frames * 1000) / device->spec.freq) * 2); + } + current_audio.impl.ThreadDeinit(device); + SDL_AudioThreadFinalize(device); +} + +static int SDLCALL PlaybackAudioThread(void *devicep) // thread entry point +{ + SDL_AudioDevice *device = (SDL_AudioDevice *)devicep; + SDL_assert(device != NULL); + SDL_assert(!device->recording); + SDL_PlaybackAudioThreadSetup(device); + + do { + if (!device->WaitDevice(device)) { + SDL_AudioDeviceDisconnected(device); // doh. (but don't break out of the loop, just be a zombie for now!) + } + } while (SDL_PlaybackAudioThreadIterate(device)); + + SDL_PlaybackAudioThreadShutdown(device); + return 0; +} + + + +// Recording device thread. This is split into chunks, so backends that need to control this directly can use the pieces they need without duplicating effort. + +void SDL_RecordingAudioThreadSetup(SDL_AudioDevice *device) +{ + SDL_assert(device->recording); + current_audio.impl.ThreadInit(device); +} + +bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device) +{ + SDL_assert(device->recording); + + SDL_LockMutex(device->lock); + + if (SDL_GetAtomicInt(&device->shutdown)) { + SDL_UnlockMutex(device->lock); + return false; // we're done, shut it down. + } + + bool failed = false; + + if (!device->logical_devices) { + device->FlushRecording(device); // nothing wants data, dump anything pending. + } else { + // this SHOULD NOT BLOCK, as we are holding a lock right now. Block in WaitRecordingDevice! + int br = device->RecordDevice(device, device->work_buffer, device->buffer_size); + if (br < 0) { // uhoh, device failed for some reason! + failed = true; + } else if (br > 0) { // queue the new data to each bound stream. + for (SDL_LogicalAudioDevice *logdev = device->logical_devices; logdev; logdev = logdev->next) { + if (SDL_GetAtomicInt(&logdev->paused)) { + continue; // paused? Skip this logical device. + } + + void *output_buffer = device->work_buffer; + + // I don't know why someone would want a postmix on a recording device, but we offer it for API consistency. + if (logdev->postmix || (logdev->gain != 1.0f)) { + // move to float format. + SDL_AudioSpec outspec; + SDL_copyp(&outspec, &device->spec); + outspec.format = SDL_AUDIO_F32; + output_buffer = device->postmix_buffer; + const int frames = br / SDL_AUDIO_FRAMESIZE(device->spec); + br = frames * SDL_AUDIO_FRAMESIZE(outspec); + ConvertAudio(frames, device->work_buffer, device->spec.format, outspec.channels, NULL, device->postmix_buffer, SDL_AUDIO_F32, outspec.channels, NULL, NULL, logdev->gain); + if (logdev->postmix) { + logdev->postmix(logdev->postmix_userdata, &outspec, device->postmix_buffer, br); + } + } + + for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) { + // We should have updated this elsewhere if the format changed! + SDL_assert(stream->src_spec.format == ((logdev->postmix || (logdev->gain != 1.0f)) ? SDL_AUDIO_F32 : device->spec.format)); + SDL_assert(stream->src_spec.channels == device->spec.channels); + SDL_assert(stream->src_spec.freq == device->spec.freq); + + void *final_buf = output_buffer; + + // generally channel maps will line up, but if the audio stream's chmap has been explicitly changed, do a final swizzle to stream layout. + if (!SDL_AudioChannelMapsEqual(device->spec.channels, stream->src_chmap, device->chmap)) { + final_buf = device->mix_buffer; // this is otherwise unused on recording devices, so it makes convenient scratch space here. + ConvertAudio(br / SDL_AUDIO_FRAMESIZE(device->spec), output_buffer, device->spec.format, device->spec.channels, NULL, + final_buf, device->spec.format, device->spec.channels, stream->src_chmap, NULL, 1.0f); + } + + /* this will hold a lock on `stream` while putting. We don't explicitly lock the streams + for iterating here because the binding linked list can only change while the device lock is held. + (we _do_ lock the stream during binding/unbinding to make sure that two threads can't try to bind + the same stream to different devices at the same time, though.) */ + if (!SDL_PutAudioStreamData(stream, final_buf, br)) { + // 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. + failed = true; + break; + } + } + } + } + } + + SDL_UnlockMutex(device->lock); + + if (failed) { + SDL_AudioDeviceDisconnected(device); // doh. + } + + return true; // always go on if not shutting down, even if device failed. +} + +void SDL_RecordingAudioThreadShutdown(SDL_AudioDevice *device) +{ + SDL_assert(device->recording); + device->FlushRecording(device); + current_audio.impl.ThreadDeinit(device); + SDL_AudioThreadFinalize(device); +} + +static int SDLCALL RecordingAudioThread(void *devicep) // thread entry point +{ + SDL_AudioDevice *device = (SDL_AudioDevice *)devicep; + SDL_assert(device != NULL); + SDL_assert(device->recording); + SDL_RecordingAudioThreadSetup(device); + + do { + if (!device->WaitRecordingDevice(device)) { + SDL_AudioDeviceDisconnected(device); // doh. (but don't break out of the loop, just be a zombie for now!) + } + } while (SDL_RecordingAudioThreadIterate(device)); + + SDL_RecordingAudioThreadShutdown(device); + return 0; +} + +typedef struct CountAudioDevicesData +{ + int devs_seen; + const int num_devices; + SDL_AudioDeviceID *result; + const bool recording; +} CountAudioDevicesData; + +static bool SDLCALL CountAudioDevices(void *userdata, const SDL_HashTable *table, const void *key, const void *value) +{ + CountAudioDevicesData *data = (CountAudioDevicesData *) userdata; + const SDL_AudioDeviceID devid = (SDL_AudioDeviceID) (uintptr_t) key; + // bit #0 of devid is set for playback devices and unset for recording. + // bit #1 of devid is set for physical devices and unset for logical. + const bool devid_recording = !(devid & (1<<0)); + const bool isphysical = !!(devid & (1<<1)); + if (isphysical && (devid_recording == data->recording)) { + SDL_assert(data->devs_seen < data->num_devices); + data->result[data->devs_seen++] = devid; + } + return true; // keep iterating. +} + +static SDL_AudioDeviceID *GetAudioDevices(int *count, bool recording) +{ + SDL_AudioDeviceID *result = NULL; + int num_devices = 0; + + if (SDL_GetCurrentAudioDriver()) { + SDL_LockRWLockForReading(current_audio.device_hash_lock); + { + num_devices = SDL_GetAtomicInt(recording ? ¤t_audio.recording_device_count : ¤t_audio.playback_device_count); + result = (SDL_AudioDeviceID *) SDL_malloc((num_devices + 1) * sizeof (SDL_AudioDeviceID)); + if (result) { + CountAudioDevicesData data = { 0, num_devices, result, recording }; + SDL_IterateHashTable(current_audio.device_hash, CountAudioDevices, &data); + SDL_assert(data.devs_seen == num_devices); + result[data.devs_seen] = 0; // null-terminated. + } + } + SDL_UnlockRWLock(current_audio.device_hash_lock); + } else { + SDL_SetError("Audio subsystem is not initialized"); + } + + if (count) { + if (result) { + *count = num_devices; + } else { + *count = 0; + } + } + return result; +} + +SDL_AudioDeviceID *SDL_GetAudioPlaybackDevices(int *count) +{ + return GetAudioDevices(count, false); +} + +SDL_AudioDeviceID *SDL_GetAudioRecordingDevices(int *count) +{ + return GetAudioDevices(count, true); +} + +typedef struct FindAudioDeviceByCallbackData +{ + bool (*callback)(SDL_AudioDevice *device, void *userdata); + void *userdata; + SDL_AudioDevice *retval; +} FindAudioDeviceByCallbackData; + +static bool SDLCALL FindAudioDeviceByCallback(void *userdata, const SDL_HashTable *table, const void *key, const void *value) +{ + FindAudioDeviceByCallbackData *data = (FindAudioDeviceByCallbackData *) userdata; + const SDL_AudioDeviceID devid = (SDL_AudioDeviceID) (uintptr_t) key; + // bit #1 of devid is set for physical devices and unset for logical. + const bool isphysical = !!(devid & (1<<1)); + if (isphysical) { + SDL_AudioDevice *device = (SDL_AudioDevice *) value; + if (data->callback(device, data->userdata)) { // found it? + data->retval = device; + return false; // stop iterating, we found it. + } + } + return true; // keep iterating. +} + +// !!! FIXME: SDL convention is for userdata to come first in the callback's params. Fix this at some point. +SDL_AudioDevice *SDL_FindPhysicalAudioDeviceByCallback(bool (*callback)(SDL_AudioDevice *device, void *userdata), void *userdata) +{ + if (!SDL_GetCurrentAudioDriver()) { + SDL_SetError("Audio subsystem is not initialized"); + return NULL; + } + + FindAudioDeviceByCallbackData data = { callback, userdata, NULL }; + SDL_LockRWLockForReading(current_audio.device_hash_lock); + SDL_IterateHashTable(current_audio.device_hash, FindAudioDeviceByCallback, &data); + SDL_UnlockRWLock(current_audio.device_hash_lock); + + if (!data.retval) { + SDL_SetError("Device not found"); + } + + return data.retval; +} + +static bool TestDeviceHandleCallback(SDL_AudioDevice *device, void *handle) +{ + return device->handle == handle; +} + +SDL_AudioDevice *SDL_FindPhysicalAudioDeviceByHandle(void *handle) +{ + return SDL_FindPhysicalAudioDeviceByCallback(TestDeviceHandleCallback, handle); +} + +const char *SDL_GetAudioDeviceName(SDL_AudioDeviceID devid) +{ + const char *result = NULL; + SDL_AudioDevice *device = ObtainPhysicalAudioDevice(devid); + if (device) { + result = SDL_GetPersistentString(device->name); + } + ReleaseAudioDevice(device); + + return result; +} + +bool SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SDL_AudioSpec *spec, int *sample_frames) +{ + if (!spec) { + return SDL_InvalidParamError("spec"); + } + + bool result = false; + SDL_AudioDevice *device = ObtainPhysicalAudioDeviceDefaultAllowed(devid); + if (device) { + SDL_copyp(spec, &device->spec); + if (sample_frames) { + *sample_frames = device->sample_frames; + } + result = true; + } + ReleaseAudioDevice(device); + + return result; +} + +int *SDL_GetAudioDeviceChannelMap(SDL_AudioDeviceID devid, int *count) +{ + int *result = NULL; + int channels = 0; + SDL_AudioDevice *device = ObtainPhysicalAudioDeviceDefaultAllowed(devid); + if (device) { + channels = device->spec.channels; + result = SDL_ChannelMapDup(device->chmap, channels); + } + ReleaseAudioDevice(device); + + if (count) { + *count = channels; + } + + return result; +} + + +// this is awkward, but this makes sure we can release the device lock +// so the device thread can terminate but also not have two things +// race to close or open the device while the lock is unprotected. +// you hold the lock when calling this, it will release the lock and +// wait while the shutdown flag is set. +// BE CAREFUL WITH THIS. +static void SerializePhysicalDeviceClose(SDL_AudioDevice *device) +{ + while (SDL_GetAtomicInt(&device->shutdown)) { + SDL_WaitCondition(device->close_cond, device->lock); + } +} + +// this expects the device lock to be held. +static void ClosePhysicalAudioDevice(SDL_AudioDevice *device) +{ + SerializePhysicalDeviceClose(device); + + SDL_SetAtomicInt(&device->shutdown, 1); + + // YOU MUST PROTECT KEY POINTS WITH SerializePhysicalDeviceClose() WHILE THE THREAD JOINS + SDL_UnlockMutex(device->lock); + + if (device->thread) { + SDL_WaitThread(device->thread, NULL); + device->thread = NULL; + } + + if (device->currently_opened) { + current_audio.impl.CloseDevice(device); // if ProvidesOwnCallbackThread, this must join on any existing device thread before returning! + device->currently_opened = false; + device->hidden = NULL; // just in case. + } + + SDL_LockMutex(device->lock); + SDL_SetAtomicInt(&device->shutdown, 0); // ready to go again. + SDL_BroadcastCondition(device->close_cond); // release anyone waiting in SerializePhysicalDeviceClose; they'll still block until we release device->lock, though. + + SDL_aligned_free(device->work_buffer); + device->work_buffer = NULL; + + SDL_aligned_free(device->mix_buffer); + device->mix_buffer = NULL; + + SDL_aligned_free(device->postmix_buffer); + device->postmix_buffer = NULL; + + SDL_copyp(&device->spec, &device->default_spec); + device->sample_frames = 0; + device->silence_value = SDL_GetSilenceValueForFormat(device->spec.format); +} + +void SDL_CloseAudioDevice(SDL_AudioDeviceID devid) +{ + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); + if (logdev) { + DestroyLogicalAudioDevice(logdev); + } + + if (device) { + if (!device->logical_devices) { // no more logical devices? Close the physical device, too. + ClosePhysicalAudioDevice(device); + } + UnrefPhysicalAudioDevice(device); // one reference for each logical device. + } + + ReleaseAudioDevice(device); +} + + +static SDL_AudioFormat ParseAudioFormatString(const char *string) +{ + if (string) { + #define CHECK_FMT_STRING(x) if (SDL_strcmp(string, #x) == 0) { return SDL_AUDIO_##x; } + CHECK_FMT_STRING(U8); + CHECK_FMT_STRING(S8); + CHECK_FMT_STRING(S16LE); + CHECK_FMT_STRING(S16BE); + CHECK_FMT_STRING(S16); + CHECK_FMT_STRING(S32LE); + CHECK_FMT_STRING(S32BE); + CHECK_FMT_STRING(S32); + CHECK_FMT_STRING(F32LE); + CHECK_FMT_STRING(F32BE); + CHECK_FMT_STRING(F32); + #undef CHECK_FMT_STRING + } + return SDL_AUDIO_UNKNOWN; +} + +static void PrepareAudioFormat(bool recording, SDL_AudioSpec *spec) +{ + if (spec->freq == 0) { + spec->freq = recording ? DEFAULT_AUDIO_RECORDING_FREQUENCY : DEFAULT_AUDIO_PLAYBACK_FREQUENCY; + + const char *hint = SDL_GetHint(SDL_HINT_AUDIO_FREQUENCY); + if (hint) { + const int val = SDL_atoi(hint); + if (val > 0) { + spec->freq = val; + } + } + } + + if (spec->channels == 0) { + spec->channels = recording ? DEFAULT_AUDIO_RECORDING_CHANNELS : DEFAULT_AUDIO_PLAYBACK_CHANNELS; + + const char *hint = SDL_GetHint(SDL_HINT_AUDIO_CHANNELS); + if (hint) { + const int val = SDL_atoi(hint); + if (val > 0) { + spec->channels = val; + } + } + } + + if (spec->format == 0) { + const SDL_AudioFormat val = ParseAudioFormatString(SDL_GetHint(SDL_HINT_AUDIO_FORMAT)); + spec->format = (val != SDL_AUDIO_UNKNOWN) ? val : (recording ? DEFAULT_AUDIO_RECORDING_FORMAT : DEFAULT_AUDIO_PLAYBACK_FORMAT); + } +} + +void SDL_UpdatedAudioDeviceFormat(SDL_AudioDevice *device) +{ + device->silence_value = SDL_GetSilenceValueForFormat(device->spec.format); + device->buffer_size = device->sample_frames * SDL_AUDIO_FRAMESIZE(device->spec); + device->work_buffer_size = device->sample_frames * sizeof (float) * device->spec.channels; + device->work_buffer_size = SDL_max(device->buffer_size, device->work_buffer_size); // just in case we end up with a 64-bit audio format at some point. +} + +char *SDL_GetAudioThreadName(SDL_AudioDevice *device, char *buf, size_t buflen) +{ + (void)SDL_snprintf(buf, buflen, "SDLAudio%c%d", (device->recording) ? 'C' : 'P', (int) device->instance_id); + return buf; +} + + +// this expects the device lock to be held. +static bool OpenPhysicalAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec *inspec) +{ + SerializePhysicalDeviceClose(device); // make sure another thread that's closing didn't release the lock to let the device thread join... + + if (device->currently_opened) { + return true; // we're already good. + } + + // Just pretend to open a zombie device. It can still collect logical devices on a default device under the assumption they will all migrate when the default device is officially changed. + if (SDL_GetAtomicInt(&device->zombie)) { + return true; // Braaaaaaaaains. + } + + // These start with the backend's implementation, but we might swap them out with zombie versions later. + device->WaitDevice = current_audio.impl.WaitDevice; + device->PlayDevice = current_audio.impl.PlayDevice; + device->GetDeviceBuf = current_audio.impl.GetDeviceBuf; + device->WaitRecordingDevice = current_audio.impl.WaitRecordingDevice; + device->RecordDevice = current_audio.impl.RecordDevice; + device->FlushRecording = current_audio.impl.FlushRecording; + + SDL_AudioSpec spec; + SDL_copyp(&spec, inspec ? inspec : &device->default_spec); + PrepareAudioFormat(device->recording, &spec); + + /* We allow the device format to change if it's better than the current settings (by various definitions of "better"). This prevents + something low quality, like an old game using S8/8000Hz audio, from ruining a music thing playing at CD quality that tries to open later. + (or some VoIP library that opens for mono output ruining your surround-sound game because it got there first). + These are just requests! The backend may change any of these values during OpenDevice method! */ + device->spec.format = (SDL_AUDIO_BITSIZE(device->default_spec.format) >= SDL_AUDIO_BITSIZE(spec.format)) ? device->default_spec.format : spec.format; + device->spec.freq = SDL_max(device->default_spec.freq, spec.freq); + device->spec.channels = SDL_max(device->default_spec.channels, spec.channels); + device->sample_frames = SDL_GetDefaultSampleFramesFromFreq(device->spec.freq); + SDL_UpdatedAudioDeviceFormat(device); // start this off sane. + + device->currently_opened = true; // mark this true even if impl.OpenDevice fails, so we know to clean up. + if (!current_audio.impl.OpenDevice(device)) { + ClosePhysicalAudioDevice(device); // clean up anything the backend left half-initialized. + return false; + } + + SDL_UpdatedAudioDeviceFormat(device); // in case the backend changed things and forgot to call this. + + // Allocate a scratch audio buffer + device->work_buffer = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), device->work_buffer_size); + if (!device->work_buffer) { + ClosePhysicalAudioDevice(device); + return false; + } + + if (device->spec.format != SDL_AUDIO_F32) { + device->mix_buffer = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), device->work_buffer_size); + if (!device->mix_buffer) { + ClosePhysicalAudioDevice(device); + return false; + } + } + + // Start the audio thread if necessary + if (!current_audio.impl.ProvidesOwnCallbackThread) { + char threadname[64]; + SDL_GetAudioThreadName(device, threadname, sizeof (threadname)); + device->thread = SDL_CreateThread(device->recording ? RecordingAudioThread : PlaybackAudioThread, threadname, device); + + if (!device->thread) { + ClosePhysicalAudioDevice(device); + return SDL_SetError("Couldn't create audio thread"); + } + } + + return true; +} + +SDL_AudioDeviceID SDL_OpenAudioDevice(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec) +{ + if (!SDL_GetCurrentAudioDriver()) { + SDL_SetError("Audio subsystem is not initialized"); + return 0; + } + + bool wants_default = ((devid == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK) || (devid == SDL_AUDIO_DEVICE_DEFAULT_RECORDING)); + + // this will let you use a logical device to make a new logical device on the parent physical device. Could be useful? + SDL_AudioDevice *device = NULL; + const bool islogical = (!wants_default && !(devid & (1<<1))); + if (!islogical) { + device = ObtainPhysicalAudioDeviceDefaultAllowed(devid); + } else { + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); + if (logdev) { + wants_default = logdev->opened_as_default; // was the original logical device meant to be a default? Make this one, too. + } + } + + SDL_AudioDeviceID result = 0; + + if (device) { + SDL_LogicalAudioDevice *logdev = NULL; + if (!wants_default && SDL_GetAtomicInt(&device->zombie)) { + // uhoh, this device is undead, and just waiting to be cleaned up. Refuse explicit opens. + SDL_SetError("Device was already lost and can't accept new opens"); + } else if ((logdev = (SDL_LogicalAudioDevice *) SDL_calloc(1, sizeof (SDL_LogicalAudioDevice))) == NULL) { + // SDL_calloc already called SDL_OutOfMemory + } else if (!OpenPhysicalAudioDevice(device, spec)) { // if this is the first thing using this physical device, open at the OS level if necessary... + SDL_free(logdev); + } else { + RefPhysicalAudioDevice(device); // unref'd on successful SDL_CloseAudioDevice + SDL_SetAtomicInt(&logdev->paused, 0); + result = logdev->instance_id = AssignAudioDeviceInstanceId(device->recording, /*islogical=*/true); + logdev->physical_device = device; + logdev->gain = 1.0f; + logdev->opened_as_default = wants_default; + logdev->next = device->logical_devices; + if (device->logical_devices) { + device->logical_devices->prev = logdev; + } + device->logical_devices = logdev; + UpdateAudioStreamFormatsPhysical(device); + } + ReleaseAudioDevice(device); + + if (result) { + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + const bool inserted = SDL_InsertIntoHashTable(current_audio.device_hash, (const void *) (uintptr_t) result, logdev, false); + SDL_UnlockRWLock(current_audio.device_hash_lock); + if (!inserted) { + SDL_CloseAudioDevice(result); + result = 0; + } + } + } + + return result; +} + +static bool SetLogicalAudioDevicePauseState(SDL_AudioDeviceID devid, int value) +{ + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); + if (logdev) { + SDL_SetAtomicInt(&logdev->paused, value); + } + ReleaseAudioDevice(device); + return logdev ? true : false; // ObtainLogicalAudioDevice will have set an error. +} + +bool SDL_PauseAudioDevice(SDL_AudioDeviceID devid) +{ + return SetLogicalAudioDevicePauseState(devid, 1); +} + +bool SDLCALL SDL_ResumeAudioDevice(SDL_AudioDeviceID devid) +{ + return SetLogicalAudioDevicePauseState(devid, 0); +} + +bool SDL_AudioDevicePaused(SDL_AudioDeviceID devid) +{ + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); + bool result = false; + if (logdev && SDL_GetAtomicInt(&logdev->paused)) { + result = true; + } + ReleaseAudioDevice(device); + return result; +} + +float SDL_GetAudioDeviceGain(SDL_AudioDeviceID devid) +{ + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); + const float result = logdev ? logdev->gain : -1.0f; + ReleaseAudioDevice(device); + return result; +} + +bool SDL_SetAudioDeviceGain(SDL_AudioDeviceID devid, float gain) +{ + if (gain < 0.0f) { + return SDL_InvalidParamError("gain"); + } + + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); + bool result = false; + if (logdev) { + logdev->gain = gain; + UpdateAudioStreamFormatsPhysical(device); + result = true; + } + ReleaseAudioDevice(device); + return result; +} + +bool SDL_SetAudioPostmixCallback(SDL_AudioDeviceID devid, SDL_AudioPostmixCallback callback, void *userdata) +{ + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(devid, &device); + bool result = true; + if (logdev) { + if (callback && !device->postmix_buffer) { + device->postmix_buffer = (float *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), device->work_buffer_size); + if (!device->postmix_buffer) { + result = false; + } + } + + if (result) { + logdev->postmix = callback; + logdev->postmix_userdata = userdata; + } + + UpdateAudioStreamFormatsPhysical(device); + } + ReleaseAudioDevice(device); + return result; +} + +bool SDL_BindAudioStreams(SDL_AudioDeviceID devid, SDL_AudioStream * const *streams, int num_streams) +{ + const bool islogical = !(devid & (1<<1)); + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = NULL; + bool result = true; + + if (num_streams == 0) { + return true; // nothing to do + } else if (num_streams < 0) { + return SDL_InvalidParamError("num_streams"); + } else if (!streams) { + return SDL_InvalidParamError("streams"); + } else if (!islogical) { + return SDL_SetError("Audio streams are bound to device ids from SDL_OpenAudioDevice, not raw physical devices"); + } + + logdev = ObtainLogicalAudioDevice(devid, &device); + if (!logdev) { + result = false; // ObtainLogicalAudioDevice set the error string. + } else if (logdev->simplified) { + result = SDL_SetError("Cannot change stream bindings on device opened with SDL_OpenAudioDeviceStream"); + } else { + + // !!! FIXME: We'll set the device's side's format below, but maybe we should refuse to bind a stream if the app's side doesn't have a format set yet. + // !!! FIXME: Actually, why do we allow there to be an invalid format, again? + + // make sure start of list is sane. + SDL_assert(!logdev->bound_streams || (logdev->bound_streams->prev_binding == NULL)); + + // lock all the streams upfront, so we can verify they aren't bound elsewhere and add them all in one block, as this is intended to add everything or nothing. + for (int i = 0; i < num_streams; i++) { + SDL_AudioStream *stream = streams[i]; + if (!stream) { + SDL_SetError("Stream #%d is NULL", i); + result = false; // to pacify the static analyzer, that doesn't realize SDL_SetError() always returns false. + } else { + SDL_LockMutex(stream->lock); + SDL_assert((stream->bound_device == NULL) == ((stream->prev_binding == NULL) || (stream->next_binding == NULL))); + if (stream->bound_device) { + result = SDL_SetError("Stream #%d is already bound to a device", i); + } else if (stream->simplified) { // You can get here if you closed the device instead of destroying the stream. + result = SDL_SetError("Cannot change binding on a stream created with SDL_OpenAudioDeviceStream"); + } + } + + if (!result) { + int j; + for (j = 0; j < i; j++) { + SDL_UnlockMutex(streams[j]->lock); + } + if (stream) { + SDL_UnlockMutex(stream->lock); + } + break; + } + } + } + + if (result) { + // Now that everything is verified, chain everything together. + for (int i = 0; i < num_streams; i++) { + SDL_AudioStream *stream = streams[i]; + if (stream) { // shouldn't be NULL, but just in case... + stream->bound_device = logdev; + stream->prev_binding = NULL; + stream->next_binding = logdev->bound_streams; + if (logdev->bound_streams) { + logdev->bound_streams->prev_binding = stream; + } + logdev->bound_streams = stream; + SDL_UnlockMutex(stream->lock); + } + } + } + + UpdateAudioStreamFormatsPhysical(device); + + ReleaseAudioDevice(device); + + return result; +} + +bool SDL_BindAudioStream(SDL_AudioDeviceID devid, SDL_AudioStream *stream) +{ + return SDL_BindAudioStreams(devid, &stream, 1); +} + +// !!! FIXME: this and BindAudioStreams are mutex nightmares. :/ +void SDL_UnbindAudioStreams(SDL_AudioStream * const *streams, int num_streams) +{ + if (num_streams <= 0 || !streams) { + return; // nothing to do + } + + /* to prevent deadlock when holding both locks, we _must_ lock the device first, and the stream second, as that is the order the audio thread will do it. + But this means we have an unlikely, pathological case where a stream could change its binding between when we lookup its bound device and when we lock everything, + so we double-check here. */ + for (int i = 0; i < num_streams; i++) { + SDL_AudioStream *stream = streams[i]; + if (!stream) { + continue; // nothing to do, it's a NULL stream. + } + + while (true) { + SDL_LockMutex(stream->lock); // lock to check this and then release it, in case the device isn't locked yet. + SDL_LogicalAudioDevice *bounddev = stream->bound_device; + SDL_UnlockMutex(stream->lock); + + // lock in correct order. + if (bounddev) { + SDL_LockMutex(bounddev->physical_device->lock); // this requires recursive mutexes, since we're likely locking the same device multiple times. + } + SDL_LockMutex(stream->lock); + + if (bounddev == stream->bound_device) { + break; // the binding didn't change in the small window where it could, so we're good. + } else { + SDL_UnlockMutex(stream->lock); // it changed bindings! Try again. + if (bounddev) { + SDL_UnlockMutex(bounddev->physical_device->lock); + } + } + } + } + + // everything is locked, start unbinding streams. + for (int i = 0; i < num_streams; i++) { + SDL_AudioStream *stream = streams[i]; + // don't allow unbinding from "simplified" devices (opened with SDL_OpenAudioDeviceStream). Just ignore them. + if (stream && stream->bound_device && !stream->bound_device->simplified) { + if (stream->bound_device->bound_streams == stream) { + SDL_assert(!stream->prev_binding); + stream->bound_device->bound_streams = stream->next_binding; + } + if (stream->prev_binding) { + stream->prev_binding->next_binding = stream->next_binding; + } + if (stream->next_binding) { + stream->next_binding->prev_binding = stream->prev_binding; + } + stream->prev_binding = stream->next_binding = NULL; + } + } + + // Finalize and unlock everything. + for (int i = 0; i < num_streams; i++) { + SDL_AudioStream *stream = streams[i]; + if (stream) { + SDL_LogicalAudioDevice *logdev = stream->bound_device; + stream->bound_device = NULL; + SDL_UnlockMutex(stream->lock); + if (logdev) { + UpdateAudioStreamFormatsPhysical(logdev->physical_device); + SDL_UnlockMutex(logdev->physical_device->lock); + } + } + } +} + +void SDL_UnbindAudioStream(SDL_AudioStream *stream) +{ + SDL_UnbindAudioStreams(&stream, 1); +} + +SDL_AudioDeviceID SDL_GetAudioStreamDevice(SDL_AudioStream *stream) +{ + SDL_AudioDeviceID result = 0; + + if (!stream) { + SDL_InvalidParamError("stream"); + return 0; + } + + SDL_LockMutex(stream->lock); + if (stream->bound_device) { + result = stream->bound_device->instance_id; + } else { + SDL_SetError("Audio stream not bound to an audio device"); + } + SDL_UnlockMutex(stream->lock); + + return result; +} + +SDL_AudioStream *SDL_OpenAudioDeviceStream(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec, SDL_AudioStreamCallback callback, void *userdata) +{ + SDL_AudioDeviceID logdevid = SDL_OpenAudioDevice(devid, spec); + if (!logdevid) { + return NULL; // error string should already be set. + } + + bool failed = false; + SDL_AudioStream *stream = NULL; + SDL_AudioDevice *device = NULL; + SDL_LogicalAudioDevice *logdev = ObtainLogicalAudioDevice(logdevid, &device); + if (!logdev) { // this shouldn't happen, but just in case. + failed = true; + } else { + SDL_SetAtomicInt(&logdev->paused, 1); // start the device paused, to match SDL2. + + SDL_assert(device != NULL); + const bool recording = device->recording; + + // if the app didn't request a format _at all_, just make a stream that does no conversion; they can query for it later. + SDL_AudioSpec tmpspec; + if (!spec) { + SDL_copyp(&tmpspec, &device->spec); + spec = &tmpspec; + } + + if (recording) { + stream = SDL_CreateAudioStream(&device->spec, spec); + } else { + stream = SDL_CreateAudioStream(spec, &device->spec); + } + + if (!stream) { + failed = true; + } else { + // don't do all the complicated validation and locking of SDL_BindAudioStream just to set a few fields here. + logdev->bound_streams = stream; + logdev->simplified = true; // forbid further binding changes on this logical device. + + stream->bound_device = logdev; + stream->simplified = true; // so we know to close the audio device when this is destroyed. + + UpdateAudioStreamFormatsPhysical(device); + + if (callback) { + bool rc; + if (recording) { + rc = SDL_SetAudioStreamPutCallback(stream, callback, userdata); + } else { + rc = SDL_SetAudioStreamGetCallback(stream, callback, userdata); + } + SDL_assert(rc); // should only fail if stream==NULL atm. + } + } + } + + ReleaseAudioDevice(device); + + if (failed) { + SDL_DestroyAudioStream(stream); + SDL_CloseAudioDevice(logdevid); + stream = NULL; + } + + return stream; +} + +bool SDL_PauseAudioStreamDevice(SDL_AudioStream *stream) +{ + SDL_AudioDeviceID devid = SDL_GetAudioStreamDevice(stream); + if (!devid) { + return false; + } + + return SDL_PauseAudioDevice(devid); +} + +bool SDL_ResumeAudioStreamDevice(SDL_AudioStream *stream) +{ + SDL_AudioDeviceID devid = SDL_GetAudioStreamDevice(stream); + if (!devid) { + return false; + } + + return SDL_ResumeAudioDevice(devid); +} + +bool SDL_AudioStreamDevicePaused(SDL_AudioStream *stream) +{ + SDL_AudioDeviceID devid = SDL_GetAudioStreamDevice(stream); + if (!devid) { + return false; + } + + return SDL_AudioDevicePaused(devid); +} + +#if SDL_BYTEORDER == SDL_LIL_ENDIAN +#define NATIVE(type) SDL_AUDIO_##type##LE +#define SWAPPED(type) SDL_AUDIO_##type##BE +#else +#define NATIVE(type) SDL_AUDIO_##type##BE +#define SWAPPED(type) SDL_AUDIO_##type##LE +#endif + +#define NUM_FORMATS 8 +// always favor Float32 in native byte order, since we're probably going to convert to that for processing anyhow. +static const SDL_AudioFormat format_list[NUM_FORMATS][NUM_FORMATS + 1] = { + { SDL_AUDIO_U8, NATIVE(F32), SWAPPED(F32), SDL_AUDIO_S8, NATIVE(S16), SWAPPED(S16), NATIVE(S32), SWAPPED(S32), SDL_AUDIO_UNKNOWN }, + { SDL_AUDIO_S8, NATIVE(F32), SWAPPED(F32), SDL_AUDIO_U8, NATIVE(S16), SWAPPED(S16), NATIVE(S32), SWAPPED(S32), SDL_AUDIO_UNKNOWN }, + { NATIVE(S16), NATIVE(F32), SWAPPED(F32), SWAPPED(S16), NATIVE(S32), SWAPPED(S32), SDL_AUDIO_U8, SDL_AUDIO_S8, SDL_AUDIO_UNKNOWN }, + { SWAPPED(S16), NATIVE(F32), SWAPPED(F32), NATIVE(S16), SWAPPED(S32), NATIVE(S32), SDL_AUDIO_U8, SDL_AUDIO_S8, SDL_AUDIO_UNKNOWN }, + { NATIVE(S32), NATIVE(F32), SWAPPED(F32), SWAPPED(S32), NATIVE(S16), SWAPPED(S16), SDL_AUDIO_U8, SDL_AUDIO_S8, SDL_AUDIO_UNKNOWN }, + { SWAPPED(S32), NATIVE(F32), SWAPPED(F32), NATIVE(S32), SWAPPED(S16), NATIVE(S16), SDL_AUDIO_U8, SDL_AUDIO_S8, SDL_AUDIO_UNKNOWN }, + { NATIVE(F32), SWAPPED(F32), NATIVE(S32), SWAPPED(S32), NATIVE(S16), SWAPPED(S16), SDL_AUDIO_U8, SDL_AUDIO_S8, SDL_AUDIO_UNKNOWN }, + { SWAPPED(F32), NATIVE(F32), SWAPPED(S32), NATIVE(S32), SWAPPED(S16), NATIVE(S16), SDL_AUDIO_U8, SDL_AUDIO_S8, SDL_AUDIO_UNKNOWN }, +}; + +#undef NATIVE +#undef SWAPPED + +const SDL_AudioFormat *SDL_ClosestAudioFormats(SDL_AudioFormat format) +{ + for (int i = 0; i < NUM_FORMATS; i++) { + if (format_list[i][0] == format) { + return &format_list[i][0]; + } + } + return &format_list[0][NUM_FORMATS]; // not found; return what looks like a list with only a zero in it. +} + +const char *SDL_GetAudioFormatName(SDL_AudioFormat format) +{ + switch (format) { +#define CASE(X) \ + case X: return #X; + CASE(SDL_AUDIO_U8) + CASE(SDL_AUDIO_S8) + CASE(SDL_AUDIO_S16LE) + CASE(SDL_AUDIO_S16BE) + CASE(SDL_AUDIO_S32LE) + CASE(SDL_AUDIO_S32BE) + CASE(SDL_AUDIO_F32LE) + CASE(SDL_AUDIO_F32BE) +#undef CASE + default: + return "SDL_AUDIO_UNKNOWN"; + } +} + +int SDL_GetSilenceValueForFormat(SDL_AudioFormat format) +{ + return (format == SDL_AUDIO_U8) ? 0x80 : 0x00; +} + +// called internally by backends when the system default device changes. +void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device) +{ + if (!new_default_device) { // !!! FIXME: what should we do in this case? Maybe all devices are lost, so there _isn't_ a default? + return; // uhoh. + } + + const bool recording = new_default_device->recording; + + // change the official default over right away, so new opens will go to the new device. + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + const SDL_AudioDeviceID current_devid = recording ? current_audio.default_recording_device_id : current_audio.default_playback_device_id; + const bool is_already_default = (new_default_device->instance_id == current_devid); + if (!is_already_default) { + if (recording) { + current_audio.default_recording_device_id = new_default_device->instance_id; + } else { + current_audio.default_playback_device_id = new_default_device->instance_id; + } + } + SDL_UnlockRWLock(current_audio.device_hash_lock); + + if (is_already_default) { + return; // this is already the default. + } + + // Queue up events to push to the queue next time it pumps (presumably + // in a safer thread). + // !!! FIXME: this duplicates some code we could probably refactor. + SDL_PendingAudioDeviceEvent pending; + pending.next = NULL; + SDL_PendingAudioDeviceEvent *pending_tail = &pending; + + // Default device gets an extra ref, so it lives until a new default replaces it, even if disconnected. + RefPhysicalAudioDevice(new_default_device); + + ObtainPhysicalAudioDeviceObj(new_default_device); + + SDL_AudioDevice *current_default_device = ObtainPhysicalAudioDevice(current_devid); + + if (current_default_device) { + // migrate any logical devices that were opened as a default to the new physical device... + + SDL_assert(current_default_device->recording == recording); + + // See if we have to open the new physical device, and if so, find the best audiospec for it. + SDL_AudioSpec spec; + bool needs_migration = false; + SDL_zero(spec); + + for (SDL_LogicalAudioDevice *logdev = current_default_device->logical_devices; logdev; logdev = logdev->next) { + if (logdev->opened_as_default) { + needs_migration = true; + for (SDL_AudioStream *stream = logdev->bound_streams; stream; stream = stream->next_binding) { + const SDL_AudioSpec *streamspec = recording ? &stream->dst_spec : &stream->src_spec; + if (SDL_AUDIO_BITSIZE(streamspec->format) > SDL_AUDIO_BITSIZE(spec.format)) { + spec.format = streamspec->format; + } + if (streamspec->channels > spec.channels) { + spec.channels = streamspec->channels; + } + if (streamspec->freq > spec.freq) { + spec.freq = streamspec->freq; + } + } + } + } + + if (needs_migration) { + // New default physical device not been opened yet? Open at the OS level... + if (!OpenPhysicalAudioDevice(new_default_device, &spec)) { + needs_migration = false; // uhoh, just leave everything on the old default, nothing to be done. + } + } + + if (needs_migration) { + // we don't currently report channel map changes, so we'll leave them as NULL for now. + const bool spec_changed = !SDL_AudioSpecsEqual(¤t_default_device->spec, &new_default_device->spec, NULL, NULL); + SDL_LogicalAudioDevice *next = NULL; + for (SDL_LogicalAudioDevice *logdev = current_default_device->logical_devices; logdev; logdev = next) { + next = logdev->next; + + if (!logdev->opened_as_default) { + continue; // not opened as a default, leave it on the current physical device. + } + + // now migrate the logical device. Hold device_hash_lock so ObtainLogicalAudioDevice doesn't get a device in the middle of transition. + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + if (logdev->next) { + logdev->next->prev = logdev->prev; + } + if (logdev->prev) { + logdev->prev->next = logdev->next; + } + if (current_default_device->logical_devices == logdev) { + current_default_device->logical_devices = logdev->next; + } + + logdev->physical_device = new_default_device; + logdev->prev = NULL; + logdev->next = new_default_device->logical_devices; + new_default_device->logical_devices = logdev; + SDL_UnlockRWLock(current_audio.device_hash_lock); + + SDL_assert(SDL_GetAtomicInt(¤t_default_device->refcount) > 1); // we should hold at least one extra reference to this device, beyond logical devices, during this phase... + RefPhysicalAudioDevice(new_default_device); + UnrefPhysicalAudioDevice(current_default_device); + + SDL_SetAudioPostmixCallback(logdev->instance_id, logdev->postmix, logdev->postmix_userdata); + + SDL_PendingAudioDeviceEvent *p; + + // Queue an event for each logical device we moved. + if (spec_changed) { + p = (SDL_PendingAudioDeviceEvent *)SDL_malloc(sizeof(SDL_PendingAudioDeviceEvent)); + if (p) { // if this failed, no event for you, but you have deeper problems anyhow. + p->type = SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED; + p->devid = logdev->instance_id; + p->next = NULL; + pending_tail->next = p; + pending_tail = p; + } + } + } + + UpdateAudioStreamFormatsPhysical(current_default_device); + UpdateAudioStreamFormatsPhysical(new_default_device); + + if (!current_default_device->logical_devices) { // nothing left on the current physical device, close it. + ClosePhysicalAudioDevice(current_default_device); + } + } + + ReleaseAudioDevice(current_default_device); + } + + ReleaseAudioDevice(new_default_device); + + // Default device gets an extra ref, so it lives until a new default replaces it, even if disconnected. + if (current_default_device) { // (despite the name, it's no longer current at this point) + UnrefPhysicalAudioDevice(current_default_device); + } + + if (pending.next) { + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + SDL_assert(current_audio.pending_events_tail != NULL); + SDL_assert(current_audio.pending_events_tail->next == NULL); + current_audio.pending_events_tail->next = pending.next; + current_audio.pending_events_tail = pending_tail; + SDL_UnlockRWLock(current_audio.device_hash_lock); + } +} + +bool SDL_AudioDeviceFormatChangedAlreadyLocked(SDL_AudioDevice *device, const SDL_AudioSpec *newspec, int new_sample_frames) +{ + const int orig_work_buffer_size = device->work_buffer_size; + + // we don't currently have any place where channel maps change from under you, but we can check that if necessary later. + if (SDL_AudioSpecsEqual(&device->spec, newspec, NULL, NULL) && (new_sample_frames == device->sample_frames)) { + return true; // we're already in that format. + } + + SDL_copyp(&device->spec, newspec); + UpdateAudioStreamFormatsPhysical(device); + + bool kill_device = false; + + device->sample_frames = new_sample_frames; + SDL_UpdatedAudioDeviceFormat(device); + if (device->work_buffer && (device->work_buffer_size > orig_work_buffer_size)) { + SDL_aligned_free(device->work_buffer); + device->work_buffer = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), device->work_buffer_size); + if (!device->work_buffer) { + kill_device = true; + } + + if (device->postmix_buffer) { + SDL_aligned_free(device->postmix_buffer); + device->postmix_buffer = (float *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), device->work_buffer_size); + if (!device->postmix_buffer) { + kill_device = true; + } + } + + SDL_aligned_free(device->mix_buffer); + device->mix_buffer = NULL; + if (device->spec.format != SDL_AUDIO_F32) { + device->mix_buffer = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), device->work_buffer_size); + if (!device->mix_buffer) { + kill_device = true; + } + } + } + + // Post an event for the physical device, and each logical device on this physical device. + if (!kill_device) { + // Queue up events to push to the queue next time it pumps (presumably + // in a safer thread). + // !!! FIXME: this duplicates some code we could probably refactor. + SDL_PendingAudioDeviceEvent pending; + pending.next = NULL; + SDL_PendingAudioDeviceEvent *pending_tail = &pending; + + SDL_PendingAudioDeviceEvent *p; + + p = (SDL_PendingAudioDeviceEvent *)SDL_malloc(sizeof(SDL_PendingAudioDeviceEvent)); + if (p) { // if this failed, no event for you, but you have deeper problems anyhow. + p->type = SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED; + p->devid = device->instance_id; + p->next = NULL; + pending_tail->next = p; + pending_tail = p; + } + + for (SDL_LogicalAudioDevice *logdev = device->logical_devices; logdev; logdev = logdev->next) { + p = (SDL_PendingAudioDeviceEvent *)SDL_malloc(sizeof(SDL_PendingAudioDeviceEvent)); + if (p) { // if this failed, no event for you, but you have deeper problems anyhow. + p->type = SDL_EVENT_AUDIO_DEVICE_FORMAT_CHANGED; + p->devid = logdev->instance_id; + p->next = NULL; + pending_tail->next = p; + pending_tail = p; + } + } + + if (pending.next) { + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + SDL_assert(current_audio.pending_events_tail != NULL); + SDL_assert(current_audio.pending_events_tail->next == NULL); + current_audio.pending_events_tail->next = pending.next; + current_audio.pending_events_tail = pending_tail; + SDL_UnlockRWLock(current_audio.device_hash_lock); + } + } + + if (kill_device) { + return false; + } + return true; +} + +bool SDL_AudioDeviceFormatChanged(SDL_AudioDevice *device, const SDL_AudioSpec *newspec, int new_sample_frames) +{ + ObtainPhysicalAudioDeviceObj(device); + const bool result = SDL_AudioDeviceFormatChangedAlreadyLocked(device, newspec, new_sample_frames); + ReleaseAudioDevice(device); + return result; +} + +// This is an internal function, so SDL_PumpEvents() can check for pending audio device events. +// ("UpdateSubsystem" is the same naming that the other things that hook into PumpEvents use.) +void SDL_UpdateAudio(void) +{ + SDL_LockRWLockForReading(current_audio.device_hash_lock); + SDL_PendingAudioDeviceEvent *pending_events = current_audio.pending_events.next; + SDL_UnlockRWLock(current_audio.device_hash_lock); + + if (!pending_events) { + return; // nothing to do, check next time. + } + + // okay, let's take this whole list of events so we can dump the lock, and new ones can queue up for a later update. + SDL_LockRWLockForWriting(current_audio.device_hash_lock); + pending_events = current_audio.pending_events.next; // in case this changed... + current_audio.pending_events.next = NULL; + current_audio.pending_events_tail = ¤t_audio.pending_events; + SDL_UnlockRWLock(current_audio.device_hash_lock); + + SDL_PendingAudioDeviceEvent *pending_next = NULL; + for (SDL_PendingAudioDeviceEvent *i = pending_events; i; i = pending_next) { + pending_next = i->next; + if (SDL_EventEnabled(i->type)) { + SDL_Event event; + SDL_zero(event); + event.type = i->type; + event.adevice.which = (Uint32) i->devid; + event.adevice.recording = ((i->devid & (1<<0)) == 0); // bit #0 of devid is set for playback devices and unset for recording. + SDL_PushEvent(&event); + } + SDL_free(i); + } +} + diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audio_c.h b/contrib/SDL-3.2.8/src/audio/SDL_audio_c.h new file mode 100644 index 0000000..0e673f1 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audio_c.h @@ -0,0 +1,27 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef SDL_audio_c_h_ +#define SDL_audio_c_h_ + +extern void SDL_UpdateAudio(void); + +#endif // SDL_audio_c_h_ diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audio_channel_converters.h b/contrib/SDL-3.2.8/src/audio/SDL_audio_channel_converters.h new file mode 100644 index 0000000..ab682e6 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audio_channel_converters.h @@ -0,0 +1,1068 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +// DO NOT EDIT, THIS FILE WAS GENERATED BY build-scripts/gen_audio_channel_conversion.c + + +typedef void (*SDL_AudioChannelConverter)(float *dst, const float *src, int num_frames); + +static void SDL_ConvertMonoToStereo(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("mono", "stereo"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1); + dst += (num_frames-1) * 2; + for (i = num_frames; i; i--, src--, dst -= 2) { + const float srcFC = src[0]; + dst[1] /* FR */ = srcFC; + dst[0] /* FL */ = srcFC; + } + +} + +static void SDL_ConvertMonoTo21(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("mono", "2.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1); + dst += (num_frames-1) * 3; + for (i = num_frames; i; i--, src--, dst -= 3) { + const float srcFC = src[0]; + dst[2] /* LFE */ = 0.0f; + dst[1] /* FR */ = srcFC; + dst[0] /* FL */ = srcFC; + } + +} + +static void SDL_ConvertMonoToQuad(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("mono", "quad"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1); + dst += (num_frames-1) * 4; + for (i = num_frames; i; i--, src--, dst -= 4) { + const float srcFC = src[0]; + dst[3] /* BR */ = 0.0f; + dst[2] /* BL */ = 0.0f; + dst[1] /* FR */ = srcFC; + dst[0] /* FL */ = srcFC; + } + +} + +static void SDL_ConvertMonoTo41(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("mono", "4.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1); + dst += (num_frames-1) * 5; + for (i = num_frames; i; i--, src--, dst -= 5) { + const float srcFC = src[0]; + dst[4] /* BR */ = 0.0f; + dst[3] /* BL */ = 0.0f; + dst[2] /* LFE */ = 0.0f; + dst[1] /* FR */ = srcFC; + dst[0] /* FL */ = srcFC; + } + +} + +static void SDL_ConvertMonoTo51(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("mono", "5.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1); + dst += (num_frames-1) * 6; + for (i = num_frames; i; i--, src--, dst -= 6) { + const float srcFC = src[0]; + dst[5] /* BR */ = 0.0f; + dst[4] /* BL */ = 0.0f; + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = srcFC; + dst[0] /* FL */ = srcFC; + } + +} + +static void SDL_ConvertMonoTo61(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("mono", "6.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1); + dst += (num_frames-1) * 7; + for (i = num_frames; i; i--, src--, dst -= 7) { + const float srcFC = src[0]; + dst[6] /* SR */ = 0.0f; + dst[5] /* SL */ = 0.0f; + dst[4] /* BC */ = 0.0f; + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = srcFC; + dst[0] /* FL */ = srcFC; + } + +} + +static void SDL_ConvertMonoTo71(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("mono", "7.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1); + dst += (num_frames-1) * 8; + for (i = num_frames; i; i--, src--, dst -= 8) { + const float srcFC = src[0]; + dst[7] /* SR */ = 0.0f; + dst[6] /* SL */ = 0.0f; + dst[5] /* BR */ = 0.0f; + dst[4] /* BL */ = 0.0f; + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = srcFC; + dst[0] /* FL */ = srcFC; + } + +} + +static void SDL_ConvertStereoToMono(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("stereo", "mono"); + + for (i = num_frames; i; i--, src += 2, dst++) { + dst[0] /* FC */ = (src[0] * 0.500000000f) + (src[1] * 0.500000000f); + } + +} + +static void SDL_ConvertStereoTo21(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("stereo", "2.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 2; + dst += (num_frames-1) * 3; + for (i = num_frames; i; i--, src -= 2, dst -= 3) { + dst[2] /* LFE */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_ConvertStereoToQuad(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("stereo", "quad"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 2; + dst += (num_frames-1) * 4; + for (i = num_frames; i; i--, src -= 2, dst -= 4) { + dst[3] /* BR */ = 0.0f; + dst[2] /* BL */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_ConvertStereoTo41(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("stereo", "4.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 2; + dst += (num_frames-1) * 5; + for (i = num_frames; i; i--, src -= 2, dst -= 5) { + dst[4] /* BR */ = 0.0f; + dst[3] /* BL */ = 0.0f; + dst[2] /* LFE */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_ConvertStereoTo51(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("stereo", "5.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 2; + dst += (num_frames-1) * 6; + for (i = num_frames; i; i--, src -= 2, dst -= 6) { + dst[5] /* BR */ = 0.0f; + dst[4] /* BL */ = 0.0f; + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_ConvertStereoTo61(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("stereo", "6.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 2; + dst += (num_frames-1) * 7; + for (i = num_frames; i; i--, src -= 2, dst -= 7) { + dst[6] /* SR */ = 0.0f; + dst[5] /* SL */ = 0.0f; + dst[4] /* BC */ = 0.0f; + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_ConvertStereoTo71(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("stereo", "7.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 2; + dst += (num_frames-1) * 8; + for (i = num_frames; i; i--, src -= 2, dst -= 8) { + dst[7] /* SR */ = 0.0f; + dst[6] /* SL */ = 0.0f; + dst[5] /* BR */ = 0.0f; + dst[4] /* BL */ = 0.0f; + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert21ToMono(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("2.1", "mono"); + + for (i = num_frames; i; i--, src += 3, dst++) { + dst[0] /* FC */ = (src[0] * 0.333333343f) + (src[1] * 0.333333343f) + (src[2] * 0.333333343f); + } + +} + +static void SDL_Convert21ToStereo(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("2.1", "stereo"); + + for (i = num_frames; i; i--, src += 3, dst += 2) { + const float srcLFE = src[2]; + dst[0] /* FL */ = (src[0] * 0.800000012f) + (srcLFE * 0.200000003f); + dst[1] /* FR */ = (src[1] * 0.800000012f) + (srcLFE * 0.200000003f); + } + +} + +static void SDL_Convert21ToQuad(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("2.1", "quad"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 3; + dst += (num_frames-1) * 4; + for (i = num_frames; i; i--, src -= 3, dst -= 4) { + const float srcLFE = src[2]; + dst[3] /* BR */ = (srcLFE * 0.111111112f); + dst[2] /* BL */ = (srcLFE * 0.111111112f); + dst[1] /* FR */ = (srcLFE * 0.111111112f) + (src[1] * 0.888888896f); + dst[0] /* FL */ = (srcLFE * 0.111111112f) + (src[0] * 0.888888896f); + } + +} + +static void SDL_Convert21To41(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("2.1", "4.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 3; + dst += (num_frames-1) * 5; + for (i = num_frames; i; i--, src -= 3, dst -= 5) { + dst[4] /* BR */ = 0.0f; + dst[3] /* BL */ = 0.0f; + dst[2] /* LFE */ = src[2]; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert21To51(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("2.1", "5.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 3; + dst += (num_frames-1) * 6; + for (i = num_frames; i; i--, src -= 3, dst -= 6) { + dst[5] /* BR */ = 0.0f; + dst[4] /* BL */ = 0.0f; + dst[3] /* LFE */ = src[2]; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert21To61(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("2.1", "6.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 3; + dst += (num_frames-1) * 7; + for (i = num_frames; i; i--, src -= 3, dst -= 7) { + dst[6] /* SR */ = 0.0f; + dst[5] /* SL */ = 0.0f; + dst[4] /* BC */ = 0.0f; + dst[3] /* LFE */ = src[2]; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert21To71(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("2.1", "7.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 3; + dst += (num_frames-1) * 8; + for (i = num_frames; i; i--, src -= 3, dst -= 8) { + dst[7] /* SR */ = 0.0f; + dst[6] /* SL */ = 0.0f; + dst[5] /* BR */ = 0.0f; + dst[4] /* BL */ = 0.0f; + dst[3] /* LFE */ = src[2]; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_ConvertQuadToMono(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("quad", "mono"); + + for (i = num_frames; i; i--, src += 4, dst++) { + dst[0] /* FC */ = (src[0] * 0.250000000f) + (src[1] * 0.250000000f) + (src[2] * 0.250000000f) + (src[3] * 0.250000000f); + } + +} + +static void SDL_ConvertQuadToStereo(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("quad", "stereo"); + + for (i = num_frames; i; i--, src += 4, dst += 2) { + const float srcBL = src[2]; + const float srcBR = src[3]; + dst[0] /* FL */ = (src[0] * 0.421000004f) + (srcBL * 0.358999997f) + (srcBR * 0.219999999f); + dst[1] /* FR */ = (src[1] * 0.421000004f) + (srcBL * 0.219999999f) + (srcBR * 0.358999997f); + } + +} + +static void SDL_ConvertQuadTo21(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("quad", "2.1"); + + for (i = num_frames; i; i--, src += 4, dst += 3) { + const float srcBL = src[2]; + const float srcBR = src[3]; + dst[0] /* FL */ = (src[0] * 0.421000004f) + (srcBL * 0.358999997f) + (srcBR * 0.219999999f); + dst[1] /* FR */ = (src[1] * 0.421000004f) + (srcBL * 0.219999999f) + (srcBR * 0.358999997f); + dst[2] /* LFE */ = 0.0f; + } + +} + +static void SDL_ConvertQuadTo41(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("quad", "4.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 4; + dst += (num_frames-1) * 5; + for (i = num_frames; i; i--, src -= 4, dst -= 5) { + dst[4] /* BR */ = src[3]; + dst[3] /* BL */ = src[2]; + dst[2] /* LFE */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_ConvertQuadTo51(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("quad", "5.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 4; + dst += (num_frames-1) * 6; + for (i = num_frames; i; i--, src -= 4, dst -= 6) { + dst[5] /* BR */ = src[3]; + dst[4] /* BL */ = src[2]; + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_ConvertQuadTo61(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("quad", "6.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 4; + dst += (num_frames-1) * 7; + for (i = num_frames; i; i--, src -= 4, dst -= 7) { + const float srcBL = src[2]; + const float srcBR = src[3]; + dst[6] /* SR */ = (srcBR * 0.796000004f); + dst[5] /* SL */ = (srcBL * 0.796000004f); + dst[4] /* BC */ = (srcBR * 0.500000000f) + (srcBL * 0.500000000f); + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = (src[1] * 0.939999998f); + dst[0] /* FL */ = (src[0] * 0.939999998f); + } + +} + +static void SDL_ConvertQuadTo71(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("quad", "7.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 4; + dst += (num_frames-1) * 8; + for (i = num_frames; i; i--, src -= 4, dst -= 8) { + dst[7] /* SR */ = 0.0f; + dst[6] /* SL */ = 0.0f; + dst[5] /* BR */ = src[3]; + dst[4] /* BL */ = src[2]; + dst[3] /* LFE */ = 0.0f; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert41ToMono(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("4.1", "mono"); + + for (i = num_frames; i; i--, src += 5, dst++) { + dst[0] /* FC */ = (src[0] * 0.200000003f) + (src[1] * 0.200000003f) + (src[2] * 0.200000003f) + (src[3] * 0.200000003f) + (src[4] * 0.200000003f); + } + +} + +static void SDL_Convert41ToStereo(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("4.1", "stereo"); + + for (i = num_frames; i; i--, src += 5, dst += 2) { + const float srcLFE = src[2]; + const float srcBL = src[3]; + const float srcBR = src[4]; + dst[0] /* FL */ = (src[0] * 0.374222219f) + (srcLFE * 0.111111112f) + (srcBL * 0.319111109f) + (srcBR * 0.195555553f); + dst[1] /* FR */ = (src[1] * 0.374222219f) + (srcLFE * 0.111111112f) + (srcBL * 0.195555553f) + (srcBR * 0.319111109f); + } + +} + +static void SDL_Convert41To21(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("4.1", "2.1"); + + for (i = num_frames; i; i--, src += 5, dst += 3) { + const float srcBL = src[3]; + const float srcBR = src[4]; + dst[0] /* FL */ = (src[0] * 0.421000004f) + (srcBL * 0.358999997f) + (srcBR * 0.219999999f); + dst[1] /* FR */ = (src[1] * 0.421000004f) + (srcBL * 0.219999999f) + (srcBR * 0.358999997f); + dst[2] /* LFE */ = src[2]; + } + +} + +static void SDL_Convert41ToQuad(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("4.1", "quad"); + + for (i = num_frames; i; i--, src += 5, dst += 4) { + const float srcLFE = src[2]; + dst[0] /* FL */ = (src[0] * 0.941176474f) + (srcLFE * 0.058823530f); + dst[1] /* FR */ = (src[1] * 0.941176474f) + (srcLFE * 0.058823530f); + dst[2] /* BL */ = (srcLFE * 0.058823530f) + (src[3] * 0.941176474f); + dst[3] /* BR */ = (srcLFE * 0.058823530f) + (src[4] * 0.941176474f); + } + +} + +static void SDL_Convert41To51(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("4.1", "5.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 5; + dst += (num_frames-1) * 6; + for (i = num_frames; i; i--, src -= 5, dst -= 6) { + dst[5] /* BR */ = src[4]; + dst[4] /* BL */ = src[3]; + dst[3] /* LFE */ = src[2]; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert41To61(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("4.1", "6.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 5; + dst += (num_frames-1) * 7; + for (i = num_frames; i; i--, src -= 5, dst -= 7) { + const float srcBL = src[3]; + const float srcBR = src[4]; + dst[6] /* SR */ = (srcBR * 0.796000004f); + dst[5] /* SL */ = (srcBL * 0.796000004f); + dst[4] /* BC */ = (srcBR * 0.500000000f) + (srcBL * 0.500000000f); + dst[3] /* LFE */ = src[2]; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = (src[1] * 0.939999998f); + dst[0] /* FL */ = (src[0] * 0.939999998f); + } + +} + +static void SDL_Convert41To71(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("4.1", "7.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 5; + dst += (num_frames-1) * 8; + for (i = num_frames; i; i--, src -= 5, dst -= 8) { + dst[7] /* SR */ = 0.0f; + dst[6] /* SL */ = 0.0f; + dst[5] /* BR */ = src[4]; + dst[4] /* BL */ = src[3]; + dst[3] /* LFE */ = src[2]; + dst[2] /* FC */ = 0.0f; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert51ToMono(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("5.1", "mono"); + + for (i = num_frames; i; i--, src += 6, dst++) { + dst[0] /* FC */ = (src[0] * 0.166666672f) + (src[1] * 0.166666672f) + (src[2] * 0.166666672f) + (src[3] * 0.166666672f) + (src[4] * 0.166666672f) + (src[5] * 0.166666672f); + } + +} + +static void SDL_Convert51ToStereo(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("5.1", "stereo"); + + for (i = num_frames; i; i--, src += 6, dst += 2) { + const float srcFC = src[2]; + const float srcLFE = src[3]; + const float srcBL = src[4]; + const float srcBR = src[5]; + dst[0] /* FL */ = (src[0] * 0.294545442f) + (srcFC * 0.208181813f) + (srcLFE * 0.090909094f) + (srcBL * 0.251818180f) + (srcBR * 0.154545456f); + dst[1] /* FR */ = (src[1] * 0.294545442f) + (srcFC * 0.208181813f) + (srcLFE * 0.090909094f) + (srcBL * 0.154545456f) + (srcBR * 0.251818180f); + } + +} + +static void SDL_Convert51To21(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("5.1", "2.1"); + + for (i = num_frames; i; i--, src += 6, dst += 3) { + const float srcFC = src[2]; + const float srcBL = src[4]; + const float srcBR = src[5]; + dst[0] /* FL */ = (src[0] * 0.324000001f) + (srcFC * 0.229000002f) + (srcBL * 0.277000010f) + (srcBR * 0.170000002f); + dst[1] /* FR */ = (src[1] * 0.324000001f) + (srcFC * 0.229000002f) + (srcBL * 0.170000002f) + (srcBR * 0.277000010f); + dst[2] /* LFE */ = src[3]; + } + +} + +static void SDL_Convert51ToQuad(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("5.1", "quad"); + + for (i = num_frames; i; i--, src += 6, dst += 4) { + const float srcFC = src[2]; + const float srcLFE = src[3]; + dst[0] /* FL */ = (src[0] * 0.558095276f) + (srcFC * 0.394285709f) + (srcLFE * 0.047619049f); + dst[1] /* FR */ = (src[1] * 0.558095276f) + (srcFC * 0.394285709f) + (srcLFE * 0.047619049f); + dst[2] /* BL */ = (srcLFE * 0.047619049f) + (src[4] * 0.558095276f); + dst[3] /* BR */ = (srcLFE * 0.047619049f) + (src[5] * 0.558095276f); + } + +} + +static void SDL_Convert51To41(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("5.1", "4.1"); + + for (i = num_frames; i; i--, src += 6, dst += 5) { + const float srcFC = src[2]; + dst[0] /* FL */ = (src[0] * 0.586000025f) + (srcFC * 0.414000005f); + dst[1] /* FR */ = (src[1] * 0.586000025f) + (srcFC * 0.414000005f); + dst[2] /* LFE */ = src[3]; + dst[3] /* BL */ = (src[4] * 0.586000025f); + dst[4] /* BR */ = (src[5] * 0.586000025f); + } + +} + +static void SDL_Convert51To61(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("5.1", "6.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 6; + dst += (num_frames-1) * 7; + for (i = num_frames; i; i--, src -= 6, dst -= 7) { + const float srcBL = src[4]; + const float srcBR = src[5]; + dst[6] /* SR */ = (srcBR * 0.796000004f); + dst[5] /* SL */ = (srcBL * 0.796000004f); + dst[4] /* BC */ = (srcBR * 0.500000000f) + (srcBL * 0.500000000f); + dst[3] /* LFE */ = src[3]; + dst[2] /* FC */ = (src[2] * 0.939999998f); + dst[1] /* FR */ = (src[1] * 0.939999998f); + dst[0] /* FL */ = (src[0] * 0.939999998f); + } + +} + +static void SDL_Convert51To71(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("5.1", "7.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 6; + dst += (num_frames-1) * 8; + for (i = num_frames; i; i--, src -= 6, dst -= 8) { + dst[7] /* SR */ = 0.0f; + dst[6] /* SL */ = 0.0f; + dst[5] /* BR */ = src[5]; + dst[4] /* BL */ = src[4]; + dst[3] /* LFE */ = src[3]; + dst[2] /* FC */ = src[2]; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert61ToMono(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("6.1", "mono"); + + for (i = num_frames; i; i--, src += 7, dst++) { + dst[0] /* FC */ = (src[0] * 0.143142849f) + (src[1] * 0.143142849f) + (src[2] * 0.143142849f) + (src[3] * 0.142857149f) + (src[4] * 0.143142849f) + (src[5] * 0.143142849f) + (src[6] * 0.143142849f); + } + +} + +static void SDL_Convert61ToStereo(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("6.1", "stereo"); + + for (i = num_frames; i; i--, src += 7, dst += 2) { + const float srcFC = src[2]; + const float srcLFE = src[3]; + const float srcBC = src[4]; + const float srcSL = src[5]; + const float srcSR = src[6]; + dst[0] /* FL */ = (src[0] * 0.247384623f) + (srcFC * 0.174461529f) + (srcLFE * 0.076923080f) + (srcBC * 0.174461529f) + (srcSL * 0.226153851f) + (srcSR * 0.100615382f); + dst[1] /* FR */ = (src[1] * 0.247384623f) + (srcFC * 0.174461529f) + (srcLFE * 0.076923080f) + (srcBC * 0.174461529f) + (srcSL * 0.100615382f) + (srcSR * 0.226153851f); + } + +} + +static void SDL_Convert61To21(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("6.1", "2.1"); + + for (i = num_frames; i; i--, src += 7, dst += 3) { + const float srcFC = src[2]; + const float srcBC = src[4]; + const float srcSL = src[5]; + const float srcSR = src[6]; + dst[0] /* FL */ = (src[0] * 0.268000007f) + (srcFC * 0.188999996f) + (srcBC * 0.188999996f) + (srcSL * 0.245000005f) + (srcSR * 0.108999997f); + dst[1] /* FR */ = (src[1] * 0.268000007f) + (srcFC * 0.188999996f) + (srcBC * 0.188999996f) + (srcSL * 0.108999997f) + (srcSR * 0.245000005f); + dst[2] /* LFE */ = src[3]; + } + +} + +static void SDL_Convert61ToQuad(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("6.1", "quad"); + + for (i = num_frames; i; i--, src += 7, dst += 4) { + const float srcFC = src[2]; + const float srcLFE = src[3]; + const float srcBC = src[4]; + const float srcSL = src[5]; + const float srcSR = src[6]; + dst[0] /* FL */ = (src[0] * 0.463679999f) + (srcFC * 0.327360004f) + (srcLFE * 0.040000003f) + (srcSL * 0.168960005f); + dst[1] /* FR */ = (src[1] * 0.463679999f) + (srcFC * 0.327360004f) + (srcLFE * 0.040000003f) + (srcSR * 0.168960005f); + dst[2] /* BL */ = (srcLFE * 0.040000003f) + (srcBC * 0.327360004f) + (srcSL * 0.431039989f); + dst[3] /* BR */ = (srcLFE * 0.040000003f) + (srcBC * 0.327360004f) + (srcSR * 0.431039989f); + } + +} + +static void SDL_Convert61To41(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("6.1", "4.1"); + + for (i = num_frames; i; i--, src += 7, dst += 5) { + const float srcFC = src[2]; + const float srcBC = src[4]; + const float srcSL = src[5]; + const float srcSR = src[6]; + dst[0] /* FL */ = (src[0] * 0.483000010f) + (srcFC * 0.340999991f) + (srcSL * 0.175999999f); + dst[1] /* FR */ = (src[1] * 0.483000010f) + (srcFC * 0.340999991f) + (srcSR * 0.175999999f); + dst[2] /* LFE */ = src[3]; + dst[3] /* BL */ = (srcBC * 0.340999991f) + (srcSL * 0.449000001f); + dst[4] /* BR */ = (srcBC * 0.340999991f) + (srcSR * 0.449000001f); + } + +} + +static void SDL_Convert61To51(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("6.1", "5.1"); + + for (i = num_frames; i; i--, src += 7, dst += 6) { + const float srcBC = src[4]; + const float srcSL = src[5]; + const float srcSR = src[6]; + dst[0] /* FL */ = (src[0] * 0.611000001f) + (srcSL * 0.223000005f); + dst[1] /* FR */ = (src[1] * 0.611000001f) + (srcSR * 0.223000005f); + dst[2] /* FC */ = (src[2] * 0.611000001f); + dst[3] /* LFE */ = src[3]; + dst[4] /* BL */ = (srcBC * 0.432000011f) + (srcSL * 0.568000019f); + dst[5] /* BR */ = (srcBC * 0.432000011f) + (srcSR * 0.568000019f); + } + +} + +static void SDL_Convert61To71(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("6.1", "7.1"); + + // convert backwards, since output is growing in-place. + src += (num_frames-1) * 7; + dst += (num_frames-1) * 8; + for (i = num_frames; i; i--, src -= 7, dst -= 8) { + const float srcBC = src[4]; + dst[7] /* SR */ = src[6]; + dst[6] /* SL */ = src[5]; + dst[5] /* BR */ = (srcBC * 0.707000017f); + dst[4] /* BL */ = (srcBC * 0.707000017f); + dst[3] /* LFE */ = src[3]; + dst[2] /* FC */ = src[2]; + dst[1] /* FR */ = src[1]; + dst[0] /* FL */ = src[0]; + } + +} + +static void SDL_Convert71ToMono(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("7.1", "mono"); + + for (i = num_frames; i; i--, src += 8, dst++) { + dst[0] /* FC */ = (src[0] * 0.125125006f) + (src[1] * 0.125125006f) + (src[2] * 0.125125006f) + (src[3] * 0.125000000f) + (src[4] * 0.125125006f) + (src[5] * 0.125125006f) + (src[6] * 0.125125006f) + (src[7] * 0.125125006f); + } + +} + +static void SDL_Convert71ToStereo(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("7.1", "stereo"); + + for (i = num_frames; i; i--, src += 8, dst += 2) { + const float srcFC = src[2]; + const float srcLFE = src[3]; + const float srcBL = src[4]; + const float srcBR = src[5]; + const float srcSL = src[6]; + const float srcSR = src[7]; + dst[0] /* FL */ = (src[0] * 0.211866662f) + (srcFC * 0.150266662f) + (srcLFE * 0.066666670f) + (srcBL * 0.181066677f) + (srcBR * 0.111066669f) + (srcSL * 0.194133341f) + (srcSR * 0.085866667f); + dst[1] /* FR */ = (src[1] * 0.211866662f) + (srcFC * 0.150266662f) + (srcLFE * 0.066666670f) + (srcBL * 0.111066669f) + (srcBR * 0.181066677f) + (srcSL * 0.085866667f) + (srcSR * 0.194133341f); + } + +} + +static void SDL_Convert71To21(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("7.1", "2.1"); + + for (i = num_frames; i; i--, src += 8, dst += 3) { + const float srcFC = src[2]; + const float srcBL = src[4]; + const float srcBR = src[5]; + const float srcSL = src[6]; + const float srcSR = src[7]; + dst[0] /* FL */ = (src[0] * 0.226999998f) + (srcFC * 0.160999998f) + (srcBL * 0.194000006f) + (srcBR * 0.119000003f) + (srcSL * 0.208000004f) + (srcSR * 0.092000000f); + dst[1] /* FR */ = (src[1] * 0.226999998f) + (srcFC * 0.160999998f) + (srcBL * 0.119000003f) + (srcBR * 0.194000006f) + (srcSL * 0.092000000f) + (srcSR * 0.208000004f); + dst[2] /* LFE */ = src[3]; + } + +} + +static void SDL_Convert71ToQuad(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("7.1", "quad"); + + for (i = num_frames; i; i--, src += 8, dst += 4) { + const float srcFC = src[2]; + const float srcLFE = src[3]; + const float srcSL = src[6]; + const float srcSR = src[7]; + dst[0] /* FL */ = (src[0] * 0.466344833f) + (srcFC * 0.329241365f) + (srcLFE * 0.034482758f) + (srcSL * 0.169931039f); + dst[1] /* FR */ = (src[1] * 0.466344833f) + (srcFC * 0.329241365f) + (srcLFE * 0.034482758f) + (srcSR * 0.169931039f); + dst[2] /* BL */ = (srcLFE * 0.034482758f) + (src[4] * 0.466344833f) + (srcSL * 0.433517247f); + dst[3] /* BR */ = (srcLFE * 0.034482758f) + (src[5] * 0.466344833f) + (srcSR * 0.433517247f); + } + +} + +static void SDL_Convert71To41(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("7.1", "4.1"); + + for (i = num_frames; i; i--, src += 8, dst += 5) { + const float srcFC = src[2]; + const float srcSL = src[6]; + const float srcSR = src[7]; + dst[0] /* FL */ = (src[0] * 0.483000010f) + (srcFC * 0.340999991f) + (srcSL * 0.175999999f); + dst[1] /* FR */ = (src[1] * 0.483000010f) + (srcFC * 0.340999991f) + (srcSR * 0.175999999f); + dst[2] /* LFE */ = src[3]; + dst[3] /* BL */ = (src[4] * 0.483000010f) + (srcSL * 0.449000001f); + dst[4] /* BR */ = (src[5] * 0.483000010f) + (srcSR * 0.449000001f); + } + +} + +static void SDL_Convert71To51(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("7.1", "5.1"); + + for (i = num_frames; i; i--, src += 8, dst += 6) { + const float srcSL = src[6]; + const float srcSR = src[7]; + dst[0] /* FL */ = (src[0] * 0.518000007f) + (srcSL * 0.188999996f); + dst[1] /* FR */ = (src[1] * 0.518000007f) + (srcSR * 0.188999996f); + dst[2] /* FC */ = (src[2] * 0.518000007f); + dst[3] /* LFE */ = src[3]; + dst[4] /* BL */ = (src[4] * 0.518000007f) + (srcSL * 0.481999993f); + dst[5] /* BR */ = (src[5] * 0.518000007f) + (srcSR * 0.481999993f); + } + +} + +static void SDL_Convert71To61(float *dst, const float *src, int num_frames) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("7.1", "6.1"); + + for (i = num_frames; i; i--, src += 8, dst += 7) { + const float srcBL = src[4]; + const float srcBR = src[5]; + dst[0] /* FL */ = (src[0] * 0.541000009f); + dst[1] /* FR */ = (src[1] * 0.541000009f); + dst[2] /* FC */ = (src[2] * 0.541000009f); + dst[3] /* LFE */ = src[3]; + dst[4] /* BC */ = (srcBL * 0.287999988f) + (srcBR * 0.287999988f); + dst[5] /* SL */ = (srcBL * 0.458999991f) + (src[6] * 0.541000009f); + dst[6] /* SR */ = (srcBR * 0.458999991f) + (src[7] * 0.541000009f); + } + +} + +static const SDL_AudioChannelConverter channel_converters[8][8] = { // [from][to] + { NULL, SDL_ConvertMonoToStereo, SDL_ConvertMonoTo21, SDL_ConvertMonoToQuad, SDL_ConvertMonoTo41, SDL_ConvertMonoTo51, SDL_ConvertMonoTo61, SDL_ConvertMonoTo71 }, + { SDL_ConvertStereoToMono, NULL, SDL_ConvertStereoTo21, SDL_ConvertStereoToQuad, SDL_ConvertStereoTo41, SDL_ConvertStereoTo51, SDL_ConvertStereoTo61, SDL_ConvertStereoTo71 }, + { SDL_Convert21ToMono, SDL_Convert21ToStereo, NULL, SDL_Convert21ToQuad, SDL_Convert21To41, SDL_Convert21To51, SDL_Convert21To61, SDL_Convert21To71 }, + { SDL_ConvertQuadToMono, SDL_ConvertQuadToStereo, SDL_ConvertQuadTo21, NULL, SDL_ConvertQuadTo41, SDL_ConvertQuadTo51, SDL_ConvertQuadTo61, SDL_ConvertQuadTo71 }, + { SDL_Convert41ToMono, SDL_Convert41ToStereo, SDL_Convert41To21, SDL_Convert41ToQuad, NULL, SDL_Convert41To51, SDL_Convert41To61, SDL_Convert41To71 }, + { SDL_Convert51ToMono, SDL_Convert51ToStereo, SDL_Convert51To21, SDL_Convert51ToQuad, SDL_Convert51To41, NULL, SDL_Convert51To61, SDL_Convert51To71 }, + { SDL_Convert61ToMono, SDL_Convert61ToStereo, SDL_Convert61To21, SDL_Convert61ToQuad, SDL_Convert61To41, SDL_Convert61To51, NULL, SDL_Convert61To71 }, + { SDL_Convert71ToMono, SDL_Convert71ToStereo, SDL_Convert71To21, SDL_Convert71ToQuad, SDL_Convert71To41, SDL_Convert71To51, SDL_Convert71To61, NULL } +}; + diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audiocvt.c b/contrib/SDL-3.2.8/src/audio/SDL_audiocvt.c new file mode 100644 index 0000000..f751b0e --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audiocvt.c @@ -0,0 +1,1381 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_sysaudio.h" + +#include "SDL_audioqueue.h" +#include "SDL_audioresample.h" + +#ifndef SDL_INT_MAX +#define SDL_INT_MAX ((int)(~0u>>1)) +#endif + +#ifdef SDL_SSE3_INTRINSICS +// Convert from stereo to mono. Average left and right. +static void SDL_TARGETING("sse3") SDL_ConvertStereoToMono_SSE3(float *dst, const float *src, int num_frames) +{ + LOG_DEBUG_AUDIO_CONVERT("stereo", "mono (using SSE3)"); + + const __m128 divby2 = _mm_set1_ps(0.5f); + int i = num_frames; + + /* Do SSE blocks as long as we have 16 bytes available. + Just use unaligned load/stores, if the memory at runtime is + aligned it'll be just as fast on modern processors */ + while (i >= 4) { // 4 * float32 + _mm_storeu_ps(dst, _mm_mul_ps(_mm_hadd_ps(_mm_loadu_ps(src), _mm_loadu_ps(src + 4)), divby2)); + i -= 4; + src += 8; + dst += 4; + } + + // Finish off any leftovers with scalar operations. + while (i) { + *dst = (src[0] + src[1]) * 0.5f; + dst++; + i--; + src += 2; + } +} +#endif + +#ifdef SDL_SSE_INTRINSICS +// Convert from mono to stereo. Duplicate to stereo left and right. +static void SDL_TARGETING("sse") SDL_ConvertMonoToStereo_SSE(float *dst, const float *src, int num_frames) +{ + LOG_DEBUG_AUDIO_CONVERT("mono", "stereo (using SSE)"); + + // convert backwards, since output is growing in-place. + src += (num_frames-4) * 1; + dst += (num_frames-4) * 2; + + /* Do SSE blocks as long as we have 16 bytes available. + Just use unaligned load/stores, if the memory at runtime is + aligned it'll be just as fast on modern processors */ + // convert backwards, since output is growing in-place. + int i = num_frames; + while (i >= 4) { // 4 * float32 + const __m128 input = _mm_loadu_ps(src); // A B C D + _mm_storeu_ps(dst, _mm_unpacklo_ps(input, input)); // A A B B + _mm_storeu_ps(dst + 4, _mm_unpackhi_ps(input, input)); // C C D D + i -= 4; + src -= 4; + dst -= 8; + } + + // Finish off any leftovers with scalar operations. + src += 3; + dst += 6; // adjust for smaller buffers. + while (i) { // convert backwards, since output is growing in-place. + const float srcFC = src[0]; + dst[1] /* FR */ = srcFC; + dst[0] /* FL */ = srcFC; + i--; + src--; + dst -= 2; + } +} +#endif + +// Include the autogenerated channel converters... +#include "SDL_audio_channel_converters.h" + +static bool SDL_IsSupportedAudioFormat(const SDL_AudioFormat fmt) +{ + switch (fmt) { + case SDL_AUDIO_U8: + case SDL_AUDIO_S8: + case SDL_AUDIO_S16LE: + case SDL_AUDIO_S16BE: + case SDL_AUDIO_S32LE: + case SDL_AUDIO_S32BE: + case SDL_AUDIO_F32LE: + case SDL_AUDIO_F32BE: + return true; // supported. + + default: + break; + } + + return false; // unsupported. +} + +static bool SDL_IsSupportedChannelCount(const int channels) +{ + return ((channels >= 1) && (channels <= 8)); +} + +bool SDL_ChannelMapIsBogus(const int *chmap, int channels) +{ + if (chmap) { + for (int i = 0; i < channels; i++) { + const int mapping = chmap[i]; + if ((mapping < -1) || (mapping >= channels)) { + return true; + } + } + } + return false; +} + +bool SDL_ChannelMapIsDefault(const int *chmap, int channels) +{ + if (chmap) { + for (int i = 0; i < channels; i++) { + if (chmap[i] != i) { + return false; + } + } + } + return true; +} + +// Swizzle audio channels. src and dst can be the same pointer. It does not change the buffer size. +static void SwizzleAudio(const int num_frames, void *dst, const void *src, int channels, const int *map, SDL_AudioFormat fmt) +{ + const int bitsize = (int) SDL_AUDIO_BITSIZE(fmt); + + bool has_null_mappings = false; // !!! FIXME: calculate this when setting the channel map instead. + for (int i = 0; i < channels; i++) { + if (map[i] == -1) { + has_null_mappings = true; + break; + } + } + + #define CHANNEL_SWIZZLE(bits) { \ + Uint##bits *tdst = (Uint##bits *) dst; /* treat as UintX; we only care about moving bits and not the type here. */ \ + const Uint##bits *tsrc = (const Uint##bits *) src; \ + if (src != dst) { /* don't need to copy to a temporary frame first. */ \ + if (has_null_mappings) { \ + const Uint##bits silence = (Uint##bits) SDL_GetSilenceValueForFormat(fmt); \ + for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \ + for (int ch = 0; ch < channels; ch++) { \ + const int m = map[ch]; \ + tdst[ch] = (m == -1) ? silence : tsrc[m]; \ + } \ + } \ + } else { \ + for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \ + for (int ch = 0; ch < channels; ch++) { \ + tdst[ch] = tsrc[map[ch]]; \ + } \ + } \ + } \ + } else { \ + bool isstack; \ + Uint##bits *tmp = (Uint##bits *) SDL_small_alloc(int, channels, &isstack); /* !!! FIXME: allocate this when setting the channel map instead. */ \ + if (tmp) { \ + if (has_null_mappings) { \ + const Uint##bits silence = (Uint##bits) SDL_GetSilenceValueForFormat(fmt); \ + for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \ + for (int ch = 0; ch < channels; ch++) { \ + const int m = map[ch]; \ + tmp[ch] = (m == -1) ? silence : tsrc[m]; \ + } \ + for (int ch = 0; ch < channels; ch++) { \ + tdst[ch] = tmp[ch]; \ + } \ + } \ + } else { \ + for (int i = 0; i < num_frames; i++, tsrc += channels, tdst += channels) { \ + for (int ch = 0; ch < channels; ch++) { \ + tmp[ch] = tsrc[map[ch]]; \ + } \ + for (int ch = 0; ch < channels; ch++) { \ + tdst[ch] = tmp[ch]; \ + } \ + } \ + } \ + SDL_small_free(tmp, isstack); \ + } \ + } \ + } + + switch (bitsize) { + case 8: CHANNEL_SWIZZLE(8); break; + case 16: CHANNEL_SWIZZLE(16); break; + case 32: CHANNEL_SWIZZLE(32); break; + // we don't currently have int64 or double audio datatypes, so no `case 64` for now. + default: SDL_assert(!"Unsupported audio datatype size"); break; + } + + #undef CHANNEL_SWIZZLE +} + + +// This does type and channel conversions _but not resampling_ (resampling happens in SDL_AudioStream). +// This does not check parameter validity, (beyond asserts), it expects you did that already! +// All of this has to function as if src==dst==scratch (conversion in-place), but as a convenience +// if you're just going to copy the final output elsewhere, you can specify a different output pointer. +// +// The scratch buffer must be able to store `num_frames * CalculateMaxSampleFrameSize(src_format, src_channels, dst_format, dst_channels)` bytes. +// If the scratch buffer is NULL, this restriction applies to the output buffer instead. +// +// Since this is a convenient point that audio goes through even if it doesn't need format conversion, +// we also handle gain adjustment here, so we don't have to make another pass over the data later. +// Strictly speaking, this is also a "conversion". :) +void ConvertAudio(int num_frames, + const void *src, SDL_AudioFormat src_format, int src_channels, const int *src_map, + void *dst, SDL_AudioFormat dst_format, int dst_channels, const int *dst_map, + void *scratch, float gain) +{ + SDL_assert(src != NULL); + SDL_assert(dst != NULL); + SDL_assert(SDL_IsSupportedAudioFormat(src_format)); + SDL_assert(SDL_IsSupportedAudioFormat(dst_format)); + SDL_assert(SDL_IsSupportedChannelCount(src_channels)); + SDL_assert(SDL_IsSupportedChannelCount(dst_channels)); + + if (!num_frames) { + return; // no data to convert, quit. + } + +#if DEBUG_AUDIO_CONVERT + SDL_Log("SDL_AUDIO_CONVERT: Convert format %04x->%04x, channels %u->%u", src_format, dst_format, src_channels, dst_channels); +#endif + + const int dst_bitsize = (int) SDL_AUDIO_BITSIZE(dst_format); + const int dst_sample_frame_size = (dst_bitsize / 8) * dst_channels; + + const bool chmaps_match = (src_channels == dst_channels) && SDL_AudioChannelMapsEqual(src_channels, src_map, dst_map); + if (chmaps_match) { + src_map = dst_map = NULL; // NULL both these out so we don't do any unnecessary swizzling. + } + + /* Type conversion goes like this now: + - swizzle through source channel map to "standard" layout. + - byteswap to CPU native format first if necessary. + - convert to native Float32 if necessary. + - change channel count if necessary. + - convert to final data format. + - byteswap back to foreign format if necessary. + - swizzle through dest channel map from "standard" layout. + + The expectation is we can process data faster in float32 + (possibly with SIMD), and making several passes over the same + buffer is likely to be CPU cache-friendly, avoiding the + biggest performance hit in modern times. Previously we had + (script-generated) custom converters for every data type and + it was a bloat on SDL compile times and final library size. */ + + // swizzle input to "standard" format if necessary. + if (src_map) { + void* buf = scratch ? scratch : dst; // use scratch if available, since it has to be big enough to hold src, unless it's NULL, then dst has to be. + SwizzleAudio(num_frames, buf, src, src_channels, src_map, src_format); + src = buf; + } + + // see if we can skip float conversion entirely. + if ((src_channels == dst_channels) && (gain == 1.0f)) { + if (src_format == dst_format) { + // nothing to do, we're already in the right format, just copy it over if necessary. + if (dst_map) { + SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_format); + } else if (src != dst) { + SDL_memcpy(dst, src, num_frames * dst_sample_frame_size); + } + return; + } + + // just a byteswap needed? + if ((src_format ^ dst_format) == SDL_AUDIO_MASK_BIG_ENDIAN) { + if (dst_map) { // do this first, in case we duplicate channels, we can avoid an extra copy if src != dst. + SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_format); + src = dst; + } + ConvertAudioSwapEndian(dst, src, num_frames * dst_channels, dst_bitsize); + return; // all done. + } + } + + if (!scratch) { + scratch = dst; + } + + const bool srcconvert = src_format != SDL_AUDIO_F32; + const bool channelconvert = src_channels != dst_channels; + const bool dstconvert = dst_format != SDL_AUDIO_F32; + + // get us to float format. + if (srcconvert) { + void* buf = (channelconvert || dstconvert) ? scratch : dst; + ConvertAudioToFloat((float *) buf, src, num_frames * src_channels, src_format); + src = buf; + } + + // Gain adjustment + if (gain != 1.0f) { + float *buf = (float *)((channelconvert || dstconvert) ? scratch : dst); + const int total_samples = num_frames * src_channels; + if (src == buf) { + for (int i = 0; i < total_samples; i++) { + buf[i] *= gain; + } + } else { + float *fsrc = (float *)src; + for (int i = 0; i < total_samples; i++) { + buf[i] = fsrc[i] * gain; + } + } + src = buf; + } + + // Channel conversion + + if (channelconvert) { + SDL_AudioChannelConverter channel_converter; + SDL_AudioChannelConverter override = NULL; + + // SDL_IsSupportedChannelCount should have caught these asserts, or we added a new format and forgot to update the table. + SDL_assert(src_channels <= SDL_arraysize(channel_converters)); + SDL_assert(dst_channels <= SDL_arraysize(channel_converters[0])); + + channel_converter = channel_converters[src_channels - 1][dst_channels - 1]; + SDL_assert(channel_converter != NULL); + + // swap in some SIMD versions for a few of these. + if (channel_converter == SDL_ConvertStereoToMono) { + #ifdef SDL_SSE3_INTRINSICS + if (!override && SDL_HasSSE3()) { override = SDL_ConvertStereoToMono_SSE3; } + #endif + } else if (channel_converter == SDL_ConvertMonoToStereo) { + #ifdef SDL_SSE_INTRINSICS + if (!override && SDL_HasSSE()) { override = SDL_ConvertMonoToStereo_SSE; } + #endif + } + + if (override) { + channel_converter = override; + } + + void* buf = dstconvert ? scratch : dst; + channel_converter((float *) buf, (const float *) src, num_frames); + src = buf; + } + + // Resampling is not done in here. SDL_AudioStream handles that. + + // Move to final data type. + if (dstconvert) { + ConvertAudioFromFloat(dst, (const float *) src, num_frames * dst_channels, dst_format); + src = dst; + } + + SDL_assert(src == dst); // if we got here, we _had_ to have done _something_. Otherwise, we should have memcpy'd! + + if (dst_map) { + SwizzleAudio(num_frames, dst, src, dst_channels, dst_map, dst_format); + } +} + +// Calculate the largest frame size needed to convert between the two formats. +static int CalculateMaxFrameSize(SDL_AudioFormat src_format, int src_channels, SDL_AudioFormat dst_format, int dst_channels) +{ + const int src_format_size = SDL_AUDIO_BYTESIZE(src_format); + const int dst_format_size = SDL_AUDIO_BYTESIZE(dst_format); + const int max_app_format_size = SDL_max(src_format_size, dst_format_size); + const int max_format_size = SDL_max(max_app_format_size, sizeof (float)); // ConvertAudio and ResampleAudio use floats. + const int max_channels = SDL_max(src_channels, dst_channels); + return max_format_size * max_channels; +} + +static Sint64 GetAudioStreamResampleRate(SDL_AudioStream* stream, int src_freq, Sint64 resample_offset) +{ + src_freq = (int)((float)src_freq * stream->freq_ratio); + + Sint64 resample_rate = SDL_GetResampleRate(src_freq, stream->dst_spec.freq); + + // If src_freq == dst_freq, and we aren't between frames, don't resample + if ((resample_rate == 0x100000000) && (resample_offset == 0)) { + resample_rate = 0; + } + + return resample_rate; +} + +static bool UpdateAudioStreamInputSpec(SDL_AudioStream *stream, const SDL_AudioSpec *spec, const int *chmap) +{ + if (SDL_AudioSpecsEqual(&stream->input_spec, spec, stream->input_chmap, chmap)) { + return true; + } + + if (!SDL_ResetAudioQueueHistory(stream->queue, SDL_GetResamplerHistoryFrames())) { + return false; + } + + if (!chmap) { + stream->input_chmap = NULL; + } else { + const size_t chmaplen = sizeof (*chmap) * spec->channels; + stream->input_chmap = stream->input_chmap_storage; + SDL_memcpy(stream->input_chmap, chmap, chmaplen); + } + + SDL_copyp(&stream->input_spec, spec); + + return true; +} + +SDL_AudioStream *SDL_CreateAudioStream(const SDL_AudioSpec *src_spec, const SDL_AudioSpec *dst_spec) +{ + SDL_ChooseAudioConverters(); + SDL_SetupAudioResampler(); + + SDL_AudioStream *result = (SDL_AudioStream *)SDL_calloc(1, sizeof(SDL_AudioStream)); + if (!result) { + return NULL; + } + + result->freq_ratio = 1.0f; + result->gain = 1.0f; + result->queue = SDL_CreateAudioQueue(8192); + + if (!result->queue) { + SDL_free(result); + return NULL; + } + + result->lock = SDL_CreateMutex(); + if (!result->lock) { + SDL_free(result->queue); + SDL_free(result); + return NULL; + } + + OnAudioStreamCreated(result); + + if (!SDL_SetAudioStreamFormat(result, src_spec, dst_spec)) { + SDL_DestroyAudioStream(result); + return NULL; + } + + return result; +} + +SDL_PropertiesID SDL_GetAudioStreamProperties(SDL_AudioStream *stream) +{ + if (!stream) { + SDL_InvalidParamError("stream"); + return 0; + } + SDL_LockMutex(stream->lock); + if (stream->props == 0) { + stream->props = SDL_CreateProperties(); + } + SDL_UnlockMutex(stream->lock); + return stream->props; +} + +bool SDL_SetAudioStreamGetCallback(SDL_AudioStream *stream, SDL_AudioStreamCallback callback, void *userdata) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + SDL_LockMutex(stream->lock); + stream->get_callback = callback; + stream->get_callback_userdata = userdata; + SDL_UnlockMutex(stream->lock); + return true; +} + +bool SDL_SetAudioStreamPutCallback(SDL_AudioStream *stream, SDL_AudioStreamCallback callback, void *userdata) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + SDL_LockMutex(stream->lock); + stream->put_callback = callback; + stream->put_callback_userdata = userdata; + SDL_UnlockMutex(stream->lock); + return true; +} + +bool SDL_LockAudioStream(SDL_AudioStream *stream) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + SDL_LockMutex(stream->lock); + return true; +} + +bool SDL_UnlockAudioStream(SDL_AudioStream *stream) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + SDL_UnlockMutex(stream->lock); + return true; +} + +bool SDL_GetAudioStreamFormat(SDL_AudioStream *stream, SDL_AudioSpec *src_spec, SDL_AudioSpec *dst_spec) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + + SDL_LockMutex(stream->lock); + if (src_spec) { + SDL_copyp(src_spec, &stream->src_spec); + } + if (dst_spec) { + SDL_copyp(dst_spec, &stream->dst_spec); + } + SDL_UnlockMutex(stream->lock); + + if (src_spec && src_spec->format == 0) { + return SDL_SetError("Stream has no source format"); + } else if (dst_spec && dst_spec->format == 0) { + return SDL_SetError("Stream has no destination format"); + } + + return true; +} + +bool SDL_SetAudioStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *src_spec, const SDL_AudioSpec *dst_spec) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + + // note that while we've removed the maximum frequency checks, SDL _will_ + // fail to resample to extremely high sample rates correctly. Really high, + // like 196608000Hz. File a bug. :P + + if (src_spec) { + if (!SDL_IsSupportedAudioFormat(src_spec->format)) { + return SDL_InvalidParamError("src_spec->format"); + } else if (!SDL_IsSupportedChannelCount(src_spec->channels)) { + return SDL_InvalidParamError("src_spec->channels"); + } else if (src_spec->freq <= 0) { + return SDL_InvalidParamError("src_spec->freq"); + } + } + + if (dst_spec) { + if (!SDL_IsSupportedAudioFormat(dst_spec->format)) { + return SDL_InvalidParamError("dst_spec->format"); + } else if (!SDL_IsSupportedChannelCount(dst_spec->channels)) { + return SDL_InvalidParamError("dst_spec->channels"); + } else if (dst_spec->freq <= 0) { + return SDL_InvalidParamError("dst_spec->freq"); + } + } + + SDL_LockMutex(stream->lock); + + // quietly refuse to change the format of the end currently bound to a device. + if (stream->bound_device) { + if (stream->bound_device->physical_device->recording) { + src_spec = NULL; + } else { + dst_spec = NULL; + } + } + + if (src_spec) { + if (src_spec->channels != stream->src_spec.channels) { + SDL_free(stream->src_chmap); + stream->src_chmap = NULL; + } + SDL_copyp(&stream->src_spec, src_spec); + } + + if (dst_spec) { + if (dst_spec->channels != stream->dst_spec.channels) { + SDL_free(stream->dst_chmap); + stream->dst_chmap = NULL; + } + SDL_copyp(&stream->dst_spec, dst_spec); + } + + SDL_UnlockMutex(stream->lock); + + return true; +} + +bool SetAudioStreamChannelMap(SDL_AudioStream *stream, const SDL_AudioSpec *spec, int **stream_chmap, const int *chmap, int channels, int isinput) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + + bool result = true; + + SDL_LockMutex(stream->lock); + + if (channels != spec->channels) { + result = SDL_SetError("Wrong number of channels"); + } else if (!*stream_chmap && !chmap) { + // already at default, we're good. + } else if (*stream_chmap && chmap && (SDL_memcmp(*stream_chmap, chmap, sizeof (*chmap) * channels) == 0)) { + // already have this map, don't allocate/copy it again. + } else if (SDL_ChannelMapIsBogus(chmap, channels)) { + result = SDL_SetError("Invalid channel mapping"); + } else { + if (SDL_ChannelMapIsDefault(chmap, channels)) { + chmap = NULL; // just apply a default mapping. + } + if (chmap) { + int *dupmap = SDL_ChannelMapDup(chmap, channels); + if (!dupmap) { + result = SDL_SetError("Invalid channel mapping"); + } else { + SDL_free(*stream_chmap); + *stream_chmap = dupmap; + } + } else { + SDL_free(*stream_chmap); + *stream_chmap = NULL; + } + } + + SDL_UnlockMutex(stream->lock); + return result; +} + +bool SDL_SetAudioStreamInputChannelMap(SDL_AudioStream *stream, const int *chmap, int channels) +{ + return SetAudioStreamChannelMap(stream, &stream->src_spec, &stream->src_chmap, chmap, channels, 1); +} + +bool SDL_SetAudioStreamOutputChannelMap(SDL_AudioStream *stream, const int *chmap, int channels) +{ + return SetAudioStreamChannelMap(stream, &stream->dst_spec, &stream->dst_chmap, chmap, channels, 0); +} + +int *SDL_GetAudioStreamInputChannelMap(SDL_AudioStream *stream, int *count) +{ + int *result = NULL; + int channels = 0; + if (stream) { + SDL_LockMutex(stream->lock); + channels = stream->src_spec.channels; + result = SDL_ChannelMapDup(stream->src_chmap, channels); + SDL_UnlockMutex(stream->lock); + } + + if (count) { + *count = channels; + } + + return result; +} + +int *SDL_GetAudioStreamOutputChannelMap(SDL_AudioStream *stream, int *count) +{ + int *result = NULL; + int channels = 0; + if (stream) { + SDL_LockMutex(stream->lock); + channels = stream->dst_spec.channels; + result = SDL_ChannelMapDup(stream->dst_chmap, channels); + SDL_UnlockMutex(stream->lock); + } + + if (count) { + *count = channels; + } + + return result; +} + +float SDL_GetAudioStreamFrequencyRatio(SDL_AudioStream *stream) +{ + if (!stream) { + SDL_InvalidParamError("stream"); + return 0.0f; + } + + SDL_LockMutex(stream->lock); + const float freq_ratio = stream->freq_ratio; + SDL_UnlockMutex(stream->lock); + + return freq_ratio; +} + +bool SDL_SetAudioStreamFrequencyRatio(SDL_AudioStream *stream, float freq_ratio) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + + // Picked mostly arbitrarily. + const float min_freq_ratio = 0.01f; + const float max_freq_ratio = 100.0f; + + if (freq_ratio < min_freq_ratio) { + return SDL_SetError("Frequency ratio is too low"); + } else if (freq_ratio > max_freq_ratio) { + return SDL_SetError("Frequency ratio is too high"); + } + + SDL_LockMutex(stream->lock); + stream->freq_ratio = freq_ratio; + SDL_UnlockMutex(stream->lock); + + return true; +} + +float SDL_GetAudioStreamGain(SDL_AudioStream *stream) +{ + if (!stream) { + SDL_InvalidParamError("stream"); + return -1.0f; + } + + SDL_LockMutex(stream->lock); + const float gain = stream->gain; + SDL_UnlockMutex(stream->lock); + + return gain; +} + +bool SDL_SetAudioStreamGain(SDL_AudioStream *stream, float gain) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } else if (gain < 0.0f) { + return SDL_InvalidParamError("gain"); + } + + SDL_LockMutex(stream->lock); + stream->gain = gain; + SDL_UnlockMutex(stream->lock); + + return true; +} + +static bool CheckAudioStreamIsFullySetup(SDL_AudioStream *stream) +{ + if (stream->src_spec.format == 0) { + return SDL_SetError("Stream has no source format"); + } else if (stream->dst_spec.format == 0) { + 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) +{ +#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); + + if (!track) { + SDL_UnlockMutex(stream->lock); + return false; + } + } + + const int prev_available = stream->put_callback ? SDL_GetAudioStreamAvailable(stream) : 0; + + bool result = true; + + if (track) { + SDL_AddTrackToAudioQueue(stream->queue, track); + } else { + result = SDL_WriteToAudioQueue(stream->queue, &stream->src_spec, stream->src_chmap, (const Uint8 *)buf, len); + } + + if (result) { + if (stream->put_callback) { + const int newavail = SDL_GetAudioStreamAvailable(stream) - prev_available; + stream->put_callback(stream->put_callback_userdata, stream, newavail, newavail); + } + } + + SDL_UnlockMutex(stream->lock); + + return result; +} + +static void SDLCALL FreeAllocatedAudioBuffer(void *userdata, const void *buf, int len) +{ + SDL_free((void*) buf); +} + +bool SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } else if (!buf) { + return SDL_InvalidParamError("buf"); + } else if (len < 0) { + return SDL_InvalidParamError("len"); + } else if (len == 0) { + return true; // nothing to do. + } + + // When copying in large amounts of data, try and do as much work as possible + // outside of the stream lock, otherwise the output device is likely to be starved. + const int large_input_thresh = 64 * 1024; + + if (len >= large_input_thresh) { + void *data = SDL_malloc(len); + + if (!data) { + return false; + } + + SDL_memcpy(data, buf, len); + buf = data; + + bool ret = PutAudioStreamBuffer(stream, buf, len, FreeAllocatedAudioBuffer, NULL); + if (!ret) { + SDL_free(data); + } + return ret; + } + + return PutAudioStreamBuffer(stream, buf, len, NULL, NULL); +} + +bool SDL_FlushAudioStream(SDL_AudioStream *stream) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + + SDL_LockMutex(stream->lock); + SDL_FlushAudioQueue(stream->queue); + SDL_UnlockMutex(stream->lock); + + return true; +} + +/* this does not save the previous contents of stream->work_buffer. It's a work buffer!! + The returned buffer is aligned/padded for use with SIMD instructions. */ +static Uint8 *EnsureAudioStreamWorkBufferSize(SDL_AudioStream *stream, size_t newlen) +{ + if (stream->work_buffer_allocation >= newlen) { + return stream->work_buffer; + } + + Uint8 *ptr = (Uint8 *) SDL_aligned_alloc(SDL_GetSIMDAlignment(), newlen); + if (!ptr) { + return NULL; // previous work buffer is still valid! + } + + SDL_aligned_free(stream->work_buffer); + stream->work_buffer = ptr; + stream->work_buffer_allocation = newlen; + return ptr; +} + +static Sint64 NextAudioStreamIter(SDL_AudioStream* stream, void** inout_iter, + Sint64* inout_resample_offset, SDL_AudioSpec* out_spec, int **out_chmap, bool* out_flushed) +{ + SDL_AudioSpec spec; + bool flushed; + int *chmap; + size_t queued_bytes = SDL_NextAudioQueueIter(stream->queue, inout_iter, &spec, &chmap, &flushed); + + if (out_spec) { + SDL_copyp(out_spec, &spec); + } + + if (out_chmap) { + *out_chmap = chmap; + } + + // There is infinite audio available, whether or not we are resampling + if (queued_bytes == SDL_SIZE_MAX) { + *inout_resample_offset = 0; + + if (out_flushed) { + *out_flushed = false; + } + + return SDL_MAX_SINT32; + } + + Sint64 resample_offset = *inout_resample_offset; + Sint64 resample_rate = GetAudioStreamResampleRate(stream, spec.freq, resample_offset); + Sint64 output_frames = (Sint64)(queued_bytes / SDL_AUDIO_FRAMESIZE(spec)); + + if (resample_rate) { + // Resampling requires padding frames to the left and right of the current position. + // Past the end of the track, the right padding is filled with silence. + // But we only want to do that if the track is actually finished (flushed). + if (!flushed) { + output_frames -= SDL_GetResamplerPaddingFrames(resample_rate); + } + + output_frames = SDL_GetResamplerOutputFrames(output_frames, resample_rate, &resample_offset); + } + + if (flushed) { + resample_offset = 0; + } + + *inout_resample_offset = resample_offset; + + if (out_flushed) { + *out_flushed = flushed; + } + + return output_frames; +} + +static Sint64 GetAudioStreamAvailableFrames(SDL_AudioStream* stream, Sint64* out_resample_offset) +{ + void* iter = SDL_BeginAudioQueueIter(stream->queue); + + Sint64 resample_offset = stream->resample_offset; + Sint64 output_frames = 0; + + while (iter) { + output_frames += NextAudioStreamIter(stream, &iter, &resample_offset, NULL, NULL, NULL); + + // Already got loads of frames. Just clamp it to something reasonable + if (output_frames >= SDL_MAX_SINT32) { + output_frames = SDL_MAX_SINT32; + break; + } + } + + if (out_resample_offset) { + *out_resample_offset = resample_offset; + } + + return output_frames; +} + +static Sint64 GetAudioStreamHead(SDL_AudioStream* stream, SDL_AudioSpec* out_spec, int **out_chmap, bool* out_flushed) +{ + void* iter = SDL_BeginAudioQueueIter(stream->queue); + + if (!iter) { + SDL_zerop(out_spec); + *out_flushed = false; + return 0; + } + + Sint64 resample_offset = stream->resample_offset; + return NextAudioStreamIter(stream, &iter, &resample_offset, out_spec, out_chmap, out_flushed); +} + +// You must hold stream->lock and validate your parameters before calling this! +// Enough input data MUST be available! +static bool GetAudioStreamDataInternal(SDL_AudioStream *stream, void *buf, int output_frames, float gain) +{ + const SDL_AudioSpec* src_spec = &stream->input_spec; + const SDL_AudioSpec* dst_spec = &stream->dst_spec; + + const SDL_AudioFormat src_format = src_spec->format; + const int src_channels = src_spec->channels; + + const SDL_AudioFormat dst_format = dst_spec->format; + const int dst_channels = dst_spec->channels; + const int *dst_map = stream->dst_chmap; + + const int max_frame_size = CalculateMaxFrameSize(src_format, src_channels, dst_format, dst_channels); + const Sint64 resample_rate = GetAudioStreamResampleRate(stream, src_spec->freq, stream->resample_offset); + +#if DEBUG_AUDIOSTREAM + SDL_Log("AUDIOSTREAM: asking for %d frames.", output_frames); +#endif + + SDL_assert(output_frames > 0); + + // Not resampling? It's an easy conversion (and maybe not even that!) + if (resample_rate == 0) { + Uint8* work_buffer = NULL; + + // Ensure we have enough scratch space for any conversions + if ((src_format != dst_format) || (src_channels != dst_channels) || (gain != 1.0f)) { + work_buffer = EnsureAudioStreamWorkBufferSize(stream, output_frames * max_frame_size); + + if (!work_buffer) { + return false; + } + } + + if (SDL_ReadFromAudioQueue(stream->queue, (Uint8 *)buf, dst_format, dst_channels, dst_map, 0, output_frames, 0, work_buffer, gain) != buf) { + return SDL_SetError("Not enough data in queue"); + } + + return true; + } + + // Time to do some resampling! + // Calculate the number of input frames necessary for this request. + // Because resampling happens "between" frames, The same number of output_frames + // can require a different number of input_frames, depending on the resample_offset. + // In fact, input_frames can sometimes even be zero when upsampling. + const int input_frames = (int) SDL_GetResamplerInputFrames(output_frames, resample_rate, stream->resample_offset); + + const int padding_frames = SDL_GetResamplerPaddingFrames(resample_rate); + + const SDL_AudioFormat resample_format = SDL_AUDIO_F32; + + // If increasing channels, do it after resampling, since we'd just + // do more work to resample duplicate channels. If we're decreasing, do + // it first so we resample the interpolated data instead of interpolating + // the resampled data. + const int resample_channels = SDL_min(src_channels, dst_channels); + + // The size of the frame used when resampling + const int resample_frame_size = SDL_AUDIO_BYTESIZE(resample_format) * resample_channels; + + // The main portion of the work_buffer can be used to store 3 things: + // src_sample_frame_size * (left_padding+input_buffer+right_padding) + // resample_frame_size * (left_padding+input_buffer+right_padding) + // dst_sample_frame_size * output_frames + // + // ResampleAudio also requires an additional buffer if it can't write straight to the output: + // resample_frame_size * output_frames + // + // Note, ConvertAudio requires (num_frames * max_sample_frame_size) of scratch space + const int work_buffer_frames = input_frames + (padding_frames * 2); + int work_buffer_capacity = work_buffer_frames * max_frame_size; + int resample_buffer_offset = -1; + + // Check if we can resample directly into the output buffer. + // Note, this is just to avoid extra copies. + // Some other formats may fit directly into the output buffer, but i'd rather process data in a SIMD-aligned buffer. + if ((dst_format != resample_format) || (dst_channels != resample_channels)) { + // Allocate space for converting the resampled output to the destination format + int resample_convert_bytes = output_frames * max_frame_size; + work_buffer_capacity = SDL_max(work_buffer_capacity, resample_convert_bytes); + + // SIMD-align the buffer + int simd_alignment = (int) SDL_GetSIMDAlignment(); + work_buffer_capacity += simd_alignment - 1; + work_buffer_capacity -= work_buffer_capacity % simd_alignment; + + // Allocate space for the resampled output + int resample_bytes = output_frames * resample_frame_size; + resample_buffer_offset = work_buffer_capacity; + work_buffer_capacity += resample_bytes; + } + + Uint8* work_buffer = EnsureAudioStreamWorkBufferSize(stream, work_buffer_capacity); + + if (!work_buffer) { + return false; + } + + // adjust gain either before resampling or after, depending on which point has less + // samples to process. + const float preresample_gain = (input_frames > output_frames) ? 1.0f : gain; + const float postresample_gain = (input_frames > output_frames) ? gain : 1.0f; + + // (dst channel map is NULL because we'll do the final swizzle on ConvertAudio after resample.) + const Uint8* input_buffer = SDL_ReadFromAudioQueue(stream->queue, + NULL, resample_format, resample_channels, NULL, + padding_frames, input_frames, padding_frames, work_buffer, preresample_gain); + + if (!input_buffer) { + return SDL_SetError("Not enough data in queue (resample)"); + } + + input_buffer += padding_frames * resample_frame_size; + + // Decide where the resampled output goes + void* resample_buffer = (resample_buffer_offset != -1) ? (work_buffer + resample_buffer_offset) : buf; + + SDL_ResampleAudio(resample_channels, + (const float *) input_buffer, input_frames, + (float*) resample_buffer, output_frames, + resample_rate, &stream->resample_offset); + + // Convert to the final format, if necessary (src channel map is NULL because SDL_ReadFromAudioQueue already handled this). + ConvertAudio(output_frames, resample_buffer, resample_format, resample_channels, NULL, buf, dst_format, dst_channels, dst_map, work_buffer, postresample_gain); + + return true; +} + +// get converted/resampled data from the stream +int SDL_GetAudioStreamDataAdjustGain(SDL_AudioStream *stream, void *voidbuf, int len, float extra_gain) +{ + Uint8 *buf = (Uint8 *) voidbuf; + +#if DEBUG_AUDIOSTREAM + SDL_Log("AUDIOSTREAM: want to get %d converted bytes", len); +#endif + + if (!stream) { + SDL_InvalidParamError("stream"); + return -1; + } else if (!buf) { + SDL_InvalidParamError("buf"); + return -1; + } else if (len < 0) { + SDL_InvalidParamError("len"); + return -1; + } else if (len == 0) { + return 0; // nothing to do. + } + + SDL_LockMutex(stream->lock); + + if (!CheckAudioStreamIsFullySetup(stream)) { + SDL_UnlockMutex(stream->lock); + return -1; + } + + const float gain = stream->gain * extra_gain; + const int dst_frame_size = SDL_AUDIO_FRAMESIZE(stream->dst_spec); + + len -= len % dst_frame_size; // chop off any fractional sample frame. + + // give the callback a chance to fill in more stream data if it wants. + if (stream->get_callback) { + Sint64 total_request = len / dst_frame_size; // start with sample frames desired + Sint64 additional_request = total_request; + + Sint64 resample_offset = 0; + Sint64 available_frames = GetAudioStreamAvailableFrames(stream, &resample_offset); + + additional_request -= SDL_min(additional_request, available_frames); + + Sint64 resample_rate = GetAudioStreamResampleRate(stream, stream->src_spec.freq, resample_offset); + + if (resample_rate) { + total_request = SDL_GetResamplerInputFrames(total_request, resample_rate, resample_offset); + additional_request = SDL_GetResamplerInputFrames(additional_request, resample_rate, resample_offset); + } + + total_request *= SDL_AUDIO_FRAMESIZE(stream->src_spec); // convert sample frames to bytes. + additional_request *= SDL_AUDIO_FRAMESIZE(stream->src_spec); // convert sample frames to bytes. + stream->get_callback(stream->get_callback_userdata, stream, (int) SDL_min(additional_request, SDL_INT_MAX), (int) SDL_min(total_request, SDL_INT_MAX)); + } + + // Process the data in chunks to avoid allocating too much memory (and potential integer overflows) + const int chunk_size = 4096; + + int total = 0; + + while (total < len) { + // Audio is processed a track at a time. + SDL_AudioSpec input_spec; + int *input_chmap; + bool flushed; + const Sint64 available_frames = GetAudioStreamHead(stream, &input_spec, &input_chmap, &flushed); + + if (available_frames == 0) { + if (flushed) { + SDL_PopAudioQueueHead(stream->queue); + SDL_zero(stream->input_spec); + stream->resample_offset = 0; + stream->input_chmap = NULL; + continue; + } + // There are no frames available, but the track hasn't been flushed, so more might be added later. + break; + } + + if (!UpdateAudioStreamInputSpec(stream, &input_spec, input_chmap)) { + total = total ? total : -1; + break; + } + + // Clamp the output length to the maximum currently available. + // GetAudioStreamDataInternal requires enough input data is available. + int output_frames = (len - total) / dst_frame_size; + output_frames = SDL_min(output_frames, chunk_size); + output_frames = (int) SDL_min(output_frames, available_frames); + + if (!GetAudioStreamDataInternal(stream, &buf[total], output_frames, gain)) { + total = total ? total : -1; + break; + } + + total += output_frames * dst_frame_size; + } + + SDL_UnlockMutex(stream->lock); + +#if DEBUG_AUDIOSTREAM + SDL_Log("AUDIOSTREAM: Final result was %d", total); +#endif + + return total; +} + +int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *voidbuf, int len) +{ + return SDL_GetAudioStreamDataAdjustGain(stream, voidbuf, len, 1.0f); +} + +// number of converted/resampled bytes available for output +int SDL_GetAudioStreamAvailable(SDL_AudioStream *stream) +{ + if (!stream) { + SDL_InvalidParamError("stream"); + return -1; + } + + SDL_LockMutex(stream->lock); + + if (!CheckAudioStreamIsFullySetup(stream)) { + SDL_UnlockMutex(stream->lock); + return 0; + } + + Sint64 count = GetAudioStreamAvailableFrames(stream, NULL); + + // convert from sample frames to bytes in destination format. + count *= SDL_AUDIO_FRAMESIZE(stream->dst_spec); + + SDL_UnlockMutex(stream->lock); + + // if this overflows an int, just clamp it to a maximum. + return (int) SDL_min(count, SDL_INT_MAX); +} + +// number of sample frames that are currently queued as input. +int SDL_GetAudioStreamQueued(SDL_AudioStream *stream) +{ + if (!stream) { + SDL_InvalidParamError("stream"); + return -1; + } + + SDL_LockMutex(stream->lock); + + size_t total = SDL_GetAudioQueueQueued(stream->queue); + + SDL_UnlockMutex(stream->lock); + + // if this overflows an int, just clamp it to a maximum. + return (int) SDL_min(total, SDL_INT_MAX); +} + +bool SDL_ClearAudioStream(SDL_AudioStream *stream) +{ + if (!stream) { + return SDL_InvalidParamError("stream"); + } + + SDL_LockMutex(stream->lock); + + SDL_ClearAudioQueue(stream->queue); + SDL_zero(stream->input_spec); + stream->input_chmap = NULL; + stream->resample_offset = 0; + + SDL_UnlockMutex(stream->lock); + return true; +} + +void SDL_DestroyAudioStream(SDL_AudioStream *stream) +{ + if (!stream) { + return; + } + + SDL_DestroyProperties(stream->props); + + OnAudioStreamDestroy(stream); + + const bool simplified = stream->simplified; + if (simplified) { + if (stream->bound_device) { + SDL_assert(stream->bound_device->simplified); + SDL_CloseAudioDevice(stream->bound_device->instance_id); // this will unbind the stream. + } + } else { + SDL_UnbindAudioStream(stream); + } + + SDL_aligned_free(stream->work_buffer); + SDL_DestroyAudioQueue(stream->queue); + SDL_DestroyMutex(stream->lock); + + SDL_free(stream); +} + +static void SDLCALL DontFreeThisAudioBuffer(void *userdata, const void *buf, int len) +{ + // We don't own the buffer, but know it will outlive the stream +} + +bool SDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data, int src_len, const SDL_AudioSpec *dst_spec, Uint8 **dst_data, int *dst_len) +{ + if (dst_data) { + *dst_data = NULL; + } + + if (dst_len) { + *dst_len = 0; + } + + if (!src_data) { + return SDL_InvalidParamError("src_data"); + } else if (src_len < 0) { + return SDL_InvalidParamError("src_len"); + } else if (!dst_data) { + return SDL_InvalidParamError("dst_data"); + } else if (!dst_len) { + return SDL_InvalidParamError("dst_len"); + } + + bool result = false; + Uint8 *dst = NULL; + int dstlen = 0; + + SDL_AudioStream *stream = SDL_CreateAudioStream(src_spec, dst_spec); + if (stream) { + if (PutAudioStreamBuffer(stream, src_data, src_len, DontFreeThisAudioBuffer, NULL) && + SDL_FlushAudioStream(stream)) { + dstlen = SDL_GetAudioStreamAvailable(stream); + if (dstlen >= 0) { + dst = (Uint8 *)SDL_malloc(dstlen); + if (dst) { + result = (SDL_GetAudioStreamData(stream, dst, dstlen) == dstlen); + } + } + } + } + + if (result) { + *dst_data = dst; + *dst_len = dstlen; + } else { + SDL_free(dst); + } + + SDL_DestroyAudioStream(stream); + return result; +} diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audiodev.c b/contrib/SDL-3.2.8/src/audio/SDL_audiodev.c new file mode 100644 index 0000000..623a380 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audiodev.c @@ -0,0 +1,124 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +// Get the name of the audio device we use for output + +#if defined(SDL_AUDIO_DRIVER_NETBSD) || defined(SDL_AUDIO_DRIVER_OSS) + +#include +#include +#include +#include // For close() + +#include "SDL_audiodev_c.h" + +#ifndef SDL_PATH_DEV_DSP +#if defined(SDL_PLATFORM_NETBSD) || defined(SDL_PLATFORM_OPENBSD) +#define SDL_PATH_DEV_DSP "/dev/audio" +#else +#define SDL_PATH_DEV_DSP "/dev/dsp" +#endif +#endif +#ifndef SDL_PATH_DEV_DSP24 +#define SDL_PATH_DEV_DSP24 "/dev/sound/dsp" +#endif +#ifndef SDL_PATH_DEV_AUDIO +#define SDL_PATH_DEV_AUDIO "/dev/audio" +#endif + +static void test_device(const bool recording, const char *fname, int flags, bool (*test)(int fd)) +{ + struct stat sb; + const int audio_fd = open(fname, flags | O_CLOEXEC, 0); + if (audio_fd >= 0) { + if ((fstat(audio_fd, &sb) == 0) && (S_ISCHR(sb.st_mode))) { + const bool okay = test(audio_fd); + close(audio_fd); + if (okay) { + static size_t dummyhandle = 0; + dummyhandle++; + SDL_assert(dummyhandle != 0); + + /* Note that spec is NULL; while we are opening the device + * endpoint here, the endpoint does not provide any mix format + * information, making this information inaccessible at + * enumeration time + */ + SDL_AddAudioDevice(recording, fname, NULL, (void *)(uintptr_t)dummyhandle); + } + } else { + close(audio_fd); + } + } +} + +static bool test_stub(int fd) +{ + return true; +} + +static void SDL_EnumUnixAudioDevices_Internal(const bool recording, const bool classic, bool (*test)(int)) +{ + const int flags = recording ? OPEN_FLAGS_INPUT : OPEN_FLAGS_OUTPUT; + const char *audiodev; + char audiopath[1024]; + + if (!test) { + test = test_stub; + } + + // Figure out what our audio device is + audiodev = SDL_getenv("AUDIODEV"); + if (!audiodev) { + if (classic) { + audiodev = SDL_PATH_DEV_AUDIO; + } else { + struct stat sb; + + // Added support for /dev/sound/\* in Linux 2.4 + if (((stat("/dev/sound", &sb) == 0) && S_ISDIR(sb.st_mode)) && ((stat(SDL_PATH_DEV_DSP24, &sb) == 0) && S_ISCHR(sb.st_mode))) { + audiodev = SDL_PATH_DEV_DSP24; + } else { + audiodev = SDL_PATH_DEV_DSP; + } + } + } + test_device(recording, audiodev, flags, test); + + if (SDL_strlen(audiodev) < (sizeof(audiopath) - 3)) { + int instance = 0; + while (instance <= 64) { + (void)SDL_snprintf(audiopath, SDL_arraysize(audiopath), + "%s%d", audiodev, instance); + instance++; + test_device(recording, audiopath, flags, test); + } + } +} + +void SDL_EnumUnixAudioDevices(const bool classic, bool (*test)(int)) +{ + SDL_EnumUnixAudioDevices_Internal(true, classic, test); + SDL_EnumUnixAudioDevices_Internal(false, classic, test); +} + +#endif // Audio device selection diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audiodev_c.h b/contrib/SDL-3.2.8/src/audio/SDL_audiodev_c.h new file mode 100644 index 0000000..ded13fc --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audiodev_c.h @@ -0,0 +1,41 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef SDL_audiodev_c_h_ +#define SDL_audiodev_c_h_ + +#include "SDL_internal.h" +#include "SDL_sysaudio.h" + +// Open the audio device for playback, and don't block if busy +//#define USE_BLOCKING_WRITES + +#ifdef USE_BLOCKING_WRITES +#define OPEN_FLAGS_OUTPUT O_WRONLY +#define OPEN_FLAGS_INPUT O_RDONLY +#else +#define OPEN_FLAGS_OUTPUT (O_WRONLY | O_NONBLOCK) +#define OPEN_FLAGS_INPUT (O_RDONLY | O_NONBLOCK) +#endif + +extern void SDL_EnumUnixAudioDevices(const bool classic, bool (*test)(int)); + +#endif // SDL_audiodev_c_h_ diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audioqueue.c b/contrib/SDL-3.2.8/src/audio/SDL_audioqueue.c new file mode 100644 index 0000000..df7cac8 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audioqueue.c @@ -0,0 +1,652 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_audioqueue.h" +#include "SDL_sysaudio.h" + +typedef struct SDL_MemoryPool SDL_MemoryPool; + +struct SDL_MemoryPool +{ + void *free_blocks; + size_t block_size; + size_t num_free; + size_t max_free; +}; + +struct SDL_AudioTrack +{ + SDL_AudioSpec spec; + int *chmap; + bool flushed; + SDL_AudioTrack *next; + + void *userdata; + SDL_ReleaseAudioBufferCallback callback; + + Uint8 *data; + size_t head; + size_t tail; + size_t capacity; + + int chmap_storage[SDL_MAX_CHANNELMAP_CHANNELS]; // !!! FIXME: this needs to grow if SDL ever supports more channels. But if it grows, we should probably be more clever about allocations. +}; + +struct SDL_AudioQueue +{ + SDL_AudioTrack *head; + SDL_AudioTrack *tail; + + Uint8 *history_buffer; + size_t history_length; + size_t history_capacity; + + SDL_MemoryPool track_pool; + SDL_MemoryPool chunk_pool; +}; + +// Allocate a new block, avoiding checking for ones already in the pool +static void *AllocNewMemoryPoolBlock(const SDL_MemoryPool *pool) +{ + return SDL_malloc(pool->block_size); +} + +// Allocate a new block, first checking if there are any in the pool +static void *AllocMemoryPoolBlock(SDL_MemoryPool *pool) +{ + if (pool->num_free == 0) { + return AllocNewMemoryPoolBlock(pool); + } + + void *block = pool->free_blocks; + pool->free_blocks = *(void **)block; + --pool->num_free; + return block; +} + +// Free a block, or add it to the pool if there's room +static void FreeMemoryPoolBlock(SDL_MemoryPool *pool, void *block) +{ + if (pool->num_free < pool->max_free) { + *(void **)block = pool->free_blocks; + pool->free_blocks = block; + ++pool->num_free; + } else { + SDL_free(block); + } +} + +// Destroy a pool and all of its blocks +static void DestroyMemoryPool(SDL_MemoryPool *pool) +{ + void *block = pool->free_blocks; + pool->free_blocks = NULL; + pool->num_free = 0; + + while (block) { + void *next = *(void **)block; + SDL_free(block); + block = next; + } +} + +// Keeping a list of free chunks reduces memory allocations, +// But also increases the amount of work to perform when freeing the track. +static void InitMemoryPool(SDL_MemoryPool *pool, size_t block_size, size_t max_free) +{ + SDL_zerop(pool); + + SDL_assert(block_size >= sizeof(void *)); + pool->block_size = block_size; + pool->max_free = max_free; +} + +// Allocates a number of blocks and adds them to the pool +static bool ReserveMemoryPoolBlocks(SDL_MemoryPool *pool, size_t num_blocks) +{ + for (; num_blocks; --num_blocks) { + void *block = AllocNewMemoryPoolBlock(pool); + + if (block == NULL) { + return false; + } + + *(void **)block = pool->free_blocks; + pool->free_blocks = block; + ++pool->num_free; + } + + return true; +} + +void SDL_DestroyAudioQueue(SDL_AudioQueue *queue) +{ + SDL_ClearAudioQueue(queue); + + DestroyMemoryPool(&queue->track_pool); + DestroyMemoryPool(&queue->chunk_pool); + SDL_aligned_free(queue->history_buffer); + + SDL_free(queue); +} + +SDL_AudioQueue *SDL_CreateAudioQueue(size_t chunk_size) +{ + SDL_AudioQueue *queue = (SDL_AudioQueue *)SDL_calloc(1, sizeof(*queue)); + + if (!queue) { + return NULL; + } + + InitMemoryPool(&queue->track_pool, sizeof(SDL_AudioTrack), 8); + InitMemoryPool(&queue->chunk_pool, chunk_size, 4); + + if (!ReserveMemoryPoolBlocks(&queue->track_pool, 2)) { + SDL_DestroyAudioQueue(queue); + return NULL; + } + + return queue; +} + +static void DestroyAudioTrack(SDL_AudioQueue *queue, SDL_AudioTrack *track) +{ + track->callback(track->userdata, track->data, (int)track->capacity); + + FreeMemoryPoolBlock(&queue->track_pool, track); +} + +void SDL_ClearAudioQueue(SDL_AudioQueue *queue) +{ + SDL_AudioTrack *track = queue->head; + + queue->head = NULL; + queue->tail = NULL; + queue->history_length = 0; + + while (track) { + SDL_AudioTrack *next = track->next; + DestroyAudioTrack(queue, track); + track = next; + } +} + +static void FlushAudioTrack(SDL_AudioTrack *track) +{ + track->flushed = true; +} + +void SDL_FlushAudioQueue(SDL_AudioQueue *queue) +{ + SDL_AudioTrack *track = queue->tail; + + if (track) { + FlushAudioTrack(track); + } +} + +void SDL_PopAudioQueueHead(SDL_AudioQueue *queue) +{ + SDL_AudioTrack *track = queue->head; + + for (;;) { + bool flushed = track->flushed; + + SDL_AudioTrack *next = track->next; + DestroyAudioTrack(queue, track); + track = next; + + if (flushed) { + break; + } + } + + queue->head = track; + queue->history_length = 0; + + if (!track) { + queue->tail = NULL; + } +} + +SDL_AudioTrack *SDL_CreateAudioTrack( + SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const int *chmap, + Uint8 *data, size_t len, size_t capacity, + SDL_ReleaseAudioBufferCallback callback, void *userdata) +{ + SDL_AudioTrack *track = (SDL_AudioTrack *)AllocMemoryPoolBlock(&queue->track_pool); + + if (!track) { + return NULL; + } + + SDL_zerop(track); + + if (chmap) { + SDL_assert(SDL_arraysize(track->chmap_storage) >= spec->channels); + SDL_memcpy(track->chmap_storage, chmap, sizeof (*chmap) * spec->channels); + track->chmap = track->chmap_storage; + } + + SDL_copyp(&track->spec, spec); + + track->userdata = userdata; + track->callback = callback; + track->data = data; + track->head = 0; + track->tail = len; + track->capacity = capacity; + + return track; +} + +static void SDLCALL FreeChunkedAudioBuffer(void *userdata, const void *buf, int len) +{ + SDL_AudioQueue *queue = (SDL_AudioQueue *)userdata; + + FreeMemoryPoolBlock(&queue->chunk_pool, (void *)buf); +} + +static SDL_AudioTrack *CreateChunkedAudioTrack(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const int *chmap) +{ + Uint8 *chunk = (Uint8 *)AllocMemoryPoolBlock(&queue->chunk_pool); + + if (!chunk) { + return NULL; + } + + size_t capacity = queue->chunk_pool.block_size; + capacity -= capacity % SDL_AUDIO_FRAMESIZE(*spec); + + SDL_AudioTrack *track = SDL_CreateAudioTrack(queue, spec, chmap, chunk, 0, capacity, FreeChunkedAudioBuffer, queue); + + if (!track) { + FreeMemoryPoolBlock(&queue->chunk_pool, chunk); + return NULL; + } + + return track; +} + +void SDL_AddTrackToAudioQueue(SDL_AudioQueue *queue, SDL_AudioTrack *track) +{ + SDL_AudioTrack *tail = queue->tail; + + if (tail) { + // If the spec has changed, make sure to flush the previous track + if (!SDL_AudioSpecsEqual(&tail->spec, &track->spec, tail->chmap, track->chmap)) { + FlushAudioTrack(tail); + } + + tail->next = track; + } else { + queue->head = track; + } + + queue->tail = track; +} + +static size_t WriteToAudioTrack(SDL_AudioTrack *track, const Uint8 *data, size_t len) +{ + if (track->flushed || track->tail >= track->capacity) { + return 0; + } + + len = SDL_min(len, track->capacity - track->tail); + SDL_memcpy(&track->data[track->tail], data, len); + track->tail += len; + + return len; +} + +bool SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const int *chmap, const Uint8 *data, size_t len) +{ + if (len == 0) { + return true; + } + + SDL_AudioTrack *track = queue->tail; + + if (track) { + if (!SDL_AudioSpecsEqual(&track->spec, spec, track->chmap, chmap)) { + FlushAudioTrack(track); + } + } else { + SDL_assert(!queue->head); + track = CreateChunkedAudioTrack(queue, spec, chmap); + + if (!track) { + return false; + } + + queue->head = track; + queue->tail = track; + } + + for (;;) { + const size_t written = WriteToAudioTrack(track, data, len); + data += written; + len -= written; + + if (len == 0) { + break; + } + + SDL_AudioTrack *new_track = CreateChunkedAudioTrack(queue, spec, chmap); + + if (!new_track) { + return false; + } + + track->next = new_track; + queue->tail = new_track; + track = new_track; + } + + return true; +} + +void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue) +{ + return queue->head; +} + +size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_AudioSpec *out_spec, int **out_chmap, bool *out_flushed) +{ + SDL_AudioTrack *iter = (SDL_AudioTrack *)(*inout_iter); + SDL_assert(iter != NULL); + + SDL_copyp(out_spec, &iter->spec); + *out_chmap = iter->chmap; + + bool flushed = false; + size_t queued_bytes = 0; + + while (iter) { + SDL_AudioTrack *track = iter; + iter = iter->next; + + size_t avail = track->tail - track->head; + + if (avail >= SDL_SIZE_MAX - queued_bytes) { + queued_bytes = SDL_SIZE_MAX; + flushed = false; + break; + } + + queued_bytes += avail; + flushed = track->flushed; + + if (flushed) { + break; + } + } + + *inout_iter = iter; + *out_flushed = flushed; + + return queued_bytes; +} + +static const Uint8 *PeekIntoAudioQueuePast(SDL_AudioQueue *queue, Uint8 *data, size_t len) +{ + SDL_AudioTrack *track = queue->head; + + if (track->head >= len) { + return &track->data[track->head - len]; + } + + size_t past = len - track->head; + + if (past > queue->history_length) { + return NULL; + } + + SDL_memcpy(data, &queue->history_buffer[queue->history_length - past], past); + SDL_memcpy(&data[past], track->data, track->head); + + return data; +} + +static void UpdateAudioQueueHistory(SDL_AudioQueue *queue, + const Uint8 *data, size_t len) +{ + Uint8 *history_buffer = queue->history_buffer; + size_t history_bytes = queue->history_length; + + if (len >= history_bytes) { + SDL_memcpy(history_buffer, &data[len - history_bytes], history_bytes); + } else { + size_t preserve = history_bytes - len; + SDL_memmove(history_buffer, &history_buffer[len], preserve); + SDL_memcpy(&history_buffer[preserve], data, len); + } +} + +static const Uint8 *ReadFromAudioQueue(SDL_AudioQueue *queue, Uint8 *data, size_t len) +{ + SDL_AudioTrack *track = queue->head; + + if (track->tail - track->head >= len) { + const Uint8 *ptr = &track->data[track->head]; + track->head += len; + return ptr; + } + + size_t total = 0; + + for (;;) { + size_t avail = SDL_min(len - total, track->tail - track->head); + SDL_memcpy(&data[total], &track->data[track->head], avail); + track->head += avail; + total += avail; + + if (total == len) { + break; + } + + if (track->flushed) { + SDL_SetError("Reading past end of flushed track"); + return NULL; + } + + SDL_AudioTrack *next = track->next; + + if (!next) { + SDL_SetError("Reading past end of incomplete track"); + return NULL; + } + + UpdateAudioQueueHistory(queue, track->data, track->tail); + + queue->head = next; + DestroyAudioTrack(queue, track); + track = next; + } + + return data; +} + +static const Uint8 *PeekIntoAudioQueueFuture(SDL_AudioQueue *queue, Uint8 *data, size_t len) +{ + SDL_AudioTrack *track = queue->head; + + if (track->tail - track->head >= len) { + return &track->data[track->head]; + } + + size_t total = 0; + + for (;;) { + size_t avail = SDL_min(len - total, track->tail - track->head); + SDL_memcpy(&data[total], &track->data[track->head], avail); + total += avail; + + if (total == len) { + break; + } + + if (track->flushed) { + // If we have run out of data, fill the rest with silence. + SDL_memset(&data[total], SDL_GetSilenceValueForFormat(track->spec.format), len - total); + break; + } + + track = track->next; + + if (!track) { + SDL_SetError("Peeking past end of incomplete track"); + return NULL; + } + } + + return data; +} + +const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue, + Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const int *dst_map, + int past_frames, int present_frames, int future_frames, + Uint8 *scratch, float gain) +{ + SDL_AudioTrack *track = queue->head; + + if (!track) { + return NULL; + } + + SDL_AudioFormat src_format = track->spec.format; + int src_channels = track->spec.channels; + const int *src_map = track->chmap; + + size_t src_frame_size = SDL_AUDIO_BYTESIZE(src_format) * src_channels; + size_t dst_frame_size = SDL_AUDIO_BYTESIZE(dst_format) * dst_channels; + + size_t src_past_bytes = past_frames * src_frame_size; + size_t src_present_bytes = present_frames * src_frame_size; + size_t src_future_bytes = future_frames * src_frame_size; + + size_t dst_past_bytes = past_frames * dst_frame_size; + size_t dst_present_bytes = present_frames * dst_frame_size; + size_t dst_future_bytes = future_frames * dst_frame_size; + + const bool convert = (src_format != dst_format) || (src_channels != dst_channels) || (gain != 1.0f); + + if (convert && !dst) { + // The user didn't ask for the data to be copied, but we need to convert it, so store it in the scratch buffer + dst = scratch; + } + + // Can we get all of the data straight from this track? + if ((track->head >= src_past_bytes) && ((track->tail - track->head) >= (src_present_bytes + src_future_bytes))) { + const Uint8 *ptr = &track->data[track->head - src_past_bytes]; + track->head += src_present_bytes; + + // Do we still need to copy/convert the data? + if (dst) { + ConvertAudio(past_frames + present_frames + future_frames, ptr, + src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain); + ptr = dst; + } + + return ptr; + } + + if (!dst) { + // The user didn't ask for the data to be copied, but we need to, so store it in the scratch buffer + dst = scratch; + } else if (!convert) { + // We are only copying, not converting, so copy straight into the dst buffer + scratch = dst; + } + + Uint8 *ptr = dst; + + if (src_past_bytes) { + ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain); + dst += dst_past_bytes; + scratch += dst_past_bytes; + } + + if (src_present_bytes) { + ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain); + dst += dst_present_bytes; + scratch += dst_present_bytes; + } + + if (src_future_bytes) { + ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, src_map, dst, dst_format, dst_channels, dst_map, scratch, gain); + dst += dst_future_bytes; + scratch += dst_future_bytes; + } + + return ptr; +} + +size_t SDL_GetAudioQueueQueued(SDL_AudioQueue *queue) +{ + size_t total = 0; + void *iter = SDL_BeginAudioQueueIter(queue); + + while (iter) { + SDL_AudioSpec src_spec; + int *src_chmap; + bool flushed; + + size_t avail = SDL_NextAudioQueueIter(queue, &iter, &src_spec, &src_chmap, &flushed); + + if (avail >= SDL_SIZE_MAX - total) { + total = SDL_SIZE_MAX; + break; + } + + total += avail; + } + + return total; +} + +bool SDL_ResetAudioQueueHistory(SDL_AudioQueue *queue, int num_frames) +{ + SDL_AudioTrack *track = queue->head; + + if (!track) { + return false; + } + + size_t length = num_frames * SDL_AUDIO_FRAMESIZE(track->spec); + Uint8 *history_buffer = queue->history_buffer; + + if (queue->history_capacity < length) { + history_buffer = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), length); + if (!history_buffer) { + return false; + } + SDL_aligned_free(queue->history_buffer); + queue->history_buffer = history_buffer; + queue->history_capacity = length; + } + + queue->history_length = length; + SDL_memset(history_buffer, SDL_GetSilenceValueForFormat(track->spec.format), length); + + return true; +} diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audioqueue.h b/contrib/SDL-3.2.8/src/audio/SDL_audioqueue.h new file mode 100644 index 0000000..4662946 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audioqueue.h @@ -0,0 +1,79 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_audioqueue_h_ +#define SDL_audioqueue_h_ + +// Internal functions used by SDL_AudioStream for queueing audio. + +typedef void (SDLCALL *SDL_ReleaseAudioBufferCallback)(void *userdata, const void *buffer, int buflen); + +typedef struct SDL_AudioQueue SDL_AudioQueue; +typedef struct SDL_AudioTrack SDL_AudioTrack; + +// Create a new audio queue +extern SDL_AudioQueue *SDL_CreateAudioQueue(size_t chunk_size); + +// Destroy an audio queue +extern void SDL_DestroyAudioQueue(SDL_AudioQueue *queue); + +// Completely clear the queue +extern void SDL_ClearAudioQueue(SDL_AudioQueue *queue); + +// Mark the last track as flushed +extern void SDL_FlushAudioQueue(SDL_AudioQueue *queue); + +// Pop the current head track +// REQUIRES: The head track must exist, and must have been flushed +extern void SDL_PopAudioQueueHead(SDL_AudioQueue *queue); + +// Write data to the end of queue +// REQUIRES: If the spec has changed, the last track must have been flushed +extern bool SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const int *chmap, const Uint8 *data, size_t len); + +// Create a track where the input data is owned by the caller +extern SDL_AudioTrack *SDL_CreateAudioTrack(SDL_AudioQueue *queue, + const SDL_AudioSpec *spec, const int *chmap, Uint8 *data, size_t len, size_t capacity, + SDL_ReleaseAudioBufferCallback callback, void *userdata); + +// Add a track to the end of the queue +// REQUIRES: `track != NULL` +extern void SDL_AddTrackToAudioQueue(SDL_AudioQueue *queue, SDL_AudioTrack *track); + +// Iterate over the tracks in the queue +extern void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue); + +// Query and update the track iterator +// REQUIRES: `*inout_iter != NULL` (a valid iterator) +extern size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_AudioSpec *out_spec, int **out_chmap, bool *out_flushed); + +extern const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue, + Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels, const int *dst_map, + int past_frames, int present_frames, int future_frames, + Uint8 *scratch, float gain); + +// Get the total number of bytes currently queued +extern size_t SDL_GetAudioQueueQueued(SDL_AudioQueue *queue); + +extern bool SDL_ResetAudioQueueHistory(SDL_AudioQueue *queue, int num_frames); + +#endif // SDL_audioqueue_h_ diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audioresample.c b/contrib/SDL-3.2.8/src/audio/SDL_audioresample.c new file mode 100644 index 0000000..371002e --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audioresample.c @@ -0,0 +1,706 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_sysaudio.h" + +#include "SDL_audioresample.h" + +// SDL's resampler uses a "bandlimited interpolation" algorithm: +// https://ccrma.stanford.edu/~jos/resample/ + +// TODO: Support changing this at runtime? +#if defined(SDL_SSE_INTRINSICS) || defined(SDL_NEON_INTRINSICS) +// In , SSE is basically mandatory anyway +// We want RESAMPLER_SAMPLES_PER_FRAME to be a multiple of 4, to make SIMD easier +#define RESAMPLER_ZERO_CROSSINGS 6 +#else +#define RESAMPLER_ZERO_CROSSINGS 5 +#endif + +#define RESAMPLER_SAMPLES_PER_FRAME (RESAMPLER_ZERO_CROSSINGS * 2) + +// For a given srcpos, `srcpos + frame` are sampled, where `-RESAMPLER_ZERO_CROSSINGS < frame <= RESAMPLER_ZERO_CROSSINGS`. +// Note, when upsampling, it is also possible to start sampling from `srcpos = -1`. +#define RESAMPLER_MAX_PADDING_FRAMES (RESAMPLER_ZERO_CROSSINGS + 1) + +// More bits gives more precision, at the cost of a larger table. +#define RESAMPLER_BITS_PER_ZERO_CROSSING 3 +#define RESAMPLER_SAMPLES_PER_ZERO_CROSSING (1 << RESAMPLER_BITS_PER_ZERO_CROSSING) +#define RESAMPLER_FILTER_INTERP_BITS (32 - RESAMPLER_BITS_PER_ZERO_CROSSING) +#define RESAMPLER_FILTER_INTERP_RANGE (1 << RESAMPLER_FILTER_INTERP_BITS) + +// ResampleFrame is just a vector/matrix/matrix multiplication. +// It performs cubic interpolation of the filter, then multiplies that with the input. +// dst = [1, frac, frac^2, frac^3] * filter * src + +// Cubic Polynomial +typedef union Cubic +{ + float v[4]; + +#ifdef SDL_SSE_INTRINSICS + // Aligned loads can be used directly as memory operands for mul/add + __m128 v128; +#endif + +#ifdef SDL_NEON_INTRINSICS + float32x4_t v128; +#endif + +} Cubic; + +static void ResampleFrame_Generic(const float *src, float *dst, const Cubic *filter, float frac, int chans) +{ + const float frac2 = frac * frac; + const float frac3 = frac * frac2; + + int i, chan; + float scales[RESAMPLER_SAMPLES_PER_FRAME]; + + for (i = 0; i < RESAMPLER_SAMPLES_PER_FRAME; ++i, ++filter) { + scales[i] = filter->v[0] + (filter->v[1] * frac) + (filter->v[2] * frac2) + (filter->v[3] * frac3); + } + + for (chan = 0; chan < chans; ++chan) { + float out = 0.0f; + + for (i = 0; i < RESAMPLER_SAMPLES_PER_FRAME; ++i) { + out += src[i * chans + chan] * scales[i]; + } + + dst[chan] = out; + } +} + +static void ResampleFrame_Mono(const float *src, float *dst, const Cubic *filter, float frac, int chans) +{ + const float frac2 = frac * frac; + const float frac3 = frac * frac2; + + int i; + float out = 0.0f; + + for (i = 0; i < RESAMPLER_SAMPLES_PER_FRAME; ++i, ++filter) { + // Interpolate between the nearest two filters + const float scale = filter->v[0] + (filter->v[1] * frac) + (filter->v[2] * frac2) + (filter->v[3] * frac3); + + out += src[i] * scale; + } + + dst[0] = out; +} + +static void ResampleFrame_Stereo(const float *src, float *dst, const Cubic *filter, float frac, int chans) +{ + const float frac2 = frac * frac; + const float frac3 = frac * frac2; + + int i; + float out0 = 0.0f; + float out1 = 0.0f; + + for (i = 0; i < RESAMPLER_SAMPLES_PER_FRAME; ++i, ++filter) { + // Interpolate between the nearest two filters + const float scale = filter->v[0] + (filter->v[1] * frac) + (filter->v[2] * frac2) + (filter->v[3] * frac3); + + out0 += src[i * 2 + 0] * scale; + out1 += src[i * 2 + 1] * scale; + } + + dst[0] = out0; + dst[1] = out1; +} + +#ifdef SDL_SSE_INTRINSICS +#define sdl_madd_ps(a, b, c) _mm_add_ps(a, _mm_mul_ps(b, c)) // Not-so-fused multiply-add + +static void SDL_TARGETING("sse") ResampleFrame_Generic_SSE(const float *src, float *dst, const Cubic *filter, float frac, int chans) +{ +#if RESAMPLER_SAMPLES_PER_FRAME != 12 +#error Invalid samples per frame +#endif + + __m128 f0, f1, f2; + + { + const __m128 frac1 = _mm_set1_ps(frac); + const __m128 frac2 = _mm_mul_ps(frac1, frac1); + const __m128 frac3 = _mm_mul_ps(frac1, frac2); + +// Transposed in SetupAudioResampler +// Explicitly use _mm_load_ps to workaround ICE in GCC 4.9.4 accessing Cubic.v128 +#define X(out) \ + out = _mm_load_ps(filter[0].v); \ + out = sdl_madd_ps(out, frac1, _mm_load_ps(filter[1].v)); \ + out = sdl_madd_ps(out, frac2, _mm_load_ps(filter[2].v)); \ + out = sdl_madd_ps(out, frac3, _mm_load_ps(filter[3].v)); \ + filter += 4 + + X(f0); + X(f1); + X(f2); + +#undef X + } + + if (chans == 2) { + // Duplicate each of the filter elements and multiply by the input + // Use two accumulators to improve throughput + __m128 out0 = _mm_mul_ps(_mm_loadu_ps(src + 0), _mm_unpacklo_ps(f0, f0)); + __m128 out1 = _mm_mul_ps(_mm_loadu_ps(src + 4), _mm_unpackhi_ps(f0, f0)); + out0 = sdl_madd_ps(out0, _mm_loadu_ps(src + 8), _mm_unpacklo_ps(f1, f1)); + out1 = sdl_madd_ps(out1, _mm_loadu_ps(src + 12), _mm_unpackhi_ps(f1, f1)); + out0 = sdl_madd_ps(out0, _mm_loadu_ps(src + 16), _mm_unpacklo_ps(f2, f2)); + out1 = sdl_madd_ps(out1, _mm_loadu_ps(src + 20), _mm_unpackhi_ps(f2, f2)); + + // Add the accumulators together + __m128 out = _mm_add_ps(out0, out1); + + // Add the lower and upper pairs together + out = _mm_add_ps(out, _mm_movehl_ps(out, out)); + + // Store the result + _mm_storel_pi((__m64 *)dst, out); + return; + } + + if (chans == 1) { + // Multiply the filter by the input + __m128 out = _mm_mul_ps(f0, _mm_loadu_ps(src + 0)); + out = sdl_madd_ps(out, f1, _mm_loadu_ps(src + 4)); + out = sdl_madd_ps(out, f2, _mm_loadu_ps(src + 8)); + + // Horizontal sum + __m128 shuf = _mm_shuffle_ps(out, out, _MM_SHUFFLE(2, 3, 0, 1)); + out = _mm_add_ps(out, shuf); + out = _mm_add_ss(out, _mm_movehl_ps(shuf, out)); + + _mm_store_ss(dst, out); + return; + } + + int chan = 0; + + // Process 4 channels at once + for (; chan + 4 <= chans; chan += 4) { + const float *in = &src[chan]; + __m128 out0 = _mm_setzero_ps(); + __m128 out1 = _mm_setzero_ps(); + +#define X(a, b, out) \ + out = sdl_madd_ps(out, _mm_loadu_ps(in), _mm_shuffle_ps(a, a, _MM_SHUFFLE(b, b, b, b))); \ + in += chans + +#define Y(a) \ + X(a, 0, out0); \ + X(a, 1, out1); \ + X(a, 2, out0); \ + X(a, 3, out1) + + Y(f0); + Y(f1); + Y(f2); + +#undef X +#undef Y + + // Add the accumulators together + __m128 out = _mm_add_ps(out0, out1); + + _mm_storeu_ps(&dst[chan], out); + } + + // Process the remaining channels one at a time. + // Channel counts 1,2,4,8 are already handled above, leaving 3,5,6,7 to deal with (looping 3,1,2,3 times). + // Without vgatherdps (AVX2), this gets quite messy. + for (; chan < chans; ++chan) { + const float *in = &src[chan]; + __m128 v0, v1, v2; + +#define X(x) \ + x = _mm_unpacklo_ps(_mm_load_ss(in), _mm_load_ss(in + chans)); \ + in += chans + chans; \ + x = _mm_movelh_ps(x, _mm_unpacklo_ps(_mm_load_ss(in), _mm_load_ss(in + chans))); \ + in += chans + chans + + X(v0); + X(v1); + X(v2); + +#undef X + + __m128 out = _mm_mul_ps(f0, v0); + out = sdl_madd_ps(out, f1, v1); + out = sdl_madd_ps(out, f2, v2); + + // Horizontal sum + __m128 shuf = _mm_shuffle_ps(out, out, _MM_SHUFFLE(2, 3, 0, 1)); + out = _mm_add_ps(out, shuf); + out = _mm_add_ss(out, _mm_movehl_ps(shuf, out)); + + _mm_store_ss(&dst[chan], out); + } +} + +#undef sdl_madd_ps +#endif + +#ifdef SDL_NEON_INTRINSICS +static void ResampleFrame_Generic_NEON(const float *src, float *dst, const Cubic *filter, float frac, int chans) +{ +#if RESAMPLER_SAMPLES_PER_FRAME != 12 +#error Invalid samples per frame +#endif + + float32x4_t f0, f1, f2; + + { + const float32x4_t frac1 = vdupq_n_f32(frac); + const float32x4_t frac2 = vmulq_f32(frac1, frac1); + const float32x4_t frac3 = vmulq_f32(frac1, frac2); + +// Transposed in SetupAudioResampler +#define X(out) \ + out = vmlaq_f32(vmlaq_f32(vmlaq_f32(filter[0].v128, filter[1].v128, frac1), filter[2].v128, frac2), filter[3].v128, frac3); \ + filter += 4 + + X(f0); + X(f1); + X(f2); + +#undef X + } + + if (chans == 2) { + float32x4x2_t g0 = vzipq_f32(f0, f0); + float32x4x2_t g1 = vzipq_f32(f1, f1); + float32x4x2_t g2 = vzipq_f32(f2, f2); + + // Duplicate each of the filter elements and multiply by the input + // Use two accumulators to improve throughput + float32x4_t out0 = vmulq_f32(vld1q_f32(src + 0), g0.val[0]); + float32x4_t out1 = vmulq_f32(vld1q_f32(src + 4), g0.val[1]); + out0 = vmlaq_f32(out0, vld1q_f32(src + 8), g1.val[0]); + out1 = vmlaq_f32(out1, vld1q_f32(src + 12), g1.val[1]); + out0 = vmlaq_f32(out0, vld1q_f32(src + 16), g2.val[0]); + out1 = vmlaq_f32(out1, vld1q_f32(src + 20), g2.val[1]); + + // Add the accumulators together + out0 = vaddq_f32(out0, out1); + + // Add the lower and upper pairs together + float32x2_t out = vadd_f32(vget_low_f32(out0), vget_high_f32(out0)); + + // Store the result + vst1_f32(dst, out); + return; + } + + if (chans == 1) { + // Multiply the filter by the input + float32x4_t out = vmulq_f32(f0, vld1q_f32(src + 0)); + out = vmlaq_f32(out, f1, vld1q_f32(src + 4)); + out = vmlaq_f32(out, f2, vld1q_f32(src + 8)); + + // Horizontal sum + float32x2_t sum = vadd_f32(vget_low_f32(out), vget_high_f32(out)); + sum = vpadd_f32(sum, sum); + + vst1_lane_f32(dst, sum, 0); + return; + } + + int chan = 0; + + // Process 4 channels at once + for (; chan + 4 <= chans; chan += 4) { + const float *in = &src[chan]; + float32x4_t out0 = vdupq_n_f32(0); + float32x4_t out1 = vdupq_n_f32(0); + +#define X(a, b, out) \ + out = vmlaq_f32(out, vld1q_f32(in), vdupq_lane_f32(a, b)); \ + in += chans + +#define Y(a) \ + X(vget_low_f32(a), 0, out0); \ + X(vget_low_f32(a), 1, out1); \ + X(vget_high_f32(a), 0, out0); \ + X(vget_high_f32(a), 1, out1) + + Y(f0); + Y(f1); + Y(f2); + +#undef X +#undef Y + + // Add the accumulators together + float32x4_t out = vaddq_f32(out0, out1); + + vst1q_f32(&dst[chan], out); + } + + // Process the remaining channels one at a time. + // Channel counts 1,2,4,8 are already handled above, leaving 3,5,6,7 to deal with (looping 3,1,2,3 times). + for (; chan < chans; ++chan) { + const float *in = &src[chan]; + float32x4_t v0, v1, v2; + +#define X(x) \ + x = vld1q_dup_f32(in); \ + in += chans; \ + x = vld1q_lane_f32(in, x, 1); \ + in += chans; \ + x = vld1q_lane_f32(in, x, 2); \ + in += chans; \ + x = vld1q_lane_f32(in, x, 3); \ + in += chans + + X(v0); + X(v1); + X(v2); + +#undef X + + float32x4_t out = vmulq_f32(f0, v0); + out = vmlaq_f32(out, f1, v1); + out = vmlaq_f32(out, f2, v2); + + // Horizontal sum + float32x2_t sum = vadd_f32(vget_low_f32(out), vget_high_f32(out)); + sum = vpadd_f32(sum, sum); + + vst1_lane_f32(&dst[chan], sum, 0); + } +} +#endif + +// Calculate the cubic equation which passes through all four points. +// https://en.wikipedia.org/wiki/Ordinary_least_squares +// https://en.wikipedia.org/wiki/Polynomial_regression +static void CubicLeastSquares(Cubic *coeffs, float y0, float y1, float y2, float y3) +{ + // Least squares matrix for xs = [0, 1/3, 2/3, 1] + // [ 1.0 0.0 0.0 0.0 ] + // [ -5.5 9.0 -4.5 1.0 ] + // [ 9.0 -22.5 18.0 -4.5 ] + // [ -4.5 13.5 -13.5 4.5 ] + + coeffs->v[0] = y0; + coeffs->v[1] = -5.5f * y0 + 9.0f * y1 - 4.5f * y2 + y3; + coeffs->v[2] = 9.0f * y0 - 22.5f * y1 + 18.0f * y2 - 4.5f * y3; + coeffs->v[3] = -4.5f * y0 + 13.5f * y1 - 13.5f * y2 + 4.5f * y3; +} + +// Zeroth-order modified Bessel function of the first kind +// https://mathworld.wolfram.com/ModifiedBesselFunctionoftheFirstKind.html +static float BesselI0(float x) +{ + float sum = 0.0f; + float i = 1.0f; + float t = 1.0f; + x *= x * 0.25f; + + while (t >= sum * SDL_FLT_EPSILON) { + sum += t; + t *= x / (i * i); + ++i; + } + + return sum; +} + +// Pre-calculate 180 degrees of sin(pi * x) / pi +// The speedup from this isn't huge, but it also avoids precision issues. +// If sinf isn't available, SDL_sinf just calls SDL_sin. +// Know what SDL_sin(SDL_PI_F) equals? Not quite zero. +static void SincTable(float *table, int len) +{ + int i; + + for (i = 0; i < len; ++i) { + table[i] = SDL_sinf(i * (SDL_PI_F / len)) / SDL_PI_F; + } +} + +// Calculate Sinc(x/y), using a lookup table +static float Sinc(const float *table, int x, int y) +{ + float s = table[x % y]; + s = ((x / y) & 1) ? -s : s; + return (s * y) / x; +} + +static Cubic ResamplerFilter[RESAMPLER_SAMPLES_PER_ZERO_CROSSING][RESAMPLER_SAMPLES_PER_FRAME]; + +static void GenerateResamplerFilter(void) +{ + enum + { + // Generate samples at 3x the target resolution, so that we have samples at [0, 1/3, 2/3, 1] of each position + TABLE_SAMPLES_PER_ZERO_CROSSING = RESAMPLER_SAMPLES_PER_ZERO_CROSSING * 3, + TABLE_SIZE = RESAMPLER_ZERO_CROSSINGS * TABLE_SAMPLES_PER_ZERO_CROSSING, + }; + + // if dB > 50, beta=(0.1102 * (dB - 8.7)), according to Matlab. + const float dB = 80.0f; + const float beta = 0.1102f * (dB - 8.7f); + const float bessel_beta = BesselI0(beta); + const float lensqr = TABLE_SIZE * TABLE_SIZE; + + int i, j; + + float sinc[TABLE_SAMPLES_PER_ZERO_CROSSING]; + SincTable(sinc, TABLE_SAMPLES_PER_ZERO_CROSSING); + + // Generate one wing of the filter + // https://en.wikipedia.org/wiki/Kaiser_window + // https://en.wikipedia.org/wiki/Whittaker%E2%80%93Shannon_interpolation_formula + float filter[TABLE_SIZE + 1]; + filter[0] = 1.0f; + + for (i = 1; i <= TABLE_SIZE; ++i) { + float b = BesselI0(beta * SDL_sqrtf((lensqr - (i * i)) / lensqr)) / bessel_beta; + float s = Sinc(sinc, i, TABLE_SAMPLES_PER_ZERO_CROSSING); + filter[i] = b * s; + } + + // Generate the coefficients for each point + // When interpolating, the fraction represents how far we are between input samples, + // so we need to align the filter by "moving" it to the right. + // + // For the left wing, this means interpolating "forwards" (away from the center) + // For the right wing, this means interpolating "backwards" (towards the center) + // + // The center of the filter is at the end of the left wing (RESAMPLER_ZERO_CROSSINGS - 1) + // The left wing is the filter, but reversed + // The right wing is the filter, but offset by 1 + // + // Since the right wing is offset by 1, this just means we interpolate backwards + // between the same points, instead of forwards + // interp(p[n], p[n+1], t) = interp(p[n+1], p[n+1-1], 1 - t) = interp(p[n+1], p[n], 1 - t) + for (i = 0; i < RESAMPLER_SAMPLES_PER_ZERO_CROSSING; ++i) { + for (j = 0; j < RESAMPLER_ZERO_CROSSINGS; ++j) { + const float *ys = &filter[((j * RESAMPLER_SAMPLES_PER_ZERO_CROSSING) + i) * 3]; + + Cubic *fwd = &ResamplerFilter[i][RESAMPLER_ZERO_CROSSINGS - j - 1]; + Cubic *rev = &ResamplerFilter[RESAMPLER_SAMPLES_PER_ZERO_CROSSING - i - 1][RESAMPLER_ZERO_CROSSINGS + j]; + + // Calculate the cubic equation of the 4 points + CubicLeastSquares(fwd, ys[0], ys[1], ys[2], ys[3]); + CubicLeastSquares(rev, ys[3], ys[2], ys[1], ys[0]); + } + } +} + +typedef void (*ResampleFrameFunc)(const float *src, float *dst, const Cubic *filter, float frac, int chans); +static ResampleFrameFunc ResampleFrame[8]; + +// Transpose 4x4 floats +static void Transpose4x4(Cubic *data) +{ + int i, j; + + Cubic temp[4] = { data[0], data[1], data[2], data[3] }; + + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + data[i].v[j] = temp[j].v[i]; + } + } +} + +static void SetupAudioResampler(void) +{ + int i, j; + bool transpose = false; + + GenerateResamplerFilter(); + +#ifdef SDL_SSE_INTRINSICS + if (SDL_HasSSE()) { + for (i = 0; i < 8; ++i) { + ResampleFrame[i] = ResampleFrame_Generic_SSE; + } + transpose = true; + } else +#endif +#ifdef SDL_NEON_INTRINSICS + if (SDL_HasNEON()) { + for (i = 0; i < 8; ++i) { + ResampleFrame[i] = ResampleFrame_Generic_NEON; + } + transpose = true; + } else +#endif + { + for (i = 0; i < 8; ++i) { + ResampleFrame[i] = ResampleFrame_Generic; + } + + ResampleFrame[0] = ResampleFrame_Mono; + ResampleFrame[1] = ResampleFrame_Stereo; + } + + if (transpose) { + // Transpose each set of 4 coefficients, to reduce work when resampling + for (i = 0; i < RESAMPLER_SAMPLES_PER_ZERO_CROSSING; ++i) { + for (j = 0; j + 4 <= RESAMPLER_SAMPLES_PER_FRAME; j += 4) { + Transpose4x4(&ResamplerFilter[i][j]); + } + } + } +} + +void SDL_SetupAudioResampler(void) +{ + static SDL_InitState init; + + if (SDL_ShouldInit(&init)) { + SetupAudioResampler(); + SDL_SetInitialized(&init, true); + } +} + +Sint64 SDL_GetResampleRate(int src_rate, int dst_rate) +{ + SDL_assert(src_rate > 0); + SDL_assert(dst_rate > 0); + + Sint64 numerator = (Sint64)src_rate << 32; + Sint64 denominator = (Sint64)dst_rate; + + // Generally it's expected that `dst_frames = (src_frames * dst_rate) / src_rate` + // To match this as closely as possible without infinite precision, always round up the resample rate. + // For example, without rounding up, a sample ratio of 2:3 would have `sample_rate = 0xAAAAAAAA` + // After 3 frames, the position would be 0x1.FFFFFFFE, meaning we haven't fully consumed the second input frame. + // By rounding up to 0xAAAAAAAB, we would instead reach 0x2.00000001, fulling consuming the second frame. + // Technically you could say this is kicking the can 0x100000000 steps down the road, but I'm fine with that :) + // sample_rate = div_ceil(numerator, denominator) + Sint64 sample_rate = ((numerator - 1) / denominator) + 1; + + SDL_assert(sample_rate > 0); + + return sample_rate; +} + +int SDL_GetResamplerHistoryFrames(void) +{ + // Even if we aren't currently resampling, make sure to keep enough history in case we need to later. + + return RESAMPLER_MAX_PADDING_FRAMES; +} + +int SDL_GetResamplerPaddingFrames(Sint64 resample_rate) +{ + // This must always be <= SDL_GetResamplerHistoryFrames() + + return resample_rate ? RESAMPLER_MAX_PADDING_FRAMES : 0; +} + +// These are not general purpose. They do not check for all possible underflow/overflow +SDL_FORCE_INLINE bool ResamplerAdd(Sint64 a, Sint64 b, Sint64 *ret) +{ + if ((b > 0) && (a > SDL_MAX_SINT64 - b)) { + return false; + } + + *ret = a + b; + return true; +} + +SDL_FORCE_INLINE bool ResamplerMul(Sint64 a, Sint64 b, Sint64 *ret) +{ + if ((b > 0) && (a > SDL_MAX_SINT64 / b)) { + return false; + } + + *ret = a * b; + return true; +} + +Sint64 SDL_GetResamplerInputFrames(Sint64 output_frames, Sint64 resample_rate, Sint64 resample_offset) +{ + // Calculate the index of the last input frame, then add 1. + // ((((output_frames - 1) * resample_rate) + resample_offset) >> 32) + 1 + + Sint64 output_offset; + if (!ResamplerMul(output_frames, resample_rate, &output_offset) || + !ResamplerAdd(output_offset, -resample_rate + resample_offset + 0x100000000, &output_offset)) { + output_offset = SDL_MAX_SINT64; + } + + Sint64 input_frames = (Sint64)(Sint32)(output_offset >> 32); + input_frames = SDL_max(input_frames, 0); + + return input_frames; +} + +Sint64 SDL_GetResamplerOutputFrames(Sint64 input_frames, Sint64 resample_rate, Sint64 *inout_resample_offset) +{ + Sint64 resample_offset = *inout_resample_offset; + + // input_offset = (input_frames << 32) - resample_offset; + Sint64 input_offset; + if (!ResamplerMul(input_frames, 0x100000000, &input_offset) || + !ResamplerAdd(input_offset, -resample_offset, &input_offset)) { + input_offset = SDL_MAX_SINT64; + } + + // output_frames = div_ceil(input_offset, resample_rate) + Sint64 output_frames = (input_offset > 0) ? ((input_offset - 1) / resample_rate) + 1 : 0; + + *inout_resample_offset = (output_frames * resample_rate) - input_offset; + + return output_frames; +} + +void SDL_ResampleAudio(int chans, const float *src, int inframes, float *dst, int outframes, + Sint64 resample_rate, Sint64 *inout_resample_offset) +{ + int i; + Sint64 srcpos = *inout_resample_offset; + ResampleFrameFunc resample_frame = ResampleFrame[chans - 1]; + + SDL_assert(resample_rate > 0); + + src -= (RESAMPLER_ZERO_CROSSINGS - 1) * chans; + + for (i = 0; i < outframes; ++i) { + int srcindex = (int)(Sint32)(srcpos >> 32); + Uint32 srcfraction = (Uint32)(srcpos & 0xFFFFFFFF); + srcpos += resample_rate; + + SDL_assert(srcindex >= -1 && srcindex < inframes); + + const Cubic *filter = ResamplerFilter[srcfraction >> RESAMPLER_FILTER_INTERP_BITS]; + const float frac = (float)(srcfraction & (RESAMPLER_FILTER_INTERP_RANGE - 1)) * (1.0f / RESAMPLER_FILTER_INTERP_RANGE); + + const float *frame = &src[srcindex * chans]; + resample_frame(frame, dst, filter, frac, chans); + + dst += chans; + } + + *inout_resample_offset = srcpos - ((Sint64)inframes << 32); +} diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audioresample.h b/contrib/SDL-3.2.8/src/audio/SDL_audioresample.h new file mode 100644 index 0000000..6620073 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audioresample.h @@ -0,0 +1,43 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_audioresample_h_ +#define SDL_audioresample_h_ + +// Internal functions used by SDL_AudioStream for resampling audio. +// The resampler uses 32:32 fixed-point arithmetic to track its position. + +Sint64 SDL_GetResampleRate(int src_rate, int dst_rate); + +int SDL_GetResamplerHistoryFrames(void); +int SDL_GetResamplerPaddingFrames(Sint64 resample_rate); + +Sint64 SDL_GetResamplerInputFrames(Sint64 output_frames, Sint64 resample_rate, Sint64 resample_offset); +Sint64 SDL_GetResamplerOutputFrames(Sint64 input_frames, Sint64 resample_rate, Sint64 *inout_resample_offset); + +// Resample some audio. +// REQUIRES: `inframes >= SDL_GetResamplerInputFrames(outframes)` +// REQUIRES: At least `SDL_GetResamplerPaddingFrames(...)` extra frames to the left of src, and right of src+inframes +void SDL_ResampleAudio(int chans, const float *src, int inframes, float *dst, int outframes, + Sint64 resample_rate, Sint64 *inout_resample_offset); + +#endif // SDL_audioresample_h_ diff --git a/contrib/SDL-3.2.8/src/audio/SDL_audiotypecvt.c b/contrib/SDL-3.2.8/src/audio/SDL_audiotypecvt.c new file mode 100644 index 0000000..d80a831 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_audiotypecvt.c @@ -0,0 +1,925 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_sysaudio.h" + +#define DIVBY2147483648 0.0000000004656612873077392578125f // 0x1p-31f + +// start fallback scalar converters + +// This code requires that floats are in the IEEE-754 binary32 format +SDL_COMPILE_TIME_ASSERT(float_bits, sizeof(float) == sizeof(Uint32)); + +union float_bits { + Uint32 u32; + float f32; +}; + +static void SDL_Convert_S8_to_F32_Scalar(float *dst, const Sint8 *src, int num_samples) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("S8", "F32"); + + for (i = num_samples - 1; i >= 0; --i) { + /* 1) Construct a float in the range [65536.0, 65538.0) + * 2) Shift the float range to [-1.0, 1.0) */ + union float_bits x; + x.u32 = (Uint8)src[i] ^ 0x47800080u; + dst[i] = x.f32 - 65537.0f; + } +} + +static void SDL_Convert_U8_to_F32_Scalar(float *dst, const Uint8 *src, int num_samples) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("U8", "F32"); + + for (i = num_samples - 1; i >= 0; --i) { + /* 1) Construct a float in the range [65536.0, 65538.0) + * 2) Shift the float range to [-1.0, 1.0) */ + union float_bits x; + x.u32 = src[i] ^ 0x47800000u; + dst[i] = x.f32 - 65537.0f; + } +} + +static void SDL_Convert_S16_to_F32_Scalar(float *dst, const Sint16 *src, int num_samples) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("S16", "F32"); + + for (i = num_samples - 1; i >= 0; --i) { + /* 1) Construct a float in the range [256.0, 258.0) + * 2) Shift the float range to [-1.0, 1.0) */ + union float_bits x; + x.u32 = (Uint16)src[i] ^ 0x43808000u; + dst[i] = x.f32 - 257.0f; + } +} + +static void SDL_Convert_S32_to_F32_Scalar(float *dst, const Sint32 *src, int num_samples) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("S32", "F32"); + + for (i = num_samples - 1; i >= 0; --i) { + dst[i] = (float)src[i] * DIVBY2147483648; + } +} + +// Create a bit-mask based on the sign-bit. Should optimize to a single arithmetic-shift-right +#define SIGNMASK(x) (Uint32)(0u - ((Uint32)(x) >> 31)) + +static void SDL_Convert_F32_to_S8_Scalar(Sint8 *dst, const float *src, int num_samples) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("F32", "S8"); + + for (i = 0; i < num_samples; ++i) { + /* 1) Shift the float range from [-1.0, 1.0] to [98303.0, 98305.0] + * 2) Shift the integer range from [0x47BFFF80, 0x47C00080] to [-128, 128] + * 3) Clamp the value to [-128, 127] */ + union float_bits x; + x.f32 = src[i] + 98304.0f; + + Uint32 y = x.u32 - 0x47C00000u; + Uint32 z = 0x7Fu - (y ^ SIGNMASK(y)); + y = y ^ (z & SIGNMASK(z)); + + dst[i] = (Sint8)(y & 0xFF); + } +} + +static void SDL_Convert_F32_to_U8_Scalar(Uint8 *dst, const float *src, int num_samples) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("F32", "U8"); + + for (i = 0; i < num_samples; ++i) { + /* 1) Shift the float range from [-1.0, 1.0] to [98303.0, 98305.0] + * 2) Shift the integer range from [0x47BFFF80, 0x47C00080] to [-128, 128] + * 3) Clamp the value to [-128, 127] + * 4) Shift the integer range from [-128, 127] to [0, 255] */ + union float_bits x; + x.f32 = src[i] + 98304.0f; + + Uint32 y = x.u32 - 0x47C00000u; + Uint32 z = 0x7Fu - (y ^ SIGNMASK(y)); + y = (y ^ 0x80u) ^ (z & SIGNMASK(z)); + + dst[i] = (Uint8)(y & 0xFF); + } +} + +static void SDL_Convert_F32_to_S16_Scalar(Sint16 *dst, const float *src, int num_samples) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("F32", "S16"); + + for (i = 0; i < num_samples; ++i) { + /* 1) Shift the float range from [-1.0, 1.0] to [383.0, 385.0] + * 2) Shift the integer range from [0x43BF8000, 0x43C08000] to [-32768, 32768] + * 3) Clamp values outside the [-32768, 32767] range */ + union float_bits x; + x.f32 = src[i] + 384.0f; + + Uint32 y = x.u32 - 0x43C00000u; + Uint32 z = 0x7FFFu - (y ^ SIGNMASK(y)); + y = y ^ (z & SIGNMASK(z)); + + dst[i] = (Sint16)(y & 0xFFFF); + } +} + +static void SDL_Convert_F32_to_S32_Scalar(Sint32 *dst, const float *src, int num_samples) +{ + int i; + + LOG_DEBUG_AUDIO_CONVERT("F32", "S32"); + + for (i = 0; i < num_samples; ++i) { + /* 1) Shift the float range from [-1.0, 1.0] to [-2147483648.0, 2147483648.0] + * 2) Set values outside the [-2147483648.0, 2147483647.0] range to -2147483648.0 + * 3) Convert the float to an integer, and fixup values outside the valid range */ + union float_bits x; + x.f32 = src[i]; + + Uint32 y = x.u32 + 0x0F800000u; + Uint32 z = y - 0xCF000000u; + z &= SIGNMASK(y ^ z); + x.u32 = y - z; + + dst[i] = (Sint32)x.f32 ^ (Sint32)SIGNMASK(z); + } +} + +#undef SIGNMASK + +static void SDL_Convert_Swap16_Scalar(Uint16* dst, const Uint16* src, int num_samples) +{ + int i; + + for (i = 0; i < num_samples; ++i) { + dst[i] = SDL_Swap16(src[i]); + } +} + +static void SDL_Convert_Swap32_Scalar(Uint32* dst, const Uint32* src, int num_samples) +{ + int i; + + for (i = 0; i < num_samples; ++i) { + dst[i] = SDL_Swap32(src[i]); + } +} + +// end fallback scalar converters + +// Convert forwards, when sizeof(*src) >= sizeof(*dst) +#define CONVERT_16_FWD(CVT1, CVT16) \ + int i = 0; \ + if (num_samples >= 16) { \ + while ((uintptr_t)(&dst[i]) & 15) { CVT1 ++i; } \ + while ((i + 16) <= num_samples) { CVT16 i += 16; } \ + } \ + while (i < num_samples) { CVT1 ++i; } + +// Convert backwards, when sizeof(*src) <= sizeof(*dst) +#define CONVERT_16_REV(CVT1, CVT16) \ + int i = num_samples; \ + if (i >= 16) { \ + while ((uintptr_t)(&dst[i]) & 15) { --i; CVT1 } \ + while (i >= 16) { i -= 16; CVT16 } \ + } \ + while (i > 0) { --i; CVT1 } + +#ifdef SDL_SSE2_INTRINSICS +static void SDL_TARGETING("sse2") SDL_Convert_S8_to_F32_SSE2(float *dst, const Sint8 *src, int num_samples) +{ + /* 1) Flip the sign bit to convert from S8 to U8 format + * 2) Construct a float in the range [65536.0, 65538.0) + * 3) Shift the float range to [-1.0, 1.0) + * dst[i] = i2f((src[i] ^ 0x80) | 0x47800000) - 65537.0 */ + const __m128i zero = _mm_setzero_si128(); + const __m128i flipper = _mm_set1_epi8(-0x80); + const __m128i caster = _mm_set1_epi16(0x4780 /* 0x47800000 = f2i(65536.0) */); + const __m128 offset = _mm_set1_ps(-65537.0); + + LOG_DEBUG_AUDIO_CONVERT("S8", "F32 (using SSE2)"); + + CONVERT_16_REV({ + _mm_store_ss(&dst[i], _mm_add_ss(_mm_castsi128_ps(_mm_cvtsi32_si128((Uint8)src[i] ^ 0x47800080u)), offset)); + }, { + const __m128i bytes = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&src[i]), flipper); + + const __m128i shorts0 = _mm_unpacklo_epi8(bytes, zero); + const __m128i shorts1 = _mm_unpackhi_epi8(bytes, zero); + + const __m128 floats0 = _mm_add_ps(_mm_castsi128_ps(_mm_unpacklo_epi16(shorts0, caster)), offset); + const __m128 floats1 = _mm_add_ps(_mm_castsi128_ps(_mm_unpackhi_epi16(shorts0, caster)), offset); + const __m128 floats2 = _mm_add_ps(_mm_castsi128_ps(_mm_unpacklo_epi16(shorts1, caster)), offset); + const __m128 floats3 = _mm_add_ps(_mm_castsi128_ps(_mm_unpackhi_epi16(shorts1, caster)), offset); + + _mm_store_ps(&dst[i], floats0); + _mm_store_ps(&dst[i + 4], floats1); + _mm_store_ps(&dst[i + 8], floats2); + _mm_store_ps(&dst[i + 12], floats3); + }) +} + +static void SDL_TARGETING("sse2") SDL_Convert_U8_to_F32_SSE2(float *dst, const Uint8 *src, int num_samples) +{ + /* 1) Construct a float in the range [65536.0, 65538.0) + * 2) Shift the float range to [-1.0, 1.0) + * dst[i] = i2f(src[i] | 0x47800000) - 65537.0 */ + const __m128i zero = _mm_setzero_si128(); + const __m128i caster = _mm_set1_epi16(0x4780 /* 0x47800000 = f2i(65536.0) */); + const __m128 offset = _mm_set1_ps(-65537.0); + + LOG_DEBUG_AUDIO_CONVERT("U8", "F32 (using SSE2)"); + + CONVERT_16_REV({ + _mm_store_ss(&dst[i], _mm_add_ss(_mm_castsi128_ps(_mm_cvtsi32_si128((Uint8)src[i] ^ 0x47800000u)), offset)); + }, { + const __m128i bytes = _mm_loadu_si128((const __m128i *)&src[i]); + + const __m128i shorts0 = _mm_unpacklo_epi8(bytes, zero); + const __m128i shorts1 = _mm_unpackhi_epi8(bytes, zero); + + const __m128 floats0 = _mm_add_ps(_mm_castsi128_ps(_mm_unpacklo_epi16(shorts0, caster)), offset); + const __m128 floats1 = _mm_add_ps(_mm_castsi128_ps(_mm_unpackhi_epi16(shorts0, caster)), offset); + const __m128 floats2 = _mm_add_ps(_mm_castsi128_ps(_mm_unpacklo_epi16(shorts1, caster)), offset); + const __m128 floats3 = _mm_add_ps(_mm_castsi128_ps(_mm_unpackhi_epi16(shorts1, caster)), offset); + + _mm_store_ps(&dst[i], floats0); + _mm_store_ps(&dst[i + 4], floats1); + _mm_store_ps(&dst[i + 8], floats2); + _mm_store_ps(&dst[i + 12], floats3); + }) +} + +static void SDL_TARGETING("sse2") SDL_Convert_S16_to_F32_SSE2(float *dst, const Sint16 *src, int num_samples) +{ + /* 1) Flip the sign bit to convert from S16 to U16 format + * 2) Construct a float in the range [256.0, 258.0) + * 3) Shift the float range to [-1.0, 1.0) + * dst[i] = i2f((src[i] ^ 0x8000) | 0x43800000) - 257.0 */ + const __m128i flipper = _mm_set1_epi16(-0x8000); + const __m128i caster = _mm_set1_epi16(0x4380 /* 0x43800000 = f2i(256.0) */); + const __m128 offset = _mm_set1_ps(-257.0f); + + LOG_DEBUG_AUDIO_CONVERT("S16", "F32 (using SSE2)"); + + CONVERT_16_REV({ + _mm_store_ss(&dst[i], _mm_add_ss(_mm_castsi128_ps(_mm_cvtsi32_si128((Uint16)src[i] ^ 0x43808000u)), offset)); + }, { + const __m128i shorts0 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&src[i]), flipper); + const __m128i shorts1 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&src[i + 8]), flipper); + + const __m128 floats0 = _mm_add_ps(_mm_castsi128_ps(_mm_unpacklo_epi16(shorts0, caster)), offset); + const __m128 floats1 = _mm_add_ps(_mm_castsi128_ps(_mm_unpackhi_epi16(shorts0, caster)), offset); + const __m128 floats2 = _mm_add_ps(_mm_castsi128_ps(_mm_unpacklo_epi16(shorts1, caster)), offset); + const __m128 floats3 = _mm_add_ps(_mm_castsi128_ps(_mm_unpackhi_epi16(shorts1, caster)), offset); + + _mm_store_ps(&dst[i], floats0); + _mm_store_ps(&dst[i + 4], floats1); + _mm_store_ps(&dst[i + 8], floats2); + _mm_store_ps(&dst[i + 12], floats3); + }) +} + +static void SDL_TARGETING("sse2") SDL_Convert_S32_to_F32_SSE2(float *dst, const Sint32 *src, int num_samples) +{ + // dst[i] = f32(src[i]) / f32(0x80000000) + const __m128 scaler = _mm_set1_ps(DIVBY2147483648); + + LOG_DEBUG_AUDIO_CONVERT("S32", "F32 (using SSE2)"); + + CONVERT_16_FWD({ + _mm_store_ss(&dst[i], _mm_mul_ss(_mm_cvt_si2ss(_mm_setzero_ps(), src[i]), scaler)); + }, { + const __m128i ints0 = _mm_loadu_si128((const __m128i *)&src[i]); + const __m128i ints1 = _mm_loadu_si128((const __m128i *)&src[i + 4]); + const __m128i ints2 = _mm_loadu_si128((const __m128i *)&src[i + 8]); + const __m128i ints3 = _mm_loadu_si128((const __m128i *)&src[i + 12]); + + const __m128 floats0 = _mm_mul_ps(_mm_cvtepi32_ps(ints0), scaler); + const __m128 floats1 = _mm_mul_ps(_mm_cvtepi32_ps(ints1), scaler); + const __m128 floats2 = _mm_mul_ps(_mm_cvtepi32_ps(ints2), scaler); + const __m128 floats3 = _mm_mul_ps(_mm_cvtepi32_ps(ints3), scaler); + + _mm_store_ps(&dst[i], floats0); + _mm_store_ps(&dst[i + 4], floats1); + _mm_store_ps(&dst[i + 8], floats2); + _mm_store_ps(&dst[i + 12], floats3); + }) +} + +static void SDL_TARGETING("sse2") SDL_Convert_F32_to_S8_SSE2(Sint8 *dst, const float *src, int num_samples) +{ + /* 1) Shift the float range from [-1.0, 1.0] to [98303.0, 98305.0] + * 2) Extract the lowest 16 bits and clamp to [-128, 127] + * Overflow is correctly handled for inputs between roughly [-255.0, 255.0] + * dst[i] = clamp(i16(f2i(src[i] + 98304.0) & 0xFFFF), -128, 127) */ + const __m128 offset = _mm_set1_ps(98304.0f); + const __m128i mask = _mm_set1_epi16(0xFF); + + LOG_DEBUG_AUDIO_CONVERT("F32", "S8 (using SSE2)"); + + CONVERT_16_FWD({ + const __m128i ints = _mm_castps_si128(_mm_add_ss(_mm_load_ss(&src[i]), offset)); + dst[i] = (Sint8)(_mm_cvtsi128_si32(_mm_packs_epi16(ints, ints)) & 0xFF); + }, { + const __m128 floats0 = _mm_loadu_ps(&src[i]); + const __m128 floats1 = _mm_loadu_ps(&src[i + 4]); + const __m128 floats2 = _mm_loadu_ps(&src[i + 8]); + const __m128 floats3 = _mm_loadu_ps(&src[i + 12]); + + const __m128i ints0 = _mm_castps_si128(_mm_add_ps(floats0, offset)); + const __m128i ints1 = _mm_castps_si128(_mm_add_ps(floats1, offset)); + const __m128i ints2 = _mm_castps_si128(_mm_add_ps(floats2, offset)); + const __m128i ints3 = _mm_castps_si128(_mm_add_ps(floats3, offset)); + + const __m128i shorts0 = _mm_and_si128(_mm_packs_epi16(ints0, ints1), mask); + const __m128i shorts1 = _mm_and_si128(_mm_packs_epi16(ints2, ints3), mask); + + const __m128i bytes = _mm_packus_epi16(shorts0, shorts1); + + _mm_store_si128((__m128i*)&dst[i], bytes); + }) +} + +static void SDL_TARGETING("sse2") SDL_Convert_F32_to_U8_SSE2(Uint8 *dst, const float *src, int num_samples) +{ + /* 1) Shift the float range from [-1.0, 1.0] to [98304.0, 98306.0] + * 2) Extract the lowest 16 bits and clamp to [0, 255] + * Overflow is correctly handled for inputs between roughly [-254.0, 254.0] + * dst[i] = clamp(i16(f2i(src[i] + 98305.0) & 0xFFFF), 0, 255) */ + const __m128 offset = _mm_set1_ps(98305.0f); + const __m128i mask = _mm_set1_epi16(0xFF); + + LOG_DEBUG_AUDIO_CONVERT("F32", "U8 (using SSE2)"); + + CONVERT_16_FWD({ + const __m128i ints = _mm_castps_si128(_mm_add_ss(_mm_load_ss(&src[i]), offset)); + dst[i] = (Uint8)(_mm_cvtsi128_si32(_mm_packus_epi16(ints, ints)) & 0xFF); + }, { + const __m128 floats0 = _mm_loadu_ps(&src[i]); + const __m128 floats1 = _mm_loadu_ps(&src[i + 4]); + const __m128 floats2 = _mm_loadu_ps(&src[i + 8]); + const __m128 floats3 = _mm_loadu_ps(&src[i + 12]); + + const __m128i ints0 = _mm_castps_si128(_mm_add_ps(floats0, offset)); + const __m128i ints1 = _mm_castps_si128(_mm_add_ps(floats1, offset)); + const __m128i ints2 = _mm_castps_si128(_mm_add_ps(floats2, offset)); + const __m128i ints3 = _mm_castps_si128(_mm_add_ps(floats3, offset)); + + const __m128i shorts0 = _mm_and_si128(_mm_packus_epi16(ints0, ints1), mask); + const __m128i shorts1 = _mm_and_si128(_mm_packus_epi16(ints2, ints3), mask); + + const __m128i bytes = _mm_packus_epi16(shorts0, shorts1); + + _mm_store_si128((__m128i*)&dst[i], bytes); + }) +} + +static void SDL_TARGETING("sse2") SDL_Convert_F32_to_S16_SSE2(Sint16 *dst, const float *src, int num_samples) +{ + /* 1) Shift the float range from [-1.0, 1.0] to [256.0, 258.0] + * 2) Shift the int range from [0x43800000, 0x43810000] to [-32768,32768] + * 3) Clamp to range [-32768,32767] + * Overflow is correctly handled for inputs between roughly [-257.0, +inf) + * dst[i] = clamp(f2i(src[i] + 257.0) - 0x43808000, -32768, 32767) */ + const __m128 offset = _mm_set1_ps(257.0f); + + LOG_DEBUG_AUDIO_CONVERT("F32", "S16 (using SSE2)"); + + CONVERT_16_FWD({ + const __m128i ints = _mm_sub_epi32(_mm_castps_si128(_mm_add_ss(_mm_load_ss(&src[i]), offset)), _mm_castps_si128(offset)); + dst[i] = (Sint16)(_mm_cvtsi128_si32(_mm_packs_epi32(ints, ints)) & 0xFFFF); + }, { + const __m128 floats0 = _mm_loadu_ps(&src[i]); + const __m128 floats1 = _mm_loadu_ps(&src[i + 4]); + const __m128 floats2 = _mm_loadu_ps(&src[i + 8]); + const __m128 floats3 = _mm_loadu_ps(&src[i + 12]); + + const __m128i ints0 = _mm_sub_epi32(_mm_castps_si128(_mm_add_ps(floats0, offset)), _mm_castps_si128(offset)); + const __m128i ints1 = _mm_sub_epi32(_mm_castps_si128(_mm_add_ps(floats1, offset)), _mm_castps_si128(offset)); + const __m128i ints2 = _mm_sub_epi32(_mm_castps_si128(_mm_add_ps(floats2, offset)), _mm_castps_si128(offset)); + const __m128i ints3 = _mm_sub_epi32(_mm_castps_si128(_mm_add_ps(floats3, offset)), _mm_castps_si128(offset)); + + const __m128i shorts0 = _mm_packs_epi32(ints0, ints1); + const __m128i shorts1 = _mm_packs_epi32(ints2, ints3); + + _mm_store_si128((__m128i*)&dst[i], shorts0); + _mm_store_si128((__m128i*)&dst[i + 8], shorts1); + }) +} + +static void SDL_TARGETING("sse2") SDL_Convert_F32_to_S32_SSE2(Sint32 *dst, const float *src, int num_samples) +{ + /* 1) Scale the float range from [-1.0, 1.0] to [-2147483648.0, 2147483648.0] + * 2) Convert to integer (values too small/large become 0x80000000 = -2147483648) + * 3) Fixup values which were too large (0x80000000 ^ 0xFFFFFFFF = 2147483647) + * dst[i] = i32(src[i] * 2147483648.0) ^ ((src[i] >= 2147483648.0) ? 0xFFFFFFFF : 0x00000000) */ + const __m128 limit = _mm_set1_ps(2147483648.0f); + + LOG_DEBUG_AUDIO_CONVERT("F32", "S32 (using SSE2)"); + + CONVERT_16_FWD({ + const __m128 floats = _mm_load_ss(&src[i]); + const __m128 values = _mm_mul_ss(floats, limit); + const __m128i ints = _mm_xor_si128(_mm_cvttps_epi32(values), _mm_castps_si128(_mm_cmpge_ss(values, limit))); + dst[i] = (Sint32)_mm_cvtsi128_si32(ints); + }, { + const __m128 floats0 = _mm_loadu_ps(&src[i]); + const __m128 floats1 = _mm_loadu_ps(&src[i + 4]); + const __m128 floats2 = _mm_loadu_ps(&src[i + 8]); + const __m128 floats3 = _mm_loadu_ps(&src[i + 12]); + + const __m128 values1 = _mm_mul_ps(floats0, limit); + const __m128 values2 = _mm_mul_ps(floats1, limit); + const __m128 values3 = _mm_mul_ps(floats2, limit); + const __m128 values4 = _mm_mul_ps(floats3, limit); + + const __m128i ints0 = _mm_xor_si128(_mm_cvttps_epi32(values1), _mm_castps_si128(_mm_cmpge_ps(values1, limit))); + const __m128i ints1 = _mm_xor_si128(_mm_cvttps_epi32(values2), _mm_castps_si128(_mm_cmpge_ps(values2, limit))); + const __m128i ints2 = _mm_xor_si128(_mm_cvttps_epi32(values3), _mm_castps_si128(_mm_cmpge_ps(values3, limit))); + const __m128i ints3 = _mm_xor_si128(_mm_cvttps_epi32(values4), _mm_castps_si128(_mm_cmpge_ps(values4, limit))); + + _mm_store_si128((__m128i*)&dst[i], ints0); + _mm_store_si128((__m128i*)&dst[i + 4], ints1); + _mm_store_si128((__m128i*)&dst[i + 8], ints2); + _mm_store_si128((__m128i*)&dst[i + 12], ints3); + }) +} +#endif + +// FIXME: SDL doesn't have SSSE3 detection, so use the next one up +#ifdef SDL_SSE4_1_INTRINSICS +static void SDL_TARGETING("ssse3") SDL_Convert_Swap16_SSSE3(Uint16* dst, const Uint16* src, int num_samples) +{ + const __m128i shuffle = _mm_set_epi8(14, 15, 12, 13, 10, 11, 8, 9, 6, 7, 4, 5, 2, 3, 0, 1); + + CONVERT_16_FWD({ + dst[i] = SDL_Swap16(src[i]); + }, { + __m128i ints0 = _mm_loadu_si128((const __m128i*)&src[i]); + __m128i ints1 = _mm_loadu_si128((const __m128i*)&src[i + 8]); + + ints0 = _mm_shuffle_epi8(ints0, shuffle); + ints1 = _mm_shuffle_epi8(ints1, shuffle); + + _mm_store_si128((__m128i*)&dst[i], ints0); + _mm_store_si128((__m128i*)&dst[i + 8], ints1); + }) +} + +static void SDL_TARGETING("ssse3") SDL_Convert_Swap32_SSSE3(Uint32* dst, const Uint32* src, int num_samples) +{ + const __m128i shuffle = _mm_set_epi8(12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3); + + CONVERT_16_FWD({ + dst[i] = SDL_Swap32(src[i]); + }, { + __m128i ints0 = _mm_loadu_si128((const __m128i*)&src[i]); + __m128i ints1 = _mm_loadu_si128((const __m128i*)&src[i + 4]); + __m128i ints2 = _mm_loadu_si128((const __m128i*)&src[i + 8]); + __m128i ints3 = _mm_loadu_si128((const __m128i*)&src[i + 12]); + + ints0 = _mm_shuffle_epi8(ints0, shuffle); + ints1 = _mm_shuffle_epi8(ints1, shuffle); + ints2 = _mm_shuffle_epi8(ints2, shuffle); + ints3 = _mm_shuffle_epi8(ints3, shuffle); + + _mm_store_si128((__m128i*)&dst[i], ints0); + _mm_store_si128((__m128i*)&dst[i + 4], ints1); + _mm_store_si128((__m128i*)&dst[i + 8], ints2); + _mm_store_si128((__m128i*)&dst[i + 12], ints3); + }) +} +#endif + +#ifdef SDL_NEON_INTRINSICS +static void SDL_Convert_S8_to_F32_NEON(float *dst, const Sint8 *src, int num_samples) +{ + LOG_DEBUG_AUDIO_CONVERT("S8", "F32 (using NEON)"); + + CONVERT_16_REV({ + vst1_lane_f32(&dst[i], vcvt_n_f32_s32(vdup_n_s32(src[i]), 7), 0); + }, { + int8x16_t bytes = vld1q_s8(&src[i]); + + int16x8_t shorts0 = vmovl_s8(vget_low_s8(bytes)); + int16x8_t shorts1 = vmovl_s8(vget_high_s8(bytes)); + + float32x4_t floats0 = vcvtq_n_f32_s32(vmovl_s16(vget_low_s16(shorts0)), 7); + float32x4_t floats1 = vcvtq_n_f32_s32(vmovl_s16(vget_high_s16(shorts0)), 7); + float32x4_t floats2 = vcvtq_n_f32_s32(vmovl_s16(vget_low_s16(shorts1)), 7); + float32x4_t floats3 = vcvtq_n_f32_s32(vmovl_s16(vget_high_s16(shorts1)), 7); + + vst1q_f32(&dst[i], floats0); + vst1q_f32(&dst[i + 4], floats1); + vst1q_f32(&dst[i + 8], floats2); + vst1q_f32(&dst[i + 12], floats3); + }) +} + +static void SDL_Convert_U8_to_F32_NEON(float *dst, const Uint8 *src, int num_samples) +{ + LOG_DEBUG_AUDIO_CONVERT("U8", "F32 (using NEON)"); + + uint8x16_t flipper = vdupq_n_u8(0x80); + + CONVERT_16_REV({ + vst1_lane_f32(&dst[i], vcvt_n_f32_s32(vdup_n_s32((Sint8)(src[i] ^ 0x80)), 7), 0); + }, { + int8x16_t bytes = vreinterpretq_s8_u8(veorq_u8(vld1q_u8(&src[i]), flipper)); + + int16x8_t shorts0 = vmovl_s8(vget_low_s8(bytes)); + int16x8_t shorts1 = vmovl_s8(vget_high_s8(bytes)); + + float32x4_t floats0 = vcvtq_n_f32_s32(vmovl_s16(vget_low_s16(shorts0)), 7); + float32x4_t floats1 = vcvtq_n_f32_s32(vmovl_s16(vget_high_s16(shorts0)), 7); + float32x4_t floats2 = vcvtq_n_f32_s32(vmovl_s16(vget_low_s16(shorts1)), 7); + float32x4_t floats3 = vcvtq_n_f32_s32(vmovl_s16(vget_high_s16(shorts1)), 7); + + vst1q_f32(&dst[i], floats0); + vst1q_f32(&dst[i + 4], floats1); + vst1q_f32(&dst[i + 8], floats2); + vst1q_f32(&dst[i + 12], floats3); + }) +} + +static void SDL_Convert_S16_to_F32_NEON(float *dst, const Sint16 *src, int num_samples) +{ + LOG_DEBUG_AUDIO_CONVERT("S16", "F32 (using NEON)"); + + CONVERT_16_REV({ + vst1_lane_f32(&dst[i], vcvt_n_f32_s32(vdup_n_s32(src[i]), 15), 0); + }, { + int16x8_t shorts0 = vld1q_s16(&src[i]); + int16x8_t shorts1 = vld1q_s16(&src[i + 8]); + + float32x4_t floats0 = vcvtq_n_f32_s32(vmovl_s16(vget_low_s16(shorts0)), 15); + float32x4_t floats1 = vcvtq_n_f32_s32(vmovl_s16(vget_high_s16(shorts0)), 15); + float32x4_t floats2 = vcvtq_n_f32_s32(vmovl_s16(vget_low_s16(shorts1)), 15); + float32x4_t floats3 = vcvtq_n_f32_s32(vmovl_s16(vget_high_s16(shorts1)), 15); + + vst1q_f32(&dst[i], floats0); + vst1q_f32(&dst[i + 4], floats1); + vst1q_f32(&dst[i + 8], floats2); + vst1q_f32(&dst[i + 12], floats3); + }) +} + +static void SDL_Convert_S32_to_F32_NEON(float *dst, const Sint32 *src, int num_samples) +{ + LOG_DEBUG_AUDIO_CONVERT("S32", "F32 (using NEON)"); + + CONVERT_16_FWD({ + vst1_lane_f32(&dst[i], vcvt_n_f32_s32(vld1_dup_s32(&src[i]), 31), 0); + }, { + int32x4_t ints0 = vld1q_s32(&src[i]); + int32x4_t ints1 = vld1q_s32(&src[i + 4]); + int32x4_t ints2 = vld1q_s32(&src[i + 8]); + int32x4_t ints3 = vld1q_s32(&src[i + 12]); + + float32x4_t floats0 = vcvtq_n_f32_s32(ints0, 31); + float32x4_t floats1 = vcvtq_n_f32_s32(ints1, 31); + float32x4_t floats2 = vcvtq_n_f32_s32(ints2, 31); + float32x4_t floats3 = vcvtq_n_f32_s32(ints3, 31); + + vst1q_f32(&dst[i], floats0); + vst1q_f32(&dst[i + 4], floats1); + vst1q_f32(&dst[i + 8], floats2); + vst1q_f32(&dst[i + 12], floats3); + }) +} + +static void SDL_Convert_F32_to_S8_NEON(Sint8 *dst, const float *src, int num_samples) +{ + LOG_DEBUG_AUDIO_CONVERT("F32", "S8 (using NEON)"); + + CONVERT_16_FWD({ + vst1_lane_s8(&dst[i], vreinterpret_s8_s32(vcvt_n_s32_f32(vld1_dup_f32(&src[i]), 31)), 3); + }, { + float32x4_t floats0 = vld1q_f32(&src[i]); + float32x4_t floats1 = vld1q_f32(&src[i + 4]); + float32x4_t floats2 = vld1q_f32(&src[i + 8]); + float32x4_t floats3 = vld1q_f32(&src[i + 12]); + + int32x4_t ints0 = vcvtq_n_s32_f32(floats0, 31); + int32x4_t ints1 = vcvtq_n_s32_f32(floats1, 31); + int32x4_t ints2 = vcvtq_n_s32_f32(floats2, 31); + int32x4_t ints3 = vcvtq_n_s32_f32(floats3, 31); + + int16x8_t shorts0 = vcombine_s16(vshrn_n_s32(ints0, 16), vshrn_n_s32(ints1, 16)); + int16x8_t shorts1 = vcombine_s16(vshrn_n_s32(ints2, 16), vshrn_n_s32(ints3, 16)); + + int8x16_t bytes = vcombine_s8(vshrn_n_s16(shorts0, 8), vshrn_n_s16(shorts1, 8)); + + vst1q_s8(&dst[i], bytes); + }) +} + +static void SDL_Convert_F32_to_U8_NEON(Uint8 *dst, const float *src, int num_samples) +{ + LOG_DEBUG_AUDIO_CONVERT("F32", "U8 (using NEON)"); + + uint8x16_t flipper = vdupq_n_u8(0x80); + + CONVERT_16_FWD({ + vst1_lane_u8(&dst[i], + veor_u8(vreinterpret_u8_s32(vcvt_n_s32_f32(vld1_dup_f32(&src[i]), 31)), + vget_low_u8(flipper)), 3); + }, { + float32x4_t floats0 = vld1q_f32(&src[i]); + float32x4_t floats1 = vld1q_f32(&src[i + 4]); + float32x4_t floats2 = vld1q_f32(&src[i + 8]); + float32x4_t floats3 = vld1q_f32(&src[i + 12]); + + int32x4_t ints0 = vcvtq_n_s32_f32(floats0, 31); + int32x4_t ints1 = vcvtq_n_s32_f32(floats1, 31); + int32x4_t ints2 = vcvtq_n_s32_f32(floats2, 31); + int32x4_t ints3 = vcvtq_n_s32_f32(floats3, 31); + + int16x8_t shorts0 = vcombine_s16(vshrn_n_s32(ints0, 16), vshrn_n_s32(ints1, 16)); + int16x8_t shorts1 = vcombine_s16(vshrn_n_s32(ints2, 16), vshrn_n_s32(ints3, 16)); + + uint8x16_t bytes = veorq_u8(vreinterpretq_u8_s8( + vcombine_s8(vshrn_n_s16(shorts0, 8), vshrn_n_s16(shorts1, 8))), + flipper); + + vst1q_u8(&dst[i], bytes); + }) +} + +static void SDL_Convert_F32_to_S16_NEON(Sint16 *dst, const float *src, int num_samples) +{ + LOG_DEBUG_AUDIO_CONVERT("F32", "S16 (using NEON)"); + + CONVERT_16_FWD({ + vst1_lane_s16(&dst[i], vreinterpret_s16_s32(vcvt_n_s32_f32(vld1_dup_f32(&src[i]), 31)), 1); + }, { + float32x4_t floats0 = vld1q_f32(&src[i]); + float32x4_t floats1 = vld1q_f32(&src[i + 4]); + float32x4_t floats2 = vld1q_f32(&src[i + 8]); + float32x4_t floats3 = vld1q_f32(&src[i + 12]); + + int32x4_t ints0 = vcvtq_n_s32_f32(floats0, 31); + int32x4_t ints1 = vcvtq_n_s32_f32(floats1, 31); + int32x4_t ints2 = vcvtq_n_s32_f32(floats2, 31); + int32x4_t ints3 = vcvtq_n_s32_f32(floats3, 31); + + int16x8_t shorts0 = vcombine_s16(vshrn_n_s32(ints0, 16), vshrn_n_s32(ints1, 16)); + int16x8_t shorts1 = vcombine_s16(vshrn_n_s32(ints2, 16), vshrn_n_s32(ints3, 16)); + + vst1q_s16(&dst[i], shorts0); + vst1q_s16(&dst[i + 8], shorts1); + }) +} + +static void SDL_Convert_F32_to_S32_NEON(Sint32 *dst, const float *src, int num_samples) +{ + LOG_DEBUG_AUDIO_CONVERT("F32", "S32 (using NEON)"); + + CONVERT_16_FWD({ + vst1_lane_s32(&dst[i], vcvt_n_s32_f32(vld1_dup_f32(&src[i]), 31), 0); + }, { + float32x4_t floats0 = vld1q_f32(&src[i]); + float32x4_t floats1 = vld1q_f32(&src[i + 4]); + float32x4_t floats2 = vld1q_f32(&src[i + 8]); + float32x4_t floats3 = vld1q_f32(&src[i + 12]); + + int32x4_t ints0 = vcvtq_n_s32_f32(floats0, 31); + int32x4_t ints1 = vcvtq_n_s32_f32(floats1, 31); + int32x4_t ints2 = vcvtq_n_s32_f32(floats2, 31); + int32x4_t ints3 = vcvtq_n_s32_f32(floats3, 31); + + vst1q_s32(&dst[i], ints0); + vst1q_s32(&dst[i + 4], ints1); + vst1q_s32(&dst[i + 8], ints2); + vst1q_s32(&dst[i + 12], ints3); + }) +} + +static void SDL_Convert_Swap16_NEON(Uint16* dst, const Uint16* src, int num_samples) +{ + CONVERT_16_FWD({ + dst[i] = SDL_Swap16(src[i]); + }, { + uint8x16_t ints0 = vld1q_u8((const Uint8*)&src[i]); + uint8x16_t ints1 = vld1q_u8((const Uint8*)&src[i + 8]); + + ints0 = vrev16q_u8(ints0); + ints1 = vrev16q_u8(ints1); + + vst1q_u8((Uint8*)&dst[i], ints0); + vst1q_u8((Uint8*)&dst[i + 8], ints1); + }) +} + +static void SDL_Convert_Swap32_NEON(Uint32* dst, const Uint32* src, int num_samples) +{ + CONVERT_16_FWD({ + dst[i] = SDL_Swap32(src[i]); + }, { + uint8x16_t ints0 = vld1q_u8((const Uint8*)&src[i]); + uint8x16_t ints1 = vld1q_u8((const Uint8*)&src[i + 4]); + uint8x16_t ints2 = vld1q_u8((const Uint8*)&src[i + 8]); + uint8x16_t ints3 = vld1q_u8((const Uint8*)&src[i + 12]); + + ints0 = vrev32q_u8(ints0); + ints1 = vrev32q_u8(ints1); + ints2 = vrev32q_u8(ints2); + ints3 = vrev32q_u8(ints3); + + vst1q_u8((Uint8*)&dst[i], ints0); + vst1q_u8((Uint8*)&dst[i + 4], ints1); + vst1q_u8((Uint8*)&dst[i + 8], ints2); + vst1q_u8((Uint8*)&dst[i + 12], ints3); + }) +} +#endif + +#undef CONVERT_16_FWD +#undef CONVERT_16_REV + +// Function pointers set to a CPU-specific implementation. +static void (*SDL_Convert_S8_to_F32)(float *dst, const Sint8 *src, int num_samples) = NULL; +static void (*SDL_Convert_U8_to_F32)(float *dst, const Uint8 *src, int num_samples) = NULL; +static void (*SDL_Convert_S16_to_F32)(float *dst, const Sint16 *src, int num_samples) = NULL; +static void (*SDL_Convert_S32_to_F32)(float *dst, const Sint32 *src, int num_samples) = NULL; +static void (*SDL_Convert_F32_to_S8)(Sint8 *dst, const float *src, int num_samples) = NULL; +static void (*SDL_Convert_F32_to_U8)(Uint8 *dst, const float *src, int num_samples) = NULL; +static void (*SDL_Convert_F32_to_S16)(Sint16 *dst, const float *src, int num_samples) = NULL; +static void (*SDL_Convert_F32_to_S32)(Sint32 *dst, const float *src, int num_samples) = NULL; + +static void (*SDL_Convert_Swap16)(Uint16* dst, const Uint16* src, int num_samples) = NULL; +static void (*SDL_Convert_Swap32)(Uint32* dst, const Uint32* src, int num_samples) = NULL; + +void ConvertAudioToFloat(float *dst, const void *src, int num_samples, SDL_AudioFormat src_fmt) +{ + switch (src_fmt) { + case SDL_AUDIO_S8: + SDL_Convert_S8_to_F32(dst, (const Sint8 *) src, num_samples); + break; + + case SDL_AUDIO_U8: + SDL_Convert_U8_to_F32(dst, (const Uint8 *) src, num_samples); + break; + + case SDL_AUDIO_S16: + SDL_Convert_S16_to_F32(dst, (const Sint16 *) src, num_samples); + break; + + case SDL_AUDIO_S16 ^ SDL_AUDIO_MASK_BIG_ENDIAN: + SDL_Convert_Swap16((Uint16*) dst, (const Uint16*) src, num_samples); + SDL_Convert_S16_to_F32(dst, (const Sint16 *) dst, num_samples); + break; + + case SDL_AUDIO_S32: + SDL_Convert_S32_to_F32(dst, (const Sint32 *) src, num_samples); + break; + + case SDL_AUDIO_S32 ^ SDL_AUDIO_MASK_BIG_ENDIAN: + SDL_Convert_Swap32((Uint32*) dst, (const Uint32*) src, num_samples); + SDL_Convert_S32_to_F32(dst, (const Sint32 *) dst, num_samples); + break; + + case SDL_AUDIO_F32 ^ SDL_AUDIO_MASK_BIG_ENDIAN: + SDL_Convert_Swap32((Uint32*) dst, (const Uint32*) src, num_samples); + break; + + default: SDL_assert(!"Unexpected audio format!"); break; + } +} + +void ConvertAudioFromFloat(void *dst, const float *src, int num_samples, SDL_AudioFormat dst_fmt) +{ + switch (dst_fmt) { + case SDL_AUDIO_S8: + SDL_Convert_F32_to_S8((Sint8 *) dst, src, num_samples); + break; + + case SDL_AUDIO_U8: + SDL_Convert_F32_to_U8((Uint8 *) dst, src, num_samples); + break; + + case SDL_AUDIO_S16: + SDL_Convert_F32_to_S16((Sint16 *) dst, src, num_samples); + break; + + case SDL_AUDIO_S16 ^ SDL_AUDIO_MASK_BIG_ENDIAN: + SDL_Convert_F32_to_S16((Sint16 *) dst, src, num_samples); + SDL_Convert_Swap16((Uint16*) dst, (const Uint16*) dst, num_samples); + break; + + case SDL_AUDIO_S32: + SDL_Convert_F32_to_S32((Sint32 *) dst, src, num_samples); + break; + + case SDL_AUDIO_S32 ^ SDL_AUDIO_MASK_BIG_ENDIAN: + SDL_Convert_F32_to_S32((Sint32 *) dst, src, num_samples); + SDL_Convert_Swap32((Uint32*) dst, (const Uint32*) dst, num_samples); + break; + + case SDL_AUDIO_F32 ^ SDL_AUDIO_MASK_BIG_ENDIAN: + SDL_Convert_Swap32((Uint32*) dst, (const Uint32*) src, num_samples); + break; + + default: SDL_assert(!"Unexpected audio format!"); break; + } +} + +void ConvertAudioSwapEndian(void* dst, const void* src, int num_samples, int bitsize) +{ + switch (bitsize) { + case 16: SDL_Convert_Swap16((Uint16*) dst, (const Uint16*) src, num_samples); break; + case 32: SDL_Convert_Swap32((Uint32*) dst, (const Uint32*) src, num_samples); break; + default: SDL_assert(!"Unexpected audio format!"); break; + } +} + +void SDL_ChooseAudioConverters(void) +{ + static bool converters_chosen = false; + if (converters_chosen) { + return; + } + +#define SET_CONVERTER_FUNCS(fntype) \ + SDL_Convert_Swap16 = SDL_Convert_Swap16_##fntype; \ + SDL_Convert_Swap32 = SDL_Convert_Swap32_##fntype; + +#ifdef SDL_SSE4_1_INTRINSICS + if (SDL_HasSSE41()) { + SET_CONVERTER_FUNCS(SSSE3); + } else +#endif +#ifdef SDL_NEON_INTRINSICS + if (SDL_HasNEON()) { + SET_CONVERTER_FUNCS(NEON); + } else +#endif + { + SET_CONVERTER_FUNCS(Scalar); + } + +#undef SET_CONVERTER_FUNCS + +#define SET_CONVERTER_FUNCS(fntype) \ + SDL_Convert_S8_to_F32 = SDL_Convert_S8_to_F32_##fntype; \ + SDL_Convert_U8_to_F32 = SDL_Convert_U8_to_F32_##fntype; \ + SDL_Convert_S16_to_F32 = SDL_Convert_S16_to_F32_##fntype; \ + SDL_Convert_S32_to_F32 = SDL_Convert_S32_to_F32_##fntype; \ + SDL_Convert_F32_to_S8 = SDL_Convert_F32_to_S8_##fntype; \ + SDL_Convert_F32_to_U8 = SDL_Convert_F32_to_U8_##fntype; \ + SDL_Convert_F32_to_S16 = SDL_Convert_F32_to_S16_##fntype; \ + SDL_Convert_F32_to_S32 = SDL_Convert_F32_to_S32_##fntype; \ + +#ifdef SDL_SSE2_INTRINSICS + if (SDL_HasSSE2()) { + SET_CONVERTER_FUNCS(SSE2); + } else +#endif +#ifdef SDL_NEON_INTRINSICS + if (SDL_HasNEON()) { + SET_CONVERTER_FUNCS(NEON); + } else +#endif + { + SET_CONVERTER_FUNCS(Scalar); + } + +#undef SET_CONVERTER_FUNCS + + converters_chosen = true; +} diff --git a/contrib/SDL-3.2.8/src/audio/SDL_mixer.c b/contrib/SDL-3.2.8/src/audio/SDL_mixer.c new file mode 100644 index 0000000..047c6fd --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_mixer.c @@ -0,0 +1,290 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +// This provides the default mixing callback for the SDL audio routines + +#include "SDL_sysaudio.h" + +/* This table is used to add two sound values together and pin + * the value to avoid overflow. (used with permission from ARDI) + */ +static const Uint8 mix8[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, + 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, + 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, + 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, + 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, + 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, + 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, + 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, + 0x7D, 0x7E, 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, + 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, + 0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, + 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, + 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, + 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, + 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, + 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, + 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF +}; + +// The volume ranges from 0 - 128 +#define MIX_MAXVOLUME 128 +#define ADJUST_VOLUME(type, s, v) ((s) = (type)(((s) * (v)) / MIX_MAXVOLUME)) +#define ADJUST_VOLUME_U8(s, v) ((s) = (Uint8)(((((s) - 128) * (v)) / MIX_MAXVOLUME) + 128)) + +// !!! FIXME: This needs some SIMD magic. +// !!! FIXME: Add fast-path for volume = 1 +// !!! FIXME: Use larger scales for 16-bit/32-bit integers + +bool SDL_MixAudio(Uint8 *dst, const Uint8 *src, SDL_AudioFormat format, Uint32 len, float fvolume) +{ + int volume = (int)SDL_roundf(fvolume * MIX_MAXVOLUME); + + if (volume == 0) { + return true; + } + + switch (format) { + + case SDL_AUDIO_U8: + { + Uint8 src_sample; + + while (len--) { + src_sample = *src; + ADJUST_VOLUME_U8(src_sample, volume); + *dst = mix8[*dst + src_sample]; + ++dst; + ++src; + } + } break; + + case SDL_AUDIO_S8: + { + Sint8 *dst8, *src8; + Sint8 src_sample; + int dst_sample; + const int max_audioval = SDL_MAX_SINT8; + const int min_audioval = SDL_MIN_SINT8; + + src8 = (Sint8 *)src; + dst8 = (Sint8 *)dst; + while (len--) { + src_sample = *src8; + ADJUST_VOLUME(Sint8, src_sample, volume); + dst_sample = *dst8 + src_sample; + if (dst_sample > max_audioval) { + dst_sample = max_audioval; + } else if (dst_sample < min_audioval) { + dst_sample = min_audioval; + } + *dst8 = (Sint8)dst_sample; + ++dst8; + ++src8; + } + } break; + + case SDL_AUDIO_S16LE: + { + Sint16 src1, src2; + int dst_sample; + const int max_audioval = SDL_MAX_SINT16; + const int min_audioval = SDL_MIN_SINT16; + + len /= 2; + while (len--) { + src1 = SDL_Swap16LE(*(Sint16 *)src); + ADJUST_VOLUME(Sint16, src1, volume); + src2 = SDL_Swap16LE(*(Sint16 *)dst); + src += 2; + dst_sample = src1 + src2; + if (dst_sample > max_audioval) { + dst_sample = max_audioval; + } else if (dst_sample < min_audioval) { + dst_sample = min_audioval; + } + *(Sint16 *)dst = SDL_Swap16LE((Sint16)dst_sample); + dst += 2; + } + } break; + + case SDL_AUDIO_S16BE: + { + Sint16 src1, src2; + int dst_sample; + const int max_audioval = SDL_MAX_SINT16; + const int min_audioval = SDL_MIN_SINT16; + + len /= 2; + while (len--) { + src1 = SDL_Swap16BE(*(Sint16 *)src); + ADJUST_VOLUME(Sint16, src1, volume); + src2 = SDL_Swap16BE(*(Sint16 *)dst); + src += 2; + dst_sample = src1 + src2; + if (dst_sample > max_audioval) { + dst_sample = max_audioval; + } else if (dst_sample < min_audioval) { + dst_sample = min_audioval; + } + *(Sint16 *)dst = SDL_Swap16BE((Sint16)dst_sample); + dst += 2; + } + } break; + + case SDL_AUDIO_S32LE: + { + const Uint32 *src32 = (Uint32 *)src; + Uint32 *dst32 = (Uint32 *)dst; + Sint64 src1, src2; + Sint64 dst_sample; + const Sint64 max_audioval = SDL_MAX_SINT32; + const Sint64 min_audioval = SDL_MIN_SINT32; + + len /= 4; + while (len--) { + src1 = (Sint64)((Sint32)SDL_Swap32LE(*src32)); + src32++; + ADJUST_VOLUME(Sint64, src1, volume); + src2 = (Sint64)((Sint32)SDL_Swap32LE(*dst32)); + dst_sample = src1 + src2; + if (dst_sample > max_audioval) { + dst_sample = max_audioval; + } else if (dst_sample < min_audioval) { + dst_sample = min_audioval; + } + *(dst32++) = SDL_Swap32LE((Uint32)((Sint32)dst_sample)); + } + } break; + + case SDL_AUDIO_S32BE: + { + const Uint32 *src32 = (Uint32 *)src; + Uint32 *dst32 = (Uint32 *)dst; + Sint64 src1, src2; + Sint64 dst_sample; + const Sint64 max_audioval = SDL_MAX_SINT32; + const Sint64 min_audioval = SDL_MIN_SINT32; + + len /= 4; + while (len--) { + src1 = (Sint64)((Sint32)SDL_Swap32BE(*src32)); + src32++; + ADJUST_VOLUME(Sint64, src1, volume); + src2 = (Sint64)((Sint32)SDL_Swap32BE(*dst32)); + dst_sample = src1 + src2; + if (dst_sample > max_audioval) { + dst_sample = max_audioval; + } else if (dst_sample < min_audioval) { + dst_sample = min_audioval; + } + *(dst32++) = SDL_Swap32BE((Uint32)((Sint32)dst_sample)); + } + } break; + + case SDL_AUDIO_F32LE: + { + const float *src32 = (float *)src; + float *dst32 = (float *)dst; + float src1, src2; + float dst_sample; + const float max_audioval = 1.0f; + const float min_audioval = -1.0f; + + len /= 4; + while (len--) { + src1 = SDL_SwapFloatLE(*src32) * fvolume; + src2 = SDL_SwapFloatLE(*dst32); + src32++; + + dst_sample = src1 + src2; + if (dst_sample > max_audioval) { + dst_sample = max_audioval; + } else if (dst_sample < min_audioval) { + dst_sample = min_audioval; + } + *(dst32++) = SDL_SwapFloatLE(dst_sample); + } + } break; + + case SDL_AUDIO_F32BE: + { + const float *src32 = (float *)src; + float *dst32 = (float *)dst; + float src1, src2; + float dst_sample; + const float max_audioval = 1.0f; + const float min_audioval = -1.0f; + + len /= 4; + while (len--) { + src1 = SDL_SwapFloatBE(*src32) * fvolume; + src2 = SDL_SwapFloatBE(*dst32); + src32++; + + dst_sample = src1 + src2; + if (dst_sample > max_audioval) { + dst_sample = max_audioval; + } else if (dst_sample < min_audioval) { + dst_sample = min_audioval; + } + *(dst32++) = SDL_SwapFloatBE(dst_sample); + } + } break; + + default: // If this happens... FIXME! + return SDL_SetError("SDL_MixAudio(): unknown audio format"); + } + + return true; +} diff --git a/contrib/SDL-3.2.8/src/audio/SDL_sysaudio.h b/contrib/SDL-3.2.8/src/audio/SDL_sysaudio.h new file mode 100644 index 0000000..4a88bd2 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_sysaudio.h @@ -0,0 +1,392 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifndef SDL_sysaudio_h_ +#define SDL_sysaudio_h_ + +#define DEBUG_AUDIOSTREAM 0 +#define DEBUG_AUDIO_CONVERT 0 + +#if DEBUG_AUDIO_CONVERT +#define LOG_DEBUG_AUDIO_CONVERT(from, to) SDL_Log("SDL_AUDIO_CONVERT: Converting %s to %s.", from, to); +#else +#define LOG_DEBUG_AUDIO_CONVERT(from, to) +#endif + +// !!! FIXME: These are wordy and unlocalized... +#define DEFAULT_PLAYBACK_DEVNAME "System audio playback device" +#define DEFAULT_RECORDING_DEVNAME "System audio recording device" + +// these are used when no better specifics are known. We default to CD audio quality. +#define DEFAULT_AUDIO_PLAYBACK_FORMAT SDL_AUDIO_S16 +#define DEFAULT_AUDIO_PLAYBACK_CHANNELS 2 +#define DEFAULT_AUDIO_PLAYBACK_FREQUENCY 44100 + +#define DEFAULT_AUDIO_RECORDING_FORMAT SDL_AUDIO_S16 +#define DEFAULT_AUDIO_RECORDING_CHANNELS 1 +#define DEFAULT_AUDIO_RECORDING_FREQUENCY 44100 + +#define SDL_MAX_CHANNELMAP_CHANNELS 8 // !!! FIXME: if SDL ever supports more channels, clean this out and make those parts dynamic. + +typedef struct SDL_AudioDevice SDL_AudioDevice; +typedef struct SDL_LogicalAudioDevice SDL_LogicalAudioDevice; + +// Used by src/SDL.c to initialize a particular audio driver. +extern bool SDL_InitAudio(const char *driver_name); + +// Used by src/SDL.c to shut down previously-initialized audio. +extern void SDL_QuitAudio(void); + +// Function to get a list of audio formats, ordered most similar to `format` to least, 0-terminated. Don't free results. +const SDL_AudioFormat *SDL_ClosestAudioFormats(SDL_AudioFormat format); + +// Must be called at least once before using converters. +extern void SDL_ChooseAudioConverters(void); +extern void SDL_SetupAudioResampler(void); + +/* Backends should call this as devices are added to the system (such as + a USB headset being plugged in), and should also be called for + for every device found during DetectDevices(). */ +extern SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const SDL_AudioSpec *spec, void *handle); + +/* Backends should call this if an opened audio device is lost. + This can happen due to i/o errors, or a device being unplugged, etc. */ +extern void SDL_AudioDeviceDisconnected(SDL_AudioDevice *device); + +// Backends should call this if the system default device changes. +extern void SDL_DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device); + +// Backends should call this if a device's format is changing (opened or not); SDL will update state and carry on with the new format. +extern bool SDL_AudioDeviceFormatChanged(SDL_AudioDevice *device, const SDL_AudioSpec *newspec, int new_sample_frames); + +// Same as above, but assume the device is already locked. +extern bool SDL_AudioDeviceFormatChangedAlreadyLocked(SDL_AudioDevice *device, const SDL_AudioSpec *newspec, int new_sample_frames); + +// Find the SDL_AudioDevice associated with the handle supplied to SDL_AddAudioDevice. NULL if not found. DOES NOT LOCK THE DEVICE. +extern SDL_AudioDevice *SDL_FindPhysicalAudioDeviceByHandle(void *handle); + +// Find an SDL_AudioDevice, selected by a callback. NULL if not found. DOES NOT LOCK THE DEVICE. +extern SDL_AudioDevice *SDL_FindPhysicalAudioDeviceByCallback(bool (*callback)(SDL_AudioDevice *device, void *userdata), void *userdata); + +// Backends should call this if they change the device format, channels, freq, or sample_frames to keep other state correct. +extern void SDL_UpdatedAudioDeviceFormat(SDL_AudioDevice *device); + +// Backends can call this to get a reasonable default sample frame count for a device's sample rate. +int SDL_GetDefaultSampleFramesFromFreq(const int freq); + +// Backends can call this to get a standardized name for a thread to power a specific audio device. +extern char *SDL_GetAudioThreadName(SDL_AudioDevice *device, char *buf, size_t buflen); + +// Backends can call these to change a device's refcount. +extern void RefPhysicalAudioDevice(SDL_AudioDevice *device); +extern void UnrefPhysicalAudioDevice(SDL_AudioDevice *device); + +// These functions are the heart of the audio threads. Backends can call them directly if they aren't using the SDL-provided thread. +extern void SDL_PlaybackAudioThreadSetup(SDL_AudioDevice *device); +extern bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device); +extern void SDL_PlaybackAudioThreadShutdown(SDL_AudioDevice *device); +extern void SDL_RecordingAudioThreadSetup(SDL_AudioDevice *device); +extern bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device); +extern void SDL_RecordingAudioThreadShutdown(SDL_AudioDevice *device); +extern void SDL_AudioThreadFinalize(SDL_AudioDevice *device); + +extern void ConvertAudioToFloat(float *dst, const void *src, int num_samples, SDL_AudioFormat src_fmt); +extern void ConvertAudioFromFloat(void *dst, const float *src, int num_samples, SDL_AudioFormat dst_fmt); +extern void ConvertAudioSwapEndian(void* dst, const void* src, int num_samples, int bitsize); + +extern bool SDL_ChannelMapIsDefault(const int *map, int channels); +extern bool SDL_ChannelMapIsBogus(const int *map, int channels); + +// this gets used from the audio device threads. It has rules, don't use this if you don't know how to use it! +extern void ConvertAudio(int num_frames, + const void *src, SDL_AudioFormat src_format, int src_channels, const int *src_map, + void *dst, SDL_AudioFormat dst_format, int dst_channels, const int *dst_map, + void* scratch, float gain); + +// Compare two SDL_AudioSpecs, return true if they match exactly. +// Using SDL_memcmp directly isn't safe, since potential padding might not be initialized. +// either channel map can be NULL for the default (and both should be if you don't care about them). +extern bool SDL_AudioSpecsEqual(const SDL_AudioSpec *a, const SDL_AudioSpec *b, const int *channel_map_a, const int *channel_map_b); + +// See if two channel maps match +// either channel map can be NULL for the default (and both should be if you don't care about them). +extern bool SDL_AudioChannelMapsEqual(int channels, const int *channel_map_a, const int *channel_map_b); + +// allocate+copy a channel map. +extern int *SDL_ChannelMapDup(const int *origchmap, int channels); + +// Special case to let something in SDL_audiocvt.c access something in SDL_audio.c. Don't use this. +extern void OnAudioStreamCreated(SDL_AudioStream *stream); +extern void OnAudioStreamDestroy(SDL_AudioStream *stream); + +// This just lets audio playback apply logical device gain at the same time as audiostream gain, so it's one multiplication instead of thousands. +extern int SDL_GetAudioStreamDataAdjustGain(SDL_AudioStream *stream, void *voidbuf, int len, float extra_gain); + +// This is the bulk of `SDL_SetAudioStream*putChannelMap`'s work, but it lets you skip the check about changing the device end of a stream if isinput==-1. +extern bool SetAudioStreamChannelMap(SDL_AudioStream *stream, const SDL_AudioSpec *spec, int **stream_chmap, const int *chmap, int channels, int isinput); + + +typedef struct SDL_AudioDriverImpl +{ + void (*DetectDevices)(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording); + bool (*OpenDevice)(SDL_AudioDevice *device); + void (*ThreadInit)(SDL_AudioDevice *device); // Called by audio thread at start + void (*ThreadDeinit)(SDL_AudioDevice *device); // Called by audio thread at end + bool (*WaitDevice)(SDL_AudioDevice *device); + bool (*PlayDevice)(SDL_AudioDevice *device, const Uint8 *buffer, int buflen); // buffer and buflen are always from GetDeviceBuf, passed here for convenience. + Uint8 *(*GetDeviceBuf)(SDL_AudioDevice *device, int *buffer_size); + bool (*WaitRecordingDevice)(SDL_AudioDevice *device); + int (*RecordDevice)(SDL_AudioDevice *device, void *buffer, int buflen); + void (*FlushRecording)(SDL_AudioDevice *device); + void (*CloseDevice)(SDL_AudioDevice *device); + void (*FreeDeviceHandle)(SDL_AudioDevice *device); // SDL is done with this device; free the handle from SDL_AddAudioDevice() + void (*DeinitializeStart)(void); // SDL calls this, then starts destroying objects, then calls Deinitialize. This is a good place to stop hotplug detection. + void (*Deinitialize)(void); + + // Some flags to push duplicate code into the core and reduce #ifdefs. + bool ProvidesOwnCallbackThread; // !!! FIXME: rename this, it's not a callback thread anymore. + bool HasRecordingSupport; + bool OnlyHasDefaultPlaybackDevice; + bool OnlyHasDefaultRecordingDevice; // !!! FIXME: is there ever a time where you'd have a default playback and not a default recording (or vice versa)? +} SDL_AudioDriverImpl; + + +typedef struct SDL_PendingAudioDeviceEvent +{ + Uint32 type; + SDL_AudioDeviceID devid; + struct SDL_PendingAudioDeviceEvent *next; +} SDL_PendingAudioDeviceEvent; + +typedef struct SDL_AudioDriver +{ + const char *name; // The name of this audio driver + const char *desc; // The description of this audio driver + SDL_AudioDriverImpl impl; // the backend's interface + SDL_RWLock *device_hash_lock; // A rwlock that protects `device_hash` + SDL_HashTable *device_hash; // the collection of currently-available audio devices (recording, playback, logical and physical!) + SDL_AudioStream *existing_streams; // a list of all existing SDL_AudioStreams. + SDL_AudioDeviceID default_playback_device_id; + SDL_AudioDeviceID default_recording_device_id; + SDL_PendingAudioDeviceEvent pending_events; + SDL_PendingAudioDeviceEvent *pending_events_tail; + + // !!! FIXME: most (all?) of these don't have to be atomic. + SDL_AtomicInt playback_device_count; + SDL_AtomicInt recording_device_count; + SDL_AtomicInt shutting_down; // non-zero during SDL_Quit, so we known not to accept any last-minute device hotplugs. +} SDL_AudioDriver; + +struct SDL_AudioQueue; // forward decl. + +struct SDL_AudioStream +{ + SDL_Mutex* lock; + + SDL_PropertiesID props; + + SDL_AudioStreamCallback get_callback; + void *get_callback_userdata; + SDL_AudioStreamCallback put_callback; + void *put_callback_userdata; + + SDL_AudioSpec src_spec; + SDL_AudioSpec dst_spec; + int *src_chmap; + int *dst_chmap; + float freq_ratio; + float gain; + + struct SDL_AudioQueue* queue; + + SDL_AudioSpec input_spec; // The spec of input data currently being processed + int *input_chmap; + int input_chmap_storage[SDL_MAX_CHANNELMAP_CHANNELS]; // !!! FIXME: this needs to grow if SDL ever supports more channels. But if it grows, we should probably be more clever about allocations. + Sint64 resample_offset; + + Uint8 *work_buffer; // used for scratch space during data conversion/resampling. + size_t work_buffer_allocation; + + bool simplified; // true if created via SDL_OpenAudioDeviceStream + + SDL_LogicalAudioDevice *bound_device; + SDL_AudioStream *next_binding; + SDL_AudioStream *prev_binding; + + SDL_AudioStream *prev; // linked list of all existing streams (so we can free them on shutdown). + SDL_AudioStream *next; // linked list of all existing streams (so we can free them on shutdown). +}; + +/* Logical devices are an abstraction in SDL3; you can open the same physical + device multiple times, and each will result in an object with its own set + of bound audio streams, etc, even though internally these are all processed + as a group when mixing the final output for the physical device. */ +struct SDL_LogicalAudioDevice +{ + // the unique instance ID of this device. + SDL_AudioDeviceID instance_id; + + // The physical device associated with this opened device. + SDL_AudioDevice *physical_device; + + // If whole logical device is paused (process no streams bound to this device). + SDL_AtomicInt paused; + + // Volume of the device output. + float gain; + + // double-linked list of all audio streams currently bound to this opened device. + SDL_AudioStream *bound_streams; + + // true if this was opened as a default device. + bool opened_as_default; + + // true if device was opened with SDL_OpenAudioDeviceStream (so it forbids binding changes, etc). + bool simplified; + + // If non-NULL, callback into the app that lets them access the final postmix buffer. + SDL_AudioPostmixCallback postmix; + + // App-supplied pointer for postmix callback. + void *postmix_userdata; + + // double-linked list of opened devices on the same physical device. + SDL_LogicalAudioDevice *next; + SDL_LogicalAudioDevice *prev; +}; + +struct SDL_AudioDevice +{ + // A mutex for locking access to this struct + SDL_Mutex *lock; + + // A condition variable to protect device close, where we can't hold the device lock forever. + SDL_Condition *close_cond; + + // Reference count of the device; logical devices, device threads, etc, add to this. + SDL_AtomicInt refcount; + + // These are, initially, set from current_audio, but we might swap them out with Zombie versions on disconnect/failure. + bool (*WaitDevice)(SDL_AudioDevice *device); + bool (*PlayDevice)(SDL_AudioDevice *device, const Uint8 *buffer, int buflen); + Uint8 *(*GetDeviceBuf)(SDL_AudioDevice *device, int *buffer_size); + bool (*WaitRecordingDevice)(SDL_AudioDevice *device); + int (*RecordDevice)(SDL_AudioDevice *device, void *buffer, int buflen); + void (*FlushRecording)(SDL_AudioDevice *device); + + // human-readable name of the device. ("SoundBlaster Pro 16") + char *name; + + // the unique instance ID of this device. + SDL_AudioDeviceID instance_id; + + // a way for the backend to identify this device _when not opened_ + void *handle; + + // The device's current audio specification + SDL_AudioSpec spec; + + // The size, in bytes, of the device's playback/recording buffer. + int buffer_size; + + // The device's channel map, or NULL for SDL default layout. + int *chmap; + + // The device's default audio specification + SDL_AudioSpec default_spec; + + // Number of sample frames the devices wants per-buffer. + int sample_frames; + + // Value to use for SDL_memset to silence a buffer in this device's format + int silence_value; + + // non-zero if we are signaling the audio thread to end. + SDL_AtomicInt shutdown; + + // non-zero if this was a disconnected device and we're waiting for it to be decommissioned. + SDL_AtomicInt zombie; + + // true if this is a recording device instead of an playback device + bool recording; + + // true if audio thread can skip silence/mix/convert stages and just do a basic memcpy. + bool simple_copy; + + // Scratch buffers used for mixing. + Uint8 *work_buffer; + Uint8 *mix_buffer; + float *postmix_buffer; + + // Size of work_buffer (and mix_buffer) in bytes. + int work_buffer_size; + + // A thread to feed the audio device + SDL_Thread *thread; + + // true if this physical device is currently opened by the backend. + bool currently_opened; + + // Data private to this driver + struct SDL_PrivateAudioData *hidden; + + // All logical devices associated with this physical device. + SDL_LogicalAudioDevice *logical_devices; +}; + +typedef struct AudioBootStrap +{ + const char *name; + const char *desc; + bool (*init)(SDL_AudioDriverImpl *impl); + bool demand_only; // if true: request explicitly, or it won't be available. + bool is_preferred; +} AudioBootStrap; + +// Not all of these are available in a given build. Use #ifdefs, etc. +extern AudioBootStrap PRIVATEAUDIO_bootstrap; +extern AudioBootStrap PIPEWIRE_PREFERRED_bootstrap; +extern AudioBootStrap PIPEWIRE_bootstrap; +extern AudioBootStrap PULSEAUDIO_bootstrap; +extern AudioBootStrap ALSA_bootstrap; +extern AudioBootStrap JACK_bootstrap; +extern AudioBootStrap SNDIO_bootstrap; +extern AudioBootStrap NETBSDAUDIO_bootstrap; +extern AudioBootStrap DSP_bootstrap; +extern AudioBootStrap WASAPI_bootstrap; +extern AudioBootStrap DSOUND_bootstrap; +extern AudioBootStrap WINMM_bootstrap; +extern AudioBootStrap HAIKUAUDIO_bootstrap; +extern AudioBootStrap COREAUDIO_bootstrap; +extern AudioBootStrap DISKAUDIO_bootstrap; +extern AudioBootStrap DUMMYAUDIO_bootstrap; +extern AudioBootStrap AAUDIO_bootstrap; +extern AudioBootStrap OPENSLES_bootstrap; +extern AudioBootStrap PS2AUDIO_bootstrap; +extern AudioBootStrap PSPAUDIO_bootstrap; +extern AudioBootStrap VITAAUD_bootstrap; +extern AudioBootStrap N3DSAUDIO_bootstrap; +extern AudioBootStrap EMSCRIPTENAUDIO_bootstrap; +extern AudioBootStrap QSAAUDIO_bootstrap; + +#endif // SDL_sysaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/SDL_wave.c b/contrib/SDL-3.2.8/src/audio/SDL_wave.c new file mode 100644 index 0000000..1d53e79 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_wave.c @@ -0,0 +1,2151 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef HAVE_LIMITS_H +#include +#endif +#ifndef INT_MAX +SDL_COMPILE_TIME_ASSERT(int_size, sizeof(int) == sizeof(Sint32)); +#define INT_MAX SDL_MAX_SINT32 +#endif +#ifndef SIZE_MAX +#define SIZE_MAX ((size_t)-1) +#endif + +// Microsoft WAVE file loading routines + +#include "SDL_wave.h" +#include "SDL_sysaudio.h" + +/* Reads the value stored at the location of the f1 pointer, multiplies it + * with the second argument and then stores the result to f1. + * Returns 0 on success, or -1 if the multiplication overflows, in which case f1 + * does not get modified. + */ +static int SafeMult(size_t *f1, size_t f2) +{ + if (*f1 > 0 && SIZE_MAX / *f1 <= f2) { + return -1; + } + *f1 *= f2; + return 0; +} + +typedef struct ADPCM_DecoderState +{ + Uint32 channels; // Number of channels. + size_t blocksize; // Size of an ADPCM block in bytes. + size_t blockheadersize; // Size of an ADPCM block header in bytes. + size_t samplesperblock; // Number of samples per channel in an ADPCM block. + size_t framesize; // Size of a sample frame (16-bit PCM) in bytes. + Sint64 framestotal; // Total number of sample frames. + Sint64 framesleft; // Number of sample frames still to be decoded. + void *ddata; // Decoder data from initialization. + void *cstate; // Decoding state for each channel. + + // ADPCM data. + struct + { + Uint8 *data; + size_t size; + size_t pos; + } input; + + // Current ADPCM block in the ADPCM data above. + struct + { + Uint8 *data; + size_t size; + size_t pos; + } block; + + // Decoded 16-bit PCM data. + struct + { + Sint16 *data; + size_t size; + size_t pos; + } output; +} ADPCM_DecoderState; + +typedef struct MS_ADPCM_CoeffData +{ + Uint16 coeffcount; + Sint16 *coeff; + Sint16 aligndummy; // Has to be last member. +} MS_ADPCM_CoeffData; + +typedef struct MS_ADPCM_ChannelState +{ + Uint16 delta; + Sint16 coeff1; + Sint16 coeff2; +} MS_ADPCM_ChannelState; + +#ifdef SDL_WAVE_DEBUG_LOG_FORMAT +static void WaveDebugLogFormat(WaveFile *file) +{ + WaveFormat *format = &file->format; + const char *fmtstr = "WAVE file: %s, %u Hz, %s, %u bits, %u %s/s"; + const char *waveformat, *wavechannel, *wavebpsunit = "B"; + Uint32 wavebps = format->byterate; + char channelstr[64]; + + SDL_zeroa(channelstr); + + switch (format->encoding) { + case PCM_CODE: + waveformat = "PCM"; + break; + case IEEE_FLOAT_CODE: + waveformat = "IEEE Float"; + break; + case ALAW_CODE: + waveformat = "A-law"; + break; + case MULAW_CODE: + waveformat = "\xc2\xb5-law"; + break; + case MS_ADPCM_CODE: + waveformat = "MS ADPCM"; + break; + case IMA_ADPCM_CODE: + waveformat = "IMA ADPCM"; + break; + default: + waveformat = "Unknown"; + break; + } + +#define SDL_WAVE_DEBUG_CHANNELCFG(STR, CODE) \ + case CODE: \ + wavechannel = STR; \ + break; +#define SDL_WAVE_DEBUG_CHANNELSTR(STR, CODE) \ + if (format->channelmask & CODE) { \ + SDL_strlcat(channelstr, channelstr[0] ? "-" STR : STR, sizeof(channelstr)); \ + } + + if (format->formattag == EXTENSIBLE_CODE && format->channelmask > 0) { + switch (format->channelmask) { + SDL_WAVE_DEBUG_CHANNELCFG("1.0 Mono", 0x4) + SDL_WAVE_DEBUG_CHANNELCFG("1.1 Mono", 0xc) + SDL_WAVE_DEBUG_CHANNELCFG("2.0 Stereo", 0x3) + SDL_WAVE_DEBUG_CHANNELCFG("2.1 Stereo", 0xb) + SDL_WAVE_DEBUG_CHANNELCFG("3.0 Stereo", 0x7) + SDL_WAVE_DEBUG_CHANNELCFG("3.1 Stereo", 0xf) + SDL_WAVE_DEBUG_CHANNELCFG("3.0 Surround", 0x103) + SDL_WAVE_DEBUG_CHANNELCFG("3.1 Surround", 0x10b) + SDL_WAVE_DEBUG_CHANNELCFG("4.0 Quad", 0x33) + SDL_WAVE_DEBUG_CHANNELCFG("4.1 Quad", 0x3b) + SDL_WAVE_DEBUG_CHANNELCFG("4.0 Surround", 0x107) + SDL_WAVE_DEBUG_CHANNELCFG("4.1 Surround", 0x10f) + SDL_WAVE_DEBUG_CHANNELCFG("5.0", 0x37) + SDL_WAVE_DEBUG_CHANNELCFG("5.1", 0x3f) + SDL_WAVE_DEBUG_CHANNELCFG("5.0 Side", 0x607) + SDL_WAVE_DEBUG_CHANNELCFG("5.1 Side", 0x60f) + SDL_WAVE_DEBUG_CHANNELCFG("6.0", 0x137) + SDL_WAVE_DEBUG_CHANNELCFG("6.1", 0x13f) + SDL_WAVE_DEBUG_CHANNELCFG("6.0 Side", 0x707) + SDL_WAVE_DEBUG_CHANNELCFG("6.1 Side", 0x70f) + SDL_WAVE_DEBUG_CHANNELCFG("7.0", 0xf7) + SDL_WAVE_DEBUG_CHANNELCFG("7.1", 0xff) + SDL_WAVE_DEBUG_CHANNELCFG("7.0 Side", 0x6c7) + SDL_WAVE_DEBUG_CHANNELCFG("7.1 Side", 0x6cf) + SDL_WAVE_DEBUG_CHANNELCFG("7.0 Surround", 0x637) + SDL_WAVE_DEBUG_CHANNELCFG("7.1 Surround", 0x63f) + SDL_WAVE_DEBUG_CHANNELCFG("9.0 Surround", 0x5637) + SDL_WAVE_DEBUG_CHANNELCFG("9.1 Surround", 0x563f) + SDL_WAVE_DEBUG_CHANNELCFG("11.0 Surround", 0x56f7) + SDL_WAVE_DEBUG_CHANNELCFG("11.1 Surround", 0x56ff) + default: + SDL_WAVE_DEBUG_CHANNELSTR("FL", 0x1) + SDL_WAVE_DEBUG_CHANNELSTR("FR", 0x2) + SDL_WAVE_DEBUG_CHANNELSTR("FC", 0x4) + SDL_WAVE_DEBUG_CHANNELSTR("LF", 0x8) + SDL_WAVE_DEBUG_CHANNELSTR("BL", 0x10) + SDL_WAVE_DEBUG_CHANNELSTR("BR", 0x20) + SDL_WAVE_DEBUG_CHANNELSTR("FLC", 0x40) + SDL_WAVE_DEBUG_CHANNELSTR("FRC", 0x80) + SDL_WAVE_DEBUG_CHANNELSTR("BC", 0x100) + SDL_WAVE_DEBUG_CHANNELSTR("SL", 0x200) + SDL_WAVE_DEBUG_CHANNELSTR("SR", 0x400) + SDL_WAVE_DEBUG_CHANNELSTR("TC", 0x800) + SDL_WAVE_DEBUG_CHANNELSTR("TFL", 0x1000) + SDL_WAVE_DEBUG_CHANNELSTR("TFC", 0x2000) + SDL_WAVE_DEBUG_CHANNELSTR("TFR", 0x4000) + SDL_WAVE_DEBUG_CHANNELSTR("TBL", 0x8000) + SDL_WAVE_DEBUG_CHANNELSTR("TBC", 0x10000) + SDL_WAVE_DEBUG_CHANNELSTR("TBR", 0x20000) + break; + } + } else { + switch (format->channels) { + default: + if (SDL_snprintf(channelstr, sizeof(channelstr), "%u channels", format->channels) >= 0) { + wavechannel = channelstr; + break; + } + case 0: + wavechannel = "Unknown"; + break; + case 1: + wavechannel = "Mono"; + break; + case 2: + wavechannel = "Setero"; + break; + } + } + +#undef SDL_WAVE_DEBUG_CHANNELCFG +#undef SDL_WAVE_DEBUG_CHANNELSTR + + if (wavebps >= 1024) { + wavebpsunit = "KiB"; + wavebps = wavebps / 1024 + (wavebps & 0x3ff ? 1 : 0); + } + + SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, fmtstr, waveformat, format->frequency, wavechannel, format->bitspersample, wavebps, wavebpsunit); +} +#endif + +#ifdef SDL_WAVE_DEBUG_DUMP_FORMAT +static void WaveDebugDumpFormat(WaveFile *file, Uint32 rifflen, Uint32 fmtlen, Uint32 datalen) +{ + WaveFormat *format = &file->format; + const char *fmtstr1 = "WAVE chunk dump:\n" + "-------------------------------------------\n" + "RIFF %11u\n" + "-------------------------------------------\n" + " fmt %11u\n" + " wFormatTag 0x%04x\n" + " nChannels %11u\n" + " nSamplesPerSec %11u\n" + " nAvgBytesPerSec %11u\n" + " nBlockAlign %11u\n"; + const char *fmtstr2 = " wBitsPerSample %11u\n"; + const char *fmtstr3 = " cbSize %11u\n"; + const char *fmtstr4a = " wValidBitsPerSample %11u\n"; + const char *fmtstr4b = " wSamplesPerBlock %11u\n"; + const char *fmtstr5 = " dwChannelMask 0x%08x\n" + " SubFormat\n" + " %08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x\n"; + const char *fmtstr6 = "-------------------------------------------\n" + " fact\n" + " dwSampleLength %11u\n"; + const char *fmtstr7 = "-------------------------------------------\n" + " data %11u\n" + "-------------------------------------------\n"; + char *dumpstr; + size_t dumppos = 0; + const size_t bufsize = 1024; + int res; + + dumpstr = SDL_malloc(bufsize); + if (!dumpstr) { + return; + } + dumpstr[0] = 0; + + res = SDL_snprintf(dumpstr, bufsize, fmtstr1, rifflen, fmtlen, format->formattag, format->channels, format->frequency, format->byterate, format->blockalign); + dumppos += res > 0 ? res : 0; + if (fmtlen >= 16) { + res = SDL_snprintf(dumpstr + dumppos, bufsize - dumppos, fmtstr2, format->bitspersample); + dumppos += res > 0 ? res : 0; + } + if (fmtlen >= 18) { + res = SDL_snprintf(dumpstr + dumppos, bufsize - dumppos, fmtstr3, format->extsize); + dumppos += res > 0 ? res : 0; + } + if (format->formattag == EXTENSIBLE_CODE && fmtlen >= 40 && format->extsize >= 22) { + const Uint8 *g = format->subformat; + const Uint32 g1 = g[0] | ((Uint32)g[1] << 8) | ((Uint32)g[2] << 16) | ((Uint32)g[3] << 24); + const Uint32 g2 = g[4] | ((Uint32)g[5] << 8); + const Uint32 g3 = g[6] | ((Uint32)g[7] << 8); + + switch (format->encoding) { + default: + res = SDL_snprintf(dumpstr + dumppos, bufsize - dumppos, fmtstr4a, format->validsamplebits); + dumppos += res > 0 ? res : 0; + break; + case MS_ADPCM_CODE: + case IMA_ADPCM_CODE: + res = SDL_snprintf(dumpstr + dumppos, bufsize - dumppos, fmtstr4b, format->samplesperblock); + dumppos += res > 0 ? res : 0; + break; + } + res = SDL_snprintf(dumpstr + dumppos, bufsize - dumppos, fmtstr5, format->channelmask, g1, g2, g3, g[8], g[9], g[10], g[11], g[12], g[13], g[14], g[15]); + dumppos += res > 0 ? res : 0; + } else { + switch (format->encoding) { + case MS_ADPCM_CODE: + case IMA_ADPCM_CODE: + if (fmtlen >= 20 && format->extsize >= 2) { + res = SDL_snprintf(dumpstr + dumppos, bufsize - dumppos, fmtstr4b, format->samplesperblock); + dumppos += res > 0 ? res : 0; + } + break; + } + } + if (file->fact.status >= 1) { + res = SDL_snprintf(dumpstr + dumppos, bufsize - dumppos, fmtstr6, file->fact.samplelength); + dumppos += res > 0 ? res : 0; + } + res = SDL_snprintf(dumpstr + dumppos, bufsize - dumppos, fmtstr7, datalen); + dumppos += res > 0 ? res : 0; + + SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "%s", dumpstr); + + SDL_free(dumpstr); +} +#endif + +static Sint64 WaveAdjustToFactValue(WaveFile *file, Sint64 sampleframes) +{ + if (file->fact.status == 2) { + if (file->facthint == FactStrict && sampleframes < file->fact.samplelength) { + SDL_SetError("Invalid number of sample frames in WAVE fact chunk (too many)"); + return -1; + } else if (sampleframes > file->fact.samplelength) { + return file->fact.samplelength; + } + } + + return sampleframes; +} + +static bool MS_ADPCM_CalculateSampleFrames(WaveFile *file, size_t datalength) +{ + WaveFormat *format = &file->format; + const size_t blockheadersize = (size_t)file->format.channels * 7; + const size_t availableblocks = datalength / file->format.blockalign; + const size_t blockframebitsize = (size_t)file->format.bitspersample * file->format.channels; + const size_t trailingdata = datalength % file->format.blockalign; + + if (file->trunchint == TruncVeryStrict || file->trunchint == TruncStrict) { + // The size of the data chunk must be a multiple of the block size. + if (datalength < blockheadersize || trailingdata > 0) { + return SDL_SetError("Truncated MS ADPCM block"); + } + } + + // Calculate number of sample frames that will be decoded. + file->sampleframes = (Sint64)availableblocks * format->samplesperblock; + if (trailingdata > 0) { + // The last block is truncated. Check if we can get any samples out of it. + if (file->trunchint == TruncDropFrame) { + // Drop incomplete sample frame. + if (trailingdata >= blockheadersize) { + size_t trailingsamples = 2 + (trailingdata - blockheadersize) * 8 / blockframebitsize; + if (trailingsamples > format->samplesperblock) { + trailingsamples = format->samplesperblock; + } + file->sampleframes += trailingsamples; + } + } + } + + file->sampleframes = WaveAdjustToFactValue(file, file->sampleframes); + if (file->sampleframes < 0) { + return false; + } + + return true; +} + +static bool MS_ADPCM_Init(WaveFile *file, size_t datalength) +{ + WaveFormat *format = &file->format; + WaveChunk *chunk = &file->chunk; + const size_t blockheadersize = (size_t)format->channels * 7; + const size_t blockdatasize = (size_t)format->blockalign - blockheadersize; + const size_t blockframebitsize = (size_t)format->bitspersample * format->channels; + const size_t blockdatasamples = (blockdatasize * 8) / blockframebitsize; + const Sint16 presetcoeffs[14] = { 256, 0, 512, -256, 0, 0, 192, 64, 240, 0, 460, -208, 392, -232 }; + size_t i, coeffcount; + MS_ADPCM_CoeffData *coeffdata; + + // Sanity checks. + + /* While it's clear how IMA ADPCM handles more than two channels, the nibble + * order of MS ADPCM makes it awkward. The Standards Update does not talk + * about supporting more than stereo anyway. + */ + if (format->channels > 2) { + return SDL_SetError("Invalid number of channels"); + } + + if (format->bitspersample != 4) { + return SDL_SetError("Invalid MS ADPCM bits per sample of %u", (unsigned int)format->bitspersample); + } + + // The block size must be big enough to contain the block header. + if (format->blockalign < blockheadersize) { + return SDL_SetError("Invalid MS ADPCM block size (nBlockAlign)"); + } + + if (format->formattag == EXTENSIBLE_CODE) { + /* Does have a GUID (like all format tags), but there's no specification + * for how the data is packed into the extensible header. Making + * assumptions here could lead to new formats nobody wants to support. + */ + return SDL_SetError("MS ADPCM with the extensible header is not supported"); + } + + /* There are wSamplesPerBlock, wNumCoef, and at least 7 coefficient pairs in + * the extended part of the header. + */ + if (chunk->size < 22) { + return SDL_SetError("Could not read MS ADPCM format header"); + } + + format->samplesperblock = chunk->data[18] | ((Uint16)chunk->data[19] << 8); + // Number of coefficient pairs. A pair has two 16-bit integers. + coeffcount = chunk->data[20] | ((size_t)chunk->data[21] << 8); + /* bPredictor, the integer offset into the coefficients array, is only + * 8 bits. It can only address the first 256 coefficients. Let's limit + * the count number here. + */ + if (coeffcount > 256) { + coeffcount = 256; + } + + if (chunk->size < 22 + coeffcount * 4) { + return SDL_SetError("Could not read custom coefficients in MS ADPCM format header"); + } else if (format->extsize < 4 + coeffcount * 4) { + return SDL_SetError("Invalid MS ADPCM format header (too small)"); + } else if (coeffcount < 7) { + return SDL_SetError("Missing required coefficients in MS ADPCM format header"); + } + + coeffdata = (MS_ADPCM_CoeffData *)SDL_malloc(sizeof(MS_ADPCM_CoeffData) + coeffcount * 4); + file->decoderdata = coeffdata; // Freed in cleanup. + if (!coeffdata) { + return false; + } + coeffdata->coeff = &coeffdata->aligndummy; + coeffdata->coeffcount = (Uint16)coeffcount; + + // Copy the 16-bit pairs. + for (i = 0; i < coeffcount * 2; i++) { + Sint32 c = chunk->data[22 + i * 2] | ((Sint32)chunk->data[23 + i * 2] << 8); + if (c >= 0x8000) { + c -= 0x10000; + } + if (i < 14 && c != presetcoeffs[i]) { + return SDL_SetError("Wrong preset coefficients in MS ADPCM format header"); + } + coeffdata->coeff[i] = (Sint16)c; + } + + /* Technically, wSamplesPerBlock is required, but we have all the + * information in the other fields to calculate it, if it's zero. + */ + if (format->samplesperblock == 0) { + /* Let's be nice to the encoders that didn't know how to fill this. + * The Standards Update calculates it this way: + * + * x = Block size (in bits) minus header size (in bits) + * y = Bit depth multiplied by channel count + * z = Number of samples per channel in block header + * wSamplesPerBlock = x / y + z + */ + format->samplesperblock = (Uint32)blockdatasamples + 2; + } + + /* nBlockAlign can be in conflict with wSamplesPerBlock. For example, if + * the number of samples doesn't fit into the block. The Standards Update + * also describes wSamplesPerBlock with a formula that makes it necessary to + * always fill the block with the maximum amount of samples, but this is not + * enforced here as there are no compatibility issues. + * A truncated block header with just one sample is not supported. + */ + if (format->samplesperblock == 1 || blockdatasamples < format->samplesperblock - 2) { + return SDL_SetError("Invalid number of samples per MS ADPCM block (wSamplesPerBlock)"); + } + + if (!MS_ADPCM_CalculateSampleFrames(file, datalength)) { + return false; + } + + return true; +} + +static Sint16 MS_ADPCM_ProcessNibble(MS_ADPCM_ChannelState *cstate, Sint32 sample1, Sint32 sample2, Uint8 nybble) +{ + const Sint32 max_audioval = 32767; + const Sint32 min_audioval = -32768; + const Uint16 max_deltaval = 65535; + const Uint16 adaptive[] = { + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230 + }; + Sint32 new_sample; + Sint32 errordelta; + Uint32 delta = cstate->delta; + + new_sample = (sample1 * cstate->coeff1 + sample2 * cstate->coeff2) / 256; + // The nibble is a signed 4-bit error delta. + errordelta = (Sint32)nybble - (nybble >= 0x08 ? 0x10 : 0); + new_sample += (Sint32)delta * errordelta; + if (new_sample < min_audioval) { + new_sample = min_audioval; + } else if (new_sample > max_audioval) { + new_sample = max_audioval; + } + delta = (delta * adaptive[nybble]) / 256; + if (delta < 16) { + delta = 16; + } else if (delta > max_deltaval) { + /* This issue is not described in the Standards Update and therefore + * undefined. It seems sensible to prevent overflows with a limit. + */ + delta = max_deltaval; + } + + cstate->delta = (Uint16)delta; + return (Sint16)new_sample; +} + +static bool MS_ADPCM_DecodeBlockHeader(ADPCM_DecoderState *state) +{ + Uint8 coeffindex; + const Uint32 channels = state->channels; + Sint32 sample; + Uint32 c; + MS_ADPCM_ChannelState *cstate = (MS_ADPCM_ChannelState *)state->cstate; + MS_ADPCM_CoeffData *ddata = (MS_ADPCM_CoeffData *)state->ddata; + + for (c = 0; c < channels; c++) { + size_t o = c; + + // Load the coefficient pair into the channel state. + coeffindex = state->block.data[o]; + if (coeffindex > ddata->coeffcount) { + return SDL_SetError("Invalid MS ADPCM coefficient index in block header"); + } + cstate[c].coeff1 = ddata->coeff[coeffindex * 2]; + cstate[c].coeff2 = ddata->coeff[coeffindex * 2 + 1]; + + // Initial delta value. + o = (size_t)channels + c * 2; + cstate[c].delta = state->block.data[o] | ((Uint16)state->block.data[o + 1] << 8); + + /* Load the samples from the header. Interestingly, the sample later in + * the output stream comes first. + */ + o = (size_t)channels * 3 + c * 2; + sample = state->block.data[o] | ((Sint32)state->block.data[o + 1] << 8); + if (sample >= 0x8000) { + sample -= 0x10000; + } + state->output.data[state->output.pos + channels] = (Sint16)sample; + + o = (size_t)channels * 5 + c * 2; + sample = state->block.data[o] | ((Sint32)state->block.data[o + 1] << 8); + if (sample >= 0x8000) { + sample -= 0x10000; + } + state->output.data[state->output.pos] = (Sint16)sample; + + state->output.pos++; + } + + state->block.pos += state->blockheadersize; + + // Skip second sample frame that came from the header. + state->output.pos += state->channels; + + // Header provided two sample frames. + state->framesleft -= 2; + + return true; +} + +/* Decodes the data of the MS ADPCM block. Decoding will stop if a block is too + * short, returning with none or partially decoded data. The partial data + * will always contain full sample frames (same sample count for each channel). + * Incomplete sample frames are discarded. + */ +static bool MS_ADPCM_DecodeBlockData(ADPCM_DecoderState *state) +{ + Uint16 nybble = 0; + Sint16 sample1, sample2; + const Uint32 channels = state->channels; + Uint32 c; + MS_ADPCM_ChannelState *cstate = (MS_ADPCM_ChannelState *)state->cstate; + + size_t blockpos = state->block.pos; + size_t blocksize = state->block.size; + + size_t outpos = state->output.pos; + + Sint64 blockframesleft = state->samplesperblock - 2; + if (blockframesleft > state->framesleft) { + blockframesleft = state->framesleft; + } + + while (blockframesleft > 0) { + for (c = 0; c < channels; c++) { + if (nybble & 0x4000) { + nybble <<= 4; + } else if (blockpos < blocksize) { + nybble = state->block.data[blockpos++] | 0x4000; + } else { + // Out of input data. Drop the incomplete frame and return. + state->output.pos = outpos - c; + return false; + } + + // Load previous samples which may come from the block header. + sample1 = state->output.data[outpos - channels]; + sample2 = state->output.data[outpos - channels * 2]; + + sample1 = MS_ADPCM_ProcessNibble(cstate + c, sample1, sample2, (nybble >> 4) & 0x0f); + state->output.data[outpos++] = sample1; + } + + state->framesleft--; + blockframesleft--; + } + + state->output.pos = outpos; + + return true; +} + +static bool MS_ADPCM_Decode(WaveFile *file, Uint8 **audio_buf, Uint32 *audio_len) +{ + bool result; + size_t bytesleft, outputsize; + WaveChunk *chunk = &file->chunk; + ADPCM_DecoderState state; + MS_ADPCM_ChannelState cstate[2]; + + SDL_zero(state); + SDL_zeroa(cstate); + + if (chunk->size != chunk->length) { + // Could not read everything. Recalculate number of sample frames. + if (!MS_ADPCM_CalculateSampleFrames(file, chunk->size)) { + return false; + } + } + + // Nothing to decode, nothing to return. + if (file->sampleframes == 0) { + *audio_buf = NULL; + *audio_len = 0; + return true; + } + + state.blocksize = file->format.blockalign; + state.channels = file->format.channels; + state.blockheadersize = (size_t)state.channels * 7; + state.samplesperblock = file->format.samplesperblock; + state.framesize = state.channels * sizeof(Sint16); + state.ddata = file->decoderdata; + state.framestotal = file->sampleframes; + state.framesleft = state.framestotal; + + state.input.data = chunk->data; + state.input.size = chunk->size; + state.input.pos = 0; + + // The output size in bytes. May get modified if data is truncated. + outputsize = (size_t)state.framestotal; + if (SafeMult(&outputsize, state.framesize)) { + return SDL_SetError("WAVE file too big"); + } else if (outputsize > SDL_MAX_UINT32 || state.framestotal > SIZE_MAX) { + return SDL_SetError("WAVE file too big"); + } + + state.output.pos = 0; + state.output.size = outputsize / sizeof(Sint16); + state.output.data = (Sint16 *)SDL_calloc(1, outputsize); + if (!state.output.data) { + return false; + } + + state.cstate = cstate; + + // Decode block by block. A truncated block will stop the decoding. + bytesleft = state.input.size - state.input.pos; + while (state.framesleft > 0 && bytesleft >= state.blockheadersize) { + state.block.data = state.input.data + state.input.pos; + state.block.size = bytesleft < state.blocksize ? bytesleft : state.blocksize; + state.block.pos = 0; + + if (state.output.size - state.output.pos < (Uint64)state.framesleft * state.channels) { + // Somehow didn't allocate enough space for the output. + SDL_free(state.output.data); + return SDL_SetError("Unexpected overflow in MS ADPCM decoder"); + } + + // Initialize decoder with the values from the block header. + result = MS_ADPCM_DecodeBlockHeader(&state); + if (!result) { + SDL_free(state.output.data); + return false; + } + + // Decode the block data. It stores the samples directly in the output. + result = MS_ADPCM_DecodeBlockData(&state); + if (!result) { + // Unexpected end. Stop decoding and return partial data if necessary. + if (file->trunchint == TruncVeryStrict || file->trunchint == TruncStrict) { + SDL_free(state.output.data); + return SDL_SetError("Truncated data chunk"); + } else if (file->trunchint != TruncDropFrame) { + state.output.pos -= state.output.pos % (state.samplesperblock * state.channels); + } + outputsize = state.output.pos * sizeof(Sint16); // Can't overflow, is always smaller. + break; + } + + state.input.pos += state.block.size; + bytesleft = state.input.size - state.input.pos; + } + + *audio_buf = (Uint8 *)state.output.data; + *audio_len = (Uint32)outputsize; + + return true; +} + +static bool IMA_ADPCM_CalculateSampleFrames(WaveFile *file, size_t datalength) +{ + WaveFormat *format = &file->format; + const size_t blockheadersize = (size_t)format->channels * 4; + const size_t subblockframesize = (size_t)format->channels * 4; + const size_t availableblocks = datalength / format->blockalign; + const size_t trailingdata = datalength % format->blockalign; + + if (file->trunchint == TruncVeryStrict || file->trunchint == TruncStrict) { + // The size of the data chunk must be a multiple of the block size. + if (datalength < blockheadersize || trailingdata > 0) { + return SDL_SetError("Truncated IMA ADPCM block"); + } + } + + // Calculate number of sample frames that will be decoded. + file->sampleframes = (Uint64)availableblocks * format->samplesperblock; + if (trailingdata > 0) { + // The last block is truncated. Check if we can get any samples out of it. + if (file->trunchint == TruncDropFrame && trailingdata > blockheadersize - 2) { + /* The sample frame in the header of the truncated block is present. + * Drop incomplete sample frames. + */ + size_t trailingsamples = 1; + + if (trailingdata > blockheadersize) { + // More data following after the header. + const size_t trailingblockdata = trailingdata - blockheadersize; + const size_t trailingsubblockdata = trailingblockdata % subblockframesize; + trailingsamples += (trailingblockdata / subblockframesize) * 8; + /* Due to the interleaved sub-blocks, the last 4 bytes determine + * how many samples of the truncated sub-block are lost. + */ + if (trailingsubblockdata > subblockframesize - 4) { + trailingsamples += (trailingsubblockdata % 4) * 2; + } + } + + if (trailingsamples > format->samplesperblock) { + trailingsamples = format->samplesperblock; + } + file->sampleframes += trailingsamples; + } + } + + file->sampleframes = WaveAdjustToFactValue(file, file->sampleframes); + if (file->sampleframes < 0) { + return false; + } + + return true; +} + +static bool IMA_ADPCM_Init(WaveFile *file, size_t datalength) +{ + WaveFormat *format = &file->format; + WaveChunk *chunk = &file->chunk; + const size_t blockheadersize = (size_t)format->channels * 4; + const size_t blockdatasize = (size_t)format->blockalign - blockheadersize; + const size_t blockframebitsize = (size_t)format->bitspersample * format->channels; + const size_t blockdatasamples = (blockdatasize * 8) / blockframebitsize; + + // Sanity checks. + + // IMA ADPCM can also have 3-bit samples, but it's not supported by SDL at this time. + if (format->bitspersample == 3) { + return SDL_SetError("3-bit IMA ADPCM currently not supported"); + } else if (format->bitspersample != 4) { + return SDL_SetError("Invalid IMA ADPCM bits per sample of %u", (unsigned int)format->bitspersample); + } + + /* The block size is required to be a multiple of 4 and it must be able to + * hold a block header. + */ + if (format->blockalign < blockheadersize || format->blockalign % 4) { + return SDL_SetError("Invalid IMA ADPCM block size (nBlockAlign)"); + } + + if (format->formattag == EXTENSIBLE_CODE) { + /* There's no specification for this, but it's basically the same + * format because the extensible header has wSampePerBlocks too. + */ + } else { + // The Standards Update says there 'should' be 2 bytes for wSamplesPerBlock. + if (chunk->size >= 20 && format->extsize >= 2) { + format->samplesperblock = chunk->data[18] | ((Uint16)chunk->data[19] << 8); + } + } + + if (format->samplesperblock == 0) { + /* Field zero? No problem. We just assume the encoder packed the block. + * The specification calculates it this way: + * + * x = Block size (in bits) minus header size (in bits) + * y = Bit depth multiplied by channel count + * z = Number of samples per channel in header + * wSamplesPerBlock = x / y + z + */ + format->samplesperblock = (Uint32)blockdatasamples + 1; + } + + /* nBlockAlign can be in conflict with wSamplesPerBlock. For example, if + * the number of samples doesn't fit into the block. The Standards Update + * also describes wSamplesPerBlock with a formula that makes it necessary + * to always fill the block with the maximum amount of samples, but this is + * not enforced here as there are no compatibility issues. + */ + if (blockdatasamples < format->samplesperblock - 1) { + return SDL_SetError("Invalid number of samples per IMA ADPCM block (wSamplesPerBlock)"); + } + + if (!IMA_ADPCM_CalculateSampleFrames(file, datalength)) { + return false; + } + + return true; +} + +static Sint16 IMA_ADPCM_ProcessNibble(Sint8 *cindex, Sint16 lastsample, Uint8 nybble) +{ + const Sint32 max_audioval = 32767; + const Sint32 min_audioval = -32768; + const Sint8 index_table_4b[16] = { + -1, -1, -1, -1, + 2, 4, 6, 8, + -1, -1, -1, -1, + 2, 4, 6, 8 + }; + const Uint16 step_table[89] = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, + 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, + 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, + 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, + 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, + 22385, 24623, 27086, 29794, 32767 + }; + Uint32 step; + Sint32 sample, delta; + Sint8 index = *cindex; + + // Clamp index into valid range. + if (index > 88) { + index = 88; + } else if (index < 0) { + index = 0; + } + + // explicit cast to avoid gcc warning about using 'char' as array index + step = step_table[(size_t)index]; + + // Update index value + *cindex = index + index_table_4b[nybble]; + + /* This calculation uses shifts and additions because multiplications were + * much slower back then. Sadly, this can't just be replaced with an actual + * multiplication now as the old algorithm drops some bits. The closest + * approximation I could find is something like this: + * (nybble & 0x8 ? -1 : 1) * ((nybble & 0x7) * step / 4 + step / 8) + */ + delta = step >> 3; + if (nybble & 0x04) { + delta += step; + } + if (nybble & 0x02) { + delta += step >> 1; + } + if (nybble & 0x01) { + delta += step >> 2; + } + if (nybble & 0x08) { + delta = -delta; + } + + sample = lastsample + delta; + + // Clamp output sample + if (sample > max_audioval) { + sample = max_audioval; + } else if (sample < min_audioval) { + sample = min_audioval; + } + + return (Sint16)sample; +} + +static bool IMA_ADPCM_DecodeBlockHeader(ADPCM_DecoderState *state) +{ + Sint16 step; + Uint32 c; + Uint8 *cstate = (Uint8 *)state->cstate; + + for (c = 0; c < state->channels; c++) { + size_t o = state->block.pos + c * 4; + + // Extract the sample from the header. + Sint32 sample = state->block.data[o] | ((Sint32)state->block.data[o + 1] << 8); + if (sample >= 0x8000) { + sample -= 0x10000; + } + state->output.data[state->output.pos++] = (Sint16)sample; + + // Channel step index. + step = (Sint16)state->block.data[o + 2]; + cstate[c] = (Sint8)(step > 0x80 ? step - 0x100 : step); + + // Reserved byte in block header, should be 0. + if (state->block.data[o + 3] != 0) { + /* Uh oh, corrupt data? Buggy code? */; + } + } + + state->block.pos += state->blockheadersize; + + // Header provided one sample frame. + state->framesleft--; + + return true; +} + +/* Decodes the data of the IMA ADPCM block. Decoding will stop if a block is too + * short, returning with none or partially decoded data. The partial data always + * contains full sample frames (same sample count for each channel). + * Incomplete sample frames are discarded. + */ +static bool IMA_ADPCM_DecodeBlockData(ADPCM_DecoderState *state) +{ + size_t i; + const Uint32 channels = state->channels; + const size_t subblockframesize = (size_t)channels * 4; + Uint64 bytesrequired; + Uint32 c; + bool result = true; + + size_t blockpos = state->block.pos; + size_t blocksize = state->block.size; + size_t blockleft = blocksize - blockpos; + + size_t outpos = state->output.pos; + + Sint64 blockframesleft = state->samplesperblock - 1; + if (blockframesleft > state->framesleft) { + blockframesleft = state->framesleft; + } + + bytesrequired = (blockframesleft + 7) / 8 * subblockframesize; + if (blockleft < bytesrequired) { + // Data truncated. Calculate how many samples we can get out if it. + const size_t guaranteedframes = blockleft / subblockframesize; + const size_t remainingbytes = blockleft % subblockframesize; + blockframesleft = guaranteedframes; + if (remainingbytes > subblockframesize - 4) { + blockframesleft += (Sint64)(remainingbytes % 4) * 2; + } + // Signal the truncation. + result = false; + } + + /* Each channel has their nibbles packed into 32-bit blocks. These blocks + * are interleaved and make up the data part of the ADPCM block. This loop + * decodes the samples as they come from the input data and puts them at + * the appropriate places in the output data. + */ + while (blockframesleft > 0) { + const size_t subblocksamples = blockframesleft < 8 ? (size_t)blockframesleft : 8; + + for (c = 0; c < channels; c++) { + Uint8 nybble = 0; + // Load previous sample which may come from the block header. + Sint16 sample = state->output.data[outpos + c - channels]; + + for (i = 0; i < subblocksamples; i++) { + if (i & 1) { + nybble >>= 4; + } else { + nybble = state->block.data[blockpos++]; + } + + sample = IMA_ADPCM_ProcessNibble((Sint8 *)state->cstate + c, sample, nybble & 0x0f); + state->output.data[outpos + c + i * channels] = sample; + } + } + + outpos += channels * subblocksamples; + state->framesleft -= subblocksamples; + blockframesleft -= subblocksamples; + } + + state->block.pos = blockpos; + state->output.pos = outpos; + + return result; +} + +static bool IMA_ADPCM_Decode(WaveFile *file, Uint8 **audio_buf, Uint32 *audio_len) +{ + bool result; + size_t bytesleft, outputsize; + WaveChunk *chunk = &file->chunk; + ADPCM_DecoderState state; + Sint8 *cstate; + + if (chunk->size != chunk->length) { + // Could not read everything. Recalculate number of sample frames. + if (!IMA_ADPCM_CalculateSampleFrames(file, chunk->size)) { + return false; + } + } + + // Nothing to decode, nothing to return. + if (file->sampleframes == 0) { + *audio_buf = NULL; + *audio_len = 0; + return true; + } + + SDL_zero(state); + state.channels = file->format.channels; + state.blocksize = file->format.blockalign; + state.blockheadersize = (size_t)state.channels * 4; + state.samplesperblock = file->format.samplesperblock; + state.framesize = state.channels * sizeof(Sint16); + state.framestotal = file->sampleframes; + state.framesleft = state.framestotal; + + state.input.data = chunk->data; + state.input.size = chunk->size; + state.input.pos = 0; + + // The output size in bytes. May get modified if data is truncated. + outputsize = (size_t)state.framestotal; + if (SafeMult(&outputsize, state.framesize)) { + return SDL_SetError("WAVE file too big"); + } else if (outputsize > SDL_MAX_UINT32 || state.framestotal > SIZE_MAX) { + return SDL_SetError("WAVE file too big"); + } + + state.output.pos = 0; + state.output.size = outputsize / sizeof(Sint16); + state.output.data = (Sint16 *)SDL_malloc(outputsize); + if (!state.output.data) { + return false; + } + + cstate = (Sint8 *)SDL_calloc(state.channels, sizeof(Sint8)); + if (!cstate) { + SDL_free(state.output.data); + return false; + } + state.cstate = cstate; + + // Decode block by block. A truncated block will stop the decoding. + bytesleft = state.input.size - state.input.pos; + while (state.framesleft > 0 && bytesleft >= state.blockheadersize) { + state.block.data = state.input.data + state.input.pos; + state.block.size = bytesleft < state.blocksize ? bytesleft : state.blocksize; + state.block.pos = 0; + + if (state.output.size - state.output.pos < (Uint64)state.framesleft * state.channels) { + // Somehow didn't allocate enough space for the output. + SDL_free(state.output.data); + SDL_free(cstate); + return SDL_SetError("Unexpected overflow in IMA ADPCM decoder"); + } + + // Initialize decoder with the values from the block header. + result = IMA_ADPCM_DecodeBlockHeader(&state); + if (result) { + // Decode the block data. It stores the samples directly in the output. + result = IMA_ADPCM_DecodeBlockData(&state); + } + + if (!result) { + // Unexpected end. Stop decoding and return partial data if necessary. + if (file->trunchint == TruncVeryStrict || file->trunchint == TruncStrict) { + SDL_free(state.output.data); + SDL_free(cstate); + return SDL_SetError("Truncated data chunk"); + } else if (file->trunchint != TruncDropFrame) { + state.output.pos -= state.output.pos % (state.samplesperblock * state.channels); + } + outputsize = state.output.pos * sizeof(Sint16); // Can't overflow, is always smaller. + break; + } + + state.input.pos += state.block.size; + bytesleft = state.input.size - state.input.pos; + } + + *audio_buf = (Uint8 *)state.output.data; + *audio_len = (Uint32)outputsize; + + SDL_free(cstate); + + return true; +} + +static bool LAW_Init(WaveFile *file, size_t datalength) +{ + WaveFormat *format = &file->format; + + // Standards Update requires this to be 8. + if (format->bitspersample != 8) { + return SDL_SetError("Invalid companded bits per sample of %u", (unsigned int)format->bitspersample); + } + + // Not going to bother with weird padding. + if (format->blockalign != format->channels) { + return SDL_SetError("Unsupported block alignment"); + } + + if ((file->trunchint == TruncVeryStrict || file->trunchint == TruncStrict)) { + if (format->blockalign > 1 && datalength % format->blockalign) { + return SDL_SetError("Truncated data chunk in WAVE file"); + } + } + + file->sampleframes = WaveAdjustToFactValue(file, datalength / format->blockalign); + if (file->sampleframes < 0) { + return false; + } + + return true; +} + +static bool LAW_Decode(WaveFile *file, Uint8 **audio_buf, Uint32 *audio_len) +{ +#ifdef SDL_WAVE_LAW_LUT + const Sint16 alaw_lut[256] = { + -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, -2752, + -2624, -3008, -2880, -2240, -2112, -2496, -2368, -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, -22016, + -20992, -24064, -23040, -17920, -16896, -19968, -18944, -30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136, -11008, + -10496, -12032, -11520, -8960, -8448, -9984, -9472, -15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568, -344, + -328, -376, -360, -280, -264, -312, -296, -472, -456, -504, -488, -408, -392, -440, -424, -88, + -72, -120, -104, -24, -8, -56, -40, -216, -200, -248, -232, -152, -136, -184, -168, -1376, + -1312, -1504, -1440, -1120, -1056, -1248, -1184, -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, -688, + -656, -752, -720, -560, -528, -624, -592, -944, -912, -1008, -976, -816, -784, -880, -848, 5504, + 5248, 6016, 5760, 4480, 4224, 4992, 4736, 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, 2752, + 2624, 3008, 2880, 2240, 2112, 2496, 2368, 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, 22016, + 20992, 24064, 23040, 17920, 16896, 19968, 18944, 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, 11008, + 10496, 12032, 11520, 8960, 8448, 9984, 9472, 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, 344, + 328, 376, 360, 280, 264, 312, 296, 472, 456, 504, 488, 408, 392, 440, 424, 88, + 72, 120, 104, 24, 8, 56, 40, 216, 200, 248, 232, 152, 136, 184, 168, 1376, + 1312, 1504, 1440, 1120, 1056, 1248, 1184, 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, 688, + 656, 752, 720, 560, 528, 624, 592, 944, 912, 1008, 976, 816, 784, 880, 848 + }; + const Sint16 mulaw_lut[256] = { + -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, -15996, + -15484, -14972, -14460, -13948, -13436, -12924, -12412, -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, -7932, + -7676, -7420, -7164, -6908, -6652, -6396, -6140, -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, -3900, + -3772, -3644, -3516, -3388, -3260, -3132, -3004, -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, -1884, + -1820, -1756, -1692, -1628, -1564, -1500, -1436, -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, -876, + -844, -812, -780, -748, -716, -684, -652, -620, -588, -556, -524, -492, -460, -428, -396, -372, + -356, -340, -324, -308, -292, -276, -260, -244, -228, -212, -196, -180, -164, -148, -132, -120, + -112, -104, -96, -88, -80, -72, -64, -56, -48, -40, -32, -24, -16, -8, 0, 32124, + 31100, 30076, 29052, 28028, 27004, 25980, 24956, 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, 15996, + 15484, 14972, 14460, 13948, 13436, 12924, 12412, 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, 7932, + 7676, 7420, 7164, 6908, 6652, 6396, 6140, 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, 3900, + 3772, 3644, 3516, 3388, 3260, 3132, 3004, 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, 1884, + 1820, 1756, 1692, 1628, 1564, 1500, 1436, 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, 876, + 844, 812, 780, 748, 716, 684, 652, 620, 588, 556, 524, 492, 460, 428, 396, 372, + 356, 340, 324, 308, 292, 276, 260, 244, 228, 212, 196, 180, 164, 148, 132, 120, + 112, 104, 96, 88, 80, 72, 64, 56, 48, 40, 32, 24, 16, 8, 0 + }; +#endif + + WaveFormat *format = &file->format; + WaveChunk *chunk = &file->chunk; + size_t i, sample_count, expanded_len; + Uint8 *src; + Sint16 *dst; + + if (chunk->length != chunk->size) { + file->sampleframes = WaveAdjustToFactValue(file, chunk->size / format->blockalign); + if (file->sampleframes < 0) { + return false; + } + } + + // Nothing to decode, nothing to return. + if (file->sampleframes == 0) { + *audio_buf = NULL; + *audio_len = 0; + return true; + } + + sample_count = (size_t)file->sampleframes; + if (SafeMult(&sample_count, format->channels)) { + return SDL_SetError("WAVE file too big"); + } + + expanded_len = sample_count; + if (SafeMult(&expanded_len, sizeof(Sint16))) { + return SDL_SetError("WAVE file too big"); + } else if (expanded_len > SDL_MAX_UINT32 || file->sampleframes > SIZE_MAX) { + return SDL_SetError("WAVE file too big"); + } + + // 1 to avoid allocating zero bytes, to keep static analysis happy. + src = (Uint8 *)SDL_realloc(chunk->data, expanded_len ? expanded_len : 1); + if (!src) { + return false; + } + chunk->data = NULL; + chunk->size = 0; + + dst = (Sint16 *)src; + + /* Work backwards, since we're expanding in-place. `format` will + * inform the caller about the byte order. + */ + i = sample_count; + switch (file->format.encoding) { +#ifdef SDL_WAVE_LAW_LUT + case ALAW_CODE: + while (i--) { + dst[i] = alaw_lut[src[i]]; + } + break; + case MULAW_CODE: + while (i--) { + dst[i] = mulaw_lut[src[i]]; + } + break; +#else + case ALAW_CODE: + while (i--) { + Uint8 nibble = src[i]; + Uint8 exponent = (nibble & 0x7f) ^ 0x55; + Sint16 mantissa = exponent & 0xf; + + exponent >>= 4; + if (exponent > 0) { + mantissa |= 0x10; + } + mantissa = (mantissa << 4) | 0x8; + if (exponent > 1) { + mantissa <<= exponent - 1; + } + + dst[i] = nibble & 0x80 ? mantissa : -mantissa; + } + break; + case MULAW_CODE: + while (i--) { + Uint8 nibble = ~src[i]; + Sint16 mantissa = nibble & 0xf; + Uint8 exponent = (nibble >> 4) & 0x7; + Sint16 step = 4 << (exponent + 1); + + mantissa = (0x80 << exponent) + step * mantissa + step / 2 - 132; + + dst[i] = nibble & 0x80 ? -mantissa : mantissa; + } + break; +#endif + default: + SDL_free(src); + return SDL_SetError("Unknown companded encoding"); + } + + *audio_buf = src; + *audio_len = (Uint32)expanded_len; + + return true; +} + +static bool PCM_Init(WaveFile *file, size_t datalength) +{ + WaveFormat *format = &file->format; + + if (format->encoding == PCM_CODE) { + switch (format->bitspersample) { + case 8: + case 16: + case 24: + case 32: + // These are supported. + break; + default: + return SDL_SetError("%u-bit PCM format not supported", (unsigned int)format->bitspersample); + } + } else if (format->encoding == IEEE_FLOAT_CODE) { + if (format->bitspersample != 32) { + return SDL_SetError("%u-bit IEEE floating-point format not supported", (unsigned int)format->bitspersample); + } + } + + /* It wouldn't be that hard to support more exotic block sizes, but + * the most common formats should do for now. + */ + // Make sure we're a multiple of the blockalign, at least. + if ((format->channels * format->bitspersample) % (format->blockalign * 8)) { + return SDL_SetError("Unsupported block alignment"); + } + + if ((file->trunchint == TruncVeryStrict || file->trunchint == TruncStrict)) { + if (format->blockalign > 1 && datalength % format->blockalign) { + return SDL_SetError("Truncated data chunk in WAVE file"); + } + } + + file->sampleframes = WaveAdjustToFactValue(file, datalength / format->blockalign); + if (file->sampleframes < 0) { + return false; + } + + return true; +} + +static bool PCM_ConvertSint24ToSint32(WaveFile *file, Uint8 **audio_buf, Uint32 *audio_len) +{ + WaveFormat *format = &file->format; + WaveChunk *chunk = &file->chunk; + size_t i, expanded_len, sample_count; + Uint8 *ptr; + + sample_count = (size_t)file->sampleframes; + if (SafeMult(&sample_count, format->channels)) { + return SDL_SetError("WAVE file too big"); + } + + expanded_len = sample_count; + if (SafeMult(&expanded_len, sizeof(Sint32))) { + return SDL_SetError("WAVE file too big"); + } else if (expanded_len > SDL_MAX_UINT32 || file->sampleframes > SIZE_MAX) { + return SDL_SetError("WAVE file too big"); + } + + // 1 to avoid allocating zero bytes, to keep static analysis happy. + ptr = (Uint8 *)SDL_realloc(chunk->data, expanded_len ? expanded_len : 1); + if (!ptr) { + return false; + } + + // This pointer is now invalid. + chunk->data = NULL; + chunk->size = 0; + + *audio_buf = ptr; + *audio_len = (Uint32)expanded_len; + + // work from end to start, since we're expanding in-place. + for (i = sample_count; i > 0; i--) { + const size_t o = i - 1; + uint8_t b[4]; + + b[0] = 0; + b[1] = ptr[o * 3]; + b[2] = ptr[o * 3 + 1]; + b[3] = ptr[o * 3 + 2]; + + ptr[o * 4 + 0] = b[0]; + ptr[o * 4 + 1] = b[1]; + ptr[o * 4 + 2] = b[2]; + ptr[o * 4 + 3] = b[3]; + } + + return true; +} + +static bool PCM_Decode(WaveFile *file, Uint8 **audio_buf, Uint32 *audio_len) +{ + WaveFormat *format = &file->format; + WaveChunk *chunk = &file->chunk; + size_t outputsize; + + if (chunk->length != chunk->size) { + file->sampleframes = WaveAdjustToFactValue(file, chunk->size / format->blockalign); + if (file->sampleframes < 0) { + return false; + } + } + + // Nothing to decode, nothing to return. + if (file->sampleframes == 0) { + *audio_buf = NULL; + *audio_len = 0; + return true; + } + + // 24-bit samples get shifted to 32 bits. + if (format->encoding == PCM_CODE && format->bitspersample == 24) { + return PCM_ConvertSint24ToSint32(file, audio_buf, audio_len); + } + + outputsize = (size_t)file->sampleframes; + if (SafeMult(&outputsize, format->blockalign)) { + return SDL_SetError("WAVE file too big"); + } else if (outputsize > SDL_MAX_UINT32 || file->sampleframes > SIZE_MAX) { + return SDL_SetError("WAVE file too big"); + } + + *audio_buf = chunk->data; + *audio_len = (Uint32)outputsize; + + // This pointer is going to be returned to the caller. Prevent free in cleanup. + chunk->data = NULL; + chunk->size = 0; + + return true; +} + +static WaveRiffSizeHint WaveGetRiffSizeHint(void) +{ + const char *hint = SDL_GetHint(SDL_HINT_WAVE_RIFF_CHUNK_SIZE); + + if (hint) { + if (SDL_strcmp(hint, "force") == 0) { + return RiffSizeForce; + } else if (SDL_strcmp(hint, "ignore") == 0) { + return RiffSizeIgnore; + } else if (SDL_strcmp(hint, "ignorezero") == 0) { + return RiffSizeIgnoreZero; + } else if (SDL_strcmp(hint, "maximum") == 0) { + return RiffSizeMaximum; + } + } + + return RiffSizeNoHint; +} + +static WaveTruncationHint WaveGetTruncationHint(void) +{ + const char *hint = SDL_GetHint(SDL_HINT_WAVE_TRUNCATION); + + if (hint) { + if (SDL_strcmp(hint, "verystrict") == 0) { + return TruncVeryStrict; + } else if (SDL_strcmp(hint, "strict") == 0) { + return TruncStrict; + } else if (SDL_strcmp(hint, "dropframe") == 0) { + return TruncDropFrame; + } else if (SDL_strcmp(hint, "dropblock") == 0) { + return TruncDropBlock; + } + } + + return TruncNoHint; +} + +static WaveFactChunkHint WaveGetFactChunkHint(void) +{ + const char *hint = SDL_GetHint(SDL_HINT_WAVE_FACT_CHUNK); + + if (hint) { + if (SDL_strcmp(hint, "truncate") == 0) { + return FactTruncate; + } else if (SDL_strcmp(hint, "strict") == 0) { + return FactStrict; + } else if (SDL_strcmp(hint, "ignorezero") == 0) { + return FactIgnoreZero; + } else if (SDL_strcmp(hint, "ignore") == 0) { + return FactIgnore; + } + } + + return FactNoHint; +} + +static void WaveFreeChunkData(WaveChunk *chunk) +{ + if (chunk->data) { + SDL_free(chunk->data); + chunk->data = NULL; + } + chunk->size = 0; +} + +static int WaveNextChunk(SDL_IOStream *src, WaveChunk *chunk) +{ + Uint32 chunkheader[2]; + Sint64 nextposition = chunk->position + chunk->length; + + // Data is no longer valid after this function returns. + WaveFreeChunkData(chunk); + + // Error on overflows. + if (SDL_MAX_SINT64 - chunk->length < chunk->position || SDL_MAX_SINT64 - 8 < nextposition) { + return -1; + } + + // RIFF chunks have a 2-byte alignment. Skip padding byte. + if (chunk->length & 1) { + nextposition++; + } + + if (SDL_SeekIO(src, nextposition, SDL_IO_SEEK_SET) != nextposition) { + // Not sure how we ended up here. Just abort. + return -2; + } else if (SDL_ReadIO(src, chunkheader, sizeof(Uint32) * 2) != (sizeof(Uint32) * 2)) { + return -1; + } + + chunk->fourcc = SDL_Swap32LE(chunkheader[0]); + chunk->length = SDL_Swap32LE(chunkheader[1]); + chunk->position = nextposition + 8; + + return 0; +} + +static int WaveReadPartialChunkData(SDL_IOStream *src, WaveChunk *chunk, size_t length) +{ + WaveFreeChunkData(chunk); + + if (length > chunk->length) { + length = chunk->length; + } + + if (length > 0) { + chunk->data = (Uint8 *)SDL_malloc(length); + if (!chunk->data) { + return -1; + } + + if (SDL_SeekIO(src, chunk->position, SDL_IO_SEEK_SET) != chunk->position) { + // Not sure how we ended up here. Just abort. + return -2; + } + + chunk->size = SDL_ReadIO(src, chunk->data, length); + if (chunk->size != length) { + // Expected to be handled by the caller. + } + } + + return 0; +} + +static int WaveReadChunkData(SDL_IOStream *src, WaveChunk *chunk) +{ + return WaveReadPartialChunkData(src, chunk, chunk->length); +} + +typedef struct WaveExtensibleGUID +{ + Uint16 encoding; + Uint8 guid[16]; +} WaveExtensibleGUID; + +// Some of the GUIDs that are used by WAVEFORMATEXTENSIBLE. +#define WAVE_FORMATTAG_GUID(tag) \ + { \ + (tag) & 0xff, (tag) >> 8, 0, 0, 0, 0, 16, 0, 128, 0, 0, 170, 0, 56, 155, 113 \ + } +static WaveExtensibleGUID extensible_guids[] = { + { PCM_CODE, WAVE_FORMATTAG_GUID(PCM_CODE) }, + { MS_ADPCM_CODE, WAVE_FORMATTAG_GUID(MS_ADPCM_CODE) }, + { IEEE_FLOAT_CODE, WAVE_FORMATTAG_GUID(IEEE_FLOAT_CODE) }, + { ALAW_CODE, WAVE_FORMATTAG_GUID(ALAW_CODE) }, + { MULAW_CODE, WAVE_FORMATTAG_GUID(MULAW_CODE) }, + { IMA_ADPCM_CODE, WAVE_FORMATTAG_GUID(IMA_ADPCM_CODE) } +}; + +static Uint16 WaveGetFormatGUIDEncoding(WaveFormat *format) +{ + size_t i; + for (i = 0; i < SDL_arraysize(extensible_guids); i++) { + if (SDL_memcmp(format->subformat, extensible_guids[i].guid, 16) == 0) { + return extensible_guids[i].encoding; + } + } + return UNKNOWN_CODE; +} + +static bool WaveReadFormat(WaveFile *file) +{ + WaveChunk *chunk = &file->chunk; + WaveFormat *format = &file->format; + SDL_IOStream *fmtsrc; + size_t fmtlen = chunk->size; + + if (fmtlen > SDL_MAX_SINT32) { + // Limit given by SDL_IOFromConstMem. + return SDL_SetError("Data of WAVE fmt chunk too big"); + } + fmtsrc = SDL_IOFromConstMem(chunk->data, (int)chunk->size); + if (!fmtsrc) { + return false; + } + + if (!SDL_ReadU16LE(fmtsrc, &format->formattag) || + !SDL_ReadU16LE(fmtsrc, &format->channels) || + !SDL_ReadU32LE(fmtsrc, &format->frequency) || + !SDL_ReadU32LE(fmtsrc, &format->byterate) || + !SDL_ReadU16LE(fmtsrc, &format->blockalign)) { + return false; + } + format->encoding = format->formattag; + + // This is PCM specific in the first version of the specification. + if (fmtlen >= 16) { + if (!SDL_ReadU16LE(fmtsrc, &format->bitspersample)) { + return false; + } + } else if (format->encoding == PCM_CODE) { + SDL_CloseIO(fmtsrc); + return SDL_SetError("Missing wBitsPerSample field in WAVE fmt chunk"); + } + + // The earlier versions also don't have this field. + if (fmtlen >= 18) { + if (!SDL_ReadU16LE(fmtsrc, &format->extsize)) { + return false; + } + } + + if (format->formattag == EXTENSIBLE_CODE) { + /* note that this ignores channel masks, smaller valid bit counts + * inside a larger container, and most subtypes. This is just enough + * to get things that didn't really _need_ WAVE_FORMAT_EXTENSIBLE + * to be useful working when they use this format flag. + */ + + // Extensible header must be at least 22 bytes. + if (fmtlen < 40 || format->extsize < 22) { + SDL_CloseIO(fmtsrc); + return SDL_SetError("Extensible WAVE header too small"); + } + + if (!SDL_ReadU16LE(fmtsrc, &format->validsamplebits) || + !SDL_ReadU32LE(fmtsrc, &format->channelmask) || + SDL_ReadIO(fmtsrc, format->subformat, 16) != 16) { + } + format->samplesperblock = format->validsamplebits; + format->encoding = WaveGetFormatGUIDEncoding(format); + } + + SDL_CloseIO(fmtsrc); + + return true; +} + +static bool WaveCheckFormat(WaveFile *file, size_t datalength) +{ + WaveFormat *format = &file->format; + + // Check for some obvious issues. + + if (format->channels == 0) { + return SDL_SetError("Invalid number of channels"); + } + + if (format->frequency == 0) { + return SDL_SetError("Invalid sample rate"); + } else if (format->frequency > INT_MAX) { + return SDL_SetError("Sample rate exceeds limit of %d", INT_MAX); + } + + // Reject invalid fact chunks in strict mode. + if (file->facthint == FactStrict && file->fact.status == -1) { + return SDL_SetError("Invalid fact chunk in WAVE file"); + } + + /* Check for issues common to all encodings. Some unsupported formats set + * the bits per sample to zero. These fall through to the 'unsupported + * format' error. + */ + switch (format->encoding) { + case IEEE_FLOAT_CODE: + case ALAW_CODE: + case MULAW_CODE: + case MS_ADPCM_CODE: + case IMA_ADPCM_CODE: + // These formats require a fact chunk. + if (file->facthint == FactStrict && file->fact.status <= 0) { + return SDL_SetError("Missing fact chunk in WAVE file"); + } + SDL_FALLTHROUGH; + case PCM_CODE: + // All supported formats require a non-zero bit depth. + if (file->chunk.size < 16) { + return SDL_SetError("Missing wBitsPerSample field in WAVE fmt chunk"); + } else if (format->bitspersample == 0) { + return SDL_SetError("Invalid bits per sample"); + } + + // All supported formats must have a proper block size. + if (format->blockalign == 0) { + format->blockalign = 1; // force it to 1 if it was unset. + } + + /* If the fact chunk is valid and the appropriate hint is set, the + * decoders will use the number of sample frames from the fact chunk. + */ + if (file->fact.status == 1) { + WaveFactChunkHint hint = file->facthint; + Uint32 samples = file->fact.samplelength; + if (hint == FactTruncate || hint == FactStrict || (hint == FactIgnoreZero && samples > 0)) { + file->fact.status = 2; + } + } + } + + // Check the format for encoding specific issues and initialize decoders. + switch (format->encoding) { + case PCM_CODE: + case IEEE_FLOAT_CODE: + if (!PCM_Init(file, datalength)) { + return false; + } + break; + case ALAW_CODE: + case MULAW_CODE: + if (!LAW_Init(file, datalength)) { + return false; + } + break; + case MS_ADPCM_CODE: + if (!MS_ADPCM_Init(file, datalength)) { + return false; + } + break; + case IMA_ADPCM_CODE: + if (!IMA_ADPCM_Init(file, datalength)) { + return false; + } + break; + case MPEG_CODE: + case MPEGLAYER3_CODE: + return SDL_SetError("MPEG formats not supported"); + default: + if (format->formattag == EXTENSIBLE_CODE) { + const char *errstr = "Unknown WAVE format GUID: %08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x"; + const Uint8 *g = format->subformat; + const Uint32 g1 = g[0] | ((Uint32)g[1] << 8) | ((Uint32)g[2] << 16) | ((Uint32)g[3] << 24); + const Uint32 g2 = g[4] | ((Uint32)g[5] << 8); + const Uint32 g3 = g[6] | ((Uint32)g[7] << 8); + return SDL_SetError(errstr, g1, g2, g3, g[8], g[9], g[10], g[11], g[12], g[13], g[14], g[15]); + } + return SDL_SetError("Unknown WAVE format tag: 0x%04x", (unsigned int)format->encoding); + } + + return true; +} + +static bool WaveLoad(SDL_IOStream *src, WaveFile *file, SDL_AudioSpec *spec, Uint8 **audio_buf, Uint32 *audio_len) +{ + int result; + Uint32 chunkcount = 0; + Uint32 chunkcountlimit = 10000; + const char *hint; + Sint64 RIFFstart, RIFFend, lastchunkpos; + bool RIFFlengthknown = false; + WaveFormat *format = &file->format; + WaveChunk *chunk = &file->chunk; + WaveChunk RIFFchunk; + WaveChunk fmtchunk; + WaveChunk datachunk; + + SDL_zero(RIFFchunk); + SDL_zero(fmtchunk); + SDL_zero(datachunk); + + hint = SDL_GetHint(SDL_HINT_WAVE_CHUNK_LIMIT); + if (hint) { + unsigned int count; + if (SDL_sscanf(hint, "%u", &count) == 1) { + chunkcountlimit = count <= SDL_MAX_UINT32 ? count : SDL_MAX_UINT32; + } + } + + RIFFstart = SDL_TellIO(src); + if (RIFFstart < 0) { + return SDL_SetError("Could not seek in file"); + } + + RIFFchunk.position = RIFFstart; + if (WaveNextChunk(src, &RIFFchunk) < 0) { + return SDL_SetError("Could not read RIFF header"); + } + + // Check main WAVE file identifiers. + if (RIFFchunk.fourcc == RIFF) { + Uint32 formtype; + // Read the form type. "WAVE" expected. + if (!SDL_ReadU32LE(src, &formtype)) { + return SDL_SetError("Could not read RIFF form type"); + } else if (formtype != WAVE) { + return SDL_SetError("RIFF form type is not WAVE (not a Waveform file)"); + } + } else if (RIFFchunk.fourcc == WAVE) { + // RIFF chunk missing or skipped. Length unknown. + RIFFchunk.position = 0; + RIFFchunk.length = 0; + } else { + return SDL_SetError("Could not find RIFF or WAVE identifiers (not a Waveform file)"); + } + + // The 4-byte form type is immediately followed by the first chunk. + chunk->position = RIFFchunk.position + 4; + + /* Use the RIFF chunk size to limit the search for the chunks. This is not + * always reliable and the hint can be used to tune the behavior. By + * default, it will never search past 4 GiB. + */ + switch (file->riffhint) { + case RiffSizeIgnore: + RIFFend = RIFFchunk.position + SDL_MAX_UINT32; + break; + default: + case RiffSizeIgnoreZero: + if (RIFFchunk.length == 0) { + RIFFend = RIFFchunk.position + SDL_MAX_UINT32; + break; + } + SDL_FALLTHROUGH; + case RiffSizeForce: + RIFFend = RIFFchunk.position + RIFFchunk.length; + RIFFlengthknown = true; + break; + case RiffSizeMaximum: + RIFFend = SDL_MAX_SINT64; + break; + } + + /* Step through all chunks and save information on the fmt, data, and fact + * chunks. Ignore the chunks we don't know as per specification. This + * currently also ignores cue, list, and slnt chunks. + */ + while ((Uint64)RIFFend > (Uint64)chunk->position + chunk->length + (chunk->length & 1)) { + // Abort after too many chunks or else corrupt files may waste time. + if (chunkcount++ >= chunkcountlimit) { + return SDL_SetError("Chunk count in WAVE file exceeds limit of %" SDL_PRIu32, chunkcountlimit); + } + + result = WaveNextChunk(src, chunk); + if (result < 0) { + // Unexpected EOF. Corrupt file or I/O issues. + if (file->trunchint == TruncVeryStrict) { + return SDL_SetError("Unexpected end of WAVE file"); + } + // Let the checks after this loop sort this issue out. + break; + } else if (result == -2) { + return SDL_SetError("Could not seek to WAVE chunk header"); + } + + if (chunk->fourcc == FMT) { + if (fmtchunk.fourcc == FMT) { + // Multiple fmt chunks. Ignore or error? + } else { + // The fmt chunk must occur before the data chunk. + if (datachunk.fourcc == DATA) { + return SDL_SetError("fmt chunk after data chunk in WAVE file"); + } + fmtchunk = *chunk; + } + } else if (chunk->fourcc == DATA) { + /* Only use the first data chunk. Handling the wavl list madness + * may require a different approach. + */ + if (datachunk.fourcc != DATA) { + datachunk = *chunk; + } + } else if (chunk->fourcc == FACT) { + /* The fact chunk data must be at least 4 bytes for the + * dwSampleLength field. Ignore all fact chunks after the first one. + */ + if (file->fact.status == 0) { + if (chunk->length < 4) { + file->fact.status = -1; + } else { + // Let's use src directly, it's just too convenient. + Sint64 position = SDL_SeekIO(src, chunk->position, SDL_IO_SEEK_SET); + if (position == chunk->position && SDL_ReadU32LE(src, &file->fact.samplelength)) { + file->fact.status = 1; + } else { + file->fact.status = -1; + } + } + } + } + + /* Go through all chunks in verystrict mode or stop the search early if + * all required chunks were found. + */ + if (file->trunchint == TruncVeryStrict) { + if ((Uint64)RIFFend < (Uint64)chunk->position + chunk->length) { + return SDL_SetError("RIFF size truncates chunk"); + } + } else if (fmtchunk.fourcc == FMT && datachunk.fourcc == DATA) { + if (file->fact.status == 1 || file->facthint == FactIgnore || file->facthint == FactNoHint) { + break; + } + } + } + + /* Save the position after the last chunk. This position will be used if the + * RIFF length is unknown. + */ + lastchunkpos = chunk->position + chunk->length; + + // The fmt chunk is mandatory. + if (fmtchunk.fourcc != FMT) { + return SDL_SetError("Missing fmt chunk in WAVE file"); + } + // A data chunk must be present. + if (datachunk.fourcc != DATA) { + return SDL_SetError("Missing data chunk in WAVE file"); + } + // Check if the last chunk has all of its data in verystrict mode. + if (file->trunchint == TruncVeryStrict) { + // data chunk is handled later. + if (chunk->fourcc != DATA && chunk->length > 0) { + Uint8 tmp; + Uint64 position = (Uint64)chunk->position + chunk->length - 1; + if (position > SDL_MAX_SINT64 || SDL_SeekIO(src, (Sint64)position, SDL_IO_SEEK_SET) != (Sint64)position) { + return SDL_SetError("Could not seek to WAVE chunk data"); + } else if (!SDL_ReadU8(src, &tmp)) { + return SDL_SetError("RIFF size truncates chunk"); + } + } + } + + // Process fmt chunk. + *chunk = fmtchunk; + + /* No need to read more than 1046 bytes of the fmt chunk data with the + * formats that are currently supported. (1046 because of MS ADPCM coefficients) + */ + if (WaveReadPartialChunkData(src, chunk, 1046) < 0) { + return SDL_SetError("Could not read data of WAVE fmt chunk"); + } + + /* The fmt chunk data must be at least 14 bytes to include all common fields. + * It usually is 16 and larger depending on the header and encoding. + */ + if (chunk->length < 14) { + return SDL_SetError("Invalid WAVE fmt chunk length (too small)"); + } else if (chunk->size < 14) { + return SDL_SetError("Could not read data of WAVE fmt chunk"); + } else if (!WaveReadFormat(file)) { + return false; + } else if (!WaveCheckFormat(file, (size_t)datachunk.length)) { + return false; + } + +#ifdef SDL_WAVE_DEBUG_LOG_FORMAT + WaveDebugLogFormat(file); +#endif +#ifdef SDL_WAVE_DEBUG_DUMP_FORMAT + WaveDebugDumpFormat(file, RIFFchunk.length, fmtchunk.length, datachunk.length); +#endif + + WaveFreeChunkData(chunk); + + // Process data chunk. + *chunk = datachunk; + + if (chunk->length > 0) { + result = WaveReadChunkData(src, chunk); + if (result < 0) { + return false; + } else if (result == -2) { + return SDL_SetError("Could not seek data of WAVE data chunk"); + } + } + + if (chunk->length != chunk->size) { + // I/O issues or corrupt file. + if (file->trunchint == TruncVeryStrict || file->trunchint == TruncStrict) { + return SDL_SetError("Could not read data of WAVE data chunk"); + } + // The decoders handle this truncation. + } + + // Decode or convert the data if necessary. + switch (format->encoding) { + case PCM_CODE: + case IEEE_FLOAT_CODE: + if (!PCM_Decode(file, audio_buf, audio_len)) { + return false; + } + break; + case ALAW_CODE: + case MULAW_CODE: + if (!LAW_Decode(file, audio_buf, audio_len)) { + return false; + } + break; + case MS_ADPCM_CODE: + if (!MS_ADPCM_Decode(file, audio_buf, audio_len)) { + return false; + } + break; + case IMA_ADPCM_CODE: + if (!IMA_ADPCM_Decode(file, audio_buf, audio_len)) { + return false; + } + break; + } + + /* Setting up the specs. All unsupported formats were filtered out + * by checks earlier in this function. + */ + spec->freq = format->frequency; + spec->channels = (Uint8)format->channels; + spec->format = SDL_AUDIO_UNKNOWN; + + switch (format->encoding) { + case MS_ADPCM_CODE: + case IMA_ADPCM_CODE: + case ALAW_CODE: + case MULAW_CODE: + // These can be easily stored in the byte order of the system. + spec->format = SDL_AUDIO_S16; + break; + case IEEE_FLOAT_CODE: + spec->format = SDL_AUDIO_F32LE; + break; + case PCM_CODE: + switch (format->bitspersample) { + case 8: + spec->format = SDL_AUDIO_U8; + break; + case 16: + spec->format = SDL_AUDIO_S16LE; + break; + case 24: // Has been shifted to 32 bits. + case 32: + spec->format = SDL_AUDIO_S32LE; + break; + default: + // Just in case something unexpected happened in the checks. + return SDL_SetError("Unexpected %u-bit PCM data format", (unsigned int)format->bitspersample); + } + break; + default: + return SDL_SetError("Unexpected data format"); + } + + // Report the end position back to the cleanup code. + if (RIFFlengthknown) { + chunk->position = RIFFend; + } else { + chunk->position = lastchunkpos; + } + + return true; +} + +bool SDL_LoadWAV_IO(SDL_IOStream *src, bool closeio, SDL_AudioSpec *spec, Uint8 **audio_buf, Uint32 *audio_len) +{ + bool result = false; + WaveFile file; + + if (spec) { + SDL_zerop(spec); + } + if (audio_buf) { + *audio_buf = NULL; + } + if (audio_len) { + *audio_len = 0; + } + + // Make sure we are passed a valid data source + if (!src) { + SDL_InvalidParamError("src"); + goto done; + } else if (!spec) { + SDL_InvalidParamError("spec"); + goto done; + } else if (!audio_buf) { + SDL_InvalidParamError("audio_buf"); + goto done; + } else if (!audio_len) { + SDL_InvalidParamError("audio_len"); + goto done; + } + + SDL_zero(file); + file.riffhint = WaveGetRiffSizeHint(); + file.trunchint = WaveGetTruncationHint(); + file.facthint = WaveGetFactChunkHint(); + + result = WaveLoad(src, &file, spec, audio_buf, audio_len); + if (!result) { + SDL_free(*audio_buf); + audio_buf = NULL; + audio_len = 0; + } + + // Cleanup + if (!closeio) { + SDL_SeekIO(src, file.chunk.position, SDL_IO_SEEK_SET); + } + WaveFreeChunkData(&file.chunk); + SDL_free(file.decoderdata); +done: + if (closeio && src) { + SDL_CloseIO(src); + } + return result; +} + +bool SDL_LoadWAV(const char *path, SDL_AudioSpec *spec, Uint8 **audio_buf, Uint32 *audio_len) +{ + SDL_IOStream *stream = SDL_IOFromFile(path, "rb"); + if (!stream) { + if (spec) { + SDL_zerop(spec); + } + if (audio_buf) { + *audio_buf = NULL; + } + if (audio_len) { + *audio_len = 0; + } + return false; + } + return SDL_LoadWAV_IO(stream, true, spec, audio_buf, audio_len); +} + diff --git a/contrib/SDL-3.2.8/src/audio/SDL_wave.h b/contrib/SDL-3.2.8/src/audio/SDL_wave.h new file mode 100644 index 0000000..0ec8965 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/SDL_wave.h @@ -0,0 +1,151 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +// RIFF WAVE files are little-endian + +/*******************************************/ +// Define values for Microsoft WAVE format +/*******************************************/ +// FOURCC +#define RIFF 0x46464952 // "RIFF" +#define WAVE 0x45564157 // "WAVE" +#define FACT 0x74636166 // "fact" +#define LIST 0x5453494c // "LIST" +#define BEXT 0x74786562 // "bext" +#define JUNK 0x4B4E554A // "JUNK" +#define FMT 0x20746D66 // "fmt " +#define DATA 0x61746164 // "data" +// Format tags +#define UNKNOWN_CODE 0x0000 +#define PCM_CODE 0x0001 +#define MS_ADPCM_CODE 0x0002 +#define IEEE_FLOAT_CODE 0x0003 +#define ALAW_CODE 0x0006 +#define MULAW_CODE 0x0007 +#define IMA_ADPCM_CODE 0x0011 +#define MPEG_CODE 0x0050 +#define MPEGLAYER3_CODE 0x0055 +#define EXTENSIBLE_CODE 0xFFFE + +// Stores the WAVE format information. +typedef struct WaveFormat +{ + Uint16 formattag; // Raw value of the first field in the fmt chunk data. + Uint16 encoding; // Actual encoding, possibly from the extensible header. + Uint16 channels; // Number of channels. + Uint32 frequency; // Sampling rate in Hz. + Uint32 byterate; // Average bytes per second. + Uint16 blockalign; // Bytes per block. + Uint16 bitspersample; // Currently supported are 8, 16, 24, 32, and 4 for ADPCM. + + /* Extra information size. Number of extra bytes starting at byte 18 in the + * fmt chunk data. This is at least 22 for the extensible header. + */ + Uint16 extsize; + + // Extensible WAVE header fields + Uint16 validsamplebits; + Uint32 samplesperblock; // For compressed formats. Can be zero. Actually 16 bits in the header. + Uint32 channelmask; + Uint8 subformat[16]; // A format GUID. +} WaveFormat; + +// Stores information on the fact chunk. +typedef struct WaveFact +{ + /* Represents the state of the fact chunk in the WAVE file. + * Set to -1 if the fact chunk is invalid. + * Set to 0 if the fact chunk is not present + * Set to 1 if the fact chunk is present and valid. + * Set to 2 if samplelength is going to be used as the number of sample frames. + */ + Sint32 status; + + /* Version 1 of the RIFF specification calls the field in the fact chunk + * dwFileSize. The Standards Update then calls it dwSampleLength and specifies + * that it is 'the length of the data in samples'. WAVE files from Windows + * with this chunk have it set to the samples per channel (sample frames). + * This is useful to truncate compressed audio to a specific sample count + * because a compressed block is usually decoded to a fixed number of + * sample frames. + */ + Uint32 samplelength; // Raw sample length value from the fact chunk. +} WaveFact; + +// Generic struct for the chunks in the WAVE file. +typedef struct WaveChunk +{ + Uint32 fourcc; // FOURCC of the chunk. + Uint32 length; // Size of the chunk data. + Sint64 position; // Position of the data in the stream. + Uint8 *data; // When allocated, this points to the chunk data. length is used for the memory allocation size. + size_t size; // Number of bytes in data that could be read from the stream. Can be smaller than length. +} WaveChunk; + +// Controls how the size of the RIFF chunk affects the loading of a WAVE file. +typedef enum WaveRiffSizeHint +{ + RiffSizeNoHint, + RiffSizeForce, + RiffSizeIgnoreZero, + RiffSizeIgnore, + RiffSizeMaximum +} WaveRiffSizeHint; + +// Controls how a truncated WAVE file is handled. +typedef enum WaveTruncationHint +{ + TruncNoHint, + TruncVeryStrict, + TruncStrict, + TruncDropFrame, + TruncDropBlock +} WaveTruncationHint; + +// Controls how the fact chunk affects the loading of a WAVE file. +typedef enum WaveFactChunkHint +{ + FactNoHint, + FactTruncate, + FactStrict, + FactIgnoreZero, + FactIgnore +} WaveFactChunkHint; + +typedef struct WaveFile +{ + WaveChunk chunk; + WaveFormat format; + WaveFact fact; + + /* Number of sample frames that will be decoded. Calculated either with the + * size of the data chunk or, if the appropriate hint is enabled, with the + * sample length value from the fact chunk. + */ + Sint64 sampleframes; + + void *decoderdata; // Some decoders require extra data for a state. + + WaveRiffSizeHint riffhint; + WaveTruncationHint trunchint; + WaveFactChunkHint facthint; +} WaveFile; diff --git a/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.c b/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.c new file mode 100644 index 0000000..3360bec --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.c @@ -0,0 +1,551 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_AAUDIO + +#include "../SDL_sysaudio.h" +#include "SDL_aaudio.h" + +#include "../../core/android/SDL_android.h" +#include + +#if __ANDROID_API__ < 31 +#define AAUDIO_FORMAT_PCM_I32 4 +#endif + +struct SDL_PrivateAudioData +{ + AAudioStream *stream; + int num_buffers; + Uint8 *mixbuf; // Raw mixing buffer + size_t mixbuf_bytes; // num_buffers * device->buffer_size + size_t callback_bytes; + size_t processed_bytes; + SDL_Semaphore *semaphore; + SDL_AtomicInt error_callback_triggered; +}; + +// Debug +#if 0 +#define LOGI(...) SDL_Log(__VA_ARGS__); +#else +#define LOGI(...) +#endif + +#define LIB_AAUDIO_SO "libaaudio.so" + +typedef struct AAUDIO_Data +{ + SDL_SharedObject *handle; +#define SDL_PROC(ret, func, params) ret (*func) params; +#include "SDL_aaudiofuncs.h" +} AAUDIO_Data; +static AAUDIO_Data ctx; + +static bool AAUDIO_LoadFunctions(AAUDIO_Data *data) +{ +#define SDL_PROC(ret, func, params) \ + do { \ + data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func); \ + if (!data->func) { \ + return SDL_SetError("Couldn't load AAUDIO function %s: %s", #func, SDL_GetError()); \ + } \ + } while (0); +#include "SDL_aaudiofuncs.h" + return true; +} + + +static void AAUDIO_errorCallback(AAudioStream *stream, void *userData, aaudio_result_t error) +{ + LOGI("SDL AAUDIO_errorCallback: %d - %s", error, ctx.AAudio_convertResultToText(error)); + + // You MUST NOT close the audio stream from this callback, so we cannot call SDL_AudioDeviceDisconnected here. + // Just flag the device so we can kill it in PlayDevice instead. + SDL_AudioDevice *device = (SDL_AudioDevice *) userData; + SDL_SetAtomicInt(&device->hidden->error_callback_triggered, (int) error); // AAUDIO_OK is zero, so !triggered means no error. + SDL_SignalSemaphore(device->hidden->semaphore); // in case we're blocking in WaitDevice. +} + +static aaudio_data_callback_result_t AAUDIO_dataCallback(AAudioStream *stream, void *userData, void *audioData, int32_t numFrames) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userData; + struct SDL_PrivateAudioData *hidden = device->hidden; + size_t framesize = SDL_AUDIO_FRAMESIZE(device->spec); + size_t callback_bytes = numFrames * framesize; + size_t old_buffer_index = hidden->callback_bytes / device->buffer_size; + + if (device->recording) { + const Uint8 *input = (const Uint8 *)audioData; + size_t available_bytes = hidden->mixbuf_bytes - (hidden->callback_bytes - hidden->processed_bytes); + size_t size = SDL_min(available_bytes, callback_bytes); + size_t offset = hidden->callback_bytes % hidden->mixbuf_bytes; + size_t end = (offset + size) % hidden->mixbuf_bytes; + SDL_assert(size <= hidden->mixbuf_bytes); + +//LOGI("Recorded %zu frames, %zu available, %zu max (%zu written, %zu read)", callback_bytes / framesize, available_bytes / framesize, hidden->mixbuf_bytes / framesize, hidden->callback_bytes / framesize, hidden->processed_bytes / framesize); + + if (offset <= end) { + SDL_memcpy(&hidden->mixbuf[offset], input, size); + } else { + size_t partial = (hidden->mixbuf_bytes - offset); + SDL_memcpy(&hidden->mixbuf[offset], &input[0], partial); + SDL_memcpy(&hidden->mixbuf[0], &input[partial], end); + } + + SDL_MemoryBarrierRelease(); + hidden->callback_bytes += size; + + if (size < callback_bytes) { + LOGI("Audio recording overflow, dropped %zu frames", (callback_bytes - size) / framesize); + } + } else { + Uint8 *output = (Uint8 *)audioData; + size_t available_bytes = (hidden->processed_bytes - hidden->callback_bytes); + size_t size = SDL_min(available_bytes, callback_bytes); + size_t offset = hidden->callback_bytes % hidden->mixbuf_bytes; + size_t end = (offset + size) % hidden->mixbuf_bytes; + SDL_assert(size <= hidden->mixbuf_bytes); + +//LOGI("Playing %zu frames, %zu available, %zu max (%zu written, %zu read)", callback_bytes / framesize, available_bytes / framesize, hidden->mixbuf_bytes / framesize, hidden->processed_bytes / framesize, hidden->callback_bytes / framesize); + + SDL_MemoryBarrierAcquire(); + if (offset <= end) { + SDL_memcpy(output, &hidden->mixbuf[offset], size); + } else { + size_t partial = (hidden->mixbuf_bytes - offset); + SDL_memcpy(&output[0], &hidden->mixbuf[offset], partial); + SDL_memcpy(&output[partial], &hidden->mixbuf[0], end); + } + hidden->callback_bytes += size; + + if (size < callback_bytes) { + LOGI("Audio playback underflow, missed %zu frames", (callback_bytes - size) / framesize); + SDL_memset(&output[size], device->silence_value, (callback_bytes - size)); + } + } + + size_t new_buffer_index = hidden->callback_bytes / device->buffer_size; + while (old_buffer_index < new_buffer_index) { + // Trigger audio processing + SDL_SignalSemaphore(hidden->semaphore); + ++old_buffer_index; + } + + return AAUDIO_CALLBACK_RESULT_CONTINUE; +} + +static Uint8 *AAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *bufsize) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + size_t offset = (hidden->processed_bytes % hidden->mixbuf_bytes); + return &hidden->mixbuf[offset]; +} + +static bool AAUDIO_WaitDevice(SDL_AudioDevice *device) +{ + while (!SDL_GetAtomicInt(&device->shutdown)) { + // this semaphore won't fire when the app is in the background (AAUDIO_PauseDevices was called). + if (SDL_WaitSemaphoreTimeout(device->hidden->semaphore, 100)) { + return true; // semaphore was signaled, let's go! + } + // Still waiting on the semaphore (or the system), check other things then wait again. + } + return true; +} + +static bool BuildAAudioStream(SDL_AudioDevice *device); + +static bool RecoverAAudioDevice(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + + // attempt to build a new stream, in case there's a new default device. + ctx.AAudioStream_requestStop(hidden->stream); + ctx.AAudioStream_close(hidden->stream); + hidden->stream = NULL; + + SDL_aligned_free(hidden->mixbuf); + hidden->mixbuf = NULL; + + SDL_DestroySemaphore(hidden->semaphore); + hidden->semaphore = NULL; + + const int prev_sample_frames = device->sample_frames; + SDL_AudioSpec prevspec; + SDL_copyp(&prevspec, &device->spec); + + if (!BuildAAudioStream(device)) { + return false; // oh well, we tried. + } + + // we don't know the new device spec until we open the new device, so we saved off the old one and force it back + // so SDL_AudioDeviceFormatChanged can set up all the important state if necessary and then set it back to the new spec. + const int new_sample_frames = device->sample_frames; + SDL_AudioSpec newspec; + SDL_copyp(&newspec, &device->spec); + + device->sample_frames = prev_sample_frames; + SDL_copyp(&device->spec, &prevspec); + if (!SDL_AudioDeviceFormatChangedAlreadyLocked(device, &newspec, new_sample_frames)) { + return false; // ugh + } + return true; +} + + +static bool AAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + + // AAUDIO_dataCallback picks up our work and unblocks AAUDIO_WaitDevice. But make sure we didn't fail here. + const aaudio_result_t err = (aaudio_result_t) SDL_GetAtomicInt(&hidden->error_callback_triggered); + if (err) { + SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "aaudio: Audio device triggered error %d (%s)", (int) err, ctx.AAudio_convertResultToText(err)); + + if (!RecoverAAudioDevice(device)) { + return false; // oh well, we went down hard. + } + } else { + SDL_MemoryBarrierRelease(); + hidden->processed_bytes += buflen; + } + return true; +} + +static int AAUDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + + // AAUDIO_dataCallback picks up our work and unblocks AAUDIO_WaitDevice. But make sure we didn't fail here. + if (SDL_GetAtomicInt(&hidden->error_callback_triggered)) { + SDL_SetAtomicInt(&hidden->error_callback_triggered, 0); + return -1; + } + + SDL_assert(buflen == device->buffer_size); // If this isn't true, we need to change semaphore trigger logic and account for wrapping copies here + size_t offset = (hidden->processed_bytes % hidden->mixbuf_bytes); + SDL_MemoryBarrierAcquire(); + SDL_memcpy(buffer, &hidden->mixbuf[offset], buflen); + hidden->processed_bytes += buflen; + return buflen; +} + +static void AAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + LOGI(__func__); + + if (hidden) { + if (hidden->stream) { + ctx.AAudioStream_requestStop(hidden->stream); + // !!! FIXME: do we have to wait for the state to change to make sure all buffered audio has played, or will close do this (or will the system do this after the close)? + // !!! FIXME: also, will this definitely wait for a running data callback to finish, and then stop the callback from firing again? + ctx.AAudioStream_close(hidden->stream); + } + + if (hidden->semaphore) { + SDL_DestroySemaphore(hidden->semaphore); + } + + SDL_aligned_free(hidden->mixbuf); + SDL_free(hidden); + device->hidden = NULL; + } +} + +static bool BuildAAudioStream(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + const bool recording = device->recording; + aaudio_result_t res; + + SDL_SetAtomicInt(&hidden->error_callback_triggered, 0); + + AAudioStreamBuilder *builder = NULL; + res = ctx.AAudio_createStreamBuilder(&builder); + if (res != AAUDIO_OK) { + LOGI("SDL Failed AAudio_createStreamBuilder %d", res); + return SDL_SetError("SDL Failed AAudio_createStreamBuilder %d", res); + } else if (!builder) { + LOGI("SDL Failed AAudio_createStreamBuilder - builder NULL"); + return SDL_SetError("SDL Failed AAudio_createStreamBuilder - builder NULL"); + } + +#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES + const int aaudio_device_id = (int) ((size_t) device->handle); + LOGI("Opening device id %d", aaudio_device_id); + ctx.AAudioStreamBuilder_setDeviceId(builder, aaudio_device_id); +#endif + + aaudio_format_t format; + if ((device->spec.format == SDL_AUDIO_S32) && (SDL_GetAndroidSDKVersion() >= 31)) { + format = AAUDIO_FORMAT_PCM_I32; + } else if (device->spec.format == SDL_AUDIO_F32) { + format = AAUDIO_FORMAT_PCM_FLOAT; + } else { + format = AAUDIO_FORMAT_PCM_I16; // sint16 is a safe bet for everything else. + } + ctx.AAudioStreamBuilder_setFormat(builder, format); + ctx.AAudioStreamBuilder_setSampleRate(builder, device->spec.freq); + ctx.AAudioStreamBuilder_setChannelCount(builder, device->spec.channels); + + const aaudio_direction_t direction = (recording ? AAUDIO_DIRECTION_INPUT : AAUDIO_DIRECTION_OUTPUT); + ctx.AAudioStreamBuilder_setDirection(builder, direction); + ctx.AAudioStreamBuilder_setErrorCallback(builder, AAUDIO_errorCallback, device); + ctx.AAudioStreamBuilder_setDataCallback(builder, AAUDIO_dataCallback, device); + // Some devices have flat sounding audio when low latency mode is enabled, but this is a better experience for most people + if (SDL_GetHintBoolean(SDL_HINT_ANDROID_LOW_LATENCY_AUDIO, true)) { + SDL_Log("Low latency audio enabled"); + ctx.AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); + } else { + SDL_Log("Low latency audio disabled"); + } + + LOGI("AAudio Try to open %u hz %s %u channels samples %u", + device->spec.freq, SDL_GetAudioFormatName(device->spec.format), + device->spec.channels, device->sample_frames); + + res = ctx.AAudioStreamBuilder_openStream(builder, &hidden->stream); + if (res != AAUDIO_OK) { + LOGI("SDL Failed AAudioStreamBuilder_openStream %d", res); + ctx.AAudioStreamBuilder_delete(builder); + return SDL_SetError("%s : %s", __func__, ctx.AAudio_convertResultToText(res)); + } + ctx.AAudioStreamBuilder_delete(builder); + + device->sample_frames = (int)ctx.AAudioStream_getFramesPerDataCallback(hidden->stream); + if (device->sample_frames == AAUDIO_UNSPECIFIED) { + // We'll get variable frames in the callback, make sure we have at least half a buffer available + device->sample_frames = (int)ctx.AAudioStream_getBufferCapacityInFrames(hidden->stream) / 2; + } + + device->spec.freq = ctx.AAudioStream_getSampleRate(hidden->stream); + device->spec.channels = ctx.AAudioStream_getChannelCount(hidden->stream); + + format = ctx.AAudioStream_getFormat(hidden->stream); + if (format == AAUDIO_FORMAT_PCM_I16) { + device->spec.format = SDL_AUDIO_S16; + } else if (format == AAUDIO_FORMAT_PCM_I32) { + device->spec.format = SDL_AUDIO_S32; + } else if (format == AAUDIO_FORMAT_PCM_FLOAT) { + device->spec.format = SDL_AUDIO_F32; + } else { + return SDL_SetError("Got unexpected audio format %d from AAudioStream_getFormat", (int) format); + } + + SDL_UpdatedAudioDeviceFormat(device); + + // Allocate a triple buffered mixing buffer + // Two buffers can be in the process of being filled while the third is being read + hidden->num_buffers = 3; + hidden->mixbuf_bytes = (hidden->num_buffers * device->buffer_size); + hidden->mixbuf = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), hidden->mixbuf_bytes); + if (!hidden->mixbuf) { + return false; + } + hidden->processed_bytes = 0; + hidden->callback_bytes = 0; + + hidden->semaphore = SDL_CreateSemaphore(recording ? 0 : hidden->num_buffers); + if (!hidden->semaphore) { + LOGI("SDL Failed SDL_CreateSemaphore %s recording:%d", SDL_GetError(), recording); + return false; + } + + LOGI("AAudio Actually opened %u hz %s %u channels samples %u, buffers %d", + device->spec.freq, SDL_GetAudioFormatName(device->spec.format), + device->spec.channels, device->sample_frames, hidden->num_buffers); + + res = ctx.AAudioStream_requestStart(hidden->stream); + if (res != AAUDIO_OK) { + LOGI("SDL Failed AAudioStream_requestStart %d recording:%d", res, recording); + return SDL_SetError("%s : %s", __func__, ctx.AAudio_convertResultToText(res)); + } + + LOGI("SDL AAudioStream_requestStart OK"); + + return true; +} + +// !!! FIXME: make this non-blocking! +static void SDLCALL RequestAndroidPermissionBlockingCallback(void *userdata, const char *permission, bool granted) +{ + SDL_SetAtomicInt((SDL_AtomicInt *) userdata, granted ? 1 : -1); +} + +static bool AAUDIO_OpenDevice(SDL_AudioDevice *device) +{ +#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES + SDL_assert(device->handle); // AAUDIO_UNSPECIFIED is zero, so legit devices should all be non-zero. +#endif + + LOGI(__func__); + + if (device->recording) { + // !!! FIXME: make this non-blocking! + SDL_AtomicInt permission_response; + SDL_SetAtomicInt(&permission_response, 0); + if (!SDL_RequestAndroidPermission("android.permission.RECORD_AUDIO", RequestAndroidPermissionBlockingCallback, &permission_response)) { + return false; + } + + while (SDL_GetAtomicInt(&permission_response) == 0) { + SDL_Delay(10); + } + + if (SDL_GetAtomicInt(&permission_response) < 0) { + LOGI("This app doesn't have RECORD_AUDIO permission"); + return SDL_SetError("This app doesn't have RECORD_AUDIO permission"); + } + } + + device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + return BuildAAudioStream(device); +} + +static bool PauseOneDevice(SDL_AudioDevice *device, void *userdata) +{ + struct SDL_PrivateAudioData *hidden = (struct SDL_PrivateAudioData *)device->hidden; + if (hidden) { + if (hidden->stream) { + aaudio_result_t res; + + if (device->recording) { + // Pause() isn't implemented for recording, use Stop() + res = ctx.AAudioStream_requestStop(hidden->stream); + } else { + res = ctx.AAudioStream_requestPause(hidden->stream); + } + + if (res != AAUDIO_OK) { + LOGI("SDL Failed AAudioStream_requestPause %d", res); + SDL_SetError("%s : %s", __func__, ctx.AAudio_convertResultToText(res)); + } + } + } + return false; // keep enumerating. +} + +// Pause (block) all non already paused audio devices by taking their mixer lock +void AAUDIO_PauseDevices(void) +{ + if (ctx.handle) { // AAUDIO driver is used? + (void) SDL_FindPhysicalAudioDeviceByCallback(PauseOneDevice, NULL); + } +} + +// Resume (unblock) all non already paused audio devices by releasing their mixer lock +static bool ResumeOneDevice(SDL_AudioDevice *device, void *userdata) +{ + struct SDL_PrivateAudioData *hidden = device->hidden; + if (hidden) { + if (hidden->stream) { + aaudio_result_t res = ctx.AAudioStream_requestStart(hidden->stream); + if (res != AAUDIO_OK) { + LOGI("SDL Failed AAudioStream_requestStart %d", res); + SDL_SetError("%s : %s", __func__, ctx.AAudio_convertResultToText(res)); + } + } + } + return false; // keep enumerating. +} + +void AAUDIO_ResumeDevices(void) +{ + if (ctx.handle) { // AAUDIO driver is used? + (void) SDL_FindPhysicalAudioDeviceByCallback(ResumeOneDevice, NULL); + } +} + +static void AAUDIO_Deinitialize(void) +{ + Android_StopAudioHotplug(); + + LOGI(__func__); + if (ctx.handle) { + SDL_UnloadObject(ctx.handle); + } + SDL_zero(ctx); + LOGI("End AAUDIO %s", SDL_GetError()); +} + + +static bool AAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + LOGI(__func__); + + /* AAudio was introduced in Android 8.0, but has reference counting crash issues in that release, + * so don't use it until 8.1. + * + * See https://github.com/google/oboe/issues/40 for more information. + */ + if (SDL_GetAndroidSDKVersion() < 27) { + return false; + } + + SDL_zero(ctx); + + ctx.handle = SDL_LoadObject(LIB_AAUDIO_SO); + if (!ctx.handle) { + LOGI("SDL couldn't find " LIB_AAUDIO_SO); + return false; + } + + if (!AAUDIO_LoadFunctions(&ctx)) { + SDL_UnloadObject(ctx.handle); + SDL_zero(ctx); + return false; + } + + impl->ThreadInit = Android_AudioThreadInit; + impl->Deinitialize = AAUDIO_Deinitialize; + impl->OpenDevice = AAUDIO_OpenDevice; + impl->CloseDevice = AAUDIO_CloseDevice; + impl->WaitDevice = AAUDIO_WaitDevice; + impl->PlayDevice = AAUDIO_PlayDevice; + impl->GetDeviceBuf = AAUDIO_GetDeviceBuf; + impl->WaitRecordingDevice = AAUDIO_WaitDevice; + impl->RecordDevice = AAUDIO_RecordDevice; + + impl->HasRecordingSupport = true; + +#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES + impl->DetectDevices = Android_StartAudioHotplug; +#else + impl->OnlyHasDefaultPlaybackDevice = true; + impl->OnlyHasDefaultRecordingDevice = true; +#endif + + LOGI("SDL AAUDIO_Init OK"); + return true; +} + +AudioBootStrap AAUDIO_bootstrap = { + "AAudio", "AAudio audio driver", AAUDIO_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_AAUDIO diff --git a/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.h b/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.h new file mode 100644 index 0000000..5326bad --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudio.h @@ -0,0 +1,38 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_aaudio_h_ +#define SDL_aaudio_h_ + +#ifdef SDL_AUDIO_DRIVER_AAUDIO + +extern void AAUDIO_ResumeDevices(void); +extern void AAUDIO_PauseDevices(void); + +#else + +#define AAUDIO_ResumeDevices() +#define AAUDIO_PauseDevices() + +#endif + +#endif // SDL_aaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudiofuncs.h b/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudiofuncs.h new file mode 100644 index 0000000..0298821 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/aaudio/SDL_aaudiofuncs.h @@ -0,0 +1,82 @@ +/* + Simple DirectMedia Layer + Copyright , (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#define SDL_PROC_UNUSED(ret, func, params) + +SDL_PROC(const char *, AAudio_convertResultToText, (aaudio_result_t returnCode)) +SDL_PROC(const char *, AAudio_convertStreamStateToText, (aaudio_stream_state_t state)) +SDL_PROC(aaudio_result_t, AAudio_createStreamBuilder, (AAudioStreamBuilder * *builder)) +SDL_PROC(void, AAudioStreamBuilder_setDeviceId, (AAudioStreamBuilder * builder, int32_t deviceId)) +SDL_PROC(void, AAudioStreamBuilder_setSampleRate, (AAudioStreamBuilder * builder, int32_t sampleRate)) +SDL_PROC(void, AAudioStreamBuilder_setChannelCount, (AAudioStreamBuilder * builder, int32_t channelCount)) +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setSamplesPerFrame, (AAudioStreamBuilder * builder, int32_t samplesPerFrame)) +SDL_PROC(void, AAudioStreamBuilder_setFormat, (AAudioStreamBuilder * builder, aaudio_format_t format)) +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setSharingMode, (AAudioStreamBuilder * builder, aaudio_sharing_mode_t sharingMode)) +SDL_PROC(void, AAudioStreamBuilder_setDirection, (AAudioStreamBuilder * builder, aaudio_direction_t direction)) +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setBufferCapacityInFrames, (AAudioStreamBuilder * builder, int32_t numFrames)) +SDL_PROC(void, AAudioStreamBuilder_setPerformanceMode, (AAudioStreamBuilder * builder, aaudio_performance_mode_t mode)) +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setUsage, (AAudioStreamBuilder * builder, aaudio_usage_t usage)) // API 28 +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setContentType, (AAudioStreamBuilder * builder, aaudio_content_type_t contentType)) // API 28 +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setInputPreset, (AAudioStreamBuilder * builder, aaudio_input_preset_t inputPreset)) // API 28 +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setAllowedCapturePolicy, (AAudioStreamBuilder * builder, aaudio_allowed_capture_policy_t capturePolicy)) // API 29 +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setSessionId, (AAudioStreamBuilder * builder, aaudio_session_id_t sessionId)) // API 28 +SDL_PROC_UNUSED(void, AAudioStreamBuilder_setPrivacySensitive, (AAudioStreamBuilder * builder, bool privacySensitive)) // API 30 +SDL_PROC(void, AAudioStreamBuilder_setDataCallback, (AAudioStreamBuilder * builder, AAudioStream_dataCallback callback, void *userData)) +SDL_PROC(void, AAudioStreamBuilder_setFramesPerDataCallback, (AAudioStreamBuilder * builder, int32_t numFrames)) +SDL_PROC(void, AAudioStreamBuilder_setErrorCallback, (AAudioStreamBuilder * builder, AAudioStream_errorCallback callback, void *userData)) +SDL_PROC(aaudio_result_t, AAudioStreamBuilder_openStream, (AAudioStreamBuilder * builder, AAudioStream **stream)) +SDL_PROC(aaudio_result_t, AAudioStreamBuilder_delete, (AAudioStreamBuilder * builder)) +SDL_PROC_UNUSED(aaudio_result_t, AAudioStream_release, (AAudioStream * stream)) // API 30 +SDL_PROC(aaudio_result_t, AAudioStream_close, (AAudioStream * stream)) +SDL_PROC(aaudio_result_t, AAudioStream_requestStart, (AAudioStream * stream)) +SDL_PROC(aaudio_result_t, AAudioStream_requestPause, (AAudioStream * stream)) +SDL_PROC_UNUSED(aaudio_result_t, AAudioStream_requestFlush, (AAudioStream * stream)) +SDL_PROC(aaudio_result_t, AAudioStream_requestStop, (AAudioStream * stream)) +SDL_PROC(aaudio_stream_state_t, AAudioStream_getState, (AAudioStream * stream)) +SDL_PROC_UNUSED(aaudio_result_t, AAudioStream_waitForStateChange, (AAudioStream * stream, aaudio_stream_state_t inputState, aaudio_stream_state_t *nextState, int64_t timeoutNanoseconds)) +SDL_PROC_UNUSED(aaudio_result_t, AAudioStream_read, (AAudioStream * stream, void *buffer, int32_t numFrames, int64_t timeoutNanoseconds)) +SDL_PROC_UNUSED(aaudio_result_t, AAudioStream_write, (AAudioStream * stream, const void *buffer, int32_t numFrames, int64_t timeoutNanoseconds)) +SDL_PROC_UNUSED(aaudio_result_t, AAudioStream_setBufferSizeInFrames, (AAudioStream * stream, int32_t numFrames)) +SDL_PROC_UNUSED(int32_t, AAudioStream_getBufferSizeInFrames, (AAudioStream * stream)) +SDL_PROC_UNUSED(int32_t, AAudioStream_getFramesPerBurst, (AAudioStream * stream)) +SDL_PROC(int32_t, AAudioStream_getBufferCapacityInFrames, (AAudioStream * stream)) +SDL_PROC(int32_t, AAudioStream_getFramesPerDataCallback, (AAudioStream * stream)) +SDL_PROC_UNUSED(int32_t, AAudioStream_getXRunCount, (AAudioStream * stream)) +SDL_PROC(int32_t, AAudioStream_getSampleRate, (AAudioStream * stream)) +SDL_PROC(int32_t, AAudioStream_getChannelCount, (AAudioStream * stream)) +SDL_PROC_UNUSED(int32_t, AAudioStream_getSamplesPerFrame, (AAudioStream * stream)) +SDL_PROC_UNUSED(int32_t, AAudioStream_getDeviceId, (AAudioStream * stream)) +SDL_PROC(aaudio_format_t, AAudioStream_getFormat, (AAudioStream * stream)) +SDL_PROC_UNUSED(aaudio_sharing_mode_t, AAudioStream_getSharingMode, (AAudioStream * stream)) +SDL_PROC_UNUSED(aaudio_performance_mode_t, AAudioStream_getPerformanceMode, (AAudioStream * stream)) +SDL_PROC_UNUSED(aaudio_direction_t, AAudioStream_getDirection, (AAudioStream * stream)) +SDL_PROC_UNUSED(int64_t, AAudioStream_getFramesWritten, (AAudioStream * stream)) +SDL_PROC_UNUSED(int64_t, AAudioStream_getFramesRead, (AAudioStream * stream)) +SDL_PROC_UNUSED(aaudio_session_id_t, AAudioStream_getSessionId, (AAudioStream * stream)) // API 28 +SDL_PROC(aaudio_result_t, AAudioStream_getTimestamp, (AAudioStream * stream, clockid_t clockid, int64_t *framePosition, int64_t *timeNanoseconds)) +SDL_PROC_UNUSED(aaudio_usage_t, AAudioStream_getUsage, (AAudioStream * stream)) // API 28 +SDL_PROC_UNUSED(aaudio_content_type_t, AAudioStream_getContentType, (AAudioStream * stream)) // API 28 +SDL_PROC_UNUSED(aaudio_input_preset_t, AAudioStream_getInputPreset, (AAudioStream * stream)) // API 28 +SDL_PROC_UNUSED(aaudio_allowed_capture_policy_t, AAudioStream_getAllowedCapturePolicy, (AAudioStream * stream)) // API 29 +SDL_PROC_UNUSED(bool, AAudioStream_isPrivacySensitive, (AAudioStream * stream)) // API 30 + +#undef SDL_PROC +#undef SDL_PROC_UNUSED diff --git a/contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.c b/contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.c new file mode 100644 index 0000000..308eb23 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.c @@ -0,0 +1,1519 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_ALSA + +#ifndef SDL_ALSA_NON_BLOCKING +#define SDL_ALSA_NON_BLOCKING 0 +#endif + +// without the thread, you will detect devices on startup, but will not get further hotplug events. But that might be okay. +#ifndef SDL_ALSA_HOTPLUG_THREAD +#define SDL_ALSA_HOTPLUG_THREAD 1 +#endif + +// this turns off debug logging completely (but by default this goes to the bitbucket). +#ifndef SDL_ALSA_DEBUG +#define SDL_ALSA_DEBUG 1 +#endif + +#include "../SDL_sysaudio.h" +#include "SDL_alsa_audio.h" + +#if SDL_ALSA_DEBUG +#define LOGDEBUG(...) SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "ALSA: " __VA_ARGS__) +#else +#define LOGDEBUG(...) +#endif + +//TODO: cleanup once the code settled down + +static int (*ALSA_snd_pcm_open)(snd_pcm_t **, const char *, snd_pcm_stream_t, int); +static int (*ALSA_snd_pcm_close)(snd_pcm_t *pcm); +static int (*ALSA_snd_pcm_start)(snd_pcm_t *pcm); +static snd_pcm_sframes_t (*ALSA_snd_pcm_writei)(snd_pcm_t *, const void *, snd_pcm_uframes_t); +static snd_pcm_sframes_t (*ALSA_snd_pcm_readi)(snd_pcm_t *, void *, snd_pcm_uframes_t); +static int (*ALSA_snd_pcm_recover)(snd_pcm_t *, int, int); +static int (*ALSA_snd_pcm_prepare)(snd_pcm_t *); +static int (*ALSA_snd_pcm_drain)(snd_pcm_t *); +static const char *(*ALSA_snd_strerror)(int); +static size_t (*ALSA_snd_pcm_hw_params_sizeof)(void); +static size_t (*ALSA_snd_pcm_sw_params_sizeof)(void); +static void (*ALSA_snd_pcm_hw_params_copy)(snd_pcm_hw_params_t *, const snd_pcm_hw_params_t *); +static int (*ALSA_snd_pcm_hw_params_any)(snd_pcm_t *, snd_pcm_hw_params_t *); +static int (*ALSA_snd_pcm_hw_params_set_access)(snd_pcm_t *, snd_pcm_hw_params_t *, snd_pcm_access_t); +static int (*ALSA_snd_pcm_hw_params_set_format)(snd_pcm_t *, snd_pcm_hw_params_t *, snd_pcm_format_t); +static int (*ALSA_snd_pcm_hw_params_set_channels)(snd_pcm_t *, snd_pcm_hw_params_t *, unsigned int); +static int (*ALSA_snd_pcm_hw_params_get_channels)(const snd_pcm_hw_params_t *, unsigned int *); +static int (*ALSA_snd_pcm_hw_params_set_rate_near)(snd_pcm_t *, snd_pcm_hw_params_t *, unsigned int *, int *); +static int (*ALSA_snd_pcm_hw_params_set_period_size_near)(snd_pcm_t *, snd_pcm_hw_params_t *, snd_pcm_uframes_t *, int *); +static int (*ALSA_snd_pcm_hw_params_get_period_size)(const snd_pcm_hw_params_t *, snd_pcm_uframes_t *, int *); +static int (*ALSA_snd_pcm_hw_params_set_periods_min)(snd_pcm_t *, snd_pcm_hw_params_t *, unsigned int *, int *); +static int (*ALSA_snd_pcm_hw_params_set_periods_first)(snd_pcm_t *, snd_pcm_hw_params_t *, unsigned int *, int *); +static int (*ALSA_snd_pcm_hw_params_get_periods)(const snd_pcm_hw_params_t *, unsigned int *, int *); +static int (*ALSA_snd_pcm_hw_params_set_buffer_size_near)(snd_pcm_t *pcm, snd_pcm_hw_params_t *, snd_pcm_uframes_t *); +static int (*ALSA_snd_pcm_hw_params_get_buffer_size)(const snd_pcm_hw_params_t *, snd_pcm_uframes_t *); +static int (*ALSA_snd_pcm_hw_params)(snd_pcm_t *, snd_pcm_hw_params_t *); +static int (*ALSA_snd_pcm_sw_params_current)(snd_pcm_t *, + snd_pcm_sw_params_t *); +static int (*ALSA_snd_pcm_sw_params_set_start_threshold)(snd_pcm_t *, snd_pcm_sw_params_t *, snd_pcm_uframes_t); +static int (*ALSA_snd_pcm_sw_params)(snd_pcm_t *, snd_pcm_sw_params_t *); +static int (*ALSA_snd_pcm_nonblock)(snd_pcm_t *, int); +static int (*ALSA_snd_pcm_wait)(snd_pcm_t *, int); +static int (*ALSA_snd_pcm_sw_params_set_avail_min)(snd_pcm_t *, snd_pcm_sw_params_t *, snd_pcm_uframes_t); +static int (*ALSA_snd_pcm_reset)(snd_pcm_t *); +static int (*ALSA_snd_device_name_hint)(int, const char *, void ***); +static char *(*ALSA_snd_device_name_get_hint)(const void *, const char *); +static int (*ALSA_snd_device_name_free_hint)(void **); +static snd_pcm_sframes_t (*ALSA_snd_pcm_avail)(snd_pcm_t *); +static size_t (*ALSA_snd_ctl_card_info_sizeof)(void); +static size_t (*ALSA_snd_pcm_info_sizeof)(void); +static int (*ALSA_snd_card_next)(int*); +static int (*ALSA_snd_ctl_open)(snd_ctl_t **,const char *,int); +static int (*ALSA_snd_ctl_close)(snd_ctl_t *); +static int (*ALSA_snd_ctl_card_info)(snd_ctl_t *, snd_ctl_card_info_t *); +static int (*ALSA_snd_ctl_pcm_next_device)(snd_ctl_t *, int *); +static unsigned int (*ALSA_snd_pcm_info_get_subdevices_count)(const snd_pcm_info_t *); +static void (*ALSA_snd_pcm_info_set_device)(snd_pcm_info_t *, unsigned int); +static void (*ALSA_snd_pcm_info_set_subdevice)(snd_pcm_info_t *, unsigned int); +static void (*ALSA_snd_pcm_info_set_stream)(snd_pcm_info_t *, snd_pcm_stream_t); +static int (*ALSA_snd_ctl_pcm_info)(snd_ctl_t *, snd_pcm_info_t *); +static unsigned int (*ALSA_snd_pcm_info_get_subdevices_count)(const snd_pcm_info_t *); +static const char *(*ALSA_snd_ctl_card_info_get_id)(const snd_ctl_card_info_t *); +static const char *(*ALSA_snd_pcm_info_get_name)(const snd_pcm_info_t *); +static const char *(*ALSA_snd_pcm_info_get_subdevice_name)(const snd_pcm_info_t *); +static const char *(*ALSA_snd_ctl_card_info_get_name)(const snd_ctl_card_info_t *); +static void (*ALSA_snd_ctl_card_info_clear)(snd_ctl_card_info_t *); +static int (*ALSA_snd_pcm_hw_free)(snd_pcm_t *); +static int (*ALSA_snd_pcm_hw_params_set_channels_near)(snd_pcm_t *, snd_pcm_hw_params_t *, unsigned int *); +static snd_pcm_chmap_query_t **(*ALSA_snd_pcm_query_chmaps)(snd_pcm_t *pcm); +static void (*ALSA_snd_pcm_free_chmaps)(snd_pcm_chmap_query_t **maps); +static int (*ALSA_snd_pcm_set_chmap)(snd_pcm_t *, const snd_pcm_chmap_t *); +static int (*ALSA_snd_pcm_chmap_print)(const snd_pcm_chmap_t *, size_t, char *); + +#ifdef SDL_AUDIO_DRIVER_ALSA_DYNAMIC +#define snd_pcm_hw_params_sizeof ALSA_snd_pcm_hw_params_sizeof +#define snd_pcm_sw_params_sizeof ALSA_snd_pcm_sw_params_sizeof + +static const char *alsa_library = SDL_AUDIO_DRIVER_ALSA_DYNAMIC; +static SDL_SharedObject *alsa_handle = NULL; + +static bool load_alsa_sym(const char *fn, void **addr) +{ + *addr = SDL_LoadFunction(alsa_handle, fn); + if (!*addr) { + // Don't call SDL_SetError(): SDL_LoadFunction already did. + return false; + } + + return true; +} + +// cast funcs to char* first, to please GCC's strict aliasing rules. +#define SDL_ALSA_SYM(x) \ + if (!load_alsa_sym(#x, (void **)(char *)&ALSA_##x)) \ + return false +#else +#define SDL_ALSA_SYM(x) ALSA_##x = x +#endif + +static bool load_alsa_syms(void) +{ + SDL_ALSA_SYM(snd_pcm_open); + SDL_ALSA_SYM(snd_pcm_close); + SDL_ALSA_SYM(snd_pcm_start); + SDL_ALSA_SYM(snd_pcm_writei); + SDL_ALSA_SYM(snd_pcm_readi); + SDL_ALSA_SYM(snd_pcm_recover); + SDL_ALSA_SYM(snd_pcm_prepare); + SDL_ALSA_SYM(snd_pcm_drain); + SDL_ALSA_SYM(snd_strerror); + SDL_ALSA_SYM(snd_pcm_hw_params_sizeof); + SDL_ALSA_SYM(snd_pcm_sw_params_sizeof); + SDL_ALSA_SYM(snd_pcm_hw_params_copy); + SDL_ALSA_SYM(snd_pcm_hw_params_any); + SDL_ALSA_SYM(snd_pcm_hw_params_set_access); + SDL_ALSA_SYM(snd_pcm_hw_params_set_format); + SDL_ALSA_SYM(snd_pcm_hw_params_set_channels); + SDL_ALSA_SYM(snd_pcm_hw_params_get_channels); + SDL_ALSA_SYM(snd_pcm_hw_params_set_rate_near); + SDL_ALSA_SYM(snd_pcm_hw_params_set_period_size_near); + SDL_ALSA_SYM(snd_pcm_hw_params_get_period_size); + SDL_ALSA_SYM(snd_pcm_hw_params_set_periods_min); + SDL_ALSA_SYM(snd_pcm_hw_params_set_periods_first); + SDL_ALSA_SYM(snd_pcm_hw_params_get_periods); + SDL_ALSA_SYM(snd_pcm_hw_params_set_buffer_size_near); + SDL_ALSA_SYM(snd_pcm_hw_params_get_buffer_size); + SDL_ALSA_SYM(snd_pcm_hw_params); + SDL_ALSA_SYM(snd_pcm_sw_params_current); + SDL_ALSA_SYM(snd_pcm_sw_params_set_start_threshold); + SDL_ALSA_SYM(snd_pcm_sw_params); + SDL_ALSA_SYM(snd_pcm_nonblock); + SDL_ALSA_SYM(snd_pcm_wait); + SDL_ALSA_SYM(snd_pcm_sw_params_set_avail_min); + SDL_ALSA_SYM(snd_pcm_reset); + SDL_ALSA_SYM(snd_device_name_hint); + SDL_ALSA_SYM(snd_device_name_get_hint); + SDL_ALSA_SYM(snd_device_name_free_hint); + SDL_ALSA_SYM(snd_pcm_avail); + SDL_ALSA_SYM(snd_ctl_card_info_sizeof); + SDL_ALSA_SYM(snd_pcm_info_sizeof); + SDL_ALSA_SYM(snd_card_next); + SDL_ALSA_SYM(snd_ctl_open); + SDL_ALSA_SYM(snd_ctl_close); + SDL_ALSA_SYM(snd_ctl_card_info); + SDL_ALSA_SYM(snd_ctl_pcm_next_device); + SDL_ALSA_SYM(snd_pcm_info_get_subdevices_count); + SDL_ALSA_SYM(snd_pcm_info_set_device); + SDL_ALSA_SYM(snd_pcm_info_set_subdevice); + SDL_ALSA_SYM(snd_pcm_info_set_stream); + SDL_ALSA_SYM(snd_ctl_pcm_info); + SDL_ALSA_SYM(snd_pcm_info_get_subdevices_count); + SDL_ALSA_SYM(snd_ctl_card_info_get_id); + SDL_ALSA_SYM(snd_pcm_info_get_name); + SDL_ALSA_SYM(snd_pcm_info_get_subdevice_name); + SDL_ALSA_SYM(snd_ctl_card_info_get_name); + SDL_ALSA_SYM(snd_ctl_card_info_clear); + SDL_ALSA_SYM(snd_pcm_hw_free); + SDL_ALSA_SYM(snd_pcm_hw_params_set_channels_near); + SDL_ALSA_SYM(snd_pcm_query_chmaps); + SDL_ALSA_SYM(snd_pcm_free_chmaps); + SDL_ALSA_SYM(snd_pcm_set_chmap); + SDL_ALSA_SYM(snd_pcm_chmap_print); + + return true; +} + +#undef SDL_ALSA_SYM + +#ifdef SDL_AUDIO_DRIVER_ALSA_DYNAMIC + +static void UnloadALSALibrary(void) +{ + if (alsa_handle) { + SDL_UnloadObject(alsa_handle); + alsa_handle = NULL; + } +} + +static bool LoadALSALibrary(void) +{ + bool retval = true; + if (!alsa_handle) { + alsa_handle = SDL_LoadObject(alsa_library); + if (!alsa_handle) { + retval = false; + // Don't call SDL_SetError(): SDL_LoadObject already did. + } else { + retval = load_alsa_syms(); + if (!retval) { + UnloadALSALibrary(); + } + } + } + return retval; +} + +#else + +static void UnloadALSALibrary(void) +{ +} + +static bool LoadALSALibrary(void) +{ + load_alsa_syms(); + return true; +} + +#endif // SDL_AUDIO_DRIVER_ALSA_DYNAMIC + +static const char *ALSA_device_prefix = NULL; +static void ALSA_guess_device_prefix(void) +{ + if (ALSA_device_prefix) { + return; // already calculated. + } + + // Apparently there are several different ways that ALSA lists + // actual hardware. It could be prefixed with "hw:" or "default:" + // or "sysdefault:" and maybe others. Go through the list and see + // if we can find a preferred prefix for the system. + + static const char *const prefixes[] = { + "hw:", "sysdefault:", "default:" + }; + + void **hints = NULL; + if (ALSA_snd_device_name_hint(-1, "pcm", &hints) == 0) { + for (int i = 0; hints[i]; i++) { + char *name = ALSA_snd_device_name_get_hint(hints[i], "NAME"); + if (name) { + for (int j = 0; j < SDL_arraysize(prefixes); j++) { + const char *prefix = prefixes[j]; + const size_t prefixlen = SDL_strlen(prefix); + if (SDL_strncmp(name, prefix, prefixlen) == 0) { + ALSA_device_prefix = prefix; + break; + } + } + free(name); // This should NOT be SDL_free() + + if (ALSA_device_prefix) { + break; + } + } + } + } + + if (!ALSA_device_prefix) { + ALSA_device_prefix = prefixes[0]; // oh well. + } + + LOGDEBUG("device prefix is probably '%s'", ALSA_device_prefix); +} + +typedef struct ALSA_Device +{ + // the unicity key is the couple (id,recording) + char *id; // empty means canonical default + char *name; + bool recording; + struct ALSA_Device *next; +} ALSA_Device; + +static const ALSA_Device default_playback_handle = { + "", + "default", + false, + NULL +}; + +static const ALSA_Device default_recording_handle = { + "", + "default", + true, + NULL +}; + +// TODO: Figure out the "right"(TM) way. For the moment we presume that if a system is using a +// software mixer for application audio sharing which is not the linux native alsa[dmix], for +// instance jack/pulseaudio2[pipewire]/pulseaudio1/esound/etc, we expect the system integrators did +// configure the canonical default to the right alsa PCM plugin for their software mixer. +// +// All the above may be completely wrong. +static char *get_pcm_str(void *handle) +{ + SDL_assert(handle != NULL); // SDL2 used NULL to mean "default" but that's not true in SDL3. + ALSA_Device *dev = (ALSA_Device *)handle; + char *pcm_str = NULL; + + if (SDL_strlen(dev->id) == 0) { + // If the user does not want to go thru the default PCM or the canonical default, the + // the configuration space being _massive_, give the user the ability to specify + // its own PCMs using environment variables. It will have to fit SDL constraints though. + const char *devname = SDL_GetHint(dev->recording ? SDL_HINT_AUDIO_ALSA_DEFAULT_RECORDING_DEVICE : SDL_HINT_AUDIO_ALSA_DEFAULT_PLAYBACK_DEVICE); + if (!devname) { + devname = SDL_GetHint(SDL_HINT_AUDIO_ALSA_DEFAULT_DEVICE); + if (!devname) { + devname = "default"; + } + } + pcm_str = SDL_strdup(devname); + } else { + SDL_asprintf(&pcm_str, "%sCARD=%s", ALSA_device_prefix, dev->id); + } + return pcm_str; +} + +// This function waits until it is possible to write a full sound buffer +static bool ALSA_WaitDevice(SDL_AudioDevice *device) +{ + const int fulldelay = (int) ((((Uint64) device->sample_frames) * 1000) / device->spec.freq); + const int delay = SDL_max(fulldelay, 10); + + while (!SDL_GetAtomicInt(&device->shutdown)) { + const int rc = ALSA_snd_pcm_wait(device->hidden->pcm, delay); + if (rc < 0 && (rc != -EAGAIN)) { + const int status = ALSA_snd_pcm_recover(device->hidden->pcm, rc, 0); + if (status < 0) { + // Hmm, not much we can do - abort + SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "ALSA: snd_pcm_wait failed (unrecoverable): %s", ALSA_snd_strerror(rc)); + return false; + } + continue; + } + + if (rc > 0) { + break; // ready to go! + } + + // Timed out! Make sure we aren't shutting down and then wait again. + } + + return true; +} + +static bool ALSA_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + SDL_assert(buffer == device->hidden->mixbuf); + Uint8 *sample_buf = (Uint8 *) buffer; // !!! FIXME: deal with this without casting away constness + const int frame_size = SDL_AUDIO_FRAMESIZE(device->spec); + snd_pcm_uframes_t frames_left = (snd_pcm_uframes_t) (buflen / frame_size); + + while ((frames_left > 0) && !SDL_GetAtomicInt(&device->shutdown)) { + const int rc = ALSA_snd_pcm_writei(device->hidden->pcm, sample_buf, frames_left); + //SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "ALSA PLAYDEVICE: WROTE %d of %d bytes", (rc >= 0) ? ((int) (rc * frame_size)) : rc, (int) (frames_left * frame_size)); + SDL_assert(rc != 0); // assuming this can't happen if we used snd_pcm_wait and queried for available space. + if (rc < 0) { + SDL_assert(rc != -EAGAIN); // assuming this can't happen if we used snd_pcm_wait and queried for available space. snd_pcm_recover won't handle it! + const int status = ALSA_snd_pcm_recover(device->hidden->pcm, rc, 0); + if (status < 0) { + // Hmm, not much we can do - abort + SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "ALSA write failed (unrecoverable): %s", ALSA_snd_strerror(rc)); + return false; + } + continue; + } + + sample_buf += rc * frame_size; + frames_left -= rc; + } + + return true; +} + +static Uint8 *ALSA_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + snd_pcm_sframes_t rc = ALSA_snd_pcm_avail(device->hidden->pcm); + if (rc <= 0) { + // Wait a bit and try again, maybe the hardware isn't quite ready yet? + SDL_Delay(1); + + rc = ALSA_snd_pcm_avail(device->hidden->pcm); + if (rc <= 0) { + // We'll catch it next time + *buffer_size = 0; + return NULL; + } + } + + const int requested_frames = SDL_min(device->sample_frames, rc); + const int requested_bytes = requested_frames * SDL_AUDIO_FRAMESIZE(device->spec); + SDL_assert(requested_bytes <= *buffer_size); + //SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "ALSA GETDEVICEBUF: NEED %d BYTES", requested_bytes); + *buffer_size = requested_bytes; + return device->hidden->mixbuf; +} + +static int ALSA_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + const int frame_size = SDL_AUDIO_FRAMESIZE(device->spec); + SDL_assert((buflen % frame_size) == 0); + + const snd_pcm_sframes_t total_available = ALSA_snd_pcm_avail(device->hidden->pcm); + const int total_frames = SDL_min(buflen / frame_size, total_available); + + const int rc = ALSA_snd_pcm_readi(device->hidden->pcm, buffer, total_frames); + + SDL_assert(rc != -EAGAIN); // assuming this can't happen if we used snd_pcm_wait and queried for available space. snd_pcm_recover won't handle it! + + if (rc < 0) { + const int status = ALSA_snd_pcm_recover(device->hidden->pcm, rc, 0); + if (status < 0) { + // Hmm, not much we can do - abort + SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "ALSA read failed (unrecoverable): %s", ALSA_snd_strerror(rc)); + return -1; + } + return 0; // go back to WaitDevice and try again. + } + + //SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "ALSA: recorded %d bytes", rc * frame_size); + + return rc * frame_size; +} + +static void ALSA_FlushRecording(SDL_AudioDevice *device) +{ + ALSA_snd_pcm_reset(device->hidden->pcm); +} + +static void ALSA_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->pcm) { + // Wait for the submitted audio to drain. ALSA_snd_pcm_drop() can hang, so don't use that. + SDL_Delay(((device->sample_frames * 1000) / device->spec.freq) * 2); + ALSA_snd_pcm_close(device->hidden->pcm); + } + SDL_free(device->hidden->mixbuf); + SDL_free(device->hidden); + } +} + + +// To make easier to track parameters during the whole alsa pcm configuration: +struct ALSA_pcm_cfg_ctx { + SDL_AudioDevice *device; + + snd_pcm_hw_params_t *hwparams; + snd_pcm_sw_params_t *swparams; + + SDL_AudioFormat matched_sdl_format; + unsigned int chans_n; + unsigned int target_chans_n; + unsigned int rate; + snd_pcm_uframes_t persize; // alsa period size, SDL audio device sample_frames + snd_pcm_chmap_query_t **chmap_queries; + unsigned int sdl_chmap[SDL_AUDIO_ALSA__CHMAP_CHANS_N_MAX]; + unsigned int alsa_chmap_installed[SDL_AUDIO_ALSA__CHMAP_CHANS_N_MAX]; + + unsigned int periods; +}; +// The following are SDL channel maps with alsa position values, from 0 channels to 8 channels. +// See SDL3/SDL_audio.h +// Strictly speaking those are "parameters" of channel maps, like alsa hwparams and swparams, they +// have to be "reduced/refined" until an exact channel map. Only the 6 channels map requires such +// "reduction/refine". +static enum snd_pcm_chmap_position sdl_channel_maps[SDL_AUDIO_ALSA__SDL_CHMAPS_N][SDL_AUDIO_ALSA__CHMAP_CHANS_N_MAX] = { + // 0 channels + { + 0 + }, + // 1 channel + { + SND_CHMAP_MONO, + }, + // 2 channels + { + SND_CHMAP_FL, + SND_CHMAP_FR, + }, + // 3 channels + { + SND_CHMAP_FL, + SND_CHMAP_FR, + SND_CHMAP_LFE, + }, + // 4 channels + { + SND_CHMAP_FL, + SND_CHMAP_FR, + SND_CHMAP_RL, + SND_CHMAP_RR, + }, + // 5 channels + { + SND_CHMAP_FL, + SND_CHMAP_FR, + SND_CHMAP_LFE, + SND_CHMAP_RL, + SND_CHMAP_RR, + }, + // 6 channels + // XXX: here we encode not a uniq channel map but a set of channel maps. We will reduce it each + // time we are going to work with an alsa 6 channels map. + { + SND_CHMAP_FL, + SND_CHMAP_FR, + SND_CHMAP_FC, + SND_CHMAP_LFE, + // The 2 following channel positions are (SND_CHMAP_SL,SND_CHMAP_SR) or + // (SND_CHMAP_RL,SND_CHMAP_RR) + SND_CHMAP_UNKNOWN, + SND_CHMAP_UNKNOWN, + }, + // 7 channels + { + SND_CHMAP_FL, + SND_CHMAP_FR, + SND_CHMAP_FC, + SND_CHMAP_LFE, + SND_CHMAP_RC, + SND_CHMAP_SL, + SND_CHMAP_SR, + }, + // 8 channels + { + SND_CHMAP_FL, + SND_CHMAP_FR, + SND_CHMAP_FC, + SND_CHMAP_LFE, + SND_CHMAP_RL, + SND_CHMAP_RR, + SND_CHMAP_SL, + SND_CHMAP_SR, + }, +}; + +// Helper for the function right below. +static bool has_pos(const unsigned int *chmap, unsigned int pos) +{ + for (unsigned int chan_idx = 0; ; chan_idx++) { + if (chan_idx == 6) { + return false; + } + if (chmap[chan_idx] == pos) { + return true; + } + } + SDL_assert(!"Shouldn't hit this code."); + return false; +} + +// XXX: Each time we are going to work on an alsa 6 channels map, we must reduce the set of channel +// maps which is encoded in sdl_channel_maps[6] to a uniq one. +#define HAVE_NONE 0 +#define HAVE_REAR 1 +#define HAVE_SIDE 2 +#define HAVE_BOTH 3 +static void sdl_6chans_set_rear_or_side_channels_from_alsa_6chans(unsigned int *sdl_6chans, const unsigned int *alsa_6chans) +{ + // For alsa channel maps with 6 channels and with SND_CHMAP_FL,SND_CHMAP_FR,SND_CHMAP_FC, + // SND_CHMAP_LFE, reduce our 6 channels maps to a uniq one. + if ( !has_pos(alsa_6chans, SND_CHMAP_FL) || + !has_pos(alsa_6chans, SND_CHMAP_FR) || + !has_pos(alsa_6chans, SND_CHMAP_FC) || + !has_pos(alsa_6chans, SND_CHMAP_LFE)) { + sdl_6chans[4] = SND_CHMAP_UNKNOWN; + sdl_6chans[5] = SND_CHMAP_UNKNOWN; + LOGDEBUG("6channels:unsupported channel map"); + return; + } + + unsigned int state = HAVE_NONE; + for (unsigned int chan_idx = 0; chan_idx < 6; chan_idx++) { + if ((alsa_6chans[chan_idx] == SND_CHMAP_SL) || (alsa_6chans[chan_idx] == SND_CHMAP_SR)) { + if (state == HAVE_NONE) { + state = HAVE_SIDE; + } else if (state == HAVE_REAR) { + state = HAVE_BOTH; + break; + } + } else if ((alsa_6chans[chan_idx] == SND_CHMAP_RL) || (alsa_6chans[chan_idx] == SND_CHMAP_RR)) { + if (state == HAVE_NONE) { + state = HAVE_REAR; + } else if (state == HAVE_SIDE) { + state = HAVE_BOTH; + break; + } + } + } + + if ((state == HAVE_BOTH) || (state == HAVE_NONE)) { + sdl_6chans[4] = SND_CHMAP_UNKNOWN; + sdl_6chans[5] = SND_CHMAP_UNKNOWN; + LOGDEBUG("6channels:unsupported channel map"); + } else if (state == HAVE_REAR) { + sdl_6chans[4] = SND_CHMAP_RL; + sdl_6chans[5] = SND_CHMAP_RR; + LOGDEBUG("6channels:sdl map set to rear"); + } else { // state == HAVE_SIDE + sdl_6chans[4] = SND_CHMAP_SL; + sdl_6chans[5] = SND_CHMAP_SR; + LOGDEBUG("6channels:sdl map set to side"); + } +} +#undef HAVE_NONE +#undef HAVE_REAR +#undef HAVE_SIDE +#undef HAVE_BOTH + +static void swizzle_map_compute_alsa_subscan(const struct ALSA_pcm_cfg_ctx *ctx, int *swizzle_map, unsigned int sdl_pos_idx) +{ + swizzle_map[sdl_pos_idx] = -1; + for (unsigned int alsa_pos_idx = 0; ; alsa_pos_idx++) { + SDL_assert(alsa_pos_idx != ctx->chans_n); // no 0 channels or not found matching position should happen here (actually enforce playback/recording symmetry). + if (ctx->alsa_chmap_installed[alsa_pos_idx] == ctx->sdl_chmap[sdl_pos_idx]) { + LOGDEBUG("swizzle SDL %u <-> alsa %u", sdl_pos_idx,alsa_pos_idx); + swizzle_map[sdl_pos_idx] = (int) alsa_pos_idx; + return; + } + } +} + +// XXX: this must stay playback/recording symetric. +static void swizzle_map_compute(const struct ALSA_pcm_cfg_ctx *ctx, int *swizzle_map, bool *needs_swizzle) +{ + *needs_swizzle = false; + for (unsigned int sdl_pos_idx = 0; sdl_pos_idx != ctx->chans_n; sdl_pos_idx++) { + swizzle_map_compute_alsa_subscan(ctx, swizzle_map, sdl_pos_idx); + if (swizzle_map[sdl_pos_idx] != sdl_pos_idx) { + *needs_swizzle = true; + } + } +} + +#define CHMAP_INSTALLED 0 +#define CHANS_N_NEXT 1 +#define CHMAP_NOT_FOUND 2 +// Should always be a queried alsa channel map unless the queried alsa channel map was of type VAR, +// namely we can program the channel positions directly from the SDL channel map. +static int alsa_chmap_install(struct ALSA_pcm_cfg_ctx *ctx, const unsigned int *chmap) +{ + bool isstack; + snd_pcm_chmap_t *chmap_to_install = (snd_pcm_chmap_t*)SDL_small_alloc(unsigned int, 1 + ctx->chans_n, &isstack); + if (!chmap_to_install) { + return -1; + } + + chmap_to_install->channels = ctx->chans_n; + SDL_memcpy(chmap_to_install->pos, chmap, sizeof (unsigned int) * ctx->chans_n); + + #if SDL_ALSA_DEBUG + char logdebug_chmap_str[128]; + ALSA_snd_pcm_chmap_print(chmap_to_install,sizeof(logdebug_chmap_str),logdebug_chmap_str); + LOGDEBUG("channel map to install:%s",logdebug_chmap_str); + #endif + + int status = ALSA_snd_pcm_set_chmap(ctx->device->hidden->pcm, chmap_to_install); + if (status < 0) { + SDL_SetError("ALSA: failed to install channel map: %s", ALSA_snd_strerror(status)); + return -1; + } + SDL_memcpy(ctx->alsa_chmap_installed, chmap, ctx->chans_n * sizeof (unsigned int)); + + SDL_small_free(chmap_to_install, isstack); + return CHMAP_INSTALLED; +} + +// We restrict the alsa channel maps because in the unordered matches we do only simple accounting. +// In the end, this will handle mostly alsa channel maps with more than one SND_CHMAP_NA position fillers. +static bool alsa_chmap_has_duplicate_position(const struct ALSA_pcm_cfg_ctx *ctx, const unsigned int *pos) +{ + if (ctx->chans_n < 2) {// we need at least 2 positions + LOGDEBUG("channel map:no duplicate"); + return false; + } + + for (unsigned int chan_idx = 1; chan_idx != ctx->chans_n; chan_idx++) { + for (unsigned int seen_idx = 0; seen_idx != chan_idx; seen_idx++) { + if (pos[seen_idx] == pos[chan_idx]) { + LOGDEBUG("channel map:have duplicate"); + return true; + } + } + } + + LOGDEBUG("channel map:no duplicate"); + return false; +} + +static int alsa_chmap_cfg_ordered_fixed_or_paired(struct ALSA_pcm_cfg_ctx *ctx) +{ + for (snd_pcm_chmap_query_t **chmap_query = ctx->chmap_queries; *chmap_query; chmap_query++) { + if ( ((*chmap_query)->map.channels != ctx->chans_n) || + (((*chmap_query)->type != SND_CHMAP_TYPE_FIXED) && ((*chmap_query)->type != SND_CHMAP_TYPE_PAIRED)) ) { + continue; + } + + #if SDL_ALSA_DEBUG + char logdebug_chmap_str[128]; + ALSA_snd_pcm_chmap_print(&(*chmap_query)->map,sizeof(logdebug_chmap_str),logdebug_chmap_str); + LOGDEBUG("channel map:ordered:fixed|paired:%s",logdebug_chmap_str); + #endif + + for (int i = 0; i < ctx->chans_n; i++) { + ctx->sdl_chmap[i] = (unsigned int) sdl_channel_maps[ctx->chans_n][i]; + } + + unsigned int *alsa_chmap = (*chmap_query)->map.pos; + if (ctx->chans_n == 6) { + sdl_6chans_set_rear_or_side_channels_from_alsa_6chans(ctx->sdl_chmap, alsa_chmap); + } + if (alsa_chmap_has_duplicate_position(ctx, alsa_chmap)) { + continue; + } + + for (unsigned int chan_idx = 0; ctx->sdl_chmap[chan_idx] == alsa_chmap[chan_idx]; chan_idx++) { + if (chan_idx == ctx->chans_n) { + return alsa_chmap_install(ctx, alsa_chmap); + } + } + } + return CHMAP_NOT_FOUND; +} + +// Here, the alsa channel positions can be programmed in the alsa frame (cf HDMI). +// If the alsa channel map is VAR, we only check we have the unordered set of channel positions we +// are looking for. +static int alsa_chmap_cfg_ordered_var(struct ALSA_pcm_cfg_ctx *ctx) +{ + for (snd_pcm_chmap_query_t **chmap_query = ctx->chmap_queries; *chmap_query; chmap_query++) { + if (((*chmap_query)->map.channels != ctx->chans_n) || ((*chmap_query)->type != SND_CHMAP_TYPE_VAR)) { + continue; + } + + #if SDL_ALSA_DEBUG + char logdebug_chmap_str[128]; + ALSA_snd_pcm_chmap_print(&(*chmap_query)->map,sizeof(logdebug_chmap_str),logdebug_chmap_str); + LOGDEBUG("channel map:ordered:var:%s",logdebug_chmap_str); + #endif + + for (int i = 0; i < ctx->chans_n; i++) { + ctx->sdl_chmap[i] = (unsigned int) sdl_channel_maps[ctx->chans_n][i]; + } + + unsigned int *alsa_chmap = (*chmap_query)->map.pos; + if (ctx->chans_n == 6) { + sdl_6chans_set_rear_or_side_channels_from_alsa_6chans(ctx->sdl_chmap, alsa_chmap); + } + if (alsa_chmap_has_duplicate_position(ctx, alsa_chmap)) { + continue; + } + + unsigned int pos_matches_n = 0; + for (unsigned int chan_idx = 0; chan_idx != ctx->chans_n; chan_idx++) { + for (unsigned int subscan_chan_idx = 0; subscan_chan_idx != ctx->chans_n; subscan_chan_idx++) { + if (ctx->sdl_chmap[chan_idx] == alsa_chmap[subscan_chan_idx]) { + pos_matches_n++; + break; + } + } + } + + if (pos_matches_n == ctx->chans_n) { + return alsa_chmap_install(ctx, ctx->sdl_chmap); // XXX: we program the SDL chmap here + } + } + + return CHMAP_NOT_FOUND; +} + +static int alsa_chmap_cfg_ordered(struct ALSA_pcm_cfg_ctx *ctx) +{ + const int status = alsa_chmap_cfg_ordered_fixed_or_paired(ctx); + return (status != CHMAP_NOT_FOUND) ? status : alsa_chmap_cfg_ordered_var(ctx); +} + +// In the unordered case, we are just interested to get the same unordered set of alsa channel +// positions than in the SDL channel map since we will swizzle (no duplicate channel position). +static int alsa_chmap_cfg_unordered(struct ALSA_pcm_cfg_ctx *ctx) +{ + for (snd_pcm_chmap_query_t **chmap_query = ctx->chmap_queries; *chmap_query; chmap_query++) { + if ( ((*chmap_query)->map.channels != ctx->chans_n) || + (((*chmap_query)->type != SND_CHMAP_TYPE_FIXED) && ((*chmap_query)->type != SND_CHMAP_TYPE_PAIRED)) ) { + continue; + } + + #if SDL_ALSA_DEBUG + char logdebug_chmap_str[128]; + ALSA_snd_pcm_chmap_print(&(*chmap_query)->map,sizeof(logdebug_chmap_str),logdebug_chmap_str); + LOGDEBUG("channel map:unordered:fixed|paired:%s",logdebug_chmap_str); + #endif + + for (int i = 0; i < ctx->chans_n; i++) { + ctx->sdl_chmap[i] = (unsigned int) sdl_channel_maps[ctx->chans_n][i]; + } + + unsigned int *alsa_chmap = (*chmap_query)->map.pos; + if (ctx->chans_n == 6) { + sdl_6chans_set_rear_or_side_channels_from_alsa_6chans(ctx->sdl_chmap, alsa_chmap); + } + + if (alsa_chmap_has_duplicate_position(ctx, alsa_chmap)) { + continue; + } + + unsigned int pos_matches_n = 0; + for (unsigned int chan_idx = 0; chan_idx != ctx->chans_n; chan_idx++) { + for (unsigned int subscan_chan_idx = 0; subscan_chan_idx != ctx->chans_n; subscan_chan_idx++) { + if (ctx->sdl_chmap[chan_idx] == alsa_chmap[subscan_chan_idx]) { + pos_matches_n++; + break; + } + } + } + + if (pos_matches_n == ctx->chans_n) { + return alsa_chmap_install(ctx, alsa_chmap); + } + } + + return CHMAP_NOT_FOUND; +} + +static int alsa_chmap_cfg(struct ALSA_pcm_cfg_ctx *ctx) +{ + int status; + + ctx->chmap_queries = ALSA_snd_pcm_query_chmaps(ctx->device->hidden->pcm); + if (ctx->chmap_queries == NULL) { + // We couldn't query the channel map, assume no swizzle necessary + LOGDEBUG("couldn't query channel map, swizzling off"); + return CHMAP_INSTALLED; + } + + //---------------------------------------------------------------------------------------------- + status = alsa_chmap_cfg_ordered(ctx); // we prefer first channel maps we don't need to swizzle + if (status == CHMAP_INSTALLED) { + LOGDEBUG("swizzling off"); + return status; + } else if (status != CHMAP_NOT_FOUND) { + return status; // < 0 error code + } + + // Fall-thru + //---------------------------------------------------------------------------------------------- + status = alsa_chmap_cfg_unordered(ctx); // those we will have to swizzle + if (status == CHMAP_INSTALLED) { + LOGDEBUG("swizzling on"); + + bool isstack; + int *swizzle_map = SDL_small_alloc(int, ctx->chans_n, &isstack); + if (!swizzle_map) { + status = -1; + } else { + bool needs_swizzle; + swizzle_map_compute(ctx, swizzle_map, &needs_swizzle); // fine grained swizzle configuration + if (needs_swizzle) { + // let SDL's swizzler handle this one. + ctx->device->chmap = SDL_ChannelMapDup(swizzle_map, ctx->chans_n); + if (!ctx->device->chmap) { + status = -1; + } + } + SDL_small_free(swizzle_map, isstack); + } + } + + if (status == CHMAP_NOT_FOUND) { + return CHANS_N_NEXT; + } + + return status; // < 0 error code +} + +#define CHANS_N_SCAN_MODE__EQUAL_OR_ABOVE_REQUESTED_CHANS_N 0 // target more hardware pressure +#define CHANS_N_SCAN_MODE__BELOW_REQUESTED_CHANS_N 1 // target less hardware pressure +#define CHANS_N_CONFIGURED 0 +#define CHANS_N_NOT_CONFIGURED 1 +static int ALSA_pcm_cfg_hw_chans_n_scan(struct ALSA_pcm_cfg_ctx *ctx, unsigned int mode) +{ + unsigned int target_chans_n = ctx->device->spec.channels; // we start at what was specified + if (mode == CHANS_N_SCAN_MODE__BELOW_REQUESTED_CHANS_N) { + target_chans_n--; + } + while (true) { + if (mode == CHANS_N_SCAN_MODE__EQUAL_OR_ABOVE_REQUESTED_CHANS_N) { + if (target_chans_n > SDL_AUDIO_ALSA__CHMAP_CHANS_N_MAX) { + return CHANS_N_NOT_CONFIGURED; + } + // else: CHANS_N_SCAN_MODE__BELOW_REQUESTED_CHANS_N + } else if (target_chans_n == 0) { + return CHANS_N_NOT_CONFIGURED; + } + + LOGDEBUG("target chans_n is %u", target_chans_n); + + int status = ALSA_snd_pcm_hw_params_any(ctx->device->hidden->pcm, ctx->hwparams); + if (status < 0) { + SDL_SetError("ALSA: Couldn't get hardware config: %s", ALSA_snd_strerror(status)); + return -1; + } + // SDL only uses interleaved sample output + status = ALSA_snd_pcm_hw_params_set_access(ctx->device->hidden->pcm, ctx->hwparams, SND_PCM_ACCESS_RW_INTERLEAVED); + if (status < 0) { + SDL_SetError("ALSA: Couldn't set interleaved access: %s", ALSA_snd_strerror(status)); + return -1; + } + // Try for a closest match on audio format + snd_pcm_format_t alsa_format = 0; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(ctx->device->spec.format); + ctx->matched_sdl_format = 0; + while ((ctx->matched_sdl_format = *(closefmts++)) != 0) { + // XXX: we are forcing the same endianness, namely we won't need byte swapping upon + // writing/reading to/from the SDL audio buffer. + switch (ctx->matched_sdl_format) { + case SDL_AUDIO_U8: + alsa_format = SND_PCM_FORMAT_U8; + break; + case SDL_AUDIO_S8: + alsa_format = SND_PCM_FORMAT_S8; + break; + case SDL_AUDIO_S16LE: + alsa_format = SND_PCM_FORMAT_S16_LE; + break; + case SDL_AUDIO_S16BE: + alsa_format = SND_PCM_FORMAT_S16_BE; + break; + case SDL_AUDIO_S32LE: + alsa_format = SND_PCM_FORMAT_S32_LE; + break; + case SDL_AUDIO_S32BE: + alsa_format = SND_PCM_FORMAT_S32_BE; + break; + case SDL_AUDIO_F32LE: + alsa_format = SND_PCM_FORMAT_FLOAT_LE; + break; + case SDL_AUDIO_F32BE: + alsa_format = SND_PCM_FORMAT_FLOAT_BE; + break; + default: + continue; + } + if (ALSA_snd_pcm_hw_params_set_format(ctx->device->hidden->pcm, ctx->hwparams, alsa_format) >= 0) { + break; + } + } + if (ctx->matched_sdl_format == 0) { + SDL_SetError("ALSA: Unsupported audio format: %s", ALSA_snd_strerror(status)); + return -1; + } + // let alsa approximate the number of channels + ctx->chans_n = target_chans_n; + status = ALSA_snd_pcm_hw_params_set_channels_near(ctx->device->hidden->pcm, ctx->hwparams, &(ctx->chans_n)); + if (status < 0) { + SDL_SetError("ALSA: Couldn't set audio channels: %s", ALSA_snd_strerror(status)); + return -1; + } + // let alsa approximate the audio rate + ctx->rate = ctx->device->spec.freq; + status = ALSA_snd_pcm_hw_params_set_rate_near(ctx->device->hidden->pcm, ctx->hwparams, &(ctx->rate), NULL); + if (status < 0) { + SDL_SetError("ALSA: Couldn't set audio frequency: %s", ALSA_snd_strerror(status)); + return -1; + } + // let approximate the period size to the requested buffer size + ctx->persize = ctx->device->sample_frames; + status = ALSA_snd_pcm_hw_params_set_period_size_near(ctx->device->hidden->pcm, ctx->hwparams, &(ctx->persize), NULL); + if (status < 0) { + SDL_SetError("ALSA: Couldn't set the period size: %s", ALSA_snd_strerror(status)); + return -1; + } + // let approximate the minimun number of periods per buffer (we target a double buffer) + ctx->periods = 2; + status = ALSA_snd_pcm_hw_params_set_periods_min(ctx->device->hidden->pcm, ctx->hwparams, &(ctx->periods), NULL); + if (status < 0) { + SDL_SetError("ALSA: Couldn't set the minimum number of periods per buffer: %s", ALSA_snd_strerror(status)); + return -1; + } + // restrict the number of periods per buffer to an approximation of the approximated minimum + // number of periods per buffer done right above + status = ALSA_snd_pcm_hw_params_set_periods_first(ctx->device->hidden->pcm, ctx->hwparams, &(ctx->periods), NULL); + if (status < 0) { + SDL_SetError("ALSA: Couldn't set the number of periods per buffer: %s", ALSA_snd_strerror(status)); + return -1; + } + // install the hw parameters + status = ALSA_snd_pcm_hw_params(ctx->device->hidden->pcm, ctx->hwparams); + if (status < 0) { + SDL_SetError("ALSA: installation of hardware parameter failed: %s", ALSA_snd_strerror(status)); + return -1; + } + //========================================================================================== + // Here the alsa pcm is in SND_PCM_STATE_PREPARED state, let's figure out a good fit for + // SDL channel map, it may request to change the target number of channels though. + status = alsa_chmap_cfg(ctx); + if (status < 0) { + return status; // we forward the SDL error + } else if (status == CHMAP_INSTALLED) { + return CHANS_N_CONFIGURED; // we are finished here + } + + // status == CHANS_N_NEXT + ALSA_snd_pcm_free_chmaps(ctx->chmap_queries); + ALSA_snd_pcm_hw_free(ctx->device->hidden->pcm); // uninstall those hw params + + if (mode == CHANS_N_SCAN_MODE__EQUAL_OR_ABOVE_REQUESTED_CHANS_N) { + target_chans_n++; + } else { // CHANS_N_SCAN_MODE__BELOW_REQUESTED_CHANS_N + target_chans_n--; + } + } + + SDL_assert(!"Shouldn't reach this code."); + return CHANS_N_NOT_CONFIGURED; +} +#undef CHMAP_INSTALLED +#undef CHANS_N_NEXT +#undef CHMAP_NOT_FOUND + +static bool ALSA_pcm_cfg_hw(struct ALSA_pcm_cfg_ctx *ctx) +{ + LOGDEBUG("target chans_n, equal or above requested chans_n mode"); + int status = ALSA_pcm_cfg_hw_chans_n_scan(ctx, CHANS_N_SCAN_MODE__EQUAL_OR_ABOVE_REQUESTED_CHANS_N); + if (status < 0) { // something went too wrong + return false; + } else if (status == CHANS_N_CONFIGURED) { + return true; + } + + // Here, status == CHANS_N_NOT_CONFIGURED + LOGDEBUG("target chans_n, below requested chans_n mode"); + status = ALSA_pcm_cfg_hw_chans_n_scan(ctx, CHANS_N_SCAN_MODE__BELOW_REQUESTED_CHANS_N); + if (status < 0) { // something went too wrong + return false; + } else if (status == CHANS_N_CONFIGURED) { + return true; + } + + // Here, status == CHANS_N_NOT_CONFIGURED + return SDL_SetError("ALSA: Coudn't configure targetting any SDL supported channel number"); +} +#undef CHANS_N_SCAN_MODE__EQUAL_OR_ABOVE_REQUESTED_CHANS_N +#undef CHANS_N_SCAN_MODE__BELOW_REQUESTED_CHANS_N +#undef CHANS_N_CONFIGURED +#undef CHANS_N_NOT_CONFIGURED + + +static bool ALSA_pcm_cfg_sw(struct ALSA_pcm_cfg_ctx *ctx) +{ + int status; + + status = ALSA_snd_pcm_sw_params_current(ctx->device->hidden->pcm, ctx->swparams); + if (status < 0) { + return SDL_SetError("ALSA: Couldn't get software config: %s", ALSA_snd_strerror(status)); + } + + status = ALSA_snd_pcm_sw_params_set_avail_min(ctx->device->hidden->pcm, ctx->swparams, ctx->persize); // will become device->sample_frames if the alsa pcm configuration is successful + if (status < 0) { + return SDL_SetError("Couldn't set minimum available samples: %s", ALSA_snd_strerror(status)); + } + + status = ALSA_snd_pcm_sw_params_set_start_threshold(ctx->device->hidden->pcm, ctx->swparams, 1); + if (status < 0) { + return SDL_SetError("ALSA: Couldn't set start threshold: %s", ALSA_snd_strerror(status)); + } + status = ALSA_snd_pcm_sw_params(ctx->device->hidden->pcm, ctx->swparams); + if (status < 0) { + return SDL_SetError("Couldn't set software audio parameters: %s", ALSA_snd_strerror(status)); + } + return true; +} + + +static bool ALSA_OpenDevice(SDL_AudioDevice *device) +{ + const bool recording = device->recording; + struct ALSA_pcm_cfg_ctx cfg_ctx; // used to track everything here + char *pcm_str; + int status = 0; + + //device->spec.channels = 8; + //SDL_SetLogPriority(SDL_LOG_CATEGORY_AUDIO, SDL_LOG_PRIORITY_VERBOSE); + LOGDEBUG("channels requested %u",device->spec.channels); + // XXX: We do not use the SDL internal swizzler yet. + device->chmap = NULL; + + SDL_zero(cfg_ctx); + cfg_ctx.device = device; + + // Initialize all variables that we clean on shutdown + cfg_ctx.device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*cfg_ctx.device->hidden)); + if (!cfg_ctx.device->hidden) { + return false; + } + + // Open the audio device + pcm_str = get_pcm_str(cfg_ctx.device->handle); + if (pcm_str == NULL) { + goto err_free_device_hidden; + } + LOGDEBUG("PCM open '%s'", pcm_str); + status = ALSA_snd_pcm_open(&cfg_ctx.device->hidden->pcm, + pcm_str, + recording ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK, + SND_PCM_NONBLOCK); + SDL_free(pcm_str); + if (status < 0) { + SDL_SetError("ALSA: Couldn't open audio device: %s", ALSA_snd_strerror(status)); + goto err_free_device_hidden; + } + + // Now we need to configure the opened pcm as close as possible from the requested parameters we + // can reasonably deal with (and that could change) + snd_pcm_hw_params_alloca(&(cfg_ctx.hwparams)); + snd_pcm_sw_params_alloca(&(cfg_ctx.swparams)); + + if (!ALSA_pcm_cfg_hw(&cfg_ctx)) { // alsa pcm "hardware" part of the pcm + goto err_close_pcm; + } + + // from here, we get only the alsa chmap queries in cfg_ctx to explicitely clean, hwparams is + // uninstalled upon pcm closing + + // This is useful for debugging + #if SDL_ALSA_DEBUG + snd_pcm_uframes_t bufsize; + ALSA_snd_pcm_hw_params_get_buffer_size(cfg_ctx.hwparams, &bufsize); + SDL_LogError(SDL_LOG_CATEGORY_AUDIO, + "ALSA: period size = %ld, periods = %u, buffer size = %lu", + cfg_ctx.persize, cfg_ctx.periods, bufsize); + #endif + + if (!ALSA_pcm_cfg_sw(&cfg_ctx)) { // alsa pcm "software" part of the pcm + goto err_cleanup_ctx; + } + + // Now we can update the following parameters in the spec: + cfg_ctx.device->spec.format = cfg_ctx.matched_sdl_format; + cfg_ctx.device->spec.channels = cfg_ctx.chans_n; + cfg_ctx.device->spec.freq = cfg_ctx.rate; + cfg_ctx.device->sample_frames = cfg_ctx.persize; + // Calculate the final parameters for this audio specification + SDL_UpdatedAudioDeviceFormat(cfg_ctx.device); + + // Allocate mixing buffer + if (!recording) { + cfg_ctx.device->hidden->mixbuf = (Uint8 *)SDL_malloc(cfg_ctx.device->buffer_size); + if (cfg_ctx.device->hidden->mixbuf == NULL) { + goto err_cleanup_ctx; + } + SDL_memset(cfg_ctx.device->hidden->mixbuf, cfg_ctx.device->silence_value, cfg_ctx.device->buffer_size); + } + +#if !SDL_ALSA_NON_BLOCKING + if (!recording) { + ALSA_snd_pcm_nonblock(cfg_ctx.device->hidden->pcm, 0); + } +#endif + ALSA_snd_pcm_start(cfg_ctx.device->hidden->pcm); + return true; // We're ready to rock and roll. :-) + +err_cleanup_ctx: + ALSA_snd_pcm_free_chmaps(cfg_ctx.chmap_queries); +err_close_pcm: + ALSA_snd_pcm_close(cfg_ctx.device->hidden->pcm); +err_free_device_hidden: + SDL_free(cfg_ctx.device->hidden); + cfg_ctx.device->hidden = NULL; + return false; +} + +static ALSA_Device *hotplug_devices = NULL; + +static int hotplug_device_process(snd_ctl_t *ctl, snd_ctl_card_info_t *ctl_card_info, int dev_idx, + snd_pcm_stream_t direction, ALSA_Device **unseen, ALSA_Device **seen) +{ + unsigned int subdevs_n = 1; // we have at least one subdevice (substream since the direction is a stream in alsa terminology) + unsigned int subdev_idx = 0; + const bool recording = direction == SND_PCM_STREAM_CAPTURE ? true : false; // used for the unicity of the device + bool isstack; + snd_pcm_info_t *pcm_info = (snd_pcm_info_t*)SDL_small_alloc(Uint8, ALSA_snd_pcm_info_sizeof(), &isstack); + SDL_memset(pcm_info, 0, ALSA_snd_pcm_info_sizeof()); + + while (true) { + ALSA_snd_pcm_info_set_stream(pcm_info, direction); + ALSA_snd_pcm_info_set_device(pcm_info, dev_idx); + ALSA_snd_pcm_info_set_subdevice(pcm_info, subdev_idx); // we have at least one subdevice (substream) of index 0 + + const int r = ALSA_snd_ctl_pcm_info(ctl, pcm_info); + if (r < 0) { + SDL_small_free(pcm_info, isstack); + // first call to ALSA_snd_ctl_pcm_info + if (subdev_idx == 0 && r == -ENOENT) { // no such direction/stream for this device + return 0; + } + return -1; + } + + if (subdev_idx == 0) { + subdevs_n = ALSA_snd_pcm_info_get_subdevices_count(pcm_info); + } + + // building the unseen list scanning the list of hotplug devices, if it is already there + // using the id, move it to the seen list. + ALSA_Device *unseen_prev_adev = NULL; + ALSA_Device *adev; + for (adev = *unseen; adev; adev = adev->next) { + // the unicity key is the couple (id,recording) + if ((SDL_strcmp(adev->id, ALSA_snd_ctl_card_info_get_id(ctl_card_info)) == 0) && (adev->recording == recording)) { + // unchain from unseen + if (*unseen == adev) { // head + *unseen = adev->next; + } else { + unseen_prev_adev->next = adev->next; + } + // chain to seen + adev->next = *seen; + *seen = adev; + break; + } + unseen_prev_adev = adev; + } + + if (adev == NULL) { // newly seen device + adev = SDL_calloc(1, sizeof(*adev)); + if (adev == NULL) { + SDL_small_free(pcm_info, isstack); + return -1; + } + + adev->id = SDL_strdup(ALSA_snd_ctl_card_info_get_id(ctl_card_info)); + if (adev->id == NULL) { + SDL_small_free(pcm_info, isstack); + SDL_free(adev); + return -1; + } + + if (SDL_asprintf(&adev->name, "%s:%s", ALSA_snd_ctl_card_info_get_name(ctl_card_info), ALSA_snd_pcm_info_get_name(pcm_info)) == -1) { + SDL_small_free(pcm_info, isstack); + SDL_free(adev->id); + SDL_free(adev); + return -1; + } + + if (direction == SND_PCM_STREAM_CAPTURE) { + adev->recording = true; + } else { + adev->recording = false; + } + + if (SDL_AddAudioDevice(recording, adev->name, NULL, adev) == NULL) { + SDL_small_free(pcm_info, isstack); + SDL_free(adev->id); + SDL_free(adev->name); + SDL_free(adev); + return -1; + } + + adev->next = *seen; + *seen = adev; + } + + subdev_idx++; + if (subdev_idx == subdevs_n) { + SDL_small_free(pcm_info, isstack); + return 0; + } + + SDL_memset(pcm_info, 0, ALSA_snd_pcm_info_sizeof()); + } + + SDL_small_free(pcm_info, isstack); + SDL_assert(!"Shouldn't reach this code"); + return -1; +} + +static void ALSA_HotplugIteration(bool *has_default_output, bool *has_default_recording) +{ + if (has_default_output != NULL) { + *has_default_output = true; + } + + if (has_default_recording != NULL) { + *has_default_recording = true; + } + + bool isstack; + snd_ctl_card_info_t *ctl_card_info = (snd_ctl_card_info_t *) SDL_small_alloc(Uint8, ALSA_snd_ctl_card_info_sizeof(), &isstack); + if (!ctl_card_info) { + return; // oh well. + } + + SDL_memset(ctl_card_info, 0, ALSA_snd_ctl_card_info_sizeof()); + + snd_ctl_t *ctl = NULL; + ALSA_Device *unseen = hotplug_devices; + ALSA_Device *seen = NULL; + int card_idx = -1; + while (true) { + int r = ALSA_snd_card_next(&card_idx); + if (r < 0) { + goto failed; + } else if (card_idx == -1) { + break; + } + + char ctl_name[64]; + SDL_snprintf(ctl_name, sizeof (ctl_name), "%s%d", ALSA_device_prefix, card_idx); // card_idx >= 0 + LOGDEBUG("hotplug ctl_name = '%s'", ctl_name); + + r = ALSA_snd_ctl_open(&ctl, ctl_name, 0); + if (r < 0) { + continue; + } + + r = ALSA_snd_ctl_card_info(ctl, ctl_card_info); + if (r < 0) { + goto failed; + } + + int dev_idx = -1; + while (true) { + r = ALSA_snd_ctl_pcm_next_device(ctl, &dev_idx); + if (r < 0) { + goto failed; + } else if (dev_idx == -1) { + break; + } + + r = hotplug_device_process(ctl, ctl_card_info, dev_idx, SND_PCM_STREAM_PLAYBACK, &unseen, &seen); + if (r < 0) { + goto failed; + } + + r = hotplug_device_process(ctl, ctl_card_info, dev_idx, SND_PCM_STREAM_CAPTURE, &unseen, &seen); + if (r < 0) { + goto failed; + } + } + ALSA_snd_ctl_close(ctl); + ALSA_snd_ctl_card_info_clear(ctl_card_info); + } + + // remove only the unseen devices + while (unseen) { + SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle(unseen)); + SDL_free(unseen->name); + SDL_free(unseen->id); + ALSA_Device *next = unseen->next; + SDL_free(unseen); + unseen = next; + } + + // update hotplug devices to be the seen devices + hotplug_devices = seen; + SDL_small_free(ctl_card_info, isstack); + return; + +failed: + if (ctl) { + ALSA_snd_ctl_close(ctl); + } + + // remove the unseen + while (unseen) { + SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle(unseen)); + SDL_free(unseen->name); + SDL_free(unseen->id); + ALSA_Device *next = unseen->next; + SDL_free(unseen); + unseen = next; + } + + // remove the seen + while (seen) { + SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle(seen)); + SDL_free(seen->name); + SDL_free(seen->id); + ALSA_Device *next = seen->next; + SDL_free(seen); + seen = next; + } + + hotplug_devices = NULL; + SDL_small_free(ctl_card_info, isstack); +} + + +#if SDL_ALSA_HOTPLUG_THREAD +static SDL_AtomicInt ALSA_hotplug_shutdown; +static SDL_Thread *ALSA_hotplug_thread; + +static int SDLCALL ALSA_HotplugThread(void *arg) +{ + SDL_SetCurrentThreadPriority(SDL_THREAD_PRIORITY_LOW); + + while (!SDL_GetAtomicInt(&ALSA_hotplug_shutdown)) { + // Block awhile before checking again, unless we're told to stop. + const Uint64 ticks = SDL_GetTicks() + 5000; + while (!SDL_GetAtomicInt(&ALSA_hotplug_shutdown) && (SDL_GetTicks() < ticks)) { + SDL_Delay(100); + } + + ALSA_HotplugIteration(NULL, NULL); // run the check. + } + + return 0; +} +#endif + +static void ALSA_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + ALSA_guess_device_prefix(); + + // ALSA doesn't have a concept of a changeable default device, afaik, so we expose a generic default + // device here. It's the best we can do at this level. + bool has_default_playback = false, has_default_recording = false; + ALSA_HotplugIteration(&has_default_playback, &has_default_recording); // run once now before a thread continues to check. + if (has_default_playback) { + *default_playback = SDL_AddAudioDevice(/*recording=*/false, "ALSA default playback device", NULL, (void*)&default_playback_handle); + } + if (has_default_recording) { + *default_recording = SDL_AddAudioDevice(/*recording=*/true, "ALSA default recording device", NULL, (void*)&default_recording_handle); + } + +#if SDL_ALSA_HOTPLUG_THREAD + SDL_SetAtomicInt(&ALSA_hotplug_shutdown, 0); + ALSA_hotplug_thread = SDL_CreateThread(ALSA_HotplugThread, "SDLHotplugALSA", NULL); + // if the thread doesn't spin, oh well, you just don't get further hotplug events. +#endif +} + +static void ALSA_DeinitializeStart(void) +{ + ALSA_Device *dev; + ALSA_Device *next; + +#if SDL_ALSA_HOTPLUG_THREAD + if (ALSA_hotplug_thread) { + SDL_SetAtomicInt(&ALSA_hotplug_shutdown, 1); + SDL_WaitThread(ALSA_hotplug_thread, NULL); + ALSA_hotplug_thread = NULL; + } +#endif + + // Shutting down! Clean up any data we've gathered. + for (dev = hotplug_devices; dev; dev = next) { + //SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "ALSA: at shutdown, removing %s device '%s'", dev->recording ? "recording" : "playback", dev->name); + next = dev->next; + SDL_free(dev->name); + SDL_free(dev); + } + hotplug_devices = NULL; +} + +static void ALSA_Deinitialize(void) +{ + UnloadALSALibrary(); +} + +static bool ALSA_Init(SDL_AudioDriverImpl *impl) +{ + if (!LoadALSALibrary()) { + return false; + } + + impl->DetectDevices = ALSA_DetectDevices; + impl->OpenDevice = ALSA_OpenDevice; + impl->WaitDevice = ALSA_WaitDevice; + impl->GetDeviceBuf = ALSA_GetDeviceBuf; + impl->PlayDevice = ALSA_PlayDevice; + impl->CloseDevice = ALSA_CloseDevice; + impl->DeinitializeStart = ALSA_DeinitializeStart; + impl->Deinitialize = ALSA_Deinitialize; + impl->WaitRecordingDevice = ALSA_WaitDevice; + impl->RecordDevice = ALSA_RecordDevice; + impl->FlushRecording = ALSA_FlushRecording; + + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap ALSA_bootstrap = { + "alsa", "ALSA PCM audio", ALSA_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_ALSA diff --git a/contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.h b/contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.h new file mode 100644 index 0000000..c68dda8 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/alsa/SDL_alsa_audio.h @@ -0,0 +1,41 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_ALSA_audio_h_ +#define SDL_ALSA_audio_h_ + +#include + +#include "../SDL_sysaudio.h" + +#define SDL_AUDIO_ALSA__CHMAP_CHANS_N_MAX 8 +#define SDL_AUDIO_ALSA__SDL_CHMAPS_N 9 // from 0 channels to 8 channels +struct SDL_PrivateAudioData +{ + // The audio device handle + snd_pcm_t *pcm; + + // Raw mixing buffer + Uint8 *mixbuf; +}; + +#endif // SDL_ALSA_audio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.h b/contrib/SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.h new file mode 100644 index 0000000..f117c09 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.h @@ -0,0 +1,68 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_coreaudio_h_ +#define SDL_coreaudio_h_ + +#include "../SDL_sysaudio.h" + +#ifndef SDL_PLATFORM_IOS +#define MACOSX_COREAUDIO +#endif + +#ifdef MACOSX_COREAUDIO +#include +#else +#import +#import +#endif + +#include +#include + +// Things named "Master" were renamed to "Main" in macOS 12.0's SDK. +#ifdef MACOSX_COREAUDIO +#include +#ifndef MAC_OS_VERSION_12_0 +#define kAudioObjectPropertyElementMain kAudioObjectPropertyElementMaster +#endif +#endif + +struct SDL_PrivateAudioData +{ + SDL_Thread *thread; + AudioQueueRef audioQueue; + int numAudioBuffers; + AudioQueueBufferRef *audioBuffer; + AudioQueueBufferRef current_buffer; + AudioStreamBasicDescription strdesc; + SDL_Semaphore *ready_semaphore; + char *thread_error; +#ifdef MACOSX_COREAUDIO + AudioDeviceID deviceID; +#else + bool interrupted; + CFTypeRef interruption_listener; +#endif +}; + +#endif // SDL_coreaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.m b/contrib/SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.m new file mode 100644 index 0000000..57b19c7 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/coreaudio/SDL_coreaudio.m @@ -0,0 +1,1040 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_COREAUDIO + +#include "../SDL_sysaudio.h" +#include "SDL_coreaudio.h" +#include "../../thread/SDL_systhread.h" + +#define DEBUG_COREAUDIO 0 + +#if DEBUG_COREAUDIO + #define CHECK_RESULT(msg) \ + if (result != noErr) { \ + SDL_Log("COREAUDIO: Got error %d from '%s'!", (int)result, msg); \ + return SDL_SetError("CoreAudio error (%s): %d", msg, (int)result); \ + } +#else + #define CHECK_RESULT(msg) \ + if (result != noErr) { \ + return SDL_SetError("CoreAudio error (%s): %d", msg, (int)result); \ + } +#endif + +#ifdef MACOSX_COREAUDIO +// Apparently AudioDeviceID values might not be unique, so we wrap it in an SDL_malloc()'d pointer +// to make it so. Use FindCoreAudioDeviceByHandle to deal with this redirection, if you need to +// map from an AudioDeviceID to a SDL handle. +typedef struct SDLCoreAudioHandle +{ + AudioDeviceID devid; + bool recording; +} SDLCoreAudioHandle; + +static bool TestCoreAudioDeviceHandleCallback(SDL_AudioDevice *device, void *handle) +{ + const SDLCoreAudioHandle *a = (const SDLCoreAudioHandle *) device->handle; + const SDLCoreAudioHandle *b = (const SDLCoreAudioHandle *) handle; + return (a->devid == b->devid) && (!!a->recording == !!b->recording); +} + +static SDL_AudioDevice *FindCoreAudioDeviceByHandle(const AudioDeviceID devid, const bool recording) +{ + SDLCoreAudioHandle handle = { devid, recording }; + return SDL_FindPhysicalAudioDeviceByCallback(TestCoreAudioDeviceHandleCallback, &handle); +} + +static const AudioObjectPropertyAddress devlist_address = { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain +}; + +static const AudioObjectPropertyAddress default_playback_device_address = { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain +}; + +static const AudioObjectPropertyAddress default_recording_device_address = { + kAudioHardwarePropertyDefaultInputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain +}; + +static const AudioObjectPropertyAddress alive_address = { + kAudioDevicePropertyDeviceIsAlive, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain +}; + + +static OSStatus DeviceAliveNotification(AudioObjectID devid, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *)data; + SDL_assert(((const SDLCoreAudioHandle *) device->handle)->devid == devid); + + UInt32 alive = 1; + UInt32 size = sizeof(alive); + const OSStatus error = AudioObjectGetPropertyData(devid, addrs, 0, NULL, &size, &alive); + + bool dead = false; + if (error == kAudioHardwareBadDeviceError) { + dead = true; // device was unplugged. + } else if ((error == kAudioHardwareNoError) && (!alive)) { + dead = true; // device died in some other way. + } + + if (dead) { + #if DEBUG_COREAUDIO + SDL_Log("COREAUDIO: device '%s' is lost!", device->name); + #endif + SDL_AudioDeviceDisconnected(device); + } + + return noErr; +} + +static void COREAUDIO_FreeDeviceHandle(SDL_AudioDevice *device) +{ + SDLCoreAudioHandle *handle = (SDLCoreAudioHandle *) device->handle; + AudioObjectRemovePropertyListener(handle->devid, &alive_address, DeviceAliveNotification, device); + SDL_free(handle); +} + +// This only _adds_ new devices. Removal is handled by devices triggering kAudioDevicePropertyDeviceIsAlive property changes. +static void RefreshPhysicalDevices(void) +{ + UInt32 size = 0; + AudioDeviceID *devs = NULL; + bool isstack; + + if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &devlist_address, 0, NULL, &size) != kAudioHardwareNoError) { + return; + } else if ((devs = (AudioDeviceID *) SDL_small_alloc(Uint8, size, &isstack)) == NULL) { + return; + } else if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &devlist_address, 0, NULL, &size, devs) != kAudioHardwareNoError) { + SDL_small_free(devs, isstack); + return; + } + + const UInt32 total_devices = (UInt32) (size / sizeof(AudioDeviceID)); + for (UInt32 i = 0; i < total_devices; i++) { + if (FindCoreAudioDeviceByHandle(devs[i], true) || FindCoreAudioDeviceByHandle(devs[i], false)) { + devs[i] = 0; // The system and SDL both agree it's already here, don't check it again. + } + } + + // any non-zero items remaining in `devs` are new devices to be added. + for (int recording = 0; recording < 2; recording++) { + const AudioObjectPropertyAddress addr = { + kAudioDevicePropertyStreamConfiguration, + recording ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + const AudioObjectPropertyAddress nameaddr = { + kAudioObjectPropertyName, + recording ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + const AudioObjectPropertyAddress freqaddr = { + kAudioDevicePropertyNominalSampleRate, + recording ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + + for (UInt32 i = 0; i < total_devices; i++) { + const AudioDeviceID dev = devs[i]; + if (!dev) { + continue; // already added. + } + + AudioBufferList *buflist = NULL; + double sampleRate = 0; + + if (AudioObjectGetPropertyDataSize(dev, &addr, 0, NULL, &size) != noErr) { + continue; + } else if ((buflist = (AudioBufferList *)SDL_malloc(size)) == NULL) { + continue; + } + + OSStatus result = AudioObjectGetPropertyData(dev, &addr, 0, NULL, &size, buflist); + + SDL_AudioSpec spec; + SDL_zero(spec); + if (result == noErr) { + for (Uint32 j = 0; j < buflist->mNumberBuffers; j++) { + spec.channels += buflist->mBuffers[j].mNumberChannels; + } + } + + SDL_free(buflist); + + if (spec.channels == 0) { + continue; + } + + size = sizeof(sampleRate); + if (AudioObjectGetPropertyData(dev, &freqaddr, 0, NULL, &size, &sampleRate) == noErr) { + spec.freq = (int)sampleRate; + } + + CFStringRef cfstr = NULL; + size = sizeof(CFStringRef); + if (AudioObjectGetPropertyData(dev, &nameaddr, 0, NULL, &size, &cfstr) != kAudioHardwareNoError) { + continue; + } + + CFIndex len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfstr), kCFStringEncodingUTF8); + char *name = (char *)SDL_malloc(len + 1); + bool usable = ((name != NULL) && (CFStringGetCString(cfstr, name, len + 1, kCFStringEncodingUTF8))); + + CFRelease(cfstr); + + if (usable) { + // Some devices have whitespace at the end...trim it. + len = (CFIndex) SDL_strlen(name); + while ((len > 0) && (name[len - 1] == ' ')) { + len--; + } + usable = (len > 0); + } + + if (usable) { + name[len] = '\0'; + + #if DEBUG_COREAUDIO + SDL_Log("COREAUDIO: Found %s device #%d: '%s' (devid %d)", ((recording) ? "recording" : "playback"), (int)i, name, (int)dev); + #endif + SDLCoreAudioHandle *newhandle = (SDLCoreAudioHandle *) SDL_calloc(1, sizeof (*newhandle)); + if (newhandle) { + newhandle->devid = dev; + newhandle->recording = recording ? true : false; + SDL_AudioDevice *device = SDL_AddAudioDevice(newhandle->recording, name, &spec, newhandle); + if (device) { + AudioObjectAddPropertyListener(dev, &alive_address, DeviceAliveNotification, device); + } else { + SDL_free(newhandle); + } + } + } + SDL_free(name); // SDL_AddAudioDevice() would have copied the string. + } + } + + SDL_small_free(devs, isstack); +} + +// this is called when the system's list of available audio devices changes. +static OSStatus DeviceListChangedNotification(AudioObjectID systemObj, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data) +{ + RefreshPhysicalDevices(); + return noErr; +} + +static OSStatus DefaultAudioDeviceChangedNotification(const bool recording, AudioObjectID inObjectID, const AudioObjectPropertyAddress *addr) +{ + AudioDeviceID devid; + UInt32 size = sizeof(devid); + if (AudioObjectGetPropertyData(inObjectID, addr, 0, NULL, &size, &devid) == noErr) { + SDL_DefaultAudioDeviceChanged(FindCoreAudioDeviceByHandle(devid, recording)); + } + return noErr; +} + +static OSStatus DefaultPlaybackDeviceChangedNotification(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void *inUserData) +{ + #if DEBUG_COREAUDIO + SDL_Log("COREAUDIO: default playback device changed!"); + #endif + SDL_assert(inNumberAddresses == 1); + return DefaultAudioDeviceChangedNotification(false, inObjectID, inAddresses); +} + +static OSStatus DefaultRecordingDeviceChangedNotification(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void *inUserData) +{ + #if DEBUG_COREAUDIO + SDL_Log("COREAUDIO: default recording device changed!"); + #endif + SDL_assert(inNumberAddresses == 1); + return DefaultAudioDeviceChangedNotification(true, inObjectID, inAddresses); +} + +static void COREAUDIO_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + RefreshPhysicalDevices(); + + AudioObjectAddPropertyListener(kAudioObjectSystemObject, &devlist_address, DeviceListChangedNotification, NULL); + + // Get the Device ID + UInt32 size; + AudioDeviceID devid; + + size = sizeof(AudioDeviceID); + if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &default_playback_device_address, 0, NULL, &size, &devid) == noErr) { + SDL_AudioDevice *device = FindCoreAudioDeviceByHandle(devid, false); + if (device) { + *default_playback = device; + } + } + AudioObjectAddPropertyListener(kAudioObjectSystemObject, &default_playback_device_address, DefaultPlaybackDeviceChangedNotification, NULL); + + size = sizeof(AudioDeviceID); + if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &default_recording_device_address, 0, NULL, &size, &devid) == noErr) { + SDL_AudioDevice *device = FindCoreAudioDeviceByHandle(devid, true); + if (device) { + *default_recording = device; + } + } + AudioObjectAddPropertyListener(kAudioObjectSystemObject, &default_recording_device_address, DefaultRecordingDeviceChangedNotification, NULL); +} + +#else // iOS-specific section follows. + +static bool session_active = false; + +static bool PauseOneAudioDevice(SDL_AudioDevice *device, void *userdata) +{ + if (device->hidden && device->hidden->audioQueue && !device->hidden->interrupted) { + AudioQueuePause(device->hidden->audioQueue); + } + return false; // keep enumerating devices until we've paused them all. +} + +static void PauseAudioDevices(void) +{ + (void) SDL_FindPhysicalAudioDeviceByCallback(PauseOneAudioDevice, NULL); +} + +static bool ResumeOneAudioDevice(SDL_AudioDevice *device, void *userdata) +{ + if (device->hidden && device->hidden->audioQueue && !device->hidden->interrupted) { + AudioQueueStart(device->hidden->audioQueue, NULL); + } + return false; // keep enumerating devices until we've resumed them all. +} + +static void ResumeAudioDevices(void) +{ + (void) SDL_FindPhysicalAudioDeviceByCallback(ResumeOneAudioDevice, NULL); +} + +static void InterruptionBegin(SDL_AudioDevice *device) +{ + if (device != NULL && device->hidden->audioQueue != NULL) { + device->hidden->interrupted = true; + AudioQueuePause(device->hidden->audioQueue); + } +} + +static void InterruptionEnd(SDL_AudioDevice *device) +{ + if (device != NULL && device->hidden != NULL && device->hidden->audioQueue != NULL && device->hidden->interrupted && AudioQueueStart(device->hidden->audioQueue, NULL) == AVAudioSessionErrorCodeNone) { + device->hidden->interrupted = false; + } +} + +@interface SDLInterruptionListener : NSObject + +@property(nonatomic, assign) SDL_AudioDevice *device; + +@end + +@implementation SDLInterruptionListener + +- (void)audioSessionInterruption:(NSNotification *)note +{ + @synchronized(self) { + NSNumber *type = note.userInfo[AVAudioSessionInterruptionTypeKey]; + if (type.unsignedIntegerValue == AVAudioSessionInterruptionTypeBegan) { + InterruptionBegin(self.device); + } else { + InterruptionEnd(self.device); + } + } +} + +- (void)applicationBecameActive:(NSNotification *)note +{ + @synchronized(self) { + InterruptionEnd(self.device); + } +} + +@end + +typedef struct +{ + int playback; + int recording; +} CountOpenAudioDevicesData; + +static bool CountOpenAudioDevices(SDL_AudioDevice *device, void *userdata) +{ + CountOpenAudioDevicesData *data = (CountOpenAudioDevicesData *) userdata; + if (device->hidden != NULL) { // assume it's open if hidden != NULL + if (device->recording) { + data->recording++; + } else { + data->playback++; + } + } + return false; // keep enumerating until all devices have been checked. +} + +static bool UpdateAudioSession(SDL_AudioDevice *device, bool open, bool allow_playandrecord) +{ + @autoreleasepool { + AVAudioSession *session = [AVAudioSession sharedInstance]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + NSString *category = AVAudioSessionCategoryPlayback; + NSString *mode = AVAudioSessionModeDefault; + NSUInteger options = AVAudioSessionCategoryOptionMixWithOthers; + NSError *err = nil; + const char *hint; + + CountOpenAudioDevicesData data; + SDL_zero(data); + (void) SDL_FindPhysicalAudioDeviceByCallback(CountOpenAudioDevices, &data); + + hint = SDL_GetHint(SDL_HINT_AUDIO_CATEGORY); + if (hint) { + if (SDL_strcasecmp(hint, "AVAudioSessionCategoryAmbient") == 0) { + category = AVAudioSessionCategoryAmbient; + } else if (SDL_strcasecmp(hint, "AVAudioSessionCategorySoloAmbient") == 0) { + category = AVAudioSessionCategorySoloAmbient; + options &= ~AVAudioSessionCategoryOptionMixWithOthers; + } else if (SDL_strcasecmp(hint, "AVAudioSessionCategoryPlayback") == 0 || + SDL_strcasecmp(hint, "playback") == 0) { + category = AVAudioSessionCategoryPlayback; + options &= ~AVAudioSessionCategoryOptionMixWithOthers; + } else if (SDL_strcasecmp(hint, "AVAudioSessionCategoryPlayAndRecord") == 0 || + SDL_strcasecmp(hint, "playandrecord") == 0) { + if (allow_playandrecord) { + category = AVAudioSessionCategoryPlayAndRecord; + } + } + } else if (data.playback && data.recording) { + if (allow_playandrecord) { + category = AVAudioSessionCategoryPlayAndRecord; + } else { + // We already failed play and record with AVAudioSessionErrorCodeResourceNotAvailable + return false; + } + } else if (data.recording) { + category = AVAudioSessionCategoryRecord; + } + + #ifndef SDL_PLATFORM_TVOS + if (category == AVAudioSessionCategoryPlayAndRecord) { + options |= AVAudioSessionCategoryOptionDefaultToSpeaker; + } + #endif + if (category == AVAudioSessionCategoryRecord || + category == AVAudioSessionCategoryPlayAndRecord) { + /* AVAudioSessionCategoryOptionAllowBluetooth isn't available in the SDK for + Apple TV but is still needed in order to output to Bluetooth devices. + */ + options |= 0x4; // AVAudioSessionCategoryOptionAllowBluetooth; + } + if (category == AVAudioSessionCategoryPlayAndRecord) { + options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP | + AVAudioSessionCategoryOptionAllowAirPlay; + } + if (category == AVAudioSessionCategoryPlayback || + category == AVAudioSessionCategoryPlayAndRecord) { + options |= AVAudioSessionCategoryOptionDuckOthers; + } + + if (![session.category isEqualToString:category] || session.categoryOptions != options) { + // Stop the current session so we don't interrupt other application audio + PauseAudioDevices(); + [session setActive:NO error:nil]; + session_active = false; + + if (![session setCategory:category mode:mode options:options error:&err]) { + NSString *desc = err.description; + SDL_SetError("Could not set Audio Session category: %s", desc.UTF8String); + return false; + } + } + + if ((data.playback || data.recording) && !session_active) { + if (![session setActive:YES error:&err]) { + if ([err code] == AVAudioSessionErrorCodeResourceNotAvailable && + category == AVAudioSessionCategoryPlayAndRecord) { + if (UpdateAudioSession(device, open, false)) { + return true; + } else { + return SDL_SetError("Could not activate Audio Session: Resource not available"); + } + } + + NSString *desc = err.description; + return SDL_SetError("Could not activate Audio Session: %s", desc.UTF8String); + } + session_active = true; + ResumeAudioDevices(); + } else if (!data.playback && !data.recording && session_active) { + PauseAudioDevices(); + [session setActive:NO error:nil]; + session_active = false; + } + + if (open) { + SDLInterruptionListener *listener = [SDLInterruptionListener new]; + listener.device = device; + + [center addObserver:listener + selector:@selector(audioSessionInterruption:) + name:AVAudioSessionInterruptionNotification + object:session]; + + /* An interruption end notification is not guaranteed to be sent if + we were previously interrupted... resuming if needed when the app + becomes active seems to be the way to go. */ + // Note: object: below needs to be nil, as otherwise it filters by the object, and session doesn't send foreground / active notifications. + [center addObserver:listener + selector:@selector(applicationBecameActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + + [center addObserver:listener + selector:@selector(applicationBecameActive:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + + device->hidden->interruption_listener = CFBridgingRetain(listener); + } else { + SDLInterruptionListener *listener = nil; + listener = (SDLInterruptionListener *)CFBridgingRelease(device->hidden->interruption_listener); + [center removeObserver:listener]; + @synchronized(listener) { + listener.device = NULL; + } + } + } + + return true; +} +#endif + + +static bool COREAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size) +{ + AudioQueueBufferRef current_buffer = device->hidden->current_buffer; + SDL_assert(current_buffer != NULL); // should have been called from PlaybackBufferReadyCallback + SDL_assert(buffer == (Uint8 *) current_buffer->mAudioData); + current_buffer->mAudioDataByteSize = current_buffer->mAudioDataBytesCapacity; + device->hidden->current_buffer = NULL; + AudioQueueEnqueueBuffer(device->hidden->audioQueue, current_buffer, 0, NULL); + return true; +} + +static Uint8 *COREAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + AudioQueueBufferRef current_buffer = device->hidden->current_buffer; + SDL_assert(current_buffer != NULL); // should have been called from PlaybackBufferReadyCallback + SDL_assert(current_buffer->mAudioData != NULL); + *buffer_size = (int) current_buffer->mAudioDataBytesCapacity; + return (Uint8 *) current_buffer->mAudioData; +} + +static void PlaybackBufferReadyCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *)inUserData; + SDL_assert(inBuffer != NULL); // ...right? + SDL_assert(device->hidden->current_buffer == NULL); // shouldn't have anything pending + device->hidden->current_buffer = inBuffer; + const bool okay = SDL_PlaybackAudioThreadIterate(device); + SDL_assert((device->hidden->current_buffer == NULL) || !okay); // PlayDevice should have enqueued and cleaned it out, unless we failed or shutdown. + + // buffer is unexpectedly here? We're probably dying, but try to requeue this buffer with silence. + if (device->hidden->current_buffer) { + AudioQueueBufferRef current_buffer = device->hidden->current_buffer; + device->hidden->current_buffer = NULL; + SDL_memset(current_buffer->mAudioData, device->silence_value, (size_t) current_buffer->mAudioDataBytesCapacity); + AudioQueueEnqueueBuffer(device->hidden->audioQueue, current_buffer, 0, NULL); + } +} + +static int COREAUDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + AudioQueueBufferRef current_buffer = device->hidden->current_buffer; + SDL_assert(current_buffer != NULL); // should have been called from RecordingBufferReadyCallback + SDL_assert(current_buffer->mAudioData != NULL); + SDL_assert(buflen >= (int) current_buffer->mAudioDataByteSize); // `cpy` makes sure this won't overflow a buffer, but we _will_ drop samples if this assertion fails! + const int cpy = SDL_min(buflen, (int) current_buffer->mAudioDataByteSize); + SDL_memcpy(buffer, current_buffer->mAudioData, cpy); + device->hidden->current_buffer = NULL; + AudioQueueEnqueueBuffer(device->hidden->audioQueue, current_buffer, 0, NULL); // requeue for capturing more data later. + return cpy; +} + +static void COREAUDIO_FlushRecording(SDL_AudioDevice *device) +{ + AudioQueueBufferRef current_buffer = device->hidden->current_buffer; + if (current_buffer != NULL) { // also gets called at shutdown, when no buffer is available. + // just requeue the current buffer without reading from it, so it can be refilled with new data later. + device->hidden->current_buffer = NULL; + AudioQueueEnqueueBuffer(device->hidden->audioQueue, current_buffer, 0, NULL); + } +} + +static void RecordingBufferReadyCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, + const AudioTimeStamp *inStartTime, UInt32 inNumberPacketDescriptions, + const AudioStreamPacketDescription *inPacketDescs) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *)inUserData; + SDL_assert(inAQ == device->hidden->audioQueue); + SDL_assert(inBuffer != NULL); // ...right? + SDL_assert(device->hidden->current_buffer == NULL); // shouldn't have anything pending + device->hidden->current_buffer = inBuffer; + SDL_RecordingAudioThreadIterate(device); + + // buffer is unexpectedly here? We're probably dying, but try to requeue this buffer anyhow. + if (device->hidden->current_buffer != NULL) { + SDL_assert(SDL_GetAtomicInt(&device->shutdown) != 0); + COREAUDIO_FlushRecording(device); // just flush it manually, which will requeue it. + } +} + +static void COREAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (!device->hidden) { + return; + } + + // dispose of the audio queue before waiting on the thread, or it might stall for a long time! + if (device->hidden->audioQueue) { + AudioQueueFlush(device->hidden->audioQueue); + AudioQueueStop(device->hidden->audioQueue, 0); + AudioQueueDispose(device->hidden->audioQueue, 0); + } + + if (device->hidden->thread) { + SDL_assert(SDL_GetAtomicInt(&device->shutdown) != 0); // should have been set by SDL_audio.c + SDL_WaitThread(device->hidden->thread, NULL); + } + + #ifndef MACOSX_COREAUDIO + UpdateAudioSession(device, false, true); + #endif + + if (device->hidden->ready_semaphore) { + SDL_DestroySemaphore(device->hidden->ready_semaphore); + } + + // AudioQueueDispose() frees the actual buffer objects. + SDL_free(device->hidden->audioBuffer); + SDL_free(device->hidden->thread_error); + SDL_free(device->hidden); +} + +#ifdef MACOSX_COREAUDIO +static bool PrepareDevice(SDL_AudioDevice *device) +{ + SDL_assert(device->handle != NULL); // this meant "system default" in SDL2, but doesn't anymore + + const SDLCoreAudioHandle *handle = (const SDLCoreAudioHandle *) device->handle; + const AudioDeviceID devid = handle->devid; + OSStatus result = noErr; + UInt32 size = 0; + + AudioObjectPropertyAddress addr = { + 0, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain + }; + + UInt32 alive = 0; + size = sizeof(alive); + addr.mSelector = kAudioDevicePropertyDeviceIsAlive; + addr.mScope = device->recording ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; + result = AudioObjectGetPropertyData(devid, &addr, 0, NULL, &size, &alive); + CHECK_RESULT("AudioDeviceGetProperty (kAudioDevicePropertyDeviceIsAlive)"); + if (!alive) { + return SDL_SetError("CoreAudio: requested device exists, but isn't alive."); + } + + // some devices don't support this property, so errors are fine here. + pid_t pid = 0; + size = sizeof(pid); + addr.mSelector = kAudioDevicePropertyHogMode; + result = AudioObjectGetPropertyData(devid, &addr, 0, NULL, &size, &pid); + if ((result == noErr) && (pid != -1)) { + return SDL_SetError("CoreAudio: requested device is being hogged."); + } + + device->hidden->deviceID = devid; + + return true; +} + +static bool AssignDeviceToAudioQueue(SDL_AudioDevice *device) +{ + const AudioObjectPropertyAddress prop = { + kAudioDevicePropertyDeviceUID, + device->recording ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + + OSStatus result; + CFStringRef devuid; + UInt32 devuidsize = sizeof(devuid); + result = AudioObjectGetPropertyData(device->hidden->deviceID, &prop, 0, NULL, &devuidsize, &devuid); + CHECK_RESULT("AudioObjectGetPropertyData (kAudioDevicePropertyDeviceUID)"); + result = AudioQueueSetProperty(device->hidden->audioQueue, kAudioQueueProperty_CurrentDevice, &devuid, devuidsize); + CFRelease(devuid); // Release devuid; we're done with it and AudioQueueSetProperty should have retained if it wants to keep it. + CHECK_RESULT("AudioQueueSetProperty (kAudioQueueProperty_CurrentDevice)"); + return true; +} +#endif + +static bool PrepareAudioQueue(SDL_AudioDevice *device) +{ + const AudioStreamBasicDescription *strdesc = &device->hidden->strdesc; + const bool recording = device->recording; + OSStatus result; + + SDL_assert(CFRunLoopGetCurrent() != NULL); + + if (recording) { + result = AudioQueueNewInput(strdesc, RecordingBufferReadyCallback, device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 0, &device->hidden->audioQueue); + CHECK_RESULT("AudioQueueNewInput"); + } else { + result = AudioQueueNewOutput(strdesc, PlaybackBufferReadyCallback, device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 0, &device->hidden->audioQueue); + CHECK_RESULT("AudioQueueNewOutput"); + } + + #ifdef MACOSX_COREAUDIO + if (!AssignDeviceToAudioQueue(device)) { + return false; + } + #endif + + SDL_UpdatedAudioDeviceFormat(device); // make sure this is correct. + + // Set the channel layout for the audio queue + AudioChannelLayout layout; + SDL_zero(layout); + switch (device->spec.channels) { + case 1: + // a standard mono stream + layout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono; + break; + case 2: + // a standard stereo stream (L R) - implied playback + layout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo; + break; + case 3: + // L R LFE + layout.mChannelLayoutTag = kAudioChannelLayoutTag_DVD_4; + break; + case 4: + // front left, front right, back left, back right + layout.mChannelLayoutTag = kAudioChannelLayoutTag_Quadraphonic; + break; + case 5: + // L R LFE Ls Rs + layout.mChannelLayoutTag = kAudioChannelLayoutTag_DVD_6; + break; + case 6: + // L R C LFE Ls Rs + layout.mChannelLayoutTag = kAudioChannelLayoutTag_DVD_12; + break; + case 7: + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + // L R C LFE Cs Ls Rs + layout.mChannelLayoutTag = kAudioChannelLayoutTag_WAVE_6_1; + } else { + // L R C LFE Ls Rs Cs + layout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_6_1_A; + + // Convert from SDL channel layout to kAudioChannelLayoutTag_MPEG_6_1_A + static const int swizzle_map[7] = { + 0, 1, 2, 3, 6, 4, 5 + }; + device->chmap = SDL_ChannelMapDup(swizzle_map, SDL_arraysize(swizzle_map)); + if (!device->chmap) { + return false; + } + } + break; + case 8: + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + // L R C LFE Rls Rrs Ls Rs + layout.mChannelLayoutTag = kAudioChannelLayoutTag_WAVE_7_1; + } else { + // L R C LFE Ls Rs Rls Rrs + layout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_7_1_C; + + // Convert from SDL channel layout to kAudioChannelLayoutTag_MPEG_7_1_C + static const int swizzle_map[8] = { + 0, 1, 2, 3, 6, 7, 4, 5 + }; + device->chmap = SDL_ChannelMapDup(swizzle_map, SDL_arraysize(swizzle_map)); + if (!device->chmap) { + return false; + } + } + break; + default: + return SDL_SetError("Unsupported audio channels"); + } + if (layout.mChannelLayoutTag != 0) { + result = AudioQueueSetProperty(device->hidden->audioQueue, kAudioQueueProperty_ChannelLayout, &layout, sizeof(layout)); + CHECK_RESULT("AudioQueueSetProperty(kAudioQueueProperty_ChannelLayout)"); + } + + // Make sure we can feed the device a minimum amount of time + double MINIMUM_AUDIO_BUFFER_TIME_MS = 15.0; + #ifdef SDL_PLATFORM_IOS + if (SDL_floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_7_1) { + // Older iOS hardware, use 40 ms as a minimum time + MINIMUM_AUDIO_BUFFER_TIME_MS = 40.0; + } + #endif + + // we use THREE audio buffers by default, unlike most things that would + // choose two alternating buffers, because it helps with issues on + // Bluetooth headsets when recording and playing at the same time. + // See conversation in #8192 for details. + int numAudioBuffers = 3; + const double msecs = (device->sample_frames / ((double)device->spec.freq)) * 1000.0; + if (msecs < MINIMUM_AUDIO_BUFFER_TIME_MS) { // use more buffers if we have a VERY small sample set. + numAudioBuffers = ((int)SDL_ceil(MINIMUM_AUDIO_BUFFER_TIME_MS / msecs) * 2); + } + + device->hidden->numAudioBuffers = numAudioBuffers; + device->hidden->audioBuffer = SDL_calloc(numAudioBuffers, sizeof(AudioQueueBufferRef)); + if (device->hidden->audioBuffer == NULL) { + return false; + } + + #if DEBUG_COREAUDIO + SDL_Log("COREAUDIO: numAudioBuffers == %d", numAudioBuffers); + #endif + + for (int i = 0; i < numAudioBuffers; i++) { + result = AudioQueueAllocateBuffer(device->hidden->audioQueue, device->buffer_size, &device->hidden->audioBuffer[i]); + CHECK_RESULT("AudioQueueAllocateBuffer"); + SDL_memset(device->hidden->audioBuffer[i]->mAudioData, device->silence_value, device->hidden->audioBuffer[i]->mAudioDataBytesCapacity); + device->hidden->audioBuffer[i]->mAudioDataByteSize = device->hidden->audioBuffer[i]->mAudioDataBytesCapacity; + // !!! FIXME: should we use AudioQueueEnqueueBufferWithParameters and specify all frames be "trimmed" so these are immediately ready to refill with SDL callback data? + result = AudioQueueEnqueueBuffer(device->hidden->audioQueue, device->hidden->audioBuffer[i], 0, NULL); + CHECK_RESULT("AudioQueueEnqueueBuffer"); + } + + result = AudioQueueStart(device->hidden->audioQueue, NULL); + CHECK_RESULT("AudioQueueStart"); + + return true; // We're running! +} + +static int AudioQueueThreadEntry(void *arg) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *)arg; + + if (device->recording) { + SDL_RecordingAudioThreadSetup(device); + } else { + SDL_PlaybackAudioThreadSetup(device); + } + + if (!PrepareAudioQueue(device)) { + device->hidden->thread_error = SDL_strdup(SDL_GetError()); + SDL_SignalSemaphore(device->hidden->ready_semaphore); + return 0; + } + + // init was successful, alert parent thread and start running... + SDL_SignalSemaphore(device->hidden->ready_semaphore); + + // This would be WaitDevice/WaitRecordingDevice in the normal SDL audio thread, but we get *BufferReadyCallback calls here to know when to iterate. + while (!SDL_GetAtomicInt(&device->shutdown)) { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.10, 1); + } + + if (device->recording) { + SDL_RecordingAudioThreadShutdown(device); + } else { + // Drain off any pending playback. + const CFTimeInterval secs = (((CFTimeInterval)device->sample_frames) / ((CFTimeInterval)device->spec.freq)) * 2.0; + CFRunLoopRunInMode(kCFRunLoopDefaultMode, secs, 0); + SDL_PlaybackAudioThreadShutdown(device); + } + + return 0; +} + +static bool COREAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + // Initialize all variables that we clean on shutdown + device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (device->hidden == NULL) { + return false; + } + + #ifndef MACOSX_COREAUDIO + if (!UpdateAudioSession(device, true, true)) { + return false; + } + + // Stop CoreAudio from doing expensive audio rate conversion + @autoreleasepool { + AVAudioSession *session = [AVAudioSession sharedInstance]; + [session setPreferredSampleRate:device->spec.freq error:nil]; + device->spec.freq = (int)session.sampleRate; + #ifdef SDL_PLATFORM_TVOS + if (device->recording) { + [session setPreferredInputNumberOfChannels:device->spec.channels error:nil]; + device->spec.channels = (int)session.preferredInputNumberOfChannels; + } else { + [session setPreferredOutputNumberOfChannels:device->spec.channels error:nil]; + device->spec.channels = (int)session.preferredOutputNumberOfChannels; + } + #else + // Calling setPreferredOutputNumberOfChannels seems to break audio output on iOS + #endif // SDL_PLATFORM_TVOS + } + #endif + + // Setup a AudioStreamBasicDescription with the requested format + AudioStreamBasicDescription *strdesc = &device->hidden->strdesc; + strdesc->mFormatID = kAudioFormatLinearPCM; + strdesc->mFormatFlags = kLinearPCMFormatFlagIsPacked; + strdesc->mChannelsPerFrame = device->spec.channels; + strdesc->mSampleRate = device->spec.freq; + strdesc->mFramesPerPacket = 1; + + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + SDL_AudioFormat test_format; + while ((test_format = *(closefmts++)) != 0) { + // CoreAudio handles most of SDL's formats natively. + switch (test_format) { + case SDL_AUDIO_U8: + case SDL_AUDIO_S8: + case SDL_AUDIO_S16LE: + case SDL_AUDIO_S16BE: + case SDL_AUDIO_S32LE: + case SDL_AUDIO_S32BE: + case SDL_AUDIO_F32LE: + case SDL_AUDIO_F32BE: + break; + + default: + continue; + } + break; + } + + if (!test_format) { // shouldn't happen, but just in case... + return SDL_SetError("%s: Unsupported audio format", "coreaudio"); + } + device->spec.format = test_format; + strdesc->mBitsPerChannel = SDL_AUDIO_BITSIZE(test_format); + if (SDL_AUDIO_ISBIGENDIAN(test_format)) { + strdesc->mFormatFlags |= kLinearPCMFormatFlagIsBigEndian; + } + + if (SDL_AUDIO_ISFLOAT(test_format)) { + strdesc->mFormatFlags |= kLinearPCMFormatFlagIsFloat; + } else if (SDL_AUDIO_ISSIGNED(test_format)) { + strdesc->mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger; + } + + strdesc->mBytesPerFrame = strdesc->mChannelsPerFrame * strdesc->mBitsPerChannel / 8; + strdesc->mBytesPerPacket = strdesc->mBytesPerFrame * strdesc->mFramesPerPacket; + +#ifdef MACOSX_COREAUDIO + if (!PrepareDevice(device)) { + return false; + } +#endif + + // This has to init in a new thread so it can get its own CFRunLoop. :/ + device->hidden->ready_semaphore = SDL_CreateSemaphore(0); + if (!device->hidden->ready_semaphore) { + return false; // oh well. + } + + char threadname[64]; + SDL_GetAudioThreadName(device, threadname, sizeof(threadname)); + device->hidden->thread = SDL_CreateThread(AudioQueueThreadEntry, threadname, device); + if (!device->hidden->thread) { + return false; + } + + SDL_WaitSemaphore(device->hidden->ready_semaphore); + SDL_DestroySemaphore(device->hidden->ready_semaphore); + device->hidden->ready_semaphore = NULL; + + if ((device->hidden->thread != NULL) && (device->hidden->thread_error != NULL)) { + SDL_WaitThread(device->hidden->thread, NULL); + device->hidden->thread = NULL; + return SDL_SetError("%s", device->hidden->thread_error); + } + + return (device->hidden->thread != NULL); +} + +static void COREAUDIO_DeinitializeStart(void) +{ +#ifdef MACOSX_COREAUDIO + AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &devlist_address, DeviceListChangedNotification, NULL); + AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &default_playback_device_address, DefaultPlaybackDeviceChangedNotification, NULL); + AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &default_recording_device_address, DefaultRecordingDeviceChangedNotification, NULL); +#endif +} + +static bool COREAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + impl->OpenDevice = COREAUDIO_OpenDevice; + impl->PlayDevice = COREAUDIO_PlayDevice; + impl->GetDeviceBuf = COREAUDIO_GetDeviceBuf; + impl->RecordDevice = COREAUDIO_RecordDevice; + impl->FlushRecording = COREAUDIO_FlushRecording; + impl->CloseDevice = COREAUDIO_CloseDevice; + impl->DeinitializeStart = COREAUDIO_DeinitializeStart; + +#ifdef MACOSX_COREAUDIO + impl->DetectDevices = COREAUDIO_DetectDevices; + impl->FreeDeviceHandle = COREAUDIO_FreeDeviceHandle; +#else + impl->OnlyHasDefaultPlaybackDevice = true; + impl->OnlyHasDefaultRecordingDevice = true; +#endif + + impl->ProvidesOwnCallbackThread = true; + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap COREAUDIO_bootstrap = { + "coreaudio", "CoreAudio", COREAUDIO_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_COREAUDIO diff --git a/contrib/SDL-3.2.8/src/audio/directsound/SDL_directsound.c b/contrib/SDL-3.2.8/src/audio/directsound/SDL_directsound.c new file mode 100644 index 0000000..7b5cb11 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/directsound/SDL_directsound.c @@ -0,0 +1,680 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_DSOUND + +#include "../SDL_sysaudio.h" +#include "SDL_directsound.h" +#include +#ifdef HAVE_MMDEVICEAPI_H +#include "../../core/windows/SDL_immdevice.h" +#endif + +#ifndef WAVE_FORMAT_IEEE_FLOAT +#define WAVE_FORMAT_IEEE_FLOAT 0x0003 +#endif + +// For Vista+, we can enumerate DSound devices with IMMDevice +#ifdef HAVE_MMDEVICEAPI_H +static bool SupportsIMMDevice = false; +#endif + +// DirectX function pointers for audio +static SDL_SharedObject *DSoundDLL = NULL; +typedef HRESULT(WINAPI *fnDirectSoundCreate8)(LPGUID, LPDIRECTSOUND *, LPUNKNOWN); +typedef HRESULT(WINAPI *fnDirectSoundEnumerateW)(LPDSENUMCALLBACKW, LPVOID); +typedef HRESULT(WINAPI *fnDirectSoundCaptureCreate8)(LPCGUID, LPDIRECTSOUNDCAPTURE8 *, LPUNKNOWN); +typedef HRESULT(WINAPI *fnDirectSoundCaptureEnumerateW)(LPDSENUMCALLBACKW, LPVOID); +typedef HRESULT(WINAPI *fnGetDeviceID)(LPCGUID, LPGUID); +static fnDirectSoundCreate8 pDirectSoundCreate8 = NULL; +static fnDirectSoundEnumerateW pDirectSoundEnumerateW = NULL; +static fnDirectSoundCaptureCreate8 pDirectSoundCaptureCreate8 = NULL; +static fnDirectSoundCaptureEnumerateW pDirectSoundCaptureEnumerateW = NULL; +static fnGetDeviceID pGetDeviceID = NULL; + +#include +DEFINE_GUID(SDL_DSDEVID_DefaultPlayback, 0xdef00000, 0x9c6d, 0x47ed, 0xaa, 0xf1, 0x4d, 0xda, 0x8f, 0x2b, 0x5c, 0x03); +DEFINE_GUID(SDL_DSDEVID_DefaultCapture, 0xdef00001, 0x9c6d, 0x47ed, 0xaa, 0xf1, 0x4d, 0xda, 0x8f, 0x2b, 0x5c, 0x03); + +static const GUID SDL_KSDATAFORMAT_SUBTYPE_PCM = { 0x00000001, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; +static const GUID SDL_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = { 0x00000003, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; + +static void DSOUND_Unload(void) +{ + pDirectSoundCreate8 = NULL; + pDirectSoundEnumerateW = NULL; + pDirectSoundCaptureCreate8 = NULL; + pDirectSoundCaptureEnumerateW = NULL; + pGetDeviceID = NULL; + + if (DSoundDLL) { + SDL_UnloadObject(DSoundDLL); + DSoundDLL = NULL; + } +} + +static bool DSOUND_Load(void) +{ + bool loaded = false; + + DSOUND_Unload(); + + DSoundDLL = SDL_LoadObject("DSOUND.DLL"); + if (!DSoundDLL) { + SDL_SetError("DirectSound: failed to load DSOUND.DLL"); + } else { +// Now make sure we have DirectX 8 or better... +#define DSOUNDLOAD(f) \ + { \ + p##f = (fn##f)SDL_LoadFunction(DSoundDLL, #f); \ + if (!p##f) \ + loaded = false; \ + } + loaded = true; // will reset if necessary. + DSOUNDLOAD(DirectSoundCreate8); + DSOUNDLOAD(DirectSoundEnumerateW); + DSOUNDLOAD(DirectSoundCaptureCreate8); + DSOUNDLOAD(DirectSoundCaptureEnumerateW); + DSOUNDLOAD(GetDeviceID); +#undef DSOUNDLOAD + + if (!loaded) { + SDL_SetError("DirectSound: System doesn't appear to have DX8."); + } + } + + if (!loaded) { + DSOUND_Unload(); + } + + return loaded; +} + +static bool SetDSerror(const char *function, int code) +{ + const char *error; + + switch (code) { + case E_NOINTERFACE: + error = "Unsupported interface -- Is DirectX 8.0 or later installed?"; + break; + case DSERR_ALLOCATED: + error = "Audio device in use"; + break; + case DSERR_BADFORMAT: + error = "Unsupported audio format"; + break; + case DSERR_BUFFERLOST: + error = "Mixing buffer was lost"; + break; + case DSERR_CONTROLUNAVAIL: + error = "Control requested is not available"; + break; + case DSERR_INVALIDCALL: + error = "Invalid call for the current state"; + break; + case DSERR_INVALIDPARAM: + error = "Invalid parameter"; + break; + case DSERR_NODRIVER: + error = "No audio device found"; + break; + case DSERR_OUTOFMEMORY: + error = "Out of memory"; + break; + case DSERR_PRIOLEVELNEEDED: + error = "Caller doesn't have priority"; + break; + case DSERR_UNSUPPORTED: + error = "Function not supported"; + break; + default: + error = "Unknown DirectSound error"; + break; + } + + return SDL_SetError("%s: %s (0x%x)", function, error, code); +} + +static void DSOUND_FreeDeviceHandle(SDL_AudioDevice *device) +{ +#ifdef HAVE_MMDEVICEAPI_H + if (SupportsIMMDevice) { + SDL_IMMDevice_FreeDeviceHandle(device); + } else +#endif + { + SDL_free(device->handle); + } +} + +// FindAllDevs is presumably only used on WinXP; Vista and later can use IMMDevice for better results. +typedef struct FindAllDevsData +{ + bool recording; + SDL_AudioDevice **default_device; + LPCGUID default_device_guid; +} FindAllDevsData; + +static BOOL CALLBACK FindAllDevs(LPGUID guid, LPCWSTR desc, LPCWSTR module, LPVOID userdata) +{ + FindAllDevsData *data = (FindAllDevsData *) userdata; + if (guid != NULL) { // skip default device + char *str = WIN_LookupAudioDeviceName(desc, guid); + if (str) { + LPGUID cpyguid = (LPGUID)SDL_malloc(sizeof(GUID)); + if (cpyguid) { + SDL_copyp(cpyguid, guid); + + /* Note that spec is NULL, because we are required to connect to the + * device before getting the channel mask and output format, making + * this information inaccessible at enumeration time + */ + SDL_AudioDevice *device = SDL_AddAudioDevice(data->recording, str, NULL, cpyguid); + if (device && data->default_device && data->default_device_guid) { + if (SDL_memcmp(cpyguid, data->default_device_guid, sizeof (GUID)) == 0) { + *data->default_device = device; + } + } + } + SDL_free(str); // SDL_AddAudioDevice() makes a copy of this string. + } + } + return TRUE; // keep enumerating. +} + +static void DSOUND_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ +#ifdef HAVE_MMDEVICEAPI_H + if (SupportsIMMDevice) { + SDL_IMMDevice_EnumerateEndpoints(default_playback, default_recording); + } else +#endif + { + // Without IMMDevice, you can enumerate devices and figure out the default devices, + // but you won't get device hotplug or default device change notifications. But this is + // only for WinXP; Windows Vista and later should be using IMMDevice. + FindAllDevsData data; + GUID guid; + + data.recording = true; + data.default_device = default_recording; + data.default_device_guid = (pGetDeviceID(&SDL_DSDEVID_DefaultCapture, &guid) == DS_OK) ? &guid : NULL; + pDirectSoundCaptureEnumerateW(FindAllDevs, &data); + + data.recording = false; + data.default_device = default_playback; + data.default_device_guid = (pGetDeviceID(&SDL_DSDEVID_DefaultPlayback, &guid) == DS_OK) ? &guid : NULL; + pDirectSoundEnumerateW(FindAllDevs, &data); + } + +} + +static bool DSOUND_WaitDevice(SDL_AudioDevice *device) +{ + /* Semi-busy wait, since we have no way of getting play notification + on a primary mixing buffer located in hardware (DirectX 5.0) + */ + while (!SDL_GetAtomicInt(&device->shutdown)) { + DWORD status = 0; + DWORD cursor = 0; + DWORD junk = 0; + HRESULT result = DS_OK; + + // Try to restore a lost sound buffer + IDirectSoundBuffer_GetStatus(device->hidden->mixbuf, &status); + if (status & DSBSTATUS_BUFFERLOST) { + IDirectSoundBuffer_Restore(device->hidden->mixbuf); + } else if (!(status & DSBSTATUS_PLAYING)) { + result = IDirectSoundBuffer_Play(device->hidden->mixbuf, 0, 0, DSBPLAY_LOOPING); + } else { + // Find out where we are playing + result = IDirectSoundBuffer_GetCurrentPosition(device->hidden->mixbuf, &junk, &cursor); + if ((result == DS_OK) && ((cursor / device->buffer_size) != device->hidden->lastchunk)) { + break; // ready for next chunk! + } + } + + if ((result != DS_OK) && (result != DSERR_BUFFERLOST)) { + return false; + } + + SDL_Delay(1); // not ready yet; sleep a bit. + } + + return true; +} + +static bool DSOUND_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + // Unlock the buffer, allowing it to play + SDL_assert(buflen == device->buffer_size); + if (IDirectSoundBuffer_Unlock(device->hidden->mixbuf, (LPVOID) buffer, buflen, NULL, 0) != DS_OK) { + return false; + } + return true; +} + +static Uint8 *DSOUND_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + DWORD cursor = 0; + DWORD junk = 0; + HRESULT result = DS_OK; + + SDL_assert(*buffer_size == device->buffer_size); + + // Figure out which blocks to fill next + device->hidden->locked_buf = NULL; + result = IDirectSoundBuffer_GetCurrentPosition(device->hidden->mixbuf, + &junk, &cursor); + if (result == DSERR_BUFFERLOST) { + IDirectSoundBuffer_Restore(device->hidden->mixbuf); + result = IDirectSoundBuffer_GetCurrentPosition(device->hidden->mixbuf, + &junk, &cursor); + } + if (result != DS_OK) { + SetDSerror("DirectSound GetCurrentPosition", result); + return NULL; + } + cursor /= device->buffer_size; +#ifdef DEBUG_SOUND + // Detect audio dropouts + { + DWORD spot = cursor; + if (spot < device->hidden->lastchunk) { + spot += device->hidden->num_buffers; + } + if (spot > device->hidden->lastchunk + 1) { + fprintf(stderr, "Audio dropout, missed %d fragments\n", + (spot - (device->hidden->lastchunk + 1))); + } + } +#endif + device->hidden->lastchunk = cursor; + cursor = (cursor + 1) % device->hidden->num_buffers; + cursor *= device->buffer_size; + + // Lock the audio buffer + DWORD rawlen = 0; + result = IDirectSoundBuffer_Lock(device->hidden->mixbuf, cursor, + device->buffer_size, + (LPVOID *)&device->hidden->locked_buf, + &rawlen, NULL, &junk, 0); + if (result == DSERR_BUFFERLOST) { + IDirectSoundBuffer_Restore(device->hidden->mixbuf); + result = IDirectSoundBuffer_Lock(device->hidden->mixbuf, cursor, + device->buffer_size, + (LPVOID *)&device->hidden->locked_buf, &rawlen, NULL, + &junk, 0); + } + if (result != DS_OK) { + SetDSerror("DirectSound Lock", result); + return NULL; + } + return device->hidden->locked_buf; +} + +static bool DSOUND_WaitRecordingDevice(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *h = device->hidden; + while (!SDL_GetAtomicInt(&device->shutdown)) { + DWORD junk, cursor; + if (IDirectSoundCaptureBuffer_GetCurrentPosition(h->capturebuf, &junk, &cursor) != DS_OK) { + return false; + } else if ((cursor / device->buffer_size) != h->lastchunk) { + break; + } + SDL_Delay(1); + } + + return true; +} + +static int DSOUND_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + struct SDL_PrivateAudioData *h = device->hidden; + DWORD ptr1len, ptr2len; + VOID *ptr1, *ptr2; + + SDL_assert(buflen == device->buffer_size); + + if (IDirectSoundCaptureBuffer_Lock(h->capturebuf, h->lastchunk * buflen, buflen, &ptr1, &ptr1len, &ptr2, &ptr2len, 0) != DS_OK) { + return -1; + } + + SDL_assert(ptr1len == (DWORD)buflen); + SDL_assert(ptr2 == NULL); + SDL_assert(ptr2len == 0); + + SDL_memcpy(buffer, ptr1, ptr1len); + + if (IDirectSoundCaptureBuffer_Unlock(h->capturebuf, ptr1, ptr1len, ptr2, ptr2len) != DS_OK) { + return -1; + } + + h->lastchunk = (h->lastchunk + 1) % h->num_buffers; + + return (int) ptr1len; +} + +static void DSOUND_FlushRecording(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *h = device->hidden; + DWORD junk, cursor; + if (IDirectSoundCaptureBuffer_GetCurrentPosition(h->capturebuf, &junk, &cursor) == DS_OK) { + h->lastchunk = cursor / device->buffer_size; + } +} + +static void DSOUND_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->mixbuf) { + IDirectSoundBuffer_Stop(device->hidden->mixbuf); + IDirectSoundBuffer_Release(device->hidden->mixbuf); + } + if (device->hidden->sound) { + IDirectSound_Release(device->hidden->sound); + } + if (device->hidden->capturebuf) { + IDirectSoundCaptureBuffer_Stop(device->hidden->capturebuf); + IDirectSoundCaptureBuffer_Release(device->hidden->capturebuf); + } + if (device->hidden->capture) { + IDirectSoundCapture_Release(device->hidden->capture); + } + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +/* This function tries to create a secondary audio buffer, and returns the + number of audio chunks available in the created buffer. This is for + playback devices, not recording. +*/ +static bool CreateSecondary(SDL_AudioDevice *device, const DWORD bufsize, WAVEFORMATEX *wfmt) +{ + LPDIRECTSOUND sndObj = device->hidden->sound; + LPDIRECTSOUNDBUFFER *sndbuf = &device->hidden->mixbuf; + HRESULT result = DS_OK; + DSBUFFERDESC format; + LPVOID pvAudioPtr1, pvAudioPtr2; + DWORD dwAudioBytes1, dwAudioBytes2; + + // Try to create the secondary buffer + SDL_zero(format); + format.dwSize = sizeof(format); + format.dwFlags = DSBCAPS_GETCURRENTPOSITION2; + format.dwFlags |= DSBCAPS_GLOBALFOCUS; + format.dwBufferBytes = bufsize; + format.lpwfxFormat = wfmt; + result = IDirectSound_CreateSoundBuffer(sndObj, &format, sndbuf, NULL); + if (result != DS_OK) { + return SetDSerror("DirectSound CreateSoundBuffer", result); + } + IDirectSoundBuffer_SetFormat(*sndbuf, wfmt); + + // Silence the initial audio buffer + result = IDirectSoundBuffer_Lock(*sndbuf, 0, format.dwBufferBytes, + (LPVOID *)&pvAudioPtr1, &dwAudioBytes1, + (LPVOID *)&pvAudioPtr2, &dwAudioBytes2, + DSBLOCK_ENTIREBUFFER); + if (result == DS_OK) { + SDL_memset(pvAudioPtr1, device->silence_value, dwAudioBytes1); + IDirectSoundBuffer_Unlock(*sndbuf, + (LPVOID)pvAudioPtr1, dwAudioBytes1, + (LPVOID)pvAudioPtr2, dwAudioBytes2); + } + + return true; // We're ready to go +} + +/* This function tries to create a capture buffer, and returns the + number of audio chunks available in the created buffer. This is for + recording devices, not playback. +*/ +static bool CreateCaptureBuffer(SDL_AudioDevice *device, const DWORD bufsize, WAVEFORMATEX *wfmt) +{ + LPDIRECTSOUNDCAPTURE capture = device->hidden->capture; + LPDIRECTSOUNDCAPTUREBUFFER *capturebuf = &device->hidden->capturebuf; + DSCBUFFERDESC format; + HRESULT result; + + SDL_zero(format); + format.dwSize = sizeof(format); + format.dwFlags = DSCBCAPS_WAVEMAPPED; + format.dwBufferBytes = bufsize; + format.lpwfxFormat = wfmt; + + result = IDirectSoundCapture_CreateCaptureBuffer(capture, &format, capturebuf, NULL); + if (result != DS_OK) { + return SetDSerror("DirectSound CreateCaptureBuffer", result); + } + + result = IDirectSoundCaptureBuffer_Start(*capturebuf, DSCBSTART_LOOPING); + if (result != DS_OK) { + IDirectSoundCaptureBuffer_Release(*capturebuf); + return SetDSerror("DirectSound Start", result); + } + +#if 0 + // presumably this starts at zero, but just in case... + result = IDirectSoundCaptureBuffer_GetCurrentPosition(*capturebuf, &junk, &cursor); + if (result != DS_OK) { + IDirectSoundCaptureBuffer_Stop(*capturebuf); + IDirectSoundCaptureBuffer_Release(*capturebuf); + return SetDSerror("DirectSound GetCurrentPosition", result); + } + + device->hidden->lastchunk = cursor / device->buffer_size; +#endif + + return true; +} + +static bool DSOUND_OpenDevice(SDL_AudioDevice *device) +{ + // Initialize all variables that we clean on shutdown + device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // Open the audio device + LPGUID guid; +#ifdef HAVE_MMDEVICEAPI_H + if (SupportsIMMDevice) { + guid = SDL_IMMDevice_GetDirectSoundGUID(device); + } else +#endif + { + guid = (LPGUID) device->handle; + } + + SDL_assert(guid != NULL); + + HRESULT result; + if (device->recording) { + result = pDirectSoundCaptureCreate8(guid, &device->hidden->capture, NULL); + if (result != DS_OK) { + return SetDSerror("DirectSoundCaptureCreate8", result); + } + } else { + result = pDirectSoundCreate8(guid, &device->hidden->sound, NULL); + if (result != DS_OK) { + return SetDSerror("DirectSoundCreate8", result); + } + result = IDirectSound_SetCooperativeLevel(device->hidden->sound, + GetDesktopWindow(), + DSSCL_NORMAL); + if (result != DS_OK) { + return SetDSerror("DirectSound SetCooperativeLevel", result); + } + } + + const DWORD numchunks = 8; + DWORD bufsize; + bool tried_format = false; + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { + switch (test_format) { + case SDL_AUDIO_U8: + case SDL_AUDIO_S16: + case SDL_AUDIO_S32: + case SDL_AUDIO_F32: + tried_format = true; + + device->spec.format = test_format; + + // Update the fragment size as size in bytes + SDL_UpdatedAudioDeviceFormat(device); + + bufsize = numchunks * device->buffer_size; + if ((bufsize < DSBSIZE_MIN) || (bufsize > DSBSIZE_MAX)) { + SDL_SetError("Sound buffer size must be between %d and %d", + (int)((DSBSIZE_MIN < numchunks) ? 1 : DSBSIZE_MIN / numchunks), + (int)(DSBSIZE_MAX / numchunks)); + } else { + WAVEFORMATEXTENSIBLE wfmt; + SDL_zero(wfmt); + if (device->spec.channels > 2) { + wfmt.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + wfmt.Format.cbSize = sizeof(wfmt) - sizeof(WAVEFORMATEX); + + if (SDL_AUDIO_ISFLOAT(device->spec.format)) { + SDL_memcpy(&wfmt.SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, sizeof(GUID)); + } else { + SDL_memcpy(&wfmt.SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_PCM, sizeof(GUID)); + } + wfmt.Samples.wValidBitsPerSample = SDL_AUDIO_BITSIZE(device->spec.format); + + switch (device->spec.channels) { + case 3: // 3.0 (or 2.1) + wfmt.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER; + break; + case 4: // 4.0 + wfmt.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; + break; + case 5: // 5.0 (or 4.1) + wfmt.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; + break; + case 6: // 5.1 + wfmt.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; + break; + case 7: // 6.1 + wfmt.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER; + break; + case 8: // 7.1 + wfmt.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT; + break; + default: + SDL_assert(!"Unsupported channel count!"); + break; + } + } else if (SDL_AUDIO_ISFLOAT(device->spec.format)) { + wfmt.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + } else { + wfmt.Format.wFormatTag = WAVE_FORMAT_PCM; + } + + wfmt.Format.wBitsPerSample = SDL_AUDIO_BITSIZE(device->spec.format); + wfmt.Format.nChannels = (WORD)device->spec.channels; + wfmt.Format.nSamplesPerSec = device->spec.freq; + wfmt.Format.nBlockAlign = wfmt.Format.nChannels * (wfmt.Format.wBitsPerSample / 8); + wfmt.Format.nAvgBytesPerSec = wfmt.Format.nSamplesPerSec * wfmt.Format.nBlockAlign; + + const bool rc = device->recording ? CreateCaptureBuffer(device, bufsize, (WAVEFORMATEX *)&wfmt) : CreateSecondary(device, bufsize, (WAVEFORMATEX *)&wfmt); + if (rc) { + device->hidden->num_buffers = numchunks; + break; + } + } + continue; + default: + continue; + } + break; + } + + if (!test_format) { + if (tried_format) { + return false; // CreateSecondary() should have called SDL_SetError(). + } + return SDL_SetError("%s: Unsupported audio format", "directsound"); + } + + // Playback buffers will auto-start playing in DSOUND_WaitDevice() + + return true; // good to go. +} + +static void DSOUND_DeinitializeStart(void) +{ +#ifdef HAVE_MMDEVICEAPI_H + if (SupportsIMMDevice) { + SDL_IMMDevice_Quit(); + } +#endif +} + +static void DSOUND_Deinitialize(void) +{ + DSOUND_Unload(); +#ifdef HAVE_MMDEVICEAPI_H + SupportsIMMDevice = false; +#endif +} + +static bool DSOUND_Init(SDL_AudioDriverImpl *impl) +{ + if (!DSOUND_Load()) { + return false; + } + +#ifdef HAVE_MMDEVICEAPI_H + SupportsIMMDevice = SDL_IMMDevice_Init(NULL); +#endif + + impl->DetectDevices = DSOUND_DetectDevices; + impl->OpenDevice = DSOUND_OpenDevice; + impl->PlayDevice = DSOUND_PlayDevice; + impl->WaitDevice = DSOUND_WaitDevice; + impl->GetDeviceBuf = DSOUND_GetDeviceBuf; + impl->WaitRecordingDevice = DSOUND_WaitRecordingDevice; + impl->RecordDevice = DSOUND_RecordDevice; + impl->FlushRecording = DSOUND_FlushRecording; + impl->CloseDevice = DSOUND_CloseDevice; + impl->FreeDeviceHandle = DSOUND_FreeDeviceHandle; + impl->DeinitializeStart = DSOUND_DeinitializeStart; + impl->Deinitialize = DSOUND_Deinitialize; + + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap DSOUND_bootstrap = { + "directsound", "DirectSound", DSOUND_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_DSOUND diff --git a/contrib/SDL-3.2.8/src/audio/directsound/SDL_directsound.h b/contrib/SDL-3.2.8/src/audio/directsound/SDL_directsound.h new file mode 100644 index 0000000..a4fa2fa --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/directsound/SDL_directsound.h @@ -0,0 +1,43 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_directsound_h_ +#define SDL_directsound_h_ + +#include "../../core/windows/SDL_directx.h" + +#include "../SDL_sysaudio.h" + +// The DirectSound objects +struct SDL_PrivateAudioData +{ + // !!! FIXME: make this a union with capture/playback sections? + LPDIRECTSOUND sound; + LPDIRECTSOUNDBUFFER mixbuf; + LPDIRECTSOUNDCAPTURE capture; + LPDIRECTSOUNDCAPTUREBUFFER capturebuf; + int num_buffers; + DWORD lastchunk; + Uint8 *locked_buf; +}; + +#endif // SDL_directsound_h_ diff --git a/contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.c b/contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.c new file mode 100644 index 0000000..9e05478 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.c @@ -0,0 +1,171 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_DISK + +// Output raw audio data to a file. + +#include "../SDL_sysaudio.h" +#include "SDL_diskaudio.h" + +#define DISKDEFAULT_OUTFILE "sdlaudio.raw" +#define DISKDEFAULT_INFILE "sdlaudio-in.raw" + +static bool DISKAUDIO_WaitDevice(SDL_AudioDevice *device) +{ + SDL_Delay(device->hidden->io_delay); + return true; +} + +static bool DISKAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size) +{ + const int written = (int)SDL_WriteIO(device->hidden->io, buffer, (size_t)buffer_size); + if (written != buffer_size) { // If we couldn't write, assume fatal error for now + return false; + } +#ifdef DEBUG_AUDIO + SDL_Log("DISKAUDIO: Wrote %d bytes of audio data", (int) written); +#endif + return true; +} + +static Uint8 *DISKAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->hidden->mixbuf; +} + +static int DISKAUDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + struct SDL_PrivateAudioData *h = device->hidden; + const int origbuflen = buflen; + + if (h->io) { + const int br = (int)SDL_ReadIO(h->io, buffer, (size_t)buflen); + buflen -= br; + buffer = ((Uint8 *)buffer) + br; + if (buflen > 0) { // EOF (or error, but whatever). + SDL_CloseIO(h->io); + h->io = NULL; + } + } + + // if we ran out of file, just write silence. + SDL_memset(buffer, device->silence_value, buflen); + + return origbuflen; +} + +static void DISKAUDIO_FlushRecording(SDL_AudioDevice *device) +{ + // no op...we don't advance the file pointer or anything. +} + +static void DISKAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->io) { + SDL_CloseIO(device->hidden->io); + } + SDL_free(device->hidden->mixbuf); + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static const char *get_filename(const bool recording) +{ + const char *devname = SDL_GetHint(recording ? SDL_HINT_AUDIO_DISK_INPUT_FILE : SDL_HINT_AUDIO_DISK_OUTPUT_FILE); + if (!devname) { + devname = recording ? DISKDEFAULT_INFILE : DISKDEFAULT_OUTFILE; + } + return devname; +} + +static bool DISKAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + bool recording = device->recording; + const char *fname = get_filename(recording); + + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + device->hidden->io_delay = ((device->sample_frames * 1000) / device->spec.freq); + + const char *hint = SDL_GetHint(SDL_HINT_AUDIO_DISK_TIMESCALE); + if (hint) { + double scale = SDL_atof(hint); + if (scale >= 0.0) { + device->hidden->io_delay = (Uint32)SDL_round(device->hidden->io_delay * scale); + } + } + + // Open the "audio device" + device->hidden->io = SDL_IOFromFile(fname, recording ? "rb" : "wb"); + if (!device->hidden->io) { + return false; + } + + // Allocate mixing buffer + if (!recording) { + device->hidden->mixbuf = (Uint8 *)SDL_malloc(device->buffer_size); + if (!device->hidden->mixbuf) { + return false; + } + SDL_memset(device->hidden->mixbuf, device->silence_value, device->buffer_size); + } + + SDL_LogCritical(SDL_LOG_CATEGORY_AUDIO, "You are using the SDL disk i/o audio driver!"); + SDL_LogCritical(SDL_LOG_CATEGORY_AUDIO, " %s file [%s].", recording ? "Reading from" : "Writing to", fname); + + return true; // We're ready to rock and roll. :-) +} + +static void DISKAUDIO_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, (void *)0x1); + *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, (void *)0x2); +} + +static bool DISKAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + impl->OpenDevice = DISKAUDIO_OpenDevice; + impl->WaitDevice = DISKAUDIO_WaitDevice; + impl->WaitRecordingDevice = DISKAUDIO_WaitDevice; + impl->PlayDevice = DISKAUDIO_PlayDevice; + impl->GetDeviceBuf = DISKAUDIO_GetDeviceBuf; + impl->RecordDevice = DISKAUDIO_RecordDevice; + impl->FlushRecording = DISKAUDIO_FlushRecording; + impl->CloseDevice = DISKAUDIO_CloseDevice; + impl->DetectDevices = DISKAUDIO_DetectDevices; + + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap DISKAUDIO_bootstrap = { + "disk", "direct-to-disk audio", DISKAUDIO_Init, true, false +}; + +#endif // SDL_AUDIO_DRIVER_DISK diff --git a/contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.h b/contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.h new file mode 100644 index 0000000..d8d9980 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/disk/SDL_diskaudio.h @@ -0,0 +1,36 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_diskaudio_h_ +#define SDL_diskaudio_h_ + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + // The file descriptor for the audio device + SDL_IOStream *io; + Uint32 io_delay; + Uint8 *mixbuf; +}; + +#endif // SDL_diskaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.c b/contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.c new file mode 100644 index 0000000..62b8990 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.c @@ -0,0 +1,303 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +// !!! FIXME: clean out perror and fprintf calls in here. + +#ifdef SDL_AUDIO_DRIVER_OSS + +#include // For perror() +#include // For strerror() +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../SDL_audiodev_c.h" +#include "SDL_dspaudio.h" + +static void DSP_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + SDL_EnumUnixAudioDevices(false, NULL); +} + +static void DSP_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->audio_fd >= 0) { + close(device->hidden->audio_fd); + } + SDL_free(device->hidden->mixbuf); + SDL_free(device->hidden); + } +} + +static bool DSP_OpenDevice(SDL_AudioDevice *device) +{ + // Make sure fragment size stays a power of 2, or OSS fails. + // (I don't know which of these are actually legal values, though...) + if (device->spec.channels > 8) { + device->spec.channels = 8; + } else if (device->spec.channels > 4) { + device->spec.channels = 4; + } else if (device->spec.channels > 2) { + device->spec.channels = 2; + } + + // Initialize all variables that we clean on shutdown + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // Open the audio device; we hardcode the device path in `device->name` for lack of better info, so use that. + const int flags = ((device->recording) ? OPEN_FLAGS_INPUT : OPEN_FLAGS_OUTPUT); + device->hidden->audio_fd = open(device->name, flags | O_CLOEXEC, 0); + if (device->hidden->audio_fd < 0) { + return SDL_SetError("Couldn't open %s: %s", device->name, strerror(errno)); + } + + // Make the file descriptor use blocking i/o with fcntl() + { + const long ctlflags = fcntl(device->hidden->audio_fd, F_GETFL) & ~O_NONBLOCK; + if (fcntl(device->hidden->audio_fd, F_SETFL, ctlflags) < 0) { + return SDL_SetError("Couldn't set audio blocking mode"); + } + } + + // Get a list of supported hardware formats + int value; + if (ioctl(device->hidden->audio_fd, SNDCTL_DSP_GETFMTS, &value) < 0) { + perror("SNDCTL_DSP_GETFMTS"); + return SDL_SetError("Couldn't get audio format list"); + } + + // Try for a closest match on audio format + int format = 0; + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { +#ifdef DEBUG_AUDIO + fprintf(stderr, "Trying format 0x%4.4x\n", test_format); +#endif + switch (test_format) { + case SDL_AUDIO_U8: + if (value & AFMT_U8) { + format = AFMT_U8; + } + break; + case SDL_AUDIO_S16LE: + if (value & AFMT_S16_LE) { + format = AFMT_S16_LE; + } + break; + case SDL_AUDIO_S16BE: + if (value & AFMT_S16_BE) { + format = AFMT_S16_BE; + } + break; + + default: + continue; + } + break; + } + if (format == 0) { + return SDL_SetError("Couldn't find any hardware audio formats"); + } + device->spec.format = test_format; + + // Set the audio format + value = format; + if ((ioctl(device->hidden->audio_fd, SNDCTL_DSP_SETFMT, &value) < 0) || + (value != format)) { + perror("SNDCTL_DSP_SETFMT"); + return SDL_SetError("Couldn't set audio format"); + } + + // Set the number of channels of output + value = device->spec.channels; + if (ioctl(device->hidden->audio_fd, SNDCTL_DSP_CHANNELS, &value) < 0) { + perror("SNDCTL_DSP_CHANNELS"); + return SDL_SetError("Cannot set the number of channels"); + } + device->spec.channels = value; + + // Set the DSP frequency + value = device->spec.freq; + if (ioctl(device->hidden->audio_fd, SNDCTL_DSP_SPEED, &value) < 0) { + perror("SNDCTL_DSP_SPEED"); + return SDL_SetError("Couldn't set audio frequency"); + } + device->spec.freq = value; + + // Calculate the final parameters for this audio specification + SDL_UpdatedAudioDeviceFormat(device); + + /* Determine the power of two of the fragment size + Since apps don't control this in SDL3, and this driver only accepts 8, 16 + bit formats and 1, 2, 4, 8 channels, this should always be a power of 2 already. */ + SDL_assert(SDL_powerof2(device->buffer_size) == device->buffer_size); + + int frag_spec = 0; + while ((0x01U << frag_spec) < device->buffer_size) { + frag_spec++; + } + frag_spec |= 0x00020000; // two fragments, for low latency + + // Set the audio buffering parameters +#ifdef DEBUG_AUDIO + fprintf(stderr, "Requesting %d fragments of size %d\n", + (frag_spec >> 16), 1 << (frag_spec & 0xFFFF)); +#endif + if (ioctl(device->hidden->audio_fd, SNDCTL_DSP_SETFRAGMENT, &frag_spec) < 0) { + perror("SNDCTL_DSP_SETFRAGMENT"); + } +#ifdef DEBUG_AUDIO + { + audio_buf_info info; + ioctl(device->hidden->audio_fd, SNDCTL_DSP_GETOSPACE, &info); + fprintf(stderr, "fragments = %d\n", info.fragments); + fprintf(stderr, "fragstotal = %d\n", info.fragstotal); + fprintf(stderr, "fragsize = %d\n", info.fragsize); + fprintf(stderr, "bytes = %d\n", info.bytes); + } +#endif + + // Allocate mixing buffer + if (!device->recording) { + device->hidden->mixbuf = (Uint8 *)SDL_malloc(device->buffer_size); + if (!device->hidden->mixbuf) { + return false; + } + SDL_memset(device->hidden->mixbuf, device->silence_value, device->buffer_size); + } + + return true; // We're ready to rock and roll. :-) +} + +static bool DSP_WaitDevice(SDL_AudioDevice *device) +{ + const unsigned long ioctlreq = device->recording ? SNDCTL_DSP_GETISPACE : SNDCTL_DSP_GETOSPACE; + struct SDL_PrivateAudioData *h = device->hidden; + + while (!SDL_GetAtomicInt(&device->shutdown)) { + audio_buf_info info; + const int rc = ioctl(h->audio_fd, ioctlreq, &info); + if (rc < 0) { + if (errno == EAGAIN) { + continue; + } + // Hmm, not much we can do - abort + fprintf(stderr, "dsp WaitDevice ioctl failed (unrecoverable): %s\n", strerror(errno)); + return false; + } else if (info.bytes < device->buffer_size) { + SDL_Delay(10); + } else { + break; // ready to go! + } + } + + return true; +} + +static bool DSP_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + struct SDL_PrivateAudioData *h = device->hidden; + if (write(h->audio_fd, buffer, buflen) == -1) { + perror("Audio write"); + return false; + } +#ifdef DEBUG_AUDIO + fprintf(stderr, "Wrote %d bytes of audio data\n", h->mixlen); +#endif + return true; +} + +static Uint8 *DSP_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->hidden->mixbuf; +} + +static int DSP_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + return (int)read(device->hidden->audio_fd, buffer, buflen); +} + +static void DSP_FlushRecording(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *h = device->hidden; + audio_buf_info info; + if (ioctl(h->audio_fd, SNDCTL_DSP_GETISPACE, &info) == 0) { + while (info.bytes > 0) { + char buf[512]; + const size_t len = SDL_min(sizeof(buf), info.bytes); + const ssize_t br = read(h->audio_fd, buf, len); + if (br <= 0) { + break; + } + info.bytes -= br; + } + } +} + +static bool InitTimeDevicesExist = false; +static bool look_for_devices_test(int fd) +{ + InitTimeDevicesExist = true; // note that _something_ exists. + // Don't add to the device list, we're just seeing if any devices exist. + return false; +} + +static bool DSP_Init(SDL_AudioDriverImpl *impl) +{ + InitTimeDevicesExist = false; + SDL_EnumUnixAudioDevices(false, look_for_devices_test); + if (!InitTimeDevicesExist) { + SDL_SetError("dsp: No such audio device"); + return false; // maybe try a different backend. + } + + impl->DetectDevices = DSP_DetectDevices; + impl->OpenDevice = DSP_OpenDevice; + impl->WaitDevice = DSP_WaitDevice; + impl->PlayDevice = DSP_PlayDevice; + impl->GetDeviceBuf = DSP_GetDeviceBuf; + impl->CloseDevice = DSP_CloseDevice; + impl->WaitRecordingDevice = DSP_WaitDevice; + impl->RecordDevice = DSP_RecordDevice; + impl->FlushRecording = DSP_FlushRecording; + + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap DSP_bootstrap = { + "dsp", "Open Sound System (/dev/dsp)", DSP_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_OSS diff --git a/contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.h b/contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.h new file mode 100644 index 0000000..65dea60 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/dsp/SDL_dspaudio.h @@ -0,0 +1,37 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_dspaudio_h_ +#define SDL_dspaudio_h_ + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + // The file descriptor for the audio device + int audio_fd; + + // Raw mixing buffer + Uint8 *mixbuf; +}; + +#endif // SDL_dspaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.c b/contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.c new file mode 100644 index 0000000..d0f1a1a --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.c @@ -0,0 +1,135 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +// Output audio to nowhere... + +#include "../SDL_sysaudio.h" +#include "SDL_dummyaudio.h" + +#if defined(SDL_PLATFORM_EMSCRIPTEN) && !defined(__EMSCRIPTEN_PTHREADS__) +#include +#endif + +static bool DUMMYAUDIO_WaitDevice(SDL_AudioDevice *device) +{ + SDL_Delay(device->hidden->io_delay); + return true; +} + +static bool DUMMYAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + if (!device->recording) { + device->hidden->mixbuf = (Uint8 *) SDL_malloc(device->buffer_size); + if (!device->hidden->mixbuf) { + return false; + } + } + + device->hidden->io_delay = ((device->sample_frames * 1000) / device->spec.freq); + + const char *hint = SDL_GetHint(SDL_HINT_AUDIO_DUMMY_TIMESCALE); + if (hint) { + double scale = SDL_atof(hint); + if (scale >= 0.0) { + device->hidden->io_delay = (Uint32)SDL_round(device->hidden->io_delay * scale); + } + } + + // on Emscripten without threads, we just fire a repeating timer to consume audio. + #if defined(SDL_PLATFORM_EMSCRIPTEN) && !defined(__EMSCRIPTEN_PTHREADS__) + MAIN_THREAD_EM_ASM({ + var a = Module['SDL3'].dummy_audio; + if (a.timers[$0] !== undefined) { clearInterval(a.timers[$0]); } + a.timers[$0] = setInterval(function() { dynCall('vi', $3, [$4]); }, ($1 / $2) * 1000); + }, device->recording ? 1 : 0, device->sample_frames, device->spec.freq, device->recording ? SDL_RecordingAudioThreadIterate : SDL_PlaybackAudioThreadIterate, device); + #endif + + return true; // we're good; don't change reported device format. +} + +static void DUMMYAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + // on Emscripten without threads, we just fire a repeating timer to consume audio. + #if defined(SDL_PLATFORM_EMSCRIPTEN) && !defined(__EMSCRIPTEN_PTHREADS__) + MAIN_THREAD_EM_ASM({ + var a = Module['SDL3'].dummy_audio; + if (a.timers[$0] !== undefined) { clearInterval(a.timers[$0]); } + a.timers[$0] = undefined; + }, device->recording ? 1 : 0); + #endif + SDL_free(device->hidden->mixbuf); + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static Uint8 *DUMMYAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->hidden->mixbuf; +} + +static int DUMMYAUDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + // always return a full buffer of silence. + SDL_memset(buffer, device->silence_value, buflen); + return buflen; +} + +static bool DUMMYAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + impl->OpenDevice = DUMMYAUDIO_OpenDevice; + impl->CloseDevice = DUMMYAUDIO_CloseDevice; + impl->WaitDevice = DUMMYAUDIO_WaitDevice; + impl->GetDeviceBuf = DUMMYAUDIO_GetDeviceBuf; + impl->WaitRecordingDevice = DUMMYAUDIO_WaitDevice; + impl->RecordDevice = DUMMYAUDIO_RecordDevice; + + impl->OnlyHasDefaultPlaybackDevice = true; + impl->OnlyHasDefaultRecordingDevice = true; + impl->HasRecordingSupport = true; + + // on Emscripten without threads, we just fire a repeating timer to consume audio. + #if defined(SDL_PLATFORM_EMSCRIPTEN) && !defined(__EMSCRIPTEN_PTHREADS__) + MAIN_THREAD_EM_ASM({ + if (typeof(Module['SDL3']) === 'undefined') { + Module['SDL3'] = {}; + } + Module['SDL3'].dummy_audio = {}; + Module['SDL3'].dummy_audio.timers = []; + Module['SDL3'].dummy_audio.timers[0] = undefined; + Module['SDL3'].dummy_audio.timers[1] = undefined; + }); + impl->ProvidesOwnCallbackThread = true; + #endif + + return true; +} + +AudioBootStrap DUMMYAUDIO_bootstrap = { + "dummy", "SDL dummy audio driver", DUMMYAUDIO_Init, true, false +}; diff --git a/contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.h b/contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.h new file mode 100644 index 0000000..d5e75c7 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/dummy/SDL_dummyaudio.h @@ -0,0 +1,34 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_dummyaudio_h_ +#define SDL_dummyaudio_h_ + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + Uint8 *mixbuf; // The file descriptor for the audio device + Uint32 io_delay; // milliseconds to sleep in WaitDevice. +}; + +#endif // SDL_dummyaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/emscripten/SDL_emscriptenaudio.c b/contrib/SDL-3.2.8/src/audio/emscripten/SDL_emscriptenaudio.c new file mode 100644 index 0000000..55fb5b4 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/emscripten/SDL_emscriptenaudio.c @@ -0,0 +1,359 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_EMSCRIPTEN + +#include "../SDL_sysaudio.h" +#include "SDL_emscriptenaudio.h" + +#include + +// just turn off clang-format for this whole file, this INDENT_OFF stuff on +// each EM_ASM section is ugly. +/* *INDENT-OFF* */ // clang-format off + +static Uint8 *EMSCRIPTENAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->hidden->mixbuf; +} + +static bool EMSCRIPTENAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size) +{ + const int framelen = SDL_AUDIO_FRAMESIZE(device->spec); + MAIN_THREAD_EM_ASM({ + /* Convert incoming buf pointer to a HEAPF32 offset. */ + #ifdef __wasm64__ + var buf = $0 / 4; + #else + var buf = $0 >>> 2; + #endif + + var SDL3 = Module['SDL3']; + var numChannels = SDL3.audio_playback.currentPlaybackBuffer['numberOfChannels']; + for (var c = 0; c < numChannels; ++c) { + var channelData = SDL3.audio_playback.currentPlaybackBuffer['getChannelData'](c); + if (channelData.length != $1) { + throw 'Web Audio playback buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!'; + } + + for (var j = 0; j < $1; ++j) { + channelData[j] = HEAPF32[buf + (j*numChannels + c)]; + } + } + }, buffer, buffer_size / framelen); + return true; +} + + +static void EMSCRIPTENAUDIO_FlushRecording(SDL_AudioDevice *device) +{ + // Do nothing, the new data will just be dropped. +} + +static int EMSCRIPTENAUDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + MAIN_THREAD_EM_ASM({ + var SDL3 = Module['SDL3']; + var numChannels = SDL3.audio_recording.currentRecordingBuffer.numberOfChannels; + for (var c = 0; c < numChannels; ++c) { + var channelData = SDL3.audio_recording.currentRecordingBuffer.getChannelData(c); + if (channelData.length != $1) { + throw 'Web Audio recording buffer length mismatch! Destination size: ' + channelData.length + ' samples vs expected ' + $1 + ' samples!'; + } + + if (numChannels == 1) { // fastpath this a little for the common (mono) case. + for (var j = 0; j < $1; ++j) { + setValue($0 + (j * 4), channelData[j], 'float'); + } + } else { + for (var j = 0; j < $1; ++j) { + setValue($0 + (((j * numChannels) + c) * 4), channelData[j], 'float'); + } + } + } + }, buffer, (buflen / sizeof(float)) / device->spec.channels); + + return buflen; +} + +static void EMSCRIPTENAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (!device->hidden) { + return; + } + + MAIN_THREAD_EM_ASM({ + var SDL3 = Module['SDL3']; + if ($0) { + if (SDL3.audio_recording.silenceTimer !== undefined) { + clearInterval(SDL3.audio_recording.silenceTimer); + } + if (SDL3.audio_recording.stream !== undefined) { + var tracks = SDL3.audio_recording.stream.getAudioTracks(); + for (var i = 0; i < tracks.length; i++) { + SDL3.audio_recording.stream.removeTrack(tracks[i]); + } + } + if (SDL3.audio_recording.scriptProcessorNode !== undefined) { + SDL3.audio_recording.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) {}; + SDL3.audio_recording.scriptProcessorNode.disconnect(); + } + if (SDL3.audio_recording.mediaStreamNode !== undefined) { + SDL3.audio_recording.mediaStreamNode.disconnect(); + } + SDL3.audio_recording = undefined; + } else { + if (SDL3.audio_playback.scriptProcessorNode != undefined) { + SDL3.audio_playback.scriptProcessorNode.disconnect(); + } + if (SDL3.audio_playback.silenceTimer !== undefined) { + clearInterval(SDL3.audio_playback.silenceTimer); + } + SDL3.audio_playback = undefined; + } + if ((SDL3.audioContext !== undefined) && (SDL3.audio_playback === undefined) && (SDL3.audio_recording === undefined)) { + SDL3.audioContext.close(); + SDL3.audioContext = undefined; + } + }, device->recording); + + SDL_free(device->hidden->mixbuf); + SDL_free(device->hidden); + device->hidden = NULL; + + SDL_AudioThreadFinalize(device); +} + +EM_JS_DEPS(sdlaudio, "$autoResumeAudioContext,$dynCall"); + +static bool EMSCRIPTENAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + // based on parts of library_sdl.js + + // create context + const bool result = MAIN_THREAD_EM_ASM_INT({ + if (typeof(Module['SDL3']) === 'undefined') { + Module['SDL3'] = {}; + } + var SDL3 = Module['SDL3']; + if (!$0) { + SDL3.audio_playback = {}; + } else { + SDL3.audio_recording = {}; + } + + if (!SDL3.audioContext) { + if (typeof(AudioContext) !== 'undefined') { + SDL3.audioContext = new AudioContext(); + } else if (typeof(webkitAudioContext) !== 'undefined') { + SDL3.audioContext = new webkitAudioContext(); + } + if (SDL3.audioContext) { + if ((typeof navigator.userActivation) === 'undefined') { + autoResumeAudioContext(SDL3.audioContext); + } + } + } + return (SDL3.audioContext !== undefined); + }, device->recording); + + if (!result) { + return SDL_SetError("Web Audio API is not available!"); + } + + device->spec.format = SDL_AUDIO_F32; // web audio only supports floats + + // Initialize all variables that we clean on shutdown + device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // limit to native freq + device->spec.freq = EM_ASM_INT({ return Module['SDL3'].audioContext.sampleRate; }); + device->sample_frames = SDL_GetDefaultSampleFramesFromFreq(device->spec.freq) * 2; // double the buffer size, some browsers need more, and we'll just have to live with the latency. + + SDL_UpdatedAudioDeviceFormat(device); + + if (!device->recording) { + device->hidden->mixbuf = (Uint8 *)SDL_malloc(device->buffer_size); + if (!device->hidden->mixbuf) { + return false; + } + SDL_memset(device->hidden->mixbuf, device->silence_value, device->buffer_size); + } + + if (device->recording) { + /* The idea is to take the recording media stream, hook it up to an + audio graph where we can pass it through a ScriptProcessorNode + to access the raw PCM samples and push them to the SDL app's + callback. From there, we "process" the audio data into silence + and forget about it. + + This should, strictly speaking, use MediaRecorder for recording, but + this API is cleaner to use and better supported, and fires a + callback whenever there's enough data to fire down into the app. + The downside is that we are spending CPU time silencing a buffer + that the audiocontext uselessly mixes into any playback. On the + upside, both of those things are not only run in native code in + the browser, they're probably SIMD code, too. MediaRecorder + feels like it's a pretty inefficient tapdance in similar ways, + to be honest. */ + + MAIN_THREAD_EM_ASM({ + var SDL3 = Module['SDL3']; + var have_microphone = function(stream) { + //console.log('SDL audio recording: we have a microphone! Replacing silence callback.'); + if (SDL3.audio_recording.silenceTimer !== undefined) { + clearInterval(SDL3.audio_recording.silenceTimer); + SDL3.audio_recording.silenceTimer = undefined; + SDL3.audio_recording.silenceBuffer = undefined + } + SDL3.audio_recording.mediaStreamNode = SDL3.audioContext.createMediaStreamSource(stream); + SDL3.audio_recording.scriptProcessorNode = SDL3.audioContext.createScriptProcessor($1, $0, 1); + SDL3.audio_recording.scriptProcessorNode.onaudioprocess = function(audioProcessingEvent) { + if ((SDL3 === undefined) || (SDL3.audio_recording === undefined)) { return; } + audioProcessingEvent.outputBuffer.getChannelData(0).fill(0.0); + SDL3.audio_recording.currentRecordingBuffer = audioProcessingEvent.inputBuffer; + dynCall('ip', $2, [$3]); + }; + SDL3.audio_recording.mediaStreamNode.connect(SDL3.audio_recording.scriptProcessorNode); + SDL3.audio_recording.scriptProcessorNode.connect(SDL3.audioContext.destination); + SDL3.audio_recording.stream = stream; + }; + + var no_microphone = function(error) { + //console.log('SDL audio recording: we DO NOT have a microphone! (' + error.name + ')...leaving silence callback running.'); + }; + + // we write silence to the audio callback until the microphone is available (user approves use, etc). + SDL3.audio_recording.silenceBuffer = SDL3.audioContext.createBuffer($0, $1, SDL3.audioContext.sampleRate); + SDL3.audio_recording.silenceBuffer.getChannelData(0).fill(0.0); + var silence_callback = function() { + SDL3.audio_recording.currentRecordingBuffer = SDL3.audio_recording.silenceBuffer; + dynCall('ip', $2, [$3]); + }; + + SDL3.audio_recording.silenceTimer = setInterval(silence_callback, ($1 / SDL3.audioContext.sampleRate) * 1000); + + if ((navigator.mediaDevices !== undefined) && (navigator.mediaDevices.getUserMedia !== undefined)) { + navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(have_microphone).catch(no_microphone); + } else if (navigator.webkitGetUserMedia !== undefined) { + navigator.webkitGetUserMedia({ audio: true, video: false }, have_microphone, no_microphone); + } + }, device->spec.channels, device->sample_frames, SDL_RecordingAudioThreadIterate, device); + } else { + // setup a ScriptProcessorNode + MAIN_THREAD_EM_ASM({ + var SDL3 = Module['SDL3']; + SDL3.audio_playback.scriptProcessorNode = SDL3.audioContext['createScriptProcessor']($1, 0, $0); + SDL3.audio_playback.scriptProcessorNode['onaudioprocess'] = function (e) { + if ((SDL3 === undefined) || (SDL3.audio_playback === undefined)) { return; } + // if we're actually running the node, we don't need the fake callback anymore, so kill it. + if (SDL3.audio_playback.silenceTimer !== undefined) { + clearInterval(SDL3.audio_playback.silenceTimer); + SDL3.audio_playback.silenceTimer = undefined; + SDL3.audio_playback.silenceBuffer = undefined; + } + SDL3.audio_playback.currentPlaybackBuffer = e['outputBuffer']; + dynCall('ip', $2, [$3]); + }; + + SDL3.audio_playback.scriptProcessorNode['connect'](SDL3.audioContext['destination']); + + if (SDL3.audioContext.state === 'suspended') { // uhoh, autoplay is blocked. + SDL3.audio_playback.silenceBuffer = SDL3.audioContext.createBuffer($0, $1, SDL3.audioContext.sampleRate); + SDL3.audio_playback.silenceBuffer.getChannelData(0).fill(0.0); + var silence_callback = function() { + if ((typeof navigator.userActivation) !== 'undefined') { + if (navigator.userActivation.hasBeenActive) { + SDL3.audioContext.resume(); + } + } + + // the buffer that gets filled here just gets ignored, so the app can make progress + // and/or avoid flooding audio queues until we can actually play audio. + SDL3.audio_playback.currentPlaybackBuffer = SDL3.audio_playback.silenceBuffer; + dynCall('ip', $2, [$3]); + SDL3.audio_playback.currentPlaybackBuffer = undefined; + }; + + SDL3.audio_playback.silenceTimer = setInterval(silence_callback, ($1 / SDL3.audioContext.sampleRate) * 1000); + } + }, device->spec.channels, device->sample_frames, SDL_PlaybackAudioThreadIterate, device); + } + + return true; +} + +static bool EMSCRIPTENAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + bool available, recording_available; + + impl->OpenDevice = EMSCRIPTENAUDIO_OpenDevice; + impl->CloseDevice = EMSCRIPTENAUDIO_CloseDevice; + impl->GetDeviceBuf = EMSCRIPTENAUDIO_GetDeviceBuf; + impl->PlayDevice = EMSCRIPTENAUDIO_PlayDevice; + impl->FlushRecording = EMSCRIPTENAUDIO_FlushRecording; + impl->RecordDevice = EMSCRIPTENAUDIO_RecordDevice; + + impl->OnlyHasDefaultPlaybackDevice = true; + + // technically, this is just runs in idle time in the main thread, but it's close enough to a "thread" for our purposes. + impl->ProvidesOwnCallbackThread = true; + + // check availability + available = MAIN_THREAD_EM_ASM_INT({ + if (typeof(AudioContext) !== 'undefined') { + return true; + } else if (typeof(webkitAudioContext) !== 'undefined') { + return true; + } + return false; + }); + + if (!available) { + SDL_SetError("No audio context available"); + } + + recording_available = available && MAIN_THREAD_EM_ASM_INT({ + if ((typeof(navigator.mediaDevices) !== 'undefined') && (typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')) { + return true; + } else if (typeof(navigator.webkitGetUserMedia) !== 'undefined') { + return true; + } + return false; + }); + + impl->HasRecordingSupport = recording_available; + impl->OnlyHasDefaultRecordingDevice = recording_available; + + return available; +} + +AudioBootStrap EMSCRIPTENAUDIO_bootstrap = { + "emscripten", "SDL emscripten audio driver", EMSCRIPTENAUDIO_Init, false, false +}; + +/* *INDENT-ON* */ // clang-format on + +#endif // SDL_AUDIO_DRIVER_EMSCRIPTEN diff --git a/contrib/SDL-3.2.8/src/audio/emscripten/SDL_emscriptenaudio.h b/contrib/SDL-3.2.8/src/audio/emscripten/SDL_emscriptenaudio.h new file mode 100644 index 0000000..aaf761b --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/emscripten/SDL_emscriptenaudio.h @@ -0,0 +1,33 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_emscriptenaudio_h_ +#define SDL_emscriptenaudio_h_ + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + Uint8 *mixbuf; +}; + +#endif // SDL_emscriptenaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.cc b/contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.cc new file mode 100644 index 0000000..730b107 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.cc @@ -0,0 +1,222 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_HAIKU + +// Allow access to the audio stream on Haiku + +#include +#include + +#include "../../core/haiku/SDL_BeApp.h" + +extern "C" +{ + +#include "../SDL_sysaudio.h" +#include "SDL_haikuaudio.h" + +} + +static Uint8 *HAIKUAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + SDL_assert(device->hidden->current_buffer != NULL); + SDL_assert(device->hidden->current_buffer_len > 0); + *buffer_size = device->hidden->current_buffer_len; + return device->hidden->current_buffer; +} + +static bool HAIKUAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size) +{ + // We already wrote our output right into the BSoundPlayer's callback's stream. Just clean up our stuff. + SDL_assert(device->hidden->current_buffer != NULL); + SDL_assert(device->hidden->current_buffer_len > 0); + device->hidden->current_buffer = NULL; + device->hidden->current_buffer_len = 0; + return true; +} + +// The Haiku callback for handling the audio buffer +static void FillSound(void *data, void *stream, size_t len, const media_raw_audio_format & format) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *)data; + SDL_assert(device->hidden->current_buffer == NULL); + SDL_assert(device->hidden->current_buffer_len == 0); + device->hidden->current_buffer = (Uint8 *) stream; + device->hidden->current_buffer_len = (int) len; + SDL_PlaybackAudioThreadIterate(device); +} + +static void HAIKUAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->audio_obj) { + device->hidden->audio_obj->Stop(); + delete device->hidden->audio_obj; + } + delete device->hidden; + device->hidden = NULL; + SDL_AudioThreadFinalize(device); + } +} + + +static const int sig_list[] = { + SIGHUP, SIGINT, SIGQUIT, SIGPIPE, SIGALRM, SIGTERM, SIGWINCH, 0 +}; + +static inline void MaskSignals(sigset_t * omask) +{ + sigset_t mask; + int i; + + sigemptyset(&mask); + for (i = 0; sig_list[i]; ++i) { + sigaddset(&mask, sig_list[i]); + } + sigprocmask(SIG_BLOCK, &mask, omask); +} + +static inline void UnmaskSignals(sigset_t * omask) +{ + sigprocmask(SIG_SETMASK, omask, NULL); +} + + +static bool HAIKUAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + // Initialize all variables that we clean on shutdown + device->hidden = new SDL_PrivateAudioData; + if (!device->hidden) { + return false; + } + SDL_zerop(device->hidden); + + // Parse the audio format and fill the Be raw audio format + media_raw_audio_format format; + SDL_zero(format); + format.byte_order = B_MEDIA_LITTLE_ENDIAN; + format.frame_rate = (float) device->spec.freq; + format.channel_count = device->spec.channels; // !!! FIXME: support > 2? + + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { + switch (test_format) { + case SDL_AUDIO_S8: + format.format = media_raw_audio_format::B_AUDIO_CHAR; + break; + + case SDL_AUDIO_U8: + format.format = media_raw_audio_format::B_AUDIO_UCHAR; + break; + + case SDL_AUDIO_S16LE: + format.format = media_raw_audio_format::B_AUDIO_SHORT; + break; + + case SDL_AUDIO_S16BE: + format.format = media_raw_audio_format::B_AUDIO_SHORT; + format.byte_order = B_MEDIA_BIG_ENDIAN; + break; + + case SDL_AUDIO_S32LE: + format.format = media_raw_audio_format::B_AUDIO_INT; + break; + + case SDL_AUDIO_S32BE: + format.format = media_raw_audio_format::B_AUDIO_INT; + format.byte_order = B_MEDIA_BIG_ENDIAN; + break; + + case SDL_AUDIO_F32LE: + format.format = media_raw_audio_format::B_AUDIO_FLOAT; + break; + + case SDL_AUDIO_F32BE: + format.format = media_raw_audio_format::B_AUDIO_FLOAT; + format.byte_order = B_MEDIA_BIG_ENDIAN; + break; + + default: + continue; + } + break; + } + + if (!test_format) { // shouldn't happen, but just in case... + return SDL_SetError("HAIKU: Unsupported audio format"); + } + device->spec.format = test_format; + + // Calculate the final parameters for this audio specification + SDL_UpdatedAudioDeviceFormat(device); + + format.buffer_size = device->buffer_size; + + // Subscribe to the audio stream (creates a new thread) + sigset_t omask; + MaskSignals(&omask); + device->hidden->audio_obj = new BSoundPlayer(&format, "SDL Audio", + FillSound, NULL, device); + UnmaskSignals(&omask); + + if (device->hidden->audio_obj->Start() == B_NO_ERROR) { + device->hidden->audio_obj->SetHasData(true); + } else { + return SDL_SetError("Unable to start Haiku audio"); + } + + return true; // We're running! +} + +static void HAIKUAUDIO_Deinitialize(void) +{ + SDL_QuitBeApp(); +} + +static bool HAIKUAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + if (!SDL_InitBeApp()) { + return false; + } + + // Set the function pointers + impl->OpenDevice = HAIKUAUDIO_OpenDevice; + impl->GetDeviceBuf = HAIKUAUDIO_GetDeviceBuf; + impl->PlayDevice = HAIKUAUDIO_PlayDevice; + impl->CloseDevice = HAIKUAUDIO_CloseDevice; + impl->Deinitialize = HAIKUAUDIO_Deinitialize; + impl->ProvidesOwnCallbackThread = true; + impl->OnlyHasDefaultPlaybackDevice = true; + + return true; +} + + +extern "C" { extern AudioBootStrap HAIKUAUDIO_bootstrap; } + +AudioBootStrap HAIKUAUDIO_bootstrap = { + "haiku", "Haiku BSoundPlayer", HAIKUAUDIO_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_HAIKU diff --git a/contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.h b/contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.h new file mode 100644 index 0000000..9547546 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/haiku/SDL_haikuaudio.h @@ -0,0 +1,35 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_haikuaudio_h_ +#define SDL_haikuaudio_h_ + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + BSoundPlayer *audio_obj; + Uint8 *current_buffer; + int current_buffer_len; +}; + +#endif // SDL_haikuaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.c b/contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.c new file mode 100644 index 0000000..3ae5137 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.c @@ -0,0 +1,435 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_JACK + +#include "../SDL_sysaudio.h" +#include "SDL_jackaudio.h" +#include "../../thread/SDL_systhread.h" + +static jack_client_t *(*JACK_jack_client_open)(const char *, jack_options_t, jack_status_t *, ...); +static int (*JACK_jack_client_close)(jack_client_t *); +static void (*JACK_jack_on_shutdown)(jack_client_t *, JackShutdownCallback, void *); +static int (*JACK_jack_activate)(jack_client_t *); +static int (*JACK_jack_deactivate)(jack_client_t *); +static void *(*JACK_jack_port_get_buffer)(jack_port_t *, jack_nframes_t); +static int (*JACK_jack_port_unregister)(jack_client_t *, jack_port_t *); +static void (*JACK_jack_free)(void *); +static const char **(*JACK_jack_get_ports)(jack_client_t *, const char *, const char *, unsigned long); +static jack_nframes_t (*JACK_jack_get_sample_rate)(jack_client_t *); +static jack_nframes_t (*JACK_jack_get_buffer_size)(jack_client_t *); +static jack_port_t *(*JACK_jack_port_register)(jack_client_t *, const char *, const char *, unsigned long, unsigned long); +static jack_port_t *(*JACK_jack_port_by_name)(jack_client_t *, const char *); +static const char *(*JACK_jack_port_name)(const jack_port_t *); +static const char *(*JACK_jack_port_type)(const jack_port_t *); +static int (*JACK_jack_connect)(jack_client_t *, const char *, const char *); +static int (*JACK_jack_set_process_callback)(jack_client_t *, JackProcessCallback, void *); +static int (*JACK_jack_set_sample_rate_callback)(jack_client_t *, JackSampleRateCallback, void *); +static int (*JACK_jack_set_buffer_size_callback)(jack_client_t *, JackBufferSizeCallback, void *); + +static bool load_jack_syms(void); + +#ifdef SDL_AUDIO_DRIVER_JACK_DYNAMIC + +static const char *jack_library = SDL_AUDIO_DRIVER_JACK_DYNAMIC; +static SDL_SharedObject *jack_handle = NULL; + +// !!! FIXME: this is copy/pasted in several places now +static bool load_jack_sym(const char *fn, void **addr) +{ + *addr = SDL_LoadFunction(jack_handle, fn); + if (!*addr) { + // Don't call SDL_SetError(): SDL_LoadFunction already did. + return false; + } + + return true; +} + +// cast funcs to char* first, to please GCC's strict aliasing rules. +#define SDL_JACK_SYM(x) \ + if (!load_jack_sym(#x, (void **)(char *)&JACK_##x)) \ + return false + +static void UnloadJackLibrary(void) +{ + if (jack_handle) { + SDL_UnloadObject(jack_handle); + jack_handle = NULL; + } +} + +static bool LoadJackLibrary(void) +{ + bool result = true; + if (!jack_handle) { + jack_handle = SDL_LoadObject(jack_library); + if (!jack_handle) { + result = false; + // Don't call SDL_SetError(): SDL_LoadObject already did. + } else { + result = load_jack_syms(); + if (!result) { + UnloadJackLibrary(); + } + } + } + return result; +} + +#else + +#define SDL_JACK_SYM(x) JACK_##x = x + +static void UnloadJackLibrary(void) +{ +} + +static bool LoadJackLibrary(void) +{ + load_jack_syms(); + return true; +} + +#endif // SDL_AUDIO_DRIVER_JACK_DYNAMIC + +static bool load_jack_syms(void) +{ + SDL_JACK_SYM(jack_client_open); + SDL_JACK_SYM(jack_client_close); + SDL_JACK_SYM(jack_on_shutdown); + SDL_JACK_SYM(jack_activate); + SDL_JACK_SYM(jack_deactivate); + SDL_JACK_SYM(jack_port_get_buffer); + SDL_JACK_SYM(jack_port_unregister); + SDL_JACK_SYM(jack_free); + SDL_JACK_SYM(jack_get_ports); + SDL_JACK_SYM(jack_get_sample_rate); + SDL_JACK_SYM(jack_get_buffer_size); + SDL_JACK_SYM(jack_port_register); + SDL_JACK_SYM(jack_port_by_name); + SDL_JACK_SYM(jack_port_name); + SDL_JACK_SYM(jack_port_type); + SDL_JACK_SYM(jack_connect); + SDL_JACK_SYM(jack_set_process_callback); + SDL_JACK_SYM(jack_set_sample_rate_callback); + SDL_JACK_SYM(jack_set_buffer_size_callback); + + return true; +} + +static void jackShutdownCallback(void *arg) // JACK went away; device is lost. +{ + SDL_AudioDeviceDisconnected((SDL_AudioDevice *)arg); +} + +static int jackSampleRateCallback(jack_nframes_t nframes, void *arg) +{ + //SDL_Log("JACK Sample Rate Callback! %d", (int) nframes); + SDL_AudioDevice *device = (SDL_AudioDevice *) arg; + SDL_AudioSpec newspec; + SDL_copyp(&newspec, &device->spec); + newspec.freq = (int) nframes; + if (!SDL_AudioDeviceFormatChanged(device, &newspec, device->sample_frames)) { + SDL_AudioDeviceDisconnected(device); + } + return 0; +} + +static int jackBufferSizeCallback(jack_nframes_t nframes, void *arg) +{ + //SDL_Log("JACK Buffer Size Callback! %d", (int) nframes); + SDL_AudioDevice *device = (SDL_AudioDevice *) arg; + SDL_AudioSpec newspec; + SDL_copyp(&newspec, &device->spec); + if (!SDL_AudioDeviceFormatChanged(device, &newspec, (int) nframes)) { + SDL_AudioDeviceDisconnected(device); + } + return 0; +} + +static int jackProcessPlaybackCallback(jack_nframes_t nframes, void *arg) +{ + SDL_assert(nframes == ((SDL_AudioDevice *)arg)->sample_frames); + SDL_PlaybackAudioThreadIterate((SDL_AudioDevice *)arg); + return 0; +} + +static bool JACK_PlayDevice(SDL_AudioDevice *device, const Uint8 *ui8buffer, int buflen) +{ + const float *buffer = (float *) ui8buffer; + jack_port_t **ports = device->hidden->sdlports; + const int total_channels = device->spec.channels; + const int total_frames = device->sample_frames; + const jack_nframes_t nframes = (jack_nframes_t) device->sample_frames; + + for (int channelsi = 0; channelsi < total_channels; channelsi++) { + float *dst = (float *)JACK_jack_port_get_buffer(ports[channelsi], nframes); + if (dst) { + const float *src = buffer + channelsi; + for (int framesi = 0; framesi < total_frames; framesi++) { + *(dst++) = *src; + src += total_channels; + } + } + } + + return true; +} + +static Uint8 *JACK_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return (Uint8 *)device->hidden->iobuffer; +} + +static int jackProcessRecordingCallback(jack_nframes_t nframes, void *arg) +{ + SDL_assert(nframes == ((SDL_AudioDevice *)arg)->sample_frames); + SDL_RecordingAudioThreadIterate((SDL_AudioDevice *)arg); + return 0; +} + +static int JACK_RecordDevice(SDL_AudioDevice *device, void *vbuffer, int buflen) +{ + float *buffer = (float *) vbuffer; + jack_port_t **ports = device->hidden->sdlports; + const int total_channels = device->spec.channels; + const int total_frames = device->sample_frames; + const jack_nframes_t nframes = (jack_nframes_t) device->sample_frames; + + for (int channelsi = 0; channelsi < total_channels; channelsi++) { + const float *src = (const float *)JACK_jack_port_get_buffer(ports[channelsi], nframes); + if (src) { + float *dst = buffer + channelsi; + for (int framesi = 0; framesi < total_frames; framesi++) { + *dst = *(src++); + dst += total_channels; + } + } + } + + return buflen; +} + +static void JACK_FlushRecording(SDL_AudioDevice *device) +{ + // do nothing, the data will just be replaced next callback. +} + +static void JACK_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->client) { + JACK_jack_deactivate(device->hidden->client); + + if (device->hidden->sdlports) { + const int channels = device->spec.channels; + int i; + for (i = 0; i < channels; i++) { + JACK_jack_port_unregister(device->hidden->client, device->hidden->sdlports[i]); + } + SDL_free(device->hidden->sdlports); + } + + JACK_jack_client_close(device->hidden->client); + } + + SDL_free(device->hidden->iobuffer); + SDL_free(device->hidden); + device->hidden = NULL; + + SDL_AudioThreadFinalize(device); + } +} + +// !!! FIXME: unify this (PulseAudio has a getAppName, Pipewire has a thing, etc) +static const char *GetJackAppName(void) +{ + return SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING); +} + +static bool JACK_OpenDevice(SDL_AudioDevice *device) +{ + /* Note that JACK uses "output" for recording devices (they output audio + data to us) and "input" for playback (we input audio data to them). + Likewise, SDL's playback port will be "output" (we write data out) + and recording will be "input" (we read data in). */ + const bool recording = device->recording; + const unsigned long sysportflags = recording ? JackPortIsOutput : JackPortIsInput; + const unsigned long sdlportflags = recording ? JackPortIsInput : JackPortIsOutput; + const JackProcessCallback callback = recording ? jackProcessRecordingCallback : jackProcessPlaybackCallback; + const char *sdlportstr = recording ? "input" : "output"; + const char **devports = NULL; + int *audio_ports; + jack_client_t *client = NULL; + jack_status_t status; + int channels = 0; + int ports = 0; + int i; + + // Initialize all variables that we clean on shutdown + device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + client = JACK_jack_client_open(GetJackAppName(), JackNoStartServer, &status, NULL); + device->hidden->client = client; + if (!client) { + return SDL_SetError("Can't open JACK client"); + } + + devports = JACK_jack_get_ports(client, NULL, NULL, JackPortIsPhysical | sysportflags); + if (!devports || !devports[0]) { + return SDL_SetError("No physical JACK ports available"); + } + + while (devports[++ports]) { + // spin to count devports + } + + // Filter out non-audio ports + audio_ports = SDL_calloc(ports, sizeof(*audio_ports)); + for (i = 0; i < ports; i++) { + const jack_port_t *dport = JACK_jack_port_by_name(client, devports[i]); + const char *type = JACK_jack_port_type(dport); + const int len = SDL_strlen(type); + // See if type ends with "audio" + if (len >= 5 && !SDL_memcmp(type + len - 5, "audio", 5)) { + audio_ports[channels++] = i; + } + } + if (channels == 0) { + SDL_free(audio_ports); + return SDL_SetError("No physical JACK ports available"); + } + + // Jack pretty much demands what it wants. + device->spec.format = SDL_AUDIO_F32; + device->spec.freq = JACK_jack_get_sample_rate(client); + device->spec.channels = channels; + device->sample_frames = JACK_jack_get_buffer_size(client); + + SDL_UpdatedAudioDeviceFormat(device); + + if (!recording) { + device->hidden->iobuffer = (float *)SDL_calloc(1, device->buffer_size); + if (!device->hidden->iobuffer) { + SDL_free(audio_ports); + return false; + } + } + + // Build SDL's ports, which we will connect to the device ports. + device->hidden->sdlports = (jack_port_t **)SDL_calloc(channels, sizeof(jack_port_t *)); + if (!device->hidden->sdlports) { + SDL_free(audio_ports); + return false; + } + + for (i = 0; i < channels; i++) { + char portname[32]; + (void)SDL_snprintf(portname, sizeof(portname), "sdl_jack_%s_%d", sdlportstr, i); + device->hidden->sdlports[i] = JACK_jack_port_register(client, portname, JACK_DEFAULT_AUDIO_TYPE, sdlportflags, 0); + if (device->hidden->sdlports[i] == NULL) { + SDL_free(audio_ports); + return SDL_SetError("jack_port_register failed"); + } + } + + if (JACK_jack_set_buffer_size_callback(client, jackBufferSizeCallback, device) != 0) { + SDL_free(audio_ports); + return SDL_SetError("JACK: Couldn't set buffer size callback"); + } else if (JACK_jack_set_sample_rate_callback(client, jackSampleRateCallback, device) != 0) { + SDL_free(audio_ports); + return SDL_SetError("JACK: Couldn't set sample rate callback"); + } else if (JACK_jack_set_process_callback(client, callback, device) != 0) { + SDL_free(audio_ports); + return SDL_SetError("JACK: Couldn't set process callback"); + } + + JACK_jack_on_shutdown(client, jackShutdownCallback, device); + + if (JACK_jack_activate(client) != 0) { + SDL_free(audio_ports); + return SDL_SetError("Failed to activate JACK client"); + } + + // once activated, we can connect all the ports. + for (i = 0; i < channels; i++) { + const char *sdlport = JACK_jack_port_name(device->hidden->sdlports[i]); + const char *srcport = recording ? devports[audio_ports[i]] : sdlport; + const char *dstport = recording ? sdlport : devports[audio_ports[i]]; + if (JACK_jack_connect(client, srcport, dstport) != 0) { + SDL_free(audio_ports); + return SDL_SetError("Couldn't connect JACK ports: %s => %s", srcport, dstport); + } + } + + // don't need these anymore. + JACK_jack_free(devports); + SDL_free(audio_ports); + + // We're ready to rock and roll. :-) + return true; +} + +static void JACK_Deinitialize(void) +{ + UnloadJackLibrary(); +} + +static bool JACK_Init(SDL_AudioDriverImpl *impl) +{ + if (!LoadJackLibrary()) { + return false; + } else { + // Make sure a JACK server is running and available. + jack_status_t status; + jack_client_t *client = JACK_jack_client_open("SDL", JackNoStartServer, &status, NULL); + if (!client) { + UnloadJackLibrary(); + return SDL_SetError("Can't open JACK client"); + } + JACK_jack_client_close(client); + } + + impl->OpenDevice = JACK_OpenDevice; + impl->GetDeviceBuf = JACK_GetDeviceBuf; + impl->PlayDevice = JACK_PlayDevice; + impl->CloseDevice = JACK_CloseDevice; + impl->Deinitialize = JACK_Deinitialize; + impl->RecordDevice = JACK_RecordDevice; + impl->FlushRecording = JACK_FlushRecording; + impl->OnlyHasDefaultPlaybackDevice = true; + impl->OnlyHasDefaultRecordingDevice = true; + impl->HasRecordingSupport = true; + impl->ProvidesOwnCallbackThread = true; + + return true; +} + +AudioBootStrap JACK_bootstrap = { + "jack", "JACK Audio Connection Kit", JACK_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_JACK diff --git a/contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.h b/contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.h new file mode 100644 index 0000000..4f972d5 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/jack/SDL_jackaudio.h @@ -0,0 +1,35 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#ifndef SDL_jackaudio_h_ +#define SDL_jackaudio_h_ + +#include + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + jack_client_t *client; + jack_port_t **sdlports; + float *iobuffer; +}; + +#endif // SDL_jackaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.c b/contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.c new file mode 100644 index 0000000..780b06c --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.c @@ -0,0 +1,287 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_N3DS + +// N3DS Audio driver + +#include "../SDL_sysaudio.h" +#include "SDL_n3dsaudio.h" + +#define N3DSAUDIO_DRIVER_NAME "n3ds" + +static dspHookCookie dsp_hook; +static SDL_AudioDevice *audio_device; + +// fully local functions related to the wavebufs / DSP, not the same as the `device->lock` SDL_Mutex! +static SDL_INLINE void contextLock(SDL_AudioDevice *device) +{ + LightLock_Lock(&device->hidden->lock); +} + +static SDL_INLINE void contextUnlock(SDL_AudioDevice *device) +{ + LightLock_Unlock(&device->hidden->lock); +} + +static void N3DSAUD_DspHook(DSP_HookType hook) +{ + if (hook == DSPHOOK_ONCANCEL) { + contextLock(audio_device); + audio_device->hidden->isCancelled = true; + SDL_AudioDeviceDisconnected(audio_device); + CondVar_Broadcast(&audio_device->hidden->cv); + contextUnlock(audio_device); + } +} + +static void AudioFrameFinished(void *vdevice) +{ + bool shouldBroadcast = false; + unsigned i; + SDL_AudioDevice *device = (SDL_AudioDevice *)vdevice; + + contextLock(device); + + for (i = 0; i < NUM_BUFFERS; i++) { + if (device->hidden->waveBuf[i].status == NDSP_WBUF_DONE) { + device->hidden->waveBuf[i].status = NDSP_WBUF_FREE; + shouldBroadcast = true; + } + } + + if (shouldBroadcast) { + CondVar_Broadcast(&device->hidden->cv); + } + + contextUnlock(device); +} + +static bool N3DSAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + Result ndsp_init_res; + Uint8 *data_vaddr; + float mix[12]; + + device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // Initialise the DSP service + ndsp_init_res = ndspInit(); + if (R_FAILED(ndsp_init_res)) { + if ((R_SUMMARY(ndsp_init_res) == RS_NOTFOUND) && (R_MODULE(ndsp_init_res) == RM_DSP)) { + return SDL_SetError("DSP init failed: dspfirm.cdc missing!"); + } else { + return SDL_SetError("DSP init failed. Error code: 0x%lX", ndsp_init_res); + } + } + + // Initialise internal state + LightLock_Init(&device->hidden->lock); + CondVar_Init(&device->hidden->cv); + + if (device->spec.channels > 2) { + device->spec.channels = 2; + } + + Uint32 format = 0; + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { + if (test_format == SDL_AUDIO_S8) { // Signed 8-bit audio supported + format = (device->spec.channels == 2) ? NDSP_FORMAT_STEREO_PCM8 : NDSP_FORMAT_MONO_PCM8; + break; + } else if (test_format == SDL_AUDIO_S16) { // Signed 16-bit audio supported + format = (device->spec.channels == 2) ? NDSP_FORMAT_STEREO_PCM16 : NDSP_FORMAT_MONO_PCM16; + break; + } + } + + if (!test_format) { // shouldn't happen, but just in case... + return SDL_SetError("No supported audio format found."); + } + + device->spec.format = test_format; + + // Update the fragment size as size in bytes + SDL_UpdatedAudioDeviceFormat(device); + + // Allocate mixing buffer + if (device->buffer_size >= SDL_MAX_UINT32 / 2) { + return SDL_SetError("Mixing buffer is too large."); + } + + device->hidden->mixbuf = (Uint8 *)SDL_malloc(device->buffer_size); + if (!device->hidden->mixbuf) { + return false; + } + + SDL_memset(device->hidden->mixbuf, device->silence_value, device->buffer_size); + + data_vaddr = (Uint8 *)linearAlloc(device->buffer_size * NUM_BUFFERS); + if (!data_vaddr) { + return SDL_OutOfMemory(); + } + + SDL_memset(data_vaddr, 0, device->buffer_size * NUM_BUFFERS); + DSP_FlushDataCache(data_vaddr, device->buffer_size * NUM_BUFFERS); + + device->hidden->nextbuf = 0; + + ndspChnReset(0); + + ndspChnSetInterp(0, NDSP_INTERP_LINEAR); + ndspChnSetRate(0, device->spec.freq); + ndspChnSetFormat(0, format); + + SDL_zeroa(mix); + mix[0] = mix[1] = 1.0f; + ndspChnSetMix(0, mix); + + SDL_memset(device->hidden->waveBuf, 0, sizeof(ndspWaveBuf) * NUM_BUFFERS); + + const int sample_frame_size = SDL_AUDIO_FRAMESIZE(device->spec); + for (unsigned i = 0; i < NUM_BUFFERS; i++) { + device->hidden->waveBuf[i].data_vaddr = data_vaddr; + device->hidden->waveBuf[i].nsamples = device->buffer_size / sample_frame_size; + data_vaddr += device->buffer_size; + } + + // Setup callback + audio_device = device; + ndspSetCallback(AudioFrameFinished, device); + dspHook(&dsp_hook, N3DSAUD_DspHook); + + return true; +} + +static bool N3DSAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + contextLock(device); + + const size_t nextbuf = device->hidden->nextbuf; + + if (device->hidden->isCancelled || + device->hidden->waveBuf[nextbuf].status != NDSP_WBUF_FREE) { + contextUnlock(device); + return true; // !!! FIXME: is this a fatal error? If so, this should return false. + } + + device->hidden->nextbuf = (nextbuf + 1) % NUM_BUFFERS; + + contextUnlock(device); + + SDL_memcpy((void *)device->hidden->waveBuf[nextbuf].data_vaddr, buffer, buflen); + DSP_FlushDataCache(device->hidden->waveBuf[nextbuf].data_vaddr, buflen); + + ndspChnWaveBufAdd(0, &device->hidden->waveBuf[nextbuf]); + + return true; +} + +static bool N3DSAUDIO_WaitDevice(SDL_AudioDevice *device) +{ + contextLock(device); + while (!device->hidden->isCancelled && !SDL_GetAtomicInt(&device->shutdown) && + device->hidden->waveBuf[device->hidden->nextbuf].status != NDSP_WBUF_FREE) { + CondVar_Wait(&device->hidden->cv, &device->hidden->lock); + } + contextUnlock(device); + return true; +} + +static Uint8 *N3DSAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->hidden->mixbuf; +} + +static void N3DSAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (!device->hidden) { + return; + } + + contextLock(device); + + dspUnhook(&dsp_hook); + ndspSetCallback(NULL, NULL); + + if (!device->hidden->isCancelled) { + ndspChnReset(0); + SDL_memset(device->hidden->waveBuf, 0, sizeof(ndspWaveBuf) * NUM_BUFFERS); + CondVar_Broadcast(&device->hidden->cv); + } + + contextUnlock(device); + + ndspExit(); + + if (device->hidden->waveBuf[0].data_vaddr) { + linearFree((void *)device->hidden->waveBuf[0].data_vaddr); + } + + if (device->hidden->mixbuf) { + SDL_free(device->hidden->mixbuf); + device->hidden->mixbuf = NULL; + } + + SDL_free(device->hidden); + device->hidden = NULL; +} + +static void N3DSAUDIO_ThreadInit(SDL_AudioDevice *device) +{ + s32 current_priority = 0x30; + svcGetThreadPriority(¤t_priority, CUR_THREAD_HANDLE); + current_priority--; + // 0x18 is reserved for video, 0x30 is the default for main thread + current_priority = SDL_clamp(current_priority, 0x19, 0x2F); + svcSetThreadPriority(CUR_THREAD_HANDLE, current_priority); +} + +static bool N3DSAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + impl->OpenDevice = N3DSAUDIO_OpenDevice; + impl->PlayDevice = N3DSAUDIO_PlayDevice; + impl->WaitDevice = N3DSAUDIO_WaitDevice; + impl->GetDeviceBuf = N3DSAUDIO_GetDeviceBuf; + impl->CloseDevice = N3DSAUDIO_CloseDevice; + impl->ThreadInit = N3DSAUDIO_ThreadInit; + impl->OnlyHasDefaultPlaybackDevice = true; + + // Should be possible, but micInit would fail + impl->HasRecordingSupport = false; + + return true; +} + +AudioBootStrap N3DSAUDIO_bootstrap = { + N3DSAUDIO_DRIVER_NAME, + "SDL N3DS audio driver", + N3DSAUDIO_Init, + false, + false +}; + +#endif // SDL_AUDIO_DRIVER_N3DS diff --git a/contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.h b/contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.h new file mode 100644 index 0000000..c9ae4f8 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/n3ds/SDL_n3dsaudio.h @@ -0,0 +1,40 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef SDL_n3dsaudio_h +#define SDL_n3dsaudio_h + +#include <3ds.h> + +#define NUM_BUFFERS 3 // -- Minimum 2! + +struct SDL_PrivateAudioData +{ + // Speaker data + Uint8 *mixbuf; + Uint32 nextbuf; + ndspWaveBuf waveBuf[NUM_BUFFERS]; + LightLock lock; + CondVar cv; + bool isCancelled; +}; + +#endif // SDL_n3dsaudio_h diff --git a/contrib/SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.c b/contrib/SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.c new file mode 100644 index 0000000..26060d3 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.c @@ -0,0 +1,328 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_NETBSD + +// Driver for native NetBSD audio(4). + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/unix/SDL_poll.h" +#include "../SDL_audiodev_c.h" +#include "SDL_netbsdaudio.h" + +//#define DEBUG_AUDIO + +static void NETBSDAUDIO_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + SDL_EnumUnixAudioDevices(false, NULL); +} + +static void NETBSDAUDIO_Status(SDL_AudioDevice *device) +{ +#ifdef DEBUG_AUDIO + /* *INDENT-OFF* */ // clang-format off + audio_info_t info; + const struct audio_prinfo *prinfo; + + if (ioctl(device->hidden->audio_fd, AUDIO_GETINFO, &info) < 0) { + fprintf(stderr, "AUDIO_GETINFO failed.\n"); + return; + } + + prinfo = device->recording ? &info.record : &info.play; + + fprintf(stderr, "\n" + "[%s info]\n" + "buffer size : %d bytes\n" + "sample rate : %i Hz\n" + "channels : %i\n" + "precision : %i-bit\n" + "encoding : 0x%x\n" + "seek : %i\n" + "sample count : %i\n" + "EOF count : %i\n" + "paused : %s\n" + "error occurred : %s\n" + "waiting : %s\n" + "active : %s\n" + "", + device->recording ? "record" : "play", + prinfo->buffer_size, + prinfo->sample_rate, + prinfo->channels, + prinfo->precision, + prinfo->encoding, + prinfo->seek, + prinfo->samples, + prinfo->eof, + prinfo->pause ? "yes" : "no", + prinfo->error ? "yes" : "no", + prinfo->waiting ? "yes" : "no", + prinfo->active ? "yes" : "no"); + + fprintf(stderr, "\n" + "[audio info]\n" + "monitor_gain : %i\n" + "hw block size : %d bytes\n" + "hi watermark : %i\n" + "lo watermark : %i\n" + "audio mode : %s\n" + "", + info.monitor_gain, + info.blocksize, + info.hiwat, info.lowat, + (info.mode == AUMODE_PLAY) ? "PLAY" + : (info.mode == AUMODE_RECORD) ? "RECORD" + : (info.mode == AUMODE_PLAY_ALL ? "PLAY_ALL" : "?")); + + fprintf(stderr, "\n" + "[audio spec]\n" + "format : 0x%x\n" + "size : %u\n" + "", + device->spec.format, + device->buffer_size); + /* *INDENT-ON* */ // clang-format on + +#endif // DEBUG_AUDIO +} + +static bool NETBSDAUDIO_WaitDevice(SDL_AudioDevice *device) +{ + const bool recording = device->recording; + while (!SDL_GetAtomicInt(&device->shutdown)) { + audio_info_t info; + const int rc = ioctl(device->hidden->audio_fd, AUDIO_GETINFO, &info); + if (rc < 0) { + if (errno == EAGAIN) { + continue; + } + // Hmm, not much we can do - abort + fprintf(stderr, "netbsdaudio WaitDevice ioctl failed (unrecoverable): %s\n", strerror(errno)); + return false; + } + const size_t remain = (size_t)((recording ? info.record.seek : info.play.seek) * SDL_AUDIO_BYTESIZE(device->spec.format)); + if (!recording && (remain >= device->buffer_size)) { + SDL_Delay(10); + } else if (recording && (remain < device->buffer_size)) { + SDL_Delay(10); + } else { + break; // ready to go! + } + } + + return true; +} + +static bool NETBSDAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + struct SDL_PrivateAudioData *h = device->hidden; + const int written = write(h->audio_fd, buffer, buflen); + if (written != buflen) { // Treat even partial writes as fatal errors. + return false; + } + +#ifdef DEBUG_AUDIO + fprintf(stderr, "Wrote %d bytes of audio data\n", written); +#endif + return true; +} + +static Uint8 *NETBSDAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->hidden->mixbuf; +} + +static int NETBSDAUDIO_RecordDevice(SDL_AudioDevice *device, void *vbuffer, int buflen) +{ + Uint8 *buffer = (Uint8 *)vbuffer; + const int br = read(device->hidden->audio_fd, buffer, buflen); + if (br == -1) { + // Non recoverable error has occurred. It should be reported!!! + perror("audio"); + return -1; + } + +#ifdef DEBUG_AUDIO + fprintf(stderr, "Recorded %d bytes of audio data\n", br); +#endif + return br; +} + +static void NETBSDAUDIO_FlushRecording(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *h = device->hidden; + audio_info_t info; + if (ioctl(device->hidden->audio_fd, AUDIO_GETINFO, &info) == 0) { + size_t remain = (size_t)(info.record.seek * SDL_AUDIO_BYTESIZE(device->spec.format)); + while (remain > 0) { + char buf[512]; + const size_t len = SDL_min(sizeof(buf), remain); + const ssize_t br = read(h->audio_fd, buf, len); + if (br <= 0) { + break; + } + remain -= br; + } + } +} + +static void NETBSDAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->audio_fd >= 0) { + close(device->hidden->audio_fd); + } + SDL_free(device->hidden->mixbuf); + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static bool NETBSDAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + const bool recording = device->recording; + int encoding = AUDIO_ENCODING_NONE; + audio_info_t info, hwinfo; + struct audio_prinfo *prinfo = recording ? &info.record : &info.play; + + // Initialize all variables that we clean on shutdown + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // Open the audio device; we hardcode the device path in `device->name` for lack of better info, so use that. + const int flags = ((device->recording) ? O_RDONLY : O_WRONLY); + device->hidden->audio_fd = open(device->name, flags | O_CLOEXEC); + if (device->hidden->audio_fd < 0) { + return SDL_SetError("Couldn't open %s: %s", device->name, strerror(errno)); + } + + AUDIO_INITINFO(&info); + +#ifdef AUDIO_GETFORMAT // Introduced in NetBSD 9.0 + if (ioctl(device->hidden->audio_fd, AUDIO_GETFORMAT, &hwinfo) != -1) { + // Use the device's native sample rate so the kernel doesn't have to resample. + device->spec.freq = recording ? hwinfo.record.sample_rate : hwinfo.play.sample_rate; + } +#endif + + prinfo->sample_rate = device->spec.freq; + prinfo->channels = device->spec.channels; + + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { + switch (test_format) { + case SDL_AUDIO_U8: + encoding = AUDIO_ENCODING_ULINEAR; + break; + case SDL_AUDIO_S8: + encoding = AUDIO_ENCODING_SLINEAR; + break; + case SDL_AUDIO_S16LE: + encoding = AUDIO_ENCODING_SLINEAR_LE; + break; + case SDL_AUDIO_S16BE: + encoding = AUDIO_ENCODING_SLINEAR_BE; + break; + case SDL_AUDIO_S32LE: + encoding = AUDIO_ENCODING_SLINEAR_LE; + break; + case SDL_AUDIO_S32BE: + encoding = AUDIO_ENCODING_SLINEAR_BE; + break; + default: + continue; + } + break; + } + + if (!test_format) { + return SDL_SetError("%s: Unsupported audio format", "netbsd"); + } + prinfo->encoding = encoding; + prinfo->precision = SDL_AUDIO_BITSIZE(test_format); + + info.hiwat = 5; + info.lowat = 3; + if (ioctl(device->hidden->audio_fd, AUDIO_SETINFO, &info) < 0) { + return SDL_SetError("AUDIO_SETINFO failed for %s: %s", device->name, strerror(errno)); + } + + if (ioctl(device->hidden->audio_fd, AUDIO_GETINFO, &info) < 0) { + return SDL_SetError("AUDIO_GETINFO failed for %s: %s", device->name, strerror(errno)); + } + + // Final spec used for the device. + device->spec.format = test_format; + device->spec.freq = prinfo->sample_rate; + device->spec.channels = prinfo->channels; + + SDL_UpdatedAudioDeviceFormat(device); + + if (!recording) { + // Allocate mixing buffer + device->hidden->mixlen = device->buffer_size; + device->hidden->mixbuf = (Uint8 *)SDL_malloc(device->hidden->mixlen); + if (!device->hidden->mixbuf) { + return false; + } + SDL_memset(device->hidden->mixbuf, device->silence_value, device->buffer_size); + } + + NETBSDAUDIO_Status(device); + + return true; // We're ready to rock and roll. :-) +} + +static bool NETBSDAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + impl->DetectDevices = NETBSDAUDIO_DetectDevices; + impl->OpenDevice = NETBSDAUDIO_OpenDevice; + impl->WaitDevice = NETBSDAUDIO_WaitDevice; + impl->PlayDevice = NETBSDAUDIO_PlayDevice; + impl->GetDeviceBuf = NETBSDAUDIO_GetDeviceBuf; + impl->CloseDevice = NETBSDAUDIO_CloseDevice; + impl->WaitRecordingDevice = NETBSDAUDIO_WaitDevice; + impl->RecordDevice = NETBSDAUDIO_RecordDevice; + impl->FlushRecording = NETBSDAUDIO_FlushRecording; + + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap NETBSDAUDIO_bootstrap = { + "netbsd", "NetBSD audio", NETBSDAUDIO_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_NETBSD diff --git a/contrib/SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.h b/contrib/SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.h new file mode 100644 index 0000000..dcdc6f4 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/netbsd/SDL_netbsdaudio.h @@ -0,0 +1,44 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_netbsdaudio_h_ +#define SDL_netbsdaudio_h_ + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + // The file descriptor for the audio device + int audio_fd; + + // Raw mixing buffer + Uint8 *mixbuf; + int mixlen; + + // Support for audio timing using a timer, in addition to SDL_IOReady() + float frame_ticks; + float next_frame; +}; + +#define FUDGE_TICKS 10 // The scheduler overhead ticks per frame + +#endif // SDL_netbsdaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/openslES/SDL_openslES.c b/contrib/SDL-3.2.8/src/audio/openslES/SDL_openslES.c new file mode 100644 index 0000000..4d5b3bd --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/openslES/SDL_openslES.c @@ -0,0 +1,807 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_OPENSLES + +// For more discussion of low latency audio on Android, see this: +// https://googlesamples.github.io/android-audio-high-performance/guides/opensl_es.html + +#include "../SDL_sysaudio.h" +#include "SDL_openslES.h" + +#include "../../core/android/SDL_android.h" +#include +#include +#include + + +#define NUM_BUFFERS 2 // -- Don't lower this! + +struct SDL_PrivateAudioData +{ + Uint8 *mixbuff; + int next_buffer; + Uint8 *pmixbuff[NUM_BUFFERS]; + SDL_Semaphore *playsem; +}; + +#if 0 +#define LOG_TAG "SDL_openslES" +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +//#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE,LOG_TAG,__VA_ARGS__) +#define LOGV(...) +#else +#define LOGE(...) +#define LOGI(...) +#define LOGV(...) +#endif + +/* +#define SL_SPEAKER_FRONT_LEFT ((SLuint32) 0x00000001) +#define SL_SPEAKER_FRONT_RIGHT ((SLuint32) 0x00000002) +#define SL_SPEAKER_FRONT_CENTER ((SLuint32) 0x00000004) +#define SL_SPEAKER_LOW_FREQUENCY ((SLuint32) 0x00000008) +#define SL_SPEAKER_BACK_LEFT ((SLuint32) 0x00000010) +#define SL_SPEAKER_BACK_RIGHT ((SLuint32) 0x00000020) +#define SL_SPEAKER_FRONT_LEFT_OF_CENTER ((SLuint32) 0x00000040) +#define SL_SPEAKER_FRONT_RIGHT_OF_CENTER ((SLuint32) 0x00000080) +#define SL_SPEAKER_BACK_CENTER ((SLuint32) 0x00000100) +#define SL_SPEAKER_SIDE_LEFT ((SLuint32) 0x00000200) +#define SL_SPEAKER_SIDE_RIGHT ((SLuint32) 0x00000400) +#define SL_SPEAKER_TOP_CENTER ((SLuint32) 0x00000800) +#define SL_SPEAKER_TOP_FRONT_LEFT ((SLuint32) 0x00001000) +#define SL_SPEAKER_TOP_FRONT_CENTER ((SLuint32) 0x00002000) +#define SL_SPEAKER_TOP_FRONT_RIGHT ((SLuint32) 0x00004000) +#define SL_SPEAKER_TOP_BACK_LEFT ((SLuint32) 0x00008000) +#define SL_SPEAKER_TOP_BACK_CENTER ((SLuint32) 0x00010000) +#define SL_SPEAKER_TOP_BACK_RIGHT ((SLuint32) 0x00020000) +*/ +#define SL_ANDROID_SPEAKER_STEREO (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT) +#define SL_ANDROID_SPEAKER_QUAD (SL_ANDROID_SPEAKER_STEREO | SL_SPEAKER_BACK_LEFT | SL_SPEAKER_BACK_RIGHT) +#define SL_ANDROID_SPEAKER_5DOT1 (SL_ANDROID_SPEAKER_QUAD | SL_SPEAKER_FRONT_CENTER | SL_SPEAKER_LOW_FREQUENCY) +#define SL_ANDROID_SPEAKER_7DOT1 (SL_ANDROID_SPEAKER_5DOT1 | SL_SPEAKER_SIDE_LEFT | SL_SPEAKER_SIDE_RIGHT) + +// engine interfaces +static SLObjectItf engineObject = NULL; +static SLEngineItf engineEngine = NULL; + +// output mix interfaces +static SLObjectItf outputMixObject = NULL; + +// buffer queue player interfaces +static SLObjectItf bqPlayerObject = NULL; +static SLPlayItf bqPlayerPlay = NULL; +static SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue = NULL; +#if 0 +static SLVolumeItf bqPlayerVolume; +#endif + +// recorder interfaces +static SLObjectItf recorderObject = NULL; +static SLRecordItf recorderRecord = NULL; +static SLAndroidSimpleBufferQueueItf recorderBufferQueue = NULL; + +#if 0 +static const char *sldevaudiorecorderstr = "SLES Audio Recorder"; +static const char *sldevaudioplayerstr = "SLES Audio Player"; + +#define SLES_DEV_AUDIO_RECORDER sldevaudiorecorderstr +#define SLES_DEV_AUDIO_PLAYER sldevaudioplayerstr +static void OPENSLES_DetectDevices( int recording ) +{ + LOGI( "openSLES_DetectDevices()" ); + if ( recording ) + addfn( SLES_DEV_AUDIO_RECORDER ); + else + addfn( SLES_DEV_AUDIO_PLAYER ); +} +#endif + +static void OPENSLES_DestroyEngine(void) +{ + LOGI("OPENSLES_DestroyEngine()"); + + // destroy output mix object, and invalidate all associated interfaces + if (outputMixObject != NULL) { + (*outputMixObject)->Destroy(outputMixObject); + outputMixObject = NULL; + } + + // destroy engine object, and invalidate all associated interfaces + if (engineObject != NULL) { + (*engineObject)->Destroy(engineObject); + engineObject = NULL; + engineEngine = NULL; + } +} + +static bool OPENSLES_CreateEngine(void) +{ + const SLInterfaceID ids[1] = { SL_IID_VOLUME }; + const SLboolean req[1] = { SL_BOOLEAN_FALSE }; + SLresult result; + + LOGI("openSLES_CreateEngine()"); + + // create engine + result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); + if (SL_RESULT_SUCCESS != result) { + LOGE("slCreateEngine failed: %d", result); + goto error; + } + LOGI("slCreateEngine OK"); + + // realize the engine + result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("RealizeEngine failed: %d", result); + goto error; + } + LOGI("RealizeEngine OK"); + + // get the engine interface, which is needed in order to create other objects + result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); + if (SL_RESULT_SUCCESS != result) { + LOGE("EngineGetInterface failed: %d", result); + goto error; + } + LOGI("EngineGetInterface OK"); + + // create output mix + result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req); + if (SL_RESULT_SUCCESS != result) { + LOGE("CreateOutputMix failed: %d", result); + goto error; + } + LOGI("CreateOutputMix OK"); + + // realize the output mix + result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("RealizeOutputMix failed: %d", result); + goto error; + } + return true; + +error: + OPENSLES_DestroyEngine(); + return false; +} + +// this callback handler is called every time a buffer finishes recording +static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) +{ + struct SDL_PrivateAudioData *audiodata = (struct SDL_PrivateAudioData *)context; + + LOGV("SLES: Recording Callback"); + SDL_SignalSemaphore(audiodata->playsem); +} + +static void OPENSLES_DestroyPCMRecorder(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *audiodata = device->hidden; + SLresult result; + + // stop recording + if (recorderRecord != NULL) { + result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED); + if (SL_RESULT_SUCCESS != result) { + LOGE("SetRecordState stopped: %d", result); + } + } + + // destroy audio recorder object, and invalidate all associated interfaces + if (recorderObject != NULL) { + (*recorderObject)->Destroy(recorderObject); + recorderObject = NULL; + recorderRecord = NULL; + recorderBufferQueue = NULL; + } + + if (audiodata->playsem) { + SDL_DestroySemaphore(audiodata->playsem); + audiodata->playsem = NULL; + } + + if (audiodata->mixbuff) { + SDL_free(audiodata->mixbuff); + } +} + +// !!! FIXME: make this non-blocking! +static void SDLCALL RequestAndroidPermissionBlockingCallback(void *userdata, const char *permission, bool granted) +{ + SDL_SetAtomicInt((SDL_AtomicInt *) userdata, granted ? 1 : -1); +} + +static bool OPENSLES_CreatePCMRecorder(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *audiodata = device->hidden; + SLDataFormat_PCM format_pcm; + SLDataLocator_AndroidSimpleBufferQueue loc_bufq; + SLDataSink audioSnk; + SLDataLocator_IODevice loc_dev; + SLDataSource audioSrc; + const SLInterfaceID ids[1] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE }; + const SLboolean req[1] = { SL_BOOLEAN_TRUE }; + SLresult result; + int i; + + // !!! FIXME: make this non-blocking! + { + SDL_AtomicInt permission_response; + SDL_SetAtomicInt(&permission_response, 0); + if (!SDL_RequestAndroidPermission("android.permission.RECORD_AUDIO", RequestAndroidPermissionBlockingCallback, &permission_response)) { + return false; + } + + while (SDL_GetAtomicInt(&permission_response) == 0) { + SDL_Delay(10); + } + + if (SDL_GetAtomicInt(&permission_response) < 0) { + LOGE("This app doesn't have RECORD_AUDIO permission"); + return SDL_SetError("This app doesn't have RECORD_AUDIO permission"); + } + } + + // Just go with signed 16-bit audio as it's the most compatible + device->spec.format = SDL_AUDIO_S16; + device->spec.channels = 1; + //device->spec.freq = SL_SAMPLINGRATE_16 / 1000;*/ + + // Update the fragment size as size in bytes + SDL_UpdatedAudioDeviceFormat(device); + + LOGI("Try to open %u hz %u bit %u channels %s samples %u", + device->spec.freq, SDL_AUDIO_BITSIZE(device->spec.format), + device->spec.channels, (device->spec.format & 0x1000) ? "BE" : "LE", device->sample_frames); + + // configure audio source + loc_dev.locatorType = SL_DATALOCATOR_IODEVICE; + loc_dev.deviceType = SL_IODEVICE_AUDIOINPUT; + loc_dev.deviceID = SL_DEFAULTDEVICEID_AUDIOINPUT; + loc_dev.device = NULL; + audioSrc.pLocator = &loc_dev; + audioSrc.pFormat = NULL; + + // configure audio sink + loc_bufq.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE; + loc_bufq.numBuffers = NUM_BUFFERS; + + format_pcm.formatType = SL_DATAFORMAT_PCM; + format_pcm.numChannels = device->spec.channels; + format_pcm.samplesPerSec = device->spec.freq * 1000; // / kilo Hz to milli Hz + format_pcm.bitsPerSample = SDL_AUDIO_BITSIZE(device->spec.format); + format_pcm.containerSize = SDL_AUDIO_BITSIZE(device->spec.format); + format_pcm.endianness = SL_BYTEORDER_LITTLEENDIAN; + format_pcm.channelMask = SL_SPEAKER_FRONT_CENTER; + + audioSnk.pLocator = &loc_bufq; + audioSnk.pFormat = &format_pcm; + + // create audio recorder + // (requires the RECORD_AUDIO permission) + result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc, &audioSnk, 1, ids, req); + if (SL_RESULT_SUCCESS != result) { + LOGE("CreateAudioRecorder failed: %d", result); + goto failed; + } + + // realize the recorder + result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("RealizeAudioPlayer failed: %d", result); + goto failed; + } + + // get the record interface + result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord); + if (SL_RESULT_SUCCESS != result) { + LOGE("SL_IID_RECORD interface get failed: %d", result); + goto failed; + } + + // get the buffer queue interface + result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorderBufferQueue); + if (SL_RESULT_SUCCESS != result) { + LOGE("SL_IID_BUFFERQUEUE interface get failed: %d", result); + goto failed; + } + + // register callback on the buffer queue + // context is '(SDL_PrivateAudioData *)device->hidden' + result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, device->hidden); + if (SL_RESULT_SUCCESS != result) { + LOGE("RegisterCallback failed: %d", result); + goto failed; + } + + // Create the audio buffer semaphore + audiodata->playsem = SDL_CreateSemaphore(0); + if (!audiodata->playsem) { + LOGE("cannot create Semaphore!"); + goto failed; + } + + // Create the sound buffers + audiodata->mixbuff = (Uint8 *)SDL_malloc(NUM_BUFFERS * device->buffer_size); + if (!audiodata->mixbuff) { + LOGE("mixbuffer allocate - out of memory"); + goto failed; + } + + for (i = 0; i < NUM_BUFFERS; i++) { + audiodata->pmixbuff[i] = audiodata->mixbuff + i * device->buffer_size; + } + + // in case already recording, stop recording and clear buffer queue + result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED); + if (SL_RESULT_SUCCESS != result) { + LOGE("Record set state failed: %d", result); + goto failed; + } + + // enqueue empty buffers to be filled by the recorder + for (i = 0; i < NUM_BUFFERS; i++) { + result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, audiodata->pmixbuff[i], device->buffer_size); + if (SL_RESULT_SUCCESS != result) { + LOGE("Record enqueue buffers failed: %d", result); + goto failed; + } + } + + // start recording + result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING); + if (SL_RESULT_SUCCESS != result) { + LOGE("Record set state failed: %d", result); + goto failed; + } + + return true; + +failed: + return SDL_SetError("Open device failed!"); +} + +// this callback handler is called every time a buffer finishes playing +static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) +{ + struct SDL_PrivateAudioData *audiodata = (struct SDL_PrivateAudioData *)context; + + LOGV("SLES: Playback Callback"); + SDL_SignalSemaphore(audiodata->playsem); +} + +static void OPENSLES_DestroyPCMPlayer(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *audiodata = device->hidden; + + // set the player's state to 'stopped' + if (bqPlayerPlay != NULL) { + const SLresult result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED); + if (SL_RESULT_SUCCESS != result) { + LOGE("SetPlayState stopped failed: %d", result); + } + } + + // destroy buffer queue audio player object, and invalidate all associated interfaces + if (bqPlayerObject != NULL) { + (*bqPlayerObject)->Destroy(bqPlayerObject); + + bqPlayerObject = NULL; + bqPlayerPlay = NULL; + bqPlayerBufferQueue = NULL; + } + + if (audiodata->playsem) { + SDL_DestroySemaphore(audiodata->playsem); + audiodata->playsem = NULL; + } + + if (audiodata->mixbuff) { + SDL_free(audiodata->mixbuff); + } +} + +static bool OPENSLES_CreatePCMPlayer(SDL_AudioDevice *device) +{ + /* If we want to add floating point audio support (requires API level 21) + it can be done as described here: + https://developer.android.com/ndk/guides/audio/opensl/android-extensions.html#floating-point + */ + if (SDL_GetAndroidSDKVersion() >= 21) { + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + SDL_AudioFormat test_format; + while ((test_format = *(closefmts++)) != 0) { + if (SDL_AUDIO_ISSIGNED(test_format)) { + break; + } + } + + if (!test_format) { + // Didn't find a compatible format : + LOGI("No compatible audio format, using signed 16-bit audio"); + test_format = SDL_AUDIO_S16; + } + device->spec.format = test_format; + } else { + // Just go with signed 16-bit audio as it's the most compatible + device->spec.format = SDL_AUDIO_S16; + } + + // Update the fragment size as size in bytes + SDL_UpdatedAudioDeviceFormat(device); + + LOGI("Try to open %u hz %s %u bit %u channels %s samples %u", + device->spec.freq, SDL_AUDIO_ISFLOAT(device->spec.format) ? "float" : "pcm", SDL_AUDIO_BITSIZE(device->spec.format), + device->spec.channels, (device->spec.format & 0x1000) ? "BE" : "LE", device->sample_frames); + + // configure audio source + SLDataLocator_AndroidSimpleBufferQueue loc_bufq; + loc_bufq.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE; + loc_bufq.numBuffers = NUM_BUFFERS; + + SLDataFormat_PCM format_pcm; + format_pcm.formatType = SL_DATAFORMAT_PCM; + format_pcm.numChannels = device->spec.channels; + format_pcm.samplesPerSec = device->spec.freq * 1000; // / kilo Hz to milli Hz + format_pcm.bitsPerSample = SDL_AUDIO_BITSIZE(device->spec.format); + format_pcm.containerSize = SDL_AUDIO_BITSIZE(device->spec.format); + + if (SDL_AUDIO_ISBIGENDIAN(device->spec.format)) { + format_pcm.endianness = SL_BYTEORDER_BIGENDIAN; + } else { + format_pcm.endianness = SL_BYTEORDER_LITTLEENDIAN; + } + + switch (device->spec.channels) { + case 1: + format_pcm.channelMask = SL_SPEAKER_FRONT_LEFT; + break; + case 2: + format_pcm.channelMask = SL_ANDROID_SPEAKER_STEREO; + break; + case 3: + format_pcm.channelMask = SL_ANDROID_SPEAKER_STEREO | SL_SPEAKER_FRONT_CENTER; + break; + case 4: + format_pcm.channelMask = SL_ANDROID_SPEAKER_QUAD; + break; + case 5: + format_pcm.channelMask = SL_ANDROID_SPEAKER_QUAD | SL_SPEAKER_FRONT_CENTER; + break; + case 6: + format_pcm.channelMask = SL_ANDROID_SPEAKER_5DOT1; + break; + case 7: + format_pcm.channelMask = SL_ANDROID_SPEAKER_5DOT1 | SL_SPEAKER_BACK_CENTER; + break; + case 8: + format_pcm.channelMask = SL_ANDROID_SPEAKER_7DOT1; + break; + default: + // Unknown number of channels, fall back to stereo + device->spec.channels = 2; + format_pcm.channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + break; + } + + SLDataSink audioSnk; + SLDataSource audioSrc; + audioSrc.pFormat = (void *)&format_pcm; + + SLAndroidDataFormat_PCM_EX format_pcm_ex; + if (SDL_AUDIO_ISFLOAT(device->spec.format)) { + // Copy all setup into PCM EX structure + format_pcm_ex.formatType = SL_ANDROID_DATAFORMAT_PCM_EX; + format_pcm_ex.endianness = format_pcm.endianness; + format_pcm_ex.channelMask = format_pcm.channelMask; + format_pcm_ex.numChannels = format_pcm.numChannels; + format_pcm_ex.sampleRate = format_pcm.samplesPerSec; + format_pcm_ex.bitsPerSample = format_pcm.bitsPerSample; + format_pcm_ex.containerSize = format_pcm.containerSize; + format_pcm_ex.representation = SL_ANDROID_PCM_REPRESENTATION_FLOAT; + audioSrc.pFormat = (void *)&format_pcm_ex; + } + + audioSrc.pLocator = &loc_bufq; + + // configure audio sink + SLDataLocator_OutputMix loc_outmix; + loc_outmix.locatorType = SL_DATALOCATOR_OUTPUTMIX; + loc_outmix.outputMix = outputMixObject; + audioSnk.pLocator = &loc_outmix; + audioSnk.pFormat = NULL; + + // create audio player + const SLInterfaceID ids[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME }; + const SLboolean req[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE }; + SLresult result; + result = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk, 2, ids, req); + if (SL_RESULT_SUCCESS != result) { + LOGE("CreateAudioPlayer failed: %d", result); + goto failed; + } + + // realize the player + result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("RealizeAudioPlayer failed: %d", result); + goto failed; + } + + // get the play interface + result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay); + if (SL_RESULT_SUCCESS != result) { + LOGE("SL_IID_PLAY interface get failed: %d", result); + goto failed; + } + + // get the buffer queue interface + result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &bqPlayerBufferQueue); + if (SL_RESULT_SUCCESS != result) { + LOGE("SL_IID_BUFFERQUEUE interface get failed: %d", result); + goto failed; + } + + // register callback on the buffer queue + // context is '(SDL_PrivateAudioData *)device->hidden' + result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, device->hidden); + if (SL_RESULT_SUCCESS != result) { + LOGE("RegisterCallback failed: %d", result); + goto failed; + } + +#if 0 + // get the volume interface + result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume); + if (SL_RESULT_SUCCESS != result) { + LOGE("SL_IID_VOLUME interface get failed: %d", result); + // goto failed; + } +#endif + + struct SDL_PrivateAudioData *audiodata = device->hidden; + + // Create the audio buffer semaphore + audiodata->playsem = SDL_CreateSemaphore(NUM_BUFFERS - 1); + if (!audiodata->playsem) { + LOGE("cannot create Semaphore!"); + goto failed; + } + + // Create the sound buffers + audiodata->mixbuff = (Uint8 *)SDL_malloc(NUM_BUFFERS * device->buffer_size); + if (!audiodata->mixbuff) { + LOGE("mixbuffer allocate - out of memory"); + goto failed; + } + + for (int i = 0; i < NUM_BUFFERS; i++) { + audiodata->pmixbuff[i] = audiodata->mixbuff + i * device->buffer_size; + } + + // set the player's state to playing + result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING); + if (SL_RESULT_SUCCESS != result) { + LOGE("Play set state failed: %d", result); + goto failed; + } + + return true; + +failed: + return false; +} + +static bool OPENSLES_OpenDevice(SDL_AudioDevice *device) +{ + device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + if (device->recording) { + LOGI("OPENSLES_OpenDevice() for recording"); + return OPENSLES_CreatePCMRecorder(device); + } else { + bool ret; + LOGI("OPENSLES_OpenDevice() for playback"); + ret = OPENSLES_CreatePCMPlayer(device); + if (!ret) { + // Another attempt to open the device with a lower frequency + if (device->spec.freq > 48000) { + OPENSLES_DestroyPCMPlayer(device); + device->spec.freq = 48000; + ret = OPENSLES_CreatePCMPlayer(device); + } + } + + if (!ret) { + return SDL_SetError("Open device failed!"); + } + } + + return true; +} + +static bool OPENSLES_WaitDevice(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *audiodata = device->hidden; + + LOGV("OPENSLES_WaitDevice()"); + + while (!SDL_GetAtomicInt(&device->shutdown)) { + // this semaphore won't fire when the app is in the background (OPENSLES_PauseDevices was called). + if (SDL_WaitSemaphoreTimeout(audiodata->playsem, 100)) { + return true; // semaphore was signaled, let's go! + } + // Still waiting on the semaphore (or the system), check other things then wait again. + } + return true; +} + +static bool OPENSLES_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + struct SDL_PrivateAudioData *audiodata = device->hidden; + + LOGV("======OPENSLES_PlayDevice()======"); + + // Queue it up + const SLresult result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, buffer, buflen); + + audiodata->next_buffer++; + if (audiodata->next_buffer >= NUM_BUFFERS) { + audiodata->next_buffer = 0; + } + + // If Enqueue fails, callback won't be called. + // Post the semaphore, not to run out of buffer + if (SL_RESULT_SUCCESS != result) { + SDL_SignalSemaphore(audiodata->playsem); + } + + return true; +} + +/// n playn sem +// getbuf 0 - 1 +// fill buff 0 - 1 +// play 0 - 0 1 +// wait 1 0 0 +// getbuf 1 0 0 +// fill buff 1 0 0 +// play 0 0 0 +// wait +// +// okay.. + +static Uint8 *OPENSLES_GetDeviceBuf(SDL_AudioDevice *device, int *bufsize) +{ + struct SDL_PrivateAudioData *audiodata = device->hidden; + + LOGV("OPENSLES_GetDeviceBuf()"); + return audiodata->pmixbuff[audiodata->next_buffer]; +} + +static int OPENSLES_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + struct SDL_PrivateAudioData *audiodata = device->hidden; + + // Copy it to the output buffer + SDL_assert(buflen == device->buffer_size); + SDL_memcpy(buffer, audiodata->pmixbuff[audiodata->next_buffer], device->buffer_size); + + // Re-enqueue the buffer + const SLresult result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, audiodata->pmixbuff[audiodata->next_buffer], device->buffer_size); + if (SL_RESULT_SUCCESS != result) { + LOGE("Record enqueue buffers failed: %d", result); + return -1; + } + + audiodata->next_buffer++; + if (audiodata->next_buffer >= NUM_BUFFERS) { + audiodata->next_buffer = 0; + } + + return device->buffer_size; +} + +static void OPENSLES_CloseDevice(SDL_AudioDevice *device) +{ + // struct SDL_PrivateAudioData *audiodata = device->hidden; + if (device->hidden) { + if (device->recording) { + LOGI("OPENSLES_CloseDevice() for recording"); + OPENSLES_DestroyPCMRecorder(device); + } else { + LOGI("OPENSLES_CloseDevice() for playing"); + OPENSLES_DestroyPCMPlayer(device); + } + + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static bool OPENSLES_Init(SDL_AudioDriverImpl *impl) +{ + LOGI("OPENSLES_Init() called"); + + if (!OPENSLES_CreateEngine()) { + return false; + } + + LOGI("OPENSLES_Init() - set pointers"); + + // Set the function pointers + // impl->DetectDevices = OPENSLES_DetectDevices; + impl->ThreadInit = Android_AudioThreadInit; + impl->OpenDevice = OPENSLES_OpenDevice; + impl->WaitDevice = OPENSLES_WaitDevice; + impl->PlayDevice = OPENSLES_PlayDevice; + impl->GetDeviceBuf = OPENSLES_GetDeviceBuf; + impl->WaitRecordingDevice = OPENSLES_WaitDevice; + impl->RecordDevice = OPENSLES_RecordDevice; + impl->CloseDevice = OPENSLES_CloseDevice; + impl->Deinitialize = OPENSLES_DestroyEngine; + + // and the capabilities + impl->HasRecordingSupport = true; + impl->OnlyHasDefaultPlaybackDevice = true; + impl->OnlyHasDefaultRecordingDevice = true; + + LOGI("OPENSLES_Init() - success"); + + // this audio target is available. + return true; +} + +AudioBootStrap OPENSLES_bootstrap = { + "openslES", "OpenSL ES audio driver", OPENSLES_Init, false, false +}; + +void OPENSLES_ResumeDevices(void) +{ + if (bqPlayerPlay != NULL) { + // set the player's state to 'playing' + SLresult result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING); + if (SL_RESULT_SUCCESS != result) { + LOGE("OPENSLES_ResumeDevices failed: %d", result); + } + } +} + +void OPENSLES_PauseDevices(void) +{ + if (bqPlayerPlay != NULL) { + // set the player's state to 'paused' + SLresult result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PAUSED); + if (SL_RESULT_SUCCESS != result) { + LOGE("OPENSLES_PauseDevices failed: %d", result); + } + } +} + +#endif // SDL_AUDIO_DRIVER_OPENSLES diff --git a/contrib/SDL-3.2.8/src/audio/openslES/SDL_openslES.h b/contrib/SDL-3.2.8/src/audio/openslES/SDL_openslES.h new file mode 100644 index 0000000..0ae2664 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/openslES/SDL_openslES.h @@ -0,0 +1,38 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_openslesaudio_h_ +#define SDL_openslesaudio_h_ + +#ifdef SDL_AUDIO_DRIVER_OPENSLES + +extern void OPENSLES_ResumeDevices(void); +extern void OPENSLES_PauseDevices(void); + +#else + +#define OPENSLES_ResumeDevices() +#define OPENSLES_PauseDevices() + +#endif + +#endif // SDL_openslesaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c b/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c new file mode 100644 index 0000000..e3f9439 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c @@ -0,0 +1,1349 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_PIPEWIRE + +#include "SDL_pipewire.h" + +#include +#include +#include + +/* + * This seems to be a sane lower limit as Pipewire + * uses it in several of it's own modules. + */ +#define PW_MIN_SAMPLES 32 // About 0.67ms at 48kHz +#define PW_BASE_CLOCK_RATE 48000 + +#define PW_POD_BUFFER_LENGTH 1024 +#define PW_THREAD_NAME_BUFFER_LENGTH 128 +#define PW_MAX_IDENTIFIER_LENGTH 256 + +enum PW_READY_FLAGS +{ + PW_READY_FLAG_BUFFER_ADDED = 0x1, + PW_READY_FLAG_STREAM_READY = 0x2, + PW_READY_FLAG_ALL_PREOPEN_BITS = 0x3, + PW_READY_FLAG_OPEN_COMPLETE = 0x4, + PW_READY_FLAG_ALL_BITS = 0x7 +}; + +#define PW_ID_TO_HANDLE(x) (void *)((uintptr_t)x) +#define PW_HANDLE_TO_ID(x) (uint32_t)((uintptr_t)x) + +static bool pipewire_initialized = false; + +// Pipewire entry points +static const char *(*PIPEWIRE_pw_get_library_version)(void); +static void (*PIPEWIRE_pw_init)(int *, char ***); +static void (*PIPEWIRE_pw_deinit)(void); +static struct pw_main_loop *(*PIPEWIRE_pw_main_loop_new)(const struct spa_dict *loop); +static struct pw_loop *(*PIPEWIRE_pw_main_loop_get_loop)(struct pw_main_loop *loop); +static int (*PIPEWIRE_pw_main_loop_run)(struct pw_main_loop *loop); +static int (*PIPEWIRE_pw_main_loop_quit)(struct pw_main_loop *loop); +static void(*PIPEWIRE_pw_main_loop_destroy)(struct pw_main_loop *loop); +static struct pw_thread_loop *(*PIPEWIRE_pw_thread_loop_new)(const char *, const struct spa_dict *); +static void (*PIPEWIRE_pw_thread_loop_destroy)(struct pw_thread_loop *); +static void (*PIPEWIRE_pw_thread_loop_stop)(struct pw_thread_loop *); +static struct pw_loop *(*PIPEWIRE_pw_thread_loop_get_loop)(struct pw_thread_loop *); +static void (*PIPEWIRE_pw_thread_loop_lock)(struct pw_thread_loop *); +static void (*PIPEWIRE_pw_thread_loop_unlock)(struct pw_thread_loop *); +static void (*PIPEWIRE_pw_thread_loop_signal)(struct pw_thread_loop *, bool); +static void (*PIPEWIRE_pw_thread_loop_wait)(struct pw_thread_loop *); +static int (*PIPEWIRE_pw_thread_loop_start)(struct pw_thread_loop *); +static struct pw_context *(*PIPEWIRE_pw_context_new)(struct pw_loop *, struct pw_properties *, size_t); +static void (*PIPEWIRE_pw_context_destroy)(struct pw_context *); +static struct pw_core *(*PIPEWIRE_pw_context_connect)(struct pw_context *, struct pw_properties *, size_t); +static void (*PIPEWIRE_pw_proxy_add_object_listener)(struct pw_proxy *, struct spa_hook *, const void *, void *); +static void *(*PIPEWIRE_pw_proxy_get_user_data)(struct pw_proxy *); +static void (*PIPEWIRE_pw_proxy_destroy)(struct pw_proxy *); +static int (*PIPEWIRE_pw_core_disconnect)(struct pw_core *); +static struct pw_stream *(*PIPEWIRE_pw_stream_new_simple)(struct pw_loop *, const char *, struct pw_properties *, + const struct pw_stream_events *, void *); +static void (*PIPEWIRE_pw_stream_destroy)(struct pw_stream *); +static int (*PIPEWIRE_pw_stream_connect)(struct pw_stream *, enum pw_direction, uint32_t, enum pw_stream_flags, + const struct spa_pod **, uint32_t); +static enum pw_stream_state (*PIPEWIRE_pw_stream_get_state)(struct pw_stream *stream, const char **error); +static struct pw_buffer *(*PIPEWIRE_pw_stream_dequeue_buffer)(struct pw_stream *); +static int (*PIPEWIRE_pw_stream_queue_buffer)(struct pw_stream *, struct pw_buffer *); +static struct pw_properties *(*PIPEWIRE_pw_properties_new)(const char *, ...)SPA_SENTINEL; +static int (*PIPEWIRE_pw_properties_set)(struct pw_properties *, const char *, const char *); +static int (*PIPEWIRE_pw_properties_setf)(struct pw_properties *, const char *, const char *, ...) SPA_PRINTF_FUNC(3, 4); + +#ifdef SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC + +static const char *pipewire_library = SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC; +static SDL_SharedObject *pipewire_handle = NULL; + +static bool pipewire_dlsym(const char *fn, void **addr) +{ + *addr = SDL_LoadFunction(pipewire_handle, fn); + if (!*addr) { + // Don't call SDL_SetError(): SDL_LoadFunction already did. + return false; + } + + return true; +} + +#define SDL_PIPEWIRE_SYM(x) \ + if (!pipewire_dlsym(#x, (void **)(char *)&PIPEWIRE_##x)) \ + return false + +static bool load_pipewire_library(void) +{ + pipewire_handle = SDL_LoadObject(pipewire_library); + return pipewire_handle ? true : false; +} + +static void unload_pipewire_library(void) +{ + if (pipewire_handle) { + SDL_UnloadObject(pipewire_handle); + pipewire_handle = NULL; + } +} + +#else + +#define SDL_PIPEWIRE_SYM(x) PIPEWIRE_##x = x + +static bool load_pipewire_library(void) +{ + return true; +} + +static void unload_pipewire_library(void) +{ + // Nothing to do +} + +#endif // SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC + +static bool load_pipewire_syms(void) +{ + SDL_PIPEWIRE_SYM(pw_get_library_version); + SDL_PIPEWIRE_SYM(pw_init); + SDL_PIPEWIRE_SYM(pw_deinit); + SDL_PIPEWIRE_SYM(pw_main_loop_new); + SDL_PIPEWIRE_SYM(pw_main_loop_get_loop); + SDL_PIPEWIRE_SYM(pw_main_loop_run); + SDL_PIPEWIRE_SYM(pw_main_loop_quit); + SDL_PIPEWIRE_SYM(pw_main_loop_destroy); + SDL_PIPEWIRE_SYM(pw_thread_loop_new); + SDL_PIPEWIRE_SYM(pw_thread_loop_destroy); + SDL_PIPEWIRE_SYM(pw_thread_loop_stop); + SDL_PIPEWIRE_SYM(pw_thread_loop_get_loop); + SDL_PIPEWIRE_SYM(pw_thread_loop_lock); + SDL_PIPEWIRE_SYM(pw_thread_loop_unlock); + SDL_PIPEWIRE_SYM(pw_thread_loop_signal); + SDL_PIPEWIRE_SYM(pw_thread_loop_wait); + SDL_PIPEWIRE_SYM(pw_thread_loop_start); + SDL_PIPEWIRE_SYM(pw_context_new); + SDL_PIPEWIRE_SYM(pw_context_destroy); + SDL_PIPEWIRE_SYM(pw_context_connect); + SDL_PIPEWIRE_SYM(pw_proxy_add_object_listener); + SDL_PIPEWIRE_SYM(pw_proxy_get_user_data); + SDL_PIPEWIRE_SYM(pw_proxy_destroy); + SDL_PIPEWIRE_SYM(pw_core_disconnect); + SDL_PIPEWIRE_SYM(pw_stream_new_simple); + SDL_PIPEWIRE_SYM(pw_stream_destroy); + SDL_PIPEWIRE_SYM(pw_stream_connect); + SDL_PIPEWIRE_SYM(pw_stream_get_state); + SDL_PIPEWIRE_SYM(pw_stream_dequeue_buffer); + SDL_PIPEWIRE_SYM(pw_stream_queue_buffer); + SDL_PIPEWIRE_SYM(pw_properties_new); + SDL_PIPEWIRE_SYM(pw_properties_set); + SDL_PIPEWIRE_SYM(pw_properties_setf); + + return true; +} + +static bool init_pipewire_library(void) +{ + if (load_pipewire_library()) { + if (load_pipewire_syms()) { + PIPEWIRE_pw_init(NULL, NULL); + return true; + } + } + + return false; +} + +static void deinit_pipewire_library(void) +{ + PIPEWIRE_pw_deinit(); + unload_pipewire_library(); +} + +// A generic Pipewire node object used for enumeration. +struct node_object +{ + struct spa_list link; + + Uint32 id; + int seq; + bool persist; + + /* + * NOTE: If used, this is *must* be allocated with SDL_malloc() or similar + * as SDL_free() will be called on it when the node_object is destroyed. + * + * If ownership of the referenced memory is transferred, this must be set + * to NULL or the memory will be freed when the node_object is destroyed. + */ + void *userdata; + + struct pw_proxy *proxy; + struct spa_hook node_listener; + struct spa_hook core_listener; +}; + +// A sink/source node used for stream I/O. +struct io_node +{ + struct spa_list link; + + Uint32 id; + bool recording; + SDL_AudioSpec spec; + + const char *name; // Friendly name + const char *path; // OS identifier (i.e. ALSA endpoint) + + char buf[]; // Buffer to hold the name and path strings. +}; + +// The global hotplug thread and associated objects. +static struct pw_thread_loop *hotplug_loop; +static struct pw_core *hotplug_core; +static struct pw_context *hotplug_context; +static struct pw_registry *hotplug_registry; +static struct spa_hook hotplug_registry_listener; +static struct spa_hook hotplug_core_listener; +static struct spa_list hotplug_pending_list; +static struct spa_list hotplug_io_list; +static int hotplug_init_seq_val; +static bool hotplug_init_complete; +static bool hotplug_events_enabled; + +static int pipewire_version_major; +static int pipewire_version_minor; +static int pipewire_version_patch; +static char *pipewire_default_sink_id = NULL; +static char *pipewire_default_source_id = NULL; + +static bool pipewire_core_version_at_least(int major, int minor, int patch) +{ + return (pipewire_version_major >= major) && + (pipewire_version_major > major || pipewire_version_minor >= minor) && + (pipewire_version_major > major || pipewire_version_minor > minor || pipewire_version_patch >= patch); +} + +// The active node list +static bool io_list_check_add(struct io_node *node) +{ + struct io_node *n; + bool ret = true; + + // See if the node is already in the list + spa_list_for_each (n, &hotplug_io_list, link) { + if (n->id == node->id) { + ret = false; + goto dup_found; + } + } + + // Add to the list if the node doesn't already exist + spa_list_append(&hotplug_io_list, &node->link); + + if (hotplug_events_enabled) { + SDL_AddAudioDevice(node->recording, node->name, &node->spec, PW_ID_TO_HANDLE(node->id)); + } + +dup_found: + + return ret; +} + +static void io_list_remove(Uint32 id) +{ + struct io_node *n, *temp; + + // Find and remove the node from the list + spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { + if (n->id == id) { + spa_list_remove(&n->link); + + if (hotplug_events_enabled) { + SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle(PW_ID_TO_HANDLE(id))); + } + + SDL_free(n); + + break; + } + } +} + +static void io_list_clear(void) +{ + struct io_node *n, *temp; + + spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { + spa_list_remove(&n->link); + SDL_free(n); + } +} + +static struct io_node *io_list_get_by_id(Uint32 id) +{ + struct io_node *n, *temp; + spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { + if (n->id == id) { + return n; + } + } + return NULL; +} + +static void node_object_destroy(struct node_object *node) +{ + SDL_assert(node != NULL); + + spa_list_remove(&node->link); + spa_hook_remove(&node->node_listener); + spa_hook_remove(&node->core_listener); + SDL_free(node->userdata); + PIPEWIRE_pw_proxy_destroy(node->proxy); +} + +// The pending node list +static void pending_list_add(struct node_object *node) +{ + SDL_assert(node != NULL); + spa_list_append(&hotplug_pending_list, &node->link); +} + +static void pending_list_remove(Uint32 id) +{ + struct node_object *node, *temp; + + spa_list_for_each_safe (node, temp, &hotplug_pending_list, link) { + if (node->id == id) { + node_object_destroy(node); + } + } +} + +static void pending_list_clear(void) +{ + struct node_object *node, *temp; + + spa_list_for_each_safe (node, temp, &hotplug_pending_list, link) { + node_object_destroy(node); + } +} + +static void *node_object_new(Uint32 id, const char *type, Uint32 version, const void *funcs, const struct pw_core_events *core_events) +{ + struct pw_proxy *proxy; + struct node_object *node; + + // Create the proxy object + proxy = pw_registry_bind(hotplug_registry, id, type, version, sizeof(struct node_object)); + if (!proxy) { + SDL_SetError("Pipewire: Failed to create proxy object (%i)", errno); + return NULL; + } + + node = PIPEWIRE_pw_proxy_get_user_data(proxy); + SDL_zerop(node); + + node->id = id; + node->proxy = proxy; + + // Add the callbacks + pw_core_add_listener(hotplug_core, &node->core_listener, core_events, node); + PIPEWIRE_pw_proxy_add_object_listener(node->proxy, &node->node_listener, funcs, node); + + // Add the node to the active list + pending_list_add(node); + + return node; +} + +// Core sync points +static void core_events_hotplug_init_callback(void *object, uint32_t id, int seq) +{ + if (id == PW_ID_CORE && seq == hotplug_init_seq_val) { + // This core listener is no longer needed. + spa_hook_remove(&hotplug_core_listener); + + // Signal that the initial I/O list is populated + hotplug_init_complete = true; + PIPEWIRE_pw_thread_loop_signal(hotplug_loop, false); + } +} + +static void core_events_hotplug_info_callback(void *data, const struct pw_core_info *info) +{ + if (SDL_sscanf(info->version, "%d.%d.%d", &pipewire_version_major, &pipewire_version_minor, &pipewire_version_patch) < 3) { + pipewire_version_major = 0; + pipewire_version_minor = 0; + pipewire_version_patch = 0; + } +} + +static void core_events_interface_callback(void *object, uint32_t id, int seq) +{ + struct node_object *node = object; + struct io_node *io = node->userdata; + + if (id == PW_ID_CORE && seq == node->seq) { + /* + * Move the I/O node to the connected list. + * On success, the list owns the I/O node object. + */ + if (io_list_check_add(io)) { + node->userdata = NULL; + } + + node_object_destroy(node); + } +} + +static void core_events_metadata_callback(void *object, uint32_t id, int seq) +{ + struct node_object *node = object; + + if (id == PW_ID_CORE && seq == node->seq && !node->persist) { + node_object_destroy(node); + } +} + +static const struct pw_core_events hotplug_init_core_events = { PW_VERSION_CORE_EVENTS, .info = core_events_hotplug_info_callback, .done = core_events_hotplug_init_callback }; +static const struct pw_core_events interface_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_interface_callback }; +static const struct pw_core_events metadata_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_metadata_callback }; + +static void hotplug_core_sync(struct node_object *node) +{ + /* + * Node sync events *must* come before the hotplug init sync events or the initial + * I/O list will be incomplete when the main hotplug sync point is hit. + */ + if (node) { + node->seq = pw_core_sync(hotplug_core, PW_ID_CORE, node->seq); + } + + if (!hotplug_init_complete) { + hotplug_init_seq_val = pw_core_sync(hotplug_core, PW_ID_CORE, hotplug_init_seq_val); + } +} + +// Helpers for retrieving values from params +static bool get_range_param(const struct spa_pod *param, Uint32 key, int *def, int *min, int *max) +{ + const struct spa_pod_prop *prop; + struct spa_pod *value; + Uint32 n_values, choice; + + prop = spa_pod_find_prop(param, NULL, key); + + if (prop && prop->value.type == SPA_TYPE_Choice) { + value = spa_pod_get_values(&prop->value, &n_values, &choice); + + if (n_values == 3 && choice == SPA_CHOICE_Range) { + Uint32 *v = SPA_POD_BODY(value); + + if (v) { + if (def) { + *def = (int)v[0]; + } + if (min) { + *min = (int)v[1]; + } + if (max) { + *max = (int)v[2]; + } + + return true; + } + } + } + + return false; +} + +static bool get_int_param(const struct spa_pod *param, Uint32 key, int *val) +{ + const struct spa_pod_prop *prop; + Sint32 v; + + prop = spa_pod_find_prop(param, NULL, key); + + if (prop && spa_pod_get_int(&prop->value, &v) == 0) { + if (val) { + *val = (int)v; + } + + return true; + } + + return false; +} + +static SDL_AudioFormat SPAFormatToSDL(enum spa_audio_format spafmt) +{ + switch (spafmt) { + #define CHECKFMT(spa,sdl) case SPA_AUDIO_FORMAT_##spa: return SDL_AUDIO_##sdl + CHECKFMT(U8, U8); + CHECKFMT(S8, S8); + CHECKFMT(S16_LE, S16LE); + CHECKFMT(S16_BE, S16BE); + CHECKFMT(S32_LE, S32LE); + CHECKFMT(S32_BE, S32BE); + CHECKFMT(F32_LE, F32LE); + CHECKFMT(F32_BE, F32BE); + #undef CHECKFMT + default: break; + } + + return SDL_AUDIO_UNKNOWN; +} + +// Interface node callbacks +static void node_event_info(void *object, const struct pw_node_info *info) +{ + struct node_object *node = object; + struct io_node *io = node->userdata; + const char *prop_val; + Uint32 i; + + if (info) { + prop_val = spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS); + if (prop_val) { + io->spec.channels = (Uint8)SDL_atoi(prop_val); + } + + // Need to parse the parameters to get the sample rate + for (i = 0; i < info->n_params; ++i) { + pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL); + } + + hotplug_core_sync(node); + } +} + +static void node_event_param(void *object, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) +{ + struct node_object *node = object; + struct io_node *io = node->userdata; + + if ((id == SPA_PARAM_Format) && (io->spec.format == SDL_AUDIO_UNKNOWN)) { + struct spa_audio_info_raw info; + SDL_zero(info); + if (spa_format_audio_raw_parse(param, &info) == 0) { + //SDL_Log("Sink Format: %d, Rate: %d Hz, Channels: %d", info.format, info.rate, info.channels); + io->spec.format = SPAFormatToSDL(info.format); + } + } + + // Get the default frequency + if (io->spec.freq == 0) { + get_range_param(param, SPA_FORMAT_AUDIO_rate, &io->spec.freq, NULL, NULL); + } + + /* + * The channel count should have come from the node properties, + * but it is stored here as well. If one failed, try the other. + */ + if (io->spec.channels == 0) { + int channels; + if (get_int_param(param, SPA_FORMAT_AUDIO_channels, &channels)) { + io->spec.channels = (Uint8)channels; + } + } +} + +static const struct pw_node_events interface_node_events = { PW_VERSION_NODE_EVENTS, .info = node_event_info, + .param = node_event_param }; + +static char *get_name_from_json(const char *json) +{ + struct spa_json parser[2]; + char key[7]; // "name" + char value[PW_MAX_IDENTIFIER_LENGTH]; + spa_json_init(&parser[0], json, SDL_strlen(json)); + if (spa_json_enter_object(&parser[0], &parser[1]) <= 0) { + // Not actually JSON + return NULL; + } + if (spa_json_get_string(&parser[1], key, sizeof(key)) <= 0) { + // Not actually a key/value pair + return NULL; + } + if (spa_json_get_string(&parser[1], value, sizeof(value)) <= 0) { + // Somehow had a key with no value? + return NULL; + } + return SDL_strdup(value); +} + +static void change_default_device(const char *path) +{ + if (hotplug_events_enabled) { + struct io_node *n, *temp; + spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { + if (SDL_strcmp(n->path, path) == 0) { + SDL_DefaultAudioDeviceChanged(SDL_FindPhysicalAudioDeviceByHandle(PW_ID_TO_HANDLE(n->id))); + return; // found it, we're done. + } + } + } +} + +// Metadata node callback +static int metadata_property(void *object, Uint32 subject, const char *key, const char *type, const char *value) +{ + struct node_object *node = object; + + if (subject == PW_ID_CORE && key && value) { + if (!SDL_strcmp(key, "default.audio.sink")) { + if (pipewire_default_sink_id) { + SDL_free(pipewire_default_sink_id); + } + pipewire_default_sink_id = get_name_from_json(value); + node->persist = true; + change_default_device(pipewire_default_sink_id); + } else if (!SDL_strcmp(key, "default.audio.source")) { + if (pipewire_default_source_id) { + SDL_free(pipewire_default_source_id); + } + pipewire_default_source_id = get_name_from_json(value); + node->persist = true; + change_default_device(pipewire_default_source_id); + } + } + + return 0; +} + +static const struct pw_metadata_events metadata_node_events = { PW_VERSION_METADATA_EVENTS, .property = metadata_property }; + +// Global registry callbacks +static void registry_event_global_callback(void *object, uint32_t id, uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + struct node_object *node; + + // We're only interested in interface and metadata nodes. + if (!SDL_strcmp(type, PW_TYPE_INTERFACE_Node)) { + const char *media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + + if (media_class) { + const char *node_desc; + const char *node_path; + struct io_node *io; + bool recording; + int desc_buffer_len; + int path_buffer_len; + + // Just want sink and source + if (!SDL_strcasecmp(media_class, "Audio/Sink")) { + recording = false; + } else if (!SDL_strcasecmp(media_class, "Audio/Source")) { + recording = true; + } else { + return; + } + + node_desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); + node_path = spa_dict_lookup(props, PW_KEY_NODE_NAME); + + if (node_desc && node_path) { + node = node_object_new(id, type, version, &interface_node_events, &interface_core_events); + if (!node) { + SDL_SetError("Pipewire: Failed to allocate interface node"); + return; + } + + // Allocate and initialize the I/O node information struct + desc_buffer_len = SDL_strlen(node_desc) + 1; + path_buffer_len = SDL_strlen(node_path) + 1; + node->userdata = io = SDL_calloc(1, sizeof(struct io_node) + desc_buffer_len + path_buffer_len); + if (!io) { + node_object_destroy(node); + return; + } + + // Begin setting the node properties + io->id = id; + io->recording = recording; + if (io->spec.format == SDL_AUDIO_UNKNOWN) { + io->spec.format = SDL_AUDIO_S16; // we'll go conservative here if for some reason the format isn't known. + } + io->name = io->buf; + io->path = io->buf + desc_buffer_len; + SDL_strlcpy(io->buf, node_desc, desc_buffer_len); + SDL_strlcpy(io->buf + desc_buffer_len, node_path, path_buffer_len); + + // Update sync points + hotplug_core_sync(node); + } + } + } else if (!SDL_strcmp(type, PW_TYPE_INTERFACE_Metadata)) { + node = node_object_new(id, type, version, &metadata_node_events, &metadata_core_events); + if (!node) { + SDL_SetError("Pipewire: Failed to allocate metadata node"); + return; + } + + // Update sync points + hotplug_core_sync(node); + } +} + +static void registry_event_remove_callback(void *object, uint32_t id) +{ + io_list_remove(id); + pending_list_remove(id); +} + +static const struct pw_registry_events registry_events = { PW_VERSION_REGISTRY_EVENTS, .global = registry_event_global_callback, + .global_remove = registry_event_remove_callback }; + +// The hotplug thread +static bool hotplug_loop_init(void) +{ + int res; + + spa_list_init(&hotplug_pending_list); + spa_list_init(&hotplug_io_list); + + hotplug_loop = PIPEWIRE_pw_thread_loop_new("SDLPwAudioPlug", NULL); + if (!hotplug_loop) { + return SDL_SetError("Pipewire: Failed to create hotplug detection loop (%i)", errno); + } + + hotplug_context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_thread_loop_get_loop(hotplug_loop), NULL, 0); + if (!hotplug_context) { + return SDL_SetError("Pipewire: Failed to create hotplug detection context (%i)", errno); + } + + hotplug_core = PIPEWIRE_pw_context_connect(hotplug_context, NULL, 0); + if (!hotplug_core) { + return SDL_SetError("Pipewire: Failed to connect hotplug detection context (%i)", errno); + } + + hotplug_registry = pw_core_get_registry(hotplug_core, PW_VERSION_REGISTRY, 0); + if (!hotplug_registry) { + return SDL_SetError("Pipewire: Failed to acquire hotplug detection registry (%i)", errno); + } + + spa_zero(hotplug_registry_listener); + pw_registry_add_listener(hotplug_registry, &hotplug_registry_listener, ®istry_events, NULL); + + spa_zero(hotplug_core_listener); + pw_core_add_listener(hotplug_core, &hotplug_core_listener, &hotplug_init_core_events, NULL); + + hotplug_init_seq_val = pw_core_sync(hotplug_core, PW_ID_CORE, 0); + + res = PIPEWIRE_pw_thread_loop_start(hotplug_loop); + if (res != 0) { + return SDL_SetError("Pipewire: Failed to start hotplug detection loop"); + } + + return true; +} + +static void hotplug_loop_destroy(void) +{ + if (hotplug_loop) { + PIPEWIRE_pw_thread_loop_stop(hotplug_loop); + } + + pending_list_clear(); + io_list_clear(); + + hotplug_init_complete = false; + hotplug_events_enabled = false; + + if (pipewire_default_sink_id) { + SDL_free(pipewire_default_sink_id); + pipewire_default_sink_id = NULL; + } + if (pipewire_default_source_id) { + SDL_free(pipewire_default_source_id); + pipewire_default_source_id = NULL; + } + + if (hotplug_registry) { + PIPEWIRE_pw_proxy_destroy((struct pw_proxy *)hotplug_registry); + hotplug_registry = NULL; + } + + if (hotplug_core) { + PIPEWIRE_pw_core_disconnect(hotplug_core); + hotplug_core = NULL; + } + + if (hotplug_context) { + PIPEWIRE_pw_context_destroy(hotplug_context); + hotplug_context = NULL; + } + + if (hotplug_loop) { + PIPEWIRE_pw_thread_loop_destroy(hotplug_loop); + hotplug_loop = NULL; + } +} + +static void PIPEWIRE_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + struct io_node *io; + + PIPEWIRE_pw_thread_loop_lock(hotplug_loop); + + // Wait until the initial registry enumeration is complete + if (!hotplug_init_complete) { + PIPEWIRE_pw_thread_loop_wait(hotplug_loop); + } + + spa_list_for_each (io, &hotplug_io_list, link) { + SDL_AudioDevice *device = SDL_AddAudioDevice(io->recording, io->name, &io->spec, PW_ID_TO_HANDLE(io->id)); + if (pipewire_default_sink_id && SDL_strcmp(io->path, pipewire_default_sink_id) == 0) { + if (!io->recording) { + *default_playback = device; + } + } else if (pipewire_default_source_id && SDL_strcmp(io->path, pipewire_default_source_id) == 0) { + if (io->recording) { + *default_recording = device; + } + } + } + + hotplug_events_enabled = true; + + PIPEWIRE_pw_thread_loop_unlock(hotplug_loop); +} + +// Channel maps that match the order in SDL_Audio.h +static const enum spa_audio_channel PIPEWIRE_channel_map_1[] = { SPA_AUDIO_CHANNEL_MONO }; +static const enum spa_audio_channel PIPEWIRE_channel_map_2[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR }; +static const enum spa_audio_channel PIPEWIRE_channel_map_3[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_LFE }; +static const enum spa_audio_channel PIPEWIRE_channel_map_4[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR }; +static const enum spa_audio_channel PIPEWIRE_channel_map_5[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR }; +static const enum spa_audio_channel PIPEWIRE_channel_map_6[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR }; +static const enum spa_audio_channel PIPEWIRE_channel_map_7[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR }; +static const enum spa_audio_channel PIPEWIRE_channel_map_8[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, + SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR }; + +#define COPY_CHANNEL_MAP(c) SDL_memcpy(info->position, PIPEWIRE_channel_map_##c, sizeof(PIPEWIRE_channel_map_##c)) + +static void initialize_spa_info(const SDL_AudioSpec *spec, struct spa_audio_info_raw *info) +{ + info->channels = spec->channels; + info->rate = spec->freq; + + switch (spec->channels) { + case 1: + COPY_CHANNEL_MAP(1); + break; + case 2: + COPY_CHANNEL_MAP(2); + break; + case 3: + COPY_CHANNEL_MAP(3); + break; + case 4: + COPY_CHANNEL_MAP(4); + break; + case 5: + COPY_CHANNEL_MAP(5); + break; + case 6: + COPY_CHANNEL_MAP(6); + break; + case 7: + COPY_CHANNEL_MAP(7); + break; + case 8: + COPY_CHANNEL_MAP(8); + break; + } + + // Pipewire natively supports all of SDL's sample formats + switch (spec->format) { + case SDL_AUDIO_U8: + info->format = SPA_AUDIO_FORMAT_U8; + break; + case SDL_AUDIO_S8: + info->format = SPA_AUDIO_FORMAT_S8; + break; + case SDL_AUDIO_S16LE: + info->format = SPA_AUDIO_FORMAT_S16_LE; + break; + case SDL_AUDIO_S16BE: + info->format = SPA_AUDIO_FORMAT_S16_BE; + break; + case SDL_AUDIO_S32LE: + info->format = SPA_AUDIO_FORMAT_S32_LE; + break; + case SDL_AUDIO_S32BE: + info->format = SPA_AUDIO_FORMAT_S32_BE; + break; + case SDL_AUDIO_F32LE: + info->format = SPA_AUDIO_FORMAT_F32_LE; + break; + case SDL_AUDIO_F32BE: + info->format = SPA_AUDIO_FORMAT_F32_BE; + break; + default: + info->format = SPA_AUDIO_FORMAT_UNKNOWN; + break; + } +} + +static Uint8 *PIPEWIRE_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + // See if a buffer is available. If this returns NULL, SDL_PlaybackAudioThreadIterate will return false, but since we own the thread, it won't kill playback. + // !!! FIXME: It's not clear to me if this ever returns NULL or if this was just defensive coding. + + struct pw_stream *stream = device->hidden->stream; + struct pw_buffer *pw_buf = PIPEWIRE_pw_stream_dequeue_buffer(stream); + if (pw_buf == NULL) { + return NULL; + } + + struct spa_buffer *spa_buf = pw_buf->buffer; + if (spa_buf->datas[0].data == NULL) { + PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf); + return NULL; + } + + device->hidden->pw_buf = pw_buf; + return (Uint8 *) spa_buf->datas[0].data; +} + +static bool PIPEWIRE_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size) +{ + struct pw_stream *stream = device->hidden->stream; + struct pw_buffer *pw_buf = device->hidden->pw_buf; + struct spa_buffer *spa_buf = pw_buf->buffer; + spa_buf->datas[0].chunk->offset = 0; + spa_buf->datas[0].chunk->stride = device->hidden->stride; + spa_buf->datas[0].chunk->size = buffer_size; + + PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf); + device->hidden->pw_buf = NULL; + + return true; +} + +static void output_callback(void *data) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) data; + + // this callback can fire in a background thread during OpenDevice, while we're still blocking + // _with the device lock_ until the stream is ready, causing a deadlock. Write silence in this case. + if (device->hidden->stream_init_status != PW_READY_FLAG_ALL_BITS) { + int bufsize = 0; + Uint8 *buf = PIPEWIRE_GetDeviceBuf(device, &bufsize); + if (buf && bufsize) { + SDL_memset(buf, device->silence_value, bufsize); + } + PIPEWIRE_PlayDevice(device, buf, bufsize); + return; + } + + SDL_PlaybackAudioThreadIterate(device); +} + +static void PIPEWIRE_FlushRecording(SDL_AudioDevice *device) +{ + struct pw_stream *stream = device->hidden->stream; + struct pw_buffer *pw_buf = PIPEWIRE_pw_stream_dequeue_buffer(stream); + if (pw_buf) { // just requeue it without any further thought. + PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf); + } +} + +static int PIPEWIRE_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + struct pw_stream *stream = device->hidden->stream; + struct pw_buffer *pw_buf = PIPEWIRE_pw_stream_dequeue_buffer(stream); + if (!pw_buf) { + return 0; + } + + struct spa_buffer *spa_buf = pw_buf->buffer; + if (!spa_buf) { + PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf); + return 0; + } + + const Uint8 *src = (const Uint8 *)spa_buf->datas[0].data; + const Uint32 offset = SPA_MIN(spa_buf->datas[0].chunk->offset, spa_buf->datas[0].maxsize); + const Uint32 size = SPA_MIN(spa_buf->datas[0].chunk->size, spa_buf->datas[0].maxsize - offset); + const int cpy = SDL_min(buflen, (int) size); + + SDL_assert(size <= buflen); // We'll have to reengineer some stuff if this turns out to not be true. + + SDL_memcpy(buffer, src + offset, cpy); + PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf); + + return cpy; +} + +static void input_callback(void *data) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) data; + + // this callback can fire in a background thread during OpenDevice, while we're still blocking + // _with the device lock_ until the stream is ready, causing a deadlock. Drop data in this case. + if (device->hidden->stream_init_status != PW_READY_FLAG_ALL_BITS) { + PIPEWIRE_FlushRecording(device); + return; + } + + SDL_RecordingAudioThreadIterate(device); +} + +static void stream_add_buffer_callback(void *data, struct pw_buffer *buffer) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) data; + + if (device->recording == false) { + /* Clamp the output spec samples and size to the max size of the Pipewire buffer. + If they exceed the maximum size of the Pipewire buffer, double buffering will be used. */ + if (device->buffer_size > buffer->buffer->datas[0].maxsize) { + SDL_LockMutex(device->lock); + device->sample_frames = buffer->buffer->datas[0].maxsize / device->hidden->stride; + device->buffer_size = buffer->buffer->datas[0].maxsize; + SDL_UnlockMutex(device->lock); + } + } + + device->hidden->stream_init_status |= PW_READY_FLAG_BUFFER_ADDED; + PIPEWIRE_pw_thread_loop_signal(device->hidden->loop, false); +} + +static void stream_state_changed_callback(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) data; + + if (state == PW_STREAM_STATE_STREAMING) { + device->hidden->stream_init_status |= PW_READY_FLAG_STREAM_READY; + } + + if (state == PW_STREAM_STATE_STREAMING || state == PW_STREAM_STATE_ERROR) { + PIPEWIRE_pw_thread_loop_signal(device->hidden->loop, false); + } +} + +static const struct pw_stream_events stream_output_events = { PW_VERSION_STREAM_EVENTS, + .state_changed = stream_state_changed_callback, + .add_buffer = stream_add_buffer_callback, + .process = output_callback }; +static const struct pw_stream_events stream_input_events = { PW_VERSION_STREAM_EVENTS, + .state_changed = stream_state_changed_callback, + .add_buffer = stream_add_buffer_callback, + .process = input_callback }; + +static bool PIPEWIRE_OpenDevice(SDL_AudioDevice *device) +{ + /* + * NOTE: The PW_STREAM_FLAG_RT_PROCESS flag can be set to call the stream + * processing callback from the realtime thread. However, it comes with some + * caveats: no file IO, allocations, locking or other blocking operations + * must occur in the mixer callback. As this cannot be guaranteed when the + * callback is in the calling application, this flag is omitted. + */ + static const enum pw_stream_flags STREAM_FLAGS = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS; + + char thread_name[PW_THREAD_NAME_BUFFER_LENGTH]; + Uint8 pod_buffer[PW_POD_BUFFER_LENGTH]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(pod_buffer, sizeof(pod_buffer)); + struct spa_audio_info_raw spa_info = { 0 }; + const struct spa_pod *params = NULL; + struct SDL_PrivateAudioData *priv; + struct pw_properties *props; + const char *app_name, *icon_name, *app_id, *stream_name, *stream_role, *error; + Uint32 node_id = !device->handle ? PW_ID_ANY : PW_HANDLE_TO_ID(device->handle); + const bool recording = device->recording; + int res; + + // Clamp the period size to sane values + const int min_period = PW_MIN_SAMPLES * SPA_MAX(device->spec.freq / PW_BASE_CLOCK_RATE, 1); + + // Get the hints for the application name, icon name, stream name and role + app_name = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING); + + icon_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_APP_ICON_NAME); + if (!icon_name || *icon_name == '\0') { + icon_name = "applications-games"; + } + + // App ID. Default to NULL if not available. + app_id = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING); + + stream_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_NAME); + if (!stream_name || *stream_name == '\0') { + stream_name = "Audio Stream"; + } + + /* + * 'Music' is the default used internally by Pipewire and it's modules, + * but 'Game' seems more appropriate for the majority of SDL applications. + */ + stream_role = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE); + if (!stream_role || *stream_role == '\0') { + stream_role = "Game"; + } + + // Initialize the Pipewire stream info from the SDL audio spec + initialize_spa_info(&device->spec, &spa_info); + params = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &spa_info); + if (!params) { + return SDL_SetError("Pipewire: Failed to set audio format parameters"); + } + + priv = SDL_calloc(1, sizeof(struct SDL_PrivateAudioData)); + device->hidden = priv; + if (!priv) { + return false; + } + + // Size of a single audio frame in bytes + priv->stride = SDL_AUDIO_FRAMESIZE(device->spec); + + if (device->sample_frames < min_period) { + device->sample_frames = min_period; + } + + SDL_UpdatedAudioDeviceFormat(device); + + SDL_GetAudioThreadName(device, thread_name, sizeof(thread_name)); + priv->loop = PIPEWIRE_pw_thread_loop_new(thread_name, NULL); + if (!priv->loop) { + return SDL_SetError("Pipewire: Failed to create stream loop (%i)", errno); + } + + // Load the realtime module so Pipewire can set the loop thread to the appropriate priority. + props = PIPEWIRE_pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL); + if (!props) { + return SDL_SetError("Pipewire: Failed to create stream context properties (%i)", errno); + } + + priv->context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_thread_loop_get_loop(priv->loop), props, 0); + if (!priv->context) { + return SDL_SetError("Pipewire: Failed to create stream context (%i)", errno); + } + + props = PIPEWIRE_pw_properties_new(NULL, NULL); + if (!props) { + return SDL_SetError("Pipewire: Failed to create stream properties (%i)", errno); + } + + PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_TYPE, "Audio"); + PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, recording ? "Capture" : "Playback"); + PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_ROLE, stream_role); + PIPEWIRE_pw_properties_set(props, PW_KEY_APP_NAME, app_name); + PIPEWIRE_pw_properties_set(props, PW_KEY_APP_ICON_NAME, icon_name); + if (app_id) { + PIPEWIRE_pw_properties_set(props, PW_KEY_APP_ID, app_id); + } + PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_NAME, stream_name); + PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, stream_name); + PIPEWIRE_pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%i", device->sample_frames, device->spec.freq); + PIPEWIRE_pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", device->spec.freq); + PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_ALWAYS_PROCESS, "true"); + PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_DONT_RECONNECT, "true"); // Requesting a specific device, don't migrate to new default hardware. + + if (node_id != PW_ID_ANY) { + PIPEWIRE_pw_thread_loop_lock(hotplug_loop); + const struct io_node *node = io_list_get_by_id(node_id); + if (node) { + PIPEWIRE_pw_properties_set(props, PW_KEY_TARGET_OBJECT, node->path); + } + PIPEWIRE_pw_thread_loop_unlock(hotplug_loop); + } + + // Create the new stream + priv->stream = PIPEWIRE_pw_stream_new_simple(PIPEWIRE_pw_thread_loop_get_loop(priv->loop), stream_name, props, + recording ? &stream_input_events : &stream_output_events, device); + if (!priv->stream) { + return SDL_SetError("Pipewire: Failed to create stream (%i)", errno); + } + + // The target node is passed via PW_KEY_TARGET_OBJECT; target_id is a legacy parameter and must be PW_ID_ANY. + res = PIPEWIRE_pw_stream_connect(priv->stream, recording ? PW_DIRECTION_INPUT : PW_DIRECTION_OUTPUT, PW_ID_ANY, STREAM_FLAGS, + ¶ms, 1); + if (res != 0) { + return SDL_SetError("Pipewire: Failed to connect stream"); + } + + res = PIPEWIRE_pw_thread_loop_start(priv->loop); + if (res != 0) { + return SDL_SetError("Pipewire: Failed to start stream loop"); + } + + // Wait until all pre-open init flags are set or the stream has failed. + PIPEWIRE_pw_thread_loop_lock(priv->loop); + while (priv->stream_init_status != PW_READY_FLAG_ALL_PREOPEN_BITS && + PIPEWIRE_pw_stream_get_state(priv->stream, NULL) != PW_STREAM_STATE_ERROR) { + PIPEWIRE_pw_thread_loop_wait(priv->loop); + } + priv->stream_init_status |= PW_READY_FLAG_OPEN_COMPLETE; + PIPEWIRE_pw_thread_loop_unlock(priv->loop); + + if (PIPEWIRE_pw_stream_get_state(priv->stream, &error) == PW_STREAM_STATE_ERROR) { + return SDL_SetError("Pipewire: Stream error: %s", error); + } + + return true; +} + +static void PIPEWIRE_CloseDevice(SDL_AudioDevice *device) +{ + if (!device->hidden) { + return; + } + + if (device->hidden->loop) { + PIPEWIRE_pw_thread_loop_stop(device->hidden->loop); + } + + if (device->hidden->stream) { + PIPEWIRE_pw_stream_destroy(device->hidden->stream); + } + + if (device->hidden->context) { + PIPEWIRE_pw_context_destroy(device->hidden->context); + } + + if (device->hidden->loop) { + PIPEWIRE_pw_thread_loop_destroy(device->hidden->loop); + } + + SDL_free(device->hidden); + device->hidden = NULL; + + SDL_AudioThreadFinalize(device); +} + +static void PIPEWIRE_DeinitializeStart(void) +{ + if (pipewire_initialized) { + hotplug_loop_destroy(); + } +} + +static void PIPEWIRE_Deinitialize(void) +{ + if (pipewire_initialized) { + hotplug_loop_destroy(); + deinit_pipewire_library(); + pipewire_initialized = false; + } +} + +static bool PipewireInitialize(SDL_AudioDriverImpl *impl) +{ + if (!pipewire_initialized) { + if (!init_pipewire_library()) { + return false; + } + + pipewire_initialized = true; + + if (!hotplug_loop_init()) { + PIPEWIRE_Deinitialize(); + return false; + } + } + + impl->DetectDevices = PIPEWIRE_DetectDevices; + impl->OpenDevice = PIPEWIRE_OpenDevice; + impl->DeinitializeStart = PIPEWIRE_DeinitializeStart; + impl->Deinitialize = PIPEWIRE_Deinitialize; + impl->PlayDevice = PIPEWIRE_PlayDevice; + impl->GetDeviceBuf = PIPEWIRE_GetDeviceBuf; + impl->RecordDevice = PIPEWIRE_RecordDevice; + impl->FlushRecording = PIPEWIRE_FlushRecording; + impl->CloseDevice = PIPEWIRE_CloseDevice; + + impl->HasRecordingSupport = true; + impl->ProvidesOwnCallbackThread = true; + + return true; +} + +static bool PIPEWIRE_PREFERRED_Init(SDL_AudioDriverImpl *impl) +{ + if (!PipewireInitialize(impl)) { + return false; + } + + // run device detection but don't add any devices to SDL; we're just waiting to see if PipeWire sees any devices. If not, fall back to the next backend. + PIPEWIRE_pw_thread_loop_lock(hotplug_loop); + + // Wait until the initial registry enumeration is complete + if (!hotplug_init_complete) { + PIPEWIRE_pw_thread_loop_wait(hotplug_loop); + } + + const bool no_devices = spa_list_is_empty(&hotplug_io_list); + + PIPEWIRE_pw_thread_loop_unlock(hotplug_loop); + + if (no_devices || !pipewire_core_version_at_least(1, 0, 0)) { + PIPEWIRE_Deinitialize(); + return false; + } + + return true; // this will move on to PIPEWIRE_DetectDevices and reuse hotplug_io_list. +} + +static bool PIPEWIRE_Init(SDL_AudioDriverImpl *impl) +{ + return PipewireInitialize(impl); +} + +AudioBootStrap PIPEWIRE_PREFERRED_bootstrap = { + "pipewire", "Pipewire", PIPEWIRE_PREFERRED_Init, false, true +}; +AudioBootStrap PIPEWIRE_bootstrap = { + "pipewire", "Pipewire", PIPEWIRE_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_PIPEWIRE diff --git a/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h b/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h new file mode 100644 index 0000000..e609e25 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h @@ -0,0 +1,43 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifndef SDL_pipewire_h_ +#define SDL_pipewire_h_ + +#include "../SDL_sysaudio.h" +#include + +struct SDL_PrivateAudioData +{ + struct pw_thread_loop *loop; + struct pw_stream *stream; + struct pw_context *context; + + Sint32 stride; // Bytes-per-frame + int stream_init_status; + + // Set in GetDeviceBuf, filled in AudioThreadIterate, queued in PlayDevice + struct pw_buffer *pw_buf; +}; + +#endif // SDL_pipewire_h_ diff --git a/contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.c b/contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.c new file mode 100644 index 0000000..6579f1b --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.c @@ -0,0 +1,159 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "../SDL_sysaudio.h" +#include "SDL_ps2audio.h" + +#include +#include +#include + +static bool PS2AUDIO_OpenDevice(SDL_AudioDevice *device) +{ + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // These are the native supported audio PS2 configs + switch (device->spec.freq) { + case 11025: + case 12000: + case 22050: + case 24000: + case 32000: + case 44100: + case 48000: + break; // acceptable value, keep it + default: + device->spec.freq = 48000; + break; + } + + device->sample_frames = 512; + device->spec.channels = device->spec.channels == 1 ? 1 : 2; + device->spec.format = device->spec.format == SDL_AUDIO_S8 ? SDL_AUDIO_S8 : SDL_AUDIO_S16; + + struct audsrv_fmt_t format; + format.bits = device->spec.format == SDL_AUDIO_S8 ? 8 : 16; + format.freq = device->spec.freq; + format.channels = device->spec.channels; + + device->hidden->channel = audsrv_set_format(&format); + audsrv_set_volume(MAX_VOLUME); + + if (device->hidden->channel < 0) { + return SDL_SetError("Couldn't reserve hardware channel"); + } + + // Update the fragment size as size in bytes. + SDL_UpdatedAudioDeviceFormat(device); + + /* Allocate the mixing buffer. Its size and starting address must + be a multiple of 64 bytes. Our sample count is already a multiple of + 64, so spec->size should be a multiple of 64 as well. */ + const int mixlen = device->buffer_size * NUM_BUFFERS; + device->hidden->rawbuf = (Uint8 *)SDL_aligned_alloc(64, mixlen); + if (!device->hidden->rawbuf) { + return SDL_SetError("Couldn't allocate mixing buffer"); + } + + SDL_memset(device->hidden->rawbuf, device->silence_value, mixlen); + for (int i = 0; i < NUM_BUFFERS; i++) { + device->hidden->mixbufs[i] = &device->hidden->rawbuf[i * device->buffer_size]; + } + + return true; +} + +static bool PS2AUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + // this returns number of bytes accepted or a negative error. We assume anything other than buflen is a fatal error. + return (audsrv_play_audio((char *)buffer, buflen) == buflen); +} + +static bool PS2AUDIO_WaitDevice(SDL_AudioDevice *device) +{ + audsrv_wait_audio(device->buffer_size); + return true; +} + +static Uint8 *PS2AUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + Uint8 *buffer = device->hidden->mixbufs[device->hidden->next_buffer]; + device->hidden->next_buffer = (device->hidden->next_buffer + 1) % NUM_BUFFERS; + return buffer; +} + +static void PS2AUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->channel >= 0) { + audsrv_stop_audio(); + device->hidden->channel = -1; + } + + if (device->hidden->rawbuf) { + SDL_aligned_free(device->hidden->rawbuf); + device->hidden->rawbuf = NULL; + } + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static void PS2AUDIO_ThreadInit(SDL_AudioDevice *device) +{ + /* Increase the priority of this audio thread by 1 to put it + ahead of other SDL threads. */ + const int32_t thid = GetThreadId(); + ee_thread_status_t status; + if (ReferThreadStatus(thid, &status) == 0) { + ChangeThreadPriority(thid, status.current_priority - 1); + } +} + +static void PS2AUDIO_Deinitialize(void) +{ + deinit_audio_driver(); +} + +static bool PS2AUDIO_Init(SDL_AudioDriverImpl *impl) +{ + if (init_audio_driver() < 0) { + return false; + } + + impl->OpenDevice = PS2AUDIO_OpenDevice; + impl->PlayDevice = PS2AUDIO_PlayDevice; + impl->WaitDevice = PS2AUDIO_WaitDevice; + impl->GetDeviceBuf = PS2AUDIO_GetDeviceBuf; + impl->CloseDevice = PS2AUDIO_CloseDevice; + impl->ThreadInit = PS2AUDIO_ThreadInit; + impl->Deinitialize = PS2AUDIO_Deinitialize; + impl->OnlyHasDefaultPlaybackDevice = true; + return true; // this audio target is available. +} + +AudioBootStrap PS2AUDIO_bootstrap = { + "ps2", "PS2 audio driver", PS2AUDIO_Init, false, false +}; diff --git a/contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.h b/contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.h new file mode 100644 index 0000000..5a0ecea --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/ps2/SDL_ps2audio.h @@ -0,0 +1,42 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_ps2audio_h_ +#define SDL_ps2audio_h_ + +#include "../SDL_sysaudio.h" + +#define NUM_BUFFERS 2 + +struct SDL_PrivateAudioData +{ + // The hardware output channel. + int channel; + // The raw allocated mixing buffer. + Uint8 *rawbuf; + // Individual mixing buffers. + Uint8 *mixbufs[NUM_BUFFERS]; + // Index of the next available mixing buffer. + int next_buffer; +}; + +#endif // SDL_ps2audio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.c b/contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.c new file mode 100644 index 0000000..18546e8 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.c @@ -0,0 +1,183 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_PSP + +#include +#include +#include + +#include "../SDL_audiodev_c.h" +#include "../SDL_sysaudio.h" +#include "SDL_pspaudio.h" + +#include +#include + +static bool isBasicAudioConfig(const SDL_AudioSpec *spec) +{ + return spec->freq == 44100; +} + +static bool PSPAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // device only natively supports S16LSB + device->spec.format = SDL_AUDIO_S16LE; + + /* PSP has some limitations with the Audio. It fully supports 44.1KHz (Mono & Stereo), + however with frequencies different than 44.1KHz, it just supports Stereo, + so a resampler must be done for these scenarios */ + if (isBasicAudioConfig(&device->spec)) { + // The sample count must be a multiple of 64. + device->sample_frames = PSP_AUDIO_SAMPLE_ALIGN(device->sample_frames); + // The number of channels (1 or 2). + device->spec.channels = device->spec.channels == 1 ? 1 : 2; + const int format = (device->spec.channels == 1) ? PSP_AUDIO_FORMAT_MONO : PSP_AUDIO_FORMAT_STEREO; + device->hidden->channel = sceAudioChReserve(PSP_AUDIO_NEXT_CHANNEL, device->sample_frames, format); + } else { + // 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11050, 8000 + switch (device->spec.freq) { + case 8000: + case 11025: + case 12000: + case 16000: + case 22050: + case 24000: + case 32000: + case 44100: + case 48000: + break; // acceptable, keep it + default: + device->spec.freq = 48000; + break; + } + // The number of samples to output in one output call (min 17, max 4111). + device->sample_frames = device->sample_frames < 17 ? 17 : (device->sample_frames > 4111 ? 4111 : device->sample_frames); + device->spec.channels = 2; // we're forcing the hardware to stereo. + device->hidden->channel = sceAudioSRCChReserve(device->sample_frames, device->spec.freq, 2); + } + + if (device->hidden->channel < 0) { + return SDL_SetError("Couldn't reserve hardware channel"); + } + + // Update the fragment size as size in bytes. + SDL_UpdatedAudioDeviceFormat(device); + + /* Allocate the mixing buffer. Its size and starting address must + be a multiple of 64 bytes. Our sample count is already a multiple of + 64, so spec->size should be a multiple of 64 as well. */ + const int mixlen = device->buffer_size * NUM_BUFFERS; + device->hidden->rawbuf = (Uint8 *)SDL_aligned_alloc(64, mixlen); + if (!device->hidden->rawbuf) { + return SDL_SetError("Couldn't allocate mixing buffer"); + } + + SDL_memset(device->hidden->rawbuf, device->silence_value, mixlen); + for (int i = 0; i < NUM_BUFFERS; i++) { + device->hidden->mixbufs[i] = &device->hidden->rawbuf[i * device->buffer_size]; + } + + return true; +} + +static bool PSPAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + int rc; + if (!isBasicAudioConfig(&device->spec)) { + SDL_assert(device->spec.channels == 2); + rc = sceAudioSRCOutputBlocking(PSP_AUDIO_VOLUME_MAX, (void *) buffer); + } else { + rc = sceAudioOutputPannedBlocking(device->hidden->channel, PSP_AUDIO_VOLUME_MAX, PSP_AUDIO_VOLUME_MAX, (void *) buffer); + } + return (rc == 0); +} + +static bool PSPAUDIO_WaitDevice(SDL_AudioDevice *device) +{ + return true; // Because we block when sending audio, there's no need for this function to do anything. +} + +static Uint8 *PSPAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + Uint8 *buffer = device->hidden->mixbufs[device->hidden->next_buffer]; + device->hidden->next_buffer = (device->hidden->next_buffer + 1) % NUM_BUFFERS; + return buffer; +} + +static void PSPAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->channel >= 0) { + if (!isBasicAudioConfig(&device->spec)) { + sceAudioSRCChRelease(); + } else { + sceAudioChRelease(device->hidden->channel); + } + device->hidden->channel = -1; + } + + if (device->hidden->rawbuf) { + SDL_aligned_free(device->hidden->rawbuf); + device->hidden->rawbuf = NULL; + } + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static void PSPAUDIO_ThreadInit(SDL_AudioDevice *device) +{ + /* Increase the priority of this audio thread by 1 to put it + ahead of other SDL threads. */ + const SceUID thid = sceKernelGetThreadId(); + SceKernelThreadInfo status; + status.size = sizeof(SceKernelThreadInfo); + if (sceKernelReferThreadStatus(thid, &status) == 0) { + sceKernelChangeThreadPriority(thid, status.currentPriority - 1); + } +} + +static bool PSPAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + impl->OpenDevice = PSPAUDIO_OpenDevice; + impl->PlayDevice = PSPAUDIO_PlayDevice; + impl->WaitDevice = PSPAUDIO_WaitDevice; + impl->GetDeviceBuf = PSPAUDIO_GetDeviceBuf; + impl->CloseDevice = PSPAUDIO_CloseDevice; + impl->ThreadInit = PSPAUDIO_ThreadInit; + impl->OnlyHasDefaultPlaybackDevice = true; + //impl->HasRecordingSupport = true; + //impl->OnlyHasDefaultRecordingDevice = true; + return true; +} + +AudioBootStrap PSPAUDIO_bootstrap = { + "psp", "PSP audio driver", PSPAUDIO_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_PSP diff --git a/contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.h b/contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.h new file mode 100644 index 0000000..d7b2056 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/psp/SDL_pspaudio.h @@ -0,0 +1,41 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef SDL_pspaudio_h_ +#define SDL_pspaudio_h_ + +#include "../SDL_sysaudio.h" + +#define NUM_BUFFERS 2 + +struct SDL_PrivateAudioData +{ + // The hardware output channel. + int channel; + // The raw allocated mixing buffer. + Uint8 *rawbuf; + // Individual mixing buffers. + Uint8 *mixbufs[NUM_BUFFERS]; + // Index of the next available mixing buffer. + int next_buffer; +}; + +#endif // SDL_pspaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.c b/contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.c new file mode 100644 index 0000000..049a5ec --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.c @@ -0,0 +1,1037 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_PULSEAUDIO + +// Allow access to a raw mixing buffer + +#ifdef HAVE_SIGNAL_H +#include +#endif +#include +#include + +#include "../SDL_sysaudio.h" +#include "SDL_pulseaudio.h" +#include "../../thread/SDL_systhread.h" + +#if (PA_PROTOCOL_VERSION < 28) +typedef void (*pa_operation_notify_cb_t) (pa_operation *o, void *userdata); +#endif + +typedef struct PulseDeviceHandle +{ + char *device_path; + uint32_t device_index; +} PulseDeviceHandle; + +// should we include monitors in the device list? Set at SDL_Init time +static bool include_monitors = false; + +static pa_threaded_mainloop *pulseaudio_threaded_mainloop = NULL; +static pa_context *pulseaudio_context = NULL; +static SDL_Thread *pulseaudio_hotplug_thread = NULL; +static SDL_AtomicInt pulseaudio_hotplug_thread_active; + +// These are the OS identifiers (i.e. ALSA strings)...these are allocated in a callback +// when the default changes, and noticed by the hotplug thread when it alerts SDL +// to the change. +static char *default_sink_path = NULL; +static char *default_source_path = NULL; +static bool default_sink_changed = false; +static bool default_source_changed = false; + + +static const char *(*PULSEAUDIO_pa_get_library_version)(void); +static pa_channel_map *(*PULSEAUDIO_pa_channel_map_init_auto)( + pa_channel_map *, unsigned, pa_channel_map_def_t); +static const char *(*PULSEAUDIO_pa_strerror)(int); +static pa_proplist *(*PULSEAUDIO_pa_proplist_new)(void); +static void (*PULSEAUDIO_pa_proplist_free)(pa_proplist *); +static int (*PULSEAUDIO_pa_proplist_sets)(pa_proplist *, const char *, const char *); + +static pa_threaded_mainloop *(*PULSEAUDIO_pa_threaded_mainloop_new)(void); +static void (*PULSEAUDIO_pa_threaded_mainloop_set_name)(pa_threaded_mainloop *, const char *); +static pa_mainloop_api *(*PULSEAUDIO_pa_threaded_mainloop_get_api)(pa_threaded_mainloop *); +static int (*PULSEAUDIO_pa_threaded_mainloop_start)(pa_threaded_mainloop *); +static void (*PULSEAUDIO_pa_threaded_mainloop_stop)(pa_threaded_mainloop *); +static void (*PULSEAUDIO_pa_threaded_mainloop_lock)(pa_threaded_mainloop *); +static void (*PULSEAUDIO_pa_threaded_mainloop_unlock)(pa_threaded_mainloop *); +static void (*PULSEAUDIO_pa_threaded_mainloop_wait)(pa_threaded_mainloop *); +static void (*PULSEAUDIO_pa_threaded_mainloop_signal)(pa_threaded_mainloop *, int); +static void (*PULSEAUDIO_pa_threaded_mainloop_free)(pa_threaded_mainloop *); + +static pa_operation_state_t (*PULSEAUDIO_pa_operation_get_state)( + const pa_operation *); +static void (*PULSEAUDIO_pa_operation_set_state_callback)(pa_operation *, pa_operation_notify_cb_t, void *); +static void (*PULSEAUDIO_pa_operation_cancel)(pa_operation *); +static void (*PULSEAUDIO_pa_operation_unref)(pa_operation *); + +static pa_context *(*PULSEAUDIO_pa_context_new_with_proplist)(pa_mainloop_api *, + const char *, + const pa_proplist *); +static void (*PULSEAUDIO_pa_context_set_state_callback)(pa_context *, pa_context_notify_cb_t, void *); +static int (*PULSEAUDIO_pa_context_connect)(pa_context *, const char *, + pa_context_flags_t, const pa_spawn_api *); +static pa_operation *(*PULSEAUDIO_pa_context_get_sink_info_list)(pa_context *, pa_sink_info_cb_t, void *); +static pa_operation *(*PULSEAUDIO_pa_context_get_source_info_list)(pa_context *, pa_source_info_cb_t, void *); +static pa_operation *(*PULSEAUDIO_pa_context_get_sink_info_by_index)(pa_context *, uint32_t, pa_sink_info_cb_t, void *); +static pa_operation *(*PULSEAUDIO_pa_context_get_source_info_by_index)(pa_context *, uint32_t, pa_source_info_cb_t, void *); +static pa_context_state_t (*PULSEAUDIO_pa_context_get_state)(const pa_context *); +static pa_operation *(*PULSEAUDIO_pa_context_subscribe)(pa_context *, pa_subscription_mask_t, pa_context_success_cb_t, void *); +static void (*PULSEAUDIO_pa_context_set_subscribe_callback)(pa_context *, pa_context_subscribe_cb_t, void *); +static void (*PULSEAUDIO_pa_context_disconnect)(pa_context *); +static void (*PULSEAUDIO_pa_context_unref)(pa_context *); + +static pa_stream *(*PULSEAUDIO_pa_stream_new)(pa_context *, const char *, + const pa_sample_spec *, const pa_channel_map *); +static void (*PULSEAUDIO_pa_stream_set_state_callback)(pa_stream *, pa_stream_notify_cb_t, void *); +static int (*PULSEAUDIO_pa_stream_connect_playback)(pa_stream *, const char *, + const pa_buffer_attr *, pa_stream_flags_t, const pa_cvolume *, pa_stream *); +static int (*PULSEAUDIO_pa_stream_connect_record)(pa_stream *, const char *, + const pa_buffer_attr *, pa_stream_flags_t); +static const pa_buffer_attr *(*PULSEAUDIO_pa_stream_get_buffer_attr)(pa_stream *); +static pa_stream_state_t (*PULSEAUDIO_pa_stream_get_state)(const pa_stream *); +static size_t (*PULSEAUDIO_pa_stream_writable_size)(const pa_stream *); +static size_t (*PULSEAUDIO_pa_stream_readable_size)(const pa_stream *); +static int (*PULSEAUDIO_pa_stream_write)(pa_stream *, const void *, size_t, + pa_free_cb_t, int64_t, pa_seek_mode_t); +static int (*PULSEAUDIO_pa_stream_begin_write)(pa_stream *, void **, size_t *); +static pa_operation *(*PULSEAUDIO_pa_stream_drain)(pa_stream *, + pa_stream_success_cb_t, void *); +static int (*PULSEAUDIO_pa_stream_peek)(pa_stream *, const void **, size_t *); +static int (*PULSEAUDIO_pa_stream_drop)(pa_stream *); +static pa_operation *(*PULSEAUDIO_pa_stream_flush)(pa_stream *, + pa_stream_success_cb_t, void *); +static int (*PULSEAUDIO_pa_stream_disconnect)(pa_stream *); +static void (*PULSEAUDIO_pa_stream_unref)(pa_stream *); +static void (*PULSEAUDIO_pa_stream_set_write_callback)(pa_stream *, pa_stream_request_cb_t, void *); +static void (*PULSEAUDIO_pa_stream_set_read_callback)(pa_stream *, pa_stream_request_cb_t, void *); +static pa_operation *(*PULSEAUDIO_pa_context_get_server_info)(pa_context *, pa_server_info_cb_t, void *); + +static bool load_pulseaudio_syms(void); + +#ifdef SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC + +static const char *pulseaudio_library = SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC; +static SDL_SharedObject *pulseaudio_handle = NULL; + +static bool load_pulseaudio_sym(const char *fn, void **addr) +{ + *addr = SDL_LoadFunction(pulseaudio_handle, fn); + if (!*addr) { + // Don't call SDL_SetError(): SDL_LoadFunction already did. + return false; + } + + return true; +} + +// cast funcs to char* first, to please GCC's strict aliasing rules. +#define SDL_PULSEAUDIO_SYM(x) \ + if (!load_pulseaudio_sym(#x, (void **)(char *)&PULSEAUDIO_##x)) \ + return false + +static void UnloadPulseAudioLibrary(void) +{ + if (pulseaudio_handle) { + SDL_UnloadObject(pulseaudio_handle); + pulseaudio_handle = NULL; + } +} + +static bool LoadPulseAudioLibrary(void) +{ + bool result = true; + if (!pulseaudio_handle) { + pulseaudio_handle = SDL_LoadObject(pulseaudio_library); + if (!pulseaudio_handle) { + result = false; + // Don't call SDL_SetError(): SDL_LoadObject already did. + } else { + result = load_pulseaudio_syms(); + if (!result) { + UnloadPulseAudioLibrary(); + } + } + } + return result; +} + +#else + +#define SDL_PULSEAUDIO_SYM(x) PULSEAUDIO_##x = x + +static void UnloadPulseAudioLibrary(void) +{ +} + +static bool LoadPulseAudioLibrary(void) +{ + load_pulseaudio_syms(); + return true; +} + +#endif // SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC + +static bool load_pulseaudio_syms(void) +{ + SDL_PULSEAUDIO_SYM(pa_get_library_version); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_new); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_get_api); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_start); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_stop); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_lock); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_unlock); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_wait); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_signal); + SDL_PULSEAUDIO_SYM(pa_threaded_mainloop_free); + SDL_PULSEAUDIO_SYM(pa_operation_get_state); + SDL_PULSEAUDIO_SYM(pa_operation_cancel); + SDL_PULSEAUDIO_SYM(pa_operation_unref); + SDL_PULSEAUDIO_SYM(pa_context_new_with_proplist); + SDL_PULSEAUDIO_SYM(pa_context_set_state_callback); + SDL_PULSEAUDIO_SYM(pa_context_connect); + SDL_PULSEAUDIO_SYM(pa_context_get_sink_info_list); + SDL_PULSEAUDIO_SYM(pa_context_get_source_info_list); + SDL_PULSEAUDIO_SYM(pa_context_get_sink_info_by_index); + SDL_PULSEAUDIO_SYM(pa_context_get_source_info_by_index); + SDL_PULSEAUDIO_SYM(pa_context_get_state); + SDL_PULSEAUDIO_SYM(pa_context_subscribe); + SDL_PULSEAUDIO_SYM(pa_context_set_subscribe_callback); + SDL_PULSEAUDIO_SYM(pa_context_disconnect); + SDL_PULSEAUDIO_SYM(pa_context_unref); + SDL_PULSEAUDIO_SYM(pa_stream_new); + SDL_PULSEAUDIO_SYM(pa_stream_set_state_callback); + SDL_PULSEAUDIO_SYM(pa_stream_connect_playback); + SDL_PULSEAUDIO_SYM(pa_stream_connect_record); + SDL_PULSEAUDIO_SYM(pa_stream_get_buffer_attr); + SDL_PULSEAUDIO_SYM(pa_stream_get_state); + SDL_PULSEAUDIO_SYM(pa_stream_writable_size); + SDL_PULSEAUDIO_SYM(pa_stream_readable_size); + SDL_PULSEAUDIO_SYM(pa_stream_begin_write); + SDL_PULSEAUDIO_SYM(pa_stream_write); + SDL_PULSEAUDIO_SYM(pa_stream_drain); + SDL_PULSEAUDIO_SYM(pa_stream_disconnect); + SDL_PULSEAUDIO_SYM(pa_stream_peek); + SDL_PULSEAUDIO_SYM(pa_stream_drop); + SDL_PULSEAUDIO_SYM(pa_stream_flush); + SDL_PULSEAUDIO_SYM(pa_stream_unref); + SDL_PULSEAUDIO_SYM(pa_channel_map_init_auto); + SDL_PULSEAUDIO_SYM(pa_strerror); + SDL_PULSEAUDIO_SYM(pa_stream_set_write_callback); + SDL_PULSEAUDIO_SYM(pa_stream_set_read_callback); + SDL_PULSEAUDIO_SYM(pa_context_get_server_info); + SDL_PULSEAUDIO_SYM(pa_proplist_new); + SDL_PULSEAUDIO_SYM(pa_proplist_free); + SDL_PULSEAUDIO_SYM(pa_proplist_sets); + + // optional +#ifdef SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC + load_pulseaudio_sym("pa_operation_set_state_callback", (void **)(char *)&PULSEAUDIO_pa_operation_set_state_callback); // needs pulseaudio 4.0 + load_pulseaudio_sym("pa_threaded_mainloop_set_name", (void **)(char *)&PULSEAUDIO_pa_threaded_mainloop_set_name); // needs pulseaudio 5.0 +#elif (PA_PROTOCOL_VERSION >= 29) + PULSEAUDIO_pa_operation_set_state_callback = pa_operation_set_state_callback; + PULSEAUDIO_pa_threaded_mainloop_set_name = pa_threaded_mainloop_set_name; +#elif (PA_PROTOCOL_VERSION >= 28) + PULSEAUDIO_pa_operation_set_state_callback = pa_operation_set_state_callback; + PULSEAUDIO_pa_threaded_mainloop_set_name = NULL; +#else + PULSEAUDIO_pa_operation_set_state_callback = NULL; + PULSEAUDIO_pa_threaded_mainloop_set_name = NULL; +#endif + + return true; +} + +static const char *getAppName(void) +{ + return SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING); +} + +static void OperationStateChangeCallback(pa_operation *o, void *userdata) +{ + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); // just signal any waiting code, it can look up the details. +} + +/* This function assume you are holding `mainloop`'s lock. The operation is unref'd in here, assuming + you did the work in the callback and just want to know it's done, though. */ +static void WaitForPulseOperation(pa_operation *o) +{ + // This checks for NO errors currently. Either fix that, check results elsewhere, or do things you don't care about. + SDL_assert(pulseaudio_threaded_mainloop != NULL); + if (o) { + // note that if PULSEAUDIO_pa_operation_set_state_callback == NULL, then `o` must have a callback that will signal pulseaudio_threaded_mainloop. + // If not, on really old (earlier PulseAudio 4.0, from the year 2013!) installs, this call will block forever. + // On more modern installs, we won't ever block forever, and maybe be more efficient, thanks to pa_operation_set_state_callback. + // WARNING: at the time of this writing: the Steam Runtime is still on PulseAudio 1.1! + if (PULSEAUDIO_pa_operation_set_state_callback) { + PULSEAUDIO_pa_operation_set_state_callback(o, OperationStateChangeCallback, NULL); + } + while (PULSEAUDIO_pa_operation_get_state(o) == PA_OPERATION_RUNNING) { + PULSEAUDIO_pa_threaded_mainloop_wait(pulseaudio_threaded_mainloop); // this releases the lock and blocks on an internal condition variable. + } + PULSEAUDIO_pa_operation_unref(o); + } +} + +static void DisconnectFromPulseServer(void) +{ + if (pulseaudio_threaded_mainloop) { + PULSEAUDIO_pa_threaded_mainloop_stop(pulseaudio_threaded_mainloop); + } + if (pulseaudio_context) { + PULSEAUDIO_pa_context_disconnect(pulseaudio_context); + PULSEAUDIO_pa_context_unref(pulseaudio_context); + pulseaudio_context = NULL; + } + if (pulseaudio_threaded_mainloop) { + PULSEAUDIO_pa_threaded_mainloop_free(pulseaudio_threaded_mainloop); + pulseaudio_threaded_mainloop = NULL; + } +} + +static void PulseContextStateChangeCallback(pa_context *context, void *userdata) +{ + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); // just signal any waiting code, it can look up the details. +} + +static bool ConnectToPulseServer(void) +{ + pa_mainloop_api *mainloop_api = NULL; + pa_proplist *proplist = NULL; + const char *icon_name; + int state = 0; + + SDL_assert(pulseaudio_threaded_mainloop == NULL); + SDL_assert(pulseaudio_context == NULL); + + // Set up a new main loop + pulseaudio_threaded_mainloop = PULSEAUDIO_pa_threaded_mainloop_new(); + if (!pulseaudio_threaded_mainloop) { + return SDL_SetError("pa_threaded_mainloop_new() failed"); + } + + if (PULSEAUDIO_pa_threaded_mainloop_set_name) { + PULSEAUDIO_pa_threaded_mainloop_set_name(pulseaudio_threaded_mainloop, "PulseMainloop"); + } + + if (PULSEAUDIO_pa_threaded_mainloop_start(pulseaudio_threaded_mainloop) < 0) { + PULSEAUDIO_pa_threaded_mainloop_free(pulseaudio_threaded_mainloop); + pulseaudio_threaded_mainloop = NULL; + return SDL_SetError("pa_threaded_mainloop_start() failed"); + } + + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + + mainloop_api = PULSEAUDIO_pa_threaded_mainloop_get_api(pulseaudio_threaded_mainloop); + SDL_assert(mainloop_api != NULL); // this never fails, right? + + proplist = PULSEAUDIO_pa_proplist_new(); + if (!proplist) { + SDL_SetError("pa_proplist_new() failed"); + goto failed; + } + + icon_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_APP_ICON_NAME); + if (!icon_name || *icon_name == '\0') { + icon_name = "applications-games"; + } + PULSEAUDIO_pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, icon_name); + + pulseaudio_context = PULSEAUDIO_pa_context_new_with_proplist(mainloop_api, getAppName(), proplist); + if (!pulseaudio_context) { + SDL_SetError("pa_context_new_with_proplist() failed"); + goto failed; + } + PULSEAUDIO_pa_proplist_free(proplist); + + PULSEAUDIO_pa_context_set_state_callback(pulseaudio_context, PulseContextStateChangeCallback, NULL); + + // Connect to the PulseAudio server + if (PULSEAUDIO_pa_context_connect(pulseaudio_context, NULL, 0, NULL) < 0) { + SDL_SetError("Could not setup connection to PulseAudio"); + goto failed; + } + + state = PULSEAUDIO_pa_context_get_state(pulseaudio_context); + while (PA_CONTEXT_IS_GOOD(state) && (state != PA_CONTEXT_READY)) { + PULSEAUDIO_pa_threaded_mainloop_wait(pulseaudio_threaded_mainloop); + state = PULSEAUDIO_pa_context_get_state(pulseaudio_context); + } + + if (state != PA_CONTEXT_READY) { + SDL_SetError("Could not connect to PulseAudio"); + goto failed; + } + + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + + return true; // connected and ready! + +failed: + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + DisconnectFromPulseServer(); + return false; +} + +static void WriteCallback(pa_stream *p, size_t nbytes, void *userdata) +{ + struct SDL_PrivateAudioData *h = (struct SDL_PrivateAudioData *)userdata; + //SDL_Log("PULSEAUDIO WRITE CALLBACK! nbytes=%u", (unsigned int) nbytes); + h->bytes_requested += nbytes; + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); +} + +// This function waits until it is possible to write a full sound buffer +static bool PULSEAUDIO_WaitDevice(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *h = device->hidden; + bool result = true; + + //SDL_Log("PULSEAUDIO PLAYDEVICE START! mixlen=%d", available); + + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + + while (!SDL_GetAtomicInt(&device->shutdown) && (h->bytes_requested == 0)) { + //SDL_Log("PULSEAUDIO WAIT IN WAITDEVICE!"); + PULSEAUDIO_pa_threaded_mainloop_wait(pulseaudio_threaded_mainloop); + + if ((PULSEAUDIO_pa_context_get_state(pulseaudio_context) != PA_CONTEXT_READY) || (PULSEAUDIO_pa_stream_get_state(h->stream) != PA_STREAM_READY)) { + //SDL_Log("PULSEAUDIO DEVICE FAILURE IN WAITDEVICE!"); + result = false; + break; + } + } + + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + + return result; +} + +static bool PULSEAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size) +{ + struct SDL_PrivateAudioData *h = device->hidden; + + //SDL_Log("PULSEAUDIO PLAYDEVICE START! mixlen=%d", available); + + SDL_assert(h->bytes_requested >= buffer_size); + + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + const int rc = PULSEAUDIO_pa_stream_write(h->stream, buffer, buffer_size, NULL, 0LL, PA_SEEK_RELATIVE); + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + + if (rc < 0) { + return false; + } + + //SDL_Log("PULSEAUDIO FEED! nbytes=%d", buffer_size); + h->bytes_requested -= buffer_size; + + //SDL_Log("PULSEAUDIO PLAYDEVICE END! written=%d", written); + return true; +} + +static Uint8 *PULSEAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + struct SDL_PrivateAudioData *h = device->hidden; + const size_t reqsize = (size_t) SDL_min(*buffer_size, h->bytes_requested); + size_t nbytes = reqsize; + void *data = NULL; + if (PULSEAUDIO_pa_stream_begin_write(h->stream, &data, &nbytes) == 0) { + *buffer_size = (int) nbytes; + return (Uint8 *) data; + } + + // don't know why this would fail, but we'll fall back just in case. + *buffer_size = (int) reqsize; + return device->hidden->mixbuf; +} + +static void ReadCallback(pa_stream *p, size_t nbytes, void *userdata) +{ + //SDL_Log("PULSEAUDIO READ CALLBACK! nbytes=%u", (unsigned int) nbytes); + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); // the recording code queries what it needs, we just need to signal to end any wait +} + +static bool PULSEAUDIO_WaitRecordingDevice(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *h = device->hidden; + + if (h->recordingbuf) { + return true; // there's still data available to read. + } + + bool result = true; + + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + + while (!SDL_GetAtomicInt(&device->shutdown)) { + PULSEAUDIO_pa_threaded_mainloop_wait(pulseaudio_threaded_mainloop); + if ((PULSEAUDIO_pa_context_get_state(pulseaudio_context) != PA_CONTEXT_READY) || (PULSEAUDIO_pa_stream_get_state(h->stream) != PA_STREAM_READY)) { + //SDL_Log("PULSEAUDIO DEVICE FAILURE IN WAITRECORDINGDEVICE!"); + result = false; + break; + } else if (PULSEAUDIO_pa_stream_readable_size(h->stream) > 0) { + // a new fragment is available! + const void *data = NULL; + size_t nbytes = 0; + PULSEAUDIO_pa_stream_peek(h->stream, &data, &nbytes); + SDL_assert(nbytes > 0); + if (!data) { // If NULL, then the buffer had a hole, ignore that + PULSEAUDIO_pa_stream_drop(h->stream); // drop this fragment. + } else { + // store this fragment's data for use with RecordDevice + //SDL_Log("PULSEAUDIO: recorded %d new bytes", (int) nbytes); + h->recordingbuf = (const Uint8 *)data; + h->recordinglen = nbytes; + break; + } + } + } + + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + + return result; +} + +static int PULSEAUDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + struct SDL_PrivateAudioData *h = device->hidden; + + if (h->recordingbuf) { + const int cpy = SDL_min(buflen, h->recordinglen); + if (cpy > 0) { + //SDL_Log("PULSEAUDIO: fed %d recorded bytes", cpy); + SDL_memcpy(buffer, h->recordingbuf, cpy); + h->recordingbuf += cpy; + h->recordinglen -= cpy; + } + if (h->recordinglen == 0) { + h->recordingbuf = NULL; + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); // don't know if you _have_ to lock for this, but just in case. + PULSEAUDIO_pa_stream_drop(h->stream); // done with this fragment. + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + } + return cpy; // new data, return it. + } + + return 0; +} + +static void PULSEAUDIO_FlushRecording(SDL_AudioDevice *device) +{ + struct SDL_PrivateAudioData *h = device->hidden; + const void *data = NULL; + size_t nbytes = 0, buflen = 0; + + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + + if (h->recordingbuf) { + PULSEAUDIO_pa_stream_drop(h->stream); + h->recordingbuf = NULL; + h->recordinglen = 0; + } + + buflen = PULSEAUDIO_pa_stream_readable_size(h->stream); + while (!SDL_GetAtomicInt(&device->shutdown) && (buflen > 0)) { + PULSEAUDIO_pa_threaded_mainloop_wait(pulseaudio_threaded_mainloop); + if ((PULSEAUDIO_pa_context_get_state(pulseaudio_context) != PA_CONTEXT_READY) || (PULSEAUDIO_pa_stream_get_state(h->stream) != PA_STREAM_READY)) { + //SDL_Log("PULSEAUDIO DEVICE FAILURE IN FLUSHRECORDING!"); + SDL_AudioDeviceDisconnected(device); + break; + } + + // a fragment of audio present before FlushCapture was call is + // still available! Just drop it. + PULSEAUDIO_pa_stream_peek(h->stream, &data, &nbytes); + PULSEAUDIO_pa_stream_drop(h->stream); + buflen -= nbytes; + } + + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); +} + +static void PULSEAUDIO_CloseDevice(SDL_AudioDevice *device) +{ + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + + if (device->hidden->stream) { + if (device->hidden->recordingbuf) { + PULSEAUDIO_pa_stream_drop(device->hidden->stream); + } + PULSEAUDIO_pa_stream_disconnect(device->hidden->stream); + PULSEAUDIO_pa_stream_unref(device->hidden->stream); + } + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); // in case the device thread is waiting somewhere, this will unblock it. + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + + SDL_free(device->hidden->mixbuf); + SDL_free(device->hidden); +} + +static void PulseStreamStateChangeCallback(pa_stream *stream, void *userdata) +{ + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); // just signal any waiting code, it can look up the details. +} + +static bool PULSEAUDIO_OpenDevice(SDL_AudioDevice *device) +{ + const bool recording = device->recording; + struct SDL_PrivateAudioData *h = NULL; + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts; + pa_sample_spec paspec; + pa_buffer_attr paattr; + pa_channel_map pacmap; + pa_stream_flags_t flags = 0; + int format = PA_SAMPLE_INVALID; + bool result = true; + + SDL_assert(pulseaudio_threaded_mainloop != NULL); + SDL_assert(pulseaudio_context != NULL); + + // Initialize all variables that we clean on shutdown + h = device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // Try for a closest match on audio format + closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { +#ifdef DEBUG_AUDIO + SDL_Log("pulseaudio: Trying format 0x%4.4x", test_format); +#endif + switch (test_format) { + case SDL_AUDIO_U8: + format = PA_SAMPLE_U8; + break; + case SDL_AUDIO_S16LE: + format = PA_SAMPLE_S16LE; + break; + case SDL_AUDIO_S16BE: + format = PA_SAMPLE_S16BE; + break; + case SDL_AUDIO_S32LE: + format = PA_SAMPLE_S32LE; + break; + case SDL_AUDIO_S32BE: + format = PA_SAMPLE_S32BE; + break; + case SDL_AUDIO_F32LE: + format = PA_SAMPLE_FLOAT32LE; + break; + case SDL_AUDIO_F32BE: + format = PA_SAMPLE_FLOAT32BE; + break; + default: + continue; + } + break; + } + if (!test_format) { + return SDL_SetError("pulseaudio: Unsupported audio format"); + } + device->spec.format = test_format; + paspec.format = format; + + // Calculate the final parameters for this audio specification + SDL_UpdatedAudioDeviceFormat(device); + + // Allocate mixing buffer + if (!recording) { + h->mixbuf = (Uint8 *)SDL_malloc(device->buffer_size); + if (!h->mixbuf) { + return false; + } + SDL_memset(h->mixbuf, device->silence_value, device->buffer_size); + } + + paspec.channels = device->spec.channels; + paspec.rate = device->spec.freq; + + // Reduced prebuffering compared to the defaults. + paattr.fragsize = device->buffer_size; // despite the name, this is only used for recording devices, according to PulseAudio docs! + paattr.tlength = device->buffer_size; + paattr.prebuf = -1; + paattr.maxlength = -1; + paattr.minreq = -1; + flags |= PA_STREAM_ADJUST_LATENCY; + + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + + const char *name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_NAME); + // The SDL ALSA output hints us that we use Windows' channel mapping + // https://bugzilla.libsdl.org/show_bug.cgi?id=110 + PULSEAUDIO_pa_channel_map_init_auto(&pacmap, device->spec.channels, PA_CHANNEL_MAP_WAVEEX); + + h->stream = PULSEAUDIO_pa_stream_new( + pulseaudio_context, + (name && *name) ? name : "Audio Stream", // stream description + &paspec, // sample format spec + &pacmap // channel map + ); + + if (!h->stream) { + result = SDL_SetError("Could not set up PulseAudio stream"); + } else { + int rc; + + PULSEAUDIO_pa_stream_set_state_callback(h->stream, PulseStreamStateChangeCallback, NULL); + + // SDL manages device moves if the default changes, so don't ever let Pulse automatically migrate this stream. + flags |= PA_STREAM_DONT_MOVE; + + const char *device_path = ((PulseDeviceHandle *) device->handle)->device_path; + if (recording) { + PULSEAUDIO_pa_stream_set_read_callback(h->stream, ReadCallback, h); + rc = PULSEAUDIO_pa_stream_connect_record(h->stream, device_path, &paattr, flags); + } else { + PULSEAUDIO_pa_stream_set_write_callback(h->stream, WriteCallback, h); + rc = PULSEAUDIO_pa_stream_connect_playback(h->stream, device_path, &paattr, flags, NULL, NULL); + } + + if (rc < 0) { + result = SDL_SetError("Could not connect PulseAudio stream"); + } else { + int state = PULSEAUDIO_pa_stream_get_state(h->stream); + while (PA_STREAM_IS_GOOD(state) && (state != PA_STREAM_READY)) { + PULSEAUDIO_pa_threaded_mainloop_wait(pulseaudio_threaded_mainloop); + state = PULSEAUDIO_pa_stream_get_state(h->stream); + } + + if (!PA_STREAM_IS_GOOD(state)) { + result = SDL_SetError("Could not connect PulseAudio stream"); + } else { + const pa_buffer_attr *actual_bufattr = PULSEAUDIO_pa_stream_get_buffer_attr(h->stream); + if (!actual_bufattr) { + result = SDL_SetError("Could not determine connected PulseAudio stream's buffer attributes"); + } else { + device->buffer_size = (int) recording ? actual_bufattr->tlength : actual_bufattr->fragsize; + device->sample_frames = device->buffer_size / SDL_AUDIO_FRAMESIZE(device->spec); + } + } + } + } + + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + + // We're (hopefully) ready to rock and roll. :-) + return result; +} + +// device handles are device index + 1, cast to void*, so we never pass a NULL. + +static SDL_AudioFormat PulseFormatToSDLFormat(pa_sample_format_t format) +{ + switch (format) { + case PA_SAMPLE_U8: + return SDL_AUDIO_U8; + case PA_SAMPLE_S16LE: + return SDL_AUDIO_S16LE; + case PA_SAMPLE_S16BE: + return SDL_AUDIO_S16BE; + case PA_SAMPLE_S32LE: + return SDL_AUDIO_S32LE; + case PA_SAMPLE_S32BE: + return SDL_AUDIO_S32BE; + case PA_SAMPLE_FLOAT32LE: + return SDL_AUDIO_F32LE; + case PA_SAMPLE_FLOAT32BE: + return SDL_AUDIO_F32BE; + default: + return 0; + } +} + +static void AddPulseAudioDevice(const bool recording, const char *description, const char *name, const uint32_t index, const pa_sample_spec *sample_spec) +{ + SDL_AudioSpec spec; + SDL_zero(spec); + spec.format = PulseFormatToSDLFormat(sample_spec->format); + spec.channels = sample_spec->channels; + spec.freq = sample_spec->rate; + PulseDeviceHandle *handle = (PulseDeviceHandle *) SDL_malloc(sizeof (PulseDeviceHandle)); + if (handle) { + handle->device_path = SDL_strdup(name); + if (!handle->device_path) { + SDL_free(handle); + } else { + handle->device_index = index; + SDL_AddAudioDevice(recording, description, &spec, handle); + } + } +} + +// This is called when PulseAudio adds an playback ("sink") device. +static void SinkInfoCallback(pa_context *c, const pa_sink_info *i, int is_last, void *data) +{ + if (i) { + AddPulseAudioDevice(false, i->description, i->name, i->index, &i->sample_spec); + } + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); +} + +// This is called when PulseAudio adds a recording ("source") device. +static void SourceInfoCallback(pa_context *c, const pa_source_info *i, int is_last, void *data) +{ + // Maybe skip "monitor" sources. These are just output from other sinks. + if (i && (include_monitors || (i->monitor_of_sink == PA_INVALID_INDEX))) { + AddPulseAudioDevice(true, i->description, i->name, i->index, &i->sample_spec); + } + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); +} + +static void ServerInfoCallback(pa_context *c, const pa_server_info *i, void *data) +{ + //SDL_Log("PULSEAUDIO ServerInfoCallback!"); + + if (!default_sink_path || (SDL_strcmp(default_sink_path, i->default_sink_name) != 0)) { + char *str = SDL_strdup(i->default_sink_name); + if (str) { + SDL_free(default_sink_path); + default_sink_path = str; + default_sink_changed = true; + } + } + + if (!default_source_path || (SDL_strcmp(default_source_path, i->default_source_name) != 0)) { + char *str = SDL_strdup(i->default_source_name); + if (str) { + SDL_free(default_source_path); + default_source_path = str; + default_source_changed = true; + } + } + + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); +} + +static bool FindAudioDeviceByIndex(SDL_AudioDevice *device, void *userdata) +{ + const uint32_t idx = (uint32_t) (uintptr_t) userdata; + const PulseDeviceHandle *handle = (const PulseDeviceHandle *) device->handle; + return (handle->device_index == idx); +} + +static bool FindAudioDeviceByPath(SDL_AudioDevice *device, void *userdata) +{ + const char *path = (const char *) userdata; + const PulseDeviceHandle *handle = (const PulseDeviceHandle *) device->handle; + return (SDL_strcmp(handle->device_path, path) == 0); +} + +// This is called when PulseAudio has a device connected/removed/changed. +static void HotplugCallback(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *data) +{ + const bool added = ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW); + const bool removed = ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE); + const bool changed = ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE); + + if (added || removed || changed) { // we only care about add/remove events. + const bool sink = ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK); + const bool source = ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE); + + if (changed) { + PULSEAUDIO_pa_operation_unref(PULSEAUDIO_pa_context_get_server_info(pulseaudio_context, ServerInfoCallback, NULL)); + } + + /* adds need sink details from the PulseAudio server. Another callback... + (just unref all these operations right away, because we aren't going to wait on them + and their callbacks will handle any work, so they can free as soon as that happens.) */ + if (added && sink) { + PULSEAUDIO_pa_operation_unref(PULSEAUDIO_pa_context_get_sink_info_by_index(pulseaudio_context, idx, SinkInfoCallback, NULL)); + } else if (added && source) { + PULSEAUDIO_pa_operation_unref(PULSEAUDIO_pa_context_get_source_info_by_index(pulseaudio_context, idx, SourceInfoCallback, NULL)); + } else if (removed && (sink || source)) { + // removes we can handle just with the device index. + SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByCallback(FindAudioDeviceByIndex, (void *)(uintptr_t)idx)); + } + } + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); +} + +static bool CheckDefaultDevice(const bool changed, char *device_path) +{ + if (!changed) { + return false; // nothing's happening, leave the flag marked as unchanged. + } else if (!device_path) { + return true; // check again later, we don't have a device name... + } + + SDL_AudioDevice *device = SDL_FindPhysicalAudioDeviceByCallback(FindAudioDeviceByPath, device_path); + if (device) { // if NULL, we might still be waiting for a SinkInfoCallback or something, we'll try later. + SDL_DefaultAudioDeviceChanged(device); + return false; // changing complete, set flag to unchanged for future tests. + } + return true; // couldn't find the changed device, leave it marked as changed to try again later. +} + +// this runs as a thread while the Pulse target is initialized to catch hotplug events. +static int SDLCALL HotplugThread(void *data) +{ + pa_operation *op; + + SDL_SetCurrentThreadPriority(SDL_THREAD_PRIORITY_LOW); + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + PULSEAUDIO_pa_context_set_subscribe_callback(pulseaudio_context, HotplugCallback, NULL); + + // don't WaitForPulseOperation on the subscription; when it's done we'll be able to get hotplug events, but waiting doesn't changing anything. + op = PULSEAUDIO_pa_context_subscribe(pulseaudio_context, PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_SERVER, NULL, NULL); + + SDL_SignalSemaphore((SDL_Semaphore *) data); + + while (SDL_GetAtomicInt(&pulseaudio_hotplug_thread_active)) { + PULSEAUDIO_pa_threaded_mainloop_wait(pulseaudio_threaded_mainloop); + if (op && PULSEAUDIO_pa_operation_get_state(op) != PA_OPERATION_RUNNING) { + PULSEAUDIO_pa_operation_unref(op); + op = NULL; + } + + // Update default devices; don't hold the pulse lock during this, since it could deadlock vs a playing device that we're about to lock here. + bool check_default_sink = default_sink_changed; + bool check_default_source = default_source_changed; + char *current_default_sink = check_default_sink ? SDL_strdup(default_sink_path) : NULL; + char *current_default_source = check_default_source ? SDL_strdup(default_source_path) : NULL; + default_sink_changed = default_source_changed = false; + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + check_default_sink = CheckDefaultDevice(check_default_sink, current_default_sink); + check_default_source = CheckDefaultDevice(check_default_source, current_default_source); + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + + // free our copies (which will be NULL if nothing changed) + SDL_free(current_default_sink); + SDL_free(current_default_source); + + // set these to true if we didn't handle the change OR there was _another_ change while we were working unlocked. + default_sink_changed = (default_sink_changed || check_default_sink); + default_source_changed = (default_source_changed || check_default_source); + } + + if (op) { + PULSEAUDIO_pa_operation_unref(op); + } + + PULSEAUDIO_pa_context_set_subscribe_callback(pulseaudio_context, NULL, NULL); + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + return 0; +} + +static void PULSEAUDIO_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + SDL_Semaphore *ready_sem = SDL_CreateSemaphore(0); + + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + WaitForPulseOperation(PULSEAUDIO_pa_context_get_server_info(pulseaudio_context, ServerInfoCallback, NULL)); + WaitForPulseOperation(PULSEAUDIO_pa_context_get_sink_info_list(pulseaudio_context, SinkInfoCallback, NULL)); + WaitForPulseOperation(PULSEAUDIO_pa_context_get_source_info_list(pulseaudio_context, SourceInfoCallback, NULL)); + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + + if (default_sink_path) { + *default_playback = SDL_FindPhysicalAudioDeviceByCallback(FindAudioDeviceByPath, default_sink_path); + } + + if (default_source_path) { + *default_recording = SDL_FindPhysicalAudioDeviceByCallback(FindAudioDeviceByPath, default_source_path); + } + + // ok, we have a sane list, let's set up hotplug notifications now... + SDL_SetAtomicInt(&pulseaudio_hotplug_thread_active, 1); + pulseaudio_hotplug_thread = SDL_CreateThread(HotplugThread, "PulseHotplug", ready_sem); + if (pulseaudio_hotplug_thread) { + SDL_WaitSemaphore(ready_sem); // wait until the thread hits it's main loop. + } else { + SDL_SetAtomicInt(&pulseaudio_hotplug_thread_active, 0); // thread failed to start, we'll go on without hotplug. + } + + SDL_DestroySemaphore(ready_sem); +} + +static void PULSEAUDIO_FreeDeviceHandle(SDL_AudioDevice *device) +{ + PulseDeviceHandle *handle = (PulseDeviceHandle *) device->handle; + SDL_free(handle->device_path); + SDL_free(handle); +} + +static void PULSEAUDIO_DeinitializeStart(void) +{ + if (pulseaudio_hotplug_thread) { + PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop); + SDL_SetAtomicInt(&pulseaudio_hotplug_thread_active, 0); + PULSEAUDIO_pa_threaded_mainloop_signal(pulseaudio_threaded_mainloop, 0); + PULSEAUDIO_pa_threaded_mainloop_unlock(pulseaudio_threaded_mainloop); + SDL_WaitThread(pulseaudio_hotplug_thread, NULL); + pulseaudio_hotplug_thread = NULL; + } +} + +static void PULSEAUDIO_Deinitialize(void) +{ + DisconnectFromPulseServer(); + + SDL_free(default_sink_path); + default_sink_path = NULL; + default_sink_changed = false; + SDL_free(default_source_path); + default_source_path = NULL; + default_source_changed = false; + + UnloadPulseAudioLibrary(); +} + +static bool PULSEAUDIO_Init(SDL_AudioDriverImpl *impl) +{ + if (!LoadPulseAudioLibrary()) { + return false; + } else if (!ConnectToPulseServer()) { + UnloadPulseAudioLibrary(); + return false; + } + + include_monitors = SDL_GetHintBoolean(SDL_HINT_AUDIO_INCLUDE_MONITORS, false); + + impl->DetectDevices = PULSEAUDIO_DetectDevices; + impl->OpenDevice = PULSEAUDIO_OpenDevice; + impl->PlayDevice = PULSEAUDIO_PlayDevice; + impl->WaitDevice = PULSEAUDIO_WaitDevice; + impl->GetDeviceBuf = PULSEAUDIO_GetDeviceBuf; + impl->CloseDevice = PULSEAUDIO_CloseDevice; + impl->DeinitializeStart = PULSEAUDIO_DeinitializeStart; + impl->Deinitialize = PULSEAUDIO_Deinitialize; + impl->WaitRecordingDevice = PULSEAUDIO_WaitRecordingDevice; + impl->RecordDevice = PULSEAUDIO_RecordDevice; + impl->FlushRecording = PULSEAUDIO_FlushRecording; + impl->FreeDeviceHandle = PULSEAUDIO_FreeDeviceHandle; + + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap PULSEAUDIO_bootstrap = { + "pulseaudio", "PulseAudio", PULSEAUDIO_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_PULSEAUDIO diff --git a/contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.h b/contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.h new file mode 100644 index 0000000..9ea44c0 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.h @@ -0,0 +1,44 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_pulseaudio_h_ +#define SDL_pulseaudio_h_ + +#include + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + // pulseaudio structures + pa_stream *stream; + + // Raw mixing buffer + Uint8 *mixbuf; + + int bytes_requested; // bytes of data the hardware wants _now_. + + const Uint8 *recordingbuf; + int recordinglen; +}; + +#endif // SDL_pulseaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.c b/contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.c new file mode 100644 index 0000000..a31bea4 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.c @@ -0,0 +1,451 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +// !!! FIXME: can this target support hotplugging? + +#include "../../SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_QNX + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SDL3/SDL_timer.h" +#include "SDL3/SDL_audio.h" +#include "../../core/unix/SDL_poll.h" +#include "../SDL_sysaudio.h" +#include "SDL_qsa_audio.h" + +// default channel communication parameters +#define DEFAULT_CPARAMS_RATE 44100 +#define DEFAULT_CPARAMS_VOICES 1 + +#define DEFAULT_CPARAMS_FRAG_SIZE 4096 +#define DEFAULT_CPARAMS_FRAGS_MIN 1 +#define DEFAULT_CPARAMS_FRAGS_MAX 1 + +#define QSA_MAX_NAME_LENGTH 81+16 // Hardcoded in QSA, can't be changed + +static bool QSA_SetError(const char *fn, int status) +{ + return SDL_SetError("QSA: %s() failed: %s", fn, snd_strerror(status)); +} + +// !!! FIXME: does this need to be here? Does the SDL version not work? +static void QSA_ThreadInit(SDL_AudioDevice *device) +{ + // Increase default 10 priority to 25 to avoid jerky sound + struct sched_param param; + if (SchedGet(0, 0, ¶m) != -1) { + param.sched_priority = param.sched_curpriority + 15; + SchedSet(0, 0, SCHED_NOCHANGE, ¶m); + } +} + +// PCM channel parameters initialize function +static void QSA_InitAudioParams(snd_pcm_channel_params_t * cpars) +{ + SDL_zerop(cpars); + cpars->channel = SND_PCM_CHANNEL_PLAYBACK; + cpars->mode = SND_PCM_MODE_BLOCK; + cpars->start_mode = SND_PCM_START_DATA; + cpars->stop_mode = SND_PCM_STOP_STOP; + cpars->format.format = SND_PCM_SFMT_S16_LE; + cpars->format.interleave = 1; + cpars->format.rate = DEFAULT_CPARAMS_RATE; + cpars->format.voices = DEFAULT_CPARAMS_VOICES; + cpars->buf.block.frag_size = DEFAULT_CPARAMS_FRAG_SIZE; + cpars->buf.block.frags_min = DEFAULT_CPARAMS_FRAGS_MIN; + cpars->buf.block.frags_max = DEFAULT_CPARAMS_FRAGS_MAX; +} + +// This function waits until it is possible to write a full sound buffer +static bool QSA_WaitDevice(SDL_AudioDevice *device) +{ + // Setup timeout for playing one fragment equal to 2 seconds + // If timeout occurred than something wrong with hardware or driver + // For example, Vortex 8820 audio driver stucks on second DAC because + // it doesn't exist ! + const int result = SDL_IOReady(device->hidden->audio_fd, + device->recording ? SDL_IOR_READ : SDL_IOR_WRITE, + 2 * 1000); + switch (result) { + case -1: + SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "QSA: SDL_IOReady() failed: %s", strerror(errno)); + return false; + case 0: + device->hidden->timeout_on_wait = true; // !!! FIXME: Should we just disconnect the device in this case? + break; + default: + device->hidden->timeout_on_wait = false; + break; + } + + return true; +} + +static bool QSA_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + if (SDL_GetAtomicInt(&device->shutdown) || !device->hidden) { + return true; + } + + int towrite = buflen; + + // Write the audio data, checking for EAGAIN (buffer full) and underrun + while ((towrite > 0) && !SDL_GetAtomicInt(&device->shutdown)); + const int bw = snd_pcm_plugin_write(device->hidden->audio_handle, buffer, towrite); + if (bw != towrite) { + // Check if samples playback got stuck somewhere in hardware or in the audio device driver + if ((errno == EAGAIN) && (bw == 0)) { + if (device->hidden->timeout_on_wait) { + return true; // oh well, try again next time. !!! FIXME: Should we just disconnect the device in this case? + } + } + + // Check for errors or conditions + if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { + SDL_Delay(1); // Let a little CPU time go by and try to write again + + // if we wrote some data + towrite -= bw; + buffer += bw * device->spec.channels; + continue; + } else if ((errno == EINVAL) || (errno == EIO)) { + snd_pcm_channel_status_t cstatus; + SDL_zero(cstatus); + cstatus.channel = device->recording ? SND_PCM_CHANNEL_CAPTURE : SND_PCM_CHANNEL_PLAYBACK; + + int status = snd_pcm_plugin_status(device->hidden->audio_handle, &cstatus); + if (status < 0) { + QSA_SetError("snd_pcm_plugin_status", status); + return false; + } else if ((cstatus.status == SND_PCM_STATUS_UNDERRUN) || (cstatus.status == SND_PCM_STATUS_READY)) { + status = snd_pcm_plugin_prepare(device->hidden->audio_handle, device->recording ? SND_PCM_CHANNEL_CAPTURE : SND_PCM_CHANNEL_PLAYBACK); + if (status < 0) { + QSA_SetError("snd_pcm_plugin_prepare", status); + return false; + } + } + continue; + } else { + return false; + } + } else { + // we wrote all remaining data + towrite -= bw; + buffer += bw * device->spec.channels; + } + } + + // If we couldn't write, assume fatal error for now + return (towrite == 0); +} + +static Uint8 *QSA_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->hidden->pcm_buf; +} + +static void QSA_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->audio_handle) { + #if _NTO_VERSION < 710 + // Finish playing available samples or cancel unread samples during recording + snd_pcm_plugin_flush(device->hidden->audio_handle, device->recording ? SND_PCM_CHANNEL_CAPTURE : SND_PCM_CHANNEL_PLAYBACK); + #endif + snd_pcm_close(device->hidden->audio_handle); + } + + SDL_free(device->hidden->pcm_buf); + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static bool QSA_OpenDevice(SDL_AudioDevice *device) +{ + if (device->recording) { + return SDL_SetError("SDL recording support isn't available on QNX atm"); // !!! FIXME: most of this code has support for recording devices, but there's no RecordDevice, etc functions. Fill them in! + } + + SDL_assert(device->handle != NULL); // NULL used to mean "system default device" in SDL2; it does not mean that in SDL3. + const Uint32 sdlhandle = (Uint32) ((size_t) device->handle); + const uint32_t cardno = (uint32_t) (sdlhandle & 0xFFFF); + const uint32_t deviceno = (uint32_t) ((sdlhandle >> 16) & 0xFFFF); + const bool recording = device->recording; + int status = 0; + + // Initialize all variables that we clean on shutdown + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, (sizeof (struct SDL_PrivateAudioData))); + if (device->hidden == NULL) { + return false; + } + + // Initialize channel transfer parameters to default + snd_pcm_channel_params_t cparams; + QSA_InitAudioParams(&cparams); + + // Open requested audio device + status = snd_pcm_open(&device->hidden->audio_handle, cardno, deviceno, recording ? SND_PCM_OPEN_CAPTURE : SND_PCM_OPEN_PLAYBACK); + if (status < 0) { + device->hidden->audio_handle = NULL; + return QSA_SetError("snd_pcm_open", status); + } + + // Try for a closest match on audio format + SDL_AudioFormat test_format = 0; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { + // if match found set format to equivalent QSA format + switch (test_format) { + #define CHECKFMT(sdlfmt, qsafmt) case SDL_AUDIO_##sdlfmt: cparams.format.format = SND_PCM_SFMT_##qsafmt; break + CHECKFMT(U8, U8); + CHECKFMT(S8, S8); + CHECKFMT(S16LSB, S16_LE); + CHECKFMT(S16MSB, S16_BE); + CHECKFMT(S32LSB, S32_LE); + CHECKFMT(S32MSB, S32_BE); + CHECKFMT(F32LSB, FLOAT_LE); + CHECKFMT(F32MSB, FLOAT_BE); + #undef CHECKFMT + default: continue; + } + break; + } + + // assumes test_format not 0 on success + if (test_format == 0) { + return SDL_SetError("QSA: Couldn't find any hardware audio formats"); + } + + device->spec.format = test_format; + + // Set mono/stereo/4ch/6ch/8ch audio + cparams.format.voices = device->spec.channels; + + // Set rate + cparams.format.rate = device->spec.freq; + + // Setup the transfer parameters according to cparams + status = snd_pcm_plugin_params(device->hidden->audio_handle, &cparams); + if (status < 0) { + return QSA_SetError("snd_pcm_plugin_params", status); + } + + // Make sure channel is setup right one last time + snd_pcm_channel_setup_t csetup; + SDL_zero(csetup); + csetup.channel = recording ? SND_PCM_CHANNEL_CAPTURE : SND_PCM_CHANNEL_PLAYBACK; + if (snd_pcm_plugin_setup(device->hidden->audio_handle, &csetup) < 0) { + return SDL_SetError("QSA: Unable to setup channel"); + } + + device->sample_frames = csetup.buf.block.frag_size; + + // Calculate the final parameters for this audio specification + SDL_UpdatedAudioDeviceFormat(device); + + device->hidden->pcm_buf = (Uint8 *) SDL_malloc(device->buffer_size); + if (device->hidden->pcm_buf == NULL) { + return false; + } + SDL_memset(device->hidden->pcm_buf, device->silence_value, device->buffer_size); + + // get the file descriptor + device->hidden->audio_fd = snd_pcm_file_descriptor(device->hidden->audio_handle, csetup.channel); + if (device->hidden->audio_fd < 0) { + return QSA_SetError("snd_pcm_file_descriptor", device->hidden->audio_fd); + } + + // Prepare an audio channel + status = snd_pcm_plugin_prepare(device->hidden->audio_handle, csetup.channel) + if (status < 0) { + return QSA_SetError("snd_pcm_plugin_prepare", status); + } + + return true; // We're really ready to rock and roll. :-) +} + +static SDL_AudioFormat QnxFormatToSDLFormat(const int32_t qnxfmt) +{ + switch (qnxfmt) { + #define CHECKFMT(sdlfmt, qsafmt) case SND_PCM_SFMT_##qsafmt: return SDL_AUDIO_##sdlfmt + CHECKFMT(U8, U8); + CHECKFMT(S8, S8); + CHECKFMT(S16LSB, S16_LE); + CHECKFMT(S16MSB, S16_BE); + CHECKFMT(S32LSB, S32_LE); + CHECKFMT(S32MSB, S32_BE); + CHECKFMT(F32LSB, FLOAT_LE); + CHECKFMT(F32MSB, FLOAT_BE); + #undef CHECKFMT + default: break; + } + return SDL_AUDIO_S16; // oh well. +} + +static void QSA_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + // Detect amount of available devices + // this value can be changed in the runtime + int num_cards = 0; + (void) snd_cards_list(NULL, 0, &alloc_num_cards); + bool isstack = false; + int *cards = SDL_small_alloc(int, num_cards, &isstack); + if (!cards) { + return; // we're in trouble. + } + int overflow_cards = 0; + const int total_num_cards = snd_cards_list(cards, num_cards, &overflow_cards); + // if overflow_cards > 0 or total_num_cards > num_cards, it changed at the last moment; oh well, we lost some. + num_cards = SDL_min(num_cards, total_num_cards); // ...but make sure it didn't _shrink_. + + // If io-audio manager is not running we will get 0 as number of available audio devices + if (num_cards == 0) { // not any available audio devices? + SDL_small_free(cards, isstack); + return; + } + + // Find requested devices by type + for (int it = 0; it < num_cards; it++) { + const int card = cards[it]; + for (uint32_t deviceno = 0; ; deviceno++) { + int32_t status; + char name[QSA_MAX_NAME_LENGTH]; + + status = snd_card_get_longname(card, name, sizeof (name)); + if (status == EOK) { + snd_pcm_t *handle; + + // Add device number to device name + char fullname[QSA_MAX_NAME_LENGTH + 32]; + SDL_snprintf(fullname, sizeof (fullname), "%s d%d", name, (int) deviceno); + + // Check if this device id could play anything + bool recording = false; + status = snd_pcm_open(&handle, card, deviceno, SND_PCM_OPEN_PLAYBACK); + if (status != EOK) { // no? See if it's a recording device instead. + #if 0 // !!! FIXME: most of this code has support for recording devices, but there's no RecordDevice, etc functions. Fill them in! + status = snd_pcm_open(&handle, card, deviceno, SND_PCM_OPEN_CAPTURE); + if (status == EOK) { + recording = true; + } + #endif + } + + if (status == EOK) { + SDL_AudioSpec spec; + SDL_zero(spec); + SDL_AudioSpec *pspec = &spec; + snd_pcm_channel_setup_t csetup; + SDL_zero(csetup); + csetup.channel = recording ? SND_PCM_CHANNEL_CAPTURE : SND_PCM_CHANNEL_PLAYBACK; + + if (snd_pcm_plugin_setup(device->hidden->audio_handle, &csetup) < 0) { + pspec = NULL; // go on without spec info. + } else { + spec.format = QnxFormatToSDLFormat(csetup.format.format); + spec.channels = csetup.format.channels; + spec.freq = csetup.format.rate; + } + + status = snd_pcm_close(handle); + if (status == EOK) { + // !!! FIXME: I'm assuming each of these values are way less than 0xFFFF. Fix this if not. + SDL_assert(card <= 0xFFFF); + SDL_assert(deviceno <= 0xFFFF); + const Uint32 sdlhandle = ((Uint32) card) | (((Uint32) deviceno) << 16); + SDL_AddAudioDevice(recording, fullname, pspec, (void *) ((size_t) sdlhandle)); + } + } else { + // Check if we got end of devices list + if (status == -ENOENT) { + break; + } + } + } else { + break; + } + } + } + + SDL_small_free(cards, isstack); + + // Try to open the "preferred" devices, which will tell us the card/device pairs for the default devices. + snd_pcm_t handle; + int cardno, deviceno; + if (snd_pcm_open_preferred(&handle, &cardno, &deviceno, SND_PCM_OPEN_PLAYBACK) == 0) { + snd_pcm_close(handle); + // !!! FIXME: I'm assuming each of these values are way less than 0xFFFF. Fix this if not. + SDL_assert(cardno <= 0xFFFF); + SDL_assert(deviceno <= 0xFFFF); + const Uint32 sdlhandle = ((Uint32) card) | (((Uint32) deviceno) << 16); + *default_playback = SDL_FindPhysicalAudioDeviceByHandle((void *) ((size_t) sdlhandle)); + } + + if (snd_pcm_open_preferred(&handle, &cardno, &deviceno, SND_PCM_OPEN_CAPTURE) == 0) { + snd_pcm_close(handle); + // !!! FIXME: I'm assuming each of these values are way less than 0xFFFF. Fix this if not. + SDL_assert(cardno <= 0xFFFF); + SDL_assert(deviceno <= 0xFFFF); + const Uint32 sdlhandle = ((Uint32) card) | (((Uint32) deviceno) << 16); + *default_recording = SDL_FindPhysicalAudioDeviceByHandle((void *) ((size_t) sdlhandle)); + } +} + +static void QSA_Deinitialize(void) +{ + // nothing to do here atm. +} + +static bool QSA_Init(SDL_AudioDriverImpl * impl) +{ + impl->DetectDevices = QSA_DetectDevices; + impl->OpenDevice = QSA_OpenDevice; + impl->ThreadInit = QSA_ThreadInit; + impl->WaitDevice = QSA_WaitDevice; + impl->PlayDevice = QSA_PlayDevice; + impl->GetDeviceBuf = QSA_GetDeviceBuf; + impl->CloseDevice = QSA_CloseDevice; + impl->Deinitialize = QSA_Deinitialize; + + // !!! FIXME: most of this code has support for recording devices, but there's no RecordDevice, etc functions. Fill them in! + //impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap QSAAUDIO_bootstrap = { + "qsa", "QNX QSA Audio", QSA_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_QNX + diff --git a/contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.h b/contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.h new file mode 100644 index 0000000..902752c --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/qnx/SDL_qsa_audio.h @@ -0,0 +1,40 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "../../SDL_internal.h" + +#ifndef __SDL_QSA_AUDIO_H__ +#define __SDL_QSA_AUDIO_H__ + +#include + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + snd_pcm_t *audio_handle; // The audio device handle + int audio_fd; // The audio file descriptor, for selecting on + bool timeout_on_wait; // Select timeout status + Uint8 *pcm_buf; // Raw mixing buffer +}; + +#endif // __SDL_QSA_AUDIO_H__ + diff --git a/contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.c b/contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.c new file mode 100644 index 0000000..a0d2020 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.c @@ -0,0 +1,356 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_SNDIO + +// OpenBSD sndio target + +#ifdef HAVE_STDIO_H +#include +#endif + +#ifdef HAVE_SIGNAL_H +#include +#endif + +#include +#include + +#include "../SDL_sysaudio.h" +#include "SDL_sndioaudio.h" + +#ifdef SDL_AUDIO_DRIVER_SNDIO_DYNAMIC +#endif + +#ifndef INFTIM +#define INFTIM -1 +#endif + +#ifndef SIO_DEVANY +#define SIO_DEVANY "default" +#endif + +static struct sio_hdl *(*SNDIO_sio_open)(const char *, unsigned int, int); +static void (*SNDIO_sio_close)(struct sio_hdl *); +static int (*SNDIO_sio_setpar)(struct sio_hdl *, struct sio_par *); +static int (*SNDIO_sio_getpar)(struct sio_hdl *, struct sio_par *); +static int (*SNDIO_sio_start)(struct sio_hdl *); +static int (*SNDIO_sio_stop)(struct sio_hdl *); +static size_t (*SNDIO_sio_read)(struct sio_hdl *, void *, size_t); +static size_t (*SNDIO_sio_write)(struct sio_hdl *, const void *, size_t); +static int (*SNDIO_sio_nfds)(struct sio_hdl *); +static int (*SNDIO_sio_pollfd)(struct sio_hdl *, struct pollfd *, int); +static int (*SNDIO_sio_revents)(struct sio_hdl *, struct pollfd *); +static int (*SNDIO_sio_eof)(struct sio_hdl *); +static void (*SNDIO_sio_initpar)(struct sio_par *); + +#ifdef SDL_AUDIO_DRIVER_SNDIO_DYNAMIC +static const char *sndio_library = SDL_AUDIO_DRIVER_SNDIO_DYNAMIC; +static SDL_SharedObject *sndio_handle = NULL; + +static bool load_sndio_sym(const char *fn, void **addr) +{ + *addr = SDL_LoadFunction(sndio_handle, fn); + if (!*addr) { + return false; // Don't call SDL_SetError(): SDL_LoadFunction already did. + } + + return true; +} + +// cast funcs to char* first, to please GCC's strict aliasing rules. +#define SDL_SNDIO_SYM(x) \ + if (!load_sndio_sym(#x, (void **)(char *)&SNDIO_##x)) \ + return false +#else +#define SDL_SNDIO_SYM(x) SNDIO_##x = x +#endif + +static bool load_sndio_syms(void) +{ + SDL_SNDIO_SYM(sio_open); + SDL_SNDIO_SYM(sio_close); + SDL_SNDIO_SYM(sio_setpar); + SDL_SNDIO_SYM(sio_getpar); + SDL_SNDIO_SYM(sio_start); + SDL_SNDIO_SYM(sio_stop); + SDL_SNDIO_SYM(sio_read); + SDL_SNDIO_SYM(sio_write); + SDL_SNDIO_SYM(sio_nfds); + SDL_SNDIO_SYM(sio_pollfd); + SDL_SNDIO_SYM(sio_revents); + SDL_SNDIO_SYM(sio_eof); + SDL_SNDIO_SYM(sio_initpar); + return true; +} + +#undef SDL_SNDIO_SYM + +#ifdef SDL_AUDIO_DRIVER_SNDIO_DYNAMIC + +static void UnloadSNDIOLibrary(void) +{ + if (sndio_handle) { + SDL_UnloadObject(sndio_handle); + sndio_handle = NULL; + } +} + +static bool LoadSNDIOLibrary(void) +{ + bool result = true; + if (!sndio_handle) { + sndio_handle = SDL_LoadObject(sndio_library); + if (!sndio_handle) { + result = false; // Don't call SDL_SetError(): SDL_LoadObject already did. + } else { + result = load_sndio_syms(); + if (!result) { + UnloadSNDIOLibrary(); + } + } + } + return result; +} + +#else + +static void UnloadSNDIOLibrary(void) +{ +} + +static bool LoadSNDIOLibrary(void) +{ + load_sndio_syms(); + return true; +} + +#endif // SDL_AUDIO_DRIVER_SNDIO_DYNAMIC + +static bool SNDIO_WaitDevice(SDL_AudioDevice *device) +{ + const bool recording = device->recording; + + while (!SDL_GetAtomicInt(&device->shutdown)) { + if (SNDIO_sio_eof(device->hidden->dev)) { + return false; + } + + const int nfds = SNDIO_sio_pollfd(device->hidden->dev, device->hidden->pfd, recording ? POLLIN : POLLOUT); + if (nfds <= 0 || poll(device->hidden->pfd, nfds, 10) < 0) { + return false; + } + + const int revents = SNDIO_sio_revents(device->hidden->dev, device->hidden->pfd); + if (recording && (revents & POLLIN)) { + break; + } else if (!recording && (revents & POLLOUT)) { + break; + } else if (revents & POLLHUP) { + return false; + } + } + + return true; +} + +static bool SNDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + // !!! FIXME: this should be non-blocking so we can check device->shutdown. + // this is set to blocking, because we _have_ to send the entire buffer down, but hopefully WaitDevice took most of the delay time. + if (SNDIO_sio_write(device->hidden->dev, buffer, buflen) != buflen) { + return false; // If we couldn't write, assume fatal error for now + } +#ifdef DEBUG_AUDIO + fprintf(stderr, "Wrote %d bytes of audio data\n", written); +#endif + return true; +} + +static int SNDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + // We set recording devices non-blocking; this can safely return 0 in SDL3, but we'll check for EOF to cause a device disconnect. + const size_t br = SNDIO_sio_read(device->hidden->dev, buffer, buflen); + if ((br == 0) && SNDIO_sio_eof(device->hidden->dev)) { + return -1; + } + return (int) br; +} + +static void SNDIO_FlushRecording(SDL_AudioDevice *device) +{ + char buf[512]; + while (!SDL_GetAtomicInt(&device->shutdown) && (SNDIO_sio_read(device->hidden->dev, buf, sizeof(buf)) > 0)) { + // do nothing + } +} + +static Uint8 *SNDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + return device->hidden->mixbuf; +} + +static void SNDIO_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->dev) { + SNDIO_sio_stop(device->hidden->dev); + SNDIO_sio_close(device->hidden->dev); + } + SDL_free(device->hidden->pfd); + SDL_free(device->hidden->mixbuf); + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static bool SNDIO_OpenDevice(SDL_AudioDevice *device) +{ + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + // Recording devices must be non-blocking for SNDIO_FlushRecording + device->hidden->dev = SNDIO_sio_open(SIO_DEVANY, + device->recording ? SIO_REC : SIO_PLAY, device->recording); + if (!device->hidden->dev) { + return SDL_SetError("sio_open() failed"); + } + + device->hidden->pfd = SDL_malloc(sizeof(struct pollfd) * SNDIO_sio_nfds(device->hidden->dev)); + if (!device->hidden->pfd) { + return false; + } + + struct sio_par par; + SNDIO_sio_initpar(&par); + + par.rate = device->spec.freq; + par.pchan = device->spec.channels; + par.round = device->sample_frames; + par.appbufsz = par.round * 2; + + // Try for a closest match on audio format + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { + if (!SDL_AUDIO_ISFLOAT(test_format)) { + par.le = SDL_AUDIO_ISLITTLEENDIAN(test_format) ? 1 : 0; + par.sig = SDL_AUDIO_ISSIGNED(test_format) ? 1 : 0; + par.bits = SDL_AUDIO_BITSIZE(test_format); + + if (SNDIO_sio_setpar(device->hidden->dev, &par) == 0) { + continue; + } + if (SNDIO_sio_getpar(device->hidden->dev, &par) == 0) { + return SDL_SetError("sio_getpar() failed"); + } + if (par.bps != SIO_BPS(par.bits)) { + continue; + } + if ((par.bits == 8 * par.bps) || (par.msb)) { + break; + } + } + } + + if (!test_format) { + return SDL_SetError("sndio: Unsupported audio format"); + } + + if ((par.bps == 4) && (par.sig) && (par.le)) { + device->spec.format = SDL_AUDIO_S32LE; + } else if ((par.bps == 4) && (par.sig) && (!par.le)) { + device->spec.format = SDL_AUDIO_S32BE; + } else if ((par.bps == 2) && (par.sig) && (par.le)) { + device->spec.format = SDL_AUDIO_S16LE; + } else if ((par.bps == 2) && (par.sig) && (!par.le)) { + device->spec.format = SDL_AUDIO_S16BE; + } else if ((par.bps == 1) && (par.sig)) { + device->spec.format = SDL_AUDIO_S8; + } else if ((par.bps == 1) && (!par.sig)) { + device->spec.format = SDL_AUDIO_U8; + } else { + return SDL_SetError("sndio: Got unsupported hardware audio format."); + } + + device->spec.freq = par.rate; + device->spec.channels = par.pchan; + device->sample_frames = par.round; + + // Calculate the final parameters for this audio specification + SDL_UpdatedAudioDeviceFormat(device); + + // Allocate mixing buffer + device->hidden->mixbuf = (Uint8 *)SDL_malloc(device->buffer_size); + if (!device->hidden->mixbuf) { + return false; + } + SDL_memset(device->hidden->mixbuf, device->silence_value, device->buffer_size); + + if (!SNDIO_sio_start(device->hidden->dev)) { + return SDL_SetError("sio_start() failed"); + } + + return true; // We're ready to rock and roll. :-) +} + +static void SNDIO_Deinitialize(void) +{ + UnloadSNDIOLibrary(); +} + +static void SNDIO_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + *default_playback = SDL_AddAudioDevice(false, DEFAULT_PLAYBACK_DEVNAME, NULL, (void *)0x1); + *default_recording = SDL_AddAudioDevice(true, DEFAULT_RECORDING_DEVNAME, NULL, (void *)0x2); +} + +static bool SNDIO_Init(SDL_AudioDriverImpl *impl) +{ + if (!LoadSNDIOLibrary()) { + return false; + } + + impl->OpenDevice = SNDIO_OpenDevice; + impl->WaitDevice = SNDIO_WaitDevice; + impl->PlayDevice = SNDIO_PlayDevice; + impl->GetDeviceBuf = SNDIO_GetDeviceBuf; + impl->CloseDevice = SNDIO_CloseDevice; + impl->WaitRecordingDevice = SNDIO_WaitDevice; + impl->RecordDevice = SNDIO_RecordDevice; + impl->FlushRecording = SNDIO_FlushRecording; + impl->Deinitialize = SNDIO_Deinitialize; + impl->DetectDevices = SNDIO_DetectDevices; + + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap SNDIO_bootstrap = { + "sndio", "OpenBSD sndio", SNDIO_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_SNDIO diff --git a/contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.h b/contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.h new file mode 100644 index 0000000..d4ff725 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/sndio/SDL_sndioaudio.h @@ -0,0 +1,38 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_sndioaudio_h_ +#define SDL_sndioaudio_h_ + +#include +#include + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + struct sio_hdl *dev; // The audio device handle + Uint8 *mixbuf; // Raw mixing buffer + struct pollfd *pfd; // Polling structures for non-blocking sndio devices +}; + +#endif // SDL_sndioaudio_h_ diff --git a/contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.c b/contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.c new file mode 100644 index 0000000..e194f21 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.c @@ -0,0 +1,238 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_VITA + +#include +#include +#include + +#include "../SDL_audiodev_c.h" +#include "../SDL_sysaudio.h" +#include "SDL_vitaaudio.h" + +#include +#include +#include + +#define SCE_AUDIO_SAMPLE_ALIGN(s) (((s) + 63) & ~63) +#define SCE_AUDIO_MAX_VOLUME 0x8000 + +static bool VITAAUD_OpenRecordingDevice(SDL_AudioDevice *device) +{ + device->spec.freq = 16000; + device->spec.channels = 1; + device->sample_frames = 512; + + SDL_UpdatedAudioDeviceFormat(device); + + device->hidden->port = sceAudioInOpenPort(SCE_AUDIO_IN_PORT_TYPE_VOICE, 512, 16000, SCE_AUDIO_IN_PARAM_FORMAT_S16_MONO); + + if (device->hidden->port < 0) { + return SDL_SetError("Couldn't open audio in port: %x", device->hidden->port); + } + + return true; +} + +static bool VITAAUD_OpenDevice(SDL_AudioDevice *device) +{ + int format, mixlen, i, port = SCE_AUDIO_OUT_PORT_TYPE_MAIN; + int vols[2] = { SCE_AUDIO_MAX_VOLUME, SCE_AUDIO_MAX_VOLUME }; + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts; + + device->hidden = (struct SDL_PrivateAudioData *) + SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } + + closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { + if (test_format == SDL_AUDIO_S16LE) { + device->spec.format = test_format; + break; + } + } + + if (!test_format) { + return SDL_SetError("Unsupported audio format"); + } + + if (device->recording) { + return VITAAUD_OpenRecordingDevice(device); + } + + // The sample count must be a multiple of 64. + device->sample_frames = SCE_AUDIO_SAMPLE_ALIGN(device->sample_frames); + + // Update the fragment size as size in bytes. + SDL_UpdatedAudioDeviceFormat(device); + + /* Allocate the mixing buffer. Its size and starting address must + be a multiple of 64 bytes. Our sample count is already a multiple of + 64, so spec->size should be a multiple of 64 as well. */ + mixlen = device->buffer_size * NUM_BUFFERS; + device->hidden->rawbuf = (Uint8 *)SDL_aligned_alloc(64, mixlen); + if (!device->hidden->rawbuf) { + return SDL_SetError("Couldn't allocate mixing buffer"); + } + + // Setup the hardware channel. + if (device->spec.channels == 1) { + format = SCE_AUDIO_OUT_MODE_MONO; + } else { + format = SCE_AUDIO_OUT_MODE_STEREO; + } + + // the main port requires 48000Hz audio, so this drops to the background music port if necessary + if (device->spec.freq < 48000) { + port = SCE_AUDIO_OUT_PORT_TYPE_BGM; + } + + device->hidden->port = sceAudioOutOpenPort(port, device->sample_frames, device->spec.freq, format); + if (device->hidden->port < 0) { + SDL_aligned_free(device->hidden->rawbuf); + device->hidden->rawbuf = NULL; + return SDL_SetError("Couldn't open audio out port: %x", device->hidden->port); + } + + sceAudioOutSetVolume(device->hidden->port, SCE_AUDIO_VOLUME_FLAG_L_CH | SCE_AUDIO_VOLUME_FLAG_R_CH, vols); + + SDL_memset(device->hidden->rawbuf, 0, mixlen); + for (i = 0; i < NUM_BUFFERS; i++) { + device->hidden->mixbufs[i] = &device->hidden->rawbuf[i * device->buffer_size]; + } + + device->hidden->next_buffer = 0; + return true; +} + +static bool VITAAUD_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size) +{ + return (sceAudioOutOutput(device->hidden->port, buffer) == 0); +} + +// This function waits until it is possible to write a full sound buffer +static bool VITAAUD_WaitDevice(SDL_AudioDevice *device) +{ + // !!! FIXME: we might just need to sleep roughly as long as playback buffers take to process, based on sample rate, etc. + while (!SDL_GetAtomicInt(&device->shutdown) && (sceAudioOutGetRestSample(device->hidden->port) >= device->buffer_size)) { + SDL_Delay(1); + } + return true; +} + +static Uint8 *VITAAUD_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + Uint8 *result = device->hidden->mixbufs[device->hidden->next_buffer]; + device->hidden->next_buffer = (device->hidden->next_buffer + 1) % NUM_BUFFERS; + return result; +} + +static void VITAAUD_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + if (device->hidden->port >= 0) { + if (device->recording) { + sceAudioInReleasePort(device->hidden->port); + } else { + sceAudioOutReleasePort(device->hidden->port); + } + device->hidden->port = -1; + } + + if (!device->recording && device->hidden->rawbuf) { + SDL_aligned_free(device->hidden->rawbuf); // this uses SDL_aligned_alloc(), not SDL_malloc() + device->hidden->rawbuf = NULL; + } + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static bool VITAAUD_WaitRecordingDevice(SDL_AudioDevice *device) +{ + // there's only a blocking call to obtain more data, so we'll just sleep as + // long as a buffer would run. + const Uint64 endticks = SDL_GetTicks() + ((device->sample_frames * 1000) / device->spec.freq); + while (!SDL_GetAtomicInt(&device->shutdown) && (SDL_GetTicks() < endticks)) { + SDL_Delay(1); + } + return true; +} + +static int VITAAUD_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + int ret; + SDL_assert(buflen == device->buffer_size); + ret = sceAudioInInput(device->hidden->port, buffer); + if (ret < 0) { + SDL_SetError("Failed to record from device: %x", ret); + return -1; + } + return device->buffer_size; +} + +static void VITAAUD_FlushRecording(SDL_AudioDevice *device) +{ + // just grab the latest and dump it. + sceAudioInInput(device->hidden->port, device->work_buffer); +} + +static void VITAAUD_ThreadInit(SDL_AudioDevice *device) +{ + // Increase the priority of this audio thread by 1 to put it ahead of other SDL threads. + SceUID thid; + SceKernelThreadInfo info; + thid = sceKernelGetThreadId(); + info.size = sizeof(SceKernelThreadInfo); + if (sceKernelGetThreadInfo(thid, &info) == 0) { + sceKernelChangeThreadPriority(thid, info.currentPriority - 1); + } +} + +static bool VITAAUD_Init(SDL_AudioDriverImpl *impl) +{ + impl->OpenDevice = VITAAUD_OpenDevice; + impl->PlayDevice = VITAAUD_PlayDevice; + impl->WaitDevice = VITAAUD_WaitDevice; + impl->GetDeviceBuf = VITAAUD_GetDeviceBuf; + impl->CloseDevice = VITAAUD_CloseDevice; + impl->ThreadInit = VITAAUD_ThreadInit; + impl->WaitRecordingDevice = VITAAUD_WaitRecordingDevice; + impl->FlushRecording = VITAAUD_FlushRecording; + impl->RecordDevice = VITAAUD_RecordDevice; + + impl->HasRecordingSupport = true; + impl->OnlyHasDefaultPlaybackDevice = true; + impl->OnlyHasDefaultRecordingDevice = true; + + return true; +} + +AudioBootStrap VITAAUD_bootstrap = { + "vita", "VITA audio driver", VITAAUD_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_VITA diff --git a/contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.h b/contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.h new file mode 100644 index 0000000..1e97499 --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/vita/SDL_vitaaudio.h @@ -0,0 +1,41 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef SDL_vitaaudio_h +#define SDL_vitaaudio_h + +#include "../SDL_sysaudio.h" + +#define NUM_BUFFERS 2 + +struct SDL_PrivateAudioData +{ + // The hardware input/output port. + int port; + // The raw allocated mixing buffer. + Uint8 *rawbuf; + // Individual mixing buffers. + Uint8 *mixbufs[NUM_BUFFERS]; + // Index of the next available mixing buffer. + int next_buffer; +}; + +#endif // SDL_vitaaudio_h diff --git a/contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.c b/contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.c new file mode 100644 index 0000000..db0974b --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.c @@ -0,0 +1,963 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_AUDIO_DRIVER_WASAPI + +#include "../../core/windows/SDL_windows.h" +#include "../../core/windows/SDL_immdevice.h" +#include "../../thread/SDL_systhread.h" +#include "../SDL_sysaudio.h" + +#define COBJMACROS +#include + +#include "SDL_wasapi.h" + +// These constants aren't available in older SDKs +#ifndef AUDCLNT_STREAMFLAGS_RATEADJUST +#define AUDCLNT_STREAMFLAGS_RATEADJUST 0x00100000 +#endif +#ifndef AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY +#define AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY 0x08000000 +#endif +#ifndef AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM +#define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 0x80000000 +#endif + +// handle to Avrt.dll--Vista and later!--for flagging the callback thread as "Pro Audio" (low latency). +static HMODULE libavrt = NULL; +typedef HANDLE(WINAPI *pfnAvSetMmThreadCharacteristicsW)(LPCWSTR, LPDWORD); +typedef BOOL(WINAPI *pfnAvRevertMmThreadCharacteristics)(HANDLE); +static pfnAvSetMmThreadCharacteristicsW pAvSetMmThreadCharacteristicsW = NULL; +static pfnAvRevertMmThreadCharacteristics pAvRevertMmThreadCharacteristics = NULL; + +// Some GUIDs we need to know without linking to libraries that aren't available before Vista. +static const IID SDL_IID_IAudioRenderClient = { 0xf294acfc, 0x3146, 0x4483, { 0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2 } }; +static const IID SDL_IID_IAudioCaptureClient = { 0xc8adbd64, 0xe71e, 0x48a0, { 0xa4, 0xde, 0x18, 0x5c, 0x39, 0x5c, 0xd3, 0x17 } }; +static const IID SDL_IID_IAudioClient = { 0x1cb9ad4c, 0xdbfa, 0x4c32, { 0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2 } }; +#ifdef __IAudioClient3_INTERFACE_DEFINED__ +static const IID SDL_IID_IAudioClient3 = { 0x7ed4ee07, 0x8e67, 0x4cd4, { 0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42 } }; +#endif // + +static bool immdevice_initialized = false; + +// WASAPI is _really_ particular about various things happening on the same thread, for COM and such, +// so we proxy various stuff to a single background thread to manage. + +typedef struct ManagementThreadPendingTask +{ + ManagementThreadTask fn; + void *userdata; + bool result; + SDL_Semaphore *task_complete_sem; + char *errorstr; + struct ManagementThreadPendingTask *next; +} ManagementThreadPendingTask; + +static SDL_Thread *ManagementThread = NULL; +static ManagementThreadPendingTask *ManagementThreadPendingTasks = NULL; +static SDL_Mutex *ManagementThreadLock = NULL; +static SDL_Condition *ManagementThreadCondition = NULL; +static SDL_AtomicInt ManagementThreadShutdown; + +static void ManagementThreadMainloop(void) +{ + SDL_LockMutex(ManagementThreadLock); + ManagementThreadPendingTask *task; + while (((task = (ManagementThreadPendingTask *)SDL_GetAtomicPointer((void **)&ManagementThreadPendingTasks)) != NULL) || !SDL_GetAtomicInt(&ManagementThreadShutdown)) { + if (!task) { + SDL_WaitCondition(ManagementThreadCondition, ManagementThreadLock); // block until there's something to do. + } else { + SDL_SetAtomicPointer((void **) &ManagementThreadPendingTasks, task->next); // take task off the pending list. + SDL_UnlockMutex(ManagementThreadLock); // let other things add to the list while we chew on this task. + task->result = task->fn(task->userdata); // run this task. + if (task->task_complete_sem) { // something waiting on result? + task->errorstr = SDL_strdup(SDL_GetError()); + SDL_SignalSemaphore(task->task_complete_sem); + } else { // nothing waiting, we're done, free it. + SDL_free(task); + } + SDL_LockMutex(ManagementThreadLock); // regrab the lock so we can get the next task; if nothing to do, we'll release the lock in SDL_WaitCondition. + } + } + SDL_UnlockMutex(ManagementThreadLock); // told to shut down and out of tasks, let go of the lock and return. +} + +bool WASAPI_ProxyToManagementThread(ManagementThreadTask task, void *userdata, bool *wait_on_result) +{ + // We want to block for a result, but we are already running from the management thread! Just run the task now so we don't deadlock. + if ((wait_on_result) && (SDL_GetCurrentThreadID() == SDL_GetThreadID(ManagementThread))) { + *wait_on_result = task(userdata); + return true; // completed! + } + + if (SDL_GetAtomicInt(&ManagementThreadShutdown)) { + return SDL_SetError("Can't add task, we're shutting down"); + } + + ManagementThreadPendingTask *pending = (ManagementThreadPendingTask *)SDL_calloc(1, sizeof(ManagementThreadPendingTask)); + if (!pending) { + return false; + } + + pending->fn = task; + pending->userdata = userdata; + + if (wait_on_result) { + pending->task_complete_sem = SDL_CreateSemaphore(0); + if (!pending->task_complete_sem) { + SDL_free(pending); + return false; + } + } + + pending->next = NULL; + + SDL_LockMutex(ManagementThreadLock); + + // add to end of task list. + ManagementThreadPendingTask *prev = NULL; + for (ManagementThreadPendingTask *i = (ManagementThreadPendingTask *)SDL_GetAtomicPointer((void **)&ManagementThreadPendingTasks); i; i = i->next) { + prev = i; + } + + if (prev) { + prev->next = pending; + } else { + SDL_SetAtomicPointer((void **) &ManagementThreadPendingTasks, pending); + } + + // task is added to the end of the pending list, let management thread rip! + SDL_SignalCondition(ManagementThreadCondition); + SDL_UnlockMutex(ManagementThreadLock); + + if (wait_on_result) { + SDL_WaitSemaphore(pending->task_complete_sem); + SDL_DestroySemaphore(pending->task_complete_sem); + *wait_on_result = pending->result; + if (pending->errorstr) { + SDL_SetError("%s", pending->errorstr); + SDL_free(pending->errorstr); + } + SDL_free(pending); + } + + return true; // successfully added (and possibly executed)! +} + +static bool mgmtthrtask_AudioDeviceDisconnected(void *userdata) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userdata; + SDL_AudioDeviceDisconnected(device); + UnrefPhysicalAudioDevice(device); // make sure this lived until the task completes. + return true; +} + +static void AudioDeviceDisconnected(SDL_AudioDevice *device) +{ + // don't wait on this, IMMDevice's own thread needs to return or everything will deadlock. + if (device) { + RefPhysicalAudioDevice(device); // make sure this lives until the task completes. + WASAPI_ProxyToManagementThread(mgmtthrtask_AudioDeviceDisconnected, device, NULL); + } +} + +static bool mgmtthrtask_DefaultAudioDeviceChanged(void *userdata) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userdata; + SDL_DefaultAudioDeviceChanged(device); + UnrefPhysicalAudioDevice(device); // make sure this lived until the task completes. + return true; +} + +static void DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device) +{ + // don't wait on this, IMMDevice's own thread needs to return or everything will deadlock. + if (new_default_device) { + RefPhysicalAudioDevice(new_default_device); // make sure this lives until the task completes. + WASAPI_ProxyToManagementThread(mgmtthrtask_DefaultAudioDeviceChanged, new_default_device, NULL); + } +} + +static void StopWasapiHotplug(void) +{ + if (immdevice_initialized) { + SDL_IMMDevice_Quit(); + immdevice_initialized = false; + } +} + +static void Deinit(void) +{ + if (libavrt) { + FreeLibrary(libavrt); + libavrt = NULL; + } + + pAvSetMmThreadCharacteristicsW = NULL; + pAvRevertMmThreadCharacteristics = NULL; + + StopWasapiHotplug(); + + WIN_CoUninitialize(); +} + +static bool ManagementThreadPrepare(void) +{ + const SDL_IMMDevice_callbacks callbacks = { AudioDeviceDisconnected, DefaultAudioDeviceChanged }; + if (FAILED(WIN_CoInitialize())) { + return SDL_SetError("CoInitialize() failed"); + } else if (!SDL_IMMDevice_Init(&callbacks)) { + return false; // Error string is set by SDL_IMMDevice_Init + } + + immdevice_initialized = true; + + libavrt = LoadLibrary(TEXT("avrt.dll")); // this library is available in Vista and later. No WinXP, so have to LoadLibrary to use it for now! + if (libavrt) { + pAvSetMmThreadCharacteristicsW = (pfnAvSetMmThreadCharacteristicsW)GetProcAddress(libavrt, "AvSetMmThreadCharacteristicsW"); + pAvRevertMmThreadCharacteristics = (pfnAvRevertMmThreadCharacteristics)GetProcAddress(libavrt, "AvRevertMmThreadCharacteristics"); + } + + ManagementThreadLock = SDL_CreateMutex(); + if (!ManagementThreadLock) { + Deinit(); + return false; + } + + ManagementThreadCondition = SDL_CreateCondition(); + if (!ManagementThreadCondition) { + SDL_DestroyMutex(ManagementThreadLock); + ManagementThreadLock = NULL; + Deinit(); + return false; + } + + return true; +} + +typedef struct +{ + char *errorstr; + SDL_Semaphore *ready_sem; +} ManagementThreadEntryData; + +static int ManagementThreadEntry(void *userdata) +{ + ManagementThreadEntryData *data = (ManagementThreadEntryData *)userdata; + + if (!ManagementThreadPrepare()) { + data->errorstr = SDL_strdup(SDL_GetError()); + SDL_SignalSemaphore(data->ready_sem); // unblock calling thread. + return 0; + } + + SDL_SignalSemaphore(data->ready_sem); // unblock calling thread. + ManagementThreadMainloop(); + + Deinit(); + return 0; +} + +static bool InitManagementThread(void) +{ + ManagementThreadEntryData mgmtdata; + SDL_zero(mgmtdata); + mgmtdata.ready_sem = SDL_CreateSemaphore(0); + if (!mgmtdata.ready_sem) { + return false; + } + + SDL_SetAtomicPointer((void **) &ManagementThreadPendingTasks, NULL); + SDL_SetAtomicInt(&ManagementThreadShutdown, 0); + ManagementThread = SDL_CreateThreadWithStackSize(ManagementThreadEntry, "SDLWASAPIMgmt", 256 * 1024, &mgmtdata); // !!! FIXME: maybe even smaller stack size? + if (!ManagementThread) { + return false; + } + + SDL_WaitSemaphore(mgmtdata.ready_sem); + SDL_DestroySemaphore(mgmtdata.ready_sem); + + if (mgmtdata.errorstr) { + SDL_WaitThread(ManagementThread, NULL); + ManagementThread = NULL; + SDL_SetError("%s", mgmtdata.errorstr); + SDL_free(mgmtdata.errorstr); + return false; + } + + return true; +} + +static void DeinitManagementThread(void) +{ + if (ManagementThread) { + SDL_SetAtomicInt(&ManagementThreadShutdown, 1); + SDL_LockMutex(ManagementThreadLock); + SDL_SignalCondition(ManagementThreadCondition); + SDL_UnlockMutex(ManagementThreadLock); + SDL_WaitThread(ManagementThread, NULL); + ManagementThread = NULL; + } + + SDL_assert(SDL_GetAtomicPointer((void **) &ManagementThreadPendingTasks) == NULL); + + SDL_DestroyCondition(ManagementThreadCondition); + SDL_DestroyMutex(ManagementThreadLock); + ManagementThreadCondition = NULL; + ManagementThreadLock = NULL; + SDL_SetAtomicInt(&ManagementThreadShutdown, 0); +} + +typedef struct +{ + SDL_AudioDevice **default_playback; + SDL_AudioDevice **default_recording; +} mgmtthrtask_DetectDevicesData; + +static bool mgmtthrtask_DetectDevices(void *userdata) +{ + mgmtthrtask_DetectDevicesData *data = (mgmtthrtask_DetectDevicesData *)userdata; + SDL_IMMDevice_EnumerateEndpoints(data->default_playback, data->default_recording); + return true; +} + +static void WASAPI_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + bool rc; + // this blocks because it needs to finish before the audio subsystem inits + mgmtthrtask_DetectDevicesData data; + data.default_playback = default_playback; + data.default_recording = default_recording; + WASAPI_ProxyToManagementThread(mgmtthrtask_DetectDevices, &data, &rc); +} + +static bool mgmtthrtask_DisconnectDevice(void *userdata) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userdata; + SDL_AudioDeviceDisconnected(device); + UnrefPhysicalAudioDevice(device); + return true; +} + +void WASAPI_DisconnectDevice(SDL_AudioDevice *device) +{ + if (SDL_CompareAndSwapAtomicInt(&device->hidden->device_disconnecting, 0, 1)) { + RefPhysicalAudioDevice(device); // will unref when the task ends. + WASAPI_ProxyToManagementThread(mgmtthrtask_DisconnectDevice, device, NULL); + } +} + +static bool WasapiFailed(SDL_AudioDevice *device, const HRESULT err) +{ + if (err == S_OK) { + return false; + } else if (err == AUDCLNT_E_DEVICE_INVALIDATED) { + device->hidden->device_lost = true; + } else { + device->hidden->device_dead = true; + } + + return true; +} + +static bool mgmtthrtask_StopAndReleaseClient(void *userdata) +{ + IAudioClient *client = (IAudioClient *) userdata; + IAudioClient_Stop(client); + IAudioClient_Release(client); + return true; +} + +static bool mgmtthrtask_ReleaseCaptureClient(void *userdata) +{ + IAudioCaptureClient_Release((IAudioCaptureClient *)userdata); + return true; +} + +static bool mgmtthrtask_ReleaseRenderClient(void *userdata) +{ + IAudioRenderClient_Release((IAudioRenderClient *)userdata); + return true; +} + +static bool mgmtthrtask_CoTaskMemFree(void *userdata) +{ + CoTaskMemFree(userdata); + return true; +} + +static bool mgmtthrtask_CloseHandle(void *userdata) +{ + CloseHandle((HANDLE) userdata); + return true; +} + +static void ResetWasapiDevice(SDL_AudioDevice *device) +{ + if (!device || !device->hidden) { + return; + } + + // just queue up all the tasks in the management thread and don't block. + // We don't care when any of these actually get free'd. + + if (device->hidden->client) { + IAudioClient *client = device->hidden->client; + device->hidden->client = NULL; + WASAPI_ProxyToManagementThread(mgmtthrtask_StopAndReleaseClient, client, NULL); + } + + if (device->hidden->render) { + IAudioRenderClient *render = device->hidden->render; + device->hidden->render = NULL; + WASAPI_ProxyToManagementThread(mgmtthrtask_ReleaseRenderClient, render, NULL); + } + + if (device->hidden->capture) { + IAudioCaptureClient *capture = device->hidden->capture; + device->hidden->capture = NULL; + WASAPI_ProxyToManagementThread(mgmtthrtask_ReleaseCaptureClient, capture, NULL); + } + + if (device->hidden->waveformat) { + void *ptr = device->hidden->waveformat; + device->hidden->waveformat = NULL; + WASAPI_ProxyToManagementThread(mgmtthrtask_CoTaskMemFree, ptr, NULL); + } + + if (device->hidden->event) { + HANDLE event = device->hidden->event; + device->hidden->event = NULL; + WASAPI_ProxyToManagementThread(mgmtthrtask_CloseHandle, (void *) event, NULL); + } +} + +static bool mgmtthrtask_ActivateDevice(void *userdata) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *) userdata; + + IMMDevice *immdevice = NULL; + if (!SDL_IMMDevice_Get(device, &immdevice, device->recording)) { + device->hidden->client = NULL; + return false; // This is already set by SDL_IMMDevice_Get + } + + // this is _not_ async in standard win32, yay! + HRESULT ret = IMMDevice_Activate(immdevice, &SDL_IID_IAudioClient, CLSCTX_ALL, NULL, (void **)&device->hidden->client); + IMMDevice_Release(immdevice); + + if (FAILED(ret)) { + SDL_assert(device->hidden->client == NULL); + return WIN_SetErrorFromHRESULT("WASAPI can't activate audio endpoint", ret); + } + + SDL_assert(device->hidden->client != NULL); + if (!WASAPI_PrepDevice(device)) { // not async, fire it right away. + return false; + } + + return true; // good to go. +} + +static bool ActivateWasapiDevice(SDL_AudioDevice *device) +{ + // this blocks because we're either being notified from a background thread or we're running during device open, + // both of which won't deadlock vs the device thread. + bool rc = false; + return (WASAPI_ProxyToManagementThread(mgmtthrtask_ActivateDevice, device, &rc) && rc); +} + +// do not call when holding the device lock! +static bool RecoverWasapiDevice(SDL_AudioDevice *device) +{ + ResetWasapiDevice(device); // dump the lost device's handles. + + // This handles a non-default device that simply had its format changed in the Windows Control Panel. + if (!ActivateWasapiDevice(device)) { + WASAPI_DisconnectDevice(device); + return false; + } + + device->hidden->device_lost = false; + + return true; // okay, carry on with new device details! +} + +// do not call when holding the device lock! +static bool RecoverWasapiIfLost(SDL_AudioDevice *device) +{ + if (SDL_GetAtomicInt(&device->shutdown)) { + return false; // closing, stop trying. + } else if (SDL_GetAtomicInt(&device->hidden->device_disconnecting)) { + return false; // failing via the WASAPI management thread, stop trying. + } else if (device->hidden->device_dead) { // had a fatal error elsewhere, clean up and quit + IAudioClient_Stop(device->hidden->client); + WASAPI_DisconnectDevice(device); + SDL_assert(SDL_GetAtomicInt(&device->shutdown)); // so we don't come back through here. + return false; // already failed. + } else if (SDL_GetAtomicInt(&device->zombie)) { + return false; // we're already dead, so just leave and let the Zombie implementations take over. + } else if (!device->hidden->client) { + return true; // still waiting for activation. + } + + return device->hidden->device_lost ? RecoverWasapiDevice(device) : true; +} + +static Uint8 *WASAPI_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) +{ + // get an endpoint buffer from WASAPI. + BYTE *buffer = NULL; + + if (device->hidden->render) { + const HRESULT ret = IAudioRenderClient_GetBuffer(device->hidden->render, device->sample_frames, &buffer); + if (ret == AUDCLNT_E_BUFFER_TOO_LARGE) { + SDL_assert(buffer == NULL); + *buffer_size = 0; // just go back to WaitDevice and try again after the hardware has consumed some more data. + } else if (WasapiFailed(device, ret)) { + SDL_assert(buffer == NULL); + if (device->hidden->device_lost) { // just use an available buffer, we won't be playing it anyhow. + *buffer_size = 0; // we'll recover during WaitDevice and try again. + } + } + } + + return (Uint8 *)buffer; +} + +static bool WASAPI_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) +{ + if (device->hidden->render && !SDL_GetAtomicInt(&device->hidden->device_disconnecting)) { // definitely activated? + // WasapiFailed() will mark the device for reacquisition or removal elsewhere. + WasapiFailed(device, IAudioRenderClient_ReleaseBuffer(device->hidden->render, device->sample_frames, 0)); + } + return true; +} + +static bool WASAPI_WaitDevice(SDL_AudioDevice *device) +{ + // WaitDevice does not hold the device lock, so check for recovery/disconnect details here. + while (RecoverWasapiIfLost(device) && device->hidden->client && device->hidden->event) { + if (device->recording) { + // Recording devices should return immediately if there is any data available + UINT32 padding = 0; + if (!WasapiFailed(device, IAudioClient_GetCurrentPadding(device->hidden->client, &padding))) { + //SDL_Log("WASAPI EVENT! padding=%u maxpadding=%u", (unsigned int)padding, (unsigned int)maxpadding); + if (padding > 0) { + break; + } + } + + switch (WaitForSingleObjectEx(device->hidden->event, 200, FALSE)) { + case WAIT_OBJECT_0: + case WAIT_TIMEOUT: + break; + + default: + //SDL_Log("WASAPI FAILED EVENT!"); + IAudioClient_Stop(device->hidden->client); + return false; + } + } else { + DWORD waitResult = WaitForSingleObjectEx(device->hidden->event, 200, FALSE); + if (waitResult == WAIT_OBJECT_0) { + UINT32 padding = 0; + if (!WasapiFailed(device, IAudioClient_GetCurrentPadding(device->hidden->client, &padding))) { + //SDL_Log("WASAPI EVENT! padding=%u maxpadding=%u", (unsigned int)padding, (unsigned int)maxpadding); + if (padding <= (UINT32)device->sample_frames) { + break; + } + } + } else if (waitResult != WAIT_TIMEOUT) { + //SDL_Log("WASAPI FAILED EVENT!");*/ + IAudioClient_Stop(device->hidden->client); + return false; + } + } + } + + return true; +} + +static int WASAPI_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) +{ + BYTE *ptr = NULL; + UINT32 frames = 0; + DWORD flags = 0; + + while (device->hidden->capture) { + const HRESULT ret = IAudioCaptureClient_GetBuffer(device->hidden->capture, &ptr, &frames, &flags, NULL, NULL); + if (ret == AUDCLNT_S_BUFFER_EMPTY) { + return 0; // in theory we should have waited until there was data, but oh well, we'll go back to waiting. Returning 0 is safe in SDL3. + } + + WasapiFailed(device, ret); // mark device lost/failed if necessary. + + if (ret == S_OK) { + const int total = ((int)frames) * device->hidden->framesize; + const int cpy = SDL_min(buflen, total); + const int leftover = total - cpy; + const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) ? true : false; + + SDL_assert(leftover == 0); // according to MSDN, this isn't everything available, just one "packet" of data per-GetBuffer call. + + if (silent) { + SDL_memset(buffer, device->silence_value, cpy); + } else { + SDL_memcpy(buffer, ptr, cpy); + } + + WasapiFailed(device, IAudioCaptureClient_ReleaseBuffer(device->hidden->capture, frames)); + + return cpy; + } + } + + return -1; // unrecoverable error. +} + +static void WASAPI_FlushRecording(SDL_AudioDevice *device) +{ + BYTE *ptr = NULL; + UINT32 frames = 0; + DWORD flags = 0; + + // just read until we stop getting packets, throwing them away. + while (!SDL_GetAtomicInt(&device->shutdown) && device->hidden->capture) { + const HRESULT ret = IAudioCaptureClient_GetBuffer(device->hidden->capture, &ptr, &frames, &flags, NULL, NULL); + if (ret == AUDCLNT_S_BUFFER_EMPTY) { + break; // no more buffered data; we're done. + } else if (WasapiFailed(device, ret)) { + break; // failed for some other reason, abort. + } else if (WasapiFailed(device, IAudioCaptureClient_ReleaseBuffer(device->hidden->capture, frames))) { + break; // something broke. + } + } +} + +static void WASAPI_CloseDevice(SDL_AudioDevice *device) +{ + if (device->hidden) { + ResetWasapiDevice(device); + SDL_free(device->hidden->devid); + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static bool mgmtthrtask_PrepDevice(void *userdata) +{ + SDL_AudioDevice *device = (SDL_AudioDevice *)userdata; + + /* !!! FIXME: we could request an exclusive mode stream, which is lower latency; + !!! it will write into the kernel's audio buffer directly instead of + !!! shared memory that a user-mode mixer then writes to the kernel with + !!! everything else. Doing this means any other sound using this device will + !!! stop playing, including the user's MP3 player and system notification + !!! sounds. You'd probably need to release the device when the app isn't in + !!! the foreground, to be a good citizen of the system. It's doable, but it's + !!! more work and causes some annoyances, and I don't know what the latency + !!! wins actually look like. Maybe add a hint to force exclusive mode at + !!! some point. To be sure, defaulting to shared mode is the right thing to + !!! do in any case. */ + const AUDCLNT_SHAREMODE sharemode = AUDCLNT_SHAREMODE_SHARED; + + IAudioClient *client = device->hidden->client; + SDL_assert(client != NULL); + + device->hidden->event = CreateEvent(NULL, FALSE, FALSE, NULL); + if (!device->hidden->event) { + return WIN_SetError("WASAPI can't create an event handle"); + } + + HRESULT ret; + + WAVEFORMATEX *waveformat = NULL; + ret = IAudioClient_GetMixFormat(client, &waveformat); + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't determine mix format", ret); + } + SDL_assert(waveformat != NULL); + device->hidden->waveformat = waveformat; + + SDL_AudioSpec newspec; + newspec.channels = (Uint8)waveformat->nChannels; + + // Make sure we have a valid format that we can convert to whatever WASAPI wants. + const SDL_AudioFormat wasapi_format = SDL_WaveFormatExToSDLFormat(waveformat); + + SDL_AudioFormat test_format; + const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); + while ((test_format = *(closefmts++)) != 0) { + if (test_format == wasapi_format) { + newspec.format = test_format; + break; + } + } + + if (!test_format) { + return SDL_SetError("%s: Unsupported audio format", "wasapi"); + } + + REFERENCE_TIME default_period = 0; + ret = IAudioClient_GetDevicePeriod(client, &default_period, NULL); + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't determine minimum device period", ret); + } + + DWORD streamflags = 0; + + /* we've gotten reports that WASAPI's resampler introduces distortions, but in the short term + it fixes some other WASAPI-specific quirks we haven't quite tracked down. + Refer to bug #6326 for the immediate concern. */ +#if 1 + // favor WASAPI's resampler over our own + if ((DWORD)device->spec.freq != waveformat->nSamplesPerSec) { + streamflags |= (AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY); + waveformat->nSamplesPerSec = device->spec.freq; + waveformat->nAvgBytesPerSec = waveformat->nSamplesPerSec * waveformat->nChannels * (waveformat->wBitsPerSample / 8); + } +#endif + + newspec.freq = waveformat->nSamplesPerSec; + + streamflags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK; + + int new_sample_frames = 0; + bool iaudioclient3_initialized = false; + +#ifdef __IAudioClient3_INTERFACE_DEFINED__ + // Try querying IAudioClient3 if sharemode is AUDCLNT_SHAREMODE_SHARED + if (sharemode == AUDCLNT_SHAREMODE_SHARED) { + IAudioClient3 *client3 = NULL; + ret = IAudioClient_QueryInterface(client, &SDL_IID_IAudioClient3, (void**)&client3); + if (SUCCEEDED(ret)) { + UINT32 default_period_in_frames = 0; + UINT32 fundamental_period_in_frames = 0; + UINT32 min_period_in_frames = 0; + UINT32 max_period_in_frames = 0; + ret = IAudioClient3_GetSharedModeEnginePeriod(client3, waveformat, + &default_period_in_frames, &fundamental_period_in_frames, &min_period_in_frames, &max_period_in_frames); + if (SUCCEEDED(ret)) { + // IAudioClient3_InitializeSharedAudioStream only accepts the integral multiple of fundamental_period_in_frames + UINT32 period_in_frames = fundamental_period_in_frames * (UINT32)SDL_round((double)device->sample_frames / fundamental_period_in_frames); + period_in_frames = SDL_clamp(period_in_frames, min_period_in_frames, max_period_in_frames); + + ret = IAudioClient3_InitializeSharedAudioStream(client3, streamflags, period_in_frames, waveformat, NULL); + if (SUCCEEDED(ret)) { + new_sample_frames = (int)period_in_frames; + iaudioclient3_initialized = true; + } + } + + IAudioClient3_Release(client3); + } + } +#endif + + if (!iaudioclient3_initialized) + ret = IAudioClient_Initialize(client, sharemode, streamflags, 0, 0, waveformat, NULL); + + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't initialize audio client", ret); + } + + ret = IAudioClient_SetEventHandle(client, device->hidden->event); + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't set event handle", ret); + } + + UINT32 bufsize = 0; // this is in sample frames, not samples, not bytes. + ret = IAudioClient_GetBufferSize(client, &bufsize); + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't determine buffer size", ret); + } + + // Match the callback size to the period size to cut down on the number of + // interrupts waited for in each call to WaitDevice + if (new_sample_frames <= 0) { + const float period_millis = default_period / 10000.0f; + const float period_frames = period_millis * newspec.freq / 1000.0f; + new_sample_frames = (int) SDL_ceilf(period_frames); + } + + // regardless of what we calculated for the period size, clamp it to the expected hardware buffer size. + if (new_sample_frames > (int) bufsize) { + new_sample_frames = (int) bufsize; + } + + // Update the fragment size as size in bytes + if (!SDL_AudioDeviceFormatChangedAlreadyLocked(device, &newspec, new_sample_frames)) { + return false; + } + + device->hidden->framesize = SDL_AUDIO_FRAMESIZE(device->spec); + + if (device->recording) { + IAudioCaptureClient *capture = NULL; + ret = IAudioClient_GetService(client, &SDL_IID_IAudioCaptureClient, (void **)&capture); + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't get capture client service", ret); + } + + SDL_assert(capture != NULL); + device->hidden->capture = capture; + ret = IAudioClient_Start(client); + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't start capture", ret); + } + + WASAPI_FlushRecording(device); // MSDN says you should flush the recording endpoint right after startup. + } else { + IAudioRenderClient *render = NULL; + ret = IAudioClient_GetService(client, &SDL_IID_IAudioRenderClient, (void **)&render); + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't get render client service", ret); + } + + SDL_assert(render != NULL); + device->hidden->render = render; + ret = IAudioClient_Start(client); + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't start playback", ret); + } + } + + return true; // good to go. +} + +// This is called once a device is activated, possibly asynchronously. +bool WASAPI_PrepDevice(SDL_AudioDevice *device) +{ + bool rc = true; + return (WASAPI_ProxyToManagementThread(mgmtthrtask_PrepDevice, device, &rc) && rc); +} + +static bool WASAPI_OpenDevice(SDL_AudioDevice *device) +{ + // Initialize all variables that we clean on shutdown + device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); + if (!device->hidden) { + return false; + } else if (!ActivateWasapiDevice(device)) { + return false; // already set error. + } + + /* Ready, but possibly waiting for async device activation. + Until activation is successful, we will report silence from recording + devices and ignore data on playback devices. Upon activation, we'll make + sure any bound audio streams are adjusted for the final device format. */ + + return true; +} + +static void WASAPI_ThreadInit(SDL_AudioDevice *device) +{ + // this thread uses COM. + if (SUCCEEDED(WIN_CoInitialize())) { // can't report errors, hope it worked! + device->hidden->coinitialized = true; + } + + // Set this thread to very high "Pro Audio" priority. + if (pAvSetMmThreadCharacteristicsW) { + DWORD idx = 0; + device->hidden->task = pAvSetMmThreadCharacteristicsW(L"Pro Audio", &idx); + } else { + SDL_SetCurrentThreadPriority(device->recording ? SDL_THREAD_PRIORITY_HIGH : SDL_THREAD_PRIORITY_TIME_CRITICAL); + } +} + +static void WASAPI_ThreadDeinit(SDL_AudioDevice *device) +{ + // Set this thread back to normal priority. + if (device->hidden->task && pAvRevertMmThreadCharacteristics) { + pAvRevertMmThreadCharacteristics(device->hidden->task); + device->hidden->task = NULL; + } + + if (device->hidden->coinitialized) { + WIN_CoUninitialize(); + device->hidden->coinitialized = false; + } +} + +static bool mgmtthrtask_FreeDeviceHandle(void *userdata) +{ + SDL_IMMDevice_FreeDeviceHandle((SDL_AudioDevice *) userdata); + return true; +} + +static void WASAPI_FreeDeviceHandle(SDL_AudioDevice *device) +{ + bool rc; + WASAPI_ProxyToManagementThread(mgmtthrtask_FreeDeviceHandle, device, &rc); +} + +static bool mgmtthrtask_DeinitializeStart(void *userdata) +{ + StopWasapiHotplug(); + return true; +} + +static void WASAPI_DeinitializeStart(void) +{ + bool rc; + WASAPI_ProxyToManagementThread(mgmtthrtask_DeinitializeStart, NULL, &rc); +} + +static void WASAPI_Deinitialize(void) +{ + DeinitManagementThread(); +} + +static bool WASAPI_Init(SDL_AudioDriverImpl *impl) +{ + if (!InitManagementThread()) { + return false; + } + + impl->DetectDevices = WASAPI_DetectDevices; + impl->ThreadInit = WASAPI_ThreadInit; + impl->ThreadDeinit = WASAPI_ThreadDeinit; + impl->OpenDevice = WASAPI_OpenDevice; + impl->PlayDevice = WASAPI_PlayDevice; + impl->WaitDevice = WASAPI_WaitDevice; + impl->GetDeviceBuf = WASAPI_GetDeviceBuf; + impl->WaitRecordingDevice = WASAPI_WaitDevice; + impl->RecordDevice = WASAPI_RecordDevice; + impl->FlushRecording = WASAPI_FlushRecording; + impl->CloseDevice = WASAPI_CloseDevice; + impl->DeinitializeStart = WASAPI_DeinitializeStart; + impl->Deinitialize = WASAPI_Deinitialize; + impl->FreeDeviceHandle = WASAPI_FreeDeviceHandle; + + impl->HasRecordingSupport = true; + + return true; +} + +AudioBootStrap WASAPI_bootstrap = { + "wasapi", "WASAPI", WASAPI_Init, false, false +}; + +#endif // SDL_AUDIO_DRIVER_WASAPI diff --git a/contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.h b/contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.h new file mode 100644 index 0000000..5e528dc --- /dev/null +++ b/contrib/SDL-3.2.8/src/audio/wasapi/SDL_wasapi.h @@ -0,0 +1,61 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_wasapi_h_ +#define SDL_wasapi_h_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "../SDL_sysaudio.h" + +struct SDL_PrivateAudioData +{ + WCHAR *devid; + WAVEFORMATEX *waveformat; + IAudioClient *client; + IAudioRenderClient *render; + IAudioCaptureClient *capture; + HANDLE event; + HANDLE task; + bool coinitialized; + int framesize; + SDL_AtomicInt device_disconnecting; + bool device_lost; + bool device_dead; +}; + +// win32 implementation calls into these. +bool WASAPI_PrepDevice(SDL_AudioDevice *device); +void WASAPI_DisconnectDevice(SDL_AudioDevice *device); // don't hold the device lock when calling this! + + +// BE CAREFUL: if you are holding the device lock and proxy to the management thread with wait_until_complete, and grab the lock again, you will deadlock. +typedef bool (*ManagementThreadTask)(void *userdata); +bool WASAPI_ProxyToManagementThread(ManagementThreadTask task, void *userdata, bool *wait_until_complete); + +#ifdef __cplusplus +} +#endif + +#endif // SDL_wasapi_h_ -- cgit v1.2.3