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 --- .../src/audio/pulseaudio/SDL_pulseaudio.c | 1037 ++++++++++++++++++++ 1 file changed, 1037 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.c (limited to 'contrib/SDL-3.2.8/src/audio/pulseaudio/SDL_pulseaudio.c') 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 -- cgit v1.2.3