summaryrefslogtreecommitdiffstats
path: root/media/libcubeb/src/cubeb_alsa.c
diff options
context:
space:
mode:
Diffstat (limited to 'media/libcubeb/src/cubeb_alsa.c')
-rw-r--r--media/libcubeb/src/cubeb_alsa.c1149
1 files changed, 1149 insertions, 0 deletions
diff --git a/media/libcubeb/src/cubeb_alsa.c b/media/libcubeb/src/cubeb_alsa.c
new file mode 100644
index 000000000..1ea0961d0
--- /dev/null
+++ b/media/libcubeb/src/cubeb_alsa.c
@@ -0,0 +1,1149 @@
+/*
+ * Copyright © 2011 Mozilla Foundation
+ *
+ * This program is made available under an ISC-style license. See the
+ * accompanying file LICENSE for details.
+ */
+#undef NDEBUG
+#define _DEFAULT_SOURCE
+#define _BSD_SOURCE
+#define _XOPEN_SOURCE 500
+#include <pthread.h>
+#include <sys/time.h>
+#include <assert.h>
+#include <limits.h>
+#include <poll.h>
+#include <unistd.h>
+#include <alsa/asoundlib.h>
+#include "cubeb/cubeb.h"
+#include "cubeb-internal.h"
+
+#define CUBEB_STREAM_MAX 16
+#define CUBEB_WATCHDOG_MS 10000
+
+#define CUBEB_ALSA_PCM_NAME "default"
+
+#define ALSA_PA_PLUGIN "ALSA <-> PulseAudio PCM I/O Plugin"
+
+/* ALSA is not thread-safe. snd_pcm_t instances are individually protected
+ by the owning cubeb_stream's mutex. snd_pcm_t creation and destruction
+ is not thread-safe until ALSA 1.0.24 (see alsa-lib.git commit 91c9c8f1),
+ so those calls must be wrapped in the following mutex. */
+static pthread_mutex_t cubeb_alsa_mutex = PTHREAD_MUTEX_INITIALIZER;
+static int cubeb_alsa_error_handler_set = 0;
+
+static struct cubeb_ops const alsa_ops;
+
+struct cubeb {
+ struct cubeb_ops const * ops;
+
+ pthread_t thread;
+
+ /* Mutex for streams array, must not be held while blocked in poll(2). */
+ pthread_mutex_t mutex;
+
+ /* Sparse array of streams managed by this context. */
+ cubeb_stream * streams[CUBEB_STREAM_MAX];
+
+ /* fds and nfds are only updated by alsa_run when rebuild is set. */
+ struct pollfd * fds;
+ nfds_t nfds;
+ int rebuild;
+
+ int shutdown;
+
+ /* Control pipe for forcing poll to wake and rebuild fds or recalculate the timeout. */
+ int control_fd_read;
+ int control_fd_write;
+
+ /* Track number of active streams. This is limited to CUBEB_STREAM_MAX
+ due to resource contraints. */
+ unsigned int active_streams;
+
+ /* Local configuration with handle_underrun workaround set for PulseAudio
+ ALSA plugin. Will be NULL if the PA ALSA plugin is not in use or the
+ workaround is not required. */
+ snd_config_t * local_config;
+ int is_pa;
+};
+
+enum stream_state {
+ INACTIVE,
+ RUNNING,
+ DRAINING,
+ PROCESSING,
+ ERROR
+};
+
+struct cubeb_stream {
+ cubeb * context;
+ pthread_mutex_t mutex;
+ snd_pcm_t * pcm;
+ cubeb_data_callback data_callback;
+ cubeb_state_callback state_callback;
+ void * user_ptr;
+ snd_pcm_uframes_t write_position;
+ snd_pcm_uframes_t last_position;
+ snd_pcm_uframes_t buffer_size;
+ cubeb_stream_params params;
+
+ /* Every member after this comment is protected by the owning context's
+ mutex rather than the stream's mutex, or is only used on the context's
+ run thread. */
+ pthread_cond_t cond; /* Signaled when the stream's state is changed. */
+
+ enum stream_state state;
+
+ struct pollfd * saved_fds; /* A copy of the pollfds passed in at init time. */
+ struct pollfd * fds; /* Pointer to this waitable's pollfds within struct cubeb's fds. */
+ nfds_t nfds;
+
+ struct timeval drain_timeout;
+
+ /* XXX: Horrible hack -- if an active stream has been idle for
+ CUBEB_WATCHDOG_MS it will be disabled and the error callback will be
+ called. This works around a bug seen with older versions of ALSA and
+ PulseAudio where streams would stop requesting new data despite still
+ being logically active and playing. */
+ struct timeval last_activity;
+ float volume;
+};
+
+static int
+any_revents(struct pollfd * fds, nfds_t nfds)
+{
+ nfds_t i;
+
+ for (i = 0; i < nfds; ++i) {
+ if (fds[i].revents) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int
+cmp_timeval(struct timeval * a, struct timeval * b)
+{
+ if (a->tv_sec == b->tv_sec) {
+ if (a->tv_usec == b->tv_usec) {
+ return 0;
+ }
+ return a->tv_usec > b->tv_usec ? 1 : -1;
+ }
+ return a->tv_sec > b->tv_sec ? 1 : -1;
+}
+
+static int
+timeval_to_relative_ms(struct timeval * tv)
+{
+ struct timeval now;
+ struct timeval dt;
+ long long t;
+ int r;
+
+ gettimeofday(&now, NULL);
+ r = cmp_timeval(tv, &now);
+ if (r >= 0) {
+ timersub(tv, &now, &dt);
+ } else {
+ timersub(&now, tv, &dt);
+ }
+ t = dt.tv_sec;
+ t *= 1000;
+ t += (dt.tv_usec + 500) / 1000;
+
+ if (t > INT_MAX) {
+ t = INT_MAX;
+ } else if (t < INT_MIN) {
+ t = INT_MIN;
+ }
+
+ return r >= 0 ? t : -t;
+}
+
+static int
+ms_until(struct timeval * tv)
+{
+ return timeval_to_relative_ms(tv);
+}
+
+static int
+ms_since(struct timeval * tv)
+{
+ return -timeval_to_relative_ms(tv);
+}
+
+static void
+rebuild(cubeb * ctx)
+{
+ nfds_t nfds;
+ int i;
+ nfds_t j;
+ cubeb_stream * stm;
+
+ assert(ctx->rebuild);
+
+ /* Always count context's control pipe fd. */
+ nfds = 1;
+ for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
+ stm = ctx->streams[i];
+ if (stm) {
+ stm->fds = NULL;
+ if (stm->state == RUNNING) {
+ nfds += stm->nfds;
+ }
+ }
+ }
+
+ free(ctx->fds);
+ ctx->fds = calloc(nfds, sizeof(struct pollfd));
+ assert(ctx->fds);
+ ctx->nfds = nfds;
+
+ /* Include context's control pipe fd. */
+ ctx->fds[0].fd = ctx->control_fd_read;
+ ctx->fds[0].events = POLLIN | POLLERR;
+
+ for (i = 0, j = 1; i < CUBEB_STREAM_MAX; ++i) {
+ stm = ctx->streams[i];
+ if (stm && stm->state == RUNNING) {
+ memcpy(&ctx->fds[j], stm->saved_fds, stm->nfds * sizeof(struct pollfd));
+ stm->fds = &ctx->fds[j];
+ j += stm->nfds;
+ }
+ }
+
+ ctx->rebuild = 0;
+}
+
+static void
+poll_wake(cubeb * ctx)
+{
+ if (write(ctx->control_fd_write, "x", 1) < 0) {
+ /* ignore write error */
+ }
+}
+
+static void
+set_timeout(struct timeval * timeout, unsigned int ms)
+{
+ gettimeofday(timeout, NULL);
+ timeout->tv_sec += ms / 1000;
+ timeout->tv_usec += (ms % 1000) * 1000;
+}
+
+static void
+alsa_set_stream_state(cubeb_stream * stm, enum stream_state state)
+{
+ cubeb * ctx;
+ int r;
+
+ ctx = stm->context;
+ stm->state = state;
+ r = pthread_cond_broadcast(&stm->cond);
+ assert(r == 0);
+ ctx->rebuild = 1;
+ poll_wake(ctx);
+}
+
+static enum stream_state
+alsa_refill_stream(cubeb_stream * stm)
+{
+ snd_pcm_sframes_t avail;
+ long got;
+ void * p;
+ int draining;
+
+ draining = 0;
+
+ pthread_mutex_lock(&stm->mutex);
+
+ avail = snd_pcm_avail_update(stm->pcm);
+ if (avail < 0) {
+ snd_pcm_recover(stm->pcm, avail, 1);
+ avail = snd_pcm_avail_update(stm->pcm);
+ }
+
+ /* Failed to recover from an xrun, this stream must be broken. */
+ if (avail < 0) {
+ pthread_mutex_unlock(&stm->mutex);
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+ return ERROR;
+ }
+
+ /* This should never happen. */
+ if ((unsigned int) avail > stm->buffer_size) {
+ avail = stm->buffer_size;
+ }
+
+ /* poll(2) claims this stream is active, so there should be some space
+ available to write. If avail is still zero here, the stream must be in
+ a funky state, bail and wait for another wakeup. */
+ if (avail == 0) {
+ pthread_mutex_unlock(&stm->mutex);
+ return RUNNING;
+ }
+
+ p = calloc(1, snd_pcm_frames_to_bytes(stm->pcm, avail));
+ assert(p);
+
+ pthread_mutex_unlock(&stm->mutex);
+ got = stm->data_callback(stm, stm->user_ptr, NULL, p, avail);
+ pthread_mutex_lock(&stm->mutex);
+ if (got < 0) {
+ pthread_mutex_unlock(&stm->mutex);
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+ free(p);
+ return ERROR;
+ }
+ if (got > 0) {
+ snd_pcm_sframes_t wrote;
+
+ if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) {
+ float * b = (float *) p;
+ for (uint32_t i = 0; i < got * stm->params.channels; i++) {
+ b[i] *= stm->volume;
+ }
+ } else {
+ short * b = (short *) p;
+ for (uint32_t i = 0; i < got * stm->params.channels; i++) {
+ b[i] *= stm->volume;
+ }
+ }
+ wrote = snd_pcm_writei(stm->pcm, p, got);
+ if (wrote < 0) {
+ snd_pcm_recover(stm->pcm, wrote, 1);
+ wrote = snd_pcm_writei(stm->pcm, p, got);
+ }
+ assert(wrote >= 0 && wrote == got);
+ stm->write_position += wrote;
+ gettimeofday(&stm->last_activity, NULL);
+ }
+ if (got != avail) {
+ long buffer_fill = stm->buffer_size - (avail - got);
+ double buffer_time = (double) buffer_fill / stm->params.rate;
+
+ /* Fill the remaining buffer with silence to guarantee one full period
+ has been written. */
+ snd_pcm_writei(stm->pcm, (char *) p + got, avail - got);
+
+ set_timeout(&stm->drain_timeout, buffer_time * 1000);
+
+ draining = 1;
+ }
+
+ free(p);
+ pthread_mutex_unlock(&stm->mutex);
+ return draining ? DRAINING : RUNNING;
+}
+
+static int
+alsa_run(cubeb * ctx)
+{
+ int r;
+ int timeout;
+ int i;
+ char dummy;
+ cubeb_stream * stm;
+ enum stream_state state;
+
+ pthread_mutex_lock(&ctx->mutex);
+
+ if (ctx->rebuild) {
+ rebuild(ctx);
+ }
+
+ /* Wake up at least once per second for the watchdog. */
+ timeout = 1000;
+ for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
+ stm = ctx->streams[i];
+ if (stm && stm->state == DRAINING) {
+ r = ms_until(&stm->drain_timeout);
+ if (r >= 0 && timeout > r) {
+ timeout = r;
+ }
+ }
+ }
+
+ pthread_mutex_unlock(&ctx->mutex);
+ r = poll(ctx->fds, ctx->nfds, timeout);
+ pthread_mutex_lock(&ctx->mutex);
+
+ if (r > 0) {
+ if (ctx->fds[0].revents & POLLIN) {
+ if (read(ctx->control_fd_read, &dummy, 1) < 0) {
+ /* ignore read error */
+ }
+
+ if (ctx->shutdown) {
+ pthread_mutex_unlock(&ctx->mutex);
+ return -1;
+ }
+ }
+
+ for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
+ stm = ctx->streams[i];
+ /* We can't use snd_pcm_poll_descriptors_revents here because of
+ https://github.com/kinetiknz/cubeb/issues/135. */
+ if (stm && stm->state == RUNNING && stm->fds && any_revents(stm->fds, stm->nfds)) {
+ alsa_set_stream_state(stm, PROCESSING);
+ pthread_mutex_unlock(&ctx->mutex);
+ state = alsa_refill_stream(stm);
+ pthread_mutex_lock(&ctx->mutex);
+ alsa_set_stream_state(stm, state);
+ }
+ }
+ } else if (r == 0) {
+ for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
+ stm = ctx->streams[i];
+ if (stm) {
+ if (stm->state == DRAINING && ms_since(&stm->drain_timeout) >= 0) {
+ alsa_set_stream_state(stm, INACTIVE);
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
+ } else if (stm->state == RUNNING && ms_since(&stm->last_activity) > CUBEB_WATCHDOG_MS) {
+ alsa_set_stream_state(stm, ERROR);
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+ }
+ }
+ }
+ }
+
+ pthread_mutex_unlock(&ctx->mutex);
+
+ return 0;
+}
+
+static void *
+alsa_run_thread(void * context)
+{
+ cubeb * ctx = context;
+ int r;
+
+ do {
+ r = alsa_run(ctx);
+ } while (r >= 0);
+
+ return NULL;
+}
+
+static snd_config_t *
+get_slave_pcm_node(snd_config_t * lconf, snd_config_t * root_pcm)
+{
+ int r;
+ snd_config_t * slave_pcm;
+ snd_config_t * slave_def;
+ snd_config_t * pcm;
+ char const * string;
+ char node_name[64];
+
+ slave_def = NULL;
+
+ r = snd_config_search(root_pcm, "slave", &slave_pcm);
+ if (r < 0) {
+ return NULL;
+ }
+
+ r = snd_config_get_string(slave_pcm, &string);
+ if (r >= 0) {
+ r = snd_config_search_definition(lconf, "pcm_slave", string, &slave_def);
+ if (r < 0) {
+ return NULL;
+ }
+ }
+
+ do {
+ r = snd_config_search(slave_def ? slave_def : slave_pcm, "pcm", &pcm);
+ if (r < 0) {
+ break;
+ }
+
+ r = snd_config_get_string(slave_def ? slave_def : slave_pcm, &string);
+ if (r < 0) {
+ break;
+ }
+
+ r = snprintf(node_name, sizeof(node_name), "pcm.%s", string);
+ if (r < 0 || r > (int) sizeof(node_name)) {
+ break;
+ }
+ r = snd_config_search(lconf, node_name, &pcm);
+ if (r < 0) {
+ break;
+ }
+
+ return pcm;
+ } while (0);
+
+ if (slave_def) {
+ snd_config_delete(slave_def);
+ }
+
+ return NULL;
+}
+
+/* Work around PulseAudio ALSA plugin bug where the PA server forces a
+ higher than requested latency, but the plugin does not update its (and
+ ALSA's) internal state to reflect that, leading to an immediate underrun
+ situation. Inspired by WINE's make_handle_underrun_config.
+ Reference: http://mailman.alsa-project.org/pipermail/alsa-devel/2012-July/05 */
+static snd_config_t *
+init_local_config_with_workaround(char const * pcm_name)
+{
+ int r;
+ snd_config_t * lconf;
+ snd_config_t * pcm_node;
+ snd_config_t * node;
+ char const * string;
+ char node_name[64];
+
+ lconf = NULL;
+
+ if (snd_config == NULL) {
+ return NULL;
+ }
+
+ r = snd_config_copy(&lconf, snd_config);
+ if (r < 0) {
+ return NULL;
+ }
+
+ do {
+ r = snd_config_search_definition(lconf, "pcm", pcm_name, &pcm_node);
+ if (r < 0) {
+ break;
+ }
+
+ r = snd_config_get_id(pcm_node, &string);
+ if (r < 0) {
+ break;
+ }
+
+ r = snprintf(node_name, sizeof(node_name), "pcm.%s", string);
+ if (r < 0 || r > (int) sizeof(node_name)) {
+ break;
+ }
+ r = snd_config_search(lconf, node_name, &pcm_node);
+ if (r < 0) {
+ break;
+ }
+
+ /* If this PCM has a slave, walk the slave configurations until we reach the bottom. */
+ while ((node = get_slave_pcm_node(lconf, pcm_node)) != NULL) {
+ pcm_node = node;
+ }
+
+ /* Fetch the PCM node's type, and bail out if it's not the PulseAudio plugin. */
+ r = snd_config_search(pcm_node, "type", &node);
+ if (r < 0) {
+ break;
+ }
+
+ r = snd_config_get_string(node, &string);
+ if (r < 0) {
+ break;
+ }
+
+ if (strcmp(string, "pulse") != 0) {
+ break;
+ }
+
+ /* Don't clobber an explicit existing handle_underrun value, set it only
+ if it doesn't already exist. */
+ r = snd_config_search(pcm_node, "handle_underrun", &node);
+ if (r != -ENOENT) {
+ break;
+ }
+
+ /* Disable pcm_pulse's asynchronous underrun handling. */
+ r = snd_config_imake_integer(&node, "handle_underrun", 0);
+ if (r < 0) {
+ break;
+ }
+
+ r = snd_config_add(pcm_node, node);
+ if (r < 0) {
+ break;
+ }
+
+ return lconf;
+ } while (0);
+
+ snd_config_delete(lconf);
+
+ return NULL;
+}
+
+static int
+alsa_locked_pcm_open(snd_pcm_t ** pcm, snd_pcm_stream_t stream, snd_config_t * local_config)
+{
+ int r;
+
+ pthread_mutex_lock(&cubeb_alsa_mutex);
+ if (local_config) {
+ r = snd_pcm_open_lconf(pcm, CUBEB_ALSA_PCM_NAME, stream, SND_PCM_NONBLOCK, local_config);
+ } else {
+ r = snd_pcm_open(pcm, CUBEB_ALSA_PCM_NAME, stream, SND_PCM_NONBLOCK);
+ }
+ pthread_mutex_unlock(&cubeb_alsa_mutex);
+
+ return r;
+}
+
+static int
+alsa_locked_pcm_close(snd_pcm_t * pcm)
+{
+ int r;
+
+ pthread_mutex_lock(&cubeb_alsa_mutex);
+ r = snd_pcm_close(pcm);
+ pthread_mutex_unlock(&cubeb_alsa_mutex);
+
+ return r;
+}
+
+static int
+alsa_register_stream(cubeb * ctx, cubeb_stream * stm)
+{
+ int i;
+
+ pthread_mutex_lock(&ctx->mutex);
+ for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
+ if (!ctx->streams[i]) {
+ ctx->streams[i] = stm;
+ break;
+ }
+ }
+ pthread_mutex_unlock(&ctx->mutex);
+
+ return i == CUBEB_STREAM_MAX;
+}
+
+static void
+alsa_unregister_stream(cubeb_stream * stm)
+{
+ cubeb * ctx;
+ int i;
+
+ ctx = stm->context;
+
+ pthread_mutex_lock(&ctx->mutex);
+ for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
+ if (ctx->streams[i] == stm) {
+ ctx->streams[i] = NULL;
+ break;
+ }
+ }
+ pthread_mutex_unlock(&ctx->mutex);
+}
+
+static void
+silent_error_handler(char const * file, int line, char const * function,
+ int err, char const * fmt, ...)
+{
+ (void)file;
+ (void)line;
+ (void)function;
+ (void)err;
+ (void)fmt;
+}
+
+/*static*/ int
+alsa_init(cubeb ** context, char const * context_name)
+{
+ (void)context_name;
+ cubeb * ctx;
+ int r;
+ int i;
+ int fd[2];
+ pthread_attr_t attr;
+ snd_pcm_t * dummy;
+
+ assert(context);
+ *context = NULL;
+
+ pthread_mutex_lock(&cubeb_alsa_mutex);
+ if (!cubeb_alsa_error_handler_set) {
+ snd_lib_error_set_handler(silent_error_handler);
+ cubeb_alsa_error_handler_set = 1;
+ }
+ pthread_mutex_unlock(&cubeb_alsa_mutex);
+
+ ctx = calloc(1, sizeof(*ctx));
+ assert(ctx);
+
+ ctx->ops = &alsa_ops;
+
+ r = pthread_mutex_init(&ctx->mutex, NULL);
+ assert(r == 0);
+
+ r = pipe(fd);
+ assert(r == 0);
+
+ for (i = 0; i < 2; ++i) {
+ fcntl(fd[i], F_SETFD, fcntl(fd[i], F_GETFD) | FD_CLOEXEC);
+ fcntl(fd[i], F_SETFL, fcntl(fd[i], F_GETFL) | O_NONBLOCK);
+ }
+
+ ctx->control_fd_read = fd[0];
+ ctx->control_fd_write = fd[1];
+
+ /* Force an early rebuild when alsa_run is first called to ensure fds and
+ nfds have been initialized. */
+ ctx->rebuild = 1;
+
+ r = pthread_attr_init(&attr);
+ assert(r == 0);
+
+ r = pthread_attr_setstacksize(&attr, 256 * 1024);
+ assert(r == 0);
+
+ r = pthread_create(&ctx->thread, &attr, alsa_run_thread, ctx);
+ assert(r == 0);
+
+ r = pthread_attr_destroy(&attr);
+ assert(r == 0);
+
+ /* Open a dummy PCM to force the configuration space to be evaluated so that
+ init_local_config_with_workaround can find and modify the default node. */
+ r = alsa_locked_pcm_open(&dummy, SND_PCM_STREAM_PLAYBACK, NULL);
+ if (r >= 0) {
+ alsa_locked_pcm_close(dummy);
+ }
+ ctx->is_pa = 0;
+ pthread_mutex_lock(&cubeb_alsa_mutex);
+ ctx->local_config = init_local_config_with_workaround(CUBEB_ALSA_PCM_NAME);
+ pthread_mutex_unlock(&cubeb_alsa_mutex);
+ if (ctx->local_config) {
+ ctx->is_pa = 1;
+ r = alsa_locked_pcm_open(&dummy, SND_PCM_STREAM_PLAYBACK, ctx->local_config);
+ /* If we got a local_config, we found a PA PCM. If opening a PCM with that
+ config fails with EINVAL, the PA PCM is too old for this workaround. */
+ if (r == -EINVAL) {
+ pthread_mutex_lock(&cubeb_alsa_mutex);
+ snd_config_delete(ctx->local_config);
+ pthread_mutex_unlock(&cubeb_alsa_mutex);
+ ctx->local_config = NULL;
+ } else if (r >= 0) {
+ alsa_locked_pcm_close(dummy);
+ }
+ }
+
+ *context = ctx;
+
+ return CUBEB_OK;
+}
+
+static char const *
+alsa_get_backend_id(cubeb * ctx)
+{
+ (void)ctx;
+ return "alsa";
+}
+
+static void
+alsa_destroy(cubeb * ctx)
+{
+ int r;
+
+ assert(ctx);
+
+ pthread_mutex_lock(&ctx->mutex);
+ ctx->shutdown = 1;
+ poll_wake(ctx);
+ pthread_mutex_unlock(&ctx->mutex);
+
+ r = pthread_join(ctx->thread, NULL);
+ assert(r == 0);
+
+ close(ctx->control_fd_read);
+ close(ctx->control_fd_write);
+ pthread_mutex_destroy(&ctx->mutex);
+ free(ctx->fds);
+
+ if (ctx->local_config) {
+ pthread_mutex_lock(&cubeb_alsa_mutex);
+ snd_config_delete(ctx->local_config);
+ pthread_mutex_unlock(&cubeb_alsa_mutex);
+ }
+
+ free(ctx);
+}
+
+static void alsa_stream_destroy(cubeb_stream * stm);
+
+static int
+alsa_stream_init(cubeb * ctx, 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)
+{
+ (void)stream_name;
+ cubeb_stream * stm;
+ int r;
+ snd_pcm_format_t format;
+ snd_pcm_uframes_t period_size;
+ int latency_us = 0;
+
+
+ assert(ctx && stream);
+
+ if (input_stream_params) {
+ /* Capture support not yet implemented. */
+ return CUBEB_ERROR_NOT_SUPPORTED;
+ }
+
+ if (input_device || output_device) {
+ /* Device selection not yet implemented. */
+ return CUBEB_ERROR_DEVICE_UNAVAILABLE;
+ }
+
+ *stream = NULL;
+
+ switch (output_stream_params->format) {
+ case CUBEB_SAMPLE_S16LE:
+ format = SND_PCM_FORMAT_S16_LE;
+ break;
+ case CUBEB_SAMPLE_S16BE:
+ format = SND_PCM_FORMAT_S16_BE;
+ break;
+ case CUBEB_SAMPLE_FLOAT32LE:
+ format = SND_PCM_FORMAT_FLOAT_LE;
+ break;
+ case CUBEB_SAMPLE_FLOAT32BE:
+ format = SND_PCM_FORMAT_FLOAT_BE;
+ break;
+ default:
+ return CUBEB_ERROR_INVALID_FORMAT;
+ }
+
+ pthread_mutex_lock(&ctx->mutex);
+ if (ctx->active_streams >= CUBEB_STREAM_MAX) {
+ pthread_mutex_unlock(&ctx->mutex);
+ return CUBEB_ERROR;
+ }
+ ctx->active_streams += 1;
+ pthread_mutex_unlock(&ctx->mutex);
+
+ stm = calloc(1, sizeof(*stm));
+ assert(stm);
+
+ stm->context = ctx;
+ stm->data_callback = data_callback;
+ stm->state_callback = state_callback;
+ stm->user_ptr = user_ptr;
+ stm->params = *output_stream_params;
+ stm->state = INACTIVE;
+ stm->volume = 1.0;
+
+ r = pthread_mutex_init(&stm->mutex, NULL);
+ assert(r == 0);
+
+ r = alsa_locked_pcm_open(&stm->pcm, SND_PCM_STREAM_PLAYBACK, ctx->local_config);
+ if (r < 0) {
+ alsa_stream_destroy(stm);
+ return CUBEB_ERROR;
+ }
+
+ r = snd_pcm_nonblock(stm->pcm, 1);
+ assert(r == 0);
+
+ latency_us = latency_frames * 1e6 / stm->params.rate;
+
+ /* Ugly hack: the PA ALSA plugin allows buffer configurations that can't
+ possibly work. See https://bugzilla.mozilla.org/show_bug.cgi?id=761274.
+ Only resort to this hack if the handle_underrun workaround failed. */
+ if (!ctx->local_config && ctx->is_pa) {
+ const int min_latency = 5e5;
+ latency_us = latency_us < min_latency ? min_latency: latency_us;
+ }
+
+ r = snd_pcm_set_params(stm->pcm, format, SND_PCM_ACCESS_RW_INTERLEAVED,
+ stm->params.channels, stm->params.rate, 1,
+ latency_us);
+ if (r < 0) {
+ alsa_stream_destroy(stm);
+ return CUBEB_ERROR_INVALID_FORMAT;
+ }
+
+ r = snd_pcm_get_params(stm->pcm, &stm->buffer_size, &period_size);
+ assert(r == 0);
+
+ stm->nfds = snd_pcm_poll_descriptors_count(stm->pcm);
+ assert(stm->nfds > 0);
+
+ stm->saved_fds = calloc(stm->nfds, sizeof(struct pollfd));
+ assert(stm->saved_fds);
+ r = snd_pcm_poll_descriptors(stm->pcm, stm->saved_fds, stm->nfds);
+ assert((nfds_t) r == stm->nfds);
+
+ r = pthread_cond_init(&stm->cond, NULL);
+ assert(r == 0);
+
+ if (alsa_register_stream(ctx, stm) != 0) {
+ alsa_stream_destroy(stm);
+ return CUBEB_ERROR;
+ }
+
+ *stream = stm;
+
+ return CUBEB_OK;
+}
+
+static void
+alsa_stream_destroy(cubeb_stream * stm)
+{
+ int r;
+ cubeb * ctx;
+
+ assert(stm && (stm->state == INACTIVE ||
+ stm->state == ERROR ||
+ stm->state == DRAINING));
+
+ ctx = stm->context;
+
+ pthread_mutex_lock(&stm->mutex);
+ if (stm->pcm) {
+ if (stm->state == DRAINING) {
+ snd_pcm_drain(stm->pcm);
+ }
+ alsa_locked_pcm_close(stm->pcm);
+ stm->pcm = NULL;
+ }
+ free(stm->saved_fds);
+ pthread_mutex_unlock(&stm->mutex);
+ pthread_mutex_destroy(&stm->mutex);
+
+ r = pthread_cond_destroy(&stm->cond);
+ assert(r == 0);
+
+ alsa_unregister_stream(stm);
+
+ pthread_mutex_lock(&ctx->mutex);
+ assert(ctx->active_streams >= 1);
+ ctx->active_streams -= 1;
+ pthread_mutex_unlock(&ctx->mutex);
+
+ free(stm);
+}
+
+static int
+alsa_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
+{
+ int r;
+ cubeb_stream * stm;
+ snd_pcm_hw_params_t* hw_params;
+ cubeb_stream_params params;
+ params.rate = 44100;
+ params.format = CUBEB_SAMPLE_FLOAT32NE;
+ params.channels = 2;
+
+ snd_pcm_hw_params_alloca(&hw_params);
+
+ assert(ctx);
+
+ r = alsa_stream_init(ctx, &stm, "", NULL, NULL, NULL, &params, 100, NULL, NULL, NULL);
+ if (r != CUBEB_OK) {
+ return CUBEB_ERROR;
+ }
+
+ r = snd_pcm_hw_params_any(stm->pcm, hw_params);
+ if (r < 0) {
+ return CUBEB_ERROR;
+ }
+
+ r = snd_pcm_hw_params_get_channels_max(hw_params, max_channels);
+ if (r < 0) {
+ return CUBEB_ERROR;
+ }
+
+ alsa_stream_destroy(stm);
+
+ return CUBEB_OK;
+}
+
+static int
+alsa_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) {
+ (void)ctx;
+ int r, dir;
+ snd_pcm_t * pcm;
+ snd_pcm_hw_params_t * hw_params;
+
+ snd_pcm_hw_params_alloca(&hw_params);
+
+ /* get a pcm, disabling resampling, so we get a rate the
+ * hardware/dmix/pulse/etc. supports. */
+ r = snd_pcm_open(&pcm, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, SND_PCM_NO_AUTO_RESAMPLE);
+ if (r < 0) {
+ return CUBEB_ERROR;
+ }
+
+ r = snd_pcm_hw_params_any(pcm, hw_params);
+ if (r < 0) {
+ snd_pcm_close(pcm);
+ return CUBEB_ERROR;
+ }
+
+ r = snd_pcm_hw_params_get_rate(hw_params, rate, &dir);
+ if (r >= 0) {
+ /* There is a default rate: use it. */
+ snd_pcm_close(pcm);
+ return CUBEB_OK;
+ }
+
+ /* Use a common rate, alsa may adjust it based on hw/etc. capabilities. */
+ *rate = 44100;
+
+ r = snd_pcm_hw_params_set_rate_near(pcm, hw_params, rate, NULL);
+ if (r < 0) {
+ snd_pcm_close(pcm);
+ return CUBEB_ERROR;
+ }
+
+ snd_pcm_close(pcm);
+
+ return CUBEB_OK;
+}
+
+static int
+alsa_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_frames)
+{
+ (void)ctx;
+ /* 40ms is found to be an acceptable minimum, even on a super low-end
+ * machine. */
+ *latency_frames = 40 * params.rate / 1000;
+
+ return CUBEB_OK;
+}
+
+static int
+alsa_stream_start(cubeb_stream * stm)
+{
+ cubeb * ctx;
+
+ assert(stm);
+ ctx = stm->context;
+
+ pthread_mutex_lock(&stm->mutex);
+ snd_pcm_pause(stm->pcm, 0);
+ gettimeofday(&stm->last_activity, NULL);
+ pthread_mutex_unlock(&stm->mutex);
+
+ pthread_mutex_lock(&ctx->mutex);
+ if (stm->state != INACTIVE) {
+ pthread_mutex_unlock(&ctx->mutex);
+ return CUBEB_ERROR;
+ }
+ alsa_set_stream_state(stm, RUNNING);
+ pthread_mutex_unlock(&ctx->mutex);
+
+ return CUBEB_OK;
+}
+
+static int
+alsa_stream_stop(cubeb_stream * stm)
+{
+ cubeb * ctx;
+ int r;
+
+ assert(stm);
+ ctx = stm->context;
+
+ pthread_mutex_lock(&ctx->mutex);
+ while (stm->state == PROCESSING) {
+ r = pthread_cond_wait(&stm->cond, &ctx->mutex);
+ assert(r == 0);
+ }
+
+ alsa_set_stream_state(stm, INACTIVE);
+ pthread_mutex_unlock(&ctx->mutex);
+
+ pthread_mutex_lock(&stm->mutex);
+ snd_pcm_pause(stm->pcm, 1);
+ pthread_mutex_unlock(&stm->mutex);
+
+ return CUBEB_OK;
+}
+
+static int
+alsa_stream_get_position(cubeb_stream * stm, uint64_t * position)
+{
+ snd_pcm_sframes_t delay;
+
+ assert(stm && position);
+
+ pthread_mutex_lock(&stm->mutex);
+
+ delay = -1;
+ if (snd_pcm_state(stm->pcm) != SND_PCM_STATE_RUNNING ||
+ snd_pcm_delay(stm->pcm, &delay) != 0) {
+ *position = stm->last_position;
+ pthread_mutex_unlock(&stm->mutex);
+ return CUBEB_OK;
+ }
+
+ assert(delay >= 0);
+
+ *position = 0;
+ if (stm->write_position >= (snd_pcm_uframes_t) delay) {
+ *position = stm->write_position - delay;
+ }
+
+ stm->last_position = *position;
+
+ pthread_mutex_unlock(&stm->mutex);
+ return CUBEB_OK;
+}
+
+static int
+alsa_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
+{
+ snd_pcm_sframes_t delay;
+ /* This function returns the delay in frames until a frame written using
+ snd_pcm_writei is sent to the DAC. The DAC delay should be < 1ms anyways. */
+ if (snd_pcm_delay(stm->pcm, &delay)) {
+ return CUBEB_ERROR;
+ }
+
+ *latency = delay;
+
+ return CUBEB_OK;
+}
+
+static int
+alsa_stream_set_volume(cubeb_stream * stm, float volume)
+{
+ /* setting the volume using an API call does not seem very stable/supported */
+ pthread_mutex_lock(&stm->mutex);
+ stm->volume = volume;
+ pthread_mutex_unlock(&stm->mutex);
+
+ return CUBEB_OK;
+}
+
+static struct cubeb_ops const alsa_ops = {
+ .init = alsa_init,
+ .get_backend_id = alsa_get_backend_id,
+ .get_max_channel_count = alsa_get_max_channel_count,
+ .get_min_latency = alsa_get_min_latency,
+ .get_preferred_sample_rate = alsa_get_preferred_sample_rate,
+ .enumerate_devices = NULL,
+ .destroy = alsa_destroy,
+ .stream_init = alsa_stream_init,
+ .stream_destroy = alsa_stream_destroy,
+ .stream_start = alsa_stream_start,
+ .stream_stop = alsa_stream_stop,
+ .stream_get_position = alsa_stream_get_position,
+ .stream_get_latency = alsa_stream_get_latency,
+ .stream_set_volume = alsa_stream_set_volume,
+ .stream_set_panning = NULL,
+ .stream_get_current_device = NULL,
+ .stream_device_destroy = NULL,
+ .stream_register_device_changed_callback = NULL,
+ .register_device_collection_changed = NULL
+};