diff options
Diffstat (limited to 'third_party/rust/cubeb-sys/libcubeb/src/cubeb_pulse.c')
-rw-r--r-- | third_party/rust/cubeb-sys/libcubeb/src/cubeb_pulse.c | 1709 |
1 files changed, 1709 insertions, 0 deletions
diff --git a/third_party/rust/cubeb-sys/libcubeb/src/cubeb_pulse.c b/third_party/rust/cubeb-sys/libcubeb/src/cubeb_pulse.c new file mode 100644 index 0000000000..6866405250 --- /dev/null +++ b/third_party/rust/cubeb-sys/libcubeb/src/cubeb_pulse.c @@ -0,0 +1,1709 @@ +/* + * Copyright © 2011 Mozilla Foundation + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#undef NDEBUG +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_mixer.h" +#include "cubeb_strings.h" +#include <assert.h> +#include <dlfcn.h> +#include <pulse/pulseaudio.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifdef DISABLE_LIBPULSE_DLOPEN +#define WRAP(x) x +#else +#define WRAP(x) (*cubeb_##x) +#define LIBPULSE_API_VISIT(X) \ + X(pa_channel_map_can_balance) \ + X(pa_channel_map_init) \ + X(pa_context_connect) \ + X(pa_context_disconnect) \ + X(pa_context_drain) \ + X(pa_context_get_server_info) \ + X(pa_context_get_sink_info_by_name) \ + X(pa_context_get_sink_info_list) \ + X(pa_context_get_sink_input_info) \ + X(pa_context_get_source_info_list) \ + X(pa_context_get_state) \ + X(pa_context_new) \ + X(pa_context_rttime_new) \ + X(pa_context_set_sink_input_volume) \ + X(pa_context_set_state_callback) \ + X(pa_context_unref) \ + X(pa_cvolume_set) \ + X(pa_cvolume_set_balance) \ + X(pa_frame_size) \ + X(pa_operation_get_state) \ + X(pa_operation_unref) \ + X(pa_proplist_gets) \ + X(pa_rtclock_now) \ + X(pa_stream_begin_write) \ + X(pa_stream_cancel_write) \ + X(pa_stream_connect_playback) \ + X(pa_stream_cork) \ + X(pa_stream_disconnect) \ + X(pa_stream_get_channel_map) \ + X(pa_stream_get_index) \ + X(pa_stream_get_latency) \ + X(pa_stream_get_sample_spec) \ + X(pa_stream_get_state) \ + X(pa_stream_get_time) \ + X(pa_stream_new) \ + X(pa_stream_set_state_callback) \ + X(pa_stream_set_write_callback) \ + X(pa_stream_unref) \ + X(pa_stream_update_timing_info) \ + X(pa_stream_write) \ + X(pa_sw_volume_from_linear) \ + X(pa_threaded_mainloop_free) \ + X(pa_threaded_mainloop_get_api) \ + X(pa_threaded_mainloop_in_thread) \ + X(pa_threaded_mainloop_lock) \ + X(pa_threaded_mainloop_new) \ + X(pa_threaded_mainloop_signal) \ + X(pa_threaded_mainloop_start) \ + X(pa_threaded_mainloop_stop) \ + X(pa_threaded_mainloop_unlock) \ + X(pa_threaded_mainloop_wait) \ + X(pa_usec_to_bytes) \ + X(pa_stream_set_read_callback) \ + X(pa_stream_connect_record) \ + X(pa_stream_readable_size) \ + X(pa_stream_writable_size) \ + X(pa_stream_peek) \ + X(pa_stream_drop) \ + X(pa_stream_get_buffer_attr) \ + X(pa_stream_get_device_name) \ + X(pa_context_set_subscribe_callback) \ + X(pa_context_subscribe) \ + X(pa_mainloop_api_once) \ + X(pa_get_library_version) \ + X(pa_channel_map_init_auto) \ + X(pa_stream_set_name) + +#define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x; +LIBPULSE_API_VISIT(MAKE_TYPEDEF); +#undef MAKE_TYPEDEF +#endif + +#if PA_CHECK_VERSION(2, 0, 0) +static int has_pulse_v2 = 0; +#endif + +static struct cubeb_ops const pulse_ops; + +struct cubeb_default_sink_info { + pa_channel_map channel_map; + uint32_t sample_spec_rate; + pa_sink_flags_t flags; +}; + +struct cubeb { + struct cubeb_ops const * ops; + void * libpulse; + pa_threaded_mainloop * mainloop; + pa_context * context; + struct cubeb_default_sink_info * default_sink_info; + char * context_name; + int error; + cubeb_device_collection_changed_callback output_collection_changed_callback; + void * output_collection_changed_user_ptr; + cubeb_device_collection_changed_callback input_collection_changed_callback; + void * input_collection_changed_user_ptr; + cubeb_strings * device_ids; +}; + +struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ + cubeb * context; + void * user_ptr; + /**/ + pa_stream * output_stream; + pa_stream * input_stream; + cubeb_data_callback data_callback; + cubeb_state_callback state_callback; + pa_time_event * drain_timer; + pa_sample_spec output_sample_spec; + pa_sample_spec input_sample_spec; + int shutdown; + float volume; + cubeb_state state; +}; + +static const float PULSE_NO_GAIN = -1.0; + +enum cork_state { UNCORK = 0, CORK = 1 << 0, NOTIFY = 1 << 1 }; + +static int +intern_device_id(cubeb * ctx, char const ** id) +{ + char const * interned; + + assert(ctx); + assert(id); + + interned = cubeb_strings_intern(ctx->device_ids, *id); + if (!interned) { + return CUBEB_ERROR; + } + + *id = interned; + + return CUBEB_OK; +} + +static void +sink_info_callback(pa_context * context, const pa_sink_info * info, int eol, + void * u) +{ + (void)context; + cubeb * ctx = u; + if (!eol) { + free(ctx->default_sink_info); + ctx->default_sink_info = malloc(sizeof(struct cubeb_default_sink_info)); + memcpy(&ctx->default_sink_info->channel_map, &info->channel_map, + sizeof(pa_channel_map)); + ctx->default_sink_info->sample_spec_rate = info->sample_spec.rate; + ctx->default_sink_info->flags = info->flags; + } + WRAP(pa_threaded_mainloop_signal)(ctx->mainloop, 0); +} + +static void +server_info_callback(pa_context * context, const pa_server_info * info, + void * u) +{ + pa_operation * o; + o = WRAP(pa_context_get_sink_info_by_name)(context, info->default_sink_name, + sink_info_callback, u); + if (o) { + WRAP(pa_operation_unref)(o); + } +} + +static void +context_state_callback(pa_context * c, void * u) +{ + cubeb * ctx = u; + if (!PA_CONTEXT_IS_GOOD(WRAP(pa_context_get_state)(c))) { + ctx->error = 1; + } + WRAP(pa_threaded_mainloop_signal)(ctx->mainloop, 0); +} + +static void +context_notify_callback(pa_context * c, void * u) +{ + (void)c; + cubeb * ctx = u; + WRAP(pa_threaded_mainloop_signal)(ctx->mainloop, 0); +} + +static void +stream_success_callback(pa_stream * s, int success, void * u) +{ + (void)s; + (void)success; + cubeb_stream * stm = u; + WRAP(pa_threaded_mainloop_signal)(stm->context->mainloop, 0); +} + +static void +stream_state_change_callback(cubeb_stream * stm, cubeb_state s) +{ + stm->state = s; + stm->state_callback(stm, stm->user_ptr, s); +} + +static void +stream_drain_callback(pa_mainloop_api * a, pa_time_event * e, + struct timeval const * tv, void * u) +{ + (void)a; + (void)tv; + cubeb_stream * stm = u; + assert(stm->drain_timer == e); + stream_state_change_callback(stm, CUBEB_STATE_DRAINED); + /* there's no pa_rttime_free, so use this instead. */ + a->time_free(stm->drain_timer); + stm->drain_timer = NULL; + WRAP(pa_threaded_mainloop_signal)(stm->context->mainloop, 0); +} + +static void +stream_state_callback(pa_stream * s, void * u) +{ + cubeb_stream * stm = u; + if (!PA_STREAM_IS_GOOD(WRAP(pa_stream_get_state)(s))) { + stream_state_change_callback(stm, CUBEB_STATE_ERROR); + } + WRAP(pa_threaded_mainloop_signal)(stm->context->mainloop, 0); +} + +static void +trigger_user_callback(pa_stream * s, void const * input_data, size_t nbytes, + cubeb_stream * stm) +{ + void * buffer; + size_t size; + int r; + long got; + size_t towrite, read_offset; + size_t frame_size; + + frame_size = WRAP(pa_frame_size)(&stm->output_sample_spec); + assert(nbytes % frame_size == 0); + + towrite = nbytes; + read_offset = 0; + while (towrite) { + size = towrite; + r = WRAP(pa_stream_begin_write)(s, &buffer, &size); + // Note: this has failed running under rr on occassion - needs + // investigation. + assert(r == 0); + assert(size > 0); + assert(size % frame_size == 0); + + LOGV("Trigger user callback with output buffer size=%zd, read_offset=%zd", + size, read_offset); + got = stm->data_callback(stm, stm->user_ptr, + (uint8_t const *)input_data + read_offset, buffer, + size / frame_size); + if (got < 0) { + WRAP(pa_stream_cancel_write)(s); + stm->shutdown = 1; + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + return; + } + // If more iterations move offset of read buffer + if (input_data) { + size_t in_frame_size = WRAP(pa_frame_size)(&stm->input_sample_spec); + read_offset += (size / frame_size) * in_frame_size; + } + + if (stm->volume != PULSE_NO_GAIN) { + uint32_t samples = size * stm->output_sample_spec.channels / frame_size; + + if (stm->output_sample_spec.format == PA_SAMPLE_S16BE || + stm->output_sample_spec.format == PA_SAMPLE_S16LE) { + short * b = buffer; + for (uint32_t i = 0; i < samples; i++) { + b[i] *= stm->volume; + } + } else { + float * b = buffer; + for (uint32_t i = 0; i < samples; i++) { + b[i] *= stm->volume; + } + } + } + + r = WRAP(pa_stream_write)(s, buffer, got * frame_size, NULL, 0, + PA_SEEK_RELATIVE); + assert(r == 0); + + if ((size_t)got < size / frame_size) { + pa_usec_t latency = 0; + r = WRAP(pa_stream_get_latency)(s, &latency, NULL); + if (r == -PA_ERR_NODATA) { + /* this needs a better guess. */ + latency = 100 * PA_USEC_PER_MSEC; + } + assert(r == 0 || r == -PA_ERR_NODATA); + /* pa_stream_drain is useless, see PA bug# 866. this is a workaround. */ + /* arbitrary safety margin: double the current latency. */ + assert(!stm->drain_timer); + stm->drain_timer = WRAP(pa_context_rttime_new)( + stm->context->context, WRAP(pa_rtclock_now)() + 2 * latency, + stream_drain_callback, stm); + stm->shutdown = 1; + return; + } + + towrite -= size; + } + + assert(towrite == 0); +} + +static int +read_from_input(pa_stream * s, void const ** buffer, size_t * size) +{ + size_t readable_size = WRAP(pa_stream_readable_size)(s); + if (readable_size > 0) { + if (WRAP(pa_stream_peek)(s, buffer, size) < 0) { + return -1; + } + } + return readable_size; +} + +static void +stream_write_callback(pa_stream * s, size_t nbytes, void * u) +{ + LOGV("Output callback to be written buffer size %zd", nbytes); + cubeb_stream * stm = u; + if (stm->shutdown || stm->state != CUBEB_STATE_STARTED) { + return; + } + + if (!stm->input_stream) { + // Output/playback only operation. + // Write directly to output + assert(!stm->input_stream && stm->output_stream); + trigger_user_callback(s, NULL, nbytes, stm); + } +} + +static void +stream_read_callback(pa_stream * s, size_t nbytes, void * u) +{ + LOGV("Input callback buffer size %zd", nbytes); + cubeb_stream * stm = u; + if (stm->shutdown) { + return; + } + + void const * read_data = NULL; + size_t read_size; + while (read_from_input(s, &read_data, &read_size) > 0) { + /* read_data can be NULL in case of a hole. */ + if (read_data) { + size_t in_frame_size = WRAP(pa_frame_size)(&stm->input_sample_spec); + size_t read_frames = read_size / in_frame_size; + + if (stm->output_stream) { + // input/capture + output/playback operation + size_t out_frame_size = WRAP(pa_frame_size)(&stm->output_sample_spec); + size_t write_size = read_frames * out_frame_size; + // Offer full duplex data for writing + trigger_user_callback(stm->output_stream, read_data, write_size, stm); + } else { + // input/capture only operation. Call callback directly + long got = stm->data_callback(stm, stm->user_ptr, read_data, NULL, + read_frames); + if (got < 0 || (size_t)got != read_frames) { + WRAP(pa_stream_cancel_write)(s); + stm->shutdown = 1; + if (got < 0) { + stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); + } + break; + } + } + } + if (read_size > 0) { + WRAP(pa_stream_drop)(s); + } + + if (stm->shutdown) { + return; + } + } +} + +static int +wait_until_context_ready(cubeb * ctx) +{ + for (;;) { + pa_context_state_t state = WRAP(pa_context_get_state)(ctx->context); + if (!PA_CONTEXT_IS_GOOD(state)) + return -1; + if (state == PA_CONTEXT_READY) + break; + WRAP(pa_threaded_mainloop_wait)(ctx->mainloop); + } + return 0; +} + +static int +wait_until_io_stream_ready(pa_stream * stream, pa_threaded_mainloop * mainloop) +{ + if (!stream || !mainloop) { + return -1; + } + for (;;) { + pa_stream_state_t state = WRAP(pa_stream_get_state)(stream); + if (!PA_STREAM_IS_GOOD(state)) + return -1; + if (state == PA_STREAM_READY) + break; + WRAP(pa_threaded_mainloop_wait)(mainloop); + } + return 0; +} + +static int +wait_until_stream_ready(cubeb_stream * stm) +{ + if (stm->output_stream && + wait_until_io_stream_ready(stm->output_stream, stm->context->mainloop) == + -1) { + return -1; + } + if (stm->input_stream && + wait_until_io_stream_ready(stm->input_stream, stm->context->mainloop) == + -1) { + return -1; + } + return 0; +} + +static int +operation_wait(cubeb * ctx, pa_stream * stream, pa_operation * o) +{ + while (WRAP(pa_operation_get_state)(o) == PA_OPERATION_RUNNING) { + WRAP(pa_threaded_mainloop_wait)(ctx->mainloop); + if (!PA_CONTEXT_IS_GOOD(WRAP(pa_context_get_state)(ctx->context))) { + return -1; + } + if (stream && !PA_STREAM_IS_GOOD(WRAP(pa_stream_get_state)(stream))) { + return -1; + } + } + return 0; +} + +static void +cork_io_stream(cubeb_stream * stm, pa_stream * io_stream, enum cork_state state) +{ + pa_operation * o; + if (!io_stream) { + return; + } + o = WRAP(pa_stream_cork)(io_stream, state & CORK, stream_success_callback, + stm); + if (o) { + operation_wait(stm->context, io_stream, o); + WRAP(pa_operation_unref)(o); + } +} + +static void +stream_cork(cubeb_stream * stm, enum cork_state state) +{ + WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); + cork_io_stream(stm, stm->output_stream, state); + cork_io_stream(stm, stm->input_stream, state); + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + + if (state & NOTIFY) { + stream_state_change_callback(stm, state & CORK ? CUBEB_STATE_STOPPED + : CUBEB_STATE_STARTED); + } +} + +static int +stream_update_timing_info(cubeb_stream * stm) +{ + int r = -1; + pa_operation * o = NULL; + if (stm->output_stream) { + o = WRAP(pa_stream_update_timing_info)(stm->output_stream, + stream_success_callback, stm); + if (o) { + r = operation_wait(stm->context, stm->output_stream, o); + WRAP(pa_operation_unref)(o); + } + if (r != 0) { + return r; + } + } + + if (stm->input_stream) { + o = WRAP(pa_stream_update_timing_info)(stm->input_stream, + stream_success_callback, stm); + if (o) { + r = operation_wait(stm->context, stm->input_stream, o); + WRAP(pa_operation_unref)(o); + } + } + + return r; +} + +static pa_channel_position_t +cubeb_channel_to_pa_channel(cubeb_channel channel) +{ + switch (channel) { + case CHANNEL_FRONT_LEFT: + return PA_CHANNEL_POSITION_FRONT_LEFT; + case CHANNEL_FRONT_RIGHT: + return PA_CHANNEL_POSITION_FRONT_RIGHT; + case CHANNEL_FRONT_CENTER: + return PA_CHANNEL_POSITION_FRONT_CENTER; + case CHANNEL_LOW_FREQUENCY: + return PA_CHANNEL_POSITION_LFE; + case CHANNEL_BACK_LEFT: + return PA_CHANNEL_POSITION_REAR_LEFT; + case CHANNEL_BACK_RIGHT: + return PA_CHANNEL_POSITION_REAR_RIGHT; + case CHANNEL_FRONT_LEFT_OF_CENTER: + return PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER; + case CHANNEL_FRONT_RIGHT_OF_CENTER: + return PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER; + case CHANNEL_BACK_CENTER: + return PA_CHANNEL_POSITION_REAR_CENTER; + case CHANNEL_SIDE_LEFT: + return PA_CHANNEL_POSITION_SIDE_LEFT; + case CHANNEL_SIDE_RIGHT: + return PA_CHANNEL_POSITION_SIDE_RIGHT; + case CHANNEL_TOP_CENTER: + return PA_CHANNEL_POSITION_TOP_CENTER; + case CHANNEL_TOP_FRONT_LEFT: + return PA_CHANNEL_POSITION_TOP_FRONT_LEFT; + case CHANNEL_TOP_FRONT_CENTER: + return PA_CHANNEL_POSITION_TOP_FRONT_CENTER; + case CHANNEL_TOP_FRONT_RIGHT: + return PA_CHANNEL_POSITION_TOP_FRONT_RIGHT; + case CHANNEL_TOP_BACK_LEFT: + return PA_CHANNEL_POSITION_TOP_REAR_LEFT; + case CHANNEL_TOP_BACK_CENTER: + return PA_CHANNEL_POSITION_TOP_REAR_CENTER; + case CHANNEL_TOP_BACK_RIGHT: + return PA_CHANNEL_POSITION_TOP_REAR_RIGHT; + default: + return PA_CHANNEL_POSITION_INVALID; + } +} + +static void +layout_to_channel_map(cubeb_channel_layout layout, pa_channel_map * cm) +{ + assert(cm && layout != CUBEB_LAYOUT_UNDEFINED); + + WRAP(pa_channel_map_init)(cm); + + uint32_t channels = 0; + cubeb_channel_layout channelMap = layout; + for (uint32_t i = 0; channelMap != 0; ++i) { + uint32_t channel = (channelMap & 1) << i; + if (channel != 0) { + cm->map[channels] = cubeb_channel_to_pa_channel(channel); + channels++; + } + channelMap = channelMap >> 1; + } + unsigned int channels_from_layout = cubeb_channel_layout_nb_channels(layout); + assert(channels_from_layout <= UINT8_MAX); + cm->channels = (uint8_t)channels_from_layout; + + // Special case single channel center mapping as mono. + if (cm->channels == 1 && cm->map[0] == PA_CHANNEL_POSITION_FRONT_CENTER) { + cm->map[0] = PA_CHANNEL_POSITION_MONO; + } +} + +static void +pulse_context_destroy(cubeb * ctx); +static void +pulse_destroy(cubeb * ctx); + +static int +pulse_context_init(cubeb * ctx) +{ + int r; + + if (ctx->context) { + assert(ctx->error == 1); + pulse_context_destroy(ctx); + } + + ctx->context = WRAP(pa_context_new)( + WRAP(pa_threaded_mainloop_get_api)(ctx->mainloop), ctx->context_name); + if (!ctx->context) { + return -1; + } + WRAP(pa_context_set_state_callback) + (ctx->context, context_state_callback, ctx); + + WRAP(pa_threaded_mainloop_lock)(ctx->mainloop); + r = WRAP(pa_context_connect)(ctx->context, NULL, 0, NULL); + + if (r < 0 || wait_until_context_ready(ctx) != 0) { + WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); + pulse_context_destroy(ctx); + ctx->context = NULL; + return -1; + } + + WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); + + ctx->error = 0; + + return 0; +} + +static int +pulse_subscribe_notifications(cubeb * context, pa_subscription_mask_t mask); + +/*static*/ int +pulse_init(cubeb ** context, char const * context_name) +{ + void * libpulse = NULL; + cubeb * ctx; + pa_operation * o; + + *context = NULL; + +#ifndef DISABLE_LIBPULSE_DLOPEN + libpulse = dlopen("libpulse.so.0", RTLD_LAZY); + if (!libpulse) { + libpulse = dlopen("libpulse.so", RTLD_LAZY); + if (!libpulse) { + return CUBEB_ERROR; + } + } + +#define LOAD(x) \ + { \ + cubeb_##x = dlsym(libpulse, #x); \ + if (!cubeb_##x) { \ + dlclose(libpulse); \ + return CUBEB_ERROR; \ + } \ + } + + LIBPULSE_API_VISIT(LOAD); +#undef LOAD +#endif + +#if PA_CHECK_VERSION(2, 0, 0) + const char * version = WRAP(pa_get_library_version)(); + has_pulse_v2 = strtol(version, NULL, 10) >= 2; +#endif + + ctx = calloc(1, sizeof(*ctx)); + assert(ctx); + + ctx->ops = &pulse_ops; + ctx->libpulse = libpulse; + if (cubeb_strings_init(&ctx->device_ids) != CUBEB_OK) { + pulse_destroy(ctx); + return CUBEB_ERROR; + } + + ctx->mainloop = WRAP(pa_threaded_mainloop_new)(); + ctx->default_sink_info = NULL; + + WRAP(pa_threaded_mainloop_start)(ctx->mainloop); + + ctx->context_name = context_name ? strdup(context_name) : NULL; + if (pulse_context_init(ctx) != 0) { + pulse_destroy(ctx); + return CUBEB_ERROR; + } + + /* server_info_callback performs a second async query, which is + responsible for initializing default_sink_info and signalling the + mainloop to end the wait. */ + WRAP(pa_threaded_mainloop_lock)(ctx->mainloop); + o = WRAP(pa_context_get_server_info)(ctx->context, server_info_callback, ctx); + if (o) { + operation_wait(ctx, NULL, o); + WRAP(pa_operation_unref)(o); + } + WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); + + /* Update `default_sink_info` when the default device changes. */ + pulse_subscribe_notifications(ctx, PA_SUBSCRIPTION_MASK_SERVER); + + *context = ctx; + + return CUBEB_OK; +} + +static char const * +pulse_get_backend_id(cubeb * ctx) +{ + (void)ctx; + return "pulse"; +} + +static int +pulse_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ + (void)ctx; + assert(ctx && max_channels); + + if (!ctx->default_sink_info) + return CUBEB_ERROR; + + *max_channels = ctx->default_sink_info->channel_map.channels; + + return CUBEB_OK; +} + +static int +pulse_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +{ + assert(ctx && rate); + (void)ctx; + + if (!ctx->default_sink_info) + return CUBEB_ERROR; + + *rate = ctx->default_sink_info->sample_spec_rate; + + return CUBEB_OK; +} + +static int +pulse_get_min_latency(cubeb * ctx, cubeb_stream_params params, + uint32_t * latency_frames) +{ + (void)ctx; + // According to PulseAudio developers, this is a safe minimum. + *latency_frames = 25 * params.rate / 1000; + + return CUBEB_OK; +} + +static void +pulse_context_destroy(cubeb * ctx) +{ + pa_operation * o; + + WRAP(pa_threaded_mainloop_lock)(ctx->mainloop); + o = WRAP(pa_context_drain)(ctx->context, context_notify_callback, ctx); + if (o) { + operation_wait(ctx, NULL, o); + WRAP(pa_operation_unref)(o); + } + WRAP(pa_context_set_state_callback)(ctx->context, NULL, NULL); + WRAP(pa_context_disconnect)(ctx->context); + WRAP(pa_context_unref)(ctx->context); + WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); +} + +static void +pulse_destroy(cubeb * ctx) +{ + assert(!ctx->input_collection_changed_callback && + !ctx->input_collection_changed_user_ptr && + !ctx->output_collection_changed_callback && + !ctx->output_collection_changed_user_ptr); + free(ctx->context_name); + if (ctx->context) { + pulse_context_destroy(ctx); + } + + if (ctx->mainloop) { + WRAP(pa_threaded_mainloop_stop)(ctx->mainloop); + WRAP(pa_threaded_mainloop_free)(ctx->mainloop); + } + + if (ctx->device_ids) { + cubeb_strings_destroy(ctx->device_ids); + } +#ifndef DISABLE_LIBPULSE_DLOPEN + if (ctx->libpulse) { + dlclose(ctx->libpulse); + } +#endif + free(ctx->default_sink_info); + free(ctx); +} + +static void +pulse_stream_destroy(cubeb_stream * stm); + +static pa_sample_format_t +to_pulse_format(cubeb_sample_format format) +{ + switch (format) { + case CUBEB_SAMPLE_S16LE: + return PA_SAMPLE_S16LE; + case CUBEB_SAMPLE_S16BE: + return PA_SAMPLE_S16BE; + case CUBEB_SAMPLE_FLOAT32LE: + return PA_SAMPLE_FLOAT32LE; + case CUBEB_SAMPLE_FLOAT32BE: + return PA_SAMPLE_FLOAT32BE; + default: + return PA_SAMPLE_INVALID; + } +} + +static cubeb_channel_layout +pulse_default_layout_for_channels(uint32_t ch) +{ + assert(ch > 0 && ch <= 8); + switch (ch) { + case 1: + return CUBEB_LAYOUT_MONO; + case 2: + return CUBEB_LAYOUT_STEREO; + case 3: + return CUBEB_LAYOUT_3F; + case 4: + return CUBEB_LAYOUT_QUAD; + case 5: + return CUBEB_LAYOUT_3F2; + case 6: + return CUBEB_LAYOUT_3F_LFE | CHANNEL_SIDE_LEFT | CHANNEL_SIDE_RIGHT; + case 7: + return CUBEB_LAYOUT_3F3R_LFE; + case 8: + return CUBEB_LAYOUT_3F4_LFE; + } + // Never get here! + return CUBEB_LAYOUT_UNDEFINED; +} + +static int +create_pa_stream(cubeb_stream * stm, pa_stream ** pa_stm, + cubeb_stream_params * stream_params, char const * stream_name) +{ + assert(stm && stream_params); + assert(&stm->input_stream == pa_stm || + (&stm->output_stream == pa_stm && + (stream_params->layout == CUBEB_LAYOUT_UNDEFINED || + (stream_params->layout != CUBEB_LAYOUT_UNDEFINED && + cubeb_channel_layout_nb_channels(stream_params->layout) == + stream_params->channels)))); + if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + return CUBEB_ERROR_NOT_SUPPORTED; + } + *pa_stm = NULL; + pa_sample_spec ss; + ss.format = to_pulse_format(stream_params->format); + if (ss.format == PA_SAMPLE_INVALID) + return CUBEB_ERROR_INVALID_FORMAT; + ss.rate = stream_params->rate; + if (stream_params->channels > UINT8_MAX) + return CUBEB_ERROR_INVALID_FORMAT; + ss.channels = (uint8_t)stream_params->channels; + + if (stream_params->layout == CUBEB_LAYOUT_UNDEFINED) { + pa_channel_map cm; + if (stream_params->channels <= 8 && + !WRAP(pa_channel_map_init_auto)(&cm, stream_params->channels, + PA_CHANNEL_MAP_DEFAULT)) { + LOG("Layout undefined and PulseAudio's default layout has not been " + "configured, guess one."); + layout_to_channel_map( + pulse_default_layout_for_channels(stream_params->channels), &cm); + *pa_stm = + WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, &cm); + } else { + LOG("Layout undefined, PulseAudio will use its default."); + *pa_stm = + WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, NULL); + } + } else { + pa_channel_map cm; + layout_to_channel_map(stream_params->layout, &cm); + *pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, &cm); + } + return (*pa_stm == NULL) ? CUBEB_ERROR : CUBEB_OK; +} + +static pa_buffer_attr +set_buffering_attribute(unsigned int latency_frames, + pa_sample_spec * sample_spec) +{ + pa_buffer_attr battr; + battr.maxlength = -1; + battr.prebuf = -1; + battr.tlength = latency_frames * WRAP(pa_frame_size)(sample_spec); + battr.minreq = battr.tlength / 4; + battr.fragsize = battr.minreq; + + LOG("Requested buffer attributes maxlength %u, tlength %u, prebuf %u, minreq " + "%u, fragsize %u", + battr.maxlength, battr.tlength, battr.prebuf, battr.minreq, + battr.fragsize); + + return battr; +} + +static int +pulse_stream_init(cubeb * context, cubeb_stream ** stream, + char const * stream_name, cubeb_devid input_device, + cubeb_stream_params * input_stream_params, + cubeb_devid output_device, + cubeb_stream_params * output_stream_params, + unsigned int latency_frames, + cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void * user_ptr) +{ + cubeb_stream * stm; + pa_buffer_attr battr; + int r; + + assert(context); + + // If the connection failed for some reason, try to reconnect + if (context->error == 1 && pulse_context_init(context) != 0) { + return CUBEB_ERROR; + } + + *stream = NULL; + + stm = calloc(1, sizeof(*stm)); + assert(stm); + + stm->context = context; + stm->data_callback = data_callback; + stm->state_callback = state_callback; + stm->user_ptr = user_ptr; + stm->volume = PULSE_NO_GAIN; + stm->state = -1; + assert(stm->shutdown == 0); + + WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); + if (output_stream_params) { + r = create_pa_stream(stm, &stm->output_stream, output_stream_params, + stream_name); + if (r != CUBEB_OK) { + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + pulse_stream_destroy(stm); + return r; + } + + stm->output_sample_spec = + *(WRAP(pa_stream_get_sample_spec)(stm->output_stream)); + + WRAP(pa_stream_set_state_callback) + (stm->output_stream, stream_state_callback, stm); + WRAP(pa_stream_set_write_callback) + (stm->output_stream, stream_write_callback, stm); + + battr = set_buffering_attribute(latency_frames, &stm->output_sample_spec); + WRAP(pa_stream_connect_playback) + (stm->output_stream, (char const *)output_device, &battr, + PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING | + PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY, + NULL, NULL); + } + + // Set up input stream + if (input_stream_params) { + r = create_pa_stream(stm, &stm->input_stream, input_stream_params, + stream_name); + if (r != CUBEB_OK) { + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + pulse_stream_destroy(stm); + return r; + } + + stm->input_sample_spec = + *(WRAP(pa_stream_get_sample_spec)(stm->input_stream)); + + WRAP(pa_stream_set_state_callback) + (stm->input_stream, stream_state_callback, stm); + WRAP(pa_stream_set_read_callback) + (stm->input_stream, stream_read_callback, stm); + + battr = set_buffering_attribute(latency_frames, &stm->input_sample_spec); + WRAP(pa_stream_connect_record) + (stm->input_stream, (char const *)input_device, &battr, + PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING | + PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY); + } + + r = wait_until_stream_ready(stm); + if (r == 0) { + /* force a timing update now, otherwise timing info does not become valid + until some point after initialization has completed. */ + r = stream_update_timing_info(stm); + } + + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + + if (r != 0) { + pulse_stream_destroy(stm); + return CUBEB_ERROR; + } + + if (cubeb_log_get_level()) { + if (output_stream_params) { + const pa_buffer_attr * output_att; + output_att = WRAP(pa_stream_get_buffer_attr)(stm->output_stream); + LOG("Output buffer attributes maxlength %u, tlength %u, prebuf %u, " + "minreq %u, fragsize %u", + output_att->maxlength, output_att->tlength, output_att->prebuf, + output_att->minreq, output_att->fragsize); + } + + if (input_stream_params) { + const pa_buffer_attr * input_att; + input_att = WRAP(pa_stream_get_buffer_attr)(stm->input_stream); + LOG("Input buffer attributes maxlength %u, tlength %u, prebuf %u, minreq " + "%u, fragsize %u", + input_att->maxlength, input_att->tlength, input_att->prebuf, + input_att->minreq, input_att->fragsize); + } + } + + *stream = stm; + LOG("Cubeb stream (%p) init successful.", *stream); + + return CUBEB_OK; +} + +static void +pulse_stream_destroy(cubeb_stream * stm) +{ + stream_cork(stm, CORK); + + WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); + if (stm->output_stream) { + + if (stm->drain_timer) { + /* there's no pa_rttime_free, so use this instead. */ + WRAP(pa_threaded_mainloop_get_api) + (stm->context->mainloop)->time_free(stm->drain_timer); + } + + WRAP(pa_stream_set_state_callback)(stm->output_stream, NULL, NULL); + WRAP(pa_stream_set_write_callback)(stm->output_stream, NULL, NULL); + WRAP(pa_stream_disconnect)(stm->output_stream); + WRAP(pa_stream_unref)(stm->output_stream); + } + + if (stm->input_stream) { + WRAP(pa_stream_set_state_callback)(stm->input_stream, NULL, NULL); + WRAP(pa_stream_set_read_callback)(stm->input_stream, NULL, NULL); + WRAP(pa_stream_disconnect)(stm->input_stream); + WRAP(pa_stream_unref)(stm->input_stream); + } + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + + LOG("Cubeb stream (%p) destroyed successfully.", stm); + free(stm); +} + +static void +pulse_defer_event_cb(pa_mainloop_api * a, void * userdata) +{ + (void)a; + cubeb_stream * stm = userdata; + if (stm->shutdown) { + return; + } + size_t writable_size = WRAP(pa_stream_writable_size)(stm->output_stream); + trigger_user_callback(stm->output_stream, NULL, writable_size, stm); +} + +static int +pulse_stream_start(cubeb_stream * stm) +{ + stm->shutdown = 0; + stream_cork(stm, UNCORK | NOTIFY); + + if (stm->output_stream && !stm->input_stream) { + /* On output only case need to manually call user cb once in order to make + * things roll. This is done via a defer event in order to execute it + * from PA server thread. */ + WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); + WRAP(pa_mainloop_api_once) + (WRAP(pa_threaded_mainloop_get_api)(stm->context->mainloop), + pulse_defer_event_cb, stm); + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + } + + LOG("Cubeb stream (%p) started successfully.", stm); + return CUBEB_OK; +} + +static int +pulse_stream_stop(cubeb_stream * stm) +{ + WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); + stm->shutdown = 1; + // If draining is taking place wait to finish + while (stm->drain_timer) { + WRAP(pa_threaded_mainloop_wait)(stm->context->mainloop); + } + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + + stream_cork(stm, CORK | NOTIFY); + LOG("Cubeb stream (%p) stopped successfully.", stm); + return CUBEB_OK; +} + +static int +pulse_stream_get_position(cubeb_stream * stm, uint64_t * position) +{ + int r, in_thread; + pa_usec_t r_usec; + uint64_t bytes; + + if (!stm || !stm->output_stream) { + return CUBEB_ERROR; + } + + in_thread = WRAP(pa_threaded_mainloop_in_thread)(stm->context->mainloop); + + if (!in_thread) { + WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); + } + r = WRAP(pa_stream_get_time)(stm->output_stream, &r_usec); + if (!in_thread) { + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + } + + if (r != 0) { + return CUBEB_ERROR; + } + + bytes = WRAP(pa_usec_to_bytes)(r_usec, &stm->output_sample_spec); + *position = bytes / WRAP(pa_frame_size)(&stm->output_sample_spec); + + return CUBEB_OK; +} + +static int +pulse_stream_get_latency(cubeb_stream * stm, uint32_t * latency) +{ + pa_usec_t r_usec; + int negative, r; + + if (!stm || !stm->output_stream) { + return CUBEB_ERROR; + } + + r = WRAP(pa_stream_get_latency)(stm->output_stream, &r_usec, &negative); + assert(!negative); + if (r) { + return CUBEB_ERROR; + } + + *latency = r_usec * stm->output_sample_spec.rate / PA_USEC_PER_SEC; + return CUBEB_OK; +} + +static void +volume_success(pa_context * c, int success, void * userdata) +{ + (void)success; + (void)c; + cubeb_stream * stream = userdata; + assert(success); + WRAP(pa_threaded_mainloop_signal)(stream->context->mainloop, 0); +} + +static void +rename_success(pa_stream * s, int success, void * userdata) +{ + cubeb_stream * stream = userdata; + assert(success); + WRAP(pa_threaded_mainloop_signal)(stream->context->mainloop, 0); +} + +static int +pulse_stream_set_volume(cubeb_stream * stm, float volume) +{ + uint32_t index; + pa_operation * op; + pa_volume_t vol; + pa_cvolume cvol; + const pa_sample_spec * ss; + cubeb * ctx; + + if (!stm->output_stream) { + return CUBEB_ERROR; + } + + WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); + + /* if the pulse daemon is configured to use flat volumes, + * apply our own gain instead of changing the input volume on the sink. */ + ctx = stm->context; + if (ctx->default_sink_info && + (ctx->default_sink_info->flags & PA_SINK_FLAT_VOLUME)) { + stm->volume = volume; + } else { + ss = WRAP(pa_stream_get_sample_spec)(stm->output_stream); + + vol = WRAP(pa_sw_volume_from_linear)(volume); + WRAP(pa_cvolume_set)(&cvol, ss->channels, vol); + + index = WRAP(pa_stream_get_index)(stm->output_stream); + + op = WRAP(pa_context_set_sink_input_volume)(ctx->context, index, &cvol, + volume_success, stm); + if (op) { + operation_wait(ctx, stm->output_stream, op); + WRAP(pa_operation_unref)(op); + } + } + + WRAP(pa_threaded_mainloop_unlock)(ctx->mainloop); + + return CUBEB_OK; +} + +static int +pulse_stream_set_name(cubeb_stream * stm, char const * stream_name) +{ + if (!stm || !stm->output_stream) { + return CUBEB_ERROR; + } + + WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop); + + pa_operation * op = WRAP(pa_stream_set_name)(stm->output_stream, stream_name, + rename_success, stm); + + if (op) { + operation_wait(stm->context, stm->output_stream, op); + WRAP(pa_operation_unref)(op); + } + + WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop); + + return CUBEB_OK; +} + +typedef struct { + char * default_sink_name; + char * default_source_name; + + cubeb_device_info * devinfo; + uint32_t max; + uint32_t count; + cubeb * context; +} pulse_dev_list_data; + +static cubeb_device_fmt +pulse_format_to_cubeb_format(pa_sample_format_t format) +{ + switch (format) { + case PA_SAMPLE_S16LE: + return CUBEB_DEVICE_FMT_S16LE; + case PA_SAMPLE_S16BE: + return CUBEB_DEVICE_FMT_S16BE; + case PA_SAMPLE_FLOAT32LE: + return CUBEB_DEVICE_FMT_F32LE; + case PA_SAMPLE_FLOAT32BE: + return CUBEB_DEVICE_FMT_F32BE; + default: + return CUBEB_DEVICE_FMT_F32NE; + } +} + +static void +pulse_ensure_dev_list_data_list_size(pulse_dev_list_data * list_data) +{ + if (list_data->count == list_data->max) { + list_data->max += 8; + list_data->devinfo = + realloc(list_data->devinfo, sizeof(cubeb_device_info) * list_data->max); + } +} + +static cubeb_device_state +pulse_get_state_from_sink_port(pa_sink_port_info * info) +{ + if (info != NULL) { +#if PA_CHECK_VERSION(2, 0, 0) + if (has_pulse_v2 && info->available == PA_PORT_AVAILABLE_NO) + return CUBEB_DEVICE_STATE_UNPLUGGED; + else /*if (info->available == PA_PORT_AVAILABLE_YES) + UNKNOWN */ +#endif + return CUBEB_DEVICE_STATE_ENABLED; + } + + return CUBEB_DEVICE_STATE_ENABLED; +} + +static void +pulse_sink_info_cb(pa_context * context, const pa_sink_info * info, int eol, + void * user_data) +{ + pulse_dev_list_data * list_data = user_data; + cubeb_device_info * devinfo; + char const * prop = NULL; + char const * device_id = NULL; + + (void)context; + + if (eol) { + WRAP(pa_threaded_mainloop_signal)(list_data->context->mainloop, 0); + return; + } + + if (info == NULL) + return; + + device_id = info->name; + if (intern_device_id(list_data->context, &device_id) != CUBEB_OK) { + assert(NULL); + return; + } + + pulse_ensure_dev_list_data_list_size(list_data); + devinfo = &list_data->devinfo[list_data->count]; + memset(devinfo, 0, sizeof(cubeb_device_info)); + + devinfo->device_id = device_id; + devinfo->devid = (cubeb_devid)devinfo->device_id; + devinfo->friendly_name = strdup(info->description); + prop = WRAP(pa_proplist_gets)(info->proplist, "sysfs.path"); + if (prop) + devinfo->group_id = strdup(prop); + prop = WRAP(pa_proplist_gets)(info->proplist, "device.vendor.name"); + if (prop) + devinfo->vendor_name = strdup(prop); + + devinfo->type = CUBEB_DEVICE_TYPE_OUTPUT; + devinfo->state = pulse_get_state_from_sink_port(info->active_port); + devinfo->preferred = (strcmp(info->name, list_data->default_sink_name) == 0) + ? CUBEB_DEVICE_PREF_ALL + : CUBEB_DEVICE_PREF_NONE; + + devinfo->format = CUBEB_DEVICE_FMT_ALL; + devinfo->default_format = + pulse_format_to_cubeb_format(info->sample_spec.format); + devinfo->max_channels = info->channel_map.channels; + devinfo->min_rate = 1; + devinfo->max_rate = PA_RATE_MAX; + devinfo->default_rate = info->sample_spec.rate; + + devinfo->latency_lo = 0; + devinfo->latency_hi = 0; + + list_data->count += 1; +} + +static cubeb_device_state +pulse_get_state_from_source_port(pa_source_port_info * info) +{ + if (info != NULL) { +#if PA_CHECK_VERSION(2, 0, 0) + if (has_pulse_v2 && info->available == PA_PORT_AVAILABLE_NO) + return CUBEB_DEVICE_STATE_UNPLUGGED; + else /*if (info->available == PA_PORT_AVAILABLE_YES) + UNKNOWN */ +#endif + return CUBEB_DEVICE_STATE_ENABLED; + } + + return CUBEB_DEVICE_STATE_ENABLED; +} + +static void +pulse_source_info_cb(pa_context * context, const pa_source_info * info, int eol, + void * user_data) +{ + pulse_dev_list_data * list_data = user_data; + cubeb_device_info * devinfo; + char const * prop = NULL; + char const * device_id = NULL; + + (void)context; + + if (eol) { + WRAP(pa_threaded_mainloop_signal)(list_data->context->mainloop, 0); + return; + } + + device_id = info->name; + if (intern_device_id(list_data->context, &device_id) != CUBEB_OK) { + assert(NULL); + return; + } + + pulse_ensure_dev_list_data_list_size(list_data); + devinfo = &list_data->devinfo[list_data->count]; + memset(devinfo, 0, sizeof(cubeb_device_info)); + + devinfo->device_id = device_id; + devinfo->devid = (cubeb_devid)devinfo->device_id; + devinfo->friendly_name = strdup(info->description); + prop = WRAP(pa_proplist_gets)(info->proplist, "sysfs.path"); + if (prop) + devinfo->group_id = strdup(prop); + prop = WRAP(pa_proplist_gets)(info->proplist, "device.vendor.name"); + if (prop) + devinfo->vendor_name = strdup(prop); + + devinfo->type = CUBEB_DEVICE_TYPE_INPUT; + devinfo->state = pulse_get_state_from_source_port(info->active_port); + devinfo->preferred = (strcmp(info->name, list_data->default_source_name) == 0) + ? CUBEB_DEVICE_PREF_ALL + : CUBEB_DEVICE_PREF_NONE; + + devinfo->format = CUBEB_DEVICE_FMT_ALL; + devinfo->default_format = + pulse_format_to_cubeb_format(info->sample_spec.format); + devinfo->max_channels = info->channel_map.channels; + devinfo->min_rate = 1; + devinfo->max_rate = PA_RATE_MAX; + devinfo->default_rate = info->sample_spec.rate; + + devinfo->latency_lo = 0; + devinfo->latency_hi = 0; + + list_data->count += 1; +} + +static void +pulse_server_info_cb(pa_context * c, const pa_server_info * i, void * userdata) +{ + pulse_dev_list_data * list_data = userdata; + + (void)c; + + free(list_data->default_sink_name); + free(list_data->default_source_name); + list_data->default_sink_name = + i->default_sink_name ? strdup(i->default_sink_name) : NULL; + list_data->default_source_name = + i->default_source_name ? strdup(i->default_source_name) : NULL; + + WRAP(pa_threaded_mainloop_signal)(list_data->context->mainloop, 0); +} + +static int +pulse_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * collection) +{ + pulse_dev_list_data user_data = {NULL, NULL, NULL, 0, 0, context}; + pa_operation * o; + + WRAP(pa_threaded_mainloop_lock)(context->mainloop); + + o = WRAP(pa_context_get_server_info)(context->context, pulse_server_info_cb, + &user_data); + if (o) { + operation_wait(context, NULL, o); + WRAP(pa_operation_unref)(o); + } + + if (type & CUBEB_DEVICE_TYPE_OUTPUT) { + o = WRAP(pa_context_get_sink_info_list)(context->context, + pulse_sink_info_cb, &user_data); + if (o) { + operation_wait(context, NULL, o); + WRAP(pa_operation_unref)(o); + } + } + + if (type & CUBEB_DEVICE_TYPE_INPUT) { + o = WRAP(pa_context_get_source_info_list)(context->context, + pulse_source_info_cb, &user_data); + if (o) { + operation_wait(context, NULL, o); + WRAP(pa_operation_unref)(o); + } + } + + WRAP(pa_threaded_mainloop_unlock)(context->mainloop); + + collection->device = user_data.devinfo; + collection->count = user_data.count; + + free(user_data.default_sink_name); + free(user_data.default_source_name); + return CUBEB_OK; +} + +static int +pulse_device_collection_destroy(cubeb * ctx, + cubeb_device_collection * collection) +{ + size_t n; + + for (n = 0; n < collection->count; n++) { + free((void *)collection->device[n].friendly_name); + free((void *)collection->device[n].vendor_name); + free((void *)collection->device[n].group_id); + } + + free(collection->device); + return CUBEB_OK; +} + +static int +pulse_stream_get_current_device(cubeb_stream * stm, + cubeb_device ** const device) +{ +#if PA_CHECK_VERSION(0, 9, 8) + *device = calloc(1, sizeof(cubeb_device)); + if (*device == NULL) + return CUBEB_ERROR; + + if (stm->input_stream) { + const char * name = WRAP(pa_stream_get_device_name)(stm->input_stream); + (*device)->input_name = (name == NULL) ? NULL : strdup(name); + } + + if (stm->output_stream) { + const char * name = WRAP(pa_stream_get_device_name)(stm->output_stream); + (*device)->output_name = (name == NULL) ? NULL : strdup(name); + } + + return CUBEB_OK; +#else + return CUBEB_ERROR_NOT_SUPPORTED; +#endif +} + +static int +pulse_stream_device_destroy(cubeb_stream * stream, cubeb_device * device) +{ + (void)stream; + free(device->input_name); + free(device->output_name); + free(device); + return CUBEB_OK; +} + +static void +pulse_subscribe_callback(pa_context * ctx, pa_subscription_event_type_t t, + uint32_t index, void * userdata) +{ + (void)ctx; + cubeb * context = userdata; + + switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { + case PA_SUBSCRIPTION_EVENT_SERVER: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE) { + LOG("Server changed %d", index); + WRAP(pa_context_get_server_info) + (context->context, server_info_callback, context); + } + break; + case PA_SUBSCRIPTION_EVENT_SOURCE: + case PA_SUBSCRIPTION_EVENT_SINK: + + if (cubeb_log_get_level()) { + if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == + PA_SUBSCRIPTION_EVENT_SOURCE && + (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == + PA_SUBSCRIPTION_EVENT_REMOVE) { + LOG("Removing source index %d", index); + } else if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == + PA_SUBSCRIPTION_EVENT_SOURCE && + (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == + PA_SUBSCRIPTION_EVENT_NEW) { + LOG("Adding source index %d", index); + } + if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == + PA_SUBSCRIPTION_EVENT_SINK && + (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == + PA_SUBSCRIPTION_EVENT_REMOVE) { + LOG("Removing sink index %d", index); + } else if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == + PA_SUBSCRIPTION_EVENT_SINK && + (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == + PA_SUBSCRIPTION_EVENT_NEW) { + LOG("Adding sink index %d", index); + } + } + + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE || + (t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) { + if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == + PA_SUBSCRIPTION_EVENT_SOURCE) { + context->input_collection_changed_callback( + context, context->input_collection_changed_user_ptr); + } + if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == + PA_SUBSCRIPTION_EVENT_SINK) { + context->output_collection_changed_callback( + context, context->output_collection_changed_user_ptr); + } + } + break; + } +} + +static void +subscribe_success(pa_context * c, int success, void * userdata) +{ + (void)c; + cubeb * context = userdata; + assert(success); + WRAP(pa_threaded_mainloop_signal)(context->mainloop, 0); +} + +static int +pulse_subscribe_notifications(cubeb * context, pa_subscription_mask_t mask) +{ + WRAP(pa_threaded_mainloop_lock)(context->mainloop); + + WRAP(pa_context_set_subscribe_callback) + (context->context, pulse_subscribe_callback, context); + + pa_operation * o; + o = WRAP(pa_context_subscribe)(context->context, mask, subscribe_success, + context); + if (o == NULL) { + WRAP(pa_threaded_mainloop_unlock)(context->mainloop); + LOG("Context subscribe failed"); + return CUBEB_ERROR; + } + operation_wait(context, NULL, o); + WRAP(pa_operation_unref)(o); + + WRAP(pa_threaded_mainloop_unlock)(context->mainloop); + + return CUBEB_OK; +} + +static int +pulse_register_device_collection_changed( + cubeb * context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback collection_changed_callback, + void * user_ptr) +{ + if (devtype & CUBEB_DEVICE_TYPE_INPUT) { + context->input_collection_changed_callback = collection_changed_callback; + context->input_collection_changed_user_ptr = user_ptr; + } + if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) { + context->output_collection_changed_callback = collection_changed_callback; + context->output_collection_changed_user_ptr = user_ptr; + } + + pa_subscription_mask_t mask = PA_SUBSCRIPTION_MASK_NULL; + if (context->input_collection_changed_callback) { + /* Input added or removed */ + mask |= PA_SUBSCRIPTION_MASK_SOURCE; + } + if (context->output_collection_changed_callback) { + /* Output added or removed */ + mask |= PA_SUBSCRIPTION_MASK_SINK; + } + /* Default device changed, this is always registered in order to update the + * `default_sink_info` when the default device changes. */ + mask |= PA_SUBSCRIPTION_MASK_SERVER; + + return pulse_subscribe_notifications(context, mask); +} + +static struct cubeb_ops const pulse_ops = { + .init = pulse_init, + .get_backend_id = pulse_get_backend_id, + .get_max_channel_count = pulse_get_max_channel_count, + .get_min_latency = pulse_get_min_latency, + .get_preferred_sample_rate = pulse_get_preferred_sample_rate, + .enumerate_devices = pulse_enumerate_devices, + .device_collection_destroy = pulse_device_collection_destroy, + .destroy = pulse_destroy, + .stream_init = pulse_stream_init, + .stream_destroy = pulse_stream_destroy, + .stream_start = pulse_stream_start, + .stream_stop = pulse_stream_stop, + .stream_get_position = pulse_stream_get_position, + .stream_get_latency = pulse_stream_get_latency, + .stream_get_input_latency = NULL, + .stream_set_volume = pulse_stream_set_volume, + .stream_set_name = pulse_stream_set_name, + .stream_get_current_device = pulse_stream_get_current_device, + .stream_device_destroy = pulse_stream_device_destroy, + .stream_register_device_changed_callback = NULL, + .register_device_collection_changed = + pulse_register_device_collection_changed}; |