From ca6600c8f80af3eafcf858bdba0a1d469d0281ef Mon Sep 17 00:00:00 2001 From: David Chappell Date: Mon, 8 Apr 2024 13:12:49 -0400 Subject: [PATCH] linux-pulseaudio: Implement audio output The pulseaudio plugin previously supported only audio inputs and capture of outputs using their monitor sources. This PR adds output to sinks. It can also create a virtual audio cable and tear it down when the output is stopped. * New pulse-output.c: implements pulse_output * New pulse-utils.c common functions extracted from pulse-input.c * pulse-wrapper.c: new functions to get sink info, load and unload modules * No changes to pulse input code (other than moving some to pulse-utils.c) * No UI changes. See PR for lua scripts to test. The pulse_output takes two properties: * device -- the name of the PulseAudio sink. Start with "obs-" to dynamically create a virtual audio cable * description -- Displayname of virtual audio cable --- plugins/linux-pulseaudio/CMakeLists.txt | 2 +- plugins/linux-pulseaudio/linux-pulseaudio.c | 2 + plugins/linux-pulseaudio/pulse-input.c | 110 +------- plugins/linux-pulseaudio/pulse-output.c | 286 ++++++++++++++++++++ plugins/linux-pulseaudio/pulse-utils.c | 115 ++++++++ plugins/linux-pulseaudio/pulse-utils.h | 31 +++ plugins/linux-pulseaudio/pulse-wrapper.c | 83 ++++++ plugins/linux-pulseaudio/pulse-wrapper.h | 40 +++ 8 files changed, 559 insertions(+), 110 deletions(-) create mode 100644 plugins/linux-pulseaudio/pulse-output.c create mode 100644 plugins/linux-pulseaudio/pulse-utils.c create mode 100644 plugins/linux-pulseaudio/pulse-utils.h diff --git a/plugins/linux-pulseaudio/CMakeLists.txt b/plugins/linux-pulseaudio/CMakeLists.txt index 7fff7d7b9f020e..a0c82dbcf622ce 100644 --- a/plugins/linux-pulseaudio/CMakeLists.txt +++ b/plugins/linux-pulseaudio/CMakeLists.txt @@ -13,7 +13,7 @@ add_library(linux-pulseaudio MODULE) add_library(OBS::pulseaudio ALIAS linux-pulseaudio) target_sources(linux-pulseaudio PRIVATE # cmake-format: sortable - linux-pulseaudio.c pulse-input.c pulse-wrapper.c) + linux-pulseaudio.c pulse-input.c pulse-output.c pulse-utils.c pulse-wrapper.c) target_link_libraries(linux-pulseaudio PRIVATE OBS::libobs PulseAudio::PulseAudio) set_target_properties_obs(linux-pulseaudio PROPERTIES FOLDER plugins PREFIX "") diff --git a/plugins/linux-pulseaudio/linux-pulseaudio.c b/plugins/linux-pulseaudio/linux-pulseaudio.c index 78f066e0e53b68..bab48dcc50b0b2 100644 --- a/plugins/linux-pulseaudio/linux-pulseaudio.c +++ b/plugins/linux-pulseaudio/linux-pulseaudio.c @@ -25,10 +25,12 @@ MODULE_EXPORT const char *obs_module_description(void) extern struct obs_source_info pulse_input_capture; extern struct obs_source_info pulse_output_capture; +extern const struct obs_output_info pulse_output; bool obs_module_load(void) { obs_register_source(&pulse_input_capture); obs_register_source(&pulse_output_capture); + obs_register_output(&pulse_output); return true; } diff --git a/plugins/linux-pulseaudio/pulse-input.c b/plugins/linux-pulseaudio/pulse-input.c index 218584d0fac201..76fb3b5efd5682 100644 --- a/plugins/linux-pulseaudio/pulse-input.c +++ b/plugins/linux-pulseaudio/pulse-input.c @@ -21,6 +21,7 @@ along with this program. If not, see . #include #include "pulse-wrapper.h" +#include "pulse-utils.h" #define NSEC_PER_SEC 1000000000LL #define NSEC_PER_MSEC 1000000L @@ -52,115 +53,6 @@ struct pulse_data { static void pulse_stop_recording(struct pulse_data *data); -/** - * get obs from pulse audio format - */ -static enum audio_format pulse_to_obs_audio_format(pa_sample_format_t format) -{ - switch (format) { - case PA_SAMPLE_U8: - return AUDIO_FORMAT_U8BIT; - case PA_SAMPLE_S16LE: - return AUDIO_FORMAT_16BIT; - case PA_SAMPLE_S32LE: - return AUDIO_FORMAT_32BIT; - case PA_SAMPLE_FLOAT32LE: - return AUDIO_FORMAT_FLOAT; - default: - return AUDIO_FORMAT_UNKNOWN; - } - - return AUDIO_FORMAT_UNKNOWN; -} - -/** - * Get obs speaker layout from number of channels - * - * @param channels number of channels reported by pulseaudio - * - * @return obs speaker_layout id - * - * @note This *might* not work for some rather unusual setups, but should work - * fine for the majority of cases. - */ -static enum speaker_layout -pulse_channels_to_obs_speakers(uint_fast32_t channels) -{ - switch (channels) { - case 1: - return SPEAKERS_MONO; - case 2: - return SPEAKERS_STEREO; - case 3: - return SPEAKERS_2POINT1; - case 4: - return SPEAKERS_4POINT0; - case 5: - return SPEAKERS_4POINT1; - case 6: - return SPEAKERS_5POINT1; - case 8: - return SPEAKERS_7POINT1; - } - - return SPEAKERS_UNKNOWN; -} - -static pa_channel_map pulse_channel_map(enum speaker_layout layout) -{ - pa_channel_map ret; - - ret.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT; - ret.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT; - ret.map[2] = PA_CHANNEL_POSITION_FRONT_CENTER; - ret.map[3] = PA_CHANNEL_POSITION_LFE; - ret.map[4] = PA_CHANNEL_POSITION_REAR_LEFT; - ret.map[5] = PA_CHANNEL_POSITION_REAR_RIGHT; - ret.map[6] = PA_CHANNEL_POSITION_SIDE_LEFT; - ret.map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT; - - switch (layout) { - case SPEAKERS_MONO: - ret.channels = 1; - ret.map[0] = PA_CHANNEL_POSITION_MONO; - break; - - case SPEAKERS_STEREO: - ret.channels = 2; - break; - - case SPEAKERS_2POINT1: - ret.channels = 3; - ret.map[2] = PA_CHANNEL_POSITION_LFE; - break; - - case SPEAKERS_4POINT0: - ret.channels = 4; - ret.map[3] = PA_CHANNEL_POSITION_REAR_CENTER; - break; - - case SPEAKERS_4POINT1: - ret.channels = 5; - ret.map[4] = PA_CHANNEL_POSITION_REAR_CENTER; - break; - - case SPEAKERS_5POINT1: - ret.channels = 6; - break; - - case SPEAKERS_7POINT1: - ret.channels = 8; - break; - - case SPEAKERS_UNKNOWN: - default: - ret.channels = 0; - break; - } - - return ret; -} - static inline uint64_t samples_to_ns(size_t frames, uint_fast32_t rate) { return util_mul_div64(frames, NSEC_PER_SEC, rate); diff --git a/plugins/linux-pulseaudio/pulse-output.c b/plugins/linux-pulseaudio/pulse-output.c new file mode 100644 index 00000000000000..9a3f02478eb12c --- /dev/null +++ b/plugins/linux-pulseaudio/pulse-output.c @@ -0,0 +1,286 @@ +#include +#include +#include +#include "pulse-wrapper.h" +#include "pulse-utils.h" + +#define PULSE_DATA(voidptr) struct pulse_data *data = voidptr; +#define blog(level, msg, ...) blog(level, "pulse-output: " msg, ##__VA_ARGS__) + +struct pulse_data { + obs_output_t *obs_output; + pa_stream *pulse_stream; + bool device_is_virtual; + const char *device_name; + const char *device_description; + pa_sample_spec pulse_sample_spec; + uint_fast32_t bytes_per_frame; + int_fast32_t module_idx1; + int_fast32_t module_idx2; +}; + +static void pulse_output_stop(void *data, uint64_t); + +static const char *pulse_output_get_name(void *unused) +{ + UNUSED_PARAMETER(unused); + return "Pulseaudio Output"; +} + +static void *pulse_output_create(obs_data_t *settings, obs_output_t *obs_output) +{ + UNUSED_PARAMETER(settings); + struct pulse_data *data = + (struct pulse_data *)bzalloc(sizeof(struct pulse_data)); + + data->device_name = obs_data_get_string(settings, "device"); + if (!data->device_name || strlen(data->device_name) == 0) + data->device_name = "default"; + + if ((data->device_is_virtual = + strncmp(data->device_name, "obs-", strlen("obs-")) == 0)) { + data->device_description = + obs_data_get_string(settings, "description"); + if (!data->device_description || + strlen(data->device_description) == 0) + data->device_description = data->device_name; + } + + blog(LOG_INFO, "Creating output on %s", data->device_name); + data->obs_output = obs_output; + pulse_init(); + return data; +} + +static void pulse_output_destroy(void *userdata) +{ + PULSE_DATA(userdata); + blog(LOG_INFO, "Destroying output on %s", data->device_name); + bfree(data); + pulse_unref(); +} + +/* Response to pulse_get_server_info() */ +static void pulse_server_info_cb(pa_context *c, const pa_server_info *i, + void *userdata) +{ + UNUSED_PARAMETER(c); + PULSE_DATA(userdata); + blog(LOG_INFO, "Using default sink: %s", i->default_sink_name); + memcpy(&data->pulse_sample_spec, &i->sample_spec, + sizeof(pa_sample_spec)); + pulse_signal(0); +} + +/* Response to pulse_get_sink_info() */ +static void pulse_sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, + void *userdata) +{ + UNUSED_PARAMETER(c); + PULSE_DATA(userdata); + if (eol == 0) + memcpy(&data->pulse_sample_spec, &i->sample_spec, + sizeof(pa_sample_spec)); + else + pulse_signal(0); +} + +static bool pulse_output_start(void *userdata) +{ + PULSE_DATA(userdata); + + if (data->device_is_virtual) { + blog(LOG_INFO, "Creating virtual cable %s (%s)", + data->device_name, data->device_description); + struct dstr argument; + dstr_init(&argument); + + dstr_printf( + &argument, + "sink_name=%s sink_properties=\"device.description='%s'\"", + data->device_name, data->device_description); + if ((data->module_idx1 = pulse_load_module( + "module-null-sink", argument.array)) < 0) { + blog(LOG_ERROR, "Failed to load module-null-sink: %s", + pa_strerror(pulse_errno())); + return false; + } + + dstr_printf( + &argument, + "master=%s.monitor source_name=%s source_properties=\"device.description='%s'\"", + data->device_name, data->device_name, + data->device_description); + if ((data->module_idx2 = pulse_load_module( + "module-remap-source", argument.array)) < 0) { + blog(LOG_ERROR, + "Failed to load module-remap-source: %s", + pa_strerror(pulse_errno())); + pulse_unload_module(data->module_idx1); + return false; + } + + dstr_free(&argument); + } + + blog(LOG_INFO, "Starting output on %s", data->device_name); + data->pulse_sample_spec.format = PA_SAMPLE_INVALID; + if (strcmp(data->device_name, "default") == 0) { + if (pulse_get_server_info(pulse_server_info_cb, (void *)data) < + 0) { + blog(LOG_ERROR, "Unable to get server info: %s", + pa_strerror(pulse_errno())); + return -1; + } + } else { + if (pulse_get_sink_info(pulse_sink_info_cb, data->device_name, + (void *)data) < 0) { + blog(LOG_ERROR, "Failed to get sink info: %s", + pa_strerror(pulse_errno())); + return false; + } + } + if (data->pulse_sample_spec.format == PA_SAMPLE_INVALID) { + blog(LOG_ERROR, "Failed to get sink info: %s", + pa_strerror(pulse_errno())); + return false; + } + + blog(LOG_INFO, + "Sink's audio format: %s, %" PRIu32 " Hz" + ", %" PRIu8 " channels", + pa_sample_format_to_string(data->pulse_sample_spec.format), + data->pulse_sample_spec.rate, data->pulse_sample_spec.channels); + + struct audio_convert_info conversion = {}; + conversion.samples_per_sec = (uint32_t)data->pulse_sample_spec.rate; + if ((conversion.format = pulse_to_obs_audio_format( + data->pulse_sample_spec.format)) == AUDIO_FORMAT_UNKNOWN) { + conversion.format = AUDIO_FORMAT_FLOAT; + data->pulse_sample_spec.format = PA_SAMPLE_FLOAT32LE; + blog(LOG_INFO, + "Sink's preferred sample format %s not supported by OBS, " + "using %s instead", + pa_sample_format_to_string(data->pulse_sample_spec.format), + pa_sample_format_to_string(PA_SAMPLE_FLOAT32LE)); + } + if ((conversion.speakers = pulse_channels_to_obs_speakers( + data->pulse_sample_spec.channels)) == SPEAKERS_UNKNOWN) { + conversion.speakers = SPEAKERS_STEREO; + data->pulse_sample_spec.channels = 2; + blog(LOG_INFO, + "Sink's %c channels not supported by OBS, " + "using 2 instead", + data->pulse_sample_spec.channels); + } + obs_output_set_audio_conversion(data->obs_output, &conversion); + pa_channel_map channel_map = pulse_channel_map(conversion.speakers); + data->bytes_per_frame = pa_frame_size(&data->pulse_sample_spec); + + if (!(data->pulse_stream = pulse_stream_new( + "OBS Output", &data->pulse_sample_spec, &channel_map))) { + blog(LOG_ERROR, "Unable to create stream: %s", + pa_strerror(pulse_errno())); + return false; + } + + pa_buffer_attr attr; + attr.fragsize = (uint32_t)-1; + attr.maxlength = (uint32_t)-1; + attr.minreq = (uint32_t)-1; + attr.prebuf = (uint32_t)-1; + attr.tlength = pa_usec_to_bytes(25000, &data->pulse_sample_spec); + + pulse_lock(); + int_fast32_t ret = pa_stream_connect_playback( + data->pulse_stream, + strcmp(data->device_name, "default") == 0 ? NULL + : data->device_name, + &attr, PA_STREAM_NOFLAGS, NULL, NULL); + pulse_unlock(); + if (ret < 0) { + blog(LOG_ERROR, "Unable to connect stream: %s", + pa_strerror(pulse_errno())); + pulse_output_stop(data, 0); + return false; + } + + while (true) { + int ready = pa_stream_get_state(data->pulse_stream); + if (ready == PA_STREAM_READY) + break; + else if (ready == PA_STREAM_FAILED) { + blog(LOG_ERROR, "pa_stream_get_state() failed: %s", + pa_strerror(pulse_errno())); + pulse_output_stop(data, 0); + return false; + } + pulse_wait(); + } + + obs_output_begin_data_capture(data->obs_output, 0 /* flags */); + + return true; +} + +static void pulse_output_stop(void *userdata, uint64_t) +{ + PULSE_DATA(userdata); + blog(LOG_INFO, "Stopping output on %s", data->device_name); + + obs_output_end_data_capture(data->obs_output); + + if (data->pulse_stream) { + pulse_lock(); + pa_stream_disconnect(data->pulse_stream); + pa_stream_unref(data->pulse_stream); + pulse_unlock(); + data->pulse_stream = NULL; + } + + if (data->module_idx2 > 0) { + blog(LOG_INFO, "Destroying virtual cable %s (%s)", + data->device_name, data->device_description); + pulse_unload_module(data->module_idx2); + data->module_idx2 = 0; + } + if (data->module_idx1 > 0) { + pulse_unload_module(data->module_idx1); + data->module_idx1 = 0; + } +} + +static void pulse_output_raw_audio(void *userdata, struct audio_data *frames) +{ + PULSE_DATA(userdata); + pulse_lock(); + + uint8_t *buffer; + size_t bytes = frames->frames * data->bytes_per_frame; + size_t bytesToFill = bytes; + if (pa_stream_begin_write(data->pulse_stream, (void **)&buffer, + &bytesToFill)) { + blog(LOG_ERROR, "pa_stream_begin_write() failed: %s", + pa_strerror(pulse_errno())); + } else if (bytesToFill < bytes) { + blog(LOG_ERROR, "Pulse buffer overrun"); + pa_stream_cancel_write(data->pulse_stream); + } else { + memcpy(buffer, frames->data[0], bytes); + pa_stream_write(data->pulse_stream, buffer, bytesToFill, NULL, + 0LL, PA_SEEK_RELATIVE); + } + + pulse_unlock(); +} + +const struct obs_output_info pulse_output = { + .id = "pulse_output", + .flags = OBS_OUTPUT_AUDIO, + .get_name = pulse_output_get_name, + .create = pulse_output_create, + .destroy = pulse_output_destroy, + .start = pulse_output_start, + .stop = pulse_output_stop, + .raw_audio = pulse_output_raw_audio, +}; diff --git a/plugins/linux-pulseaudio/pulse-utils.c b/plugins/linux-pulseaudio/pulse-utils.c new file mode 100644 index 00000000000000..f0c250fa3da8df --- /dev/null +++ b/plugins/linux-pulseaudio/pulse-utils.c @@ -0,0 +1,115 @@ +/* +Copyright (C) 2014 by Leonhard Oelke + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include + +#include "pulse-utils.h" + +enum audio_format pulse_to_obs_audio_format(pa_sample_format_t format) +{ + switch (format) { + case PA_SAMPLE_U8: + return AUDIO_FORMAT_U8BIT; + case PA_SAMPLE_S16LE: + return AUDIO_FORMAT_16BIT; + case PA_SAMPLE_S32LE: + return AUDIO_FORMAT_32BIT; + case PA_SAMPLE_FLOAT32LE: + return AUDIO_FORMAT_FLOAT; + default: + return AUDIO_FORMAT_UNKNOWN; + } + + return AUDIO_FORMAT_UNKNOWN; +} + +enum speaker_layout pulse_channels_to_obs_speakers(uint_fast32_t channels) +{ + switch (channels) { + case 1: + return SPEAKERS_MONO; + case 2: + return SPEAKERS_STEREO; + case 3: + return SPEAKERS_2POINT1; + case 4: + return SPEAKERS_4POINT0; + case 5: + return SPEAKERS_4POINT1; + case 6: + return SPEAKERS_5POINT1; + case 8: + return SPEAKERS_7POINT1; + } + + return SPEAKERS_UNKNOWN; +} + +pa_channel_map pulse_channel_map(enum speaker_layout layout) +{ + pa_channel_map ret; + + ret.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT; + ret.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT; + ret.map[2] = PA_CHANNEL_POSITION_FRONT_CENTER; + ret.map[3] = PA_CHANNEL_POSITION_LFE; + ret.map[4] = PA_CHANNEL_POSITION_REAR_LEFT; + ret.map[5] = PA_CHANNEL_POSITION_REAR_RIGHT; + ret.map[6] = PA_CHANNEL_POSITION_SIDE_LEFT; + ret.map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT; + + switch (layout) { + case SPEAKERS_MONO: + ret.channels = 1; + ret.map[0] = PA_CHANNEL_POSITION_MONO; + break; + + case SPEAKERS_STEREO: + ret.channels = 2; + break; + + case SPEAKERS_2POINT1: + ret.channels = 3; + ret.map[2] = PA_CHANNEL_POSITION_LFE; + break; + + case SPEAKERS_4POINT0: + ret.channels = 4; + ret.map[3] = PA_CHANNEL_POSITION_REAR_CENTER; + break; + + case SPEAKERS_4POINT1: + ret.channels = 5; + ret.map[4] = PA_CHANNEL_POSITION_REAR_CENTER; + break; + + case SPEAKERS_5POINT1: + ret.channels = 6; + break; + + case SPEAKERS_7POINT1: + ret.channels = 8; + break; + + case SPEAKERS_UNKNOWN: + default: + ret.channels = 0; + break; + } + + return ret; +} diff --git a/plugins/linux-pulseaudio/pulse-utils.h b/plugins/linux-pulseaudio/pulse-utils.h new file mode 100644 index 00000000000000..dec6c744220018 --- /dev/null +++ b/plugins/linux-pulseaudio/pulse-utils.h @@ -0,0 +1,31 @@ +#include +#include + +/** + * Get obs audo format from pulse audio format + * @param format pulseaudio format + * + * @return obs audio format + */ +enum audio_format pulse_to_obs_audio_format(pa_sample_format_t format); + +/** + * Get obs speaker layout from number of channels + * + * @param channels number of channels reported by pulseaudio + * + * @return obs speaker_layout id + * + * @note This *might* not work for some rather unusual setups, but should work + * fine for the majority of cases. + */ +enum speaker_layout pulse_channels_to_obs_speakers(uint_fast32_t channels); + +/** + * Get a pulseaudio channel map for an obs speaker layout + * + * @param layout obs speaker layout + * + * @return pulseaudio channel map + */ +pa_channel_map pulse_channel_map(enum speaker_layout layout); diff --git a/plugins/linux-pulseaudio/pulse-wrapper.c b/plugins/linux-pulseaudio/pulse-wrapper.c index bb65148bd13faa..d7e4b5039ac3d6 100644 --- a/plugins/linux-pulseaudio/pulse-wrapper.c +++ b/plugins/linux-pulseaudio/pulse-wrapper.c @@ -267,3 +267,86 @@ pa_stream *pulse_stream_new(const char *name, const pa_sample_spec *ss, pulse_unlock(); return s; } + +/* Wait for callback to finish + TODO: Finish refactoring by replacing the copy-pasted code in four + pre-existing functions above with calls to this. +*/ +static int_fast32_t pulse_op_tail(pa_operation *op) +{ + if (!op) { + pulse_unlock(); + return -1; + } + while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) + pulse_wait(); + pa_operation_unref(op); + + pulse_unlock(); + + return 0; +} + +int_fast32_t pulse_get_sink_info(pa_sink_info_cb_t cb, const char *name, + void *userdata) +{ + if (pulse_context_ready() < 0) + return -1; + + pulse_lock(); + + return pulse_op_tail(pa_context_get_sink_info_by_name( + pulse_context, name, cb, userdata)); +} + +static void module_load_cb(pa_context *c, uint32_t idx, void *userdata) +{ + UNUSED_PARAMETER(c); + blog(LOG_INFO, "Module loaded: %d", (int)idx); + *(uint32_t *)userdata = idx; + pulse_signal(0); +} + +int_fast32_t pulse_load_module(const char *name, const char *argument) +{ + if (pulse_context_ready() < 0) + return -1; + + pulse_lock(); + + uint32_t idx = -1; + int32_t result = pulse_op_tail(pa_context_load_module( + pulse_context, name, argument, module_load_cb, &idx)); + if (result < 0) + return -1; + return idx; +} + +static void module_unload_cb(pa_context *c, int success, void *userdata) +{ + UNUSED_PARAMETER(c); + UNUSED_PARAMETER(success); + blog(LOG_INFO, "Module unloaded: %d", success); + *(int *)userdata = success; + pulse_signal(0); +} + +int_fast32_t pulse_unload_module(uint32_t idx) +{ + if (pulse_context_ready() < 0) + return -1; + + pulse_lock(); + + int success = -1; + int32_t result = pulse_op_tail(pa_context_unload_module( + pulse_context, idx, module_unload_cb, &success)); + if (result < 0) + return -1; + return success; +} + +int pulse_errno() +{ + return pa_context_errno(pulse_context); +} diff --git a/plugins/linux-pulseaudio/pulse-wrapper.h b/plugins/linux-pulseaudio/pulse-wrapper.h index afd738e1c47b5f..0feae73f0b40a0 100644 --- a/plugins/linux-pulseaudio/pulse-wrapper.h +++ b/plugins/linux-pulseaudio/pulse-wrapper.h @@ -149,3 +149,43 @@ int_fast32_t pulse_get_server_info(pa_server_info_cb_t cb, void *userdata); */ pa_stream *pulse_stream_new(const char *name, const pa_sample_spec *ss, const pa_channel_map *map); + +/** + * Request source information from a specific sink + * + * The function will block until the operation was executed and the mainloop + * called the provided callback function. + * + * @param cb pointer to the callback function + * @param name the sink name to get information for + * @param userdata pointer to userdata the callback will be called with + * + * @return negative on error + * + * @note The function will block until the server context is ready. + * + * @warning call without active locks + */ +int_fast32_t pulse_get_sink_info(pa_sink_info_cb_t cb, const char *name, + void *userdata); + +/** Load module into pulseaudio + * + * @param name name of module + * @param argument string with arguments to pass to module + * + * @return negative on error, module index on success + */ +int_fast32_t pulse_load_module(const char *name, const char *argument); + +/** Remove module from pulseaudio + * + * @param idx module index previously returned by pulse_module_load() + */ +int_fast32_t pulse_unload_module(uint32_t idx); + +/** Get the last pulseaudio error code from the wrapped context + * + * @return error code suitable for passing to pa_strerror() +*/ +int pulse_errno();