From 41fe790f4dee486a5a536ecba5462ba7d60072f6 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Fri, 5 Sep 2025 12:24:35 +0800 Subject: [PATCH 01/22] 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) { From 8b5d0cb90581d3d21adba38428c821bfe98279eb Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Sun, 21 Dec 2025 12:14:14 +0800 Subject: [PATCH 02/22] Apply suggestions --- README.md | 2 +- virtio-snd.c | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ccf7056f..db39c5b5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ A minimalist RISC-V system emulator capable of running Linux the kernel and corr - 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. + - 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 d93b5c1a..d778e4bb 100644 --- a/virtio-snd.c +++ b/virtio-snd.c @@ -855,6 +855,7 @@ static void virtio_snd_read_pcm_prepare(const virtio_snd_pcm_hdr_t *query, pa_err: free(props->intermediate); + props->intermediate = NULL; fprintf(stderr, "Cannot create PortAudio\n"); printf("PortAudio error: %s\n", Pa_GetErrorText(err)); } @@ -1223,12 +1224,19 @@ static void __virtio_snd_rx_frame_enqueue(const void *payload, /* 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); +rx_frame_enque_finally: props->lock.buf_ev_notity++; pthread_cond_signal(&props->lock.readable); pthread_mutex_unlock(&props->lock.lock); From 60ab79ac9185b024e7fa3d5762582d8df1170dd6 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Sun, 21 Dec 2025 12:21:54 +0800 Subject: [PATCH 03/22] Apply suggestion --- README.md | 2 +- virtio-snd.c | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index db39c5b5..85d03456 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A minimalist RISC-V system emulator capable of running Linux the kernel and corr - virtio-net is mapped as TAP interface. - 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. + the ALSA stack will be stucked 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`. diff --git a/virtio-snd.c b/virtio-snd.c index d778e4bb..ef08c4f9 100644 --- a/virtio-snd.c +++ b/virtio-snd.c @@ -369,7 +369,7 @@ 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 +// FIXME: set this variables into each capture stream's structure static int rx_ev_start; /* vsnd virtq callback type */ @@ -846,7 +846,9 @@ static void virtio_snd_read_pcm_prepare(const virtio_snd_pcm_hdr_t *query, 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; @@ -883,8 +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) + 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; } @@ -912,8 +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) + 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; } From 5bca78097f1b0f0ebec2ed36ef57adf0f7ab26c6 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Sun, 21 Dec 2025 12:24:17 +0800 Subject: [PATCH 04/22] Apply suggestion --- virtio-snd.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/virtio-snd.c b/virtio-snd.c index ef08c4f9..64942cd5 100644 --- a/virtio-snd.c +++ b/virtio-snd.c @@ -1244,9 +1244,10 @@ static void __virtio_snd_rx_frame_enqueue(const void *payload, node->pos = 0; list_push(&node->q, &props->buf_queue_head); -rx_frame_enque_finally: props->lock.buf_ev_notity++; pthread_cond_signal(&props->lock.readable); + +rx_frame_enque_finally: pthread_mutex_unlock(&props->lock.lock); } From a8c5d37cb3104bfa31e1537c1d772e707753e2b4 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Sun, 21 Dec 2025 12:28:21 +0800 Subject: [PATCH 05/22] Apply suggestion --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85d03456..2ef188c6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A minimalist RISC-V system emulator capable of running Linux the kernel and corr - virtio-net is mapped as TAP interface. - 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 will be stucked in XRUN state until you reboot `semu`. + 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`. From 7d09cf1a0f7820539e84e7f01c8192dfb4b2ee23 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Sun, 21 Dec 2025 20:58:24 +0800 Subject: [PATCH 06/22] Test playback --- .ci/test-sound.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 8b0b71da..b22c6c95 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -8,6 +8,7 @@ export SCRIPT_DIR source "${SCRIPT_DIR}/common.sh" SAMPLE_SOUND="/usr/share/sounds/alsa/Front_Center.wav" +PLAYBACK_EXPECTED="Playing WAVE '/usr/share/sounds/alsa/Front_Center.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono" 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 ${PLAYBACK_EXPECTED} { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n"} timeout { exit 4 } expect "# " { } timeout { exit 5 } DONE } From 46bbb676a8992f70ab63d7d8e1a8b38a52c0f92f Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Sun, 21 Dec 2025 21:04:42 +0800 Subject: [PATCH 07/22] Test playback --- .ci/test-sound.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index b22c6c95..be0a2ebc 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -18,7 +18,7 @@ test_sound() { expect "# " { send "uname -a\\n" } timeout { exit 2 } expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /dev/null\\n"} timeout { exit 3 } - expect ${PLAYBACK_EXPECTED} { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n"} timeout { exit 4 } + expect "Mono" { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n"} timeout { exit 4 } expect "# " { } timeout { exit 5 } DONE } From fd9ed7019e5e202fbd31115c217572673cd4a9c2 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Sun, 21 Dec 2025 21:18:58 +0800 Subject: [PATCH 08/22] Add test status --- .ci/test-sound.sh | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index be0a2ebc..48ac08fe 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -18,7 +18,7 @@ test_sound() { expect "# " { send "uname -a\\n" } timeout { exit 2 } expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /dev/null\\n"} timeout { exit 3 } - expect "Mono" { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n"} timeout { exit 4 } + expect "# " { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n"} timeout { exit 4 } expect "# " { } timeout { exit 5 } DONE } @@ -30,4 +30,19 @@ cleanup test_sound echo "✓ sound test passed" -exit 0 +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" From 53751e285cacb8913591ced85ab9fdd32a444405 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 17:07:22 +0800 Subject: [PATCH 09/22] Debug CI detect device --- virtio-snd.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/virtio-snd.c b/virtio-snd.c index 64942cd5..1458f23f 100644 --- a/virtio-snd.c +++ b/virtio-snd.c @@ -823,6 +823,8 @@ static void virtio_snd_read_pcm_prepare(const virtio_snd_pcm_hdr_t *query, .suggestedLatency = 0.1, /* 100 ms */ .hostApiSpecificStreamInfo = NULL, }; + if(params.device == paNoDevice) + fprintf(stderr, "=== \n"); props->intermediate = (void *) malloc(sz); err = Pa_OpenStream(&props->pa_stream, NULL, /* no input */ ¶ms, rate, cnfa_period_frames, paClipOff, @@ -841,6 +843,8 @@ static void virtio_snd_read_pcm_prepare(const virtio_snd_pcm_hdr_t *query, cnfa_period_frames = props->pp.period_bytes / VSND_CNFA_FRAME_SZ; sz = sizeof(*props->intermediate) * props->pp.buffer_bytes; props->intermediate = (void *) malloc(sz); + if(params.device == paNoDevice) + fprintf(stderr, "=== \n"); err = Pa_OpenStream(&props->pa_stream, ¶ms, NULL /* no output */, rate, cnfa_period_frames, paClipOff, virtio_snd_rx_stream_cb, &props->v); From de2d7e45cb240e2146e231e8d73c58a0a8eb49db Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 17:46:34 +0800 Subject: [PATCH 10/22] List playback --- .ci/test-sound.sh | 1 + virtio-snd.c | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 48ac08fe..c4773772 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -18,6 +18,7 @@ test_sound() { expect "# " { send "uname -a\\n" } timeout { exit 2 } expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /dev/null\\n"} timeout { exit 3 } + expect "# " { send "aplay -L\\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 diff --git a/virtio-snd.c b/virtio-snd.c index 1458f23f..64942cd5 100644 --- a/virtio-snd.c +++ b/virtio-snd.c @@ -823,8 +823,6 @@ static void virtio_snd_read_pcm_prepare(const virtio_snd_pcm_hdr_t *query, .suggestedLatency = 0.1, /* 100 ms */ .hostApiSpecificStreamInfo = NULL, }; - if(params.device == paNoDevice) - fprintf(stderr, "=== \n"); props->intermediate = (void *) malloc(sz); err = Pa_OpenStream(&props->pa_stream, NULL, /* no input */ ¶ms, rate, cnfa_period_frames, paClipOff, @@ -843,8 +841,6 @@ static void virtio_snd_read_pcm_prepare(const virtio_snd_pcm_hdr_t *query, cnfa_period_frames = props->pp.period_bytes / VSND_CNFA_FRAME_SZ; sz = sizeof(*props->intermediate) * props->pp.buffer_bytes; props->intermediate = (void *) malloc(sz); - if(params.device == paNoDevice) - fprintf(stderr, "=== \n"); err = Pa_OpenStream(&props->pa_stream, ¶ms, NULL /* no output */, rate, cnfa_period_frames, paClipOff, virtio_snd_rx_stream_cb, &props->v); From a7b2bb3ec90292896cde1b145bec6037bd8a82ef Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 17:50:53 +0800 Subject: [PATCH 11/22] Change precedence --- .ci/test-sound.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index c4773772..b057b5aa 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -8,7 +8,6 @@ export SCRIPT_DIR source "${SCRIPT_DIR}/common.sh" SAMPLE_SOUND="/usr/share/sounds/alsa/Front_Center.wav" -PLAYBACK_EXPECTED="Playing WAVE '/usr/share/sounds/alsa/Front_Center.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono" test_sound() { ASSERT expect < /dev/null\\n"} timeout { exit 3 } + expect "riscv32 GNU/Linux" { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n"} timeout { exit 4 } + expect "# " { send "aplay ${SAMPLE_SOUND} --fatal-errors > /dev/null\\n"} timeout { exit 3 } expect "# " { send "aplay -L\\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 } From 812487e7f9b693e3b68de1ac61d8a92f0c0e10ac Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 19:21:29 +0800 Subject: [PATCH 12/22] Change precedence --- .ci/test-sound.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index b057b5aa..75576415 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -16,8 +16,8 @@ test_sound() { expect "buildroot login:" { send "root\\n" } timeout { exit 1 } expect "# " { send "uname -a\\n" } timeout { exit 2 } - expect "riscv32 GNU/Linux" { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n"} timeout { exit 4 } - expect "# " { send "aplay ${SAMPLE_SOUND} --fatal-errors > /dev/null\\n"} timeout { exit 3 } + expect "riscv32 GNU/Linux " { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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" } timeout { exit 3 } expect "# " { } timeout { exit 5 } DONE From 0e3997e43cc56c41338ef9507222bb362354359f Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 20:00:48 +0800 Subject: [PATCH 13/22] fix --- .ci/test-sound.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 75576415..c94e9add 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -16,7 +16,7 @@ test_sound() { expect "buildroot login:" { send "root\\n" } timeout { exit 1 } expect "# " { send "uname -a\\n" } timeout { exit 2 } - expect "riscv32 GNU/Linux " { send "aplay ${SAMPLE_SOUND} --fatal-errors > /dev/null\\n"} timeout { exit 3 } + expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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" } timeout { exit 3 } expect "# " { } timeout { exit 5 } From f266edff3d808567c7a2706cd812d5454b703ff6 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 20:23:22 +0800 Subject: [PATCH 14/22] test --- .ci/test-sound.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index c94e9add..eb33b50b 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -18,7 +18,6 @@ test_sound() { expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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" } timeout { exit 3 } expect "# " { } timeout { exit 5 } DONE } From 3ea730a264914af8fd8f6df898a428c892eec294 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 20:28:36 +0800 Subject: [PATCH 15/22] test --- .ci/test-sound.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index eb33b50b..6227c7c7 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -18,7 +18,7 @@ test_sound() { expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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 } + expect "Input/output error" { } timeout { exit 5 } DONE } From f01572929bae619e4dd01c4b2143e510b3975e28 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 20:48:56 +0800 Subject: [PATCH 16/22] test --- .ci/test-sound.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 6227c7c7..5b0d43fa 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -16,9 +16,10 @@ test_sound() { expect "buildroot login:" { send "root\\n" } timeout { exit 1 } expect "# " { send "uname -a\\n" } timeout { exit 2 } - expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /dev/null\\n"} timeout { exit 3 } - expect "# " { send "aplay -C -d 3 --fatal-errors -f S16_LE > /dev/null\\n"} timeout { exit 4 } - expect "Input/output error" { } timeout { exit 5 } + expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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" } timeout { exit 3 } + expect "# " { } timeout { exit 5 } DONE } From 72b6f7b45878ccd1a4f4acf1671b84a3f39711cc Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 20:54:12 +0800 Subject: [PATCH 17/22] test --- .ci/test-sound.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 5b0d43fa..56fe9772 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -18,8 +18,6 @@ test_sound() { expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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" } timeout { exit 3 } - expect "# " { } timeout { exit 5 } DONE } From b4d45f48033d666f4cd67f0be71d084c51549d52 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 21:00:10 +0800 Subject: [PATCH 18/22] test --- .ci/test-sound.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 56fe9772..682a8c17 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -18,6 +18,7 @@ test_sound() { expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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" } timeout { exit 3 } DONE } From 8f2038497e61c85f4a7e363edcd813e556e7327b Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 21:07:21 +0800 Subject: [PATCH 19/22] test --- .ci/test-sound.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 682a8c17..6a6b06a3 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -9,6 +9,20 @@ 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" } timeout { exit 3 } + expect "# " { } timeout { exit 3 } DONE } From 2ac448dc4af323d7298b5fe47c5e6d8f1965e2a4 Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 21:11:55 +0800 Subject: [PATCH 20/22] test --- .ci/test-sound.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 6a6b06a3..45f9af88 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -32,7 +32,8 @@ test_sound() { expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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 3 } + expect "# " { send "aplay -L" } + expect "# " { } DONE } From 5f6c82b2c48ae01d5537194bb802d5ed6b88eafe Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 21:25:03 +0800 Subject: [PATCH 21/22] Lint --- virtio-snd.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/virtio-snd.c b/virtio-snd.c index 64942cd5..0f23bdd5 100644 --- a/virtio-snd.c +++ b/virtio-snd.c @@ -1232,10 +1232,10 @@ static void __virtio_snd_rx_frame_enqueue(const void *payload, /* Add a PCM frame to queue */ vsnd_buf_queue_node_t *node = malloc(sizeof(*node)); - if(!node) + if (!node) goto rx_frame_enque_finally; node->addr = malloc(sizeof(*node->addr) * n); - if(!node->addr) { + if (!node->addr) { free(node); goto rx_frame_enque_finally; } From a5cbdc17c8b7ee0842484ce48b3d7e9453b8af2a Mon Sep 17 00:00:00 2001 From: Cuda-Chen Date: Mon, 22 Dec 2025 21:59:02 +0800 Subject: [PATCH 22/22] test --- .ci/test-sound.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/test-sound.sh b/.ci/test-sound.sh index 45f9af88..6cbd63fe 100755 --- a/.ci/test-sound.sh +++ b/.ci/test-sound.sh @@ -32,7 +32,7 @@ test_sound() { expect "riscv32 GNU/Linux" { send "aplay ${SAMPLE_SOUND} --fatal-errors > /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" } + expect "# " { send "aplay -L\\n" } expect "# " { } DONE }