Skip to content

Commit

Permalink
virtio-sound: implement audio capture (RX)
Browse files Browse the repository at this point in the history
To perform audio capture we duplicate the TX logic of the previous
commit with the following difference: we receive data from the QEMU
audio backend and write it in the virt queue IO buffers the guest sends
to QEMU. When they are full (i.e. they have `period_bytes` amount of
data) or when recording stops in QEMU's audio backend, the buffer is
returned to the guest by notifying it.

Signed-off-by: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
Tested-by: Alex Bennée <alex.bennee@linaro.org>
Reviewed-by: Alex Bennée <alex.bennee@linaro.org>
Message-Id: <e56a17741a24ccadfbbea19d3c60c9406b795b23.1698062525.git.manos.pitsidianakis@linaro.org>
Acked-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
  • Loading branch information
epilys authored and mstsirkin committed Nov 7, 2023
1 parent 18a7528 commit d8d64ac
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 35 deletions.
3 changes: 2 additions & 1 deletion hw/audio/trace-events
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ virtio_snd_handle_code(uint32_t val, const char *code) "ctrl code msg val = %"PR
virtio_snd_handle_chmap_info(void) "VIRTIO_SND_CHMAP_INFO called"
virtio_snd_handle_event(void) "event queue callback called"
virtio_snd_pcm_stream_flush(uint32_t stream) "flushing stream %"PRIu32
virtio_snd_handle_xfer(void) "tx/rx queue callback called"
virtio_snd_handle_tx_xfer(void) "tx queue callback called"
virtio_snd_handle_rx_xfer(void) "rx queue callback called"
262 changes: 228 additions & 34 deletions hw/audio/virtio-snd.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@

#define VIRTIO_SOUND_VM_VERSION 1
#define VIRTIO_SOUND_JACK_DEFAULT 0
#define VIRTIO_SOUND_STREAM_DEFAULT 1
#define VIRTIO_SOUND_STREAM_DEFAULT 2
#define VIRTIO_SOUND_CHMAP_DEFAULT 0
#define VIRTIO_SOUND_HDA_FN_NID 0

static void virtio_snd_pcm_out_cb(void *data, int available);
static void virtio_snd_process_cmdq(VirtIOSound *s);
static void virtio_snd_pcm_flush(VirtIOSoundPCMStream *stream);
static void virtio_snd_pcm_in_cb(void *data, int available);

static uint32_t supported_formats = BIT(VIRTIO_SND_PCM_FMT_S8)
| BIT(VIRTIO_SND_PCM_FMT_U8)
Expand Down Expand Up @@ -408,10 +409,13 @@ static void virtio_snd_get_qemu_audsettings(audsettings *as,
static void virtio_snd_pcm_close(VirtIOSoundPCMStream *stream)
{
if (stream) {
virtio_snd_pcm_flush(stream);
if (stream->info.direction == VIRTIO_SND_D_OUTPUT) {
virtio_snd_pcm_flush(stream);
AUD_close_out(&stream->pcm->snd->card, stream->voice.out);
stream->voice.out = NULL;
} else if (stream->info.direction == VIRTIO_SND_D_INPUT) {
AUD_close_in(&stream->pcm->snd->card, stream->voice.in);
stream->voice.in = NULL;
}
}
}
Expand Down Expand Up @@ -482,7 +486,13 @@ static uint32_t virtio_snd_pcm_prepare(VirtIOSound *s, uint32_t stream_id)
&as);
AUD_set_volume_out(stream->voice.out, 0, 255, 255);
} else {
qemu_log_mask(LOG_UNIMP, "virtio_snd: input/capture is unimplemented.");
stream->voice.in = AUD_open_in(&s->card,
stream->voice.in,
"virtio-sound.in",
stream,
virtio_snd_pcm_in_cb,
&as);
AUD_set_volume_in(stream->voice.in, 0, 255, 255);
}

return cpu_to_le32(VIRTIO_SND_S_OK);
Expand Down Expand Up @@ -573,6 +583,8 @@ static void virtio_snd_handle_pcm_start_stop(VirtIOSound *s,
}
if (stream->info.direction == VIRTIO_SND_D_OUTPUT) {
AUD_set_active_out(stream->voice.out, start);
} else {
AUD_set_active_in(stream->voice.in, start);
}
} else {
error_report("Invalid stream id: %"PRIu32, stream_id);
Expand Down Expand Up @@ -814,22 +826,64 @@ static void virtio_snd_handle_event(VirtIODevice *vdev, VirtQueue *vq)
trace_virtio_snd_handle_event();
}

static inline void empty_invalid_queue(VirtIODevice *vdev, VirtQueue *vq)
{
VirtIOSoundPCMBuffer *buffer = NULL;
VirtIOSoundPCMStream *stream = NULL;
virtio_snd_pcm_status resp = { 0 };
VirtIOSound *vsnd = VIRTIO_SND(vdev);
bool any = false;

for (uint32_t i = 0; i < vsnd->snd_conf.streams; i++) {
stream = vsnd->pcm->streams[i];
if (stream) {
any = false;
WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) {
while (!QSIMPLEQ_EMPTY(&stream->invalid)) {
buffer = QSIMPLEQ_FIRST(&stream->invalid);
if (buffer->vq != vq) {
break;
}
any = true;
resp.status = cpu_to_le32(VIRTIO_SND_S_BAD_MSG);
iov_from_buf(buffer->elem->in_sg,
buffer->elem->in_num,
0,
&resp,
sizeof(virtio_snd_pcm_status));
virtqueue_push(vq,
buffer->elem,
sizeof(virtio_snd_pcm_status));
QSIMPLEQ_REMOVE_HEAD(&stream->invalid, entry);
virtio_snd_pcm_buffer_free(buffer);
}
if (any) {
/*
* Notify vq about virtio_snd_pcm_status responses.
* Buffer responses must be notified separately later.
*/
virtio_notify(vdev, vq);
}
}
}
}
}

/*
* The tx virtqueue handler. Makes the buffers available to their respective
* streams for consumption.
*
* @vdev: VirtIOSound device
* @vq: tx virtqueue
*/
static void virtio_snd_handle_tx(VirtIODevice *vdev, VirtQueue *vq)
static void virtio_snd_handle_tx_xfer(VirtIODevice *vdev, VirtQueue *vq)
{
VirtIOSound *s = VIRTIO_SND(vdev);
VirtIOSoundPCMStream *stream = NULL;
VirtIOSoundPCMBuffer *buffer;
VirtQueueElement *elem;
size_t msg_sz, size;
virtio_snd_pcm_xfer hdr;
virtio_snd_pcm_status resp = { 0 };
uint32_t stream_id;
/*
* If any of the I/O messages are invalid, put them in stream->invalid and
Expand All @@ -840,7 +894,7 @@ static void virtio_snd_handle_tx(VirtIODevice *vdev, VirtQueue *vq)
if (!virtio_queue_ready(vq)) {
return;
}
trace_virtio_snd_handle_xfer();
trace_virtio_snd_handle_tx_xfer();

for (;;) {
elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
Expand Down Expand Up @@ -893,36 +947,88 @@ static void virtio_snd_handle_tx(VirtIODevice *vdev, VirtQueue *vq)
}

if (must_empty_invalid_queue) {
WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) {
while (!QSIMPLEQ_EMPTY(&stream->invalid)) {
buffer = QSIMPLEQ_FIRST(&stream->invalid);

resp.status = cpu_to_le32(VIRTIO_SND_S_BAD_MSG);
iov_from_buf(buffer->elem->in_sg,
buffer->elem->in_num,
0,
&resp,
sizeof(virtio_snd_pcm_status));
virtqueue_push(vq, buffer->elem, sizeof(virtio_snd_pcm_status));
QSIMPLEQ_REMOVE_HEAD(&stream->invalid, entry);
virtio_snd_pcm_buffer_free(buffer);
}
/*
* Notify vq about virtio_snd_pcm_status responses.
* Buffer responses must be notified separately later.
*/
virtio_notify(vdev, vq);
}
empty_invalid_queue(vdev, vq);
}
}

/*
* Stub buffer virtqueue handler.
* The rx virtqueue handler. Makes the buffers available to their respective
* streams for consumption.
*
* @vdev: VirtIOSound device
* @vq: virtqueue
* @vq: rx virtqueue
*/
static void virtio_snd_handle_xfer(VirtIODevice *vdev, VirtQueue *vq) {}
static void virtio_snd_handle_rx_xfer(VirtIODevice *vdev, VirtQueue *vq)
{
VirtIOSound *s = VIRTIO_SND(vdev);
VirtIOSoundPCMStream *stream = NULL;
VirtIOSoundPCMBuffer *buffer;
VirtQueueElement *elem;
size_t msg_sz, size;
virtio_snd_pcm_xfer hdr;
uint32_t stream_id;
/*
* if any of the I/O messages are invalid, put them in stream->invalid and
* return them after the for loop.
*/
bool must_empty_invalid_queue = false;

if (!virtio_queue_ready(vq)) {
return;
}
trace_virtio_snd_handle_rx_xfer();

for (;;) {
elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
if (!elem) {
break;
}
/* get the message hdr object */
msg_sz = iov_to_buf(elem->out_sg,
elem->out_num,
0,
&hdr,
sizeof(virtio_snd_pcm_xfer));
if (msg_sz != sizeof(virtio_snd_pcm_xfer)) {
goto rx_err;
}
stream_id = le32_to_cpu(hdr.stream_id);

if (stream_id >= s->snd_conf.streams
|| !s->pcm->streams[stream_id]) {
goto rx_err;
}

stream = s->pcm->streams[stream_id];
if (stream == NULL || stream->info.direction != VIRTIO_SND_D_INPUT) {
goto rx_err;
}
WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) {
size = iov_size(elem->in_sg, elem->in_num) -
sizeof(virtio_snd_pcm_status);
buffer = g_malloc0(sizeof(VirtIOSoundPCMBuffer) + size);
buffer->elem = elem;
buffer->vq = vq;
buffer->size = 0;
buffer->offset = 0;
QSIMPLEQ_INSERT_TAIL(&stream->queue, buffer, entry);
}
continue;

rx_err:
WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) {
must_empty_invalid_queue = true;
buffer = g_malloc0(sizeof(VirtIOSoundPCMBuffer));
buffer->elem = elem;
buffer->vq = vq;
QSIMPLEQ_INSERT_TAIL(&stream->invalid, buffer, entry);
}
}

if (must_empty_invalid_queue) {
empty_invalid_queue(vdev, vq);
}
}

static uint64_t get_features(VirtIODevice *vdev, uint64_t features,
Error **errp)
Expand Down Expand Up @@ -1010,9 +1116,9 @@ static void virtio_snd_realize(DeviceState *dev, Error **errp)
vsnd->queues[VIRTIO_SND_VQ_EVENT] =
virtio_add_queue(vdev, 64, virtio_snd_handle_event);
vsnd->queues[VIRTIO_SND_VQ_TX] =
virtio_add_queue(vdev, 64, virtio_snd_handle_tx);
virtio_add_queue(vdev, 64, virtio_snd_handle_tx_xfer);
vsnd->queues[VIRTIO_SND_VQ_RX] =
virtio_add_queue(vdev, 64, virtio_snd_handle_xfer);
virtio_add_queue(vdev, 64, virtio_snd_handle_rx_xfer);
qemu_mutex_init(&vsnd->cmdq_mutex);
QTAILQ_INIT(&vsnd->cmdq);

Expand Down Expand Up @@ -1116,19 +1222,107 @@ static void virtio_snd_pcm_out_cb(void *data, int available)
}

/*
* Flush all buffer data from this stream's queue into the driver's virtual
* queue.
* Flush all buffer data from this input stream's queue into the driver's
* virtual queue.
*
* @stream: VirtIOSoundPCMStream *stream
*/
static inline void return_rx_buffer(VirtIOSoundPCMStream *stream,
VirtIOSoundPCMBuffer *buffer)
{
virtio_snd_pcm_status resp = { 0 };
resp.status = cpu_to_le32(VIRTIO_SND_S_OK);
resp.latency_bytes = 0;
/* Copy data -if any- to guest */
iov_from_buf(buffer->elem->in_sg,
buffer->elem->in_num,
0,
buffer->data,
buffer->size);
iov_from_buf(buffer->elem->in_sg,
buffer->elem->in_num,
buffer->size,
&resp,
sizeof(virtio_snd_pcm_status));
virtqueue_push(buffer->vq,
buffer->elem,
sizeof(virtio_snd_pcm_status) + buffer->size);
virtio_notify(VIRTIO_DEVICE(stream->s), buffer->vq);
QSIMPLEQ_REMOVE(&stream->queue,
buffer,
VirtIOSoundPCMBuffer,
entry);
virtio_snd_pcm_buffer_free(buffer);
}


/*
* AUD_* input callback.
*
* @data: VirtIOSoundPCMStream stream
* @available: number of bytes that can be read with AUD_read()
*/
static void virtio_snd_pcm_in_cb(void *data, int available)
{
VirtIOSoundPCMStream *stream = data;
VirtIOSoundPCMBuffer *buffer;
size_t size;

WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) {
while (!QSIMPLEQ_EMPTY(&stream->queue)) {
buffer = QSIMPLEQ_FIRST(&stream->queue);
if (!virtio_queue_ready(buffer->vq)) {
return;
}
if (!stream->active) {
/* Stream has stopped, so do not perform AUD_read. */
return_rx_buffer(stream, buffer);
continue;
}

for (;;) {
size = AUD_read(stream->voice.in,
buffer->data + buffer->size,
MIN(available, (stream->params.period_bytes -
buffer->size)));
if (!size) {
available = 0;
break;
}
buffer->size += size;
available -= size;
if (buffer->size >= stream->params.period_bytes) {
return_rx_buffer(stream, buffer);
break;
}
if (!available) {
break;
}
}
if (!available) {
break;
}
}
}
}

/*
* Flush all buffer data from this output stream's queue into the driver's
* virtual queue.
*
* @stream: VirtIOSoundPCMStream *stream
*/
static inline void virtio_snd_pcm_flush(VirtIOSoundPCMStream *stream)
{
VirtIOSoundPCMBuffer *buffer;
void (*cb)(VirtIOSoundPCMStream *, VirtIOSoundPCMBuffer *) =
(stream->info.direction == VIRTIO_SND_D_OUTPUT) ? return_tx_buffer :
return_rx_buffer;

WITH_QEMU_LOCK_GUARD(&stream->queue_mutex) {
while (!QSIMPLEQ_EMPTY(&stream->queue)) {
buffer = QSIMPLEQ_FIRST(&stream->queue);
return_tx_buffer(stream, buffer);
cb(stream, buffer);
}
}
}
Expand Down

0 comments on commit d8d64ac

Please sign in to comment.