Virtio Sound is a paravirtualized sound device to access sound device from virtual machine. Unlike other virtual sound device among QEMU implementations, Virtio Sound doesn't assume any physical sound device to derive the virtual device behavior. Instead, Virtio Sound relays on VIRTIO specification to define device behavior so that virtual machine can exchange sound data without any restriction coming from physical device in general. One such example is the number of streams per single sound device. This article demonstrate how the number of streams is configured on QEMU and how it is tested from virtual machie side.
Prerequisite
- Linux kernel 5.15 with CONFIG_SND_VIRTIO
- YoctoLinux 5.0.14 guest root filesystem localbuild.
- QEMU 10.2.0 localbuild.
Launching QEMU with virtio-snd configuration
qemu configuration of virtio-snd is described on official documentation. While VIRTIO specification doesn't have limit of the number of streams, QEMU implementation define its own limit of streams. virtio-snd.c:1024 shows that 10 streams per single device is maximum.
-audiodev pipewire,id=audio-main \
-device virtio-sound-pci,audiodev=audio-main,streams=10 \
Using this launch time QEMU configuration, stream_id is ranging from 0 to 9. stream_id 0-4 is configured to VIRTIO_SND_D_OUTPUT, 5-9 is configured to VIRTIO_SND_D_INPUT as we can see at virtio-snd.c445. VIRTIO_SND_D_OUTPUT/VIRTIO_SND_D_INPUT is also called as PLAYBACK/CAPTURE in the context of ALSA. From here, we use streams=10 configuration to maximize coverage.
Note, for reference QEMU virtio-snd.c and VIRTIO specification defines jacks=, chmaps= other than streams=. These two parameters are outside scope of this article, however, it seems that there is no meaningful backend implementation that actively tries to utilize this jacks, chmaps parameter as intended.
Test each stream from guest virtual machine
Kernel driver virtio-snd maps each stream as substream in the context ALSA subsystem. The representative code is virtio_pcm.c:426, and will be explained more. The most quick way to see this from guest side is to run following commands:
root@qemux86-64:~# aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: SoundCard [VirtIO SoundCard], device 0: virtio-snd [VirtIO PCM 0]
Subdevices: 5/5
Subdevice #0: subdevice #0
Subdevice #1: subdevice #1
Subdevice #2: subdevice #2
Subdevice #3: subdevice #3
Subdevice #4: subdevice #4
root@qemux86-64:~# arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: SoundCard [VirtIO SoundCard], device 0: virtio-snd [VirtIO PCM 0]
Subdevices: 5/5
Subdevice #0: subdevice #0
Subdevice #1: subdevice #1
Subdevice #2: subdevice #2
Subdevice #3: subdevice #3
Subdevice #4: subdevice #4
root@qemux86-64:~#
As expected from QEMU launch configuration, there are 10 streams and these 10 are split into 5 PLAYBACK streams and 5 CAPTURE streams. At this moment, simple playback can be done through following command:
root@qemux86-64:~# aplay /tmp/Free_Test_Data_1MB_MP3.wav
dump hardware params
-v and --dump-hw-params shows some useful debug information. This is example output using default QEMU implmentation/ALSA guest userspace contained on YoctoLinux 5.0.14. Skipping details here, some of parameters is transfered from host to guest using VIRTIO protocol specification so this output can be used as part of testing device/driver behavior agains specification.
root@qemux86-64:~# aplay -v /tmp/Free_Test_Data_1MB_MP3.wav
Playing WAVE '/tmp/Free_Test_Data_1MB_MP3.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
Plug PCM: Hardware PCM card 0 'VirtIO SoundCard' device 0 subdevice 0
Its setup is:
stream : PLAYBACK
access : RW_INTERLEAVED
format : S16_LE
subformat : STD
channels : 2
rate : 44100
exact rate : 44100 (44100/1)
msbits : 16
buffer_size : 22052
period_size : 5513
period_time : 125011
tstamp_mode : ENABLE
tstamp_type : MONOTONIC
period_step : 1
avail_min : 5513
period_event : 0
start_threshold : 22052
stop_threshold : 22052
silence_threshold: 0
silence_size : 0
boundary : 6207086186423386112
appl_ptr : 0
hw_ptr : 0
^CAborted by signal Interrupt...
root@qemux86-64:~#
root@qemux86-64:~# aplay --dump-hw-params /tmp/Free_Test_Data_1MB_MP3.wav
Playing WAVE '/tmp/Free_Test_Data_1MB_MP3.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
HW Params of device "default":
--------------------
ACCESS: MMAP_INTERLEAVED MMAP_NONINTERLEAVED MMAP_COMPLEX RW_INTERLEAVED RW_NONINTERLEAVED
FORMAT: S8 U8 S16_LE S16_BE U16_LE U16_BE S24_LE S24_BE U24_LE U24_BE S32_LE S32_BE U32_LE U32_BE FLOAT_LE FLOAT_BE FLOAT64_LE FLOAT64_BE MU_LAW A_LAW IMA_ADPCM S20_LE S20_BE U20_LE U20_BE
S24_3LE S24_3BE U24_3LE U24_3BE S20_3LE S20_3BE U20_3LE U20_3BE S18_3LE S18_3BE U18_3LE U18_3BE
SUBFORMAT: STD MSBITS_MAX MSBITS_20 MSBITS_24
SAMPLE_BITS: [4 64]
FRAME_BITS: [4 640000]
CHANNELS: [1 10000]
RATE: [4000 4294967295)
PERIOD_TIME: (36 22293179)
PERIOD_SIZE: (0 4294967295)
PERIOD_BYTES: (0 4294967295)
PERIODS: (0 4294967295]
BUFFER_TIME: [1 4294967295]
BUFFER_SIZE: [1 4294967294]
BUFFER_BYTES: [1 4294967295]
TICK_TIME: ALL
--------------------
^CAborted by signal Interrupt...
hw:0,0,X
To choose substream from aplay command, we can put -D hw:0,0,X option as follows:
root@qemux86-64:~# aplay -D hw:0,0,0 -v /tmp/Free_Test_Data_1MB_MP3.wav &
root@qemux86-64:~# aplay -D hw:0,0,1 -v /tmp/Free_Test_Data_1MB_MP3.wav &
root@qemux86-64:~# aplay -D hw:0,0,2 -v /tmp/Free_Test_Data_1MB_MP3.wav &
root@qemux86-64:~# aplay -D hw:0,0,3 -v /tmp/Free_Test_Data_1MB_MP3.wav &
root@qemux86-64:~# aplay -D hw:0,0,4 -v /tmp/Free_Test_Data_1MB_MP3.wav
Using -v option shows subsdevice to be used for each command. In this case, we can test simultaneous audio playback for single virtio sound device with multiple streams configuration. QEMU just put sound output to pipewire server without any special processing for each output of streams. In this case, the physical output sound is identical to the case where the sound is mixed within guest machine side and guest put the mixed result as a single stream.
.asoundrc
Asoundrc allows us to choose specific substream by name. At first, save following configuration as "~/.asoundrc". And then we can use -D stream0 ... -D stream4 option for aplay command to choose one specific substream.
pcm.stream0 {
type hw
card 0
device 0
subdevice 0
}
pcm.stream1 {
type hw
card 0
device 0
subdevice 1
}
pcm.stream2 {
type hw
card 0
device 0
subdevice 2
}
pcm.stream3 {
type hw
card 0
device 0
subdevice 3
}
pcm.stream4 {
type hw
card 0
device 0
subdevice 4
}
This output indicate that subdevice 4 was chosen to playback wavefile. This asoundrc way mabe useful to abstract subdevice number as more generic identifier for some specific test situation.
root@qemux86-64:~# aplay -D stream4 -v /tmp/Free_Test_Data_1MB_MP3.wav
Playing WAVE '/tmp/Free_Test_Data_1MB_MP3.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
Hardware PCM card 0 'VirtIO SoundCard' device 0 subdevice 4
How Subdevice/substream is determined on kernel
Linux kernel sound subsystem defines three levels of concept: card, device, subdevice. Those who knows well about the basic of linux device driver (but not familer with ALSA) will wonder how subdevice is determined on the kernel-user boundary by seeing following device files.
root@qemux86-64:~# ls /dev/snd/ -l
total 0
drwxr-xr-x 2 root root 60 Jan 3 08:44 by-path
crw-rw----+ 1 root audio 116, 4 Jan 3 08:44 controlC0
crw-rw----+ 1 root audio 116, 3 Jan 3 08:44 pcmC0D0c
crw-rw----+ 1 root audio 116, 2 Jan 3 13:21 pcmC0D0p
crw-rw----+ 1 root audio 116, 33 Jan 3 08:23 timer
root@qemux86-64:~#
The sound device files have naming format with C=card, D=device. So pcmC0D0c is capture device for card 0 device 0. pcmC0D0p is playback device for card0 device 0. Thus, if there are multiple cards or devices, we can immediately determine new card or device by device file. Unlike card and device, subdevice has a different manner to be determined on kernel and ALSA library.
Starting from kernel side investigation, SNDRV_CTL_IOCTL_PCM_PREFER_SUBDEVICE is a good starting point. The ALSA library consumer process can open controlC0 at first and mark the prefered subdevice via ioctl call. This ioctl is followed by open device /dev/snd/pcmC0D0p or /dev/snd/pcmC0D0c, for playback or capture respectively. At open call, snd_ctl_get_preferred_subdevice function retrieves process prefered subdevice to connect opened fd to specific substream. When using ALSA library, this operation is hidden by snd_pcm_hw_open function in ALSA-lib which has subdevice argument. Therefore ALSA consumer process don't have to aware like the current prefereed subdevice, at all.
When guest machine is Android, tinyalsa is often integrated instead of full featured ALSA-lib. In this case, tinyalsa seems not have the subdevice selection using SNDRV_CTL_IOCTL_PCM_PREFER_SUBDEVICE because I coudn't find ioctl definition anywhere on source code. To choose subdevice on Android, extra mechanism/library will be required.
cd external
grep SNDRV_CTL_IOCTL_PCM_PREFER_SUBDEVICE -rn tinyalsa*