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();