/* * SPDX-License-Identifier: ISC * * Copyright (c) 2019 Alexandre Ratchov <alex@caoua.org> */ /* * TODO : * * Use a single device and open it in full-duplex rather than * opening it twice (once for playback once for recording). * * This is the only way to ensure that playback doesn't drift with respect * to recording, which is what guest systems expect. */ #include <poll.h> #include <sndio.h> #include "qemu/osdep.h" #include "qemu/main-loop.h" #include "audio.h" #include "trace.h" #define AUDIO_CAP "sndio" #include "audio_int.h" /* default latency in microseconds if no option is set */ #define SNDIO_LATENCY_US 50000 typedef struct SndioVoice { union { HWVoiceOut out; HWVoiceIn in; } hw; struct sio_par par; struct sio_hdl *hdl; struct pollfd *pfds; struct pollindex { struct SndioVoice *self; int index; } *pindexes; unsigned char *buf; size_t buf_size; size_t sndio_pos; size_t qemu_pos; unsigned int mode; unsigned int nfds; bool enabled; } SndioVoice; typedef struct SndioConf { const char *devname; unsigned int latency; } SndioConf; /* needed for forward reference */ static void sndio_poll_in(void *arg); static void sndio_poll_out(void *arg); /* * stop polling descriptors */ static void sndio_poll_clear(SndioVoice *self) { struct pollfd *pfd; int i; for (i = 0; i < self->nfds; i++) { pfd = &self->pfds[i]; qemu_set_fd_handler(pfd->fd, NULL, NULL, NULL); } self->nfds = 0; } /* * write data to the device until it blocks or * all of our buffered data is written */ static void sndio_write(SndioVoice *self) { size_t todo, n; todo = self->qemu_pos - self->sndio_pos; /* * transfer data to device, until it blocks */ while (todo > 0) { n = sio_write(self->hdl, self->buf + self->sndio_pos, todo); if (n == 0) { break; } self->sndio_pos += n; todo -= n; } if (self->sndio_pos == self->buf_size) { /* * we complete the block */ self->sndio_pos = 0; self->qemu_pos = 0; } } /* * read data from the device until it blocks or * there no room any longer */ static void sndio_read(SndioVoice *self) { size_t todo, n; todo = self->buf_size - self->sndio_pos; /* * transfer data from the device, until it blocks */ while (todo > 0) { n = sio_read(self->hdl, self->buf + self->sndio_pos, todo); if (n == 0) { break; } self->sndio_pos += n; todo -= n; } } /* * Set handlers for all descriptors libsndio needs to * poll */ static void sndio_poll_wait(SndioVoice *self) { struct pollfd *pfd; int events, i; events = 0; if (self->mode == SIO_PLAY) { if (self->sndio_pos < self->qemu_pos) { events |= POLLOUT; } } else { if (self->sndio_pos < self->buf_size) { events |= POLLIN; } } /* * fill the given array of descriptors with the events sndio * wants, they are different from our 'event' variable because * sndio may use descriptors internally. */ self->nfds = sio_pollfd(self->hdl, self->pfds, events); for (i = 0; i < self->nfds; i++) { pfd = &self->pfds[i]; if (pfd->fd < 0) { continue; } qemu_set_fd_handler(pfd->fd, (pfd->events & POLLIN) ? sndio_poll_in : NULL, (pfd->events & POLLOUT) ? sndio_poll_out : NULL, &self->pindexes[i]); pfd->revents = 0; } } /* * call-back called when one of the descriptors * became readable or writable */ static void sndio_poll_event(SndioVoice *self, int index, int event) { int revents; /* * ensure we're not called twice this cycle */ sndio_poll_clear(self); /* * make self->pfds[] look as we're returning from poll syscal, * this is how sio_revents expects events to be. */ self->pfds[index].revents = event; /* * tell sndio to handle events and return whether we can read or * write without blocking. */ revents = sio_revents(self->hdl, self->pfds); if (self->mode == SIO_PLAY) { if (revents & POLLOUT) { sndio_write(self); } if (self->qemu_pos < self->buf_size) { audio_run(self->hw.out.s, "sndio_out"); } } else { if (revents & POLLIN) { sndio_read(self); } if (self->qemu_pos < self->sndio_pos) { audio_run(self->hw.in.s, "sndio_in"); } } /* * audio_run() may have changed state */ if (self->enabled) { sndio_poll_wait(self); } } /* * return the upper limit of the amount of free play buffer space */ static size_t sndio_buffer_get_free(HWVoiceOut *hw) { SndioVoice *self = (SndioVoice *) hw; return self->buf_size - self->qemu_pos; } /* * return a buffer where data to play can be stored, * its size is stored in the location pointed by the size argument. */ static void *sndio_get_buffer_out(HWVoiceOut *hw, size_t *size) { SndioVoice *self = (SndioVoice *) hw; *size = self->buf_size - self->qemu_pos; return self->buf + self->qemu_pos; } /* * put back to sndio back-end a buffer returned by sndio_get_buffer_out() */ static size_t sndio_put_buffer_out(HWVoiceOut *hw, void *buf, size_t size) { SndioVoice *self = (SndioVoice *) hw; self->qemu_pos += size; sndio_poll_wait(self); return size; } /* * return a buffer from where recorded data is available, * its size is stored in the location pointed by the size argument. * it may not exceed the initial value of "*size". */ static void *sndio_get_buffer_in(HWVoiceIn *hw, size_t *size) { SndioVoice *self = (SndioVoice *) hw; size_t todo, max_todo; /* * unlike the get_buffer_out() method, get_buffer_in() * must return a buffer of at most the given size, see audio.c */ max_todo = *size; todo = self->sndio_pos - self->qemu_pos; if (todo > max_todo) { todo = max_todo; } *size = todo; return self->buf + self->qemu_pos; } /* * discard the given amount of recorded data */ static void sndio_put_buffer_in(HWVoiceIn *hw, void *buf, size_t size) { SndioVoice *self = (SndioVoice *) hw; self->qemu_pos += size; if (self->qemu_pos == self->buf_size) { self->qemu_pos = 0; self->sndio_pos = 0; } sndio_poll_wait(self); } /* * call-back called when one of our descriptors becomes writable */ static void sndio_poll_out(void *arg) { struct pollindex *pindex = (struct pollindex *) arg; sndio_poll_event(pindex->self, pindex->index, POLLOUT); } /* * call-back called when one of our descriptors becomes readable */ static void sndio_poll_in(void *arg) { struct pollindex *pindex = (struct pollindex *) arg; sndio_poll_event(pindex->self, pindex->index, POLLIN); } static void sndio_fini(SndioVoice *self) { if (self->hdl) { sio_close(self->hdl); self->hdl = NULL; } g_free(self->pfds); g_free(self->pindexes); g_free(self->buf); } static int sndio_init(SndioVoice *self, struct audsettings *as, int mode, Audiodev *dev) { AudiodevSndioOptions *opts = &dev->u.sndio; unsigned long long latency; const char *dev_name; struct sio_par req; unsigned int nch; int i, nfds; dev_name = opts->dev ?: SIO_DEVANY; latency = opts->has_latency ? opts->latency : SNDIO_LATENCY_US; /* open the device in non-blocking mode */ self->hdl = sio_open(dev_name, mode, 1); if (self->hdl == NULL) { dolog("failed to open device\n"); return -1; } self->mode = mode; sio_initpar(&req); switch (as->fmt) { case AUDIO_FORMAT_S8: req.bits = 8; req.sig = 1; break; case AUDIO_FORMAT_U8: req.bits = 8; req.sig = 0; break; case AUDIO_FORMAT_S16: req.bits = 16; req.sig = 1; break; case AUDIO_FORMAT_U16: req.bits = 16; req.sig = 0; break; case AUDIO_FORMAT_S32: req.bits = 32; req.sig = 1; break; case AUDIO_FORMAT_U32: req.bits = 32; req.sig = 0; break; default: dolog("unknown audio sample format\n"); return -1; } if (req.bits > 8) { req.le = as->endianness ? 0 : 1; } req.rate = as->freq; if (mode == SIO_PLAY) { req.pchan = as->nchannels; } else { req.rchan = as->nchannels; } /* set on-device buffer size */ req.appbufsz = req.rate * latency / 1000000; if (!sio_setpar(self->hdl, &req)) { dolog("failed set audio params\n"); goto fail; } if (!sio_getpar(self->hdl, &self->par)) { dolog("failed get audio params\n"); goto fail; } nch = (mode == SIO_PLAY) ? self->par.pchan : self->par.rchan; /* * With the default setup, sndio supports any combination of parameters * so these checks are mostly to catch configuration errors. */ if (self->par.bits != req.bits || self->par.bps != req.bits / 8 || self->par.sig != req.sig || (req.bits > 8 && self->par.le != req.le) || self->par.rate != as->freq || nch != as->nchannels) { dolog("unsupported audio params\n"); goto fail; } /* * we use one block as buffer size; this is how * transfers get well aligned */ self->buf_size = self->par.round * self->par.bps * nch; self->buf = g_malloc(self->buf_size); if (self->buf == NULL) { dolog("failed to allocate audio buffer\n"); goto fail; } nfds = sio_nfds(self->hdl); self->pfds = g_malloc_n(nfds, sizeof(struct pollfd)); if (self->pfds == NULL) { dolog("failed to allocate pollfd structures\n"); goto fail; } self->pindexes = g_malloc_n(nfds, sizeof(struct pollindex)); if (self->pindexes == NULL) { dolog("failed to allocate pollindex structures\n"); goto fail; } for (i = 0; i < nfds; i++) { self->pindexes[i].self = self; self->pindexes[i].index = i; } return 0; fail: sndio_fini(self); return -1; } static void sndio_enable(SndioVoice *self, bool enable) { if (enable) { sio_start(self->hdl); self->enabled = true; sndio_poll_wait(self); } else { self->enabled = false; sndio_poll_clear(self); sio_stop(self->hdl); } } static void sndio_enable_out(HWVoiceOut *hw, bool enable) { SndioVoice *self = (SndioVoice *) hw; sndio_enable(self, enable); } static void sndio_enable_in(HWVoiceIn *hw, bool enable) { SndioVoice *self = (SndioVoice *) hw; sndio_enable(self, enable); } static int sndio_init_out(HWVoiceOut *hw, struct audsettings *as, void *opaque) { SndioVoice *self = (SndioVoice *) hw; if (sndio_init(self, as, SIO_PLAY, opaque) == -1) { return -1; } audio_pcm_init_info(&hw->info, as); hw->samples = self->par.round; return 0; } static int sndio_init_in(HWVoiceIn *hw, struct audsettings *as, void *opaque) { SndioVoice *self = (SndioVoice *) hw; if (sndio_init(self, as, SIO_REC, opaque) == -1) { return -1; } audio_pcm_init_info(&hw->info, as); hw->samples = self->par.round; return 0; } static void sndio_fini_out(HWVoiceOut *hw) { SndioVoice *self = (SndioVoice *) hw; sndio_fini(self); } static void sndio_fini_in(HWVoiceIn *hw) { SndioVoice *self = (SndioVoice *) hw; sndio_fini(self); } static void *sndio_audio_init(Audiodev *dev) { assert(dev->driver == AUDIODEV_DRIVER_SNDIO); return dev; } static void sndio_audio_fini(void *opaque) { } static struct audio_pcm_ops sndio_pcm_ops = { .init_out = sndio_init_out, .fini_out = sndio_fini_out, .enable_out = sndio_enable_out, .write = audio_generic_write, .buffer_get_free = sndio_buffer_get_free, .get_buffer_out = sndio_get_buffer_out, .put_buffer_out = sndio_put_buffer_out, .init_in = sndio_init_in, .fini_in = sndio_fini_in, .read = audio_generic_read, .enable_in = sndio_enable_in, .get_buffer_in = sndio_get_buffer_in, .put_buffer_in = sndio_put_buffer_in, }; static struct audio_driver sndio_audio_driver = { .name = "sndio", .descr = "sndio https://sndio.org", .init = sndio_audio_init, .fini = sndio_audio_fini, .pcm_ops = &sndio_pcm_ops, .can_be_default = 1, .max_voices_out = INT_MAX, .max_voices_in = INT_MAX, .voice_size_out = sizeof(SndioVoice), .voice_size_in = sizeof(SndioVoice) }; static void register_audio_sndio(void) { audio_driver_register(&sndio_audio_driver); } type_init(register_audio_sndio);