1 /*
2 * Copyright (c) 2011 Alexandre Ratchov <alex@caoua.org>
3 *
4 * This program is made available under an ISC-style license. See the
5 * accompanying file LICENSE for details.
6 */
7 #include "cubeb-internal.h"
8 #include "cubeb/cubeb.h"
9 #include <assert.h>
10 #include <dlfcn.h>
11 #include <inttypes.h>
12 #include <math.h>
13 #include <poll.h>
14 #include <pthread.h>
15 #include <sndio.h>
16 #include <stdbool.h>
17 #include <stdio.h>
18 #include <stdlib.h>
19
20 #if defined(CUBEB_SNDIO_DEBUG)
21 #define DPR(...) fprintf(stderr, __VA_ARGS__);
22 #else
23 #define DPR(...) \
24 do { \
25 } while (0)
26 #endif
27
28 #ifdef DISABLE_LIBSNDIO_DLOPEN
29 #define WRAP(x) x
30 #else
31 #define WRAP(x) (*cubeb_##x)
32 #define LIBSNDIO_API_VISIT(X) \
33 X(sio_close) \
34 X(sio_eof) \
35 X(sio_getpar) \
36 X(sio_initpar) \
37 X(sio_nfds) \
38 X(sio_onmove) \
39 X(sio_open) \
40 X(sio_pollfd) \
41 X(sio_read) \
42 X(sio_revents) \
43 X(sio_setpar) \
44 X(sio_start) \
45 X(sio_stop) \
46 X(sio_write)
47
48 #define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x;
49 LIBSNDIO_API_VISIT(MAKE_TYPEDEF);
50 #undef MAKE_TYPEDEF
51 #endif
52
53 static struct cubeb_ops const sndio_ops;
54
55 struct cubeb {
56 struct cubeb_ops const * ops;
57 void * libsndio;
58 };
59
60 struct cubeb_stream {
61 /* Note: Must match cubeb_stream layout in cubeb.c. */
62 cubeb * context;
63 void * arg; /* user arg to {data,state}_cb */
64 /**/
65 pthread_t th; /* to run real-time audio i/o */
66 pthread_mutex_t mtx; /* protects hdl and pos */
67 struct sio_hdl * hdl; /* link us to sndio */
68 int mode; /* bitmap of SIO_{PLAY,REC} */
69 int active; /* cubec_start() called */
70 int conv; /* need float->s16 conversion */
71 unsigned char * rbuf; /* rec data consumed from here */
72 unsigned char * pbuf; /* play data is prepared here */
73 unsigned int nfr; /* number of frames in ibuf and obuf */
74 unsigned int rbpf; /* rec bytes per frame */
75 unsigned int pbpf; /* play bytes per frame */
76 unsigned int rchan; /* number of rec channels */
77 unsigned int pchan; /* number of play channels */
78 unsigned int nblks; /* number of blocks in the buffer */
79 uint64_t hwpos; /* frame number Joe hears right now */
80 uint64_t swpos; /* number of frames produced/consumed */
81 cubeb_data_callback data_cb; /* cb to preapare data */
82 cubeb_state_callback state_cb; /* cb to notify about state changes */
83 float volume; /* current volume */
84 };
85
86 static void
s16_setvol(void * ptr,long nsamp,float volume)87 s16_setvol(void * ptr, long nsamp, float volume)
88 {
89 int16_t * dst = ptr;
90 int32_t mult = volume * 32768;
91 int32_t s;
92
93 while (nsamp-- > 0) {
94 s = *dst;
95 s = (s * mult) >> 15;
96 *(dst++) = s;
97 }
98 }
99
100 static void
float_to_s16(void * ptr,long nsamp,float volume)101 float_to_s16(void * ptr, long nsamp, float volume)
102 {
103 int16_t * dst = ptr;
104 float * src = ptr;
105 float mult = volume * 32768;
106 int s;
107
108 while (nsamp-- > 0) {
109 s = lrintf(*(src++) * mult);
110 if (s < -32768)
111 s = -32768;
112 else if (s > 32767)
113 s = 32767;
114 *(dst++) = s;
115 }
116 }
117
118 static void
s16_to_float(void * ptr,long nsamp)119 s16_to_float(void * ptr, long nsamp)
120 {
121 int16_t * src = ptr;
122 float * dst = ptr;
123
124 src += nsamp;
125 dst += nsamp;
126 while (nsamp-- > 0)
127 *(--dst) = (1. / 32768) * *(--src);
128 }
129
130 static const char *
sndio_get_device()131 sndio_get_device()
132 {
133 #ifdef __linux__
134 /*
135 * On other platforms default to sndio devices,
136 * so cubebs other backends can be used instead.
137 */
138 const char * dev = getenv("AUDIODEVICE");
139 if (dev == NULL || *dev == '\0')
140 return "snd/0";
141 return dev;
142 #else
143 return SIO_DEVANY;
144 #endif
145 }
146
147 static void
sndio_onmove(void * arg,int delta)148 sndio_onmove(void * arg, int delta)
149 {
150 cubeb_stream * s = (cubeb_stream *)arg;
151
152 s->hwpos += delta;
153 }
154
155 static void *
sndio_mainloop(void * arg)156 sndio_mainloop(void * arg)
157 {
158 struct pollfd * pfds;
159 cubeb_stream * s = arg;
160 int n, eof = 0, prime, nfds, events, revents, state = CUBEB_STATE_STARTED;
161 size_t pstart = 0, pend = 0, rstart = 0, rend = 0;
162 long nfr;
163
164 nfds = WRAP(sio_nfds)(s->hdl);
165 pfds = calloc(nfds, sizeof(struct pollfd));
166 if (pfds == NULL)
167 return NULL;
168
169 DPR("sndio_mainloop()\n");
170 s->state_cb(s, s->arg, CUBEB_STATE_STARTED);
171 pthread_mutex_lock(&s->mtx);
172 if (!WRAP(sio_start)(s->hdl)) {
173 pthread_mutex_unlock(&s->mtx);
174 free(pfds);
175 return NULL;
176 }
177 DPR("sndio_mainloop(), started\n");
178
179 if (s->mode & SIO_PLAY) {
180 pstart = pend = s->nfr * s->pbpf;
181 prime = s->nblks;
182 if (s->mode & SIO_REC) {
183 memset(s->rbuf, 0, s->nfr * s->rbpf);
184 rstart = rend = s->nfr * s->rbpf;
185 }
186 } else {
187 prime = 0;
188 rstart = 0;
189 rend = s->nfr * s->rbpf;
190 }
191
192 for (;;) {
193 if (!s->active) {
194 DPR("sndio_mainloop() stopped\n");
195 state = CUBEB_STATE_STOPPED;
196 break;
197 }
198
199 /* do we have a complete block? */
200 if ((!(s->mode & SIO_PLAY) || pstart == pend) &&
201 (!(s->mode & SIO_REC) || rstart == rend)) {
202
203 if (eof) {
204 DPR("sndio_mainloop() drained\n");
205 state = CUBEB_STATE_DRAINED;
206 break;
207 }
208
209 if ((s->mode & SIO_REC) && s->conv)
210 s16_to_float(s->rbuf, s->nfr * s->rchan);
211
212 /* invoke call-back, it returns less that s->nfr if done */
213 pthread_mutex_unlock(&s->mtx);
214 nfr = s->data_cb(s, s->arg, s->rbuf, s->pbuf, s->nfr);
215 pthread_mutex_lock(&s->mtx);
216 if (nfr < 0) {
217 DPR("sndio_mainloop() cb err\n");
218 state = CUBEB_STATE_ERROR;
219 break;
220 }
221 s->swpos += nfr;
222
223 /* was this last call-back invocation (aka end-of-stream) ? */
224 if (nfr < s->nfr) {
225
226 if (!(s->mode & SIO_PLAY) || nfr == 0) {
227 state = CUBEB_STATE_DRAINED;
228 break;
229 }
230
231 /* need to write (aka drain) the partial play block we got */
232 pend = nfr * s->pbpf;
233 eof = 1;
234 }
235
236 if (prime > 0)
237 prime--;
238
239 if (s->mode & SIO_PLAY) {
240 if (s->conv)
241 float_to_s16(s->pbuf, nfr * s->pchan, s->volume);
242 else
243 s16_setvol(s->pbuf, nfr * s->pchan, s->volume);
244 }
245
246 if (s->mode & SIO_REC)
247 rstart = 0;
248 if (s->mode & SIO_PLAY)
249 pstart = 0;
250 }
251
252 events = 0;
253 if ((s->mode & SIO_REC) && rstart < rend && prime == 0)
254 events |= POLLIN;
255 if ((s->mode & SIO_PLAY) && pstart < pend)
256 events |= POLLOUT;
257 nfds = WRAP(sio_pollfd)(s->hdl, pfds, events);
258
259 if (nfds > 0) {
260 pthread_mutex_unlock(&s->mtx);
261 n = poll(pfds, nfds, -1);
262 pthread_mutex_lock(&s->mtx);
263 if (n < 0)
264 continue;
265 }
266
267 revents = WRAP(sio_revents)(s->hdl, pfds);
268
269 if (revents & POLLHUP) {
270 state = CUBEB_STATE_ERROR;
271 break;
272 }
273
274 if (revents & POLLOUT) {
275 n = WRAP(sio_write)(s->hdl, s->pbuf + pstart, pend - pstart);
276 if (n == 0 && WRAP(sio_eof)(s->hdl)) {
277 DPR("sndio_mainloop() werr\n");
278 state = CUBEB_STATE_ERROR;
279 break;
280 }
281 pstart += n;
282 }
283
284 if (revents & POLLIN) {
285 n = WRAP(sio_read)(s->hdl, s->rbuf + rstart, rend - rstart);
286 if (n == 0 && WRAP(sio_eof)(s->hdl)) {
287 DPR("sndio_mainloop() rerr\n");
288 state = CUBEB_STATE_ERROR;
289 break;
290 }
291 rstart += n;
292 }
293
294 /* skip rec block, if not recording (yet) */
295 if (prime > 0 && (s->mode & SIO_REC))
296 rstart = rend;
297 }
298 WRAP(sio_stop)(s->hdl);
299 s->hwpos = s->swpos;
300 pthread_mutex_unlock(&s->mtx);
301 s->state_cb(s, s->arg, state);
302 free(pfds);
303 return NULL;
304 }
305
306 /*static*/ int
sndio_init(cubeb ** context,char const * context_name)307 sndio_init(cubeb ** context, char const * context_name)
308 {
309 void * libsndio = NULL;
310 struct sio_hdl * hdl;
311
312 assert(context);
313
314 #ifndef DISABLE_LIBSNDIO_DLOPEN
315 libsndio = dlopen("libsndio.so.7.0", RTLD_LAZY);
316 if (!libsndio) {
317 libsndio = dlopen("libsndio.so", RTLD_LAZY);
318 if (!libsndio) {
319 DPR("sndio_init(%s) failed dlopen(libsndio.so)\n", context_name);
320 return CUBEB_ERROR;
321 }
322 }
323
324 #define LOAD(x) \
325 { \
326 cubeb_##x = dlsym(libsndio, #x); \
327 if (!cubeb_##x) { \
328 DPR("sndio_init(%s) failed dlsym(%s)\n", context_name, #x); \
329 dlclose(libsndio); \
330 return CUBEB_ERROR; \
331 } \
332 }
333
334 LIBSNDIO_API_VISIT(LOAD);
335 #undef LOAD
336 #endif
337
338 /* test if sndio works */
339 hdl = WRAP(sio_open)(sndio_get_device(), SIO_PLAY, 1);
340 if (hdl == NULL) {
341 return CUBEB_ERROR;
342 }
343 WRAP(sio_close)(hdl);
344
345 DPR("sndio_init(%s)\n", context_name);
346 *context = malloc(sizeof(**context));
347 if (*context == NULL)
348 return CUBEB_ERROR;
349 (*context)->libsndio = libsndio;
350 (*context)->ops = &sndio_ops;
351 (void)context_name;
352 return CUBEB_OK;
353 }
354
355 static char const *
sndio_get_backend_id(cubeb * context)356 sndio_get_backend_id(cubeb * context)
357 {
358 return "sndio";
359 }
360
361 static void
sndio_destroy(cubeb * context)362 sndio_destroy(cubeb * context)
363 {
364 DPR("sndio_destroy()\n");
365 if (context->libsndio)
366 dlclose(context->libsndio);
367 free(context);
368 }
369
370 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)371 sndio_stream_init(cubeb * context, cubeb_stream ** stream,
372 char const * stream_name, cubeb_devid input_device,
373 cubeb_stream_params * input_stream_params,
374 cubeb_devid output_device,
375 cubeb_stream_params * output_stream_params,
376 unsigned int latency_frames,
377 cubeb_data_callback data_callback,
378 cubeb_state_callback state_callback, void * user_ptr)
379 {
380 cubeb_stream * s;
381 struct sio_par wpar, rpar;
382 cubeb_sample_format format;
383 int rate;
384 size_t bps;
385
386 DPR("sndio_stream_init(%s)\n", stream_name);
387
388 s = malloc(sizeof(cubeb_stream));
389 if (s == NULL)
390 return CUBEB_ERROR;
391 memset(s, 0, sizeof(cubeb_stream));
392 s->mode = 0;
393 if (input_stream_params) {
394 if (input_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) {
395 DPR("sndio_stream_init(), loopback not supported\n");
396 goto err;
397 }
398 s->mode |= SIO_REC;
399 format = input_stream_params->format;
400 rate = input_stream_params->rate;
401 }
402 if (output_stream_params) {
403 if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) {
404 DPR("sndio_stream_init(), loopback not supported\n");
405 goto err;
406 }
407 s->mode |= SIO_PLAY;
408 format = output_stream_params->format;
409 rate = output_stream_params->rate;
410 }
411 if (s->mode == 0) {
412 DPR("sndio_stream_init(), neither playing nor recording\n");
413 goto err;
414 }
415 s->context = context;
416 s->hdl = WRAP(sio_open)(sndio_get_device(), s->mode, 1);
417 if (s->hdl == NULL) {
418 DPR("sndio_stream_init(), sio_open() failed\n");
419 goto err;
420 }
421 WRAP(sio_initpar)(&wpar);
422 wpar.sig = 1;
423 wpar.bits = 16;
424 switch (format) {
425 case CUBEB_SAMPLE_S16LE:
426 wpar.le = 1;
427 break;
428 case CUBEB_SAMPLE_S16BE:
429 wpar.le = 0;
430 break;
431 case CUBEB_SAMPLE_FLOAT32NE:
432 wpar.le = SIO_LE_NATIVE;
433 break;
434 default:
435 DPR("sndio_stream_init() unsupported format\n");
436 goto err;
437 }
438 wpar.rate = rate;
439 if (s->mode & SIO_REC)
440 wpar.rchan = input_stream_params->channels;
441 if (s->mode & SIO_PLAY)
442 wpar.pchan = output_stream_params->channels;
443 wpar.appbufsz = latency_frames;
444 if (!WRAP(sio_setpar)(s->hdl, &wpar) || !WRAP(sio_getpar)(s->hdl, &rpar)) {
445 DPR("sndio_stream_init(), sio_setpar() failed\n");
446 goto err;
447 }
448 if (rpar.bits != wpar.bits || rpar.le != wpar.le || rpar.sig != wpar.sig ||
449 rpar.rate != wpar.rate ||
450 ((s->mode & SIO_REC) && rpar.rchan != wpar.rchan) ||
451 ((s->mode & SIO_PLAY) && rpar.pchan != wpar.pchan)) {
452 DPR("sndio_stream_init() unsupported params\n");
453 goto err;
454 }
455 WRAP(sio_onmove)(s->hdl, sndio_onmove, s);
456 s->active = 0;
457 s->nfr = rpar.round;
458 s->rbpf = rpar.bps * rpar.rchan;
459 s->pbpf = rpar.bps * rpar.pchan;
460 s->rchan = rpar.rchan;
461 s->pchan = rpar.pchan;
462 s->nblks = rpar.bufsz / rpar.round;
463 s->data_cb = data_callback;
464 s->state_cb = state_callback;
465 s->arg = user_ptr;
466 s->mtx = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
467 s->hwpos = s->swpos = 0;
468 if (format == CUBEB_SAMPLE_FLOAT32LE) {
469 s->conv = 1;
470 bps = sizeof(float);
471 } else {
472 s->conv = 0;
473 bps = rpar.bps;
474 }
475 if (s->mode & SIO_PLAY) {
476 s->pbuf = malloc(bps * rpar.pchan * rpar.round);
477 if (s->pbuf == NULL)
478 goto err;
479 }
480 if (s->mode & SIO_REC) {
481 s->rbuf = malloc(bps * rpar.rchan * rpar.round);
482 if (s->rbuf == NULL)
483 goto err;
484 }
485 s->volume = 1.;
486 *stream = s;
487 DPR("sndio_stream_init() end, ok\n");
488 (void)context;
489 (void)stream_name;
490 return CUBEB_OK;
491 err:
492 if (s->hdl)
493 WRAP(sio_close)(s->hdl);
494 if (s->pbuf)
495 free(s->pbuf);
496 if (s->rbuf)
497 free(s->pbuf);
498 free(s);
499 return CUBEB_ERROR;
500 }
501
502 static int
sndio_get_max_channel_count(cubeb * ctx,uint32_t * max_channels)503 sndio_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
504 {
505 assert(ctx && max_channels);
506
507 *max_channels = 8;
508
509 return CUBEB_OK;
510 }
511
512 static int
sndio_get_preferred_sample_rate(cubeb * ctx,uint32_t * rate)513 sndio_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
514 {
515 /*
516 * We've no device-independent prefered rate; any rate will work if
517 * sndiod is running. If it isn't, 48kHz is what is most likely to
518 * work as most (but not all) devices support it.
519 */
520 *rate = 48000;
521 return CUBEB_OK;
522 }
523
524 static int
sndio_get_min_latency(cubeb * ctx,cubeb_stream_params params,uint32_t * latency_frames)525 sndio_get_min_latency(cubeb * ctx, cubeb_stream_params params,
526 uint32_t * latency_frames)
527 {
528 /*
529 * We've no device-independent minimum latency.
530 */
531 *latency_frames = 2048;
532
533 return CUBEB_OK;
534 }
535
536 static void
sndio_stream_destroy(cubeb_stream * s)537 sndio_stream_destroy(cubeb_stream * s)
538 {
539 DPR("sndio_stream_destroy()\n");
540 WRAP(sio_close)(s->hdl);
541 if (s->mode & SIO_PLAY)
542 free(s->pbuf);
543 if (s->mode & SIO_REC)
544 free(s->rbuf);
545 free(s);
546 }
547
548 static int
sndio_stream_start(cubeb_stream * s)549 sndio_stream_start(cubeb_stream * s)
550 {
551 int err;
552
553 DPR("sndio_stream_start()\n");
554 s->active = 1;
555 err = pthread_create(&s->th, NULL, sndio_mainloop, s);
556 if (err) {
557 s->active = 0;
558 return CUBEB_ERROR;
559 }
560 return CUBEB_OK;
561 }
562
563 static int
sndio_stream_stop(cubeb_stream * s)564 sndio_stream_stop(cubeb_stream * s)
565 {
566 void * dummy;
567
568 DPR("sndio_stream_stop()\n");
569 if (s->active) {
570 s->active = 0;
571 pthread_join(s->th, &dummy);
572 }
573 return CUBEB_OK;
574 }
575
576 static int
sndio_stream_get_position(cubeb_stream * s,uint64_t * p)577 sndio_stream_get_position(cubeb_stream * s, uint64_t * p)
578 {
579 pthread_mutex_lock(&s->mtx);
580 DPR("sndio_stream_get_position() %" PRId64 "\n", s->hwpos);
581 *p = s->hwpos;
582 pthread_mutex_unlock(&s->mtx);
583 return CUBEB_OK;
584 }
585
586 static int
sndio_stream_set_volume(cubeb_stream * s,float volume)587 sndio_stream_set_volume(cubeb_stream * s, float volume)
588 {
589 DPR("sndio_stream_set_volume(%f)\n", volume);
590 pthread_mutex_lock(&s->mtx);
591 if (volume < 0.)
592 volume = 0.;
593 else if (volume > 1.0)
594 volume = 1.;
595 s->volume = volume;
596 pthread_mutex_unlock(&s->mtx);
597 return CUBEB_OK;
598 }
599
600 int
sndio_stream_get_latency(cubeb_stream * stm,uint32_t * latency)601 sndio_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
602 {
603 // http://www.openbsd.org/cgi-bin/man.cgi?query=sio_open
604 // in the "Measuring the latency and buffers usage" paragraph.
605 *latency = stm->swpos - stm->hwpos;
606 return CUBEB_OK;
607 }
608
609 static int
sndio_enumerate_devices(cubeb * context,cubeb_device_type type,cubeb_device_collection * collection)610 sndio_enumerate_devices(cubeb * context, cubeb_device_type type,
611 cubeb_device_collection * collection)
612 {
613 static char dev[] = SIO_DEVANY;
614 cubeb_device_info * device;
615
616 device = malloc(sizeof(cubeb_device_info));
617 if (device == NULL)
618 return CUBEB_ERROR;
619
620 device->devid = dev; /* passed to stream_init() */
621 device->device_id = dev; /* printable in UI */
622 device->friendly_name = dev; /* same, but friendly */
623 device->group_id = dev; /* actual device if full-duplex */
624 device->vendor_name = NULL; /* may be NULL */
625 device->type = type; /* Input/Output */
626 device->state = CUBEB_DEVICE_STATE_ENABLED;
627 device->preferred = CUBEB_DEVICE_PREF_ALL;
628 device->format = CUBEB_DEVICE_FMT_S16NE;
629 device->default_format = CUBEB_DEVICE_FMT_S16NE;
630 device->max_channels = (type == CUBEB_DEVICE_TYPE_INPUT) ? 2 : 8;
631 device->default_rate = 48000;
632 device->min_rate = 4000;
633 device->max_rate = 192000;
634 device->latency_lo = 480;
635 device->latency_hi = 9600;
636 collection->device = device;
637 collection->count = 1;
638 return CUBEB_OK;
639 }
640
641 static int
sndio_device_collection_destroy(cubeb * context,cubeb_device_collection * collection)642 sndio_device_collection_destroy(cubeb * context,
643 cubeb_device_collection * collection)
644 {
645 free(collection->device);
646 return CUBEB_OK;
647 }
648
649 static struct cubeb_ops const sndio_ops = {
650 .init = sndio_init,
651 .get_backend_id = sndio_get_backend_id,
652 .get_max_channel_count = sndio_get_max_channel_count,
653 .get_min_latency = sndio_get_min_latency,
654 .get_preferred_sample_rate = sndio_get_preferred_sample_rate,
655 .enumerate_devices = sndio_enumerate_devices,
656 .device_collection_destroy = sndio_device_collection_destroy,
657 .destroy = sndio_destroy,
658 .stream_init = sndio_stream_init,
659 .stream_destroy = sndio_stream_destroy,
660 .stream_start = sndio_stream_start,
661 .stream_stop = sndio_stream_stop,
662 .stream_get_position = sndio_stream_get_position,
663 .stream_get_latency = sndio_stream_get_latency,
664 .stream_set_volume = sndio_stream_set_volume,
665 .stream_set_name = NULL,
666 .stream_get_current_device = NULL,
667 .stream_device_destroy = NULL,
668 .stream_register_device_changed_callback = NULL,
669 .register_device_collection_changed = NULL};
670