diff options
Diffstat (limited to 'media/libcubeb/src/cubeb_sndio.c')
-rw-r--r-- | media/libcubeb/src/cubeb_sndio.c | 678 |
1 files changed, 678 insertions, 0 deletions
diff --git a/media/libcubeb/src/cubeb_sndio.c b/media/libcubeb/src/cubeb_sndio.c new file mode 100644 index 0000000000..665110b8f8 --- /dev/null +++ b/media/libcubeb/src/cubeb_sndio.c @@ -0,0 +1,678 @@ +/* + * Copyright (c) 2011 Alexandre Ratchov <alex@caoua.org> + * + * This program is made available under an ISC-style license. See the + * accompanying file LICENSE for details. + */ +#include "cubeb-internal.h" +#include "cubeb/cubeb.h" +#include "cubeb_tracing.h" +#include <assert.h> +#include <dlfcn.h> +#include <inttypes.h> +#include <math.h> +#include <poll.h> +#include <pthread.h> +#include <sndio.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> + +#if defined(CUBEB_SNDIO_DEBUG) +#define DPR(...) fprintf(stderr, __VA_ARGS__); +#else +#define DPR(...) \ + do { \ + } while (0) +#endif + +#ifdef DISABLE_LIBSNDIO_DLOPEN +#define WRAP(x) x +#else +#define WRAP(x) (*cubeb_##x) +#define LIBSNDIO_API_VISIT(X) \ + X(sio_close) \ + X(sio_eof) \ + X(sio_getpar) \ + X(sio_initpar) \ + X(sio_nfds) \ + X(sio_onmove) \ + X(sio_open) \ + X(sio_pollfd) \ + X(sio_read) \ + X(sio_revents) \ + X(sio_setpar) \ + X(sio_start) \ + X(sio_stop) \ + X(sio_write) + +#define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x; +LIBSNDIO_API_VISIT(MAKE_TYPEDEF); +#undef MAKE_TYPEDEF +#endif + +static struct cubeb_ops const sndio_ops; + +struct cubeb { + struct cubeb_ops const * ops; + void * libsndio; +}; + +struct cubeb_stream { + /* Note: Must match cubeb_stream layout in cubeb.c. */ + cubeb * context; + void * arg; /* user arg to {data,state}_cb */ + /**/ + pthread_t th; /* to run real-time audio i/o */ + pthread_mutex_t mtx; /* protects hdl and pos */ + struct sio_hdl * hdl; /* link us to sndio */ + int mode; /* bitmap of SIO_{PLAY,REC} */ + int active; /* cubec_start() called */ + int conv; /* need float->s16 conversion */ + unsigned char * rbuf; /* rec data consumed from here */ + unsigned char * pbuf; /* play data is prepared here */ + unsigned int nfr; /* number of frames in ibuf and obuf */ + unsigned int rbpf; /* rec bytes per frame */ + unsigned int pbpf; /* play bytes per frame */ + unsigned int rchan; /* number of rec channels */ + unsigned int pchan; /* number of play channels */ + unsigned int nblks; /* number of blocks in the buffer */ + uint64_t hwpos; /* frame number Joe hears right now */ + uint64_t swpos; /* number of frames produced/consumed */ + cubeb_data_callback data_cb; /* cb to preapare data */ + cubeb_state_callback state_cb; /* cb to notify about state changes */ + float volume; /* current volume */ +}; + +static void +s16_setvol(void * ptr, long nsamp, float volume) +{ + int16_t * dst = ptr; + int32_t mult = volume * 32768; + int32_t s; + + while (nsamp-- > 0) { + s = *dst; + s = (s * mult) >> 15; + *(dst++) = s; + } +} + +static void +float_to_s16(void * ptr, long nsamp, float volume) +{ + int16_t * dst = ptr; + float * src = ptr; + float mult = volume * 32768; + int s; + + while (nsamp-- > 0) { + s = lrintf(*(src++) * mult); + if (s < -32768) + s = -32768; + else if (s > 32767) + s = 32767; + *(dst++) = s; + } +} + +static void +s16_to_float(void * ptr, long nsamp) +{ + int16_t * src = ptr; + float * dst = ptr; + + src += nsamp; + dst += nsamp; + while (nsamp-- > 0) + *(--dst) = (1. / 32768) * *(--src); +} + +static const char * +sndio_get_device() +{ +#ifdef __linux__ + /* + * On other platforms default to sndio devices, + * so cubebs other backends can be used instead. + */ + const char * dev = getenv("AUDIODEVICE"); + if (dev == NULL || *dev == '\0') + return "snd/0"; + return dev; +#else + return SIO_DEVANY; +#endif +} + +static void +sndio_onmove(void * arg, int delta) +{ + cubeb_stream * s = (cubeb_stream *)arg; + + s->hwpos += delta; +} + +static void * +sndio_mainloop(void * arg) +{ + struct pollfd * pfds; + cubeb_stream * s = arg; + int n, eof = 0, prime, nfds, events, revents, state = CUBEB_STATE_STARTED; + size_t pstart = 0, pend = 0, rstart = 0, rend = 0; + long nfr; + + CUBEB_REGISTER_THREAD("cubeb rendering thread"); + + nfds = WRAP(sio_nfds)(s->hdl); + pfds = calloc(nfds, sizeof(struct pollfd)); + if (pfds == NULL) { + CUBEB_UNREGISTER_THREAD(); + return NULL; + } + + DPR("sndio_mainloop()\n"); + s->state_cb(s, s->arg, CUBEB_STATE_STARTED); + pthread_mutex_lock(&s->mtx); + if (!WRAP(sio_start)(s->hdl)) { + pthread_mutex_unlock(&s->mtx); + free(pfds); + CUBEB_UNREGISTER_THREAD(); + return NULL; + } + DPR("sndio_mainloop(), started\n"); + + if (s->mode & SIO_PLAY) { + pstart = pend = s->nfr * s->pbpf; + prime = s->nblks; + if (s->mode & SIO_REC) { + memset(s->rbuf, 0, s->nfr * s->rbpf); + rstart = rend = s->nfr * s->rbpf; + } + } else { + prime = 0; + rstart = 0; + rend = s->nfr * s->rbpf; + } + + for (;;) { + if (!s->active) { + DPR("sndio_mainloop() stopped\n"); + state = CUBEB_STATE_STOPPED; + break; + } + + /* do we have a complete block? */ + if ((!(s->mode & SIO_PLAY) || pstart == pend) && + (!(s->mode & SIO_REC) || rstart == rend)) { + + if (eof) { + DPR("sndio_mainloop() drained\n"); + state = CUBEB_STATE_DRAINED; + break; + } + + if ((s->mode & SIO_REC) && s->conv) + s16_to_float(s->rbuf, s->nfr * s->rchan); + + /* invoke call-back, it returns less that s->nfr if done */ + pthread_mutex_unlock(&s->mtx); + nfr = s->data_cb(s, s->arg, s->rbuf, s->pbuf, s->nfr); + pthread_mutex_lock(&s->mtx); + if (nfr < 0) { + DPR("sndio_mainloop() cb err\n"); + state = CUBEB_STATE_ERROR; + break; + } + s->swpos += nfr; + + /* was this last call-back invocation (aka end-of-stream) ? */ + if (nfr < s->nfr) { + + if (!(s->mode & SIO_PLAY) || nfr == 0) { + state = CUBEB_STATE_DRAINED; + break; + } + + /* need to write (aka drain) the partial play block we got */ + pend = nfr * s->pbpf; + eof = 1; + } + + if (prime > 0) + prime--; + + if (s->mode & SIO_PLAY) { + if (s->conv) + float_to_s16(s->pbuf, nfr * s->pchan, s->volume); + else + s16_setvol(s->pbuf, nfr * s->pchan, s->volume); + } + + if (s->mode & SIO_REC) + rstart = 0; + if (s->mode & SIO_PLAY) + pstart = 0; + } + + events = 0; + if ((s->mode & SIO_REC) && rstart < rend && prime == 0) + events |= POLLIN; + if ((s->mode & SIO_PLAY) && pstart < pend) + events |= POLLOUT; + nfds = WRAP(sio_pollfd)(s->hdl, pfds, events); + + if (nfds > 0) { + pthread_mutex_unlock(&s->mtx); + n = poll(pfds, nfds, -1); + pthread_mutex_lock(&s->mtx); + if (n < 0) + continue; + } + + revents = WRAP(sio_revents)(s->hdl, pfds); + + if (revents & POLLHUP) { + state = CUBEB_STATE_ERROR; + break; + } + + if (revents & POLLOUT) { + n = WRAP(sio_write)(s->hdl, s->pbuf + pstart, pend - pstart); + if (n == 0 && WRAP(sio_eof)(s->hdl)) { + DPR("sndio_mainloop() werr\n"); + state = CUBEB_STATE_ERROR; + break; + } + pstart += n; + } + + if (revents & POLLIN) { + n = WRAP(sio_read)(s->hdl, s->rbuf + rstart, rend - rstart); + if (n == 0 && WRAP(sio_eof)(s->hdl)) { + DPR("sndio_mainloop() rerr\n"); + state = CUBEB_STATE_ERROR; + break; + } + rstart += n; + } + + /* skip rec block, if not recording (yet) */ + if (prime > 0 && (s->mode & SIO_REC)) + rstart = rend; + } + WRAP(sio_stop)(s->hdl); + s->hwpos = s->swpos; + pthread_mutex_unlock(&s->mtx); + s->state_cb(s, s->arg, state); + free(pfds); + CUBEB_UNREGISTER_THREAD(); + return NULL; +} + +/*static*/ int +sndio_init(cubeb ** context, char const * context_name) +{ + void * libsndio = NULL; + struct sio_hdl * hdl; + + assert(context); + +#ifndef DISABLE_LIBSNDIO_DLOPEN + libsndio = dlopen("libsndio.so.7.0", RTLD_LAZY); + if (!libsndio) { + libsndio = dlopen("libsndio.so", RTLD_LAZY); + if (!libsndio) { + DPR("sndio_init(%s) failed dlopen(libsndio.so)\n", context_name); + return CUBEB_ERROR; + } + } + +#define LOAD(x) \ + { \ + cubeb_##x = dlsym(libsndio, #x); \ + if (!cubeb_##x) { \ + DPR("sndio_init(%s) failed dlsym(%s)\n", context_name, #x); \ + dlclose(libsndio); \ + return CUBEB_ERROR; \ + } \ + } + + LIBSNDIO_API_VISIT(LOAD); +#undef LOAD +#endif + + /* test if sndio works */ + hdl = WRAP(sio_open)(sndio_get_device(), SIO_PLAY, 1); + if (hdl == NULL) { + return CUBEB_ERROR; + } + WRAP(sio_close)(hdl); + + DPR("sndio_init(%s)\n", context_name); + *context = malloc(sizeof(**context)); + if (*context == NULL) + return CUBEB_ERROR; + (*context)->libsndio = libsndio; + (*context)->ops = &sndio_ops; + (void)context_name; + return CUBEB_OK; +} + +static char const * +sndio_get_backend_id(cubeb * context) +{ + return "sndio"; +} + +static void +sndio_destroy(cubeb * context) +{ + DPR("sndio_destroy()\n"); +#ifndef DISABLE_LIBSNDIO_DLOPEN + if (context->libsndio) + dlclose(context->libsndio); +#endif + free(context); +} + +static int +sndio_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 * s; + struct sio_par wpar, rpar; + cubeb_sample_format format; + int rate; + size_t bps; + + DPR("sndio_stream_init(%s)\n", stream_name); + + s = malloc(sizeof(cubeb_stream)); + if (s == NULL) + return CUBEB_ERROR; + memset(s, 0, sizeof(cubeb_stream)); + s->mode = 0; + if (input_stream_params) { + if (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + DPR("sndio_stream_init(), loopback not supported\n"); + goto err; + } + s->mode |= SIO_REC; + format = input_stream_params->format; + rate = input_stream_params->rate; + } + if (output_stream_params) { + if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { + DPR("sndio_stream_init(), loopback not supported\n"); + goto err; + } + s->mode |= SIO_PLAY; + format = output_stream_params->format; + rate = output_stream_params->rate; + } + if (s->mode == 0) { + DPR("sndio_stream_init(), neither playing nor recording\n"); + goto err; + } + s->context = context; + s->hdl = WRAP(sio_open)(sndio_get_device(), s->mode, 1); + if (s->hdl == NULL) { + DPR("sndio_stream_init(), sio_open() failed\n"); + goto err; + } + WRAP(sio_initpar)(&wpar); + wpar.sig = 1; + wpar.bits = 16; + switch (format) { + case CUBEB_SAMPLE_S16LE: + wpar.le = 1; + break; + case CUBEB_SAMPLE_S16BE: + wpar.le = 0; + break; + case CUBEB_SAMPLE_FLOAT32NE: + wpar.le = SIO_LE_NATIVE; + break; + default: + DPR("sndio_stream_init() unsupported format\n"); + goto err; + } + wpar.rate = rate; + if (s->mode & SIO_REC) + wpar.rchan = input_stream_params->channels; + if (s->mode & SIO_PLAY) + wpar.pchan = output_stream_params->channels; + wpar.appbufsz = latency_frames; + if (!WRAP(sio_setpar)(s->hdl, &wpar) || !WRAP(sio_getpar)(s->hdl, &rpar)) { + DPR("sndio_stream_init(), sio_setpar() failed\n"); + goto err; + } + if (rpar.bits != wpar.bits || rpar.le != wpar.le || rpar.sig != wpar.sig || + rpar.rate != wpar.rate || + ((s->mode & SIO_REC) && rpar.rchan != wpar.rchan) || + ((s->mode & SIO_PLAY) && rpar.pchan != wpar.pchan)) { + DPR("sndio_stream_init() unsupported params\n"); + goto err; + } + WRAP(sio_onmove)(s->hdl, sndio_onmove, s); + s->active = 0; + s->nfr = rpar.round; + s->rbpf = rpar.bps * rpar.rchan; + s->pbpf = rpar.bps * rpar.pchan; + s->rchan = rpar.rchan; + s->pchan = rpar.pchan; + s->nblks = rpar.bufsz / rpar.round; + s->data_cb = data_callback; + s->state_cb = state_callback; + s->arg = user_ptr; + s->mtx = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; + s->hwpos = s->swpos = 0; + if (format == CUBEB_SAMPLE_FLOAT32LE) { + s->conv = 1; + bps = sizeof(float); + } else { + s->conv = 0; + bps = rpar.bps; + } + if (s->mode & SIO_PLAY) { + s->pbuf = malloc(bps * rpar.pchan * rpar.round); + if (s->pbuf == NULL) + goto err; + } + if (s->mode & SIO_REC) { + s->rbuf = malloc(bps * rpar.rchan * rpar.round); + if (s->rbuf == NULL) + goto err; + } + s->volume = 1.; + *stream = s; + DPR("sndio_stream_init() end, ok\n"); + (void)context; + (void)stream_name; + return CUBEB_OK; +err: + if (s->hdl) + WRAP(sio_close)(s->hdl); + if (s->pbuf) + free(s->pbuf); + if (s->rbuf) + free(s->pbuf); + free(s); + return CUBEB_ERROR; +} + +static int +sndio_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) +{ + assert(ctx && max_channels); + + *max_channels = 8; + + return CUBEB_OK; +} + +static int +sndio_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) +{ + /* + * We've no device-independent prefered rate; any rate will work if + * sndiod is running. If it isn't, 48kHz is what is most likely to + * work as most (but not all) devices support it. + */ + *rate = 48000; + return CUBEB_OK; +} + +static int +sndio_get_min_latency(cubeb * ctx, cubeb_stream_params params, + uint32_t * latency_frames) +{ + /* + * We've no device-independent minimum latency. + */ + *latency_frames = 2048; + + return CUBEB_OK; +} + +static void +sndio_stream_destroy(cubeb_stream * s) +{ + DPR("sndio_stream_destroy()\n"); + WRAP(sio_close)(s->hdl); + if (s->mode & SIO_PLAY) + free(s->pbuf); + if (s->mode & SIO_REC) + free(s->rbuf); + free(s); +} + +static int +sndio_stream_start(cubeb_stream * s) +{ + int err; + + DPR("sndio_stream_start()\n"); + s->active = 1; + err = pthread_create(&s->th, NULL, sndio_mainloop, s); + if (err) { + s->active = 0; + return CUBEB_ERROR; + } + return CUBEB_OK; +} + +static int +sndio_stream_stop(cubeb_stream * s) +{ + void * dummy; + + DPR("sndio_stream_stop()\n"); + if (s->active) { + s->active = 0; + pthread_join(s->th, &dummy); + } + return CUBEB_OK; +} + +static int +sndio_stream_get_position(cubeb_stream * s, uint64_t * p) +{ + pthread_mutex_lock(&s->mtx); + DPR("sndio_stream_get_position() %" PRId64 "\n", s->hwpos); + *p = s->hwpos; + pthread_mutex_unlock(&s->mtx); + return CUBEB_OK; +} + +static int +sndio_stream_set_volume(cubeb_stream * s, float volume) +{ + DPR("sndio_stream_set_volume(%f)\n", volume); + pthread_mutex_lock(&s->mtx); + if (volume < 0.) + volume = 0.; + else if (volume > 1.0) + volume = 1.; + s->volume = volume; + pthread_mutex_unlock(&s->mtx); + return CUBEB_OK; +} + +int +sndio_stream_get_latency(cubeb_stream * stm, uint32_t * latency) +{ + // http://www.openbsd.org/cgi-bin/man.cgi?query=sio_open + // in the "Measuring the latency and buffers usage" paragraph. + *latency = stm->swpos - stm->hwpos; + return CUBEB_OK; +} + +static int +sndio_enumerate_devices(cubeb * context, cubeb_device_type type, + cubeb_device_collection * collection) +{ + static char dev[] = SIO_DEVANY; + cubeb_device_info * device; + + device = malloc(sizeof(cubeb_device_info)); + if (device == NULL) + return CUBEB_ERROR; + + device->devid = dev; /* passed to stream_init() */ + device->device_id = dev; /* printable in UI */ + device->friendly_name = dev; /* same, but friendly */ + device->group_id = dev; /* actual device if full-duplex */ + device->vendor_name = NULL; /* may be NULL */ + device->type = type; /* Input/Output */ + device->state = CUBEB_DEVICE_STATE_ENABLED; + device->preferred = CUBEB_DEVICE_PREF_ALL; + device->format = CUBEB_DEVICE_FMT_S16NE; + device->default_format = CUBEB_DEVICE_FMT_S16NE; + device->max_channels = (type == CUBEB_DEVICE_TYPE_INPUT) ? 2 : 8; + device->default_rate = 48000; + device->min_rate = 4000; + device->max_rate = 192000; + device->latency_lo = 480; + device->latency_hi = 9600; + collection->device = device; + collection->count = 1; + return CUBEB_OK; +} + +static int +sndio_device_collection_destroy(cubeb * context, + cubeb_device_collection * collection) +{ + free(collection->device); + return CUBEB_OK; +} + +static struct cubeb_ops const sndio_ops = { + .init = sndio_init, + .get_backend_id = sndio_get_backend_id, + .get_max_channel_count = sndio_get_max_channel_count, + .get_min_latency = sndio_get_min_latency, + .get_preferred_sample_rate = sndio_get_preferred_sample_rate, + .enumerate_devices = sndio_enumerate_devices, + .device_collection_destroy = sndio_device_collection_destroy, + .destroy = sndio_destroy, + .stream_init = sndio_stream_init, + .stream_destroy = sndio_stream_destroy, + .stream_start = sndio_stream_start, + .stream_stop = sndio_stream_stop, + .stream_get_position = sndio_stream_get_position, + .stream_get_latency = sndio_stream_get_latency, + .stream_set_volume = sndio_stream_set_volume, + .stream_set_name = NULL, + .stream_get_current_device = NULL, + .stream_device_destroy = NULL, + .stream_register_device_changed_callback = NULL, + .register_device_collection_changed = NULL}; |