audio: Add sndio backend

sndio is the native API used by OpenBSD, although it has been ported to
other *BSD's and Linux (packages for Ubuntu, Debian, Void, Arch, etc.).

Signed-off-by: Brad Smith <brad@comstyle.com>
Signed-off-by: Alexandre Ratchov <alex@caoua.org>
Reviewed-by: Volker Rümelin <vr_qemu@t-online.de>
Tested-by: Volker Rümelin <vr_qemu@t-online.de>
Message-Id: <YxibXrWsrS3XYQM3@vm1.arverb.com>
Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
This commit is contained in:
Alexandre Ratchov 2022-09-07 15:23:42 +02:00 committed by Gerd Hoffmann
parent f3def4dd42
commit 663df1cc68
10 changed files with 632 additions and 5 deletions

View file

@ -2438,6 +2438,7 @@ X: audio/jackaudio.c
X: audio/ossaudio.c
X: audio/paaudio.c
X: audio/sdlaudio.c
X: audio/sndioaudio.c
X: audio/spiceaudio.c
F: qapi/audio.json
@ -2482,6 +2483,12 @@ R: Thomas Huth <huth@tuxfamily.org>
S: Odd Fixes
F: audio/sdlaudio.c
Sndio Audio backend
M: Gerd Hoffmann <kraxel@redhat.com>
R: Alexandre Ratchov <alex@caoua.org>
S: Odd Fixes
F: audio/sndioaudio.c
Block layer core
M: Kevin Wolf <kwolf@redhat.com>
M: Hanna Reitz <hreitz@redhat.com>

View file

@ -2030,6 +2030,7 @@ void audio_create_pdos(Audiodev *dev)
CASE(OSS, oss, Oss);
CASE(PA, pa, Pa);
CASE(SDL, sdl, Sdl);
CASE(SNDIO, sndio, );
CASE(SPICE, spice, );
CASE(WAV, wav, );

View file

@ -336,6 +336,8 @@ AudiodevPerDirectionOptions *glue(audio_get_pdo_, TYPE)(Audiodev *dev)
return qapi_AudiodevPaPerDirectionOptions_base(dev->u.pa.TYPE);
case AUDIODEV_DRIVER_SDL:
return qapi_AudiodevSdlPerDirectionOptions_base(dev->u.sdl.TYPE);
case AUDIODEV_DRIVER_SNDIO:
return dev->u.sndio.TYPE;
case AUDIODEV_DRIVER_SPICE:
return dev->u.spice.TYPE;
case AUDIODEV_DRIVER_WAV:

View file

@ -17,6 +17,7 @@ foreach m : [
['pa', pulse, files('paaudio.c')],
['sdl', sdl, files('sdlaudio.c')],
['jack', jack, files('jackaudio.c')],
['sndio', sndio, files('sndioaudio.c')],
['spice', spice, files('spiceaudio.c')]
]
if m[1].found()

565
audio/sndioaudio.c Normal file
View file

@ -0,0 +1,565 @@
/*
* 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->has_dev ? 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);

View file

@ -675,6 +675,11 @@ if not get_option('jack').auto() or have_system
jack = dependency('jack', required: get_option('jack'),
method: 'pkg-config', kwargs: static_kwargs)
endif
sndio = not_found
if not get_option('sndio').auto() or have_system
sndio = dependency('sndio', required: get_option('sndio'),
method: 'pkg-config', kwargs: static_kwargs)
endif
spice_protocol = not_found
if not get_option('spice_protocol').auto() or have_system
@ -1591,6 +1596,7 @@ if have_system
'oss': oss.found(),
'pa': pulse.found(),
'sdl': sdl.found(),
'sndio': sndio.found(),
}
foreach k, v: audio_drivers_available
config_host_data.set('CONFIG_AUDIO_' + k.to_upper(), v)
@ -1598,7 +1604,7 @@ if have_system
# Default to native drivers first, OSS second, SDL third
audio_drivers_priority = \
[ 'pa', 'coreaudio', 'dsound', 'oss' ] + \
[ 'pa', 'coreaudio', 'dsound', 'sndio', 'oss' ] + \
(targetos == 'linux' ? [] : [ 'sdl' ])
audio_drivers_default = []
foreach k: audio_drivers_priority
@ -3922,6 +3928,7 @@ if vnc.found()
endif
if targetos not in ['darwin', 'haiku', 'windows']
summary_info += {'OSS support': oss}
summary_info += {'sndio support': sndio}
elif targetos == 'darwin'
summary_info += {'CoreAudio support': coreaudio}
elif targetos == 'windows'

View file

@ -21,7 +21,7 @@ option('tls_priority', type : 'string', value : 'NORMAL',
option('default_devices', type : 'boolean', value : true,
description: 'Include a default selection of devices in emulators')
option('audio_drv_list', type: 'array', value: ['default'],
choices: ['alsa', 'coreaudio', 'default', 'dsound', 'jack', 'oss', 'pa', 'sdl'],
choices: ['alsa', 'coreaudio', 'default', 'dsound', 'jack', 'oss', 'pa', 'sdl', 'sndio'],
description: 'Set audio driver list')
option('block_drv_rw_whitelist', type : 'string', value : '',
description: 'set block driver read-write whitelist (by default affects only QEMU, not tools like qemu-img)')
@ -240,6 +240,8 @@ option('oss', type: 'feature', value: 'auto',
description: 'OSS sound support')
option('pa', type: 'feature', value: 'auto',
description: 'PulseAudio sound support')
option('sndio', type: 'feature', value: 'auto',
description: 'sndio sound support')
option('vhost_kernel', type: 'feature', value: 'auto',
description: 'vhost kernel backend support')

View file

@ -106,6 +106,28 @@
'*out': 'AudiodevAlsaPerDirectionOptions',
'*threshold': 'uint32' } }
##
# @AudiodevSndioOptions:
#
# Options of the sndio audio backend.
#
# @in: options of the capture stream
#
# @out: options of the playback stream
#
# @dev: the name of the sndio device to use (default 'default')
#
# @latency: play buffer size (in microseconds)
#
# Since: 7.2
##
{ 'struct': 'AudiodevSndioOptions',
'data': {
'*in': 'AudiodevPerDirectionOptions',
'*out': 'AudiodevPerDirectionOptions',
'*dev': 'str',
'*latency': 'uint32'} }
##
# @AudiodevCoreaudioPerDirectionOptions:
#
@ -387,7 +409,7 @@
##
{ 'enum': 'AudiodevDriver',
'data': [ 'none', 'alsa', 'coreaudio', 'dbus', 'dsound', 'jack', 'oss', 'pa',
'sdl', 'spice', 'wav' ] }
'sdl', 'sndio', 'spice', 'wav' ] }
##
# @Audiodev:
@ -418,5 +440,6 @@
'oss': 'AudiodevOssOptions',
'pa': 'AudiodevPaOptions',
'sdl': 'AudiodevSdlOptions',
'sndio': 'AudiodevSndioOptions',
'spice': 'AudiodevGenericOptions',
'wav': 'AudiodevWavOptions' } }

View file

@ -769,6 +769,9 @@ DEF("audiodev", HAS_ARG, QEMU_OPTION_audiodev,
"-audiodev sdl,id=id[,prop[=value][,...]]\n"
" in|out.buffer-count= number of buffers\n"
#endif
#ifdef CONFIG_AUDIO_SNDIO
"-audiodev sndio,id=id[,prop[=value][,...]]\n"
#endif
#ifdef CONFIG_SPICE
"-audiodev spice,id=id[,prop[=value][,...]]\n"
#endif
@ -935,6 +938,19 @@ SRST
``in|out.buffer-count=count``
Sets the count of the buffers.
``-audiodev sndio,id=id[,prop[=value][,...]]``
Creates a backend using SNDIO. This backend is available on
OpenBSD and most other Unix-like systems.
Sndio specific options are:
``in|out.dev=device``
Specify the sndio device to use for input and/or output. Default
is ``default``.
``in|out.latency=usecs``
Sets the desired period length in microseconds.
``-audiodev spice,id=id[,prop[=value][,...]]``
Creates a backend that sends audio through SPICE. This backend
requires ``-spice`` and automatically selected in that case, so

View file

@ -1,7 +1,7 @@
# This file is generated by meson-buildoptions.py, do not edit!
meson_options_help() {
printf "%s\n" ' --audio-drv-list=CHOICES Set audio driver list [default] (choices:'
printf "%s\n" ' alsa/coreaudio/default/dsound/jack/oss/pa/sdl)'
printf "%s\n" ' --audio-drv-list=CHOICES Set audio driver list [default] (choices: alsa/co'
printf "%s\n" ' reaudio/default/dsound/jack/oss/pa/sdl/sndio)'
printf "%s\n" ' --block-drv-ro-whitelist=VALUE'
printf "%s\n" ' set block driver read-only whitelist (by default'
printf "%s\n" ' affects only QEMU, not tools like qemu-img)'
@ -144,6 +144,7 @@ meson_options_help() {
printf "%s\n" ' slirp-smbd use smbd (at path --smbd=*) in slirp networking'
printf "%s\n" ' smartcard CA smartcard emulation support'
printf "%s\n" ' snappy snappy compression support'
printf "%s\n" ' sndio sndio sound support'
printf "%s\n" ' sparse sparse checker'
printf "%s\n" ' spice Spice server support'
printf "%s\n" ' spice-protocol Spice protocol support'
@ -393,6 +394,8 @@ _meson_option_parse() {
--disable-smartcard) printf "%s" -Dsmartcard=disabled ;;
--enable-snappy) printf "%s" -Dsnappy=enabled ;;
--disable-snappy) printf "%s" -Dsnappy=disabled ;;
--enable-sndio) printf "%s" -Dsndio=enabled ;;
--disable-sndio) printf "%s" -Dsndio=disabled ;;
--enable-sparse) printf "%s" -Dsparse=enabled ;;
--disable-sparse) printf "%s" -Dsparse=disabled ;;
--sphinx-build=*) quote_sh "-Dsphinx_build=$2" ;;