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