| /* |
| * 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); |