/* * Copyright © 2011 Mozilla Foundation * * This program is made available under an ISC-style license. See the * accompanying file LICENSE for details. */ #undef NDEBUG #include #include #include #include #include #include #include "cubeb-internal.h" #include "cubeb/cubeb.h" #include "cubeb_mixer.h" #include "cubeb_strings.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) \ #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; 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; 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) { 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); } if (ctx->libpulse) { dlclose(ctx->libpulse); } 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 (g_cubeb_log_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 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; } 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 (g_cubeb_log_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_reset_default_device = NULL, .stream_get_position = pulse_stream_get_position, .stream_get_latency = pulse_stream_get_latency, .stream_set_volume = pulse_stream_set_volume, .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 };