/* * Copyright (c) 2011 Alexandre Ratchov * * 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 #include #include #include #include #include #include #include #include #include #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->s24 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_s24(void * ptr, long nsamp, float volume) { int32_t * dst = ptr; float * src = ptr; float mult = volume * 8388608; int s; while (nsamp-- > 0) { s = lrintf(*(src++) * mult); if (s < -8388608) s = -8388608; else if (s > 8388607) s = 8388607; *(dst++) = s; } } static void s24_to_float(void * ptr, long nsamp) { int32_t * src = ptr; float * dst = ptr; src += nsamp; dst += nsamp; while (nsamp-- > 0) *(--dst) = (1. / 8388608) * *(--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) s24_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_s24(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; switch (format) { case CUBEB_SAMPLE_S16LE: wpar.le = 1; wpar.bits = 16; break; case CUBEB_SAMPLE_S16BE: wpar.le = 0; wpar.bits = 16; break; case CUBEB_SAMPLE_FLOAT32NE: wpar.le = SIO_LE_NATIVE; wpar.bits = 24; wpar.msb = 0; break; default: DPR("sndio_stream_init() unsupported format\n"); goto err; } wpar.bps = SIO_BPS(wpar.bits); 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.bps != wpar.bps || (wpar.bits < 8 * wpar.bps && rpar.msb != wpar.msb) || 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, .get_supported_input_processing_params = NULL, .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_set_input_processing_params = NULL, .stream_device_destroy = NULL, .stream_register_device_changed_callback = NULL, .register_device_collection_changed = NULL};