/* * 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 #include #include #include #include #include #include #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); } if (wrote < 0 || wrote != got) { /* Recovery failed, somehow. */ pthread_mutex_unlock(&stm->mutex); stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); return ERROR; } 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, ¶ms, 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 };