mirror of https://github.com/libsdl-org/SDL.git
1029 lines
41 KiB
C
1029 lines
41 KiB
C
/*
|
|
Simple DirectMedia Layer
|
|
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
|
|
|
|
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 <signal.h>
|
|
#endif
|
|
#include <unistd.h>
|
|
#include <sys/types.h>
|
|
|
|
#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 void *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
|
|
if (!(pulseaudio_threaded_mainloop = PULSEAUDIO_pa_threaded_mainloop_new())) {
|
|
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?
|
|
|
|
if (!(proplist = PULSEAUDIO_pa_proplist_new())) {
|
|
return SDL_SetError("pa_proplist_new() 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) {
|
|
return 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_AtomicGet(&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_AtomicGet(&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;
|
|
|
|
PULSEAUDIO_pa_threaded_mainloop_lock(pulseaudio_threaded_mainloop);
|
|
|
|
if (h->recordingbuf) {
|
|
PULSEAUDIO_pa_stream_drop(h->stream);
|
|
h->recordingbuf = NULL;
|
|
h->recordinglen = 0;
|
|
}
|
|
|
|
while (!SDL_AtomicGet(&device->shutdown) && (PULSEAUDIO_pa_stream_readable_size(h->stream) > 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;
|
|
}
|
|
|
|
if (PULSEAUDIO_pa_stream_readable_size(h->stream) > 0) {
|
|
// a new fragment is available! Just dump it.
|
|
PULSEAUDIO_pa_stream_peek(h->stream, &data, &nbytes);
|
|
PULSEAUDIO_pa_stream_drop(h->stream); // drop this fragment.
|
|
}
|
|
}
|
|
|
|
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_SetThreadPriority(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_AtomicGet(&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_AtomicSet(&pulseaudio_hotplug_thread_active, 1);
|
|
pulseaudio_hotplug_thread = SDL_CreateThreadWithStackSize(HotplugThread, "PulseHotplug", 256 * 1024, ready_sem); // !!! FIXME: this can probably survive in significantly less stack space.
|
|
SDL_WaitSemaphore(ready_sem);
|
|
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_AtomicSet(&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
|
|
};
|
|
|
|
#endif // SDL_AUDIO_DRIVER_PULSEAUDIO
|