From 71e1db194cccb7c0625f67c847b7100b84949ee1 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Fri, 5 Sep 2025 12:24:35 +0800 Subject: [PATCH] Implement VirtIO sound device capture Implement VirtIO sound device capture. Due to 'semu' emulation part thus 'semu' cannot send or receive PCM frames in time, the capture feature usually doesn't work. --- .ci/test-sound.sh | 62 +++++ .github/workflows/main.yml | 8 + README.md | 9 +- virtio-snd.c | 507 ++++++++++++++++++++++++++++++------- 4 files changed, 493 insertions(+), 93 deletions(-) create mode 100755 .ci/test-sound.sh diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh new file mode 100755 index 00000000..71bca879 --- /dev/null +++ b/.ci/test-sound.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Source common functions and settings +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export SCRIPT_DIR +source "${SCRIPT_DIR}/common.sh" + +SAMPLE_SOUND="/usr/share/sounds/alsa/Front_Center.wav" + +# Override timeout for sound tests +# Sound tests need different timeout: 30s for Linux, 900s for macOS +case "${OS_TYPE}" in + Darwin) + TIMEOUT=900 + ;; + Linux) + TIMEOUT=30 + ;; + *) + TIMEOUT=30 + ;; +esac + +test_sound() { + ASSERT expect < /dev/null\\n" } timeout { exit 3 } + expect "# " { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n" } timeout { exit 4 } + expect "# " { send "aplay -L\\n" } + expect "# " { } +DONE + echo "✓ sound test passed" +} + +# Clean up any existing semu processes before starting tests +cleanup + +# Test sound device +test_sound + +ret="$?" + +MESSAGES=("OK!" \ + "Fail to boot" \ + "Fail to login" \ + "Playback fails" \ + "Capture fails" \ +) + +if [ "$ret" -eq 0 ]; then + print_success "${MESSAGES["$ret"]}" +else + print_error "${MESSAGES["$ret"]}" +fi + +exit "$ret" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0f45418d..a091bc22 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,6 +69,10 @@ jobs: run: sudo .ci/test-netdev.sh shell: bash timeout-minutes: 10 + - name: sound test + run: .ci/test-sound.sh + shell: bash + timeout-minutes: 5 semu-macOS: runs-on: macos-latest @@ -118,6 +122,10 @@ jobs: shell: bash timeout-minutes: 20 if: ${{ success() }} + - name: sound test + run: .ci/test-sound.sh + shell: bash + timeout-minutes: 20 coding_style: runs-on: ubuntu-24.04 diff --git a/README.md b/README.md index c773bcf4..3187c08c 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,13 @@ A minimalist RISC-V system emulator capable of running Linux the kernel and corr - Three types of I/O support using VirtIO standard: - virtio-blk acquires disk image from the host. - virtio-net is mapped as TAP interface. - - virtio-snd uses [PortAudio](https://github.com/PortAudio/portaudio) for sound playback on the host with one limitations: - - As some unknown issues in guest Linux OS (confirmed in v6.7 and v6.12), you need - to adjust the buffer size to more than four times of period size, or - the program cannot write the PCM frames into guest OS ALSA stack. + - virtio-snd uses [PortAudio](https://github.com/PortAudio/portaudio) for sound playback and capture on the host with one limitation: + - Due to the emulation part, `semu` cannot send/receive PCM frames in time, causing + the ALSA stack will get stuck in XRUN state until you reboot `semu`. + - For playback, you can try to adjust the buffer size to more than four times of period size. - For instance, the following buffer/period size settings on `aplay` has been tested with broken and stutter effects yet complete with no any errors: `aplay --buffer-size=32768 --period-size=4096 /usr/share/sounds/alsa/Front_Center.wav`. + - For capture, ALSA usually gets stuck in XRUN state, so you may need to try multiple times. ## Prerequisites diff --git a/virtio-snd.c b/virtio-snd.c index efdbb503..0f23bdd5 100644 --- a/virtio-snd.c +++ b/virtio-snd.c @@ -13,7 +13,7 @@ #include "utils.h" #include "virtio.h" -#define VSND_DEV_CNT_MAX 1 +#define VSND_DEV_CNT_MAX 2 #define VSND_QUEUE_NUM_MAX 1024 #define vsndq (vsnd->queues[vsnd->QueueSel]) @@ -297,15 +297,61 @@ typedef struct { // PCM frame doubly-ended queue vsnd_buf_queue_node_t buf; struct list_head buf_queue_head; - // PCM frame intermediate buffer; + // PCM frame intermediate buffer void *intermediate; + uint32_t buf_sz; + uint32_t buf_idx; // playback control vsnd_stream_sel_t v; } virtio_snd_prop_t; +#define VIRTIO_SND_JACK_DEFAULT_CONFIG \ + .hdr.hda_fn_nid = 0, .features = 0, .hda_reg_defconf = 0, \ + .hda_reg_caps = 0, .connected = 1, + static virtio_snd_config_t vsnd_configs[VSND_DEV_CNT_MAX]; static virtio_snd_prop_t vsnd_props[VSND_DEV_CNT_MAX] = { + [0].j = {VIRTIO_SND_JACK_DEFAULT_CONFIG}, + [1].j = {VIRTIO_SND_JACK_DEFAULT_CONFIG}, + [0].p = + { + .hdr.hda_fn_nid = 0, + .features = 0, + .formats = (1 << VIRTIO_SND_PCM_FMT_S16), +#define _(rate) (1 << VIRTIO_SND_PCM_RATE_##rate) | + .rates = (SND_PCM_RATE 0), +#undef _ + .direction = VIRTIO_SND_D_OUTPUT, + .channels_min = 1, + .channels_max = 1, + }, + [1].p = + { + .hdr.hda_fn_nid = 0, + .features = 0, + .formats = (1 << VIRTIO_SND_PCM_FMT_S16), +#define _(rate) (1 << VIRTIO_SND_PCM_RATE_##rate) | + .rates = (SND_PCM_RATE 0), +#undef _ + .direction = VIRTIO_SND_D_INPUT, + .channels_min = 1, + .channels_max = 1, + }, + [0].c = + { + .hdr.hda_fn_nid = 0, + .direction = VIRTIO_SND_D_OUTPUT, + .channels = 1, + .positions[0] = VIRTIO_SND_CHMAP_MONO, + }, + [1].c = + { + .hdr.hda_fn_nid = 0, + .direction = VIRTIO_SND_D_INPUT, + .channels = 1, + .positions[0] = VIRTIO_SND_CHMAP_MONO, + }, [0 ... VSND_DEV_CNT_MAX - 1].pp.hdr.hdr.code = VIRTIO_SND_R_PCM_SET_PARAMS, [0 ... VSND_DEV_CNT_MAX - 1].lock = { @@ -316,9 +362,15 @@ static virtio_snd_prop_t vsnd_props[VSND_DEV_CNT_MAX] = { }; static int vsnd_dev_cnt = 0; -static pthread_mutex_t virtio_snd_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t virtio_snd_tx_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t virtio_snd_rx_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t virtio_snd_tx_cond = PTHREAD_COND_INITIALIZER; +static pthread_cond_t virtio_snd_rx_cond = PTHREAD_COND_INITIALIZER; static int tx_ev_notify; +static int rx_ev_notify; + +// FIXME: set this variables into each capture stream's structure +static int rx_ev_start; /* vsnd virtq callback type */ typedef int (*vsnd_virtq_cb)(virtio_snd_state_t *, /* vsnd state */ @@ -327,27 +379,40 @@ typedef int (*vsnd_virtq_cb)(virtio_snd_state_t *, /* vsnd state */ uint32_t * /* response length */); /* Forward declaration */ -static int virtio_snd_stream_cb(const void *input, - void *output, - unsigned long frame_cnt, - const PaStreamCallbackTimeInfo *time_info, - PaStreamCallbackFlags status_flags, - void *user_data); +static int virtio_snd_tx_stream_cb(const void *input, + void *output, + unsigned long frame_cnt, + const PaStreamCallbackTimeInfo *time_info, + PaStreamCallbackFlags status_flags, + void *user_data); +static int virtio_snd_rx_stream_cb(const void *input, + void *output, + unsigned long frame_cnt, + const PaStreamCallbackTimeInfo *time_info, + PaStreamCallbackFlags status_flags, + void *user_data); static void virtio_queue_notify_handler(virtio_snd_state_t *vsnd, int index, /* virtq index */ vsnd_virtq_cb cb); static void __virtio_snd_frame_enqueue(void *payload, uint32_t n, uint32_t stream_id); +static void __virtio_snd_rx_frame_enqueue(const void *payload, + uint32_t n, + uint32_t stream_id); +static void __virtio_snd_rx_frame_dequeue(void *out, + uint32_t n, + uint32_t stream_id); typedef struct { struct virtq_desc vq_desc; struct list_head q; } virtq_desc_queue_node_t; -/* Flush only stream_id 0. - * FIXME: let TX queue flushing can select arbitrary stream_id. +/* Flush only default streams (TX: stream_id 0, RX: stream_id 1). + * FIXME: let TX and RX queue to flush can select arbitrary stream_id. */ -static uint32_t flush_stream_id = 0; +static uint32_t flush_tx_stream_id = 0; +static uint32_t flush_rx_stream_id = 1; #define VSND_GEN_TX_QUEUE_HANDLER(NAME_SUFFIX, WRITE) \ static int virtio_snd_tx_desc_##NAME_SUFFIX##_handler( \ @@ -405,7 +470,7 @@ static uint32_t flush_stream_id = 0; (/* enqueue frames */ \ bad_msg_err = stream_id >= VSND_DEV_CNT_MAX ? 1 : 0; \ , /* flush queue */ \ - bad_msg_err = stream_id != flush_stream_id \ + bad_msg_err = stream_id != flush_tx_stream_id \ ? 1 \ : 0; /* select only stream_id 0 */ \ ) goto early_continue; \ @@ -462,6 +527,122 @@ static uint32_t flush_stream_id = 0; VSND_GEN_TX_QUEUE_HANDLER(normal, 1); VSND_GEN_TX_QUEUE_HANDLER(flush, 0); +#define VSND_GEN_RX_QUEUE_HANDLER(NAME_SUFFIX, WRITE) \ + static int virtio_snd_rx_desc_##NAME_SUFFIX##_handler( \ + virtio_snd_state_t *vsnd, const virtio_snd_queue_t *queue, \ + uint32_t desc_idx, uint32_t *plen) \ + { \ + /* A PCM I/O message uses at least 3 virtqueue descriptors to \ + * represent a PCM data of a period size. \ + * The first part contains one descriptor as follows: \ + * struct virtio_snd_pcm_xfer \ + * The second part contains one or more descriptors \ + * representing PCM frames. \ + * the last part contains one descriptor as follows: \ + * struct virtio_snd_pcm_status \ + */ \ + virtq_desc_queue_node_t *node; \ + struct list_head q; \ + INIT_LIST_HEAD(&q); \ + \ + /* Collect the descriptors */ \ + int cnt = 0; \ + for (;;) { \ + /* The size of the `struct virtq_desc` is 4 words */ \ + const uint32_t *desc = \ + &vsnd->ram[queue->QueueDesc + desc_idx * 4]; \ + \ + /* Retrieve the fields of current descriptor */ \ + node = (virtq_desc_queue_node_t *) malloc(sizeof(*node)); \ + node->vq_desc.addr = desc[0]; \ + node->vq_desc.len = desc[2]; \ + node->vq_desc.flags = desc[3]; \ + list_push(&node->q, &q); \ + desc_idx = desc[3] >> 16; /* vq_desc[desc_cnt].next */ \ + \ + cnt++; \ + \ + /* Leave the loop if next-flag is not set */ \ + if (!(desc[3] & VIRTIO_DESC_F_NEXT)) \ + break; \ + } \ + \ + int idx = 0; \ + uint32_t stream_id = 0; /* Explicitly set the stream_id */ \ + uintptr_t base = (uintptr_t) vsnd->ram; \ + uint32_t ret_len = 0; \ + uint8_t bad_msg_err = 0; \ + list_for_each_entry (node, &q, q) { \ + uint32_t addr = node->vq_desc.addr; \ + uint32_t len = node->vq_desc.len; \ + if (idx == 0) { /* the first descriptor */ \ + const virtio_snd_pcm_xfer_t *request = \ + (virtio_snd_pcm_xfer_t *) (base + addr); \ + stream_id = request->stream_id; \ + IIF(WRITE) \ + (/* dequeue frames */ \ + bad_msg_err = stream_id >= VSND_DEV_CNT_MAX ? 1 : 0; \ + , /* flush queue */ \ + bad_msg_err = stream_id != flush_rx_stream_id \ + ? 1 \ + : 0; /* select only stream_id 1 */ \ + ) goto early_continue; \ + } else if (idx == cnt - 1) { /* the last descriptor */ \ + IIF(WRITE) \ + ( /* dequeue frames */ \ + , /* flush queue */ \ + if (bad_msg_err == 1) { \ + fprintf(stderr, "ignore flush stream_id %" PRIu32 "\n", \ + stream_id); \ + goto early_continue; \ + } fprintf(stderr, "flush stream_id %" PRIu32 "\n", \ + stream_id);) virtio_snd_pcm_status_t *response = \ + (virtio_snd_pcm_status_t *) (base + addr); \ + response->status = \ + bad_msg_err ? VIRTIO_SND_S_IO_ERR : VIRTIO_SND_S_OK; \ + response->latency_bytes = ret_len; \ + *plen = sizeof(virtio_snd_pcm_status_t) + ret_len; \ + goto early_continue; \ + } \ + \ + IIF(WRITE) \ + (/* dequeue frames */ \ + void *payload = (void *) (base + addr); \ + if (bad_msg_err != 0) goto early_continue; \ + __virtio_snd_rx_frame_dequeue(payload, len, stream_id); \ + , /* flush queue */ \ + (void) stream_id; \ + /* Suppress unused variable warning. */) ret_len += len; \ + \ + early_continue: \ + idx++; \ + } \ + \ + if (bad_msg_err != 0) \ + goto finally; \ + IIF(WRITE) \ + (/* dequeue frames */ /* Send signal if and only if we consume : 1) a \ + period size of frames 2) the end of the \ + stream. */ \ + virtio_snd_prop_t *props = &vsnd_props[stream_id]; \ + props->lock.buf_ev_notity--; \ + pthread_cond_signal(&props->lock.writable);, /* flush queue */ \ + ) \ + \ + /* Tear down the descriptor list and free space. */ \ + virtq_desc_queue_node_t *tmp = NULL; \ + list_for_each_entry_safe (node, tmp, &q, q) { \ + list_del(&node->q); \ + free(node); \ + } \ + \ + finally: \ + return 0; \ + } + +VSND_GEN_RX_QUEUE_HANDLER(normal, 1); +VSND_GEN_RX_QUEUE_HANDLER(flush, 0); + static void virtio_snd_set_fail(virtio_snd_state_t *vsnd) { vsnd->Status |= VIRTIO_STATUS__DEVICE_NEEDS_RESET; @@ -508,19 +689,13 @@ static void virtio_snd_read_jack_info_handler( { uint32_t cnt = query->count; for (uint32_t i = 0; i < cnt; i++) { - info[i].hdr.hda_fn_nid = 0; - info[i].features = 0; - info[i].hda_reg_defconf = 0; - info[i].hda_reg_caps = 0; - info[i].connected = 1; - memset(&info[i].padding, 0, sizeof(info[i].padding)); - virtio_snd_prop_t *props = &vsnd_props[i]; - props->j.hdr.hda_fn_nid = 0; - props->j.features = 0; - props->j.hda_reg_defconf = 0; - props->j.hda_reg_caps = 0; - props->j.connected = 1; + info[i].hdr.hda_fn_nid = props->j.hdr.hda_fn_nid; + info[i].features = props->j.features; + info[i].hda_reg_defconf = props->j.hda_reg_defconf; + info[i].hda_reg_caps = props->j.hda_reg_caps; + info[i].connected = props->j.connected; + memset(&info[i].padding, 0, sizeof(info[i].padding)); memset(&props->j.padding, 0, sizeof(props->j.padding)); } @@ -534,29 +709,15 @@ static void virtio_snd_read_pcm_info_handler( { uint32_t cnt = query->count; for (uint32_t i = 0; i < cnt; i++) { - info[i].hdr.hda_fn_nid = 0; - info[i].features = 0; - info[i].formats = (1 << VIRTIO_SND_PCM_FMT_S16); - - info[i].rates = 0; -#define _(rate) info[i].rates |= (1 << VIRTIO_SND_PCM_RATE_##rate); - SND_PCM_RATE -#undef _ - info[i].direction = VIRTIO_SND_D_OUTPUT; - info[i].channels_min = 1; - info[i].channels_max = 1; - memset(&info[i].padding, 0, sizeof(info[i].padding)); - virtio_snd_prop_t *props = &vsnd_props[i]; - props->p.hdr.hda_fn_nid = 0; - props->p.features = 0; - props->p.formats = (1 << VIRTIO_SND_PCM_FMT_S16); -#define _(rate) props->p.rates |= (1 << VIRTIO_SND_PCM_RATE_##rate); - SND_PCM_RATE -#undef _ - props->p.direction = VIRTIO_SND_D_OUTPUT; - props->p.channels_min = 1; - props->p.channels_max = 1; + info[i].hdr.hda_fn_nid = props->p.hdr.hda_fn_nid; + info[i].features = props->p.features; + info[i].formats = props->p.formats; + info[i].rates = props->p.rates; + info[i].direction = props->p.direction; + info[i].channels_min = props->p.channels_min; + info[i].channels_max = props->p.channels_max; + memset(&info[i].padding, 0, sizeof(info[i].padding)); memset(&props->p.padding, 0, sizeof(props->p.padding)); } *plen = cnt * sizeof(*info); @@ -569,16 +730,11 @@ static void virtio_snd_read_chmap_info_handler( { uint32_t cnt = query->count; for (uint32_t i = 0; i < cnt; i++) { - info[i].hdr.hda_fn_nid = 0; - info[i].direction = VIRTIO_SND_D_OUTPUT; - info[i].channels = 1; - info[i].positions[0] = VIRTIO_SND_CHMAP_MONO; - virtio_snd_prop_t *props = &vsnd_props[i]; - props->c.hdr.hda_fn_nid = 0; - props->c.direction = VIRTIO_SND_D_OUTPUT; - props->c.channels = 1; - props->c.positions[0] = VIRTIO_SND_CHMAP_MONO; + info[i].hdr.hda_fn_nid = props->c.hdr.hda_fn_nid; + info[i].direction = props->c.direction; + info[i].channels = props->c.channels; + info[i].positions[0] = props->c.positions[0]; } *plen = cnt * sizeof(info); } @@ -650,27 +806,60 @@ static void virtio_snd_read_pcm_prepare(const virtio_snd_pcm_hdr_t *query, uint32_t cnfa_period_bytes = bps_rate / 10; /* Calculate the period size (in frames) for CNFA . */ uint32_t cnfa_period_frames = cnfa_period_bytes / VSND_CNFA_FRAME_SZ; + /* Get the number of buffer multiplier */ + uint32_t mul = props->pp.buffer_bytes / props->pp.period_bytes; INIT_LIST_HEAD(&props->buf_queue_head); - props->intermediate = - (void *) malloc(sizeof(*props->intermediate) * cnfa_period_bytes); - PaStreamParameters params = { - .device = Pa_GetDefaultOutputDevice(), - .channelCount = props->pp.channels, - .sampleFormat = paInt16, - .suggestedLatency = 0.1, /* 100 ms */ - .hostApiSpecificStreamInfo = NULL, - }; - PaError err = Pa_OpenStream(&props->pa_stream, NULL, /* no input */ - ¶ms, rate, cnfa_period_frames, paClipOff, - virtio_snd_stream_cb, &props->v); - if (err != paNoError) { - fprintf(stderr, "Cannot create PortAudio\n"); - printf("PortAudio error: %s\n", Pa_GetErrorText(err)); - return; + props->buf_sz = cnfa_period_bytes * mul; + props->buf_idx = 0; + uint32_t sz = sizeof(*props->intermediate) * props->buf_sz; + uint32_t dir = props->p.direction; + PaError err = paNoError; + if (dir == VIRTIO_SND_D_OUTPUT) { + PaStreamParameters params = { + .device = Pa_GetDefaultOutputDevice(), + .channelCount = props->pp.channels, + .sampleFormat = paInt16, /* FIXME: set mapping of format */ + .suggestedLatency = 0.1, /* 100 ms */ + .hostApiSpecificStreamInfo = NULL, + }; + props->intermediate = (void *) malloc(sz); + err = Pa_OpenStream(&props->pa_stream, NULL, /* no input */ + ¶ms, rate, cnfa_period_frames, paClipOff, + virtio_snd_tx_stream_cb, &props->v); + if (err != paNoError) + goto pa_err; + } else if (dir == VIRTIO_SND_D_INPUT) { + PaStreamParameters params = { + .device = Pa_GetDefaultInputDevice(), + .channelCount = props->pp.channels, + .sampleFormat = paInt16, /* FIXME: set mapping of format */ + .hostApiSpecificStreamInfo = NULL, + }; + /* Change 'period_bytes' and 'sz' to the suggestion mentioned by the + * driver. */ + cnfa_period_frames = props->pp.period_bytes / VSND_CNFA_FRAME_SZ; + sz = sizeof(*props->intermediate) * props->pp.buffer_bytes; + props->intermediate = (void *) malloc(sz); + err = Pa_OpenStream(&props->pa_stream, ¶ms, NULL /* no output */, + rate, cnfa_period_frames, paClipOff, + virtio_snd_rx_stream_cb, &props->v); + if (err != paNoError) + goto pa_err; + pthread_mutex_lock(&props->lock.lock); + rx_ev_start = 0; + pthread_mutex_unlock(&props->lock.lock); } *plen = 0; + + return; + +pa_err: + free(props->intermediate); + props->intermediate = NULL; + fprintf(stderr, "Cannot create PortAudio\n"); + printf("PortAudio error: %s\n", Pa_GetErrorText(err)); } static void virtio_snd_read_pcm_start(const virtio_snd_pcm_hdr_t *query, @@ -696,6 +885,11 @@ static void virtio_snd_read_pcm_start(const virtio_snd_pcm_hdr_t *query, printf("PortAudio error: %s\n", Pa_GetErrorText(err)); return; } + if (props->p.direction == VIRTIO_SND_D_INPUT) { + pthread_mutex_lock(&props->lock.lock); + rx_ev_start = 1; + pthread_mutex_unlock(&props->lock.lock); + } *plen = 0; } @@ -723,6 +917,11 @@ static void virtio_snd_read_pcm_stop(const virtio_snd_pcm_hdr_t *query, printf("PortAudio error: %s\n", Pa_GetErrorText(err)); return; } + if (props->p.direction == VIRTIO_SND_D_INPUT) { + pthread_mutex_lock(&props->lock.lock); + rx_ev_start = 0; + pthread_mutex_unlock(&props->lock.lock); + } *plen = 0; } @@ -778,7 +977,10 @@ static void virtio_snd_read_pcm_release(const virtio_snd_pcm_hdr_t *query, * - The device MUST NOT complete the control request while there * are pending I/O messages for the specified stream ID. */ - virtio_queue_notify_handler(vsnd, 2, virtio_snd_tx_desc_flush_handler); + if (props->p.direction == VIRTIO_SND_D_OUTPUT) + virtio_queue_notify_handler(vsnd, 2, virtio_snd_tx_desc_flush_handler); + else if (props->p.direction == VIRTIO_SND_D_INPUT) + virtio_queue_notify_handler(vsnd, 3, virtio_snd_rx_desc_flush_handler); *plen = 0; } @@ -809,6 +1011,8 @@ static void __virtio_snd_frame_dequeue(void *out, written_bytes += len; node->pos += len; + + /* FIXME: free the PCM frame nodes */ if (node->pos >= node->len) list_del(&node->q); } @@ -819,13 +1023,53 @@ static void __virtio_snd_frame_dequeue(void *out, pthread_mutex_unlock(&props->lock.lock); } -static int virtio_snd_stream_cb(const void *input, - void *output, - unsigned long frame_cnt, - const PaStreamCallbackTimeInfo *time_info, - PaStreamCallbackFlags status_flags, - void *user_data) +static void __virtio_snd_rx_frame_dequeue(void *out, + uint32_t n, + uint32_t stream_id) +{ + virtio_snd_prop_t *props = &vsnd_props[stream_id]; + + pthread_mutex_lock(&props->lock.lock); + while (!(props->lock.buf_ev_notity > 0 && rx_ev_start == 1)) + pthread_cond_wait(&props->lock.readable, &props->lock.lock); + + /* Get the PCM frames from queue */ + uint32_t written_bytes = 0; + while (!list_empty(&props->buf_queue_head) && written_bytes < n) { + vsnd_buf_queue_node_t *node = + list_first_entry(&props->buf_queue_head, vsnd_buf_queue_node_t, q); + uint32_t left = n - written_bytes; + uint32_t actual = node->len - node->pos; + uint32_t len = + left < actual ? left : actual; /* Naive min implementation */ + + memcpy(out + written_bytes, node->addr + node->pos, len); + + written_bytes += len; + node->pos += len; + if (node->pos >= node->len) { + list_del(&node->q); + free(node->addr); + free(node); + } + } + + pthread_mutex_unlock(&props->lock.lock); +} + + +static int virtio_snd_tx_stream_cb(const void *input, + void *output, + unsigned long frame_cnt, + const PaStreamCallbackTimeInfo *time_info, + PaStreamCallbackFlags status_flags, + void *user_data) { + /* suppress unused variables warning */ + (void) input; + (void) time_info; + (void) status_flags; + vsnd_stream_sel_t *v_ptr = (vsnd_stream_sel_t *) user_data; uint32_t id = v_ptr->stream_id; int channels = vsnd_props[id].pp.channels; @@ -836,6 +1080,30 @@ static int virtio_snd_stream_cb(const void *input, return paContinue; } +static int virtio_snd_rx_stream_cb(const void *input, + void *output, + unsigned long frame_cnt, + const PaStreamCallbackTimeInfo *time_info, + PaStreamCallbackFlags status_flags, + void *user_data) +{ + /* suppress unused variable warning */ + (void) output; + (void) time_info; + (void) status_flags; + + vsnd_stream_sel_t *v_ptr = (vsnd_stream_sel_t *) user_data; + uint32_t id = v_ptr->stream_id; + virtio_snd_prop_t *props = &vsnd_props[id]; + int channels = props->pp.channels; + uint32_t out_buf_sz = frame_cnt * channels; + uint32_t out_buf_bytes = out_buf_sz * VSND_CNFA_FRAME_SZ; + + __virtio_snd_rx_frame_enqueue(input, out_buf_bytes, id); + + return paContinue; +} + #define VSND_DESC_CNT 3 static int virtio_snd_ctrl_desc_handler(virtio_snd_state_t *vsnd, const virtio_snd_queue_t *queue, @@ -952,6 +1220,37 @@ static void __virtio_snd_frame_enqueue(void *payload, pthread_mutex_unlock(&props->lock.lock); } +static void __virtio_snd_rx_frame_enqueue(const void *payload, + uint32_t n, + uint32_t stream_id) +{ + virtio_snd_prop_t *props = &vsnd_props[stream_id]; + + pthread_mutex_lock(&props->lock.lock); + while (props->lock.buf_ev_notity > 0) + pthread_cond_wait(&props->lock.writable, &props->lock.lock); + + /* Add a PCM frame to queue */ + vsnd_buf_queue_node_t *node = malloc(sizeof(*node)); + if (!node) + goto rx_frame_enque_finally; + node->addr = malloc(sizeof(*node->addr) * n); + if (!node->addr) { + free(node); + goto rx_frame_enque_finally; + } + memcpy(node->addr, payload, n); + node->len = n; + node->pos = 0; + list_push(&node->q, &props->buf_queue_head); + + props->lock.buf_ev_notity++; + pthread_cond_signal(&props->lock.readable); + +rx_frame_enque_finally: + pthread_mutex_unlock(&props->lock.lock); +} + static void virtio_queue_notify_handler( virtio_snd_state_t *vsnd, int index, @@ -1020,18 +1319,36 @@ static void virtio_queue_notify_handler( /* TX thread context */ /* Receive PCM frames from driver. */ -static void *func(void *args) +static void *tx_func(void *args) { virtio_snd_state_t *vsnd = (virtio_snd_state_t *) args; for (;;) { - pthread_mutex_lock(&virtio_snd_mutex); + pthread_mutex_lock(&virtio_snd_tx_mutex); while (tx_ev_notify <= 0) - pthread_cond_wait(&virtio_snd_tx_cond, &virtio_snd_mutex); + pthread_cond_wait(&virtio_snd_tx_cond, &virtio_snd_tx_mutex); tx_ev_notify--; virtio_queue_notify_handler(vsnd, 2, virtio_snd_tx_desc_normal_handler); - pthread_mutex_unlock(&virtio_snd_mutex); + pthread_mutex_unlock(&virtio_snd_tx_mutex); + } + pthread_exit(NULL); +} + +/* RX thread context */ +/* Send PCM frames to driver. */ +static void *rx_func(void *args) +{ + virtio_snd_state_t *vsnd = (virtio_snd_state_t *) args; + for (;;) { + pthread_mutex_lock(&virtio_snd_rx_mutex); + while (rx_ev_notify <= 0) + pthread_cond_wait(&virtio_snd_rx_cond, &virtio_snd_rx_mutex); + + rx_ev_notify--; + virtio_queue_notify_handler(vsnd, 3, virtio_snd_rx_desc_normal_handler); + + pthread_mutex_unlock(&virtio_snd_rx_mutex); } pthread_exit(NULL); } @@ -1150,6 +1467,12 @@ static bool virtio_snd_reg_write(virtio_snd_state_t *vsnd, tx_ev_notify++; pthread_cond_signal(&virtio_snd_tx_cond); break; + case VSND_QUEUE_RX: + pthread_mutex_lock(&virtio_snd_rx_mutex); + rx_ev_notify++; + pthread_cond_signal(&virtio_snd_rx_cond); + pthread_mutex_unlock(&virtio_snd_rx_mutex); + break; default: fprintf(stderr, "value %d not supported\n", value); return false; @@ -1232,20 +1555,26 @@ bool virtio_snd_init(virtio_snd_state_t *vsnd) } /* Allocate the memory of private member. */ - vsnd->priv = &vsnd_configs[vsnd_dev_cnt++]; + vsnd->priv = &vsnd_configs[vsnd_dev_cnt]; + vsnd_dev_cnt += 2; - PRIV(vsnd)->jacks = 1; - PRIV(vsnd)->streams = 1; - PRIV(vsnd)->chmaps = 1; + PRIV(vsnd)->jacks = 2; + PRIV(vsnd)->streams = 2; + PRIV(vsnd)->chmaps = 2; PRIV(vsnd)->controls = 0; /* virtio-snd device does not support control elements */ tx_ev_notify = 0; - pthread_t tid; - if (pthread_create(&tid, NULL, func, vsnd) != 0) { + rx_ev_notify = 0; + pthread_t tx_tid, rx_tid; + if (pthread_create(&tx_tid, NULL, tx_func, vsnd) != 0) { fprintf(stderr, "cannot create TX thread\n"); return false; } + if (pthread_create(&rx_tid, NULL, rx_func, vsnd) != 0) { + fprintf(stderr, "cannot create RX thread\n"); + return false; + } /* Initialize PortAudio */ PaError err = Pa_Initialize(); if (err != paNoError) {