From 41fe790f4dee486a5a536ecba5462ba7d60072f6 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, with adding notice that the capture usually doesn't work for the reason of 'semu' emulation part. --- .ci/test-sound.sh | 32 +++ .github/workflows/main.yml | 8 + README.md | 9 +- virtio-snd.c | 490 ++++++++++++++++++++++++++++++------- 4 files changed, 446 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..8b0b71da --- /dev/null +++ b/.ci/test-sound.sh @@ -0,0 +1,32 @@ +#!/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" + +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 "# " { } timeout { exit 5 } +DONE +} + +# Clean up any existing semu processes before starting tests +cleanup + +# Test sound device +test_sound +echo "✓ sound test passed" + +exit 0 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..ccf7056f 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: + - As a confirmed issue that `semu` cannot send/receive PCM frames in time, causing + the ALSA stack stuck in XRUN state until `semu` being rebooted. + - 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 get 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..d93b5c1a 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 these 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,57 @@ 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; + rx_ev_start = 0; } *plen = 0; + + return; + +pa_err: + free(props->intermediate); + 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 +882,8 @@ 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) + rx_ev_start = 1; *plen = 0; } @@ -723,6 +911,8 @@ 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) + rx_ev_start = 0; *plen = 0; } @@ -778,7 +968,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 +1002,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 +1014,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 +1071,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 +1211,29 @@ 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)); + node->addr = malloc(sizeof(*node->addr) * n); + 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); + pthread_mutex_unlock(&props->lock.lock); +} + static void virtio_queue_notify_handler( virtio_snd_state_t *vsnd, int index, @@ -1020,18 +1302,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 +1450,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 +1538,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) {