mirror of
https://github.com/rust-vmm/vhost-device.git
synced 2025-12-27 07:29:24 +00:00
Merge pull request #475 from epilys/virtio-sound-pr-2
Add vhost-user-sound crate
This commit is contained in:
commit
114cbe456b
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -79,7 +79,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.29",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -112,9 +112,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@ -188,7 +188,7 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.29",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -242,7 +242,7 @@ version = "4.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -265,11 +265,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.2"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
|
||||
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
@ -500,7 +499,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.29",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -641,8 +640,8 @@ version = "0.38.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"errno 0.3.2",
|
||||
"bitflags 2.4.1",
|
||||
"errno 0.3.5",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
@ -671,7 +670,7 @@ checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.29",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -730,9 +729,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.29"
|
||||
version = "2.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
|
||||
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -793,7 +792,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.29",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -22,7 +22,7 @@ More information may be found in its [README file](./staging/README.md).
|
||||
|
||||
Here is the list of device backends in **staging**:
|
||||
|
||||
- Currently none.
|
||||
- [Sound](https://github.com/rust-vmm/vhost-device/blob/main/staging/vhost-device-sound/README.md)
|
||||
|
||||
<!--
|
||||
Template:
|
||||
|
||||
1174
staging/Cargo.lock
generated
Normal file
1174
staging/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = []
|
||||
members = [
|
||||
"vhost-device-sound",
|
||||
]
|
||||
|
||||
3
staging/vhost-device-sound/CHANGELOG.md
Normal file
3
staging/vhost-device-sound/CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Upcoming Release
|
||||
|
||||
- First initial daemon implementation.
|
||||
33
staging/vhost-device-sound/Cargo.toml
Normal file
33
staging/vhost-device-sound/Cargo.toml
Normal file
@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "vhost-device-sound"
|
||||
version = "0.1.0"
|
||||
authors = ["Stefano Garzarella <sgarzare@redhat.com>", "Manos Pitsidianakis <manos.pitsidianakis@linaro.org>"]
|
||||
description = "A virtio-sound device using the vhost-user protocol."
|
||||
repository = "https://github.com/rust-vmm/vhost-device"
|
||||
readme = "README.md"
|
||||
keywords = ["vhost", "sound", "virtio-sound", "virtio-snd", "virtio"]
|
||||
license = "Apache-2.0 OR BSD-3-Clause"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["alsa-backend", "pw-backend"]
|
||||
alsa-backend = ["dep:alsa"]
|
||||
pw-backend = ["pw"]
|
||||
|
||||
[dependencies]
|
||||
alsa = { version = "0.7", optional = true }
|
||||
clap = { version = "4.1", features = ["derive"] }
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
pw = { package = "pipewire", git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", rev = "5fe090b3ac8f6fed756c4871ac18f26edda3ac89", optional = true }
|
||||
thiserror = "1.0"
|
||||
vhost = { version = "0.6", features = ["vhost-user-slave"] }
|
||||
vhost-user-backend = "0.8"
|
||||
virtio-bindings = "0.2.1"
|
||||
virtio-queue = "0.7"
|
||||
vm-memory = "0.10"
|
||||
vmm-sys-util = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "1.0"
|
||||
rstest = "0.18.2"
|
||||
1
staging/vhost-device-sound/LICENSE-APACHE
Symbolic link
1
staging/vhost-device-sound/LICENSE-APACHE
Symbolic link
@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
1
staging/vhost-device-sound/LICENSE-BSD-3-Clause
Symbolic link
1
staging/vhost-device-sound/LICENSE-BSD-3-Clause
Symbolic link
@ -0,0 +1 @@
|
||||
../../LICENSE-BSD-3-Clause
|
||||
48
staging/vhost-device-sound/README.md
Normal file
48
staging/vhost-device-sound/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# vhost-device-sound
|
||||
|
||||
<!--
|
||||
generated with help2man target/debug/vhost-device-sound |mandoc
|
||||
-->
|
||||
## Synopsis
|
||||
vhost-device-sound --socket <SOCKET> --backend <BACKEND>
|
||||
|
||||
## Description
|
||||
A virtio-sound device using the vhost-user protocol.
|
||||
|
||||
## Options
|
||||
|
||||
```text
|
||||
--socket <SOCKET>
|
||||
vhost-user Unix domain socket path
|
||||
|
||||
--backend <BACKEND>
|
||||
audio backend to be used [possible values: null, pipewire, alsa]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
-V, --version
|
||||
Print version
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Launch the backend on the host machine:
|
||||
|
||||
```shell
|
||||
host# vhost-device-sound --socket /tmp/snd.sock --backend null
|
||||
```
|
||||
|
||||
With QEMU, you can add a `virtio` device that uses the backend's socket with the following flags:
|
||||
|
||||
```text
|
||||
-chardev socket,id=vsnd,path=/tmp/snd.sock \
|
||||
-device vhost-user-snd-pci,chardev=vsnd,id=snd
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under either of
|
||||
|
||||
- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0
|
||||
- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
7
staging/vhost-device-sound/rustfmt.toml
Normal file
7
staging/vhost-device-sound/rustfmt.toml
Normal file
@ -0,0 +1,7 @@
|
||||
edition = "2018"
|
||||
format_generated_files = false
|
||||
format_code_in_doc_comments = true
|
||||
format_strings = true
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
wrap_comments = true
|
||||
58
staging/vhost-device-sound/src/audio_backends.rs
Normal file
58
staging/vhost-device-sound/src/audio_backends.rs
Normal file
@ -0,0 +1,58 @@
|
||||
// Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
mod alsa;
|
||||
mod null;
|
||||
|
||||
#[cfg(feature = "pw-backend")]
|
||||
mod pipewire;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
use self::alsa::AlsaBackend;
|
||||
use self::null::NullBackend;
|
||||
#[cfg(feature = "pw-backend")]
|
||||
use self::pipewire::PwBackend;
|
||||
use crate::{device::ControlMessage, stream::Stream, BackendType, Result};
|
||||
|
||||
pub trait AudioBackend {
|
||||
fn write(&self, stream_id: u32) -> Result<()>;
|
||||
|
||||
fn read(&self, stream_id: u32) -> Result<()>;
|
||||
|
||||
fn set_parameters(&self, _stream_id: u32, _: ControlMessage) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare(&self, _stream_id: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&self, _stream_id: u32, _: ControlMessage) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&self, _stream_id: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self, _stream_id: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_audio_backend(
|
||||
backend: BackendType,
|
||||
streams: Arc<RwLock<Vec<Stream>>>,
|
||||
) -> Result<Box<dyn AudioBackend + Send + Sync>> {
|
||||
log::trace!("allocating audio backend {:?}", backend);
|
||||
match backend {
|
||||
BackendType::Null => Ok(Box::new(NullBackend::new(streams))),
|
||||
#[cfg(feature = "pw-backend")]
|
||||
BackendType::Pipewire => Ok(Box::new(PwBackend::new(streams))),
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
BackendType::Alsa => Ok(Box::new(AlsaBackend::new(streams))),
|
||||
}
|
||||
}
|
||||
527
staging/vhost-device-sound/src/audio_backends/alsa.rs
Normal file
527
staging/vhost-device-sound/src/audio_backends/alsa.rs
Normal file
@ -0,0 +1,527 @@
|
||||
/// Alsa backend
|
||||
//
|
||||
// Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
sync::mpsc::{channel, Receiver, Sender},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
thread,
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use alsa::{
|
||||
pcm::{Access, Format, HwParams, State, PCM},
|
||||
Direction, PollDescriptors, ValueOr,
|
||||
};
|
||||
use virtio_queue::Descriptor;
|
||||
use vm_memory::Bytes;
|
||||
|
||||
use super::AudioBackend;
|
||||
use crate::{
|
||||
device::ControlMessage,
|
||||
stream::{PCMState, Stream},
|
||||
virtio_sound::{
|
||||
self, VirtioSndPcmSetParams, VIRTIO_SND_D_INPUT, VIRTIO_SND_D_OUTPUT, VIRTIO_SND_S_BAD_MSG,
|
||||
VIRTIO_SND_S_NOT_SUPP,
|
||||
},
|
||||
Result as CrateResult,
|
||||
};
|
||||
|
||||
type AResult<T> = std::result::Result<T, alsa::Error>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AlsaBackend {
|
||||
sender: Arc<Mutex<Sender<AlsaAction>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AlsaAction {
|
||||
SetParameters(usize, ControlMessage),
|
||||
Prepare(usize),
|
||||
Release(usize, ControlMessage),
|
||||
Start(usize),
|
||||
Stop(usize),
|
||||
Write(usize),
|
||||
Read(usize),
|
||||
}
|
||||
|
||||
fn update_pcm(
|
||||
pcm_: &Arc<Mutex<PCM>>,
|
||||
stream_id: usize,
|
||||
streams: &RwLock<Vec<Stream>>,
|
||||
) -> AResult<()> {
|
||||
*pcm_.lock().unwrap() = {
|
||||
let streams = streams.read().unwrap();
|
||||
let s = &streams[stream_id];
|
||||
let pcm = PCM::new(
|
||||
"default",
|
||||
match s.direction {
|
||||
d if d == VIRTIO_SND_D_OUTPUT => Direction::Playback,
|
||||
d if d == VIRTIO_SND_D_INPUT => Direction::Capture,
|
||||
other => panic!("Invalid virtio-sound stream: {}", other),
|
||||
},
|
||||
false,
|
||||
)?;
|
||||
|
||||
{
|
||||
let hwp = HwParams::any(&pcm)?;
|
||||
hwp.set_channels(s.params.channels.into())?;
|
||||
hwp.set_rate(
|
||||
match s.params.rate {
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_5512 => 5512,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_8000 => 8000,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_11025 => 11025,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_16000 => 16000,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_22050 => 22050,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_32000 => 32000,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_44100 => 44100,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_48000 => 48000,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_64000 => 64000,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_88200 => 88200,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_96000 => 96000,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_176400 => 176400,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_192000 => 192000,
|
||||
virtio_sound::VIRTIO_SND_PCM_RATE_384000 => 384000,
|
||||
_ => 44100,
|
||||
},
|
||||
ValueOr::Nearest,
|
||||
)?;
|
||||
hwp.set_format(match s.params.format {
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_IMA_ADPCM => Format::ImaAdPCM,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_MU_LAW => Format::MuLaw,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_A_LAW => Format::ALaw,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_S8 => Format::S8,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_U8 => Format::U8,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_S16 => Format::s16(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_U16 => Format::r#u16(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_S18_3 => Format::S183LE,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_U18_3 => Format::U183LE,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_S20_3 => Format::S203LE,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_U20_3 => Format::U203LE,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_S24_3 => Format::S24LE,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_U24_3 => Format::U24LE,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_S20 => Format::s20_3(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_U20 => Format::u20_3(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_S24 => Format::s24(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_U24 => Format::u24(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_S32 => Format::s32(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_U32 => Format::r#u32(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_FLOAT => Format::float(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_FLOAT64 => Format::float64(),
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_DSD_U8 => Format::DSDU8,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_DSD_U16 => Format::DSDU16LE,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_DSD_U32 => Format::DSDU32LE,
|
||||
virtio_sound::VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME => Format::iec958_subframe(),
|
||||
_ => Format::Unknown,
|
||||
})?;
|
||||
|
||||
hwp.set_access(Access::RWInterleaved)?;
|
||||
|
||||
// > A period is the number of frames in between each hardware interrupt.
|
||||
// - https://www.alsa-project.org/wiki/FramesPeriods
|
||||
//
|
||||
// FIXME: What values should we set for buffer size and period size? (Should we
|
||||
// set them at all?) virtio-sound spec deals in bytes but ALSA deals
|
||||
// in frames. The alsa bindings sometimes use frames and sometimes bytes.
|
||||
|
||||
pcm.hw_params(&hwp)?;
|
||||
}
|
||||
pcm
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Returns `true` if the function should be called again, because there are are
|
||||
// more data left to write.
|
||||
fn write_samples_direct(
|
||||
pcm: &alsa::PCM,
|
||||
stream: &mut Stream,
|
||||
mmap: &mut alsa::direct::pcm::MmapPlayback<u8>,
|
||||
) -> AResult<bool> {
|
||||
while mmap.avail() > 0 {
|
||||
// Write samples to DMA area from iterator
|
||||
let Some(buffer) = stream.buffers.front_mut() else {
|
||||
return Ok(false);
|
||||
};
|
||||
let mut buf = vec![0; buffer.data_descriptor.len() as usize];
|
||||
let read_bytes = buffer
|
||||
.consume(&mut buf)
|
||||
.expect("failed to read buffer from guest");
|
||||
let mut iter = buf[0..read_bytes as usize].iter().cloned();
|
||||
let frames = mmap.write(&mut iter);
|
||||
let written_bytes = pcm.frames_to_bytes(frames);
|
||||
if let Ok(written_bytes) = usize::try_from(written_bytes) {
|
||||
buffer.pos += written_bytes;
|
||||
}
|
||||
if buffer.pos >= buffer.data_descriptor.len() as usize {
|
||||
stream.buffers.pop_front();
|
||||
}
|
||||
}
|
||||
match mmap.status().state() {
|
||||
State::Running => {
|
||||
return Ok(false);
|
||||
}
|
||||
State::Prepared => {}
|
||||
State::XRun => {
|
||||
log::trace!("Underrun in audio output stream!");
|
||||
pcm.prepare()?
|
||||
}
|
||||
State::Suspended => {}
|
||||
n => panic!("Unexpected pcm state {:?}", n),
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn write_samples_io(
|
||||
p: &alsa::PCM,
|
||||
stream: &mut Stream,
|
||||
io: &mut alsa::pcm::IO<u8>,
|
||||
) -> AResult<bool> {
|
||||
loop {
|
||||
let avail = match p.avail_update() {
|
||||
Ok(n) => n,
|
||||
Err(err) => {
|
||||
log::trace!("Recovering from {}", err);
|
||||
p.recover(err.errno() as std::os::raw::c_int, true)?;
|
||||
p.avail_update()?
|
||||
}
|
||||
};
|
||||
if avail == 0 {
|
||||
break;
|
||||
}
|
||||
let written = io.mmap(avail as usize, |buf| {
|
||||
let Some(buffer) = stream.buffers.front_mut() else {
|
||||
return 0;
|
||||
};
|
||||
let mut data = vec![0; buffer.data_descriptor.len() as usize];
|
||||
|
||||
// consume() always reads (buffer.data_descriptor.len() -
|
||||
// buffer.pos) bytes
|
||||
let read_bytes = buffer
|
||||
.consume(&mut data)
|
||||
.expect("failed to read buffer from guest");
|
||||
let mut iter = data[0..read_bytes as usize].iter().cloned();
|
||||
|
||||
let mut written_bytes = 0;
|
||||
for (sample, byte) in buf.iter_mut().zip(&mut iter) {
|
||||
*sample = byte;
|
||||
written_bytes += 1;
|
||||
}
|
||||
buffer.pos += written_bytes as usize;
|
||||
if buffer.pos >= buffer.data_descriptor.len() as usize {
|
||||
stream.buffers.pop_front();
|
||||
}
|
||||
p.bytes_to_frames(written_bytes)
|
||||
.try_into()
|
||||
.unwrap_or_default()
|
||||
})?;
|
||||
if written == 0 {
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
match p.state() {
|
||||
State::Suspended | State::Running => Ok(false),
|
||||
State::Prepared => Ok(false),
|
||||
State::XRun => Ok(true), // Recover from this in next round
|
||||
n => panic!("Unexpected pcm state {:?}", n),
|
||||
}
|
||||
}
|
||||
|
||||
fn alsa_worker(
|
||||
pcm: Arc<Mutex<PCM>>,
|
||||
streams: Arc<RwLock<Vec<Stream>>>,
|
||||
receiver: &Receiver<bool>,
|
||||
stream_id: usize,
|
||||
) -> AResult<()> {
|
||||
loop {
|
||||
let Ok(do_write) = receiver.recv() else {
|
||||
return Ok(());
|
||||
};
|
||||
if do_write {
|
||||
loop {
|
||||
if matches!(receiver.try_recv(), Ok(false)) {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut fds = {
|
||||
let lck = pcm.lock().unwrap();
|
||||
if matches!(lck.state(), State::Running | State::Prepared | State::XRun) {
|
||||
let mut mmap = lck.direct_mmap_playback::<u8>().ok();
|
||||
|
||||
if let Some(ref mut mmap) = mmap {
|
||||
if write_samples_direct(
|
||||
&lck,
|
||||
&mut streams.write().unwrap()[stream_id],
|
||||
mmap,
|
||||
)? {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
let mut io = lck.io_bytes();
|
||||
// Direct mode unavailable, use alsa-lib's mmap emulation instead
|
||||
if write_samples_io(
|
||||
&lck,
|
||||
&mut streams.write().unwrap()[stream_id],
|
||||
&mut io,
|
||||
)? {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
lck.get()?
|
||||
} else {
|
||||
drop(lck);
|
||||
sleep(Duration::from_millis(500));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Nothing to do, sleep until woken up by the kernel.
|
||||
alsa::poll::poll(&mut fds, 100)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AlsaBackend {
|
||||
pub fn new(streams: Arc<RwLock<Vec<Stream>>>) -> Self {
|
||||
let (sender, receiver) = channel();
|
||||
let sender = Arc::new(Mutex::new(sender));
|
||||
|
||||
thread::spawn(move || {
|
||||
if let Err(err) = Self::run(streams, receiver) {
|
||||
log::error!("Main thread exited with error: {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
fn run(
|
||||
streams: Arc<RwLock<Vec<Stream>>>,
|
||||
receiver: Receiver<AlsaAction>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let streams_no: usize;
|
||||
|
||||
let (mut pcms, senders) = {
|
||||
streams_no = streams.read().unwrap().len();
|
||||
let mut vec = Vec::with_capacity(streams_no);
|
||||
let mut senders = Vec::with_capacity(streams_no);
|
||||
for i in 0..streams_no {
|
||||
let (sender, receiver) = channel();
|
||||
let pcm = Arc::new(Mutex::new(PCM::new("default", Direction::Playback, false)?));
|
||||
|
||||
let mtx = Arc::clone(&pcm);
|
||||
let streams = Arc::clone(&streams);
|
||||
thread::spawn(move || {
|
||||
// TODO: exponential backoff? send fatal error to daemon?
|
||||
while let Err(err) = alsa_worker(mtx.clone(), streams.clone(), &receiver, i) {
|
||||
log::error!(
|
||||
"Worker thread exited with error: {}, sleeping for 500ms",
|
||||
err
|
||||
);
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
});
|
||||
|
||||
senders.push(sender);
|
||||
vec.push(pcm);
|
||||
}
|
||||
(vec, senders)
|
||||
};
|
||||
for (i, pcm) in pcms.iter_mut().enumerate() {
|
||||
update_pcm(pcm, i, &streams)?;
|
||||
}
|
||||
|
||||
while let Ok(action) = receiver.recv() {
|
||||
match action {
|
||||
AlsaAction::Read(_) => {}
|
||||
AlsaAction::Write(stream_id) => {
|
||||
if stream_id >= streams_no {
|
||||
log::error!(
|
||||
"Received Write action for stream id {} but there are only {} PCM \
|
||||
streams.",
|
||||
stream_id,
|
||||
pcms.len()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if matches!(
|
||||
streams.write().unwrap()[stream_id].state,
|
||||
PCMState::Start | PCMState::Prepare
|
||||
) {
|
||||
senders[stream_id].send(true).unwrap();
|
||||
}
|
||||
}
|
||||
AlsaAction::Start(stream_id) => {
|
||||
if stream_id >= streams_no {
|
||||
log::error!(
|
||||
"Received Start action for stream id {} but there are only {} PCM \
|
||||
streams.",
|
||||
stream_id,
|
||||
pcms.len()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let start_result = streams.write().unwrap()[stream_id].state.start();
|
||||
if let Err(err) = start_result {
|
||||
log::error!("Stream {} start {}", stream_id, err);
|
||||
} else {
|
||||
let pcm = &pcms[stream_id];
|
||||
let lck = pcm.lock().unwrap();
|
||||
match lck.state() {
|
||||
State::Running => {}
|
||||
_ => lck.start()?,
|
||||
}
|
||||
}
|
||||
}
|
||||
AlsaAction::Stop(stream_id) => {
|
||||
if stream_id >= streams_no {
|
||||
log::error!(
|
||||
"Received Stop action for stream id {} but there are only {} PCM \
|
||||
streams.",
|
||||
stream_id,
|
||||
pcms.len()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let stop_result = streams.write().unwrap()[stream_id].state.stop();
|
||||
if let Err(err) = stop_result {
|
||||
log::error!("Stream {} stop {}", stream_id, err);
|
||||
}
|
||||
}
|
||||
AlsaAction::Prepare(stream_id) => {
|
||||
if stream_id >= streams_no {
|
||||
log::error!(
|
||||
"Received Prepare action for stream id {} but there are only {} PCM \
|
||||
streams.",
|
||||
stream_id,
|
||||
pcms.len()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let prepare_result = streams.write().unwrap()[stream_id].state.prepare();
|
||||
if let Err(err) = prepare_result {
|
||||
log::error!("Stream {} prepare {}", stream_id, err);
|
||||
} else {
|
||||
let pcm = &pcms[stream_id];
|
||||
let lck = pcm.lock().unwrap();
|
||||
match lck.state() {
|
||||
State::Running => {}
|
||||
_ => lck.prepare()?,
|
||||
}
|
||||
}
|
||||
}
|
||||
AlsaAction::Release(stream_id, mut msg) => {
|
||||
if stream_id >= streams_no {
|
||||
log::error!(
|
||||
"Received Release action for stream id {} but there are only {} PCM \
|
||||
streams.",
|
||||
stream_id,
|
||||
pcms.len()
|
||||
);
|
||||
msg.code = VIRTIO_SND_S_BAD_MSG;
|
||||
continue;
|
||||
};
|
||||
let release_result = streams.write().unwrap()[stream_id].state.release();
|
||||
if let Err(err) = release_result {
|
||||
log::error!("Stream {} release {}", stream_id, err);
|
||||
msg.code = VIRTIO_SND_S_BAD_MSG;
|
||||
} else {
|
||||
senders[stream_id].send(false).unwrap();
|
||||
let mut streams = streams.write().unwrap();
|
||||
std::mem::take(&mut streams[stream_id].buffers);
|
||||
}
|
||||
}
|
||||
AlsaAction::SetParameters(stream_id, mut msg) => {
|
||||
if stream_id >= streams_no {
|
||||
log::error!(
|
||||
"Received SetParameters action for stream id {} but there are only {} \
|
||||
PCM streams.",
|
||||
stream_id,
|
||||
pcms.len()
|
||||
);
|
||||
msg.code = VIRTIO_SND_S_BAD_MSG;
|
||||
continue;
|
||||
};
|
||||
let descriptors: Vec<Descriptor> = msg.desc_chain.clone().collect();
|
||||
let desc_request = &descriptors[0];
|
||||
let request = msg
|
||||
.desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSndPcmSetParams>(desc_request.addr())
|
||||
.unwrap();
|
||||
{
|
||||
let mut streams = streams.write().unwrap();
|
||||
let st = &mut streams[stream_id];
|
||||
if let Err(err) = st.state.set_parameters() {
|
||||
log::error!("Stream {} set_parameters {}", stream_id, err);
|
||||
msg.code = VIRTIO_SND_S_BAD_MSG;
|
||||
continue;
|
||||
} else if !st.supports_format(request.format)
|
||||
|| !st.supports_rate(request.rate)
|
||||
{
|
||||
msg.code = VIRTIO_SND_S_NOT_SUPP;
|
||||
continue;
|
||||
} else {
|
||||
st.params.buffer_bytes = request.buffer_bytes;
|
||||
st.params.period_bytes = request.period_bytes;
|
||||
st.params.features = request.features;
|
||||
st.params.channels = request.channels;
|
||||
st.params.format = request.format;
|
||||
st.params.rate = request.rate;
|
||||
}
|
||||
}
|
||||
// Manually drop msg for faster response: the kernel has a timeout.
|
||||
drop(msg);
|
||||
update_pcm(&pcms[stream_id], stream_id, &streams)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! send_action {
|
||||
($($fn_name:ident $action:tt),+$(,)?) => {
|
||||
$(
|
||||
fn $fn_name(&self, id: u32) -> CrateResult<()> {
|
||||
self.sender
|
||||
.lock()
|
||||
.unwrap()
|
||||
.send(AlsaAction::$action(id as usize))
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
)*
|
||||
};
|
||||
($(ctrl $fn_name:ident $action:tt),+$(,)?) => {
|
||||
$(
|
||||
fn $fn_name(&self, id: u32, msg: ControlMessage) -> CrateResult<()> {
|
||||
self.sender
|
||||
.lock()
|
||||
.unwrap()
|
||||
.send(AlsaAction::$action(id as usize, msg))
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioBackend for AlsaBackend {
|
||||
send_action! {
|
||||
write Write,
|
||||
read Read,
|
||||
prepare Prepare,
|
||||
start Start,
|
||||
stop Stop,
|
||||
}
|
||||
send_action! {
|
||||
ctrl set_parameters SetParameters,
|
||||
ctrl release Release,
|
||||
}
|
||||
}
|
||||
29
staging/vhost-device-sound/src/audio_backends/null.rs
Normal file
29
staging/vhost-device-sound/src/audio_backends/null.rs
Normal file
@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use super::AudioBackend;
|
||||
use crate::{Result, Stream};
|
||||
|
||||
pub struct NullBackend {
|
||||
streams: Arc<RwLock<Vec<Stream>>>,
|
||||
}
|
||||
|
||||
impl NullBackend {
|
||||
pub fn new(streams: Arc<RwLock<Vec<Stream>>>) -> Self {
|
||||
Self { streams }
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioBackend for NullBackend {
|
||||
fn write(&self, stream_id: u32) -> Result<()> {
|
||||
log::trace!("NullBackend write stream_id {}", stream_id);
|
||||
_ = std::mem::take(&mut self.streams.write().unwrap()[stream_id as usize].buffers);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(&self, _id: u32) -> Result<()> {
|
||||
log::trace!("NullBackend read stream_id {}", _id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
465
staging/vhost-device-sound/src/audio_backends/pipewire.rs
Normal file
465
staging/vhost-device-sound/src/audio_backends/pipewire.rs
Normal file
@ -0,0 +1,465 @@
|
||||
// Pipewire backend device
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::TryInto,
|
||||
mem::size_of,
|
||||
ptr,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use log::debug;
|
||||
use pw::{properties, spa, sys::PW_ID_CORE, Context, Core, ThreadLoop};
|
||||
use spa::{
|
||||
param::{
|
||||
audio::{AudioFormat, AudioInfoRaw},
|
||||
ParamType,
|
||||
},
|
||||
pod::{serialize::PodSerializer, Object, Pod, Value},
|
||||
sys::{
|
||||
spa_audio_info_raw, SPA_PARAM_EnumFormat, SPA_TYPE_OBJECT_Format, SPA_AUDIO_CHANNEL_FC,
|
||||
SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_MONO,
|
||||
SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR,
|
||||
SPA_AUDIO_CHANNEL_UNKNOWN, SPA_AUDIO_FORMAT_ALAW, SPA_AUDIO_FORMAT_F32,
|
||||
SPA_AUDIO_FORMAT_F64, SPA_AUDIO_FORMAT_S16, SPA_AUDIO_FORMAT_S18_LE, SPA_AUDIO_FORMAT_S20,
|
||||
SPA_AUDIO_FORMAT_S20_LE, SPA_AUDIO_FORMAT_S24, SPA_AUDIO_FORMAT_S24_LE,
|
||||
SPA_AUDIO_FORMAT_S32, SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_U16, SPA_AUDIO_FORMAT_U18_LE,
|
||||
SPA_AUDIO_FORMAT_U20, SPA_AUDIO_FORMAT_U20_LE, SPA_AUDIO_FORMAT_U24,
|
||||
SPA_AUDIO_FORMAT_U24_LE, SPA_AUDIO_FORMAT_U32, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_ULAW,
|
||||
SPA_AUDIO_FORMAT_UNKNOWN,
|
||||
},
|
||||
};
|
||||
use virtio_queue::Descriptor;
|
||||
use vm_memory::Bytes;
|
||||
|
||||
use super::AudioBackend;
|
||||
use crate::{
|
||||
device::ControlMessage,
|
||||
virtio_sound::{
|
||||
VirtioSndPcmSetParams, VIRTIO_SND_D_INPUT, VIRTIO_SND_D_OUTPUT, VIRTIO_SND_PCM_FMT_A_LAW,
|
||||
VIRTIO_SND_PCM_FMT_FLOAT, VIRTIO_SND_PCM_FMT_FLOAT64, VIRTIO_SND_PCM_FMT_MU_LAW,
|
||||
VIRTIO_SND_PCM_FMT_S16, VIRTIO_SND_PCM_FMT_S18_3, VIRTIO_SND_PCM_FMT_S20,
|
||||
VIRTIO_SND_PCM_FMT_S20_3, VIRTIO_SND_PCM_FMT_S24, VIRTIO_SND_PCM_FMT_S24_3,
|
||||
VIRTIO_SND_PCM_FMT_S32, VIRTIO_SND_PCM_FMT_S8, VIRTIO_SND_PCM_FMT_U16,
|
||||
VIRTIO_SND_PCM_FMT_U18_3, VIRTIO_SND_PCM_FMT_U20, VIRTIO_SND_PCM_FMT_U20_3,
|
||||
VIRTIO_SND_PCM_FMT_U24, VIRTIO_SND_PCM_FMT_U24_3, VIRTIO_SND_PCM_FMT_U32,
|
||||
VIRTIO_SND_PCM_FMT_U8, VIRTIO_SND_PCM_RATE_11025, VIRTIO_SND_PCM_RATE_16000,
|
||||
VIRTIO_SND_PCM_RATE_176400, VIRTIO_SND_PCM_RATE_192000, VIRTIO_SND_PCM_RATE_22050,
|
||||
VIRTIO_SND_PCM_RATE_32000, VIRTIO_SND_PCM_RATE_384000, VIRTIO_SND_PCM_RATE_44100,
|
||||
VIRTIO_SND_PCM_RATE_48000, VIRTIO_SND_PCM_RATE_5512, VIRTIO_SND_PCM_RATE_64000,
|
||||
VIRTIO_SND_PCM_RATE_8000, VIRTIO_SND_PCM_RATE_88200, VIRTIO_SND_PCM_RATE_96000,
|
||||
VIRTIO_SND_S_BAD_MSG, VIRTIO_SND_S_NOT_SUPP,
|
||||
},
|
||||
Error, Result, Stream,
|
||||
};
|
||||
|
||||
// SAFETY: Safe as the structure can be sent to another thread.
|
||||
unsafe impl Send for PwBackend {}
|
||||
|
||||
// SAFETY: Safe as the structure can be shared with another thread as the state
|
||||
// is protected with a lock.
|
||||
unsafe impl Sync for PwBackend {}
|
||||
|
||||
pub struct PwBackend {
|
||||
pub stream_params: Arc<RwLock<Vec<Stream>>>,
|
||||
thread_loop: ThreadLoop,
|
||||
pub core: Core,
|
||||
#[allow(dead_code)]
|
||||
context: Context,
|
||||
pub stream_hash: RwLock<HashMap<u32, pw::stream::Stream>>,
|
||||
pub stream_listener: RwLock<HashMap<u32, pw::stream::StreamListener<i32>>>,
|
||||
}
|
||||
|
||||
impl PwBackend {
|
||||
pub fn new(stream_params: Arc<RwLock<Vec<Stream>>>) -> Self {
|
||||
pw::init();
|
||||
|
||||
let thread_loop = unsafe { ThreadLoop::new(Some("Pipewire thread loop")).unwrap() };
|
||||
|
||||
let lock_guard = thread_loop.lock();
|
||||
|
||||
let context = pw::Context::new(&thread_loop).expect("failed to create context");
|
||||
thread_loop.start();
|
||||
let core = context.connect(None).expect("Failed to connect to core");
|
||||
|
||||
// Create new reference for the variable so that it can be moved into the
|
||||
// closure.
|
||||
let thread_clone = thread_loop.clone();
|
||||
|
||||
// Trigger the sync event. The server's answer won't be processed until we start
|
||||
// the thread loop, so we can safely do this before setting up a
|
||||
// callback. This lets us avoid using a Cell.
|
||||
let pending = core.sync(0).expect("sync failed");
|
||||
let _listener_core = core
|
||||
.add_listener_local()
|
||||
.done(move |id, seq| {
|
||||
if id == PW_ID_CORE && seq == pending {
|
||||
thread_clone.signal(false);
|
||||
}
|
||||
})
|
||||
.register();
|
||||
|
||||
thread_loop.wait();
|
||||
lock_guard.unlock();
|
||||
|
||||
log::trace!("pipewire backend running");
|
||||
|
||||
Self {
|
||||
stream_params,
|
||||
thread_loop,
|
||||
core,
|
||||
context,
|
||||
stream_hash: RwLock::new(HashMap::new()),
|
||||
stream_listener: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioBackend for PwBackend {
|
||||
fn write(&self, _stream_id: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(&self, _stream_id: u32) -> Result<()> {
|
||||
log::trace!("PipewireBackend read stream_id {}", _stream_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_parameters(&self, stream_id: u32, mut msg: ControlMessage) -> Result<()> {
|
||||
let descriptors: Vec<Descriptor> = msg.desc_chain.clone().collect();
|
||||
let desc_request = &descriptors[0];
|
||||
let request = msg
|
||||
.desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSndPcmSetParams>(desc_request.addr())
|
||||
.unwrap();
|
||||
{
|
||||
let stream_clone = self.stream_params.clone();
|
||||
let mut stream_params = stream_clone.write().unwrap();
|
||||
let st = stream_params
|
||||
.get_mut(stream_id as usize)
|
||||
.expect("Stream does not exist");
|
||||
if let Err(err) = st.state.set_parameters() {
|
||||
log::error!("Stream {} set_parameters {}", stream_id, err);
|
||||
msg.code = VIRTIO_SND_S_BAD_MSG;
|
||||
} else if !st.supports_format(request.format) || !st.supports_rate(request.rate) {
|
||||
msg.code = VIRTIO_SND_S_NOT_SUPP;
|
||||
} else {
|
||||
st.params.features = request.features;
|
||||
st.params.buffer_bytes = request.buffer_bytes;
|
||||
st.params.period_bytes = request.period_bytes;
|
||||
st.params.channels = request.channels;
|
||||
st.params.format = request.format;
|
||||
st.params.rate = request.rate;
|
||||
}
|
||||
}
|
||||
drop(msg);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare(&self, stream_id: u32) -> Result<()> {
|
||||
debug!("pipewire prepare");
|
||||
let prepare_result = self.stream_params.write().unwrap()[stream_id as usize]
|
||||
.state
|
||||
.prepare();
|
||||
if let Err(err) = prepare_result {
|
||||
log::error!("Stream {} prepare {}", stream_id, err);
|
||||
} else {
|
||||
let mut stream_hash = self.stream_hash.write().unwrap();
|
||||
let mut stream_listener = self.stream_listener.write().unwrap();
|
||||
let lock_guard = self.thread_loop.lock();
|
||||
let stream_params = self.stream_params.read().unwrap();
|
||||
|
||||
let params = &stream_params[stream_id as usize].params;
|
||||
|
||||
let mut pos: [u32; 64] = [SPA_AUDIO_CHANNEL_UNKNOWN; 64];
|
||||
|
||||
match params.channels {
|
||||
6 => {
|
||||
pos[0] = SPA_AUDIO_CHANNEL_FL;
|
||||
pos[1] = SPA_AUDIO_CHANNEL_FR;
|
||||
pos[2] = SPA_AUDIO_CHANNEL_FC;
|
||||
pos[3] = SPA_AUDIO_CHANNEL_LFE;
|
||||
pos[4] = SPA_AUDIO_CHANNEL_RL;
|
||||
pos[5] = SPA_AUDIO_CHANNEL_RR;
|
||||
}
|
||||
5 => {
|
||||
pos[0] = SPA_AUDIO_CHANNEL_FL;
|
||||
pos[1] = SPA_AUDIO_CHANNEL_FR;
|
||||
pos[2] = SPA_AUDIO_CHANNEL_FC;
|
||||
pos[3] = SPA_AUDIO_CHANNEL_LFE;
|
||||
pos[4] = SPA_AUDIO_CHANNEL_RC;
|
||||
}
|
||||
4 => {
|
||||
pos[0] = SPA_AUDIO_CHANNEL_FL;
|
||||
pos[1] = SPA_AUDIO_CHANNEL_FR;
|
||||
pos[2] = SPA_AUDIO_CHANNEL_FC;
|
||||
pos[3] = SPA_AUDIO_CHANNEL_RC;
|
||||
}
|
||||
3 => {
|
||||
pos[0] = SPA_AUDIO_CHANNEL_FL;
|
||||
pos[1] = SPA_AUDIO_CHANNEL_FR;
|
||||
pos[2] = SPA_AUDIO_CHANNEL_LFE;
|
||||
}
|
||||
2 => {
|
||||
pos[0] = SPA_AUDIO_CHANNEL_FL;
|
||||
pos[1] = SPA_AUDIO_CHANNEL_FR;
|
||||
}
|
||||
1 => {
|
||||
pos[0] = SPA_AUDIO_CHANNEL_MONO;
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::ChannelNotSupported(params.channels));
|
||||
}
|
||||
}
|
||||
|
||||
let info = spa_audio_info_raw {
|
||||
format: match params.format {
|
||||
VIRTIO_SND_PCM_FMT_MU_LAW => SPA_AUDIO_FORMAT_ULAW,
|
||||
VIRTIO_SND_PCM_FMT_A_LAW => SPA_AUDIO_FORMAT_ALAW,
|
||||
VIRTIO_SND_PCM_FMT_S8 => SPA_AUDIO_FORMAT_S8,
|
||||
VIRTIO_SND_PCM_FMT_U8 => SPA_AUDIO_FORMAT_U8,
|
||||
VIRTIO_SND_PCM_FMT_S16 => SPA_AUDIO_FORMAT_S16,
|
||||
VIRTIO_SND_PCM_FMT_U16 => SPA_AUDIO_FORMAT_U16,
|
||||
VIRTIO_SND_PCM_FMT_S18_3 => SPA_AUDIO_FORMAT_S18_LE,
|
||||
VIRTIO_SND_PCM_FMT_U18_3 => SPA_AUDIO_FORMAT_U18_LE,
|
||||
VIRTIO_SND_PCM_FMT_S20_3 => SPA_AUDIO_FORMAT_S20_LE,
|
||||
VIRTIO_SND_PCM_FMT_U20_3 => SPA_AUDIO_FORMAT_U20_LE,
|
||||
VIRTIO_SND_PCM_FMT_S24_3 => SPA_AUDIO_FORMAT_S24_LE,
|
||||
VIRTIO_SND_PCM_FMT_U24_3 => SPA_AUDIO_FORMAT_U24_LE,
|
||||
VIRTIO_SND_PCM_FMT_S20 => SPA_AUDIO_FORMAT_S20,
|
||||
VIRTIO_SND_PCM_FMT_U20 => SPA_AUDIO_FORMAT_U20,
|
||||
VIRTIO_SND_PCM_FMT_S24 => SPA_AUDIO_FORMAT_S24,
|
||||
VIRTIO_SND_PCM_FMT_U24 => SPA_AUDIO_FORMAT_U24,
|
||||
VIRTIO_SND_PCM_FMT_S32 => SPA_AUDIO_FORMAT_S32,
|
||||
VIRTIO_SND_PCM_FMT_U32 => SPA_AUDIO_FORMAT_U32,
|
||||
VIRTIO_SND_PCM_FMT_FLOAT => SPA_AUDIO_FORMAT_F32,
|
||||
VIRTIO_SND_PCM_FMT_FLOAT64 => SPA_AUDIO_FORMAT_F64,
|
||||
_ => SPA_AUDIO_FORMAT_UNKNOWN,
|
||||
},
|
||||
rate: match params.rate {
|
||||
VIRTIO_SND_PCM_RATE_5512 => 5512,
|
||||
VIRTIO_SND_PCM_RATE_8000 => 8000,
|
||||
VIRTIO_SND_PCM_RATE_11025 => 11025,
|
||||
VIRTIO_SND_PCM_RATE_16000 => 16000,
|
||||
VIRTIO_SND_PCM_RATE_22050 => 22050,
|
||||
VIRTIO_SND_PCM_RATE_32000 => 32000,
|
||||
VIRTIO_SND_PCM_RATE_44100 => 44100,
|
||||
VIRTIO_SND_PCM_RATE_48000 => 48000,
|
||||
VIRTIO_SND_PCM_RATE_64000 => 64000,
|
||||
VIRTIO_SND_PCM_RATE_88200 => 88200,
|
||||
VIRTIO_SND_PCM_RATE_96000 => 96000,
|
||||
VIRTIO_SND_PCM_RATE_176400 => 176400,
|
||||
VIRTIO_SND_PCM_RATE_192000 => 192000,
|
||||
VIRTIO_SND_PCM_RATE_384000 => 384000,
|
||||
_ => 44100,
|
||||
},
|
||||
flags: 0,
|
||||
channels: params.channels as u32,
|
||||
position: pos,
|
||||
};
|
||||
|
||||
let mut audio_info = AudioInfoRaw::new();
|
||||
audio_info.set_format(AudioFormat::S16LE);
|
||||
audio_info.set_rate(info.rate);
|
||||
audio_info.set_channels(info.channels);
|
||||
|
||||
let values: Vec<u8> = PodSerializer::serialize(
|
||||
std::io::Cursor::new(Vec::new()),
|
||||
&Value::Object(Object {
|
||||
type_: SPA_TYPE_OBJECT_Format,
|
||||
id: SPA_PARAM_EnumFormat,
|
||||
properties: audio_info.into(),
|
||||
}),
|
||||
)
|
||||
.unwrap()
|
||||
.0
|
||||
.into_inner();
|
||||
|
||||
let value_clone = values.clone();
|
||||
|
||||
let mut param = [Pod::from_bytes(&values).unwrap()];
|
||||
|
||||
let props = properties! {
|
||||
*pw::keys::MEDIA_TYPE => "Audio",
|
||||
*pw::keys::MEDIA_CATEGORY => "Playback",
|
||||
};
|
||||
|
||||
let stream = pw::stream::Stream::new(&self.core, "audio-output", props)
|
||||
.expect("could not create new stream");
|
||||
|
||||
let streams = self.stream_params.clone();
|
||||
|
||||
let listener_stream = stream
|
||||
.add_local_listener()
|
||||
.state_changed(|old, new| {
|
||||
debug!("State changed: {:?} -> {:?}", old, new);
|
||||
})
|
||||
.param_changed(move |stream, id, _data, param| {
|
||||
let Some(_param) = param else {
|
||||
return;
|
||||
};
|
||||
if id != ParamType::Format.as_raw() {
|
||||
return;
|
||||
}
|
||||
let mut param = [Pod::from_bytes(&value_clone).unwrap()];
|
||||
|
||||
//callback to negotiate new set of streams
|
||||
stream
|
||||
.update_params(&mut param)
|
||||
.expect("could not update params");
|
||||
})
|
||||
.process(move |stream, _data| match stream.dequeue_buffer() {
|
||||
None => debug!("No buffer recieved"),
|
||||
Some(mut buf) => {
|
||||
let datas = buf.datas_mut();
|
||||
let frame_size = info.channels * size_of::<i16>() as u32;
|
||||
let data = &mut datas[0];
|
||||
let n_bytes = if let Some(slice) = data.data() {
|
||||
let mut n_bytes = slice.len();
|
||||
let mut streams = streams.write().unwrap();
|
||||
let streams = streams
|
||||
.get_mut(stream_id as usize)
|
||||
.expect("Stream does not exist");
|
||||
let Some(buffer) = streams.buffers.front_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut start = buffer.pos;
|
||||
|
||||
let avail = (buffer.data_descriptor.len() - start as u32) as i32;
|
||||
|
||||
if avail < n_bytes as i32 {
|
||||
n_bytes = avail.try_into().unwrap();
|
||||
}
|
||||
let p = &mut slice[0..n_bytes];
|
||||
if avail <= 0 {
|
||||
// pad with silence
|
||||
unsafe {
|
||||
ptr::write_bytes(p.as_mut_ptr(), 0, n_bytes);
|
||||
}
|
||||
} else {
|
||||
// consume() always reads (buffer.data_descriptor.len() -
|
||||
// buffer.pos) bytes
|
||||
buffer.consume(p).expect("failed to read buffer from guest");
|
||||
|
||||
start += n_bytes;
|
||||
|
||||
buffer.pos = start;
|
||||
|
||||
if start >= buffer.data_descriptor.len() as usize {
|
||||
streams.buffers.pop_front();
|
||||
}
|
||||
}
|
||||
n_bytes
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let chunk = data.chunk_mut();
|
||||
*chunk.offset_mut() = 0;
|
||||
*chunk.stride_mut() = frame_size as _;
|
||||
*chunk.size_mut() = n_bytes as _;
|
||||
}
|
||||
})
|
||||
.register()
|
||||
.expect("failed to register stream listener");
|
||||
|
||||
stream_listener.insert(stream_id, listener_stream);
|
||||
|
||||
let direction = match stream_params[stream_id as usize].direction {
|
||||
VIRTIO_SND_D_OUTPUT => spa::Direction::Output,
|
||||
VIRTIO_SND_D_INPUT => spa::Direction::Input,
|
||||
_ => panic!("Invalid direction"),
|
||||
};
|
||||
|
||||
stream
|
||||
.connect(
|
||||
direction,
|
||||
Some(pw::constants::ID_ANY),
|
||||
pw::stream::StreamFlags::RT_PROCESS
|
||||
| pw::stream::StreamFlags::AUTOCONNECT
|
||||
| pw::stream::StreamFlags::INACTIVE
|
||||
| pw::stream::StreamFlags::MAP_BUFFERS,
|
||||
&mut param,
|
||||
)
|
||||
.expect("could not connect to the stream");
|
||||
|
||||
// insert created stream in a hash table
|
||||
stream_hash.insert(stream_id, stream);
|
||||
|
||||
lock_guard.unlock();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&self, stream_id: u32, mut msg: ControlMessage) -> Result<()> {
|
||||
debug!("pipewire backend, release function");
|
||||
let release_result = self.stream_params.write().unwrap()[stream_id as usize]
|
||||
.state
|
||||
.release();
|
||||
if let Err(err) = release_result {
|
||||
log::error!("Stream {} release {}", stream_id, err);
|
||||
msg.code = VIRTIO_SND_S_BAD_MSG;
|
||||
} else {
|
||||
let lock_guard = self.thread_loop.lock();
|
||||
let mut stream_hash = self.stream_hash.write().unwrap();
|
||||
let mut stream_listener = self.stream_listener.write().unwrap();
|
||||
let st_buffer = &mut self.stream_params.write().unwrap();
|
||||
|
||||
let Some(stream) = stream_hash.get(&stream_id) else {
|
||||
return Err(Error::StreamWithIdNotFound(stream_id));
|
||||
};
|
||||
stream.disconnect().expect("could not disconnect stream");
|
||||
std::mem::take(&mut st_buffer[stream_id as usize].buffers);
|
||||
stream_hash.remove(&stream_id);
|
||||
stream_listener.remove(&stream_id);
|
||||
|
||||
lock_guard.unlock();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&self, stream_id: u32) -> Result<()> {
|
||||
debug!("pipewire start");
|
||||
let start_result = self.stream_params.write().unwrap()[stream_id as usize]
|
||||
.state
|
||||
.start();
|
||||
if let Err(err) = start_result {
|
||||
// log the error and continue
|
||||
log::error!("Stream {} start {}", stream_id, err);
|
||||
} else {
|
||||
let lock_guard = self.thread_loop.lock();
|
||||
let stream_hash = self.stream_hash.read().unwrap();
|
||||
let Some(stream) = stream_hash.get(&stream_id) else {
|
||||
return Err(Error::StreamWithIdNotFound(stream_id));
|
||||
};
|
||||
stream.set_active(true).expect("could not start stream");
|
||||
lock_guard.unlock();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self, stream_id: u32) -> Result<()> {
|
||||
debug!("pipewire stop");
|
||||
let stop_result = self.stream_params.write().unwrap()[stream_id as usize]
|
||||
.state
|
||||
.stop();
|
||||
if let Err(err) = stop_result {
|
||||
log::error!("Stream {} stop {}", stream_id, err);
|
||||
} else {
|
||||
let lock_guard = self.thread_loop.lock();
|
||||
let stream_hash = self.stream_hash.read().unwrap();
|
||||
let Some(stream) = stream_hash.get(&stream_id) else {
|
||||
return Err(Error::StreamWithIdNotFound(stream_id));
|
||||
};
|
||||
stream.set_active(false).expect("could not stop stream");
|
||||
lock_guard.unlock();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
816
staging/vhost-device-sound/src/device.rs
Normal file
816
staging/vhost-device-sound/src/device.rs
Normal file
@ -0,0 +1,816 @@
|
||||
// Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
|
||||
// Stefano Garzarella <sgarzare@redhat.com>
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
convert::TryFrom,
|
||||
io::Result as IoResult,
|
||||
mem::size_of,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures};
|
||||
use vhost_user_backend::{VhostUserBackend, VringRwLock, VringT};
|
||||
use virtio_bindings::{
|
||||
bindings::virtio_config::{VIRTIO_F_NOTIFY_ON_EMPTY, VIRTIO_F_VERSION_1},
|
||||
virtio_ring::{VIRTIO_RING_F_EVENT_IDX, VIRTIO_RING_F_INDIRECT_DESC},
|
||||
};
|
||||
use virtio_queue::{DescriptorChain, QueueOwnedT};
|
||||
use vm_memory::{
|
||||
ByteValued, Bytes, GuestAddressSpace, GuestMemoryAtomic, GuestMemoryLoadGuard, GuestMemoryMmap,
|
||||
};
|
||||
use vmm_sys_util::{
|
||||
epoll::EventSet,
|
||||
eventfd::{EventFd, EFD_NONBLOCK},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
audio_backends::{alloc_audio_backend, AudioBackend},
|
||||
stream::{Buffer, Error as StreamError, Stream},
|
||||
virtio_sound::{self, *},
|
||||
ControlMessageKind, Error, IOMessage, Result, SoundConfig,
|
||||
};
|
||||
|
||||
struct VhostUserSoundThread {
|
||||
mem: Option<GuestMemoryAtomic<GuestMemoryMmap>>,
|
||||
event_idx: bool,
|
||||
chmaps: Arc<RwLock<Vec<VirtioSoundChmapInfo>>>,
|
||||
jacks: Arc<RwLock<Vec<VirtioSoundJackInfo>>>,
|
||||
queue_indexes: Vec<u16>,
|
||||
streams: Arc<RwLock<Vec<Stream>>>,
|
||||
streams_no: usize,
|
||||
}
|
||||
|
||||
type SoundDescriptorChain = DescriptorChain<GuestMemoryLoadGuard<GuestMemoryMmap<()>>>;
|
||||
|
||||
impl VhostUserSoundThread {
|
||||
pub fn new(
|
||||
chmaps: Arc<RwLock<Vec<VirtioSoundChmapInfo>>>,
|
||||
jacks: Arc<RwLock<Vec<VirtioSoundJackInfo>>>,
|
||||
mut queue_indexes: Vec<u16>,
|
||||
streams: Arc<RwLock<Vec<Stream>>>,
|
||||
streams_no: usize,
|
||||
) -> Result<Self> {
|
||||
queue_indexes.sort();
|
||||
|
||||
Ok(Self {
|
||||
event_idx: false,
|
||||
mem: None,
|
||||
chmaps,
|
||||
jacks,
|
||||
queue_indexes,
|
||||
streams,
|
||||
streams_no,
|
||||
})
|
||||
}
|
||||
|
||||
fn queues_per_thread(&self) -> u64 {
|
||||
let mut queues_per_thread = 0u64;
|
||||
|
||||
for idx in self.queue_indexes.iter() {
|
||||
queues_per_thread |= 1u64 << idx
|
||||
}
|
||||
|
||||
queues_per_thread
|
||||
}
|
||||
|
||||
fn set_event_idx(&mut self, enabled: bool) {
|
||||
self.event_idx = enabled;
|
||||
}
|
||||
|
||||
fn update_memory(&mut self, mem: GuestMemoryAtomic<GuestMemoryMmap>) -> IoResult<()> {
|
||||
self.mem = Some(mem);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(
|
||||
&self,
|
||||
device_event: u16,
|
||||
vrings: &[VringRwLock],
|
||||
audio_backend: &RwLock<Box<dyn AudioBackend + Send + Sync>>,
|
||||
) -> IoResult<bool> {
|
||||
let vring = &vrings[device_event as usize];
|
||||
let queue_idx = self.queue_indexes[device_event as usize];
|
||||
if self.event_idx {
|
||||
// vm-virtio's Queue implementation only checks avail_index
|
||||
// once, so to properly support EVENT_IDX we need to keep
|
||||
// calling process_request_queue() until it stops finding
|
||||
// new requests on the queue.
|
||||
loop {
|
||||
vring.disable_notification().unwrap();
|
||||
match queue_idx {
|
||||
CONTROL_QUEUE_IDX => self.process_control(vring, audio_backend),
|
||||
EVENT_QUEUE_IDX => self.process_event(vring),
|
||||
TX_QUEUE_IDX => self.process_tx(vring, audio_backend),
|
||||
RX_QUEUE_IDX => self.process_rx(vring, audio_backend),
|
||||
_ => Err(Error::HandleUnknownEvent.into()),
|
||||
}?;
|
||||
if !vring.enable_notification().unwrap() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Without EVENT_IDX, a single call is enough.
|
||||
match queue_idx {
|
||||
CONTROL_QUEUE_IDX => self.process_control(vring, audio_backend),
|
||||
EVENT_QUEUE_IDX => self.process_event(vring),
|
||||
TX_QUEUE_IDX => self.process_tx(vring, audio_backend),
|
||||
RX_QUEUE_IDX => self.process_rx(vring, audio_backend),
|
||||
_ => Err(Error::HandleUnknownEvent.into()),
|
||||
}?;
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn process_control(
|
||||
&self,
|
||||
vring: &VringRwLock,
|
||||
audio_backend: &RwLock<Box<dyn AudioBackend + Send + Sync>>,
|
||||
) -> IoResult<bool> {
|
||||
let requests: Vec<SoundDescriptorChain> = vring
|
||||
.get_mut()
|
||||
.get_queue_mut()
|
||||
.iter(self.mem.as_ref().unwrap().memory())
|
||||
.map_err(|_| Error::DescriptorNotFound)?
|
||||
.collect();
|
||||
|
||||
if requests.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Reply to some requests right away, and defer others to the audio backend (for
|
||||
// example PcmRelease needs to complete all I/O before replying.
|
||||
//
|
||||
// Mark `any` as true if we need to reply to any request right away so we can
|
||||
// signal the queue as used.
|
||||
let mut any = false;
|
||||
|
||||
for desc_chain in requests {
|
||||
let descriptors: Vec<_> = desc_chain.clone().collect();
|
||||
if descriptors.len() < 2 {
|
||||
return Err(Error::UnexpectedDescriptorCount(descriptors.len()).into());
|
||||
}
|
||||
|
||||
// Request descriptor.
|
||||
let desc_request = descriptors[0];
|
||||
if desc_request.is_write_only() {
|
||||
return Err(Error::UnexpectedWriteOnlyDescriptor(0).into());
|
||||
}
|
||||
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundHeader>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
|
||||
// Keep track of bytes that will be written in the VQ.
|
||||
let mut used_len = 0;
|
||||
|
||||
// Reply header descriptor.
|
||||
let desc_hdr = descriptors[1];
|
||||
if !desc_hdr.is_write_only() {
|
||||
return Err(Error::UnexpectedReadableDescriptor(1).into());
|
||||
}
|
||||
|
||||
let mut resp = VirtioSoundHeader {
|
||||
code: VIRTIO_SND_S_OK.into(),
|
||||
};
|
||||
|
||||
let code = ControlMessageKind::try_from(request.code).map_err(Error::from)?;
|
||||
match code {
|
||||
ControlMessageKind::ChmapInfo => {
|
||||
if descriptors.len() != 3 {
|
||||
log::error!("a CHMAP_INFO request should have three descriptors total.");
|
||||
return Err(Error::UnexpectedDescriptorCount(descriptors.len()).into());
|
||||
} else if !descriptors[2].is_write_only() {
|
||||
log::error!(
|
||||
"a CHMAP_INFO request should have a writeable descriptor for the info \
|
||||
payload response after the header status response"
|
||||
);
|
||||
return Err(Error::UnexpectedReadableDescriptor(2).into());
|
||||
}
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundQueryInfo>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
let start_id = u32::from(request.start_id) as usize;
|
||||
let count = u32::from(request.count) as usize;
|
||||
let chmaps = self.chmaps.read().unwrap();
|
||||
if chmaps.len() <= start_id || chmaps.len() < start_id + count {
|
||||
resp.code = VIRTIO_SND_S_BAD_MSG.into();
|
||||
} else {
|
||||
let desc_response = descriptors[2];
|
||||
let mut buf = vec![];
|
||||
|
||||
for i in chmaps.iter().skip(start_id).take(count) {
|
||||
buf.extend_from_slice(i.as_slice());
|
||||
}
|
||||
desc_chain
|
||||
.memory()
|
||||
.write_slice(&buf, desc_response.addr())
|
||||
.map_err(|_| Error::DescriptorWriteFailed)?;
|
||||
used_len += desc_response.len();
|
||||
}
|
||||
}
|
||||
ControlMessageKind::JackInfo => {
|
||||
if descriptors.len() != 3 {
|
||||
log::error!("a JACK_INFO request should have three descriptors total.");
|
||||
return Err(Error::UnexpectedDescriptorCount(descriptors.len()).into());
|
||||
} else if !descriptors[2].is_write_only() {
|
||||
log::error!(
|
||||
"a JACK_INFO request should have a writeable descriptor for the info \
|
||||
payload response after the header status response"
|
||||
);
|
||||
return Err(Error::UnexpectedReadableDescriptor(2).into());
|
||||
}
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundQueryInfo>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
|
||||
let start_id = u32::from(request.start_id) as usize;
|
||||
let count = u32::from(request.count) as usize;
|
||||
let jacks = self.jacks.read().unwrap();
|
||||
if jacks.len() <= start_id || jacks.len() < start_id + count {
|
||||
resp.code = VIRTIO_SND_S_BAD_MSG.into();
|
||||
} else {
|
||||
let desc_response = descriptors[2];
|
||||
let mut buf = vec![];
|
||||
|
||||
for i in jacks.iter().skip(start_id).take(count) {
|
||||
buf.extend_from_slice(i.as_slice());
|
||||
}
|
||||
desc_chain
|
||||
.memory()
|
||||
.write_slice(&buf, desc_response.addr())
|
||||
.map_err(|_| Error::DescriptorWriteFailed)?;
|
||||
used_len += desc_response.len();
|
||||
}
|
||||
}
|
||||
ControlMessageKind::JackRemap => {
|
||||
resp.code = VIRTIO_SND_S_NOT_SUPP.into();
|
||||
}
|
||||
ControlMessageKind::PcmInfo => {
|
||||
if descriptors.len() != 3 {
|
||||
log::error!("a PCM_INFO request should have three descriptors total.");
|
||||
return Err(Error::UnexpectedDescriptorCount(descriptors.len()).into());
|
||||
} else if !descriptors[2].is_write_only() {
|
||||
log::error!(
|
||||
"a PCM_INFO request should have a writeable descriptor for the info \
|
||||
payload response after the header status response"
|
||||
);
|
||||
return Err(Error::UnexpectedReadableDescriptor(2).into());
|
||||
}
|
||||
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundQueryInfo>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
|
||||
let start_id = u32::from(request.start_id) as usize;
|
||||
let count = u32::from(request.count) as usize;
|
||||
let streams = self.streams.read().unwrap();
|
||||
if streams.len() <= start_id || streams.len() < start_id + count {
|
||||
resp.code = VIRTIO_SND_S_BAD_MSG.into();
|
||||
} else {
|
||||
let desc_response = descriptors[2];
|
||||
|
||||
let mut buf = vec![];
|
||||
let mut p: VirtioSoundPcmInfo;
|
||||
|
||||
for s in streams
|
||||
.iter()
|
||||
.skip(u32::from(request.start_id) as usize)
|
||||
.take(u32::from(request.count) as usize)
|
||||
{
|
||||
p = VirtioSoundPcmInfo::default();
|
||||
p.hdr.hda_fn_nid = 0.into();
|
||||
p.features = s.params.features;
|
||||
p.formats = s.formats;
|
||||
p.rates = s.rates;
|
||||
p.direction = s.direction;
|
||||
p.channels_min = s.channels_min;
|
||||
p.channels_max = s.channels_max;
|
||||
buf.extend_from_slice(p.as_slice());
|
||||
}
|
||||
desc_chain
|
||||
.memory()
|
||||
.write_slice(&buf, desc_response.addr())
|
||||
.map_err(|_| Error::DescriptorWriteFailed)?;
|
||||
used_len += desc_response.len();
|
||||
}
|
||||
}
|
||||
ControlMessageKind::PcmSetParams => {
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSndPcmSetParams>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
let stream_id: u32 = request.hdr.stream_id.into();
|
||||
|
||||
if stream_id as usize >= self.streams_no {
|
||||
log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id)));
|
||||
resp.code = VIRTIO_SND_S_BAD_MSG.into();
|
||||
} else {
|
||||
audio_backend
|
||||
.read()
|
||||
.unwrap()
|
||||
.set_parameters(
|
||||
stream_id,
|
||||
ControlMessage {
|
||||
kind: code,
|
||||
code: VIRTIO_SND_S_OK,
|
||||
desc_chain,
|
||||
descriptor: desc_hdr,
|
||||
vring: vring.clone(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// PcmSetParams needs check valid formats/rates; the audio backend will
|
||||
// reply when it drops the ControlMessage.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ControlMessageKind::PcmPrepare => {
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundPcmHeader>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
let stream_id = request.stream_id.into();
|
||||
|
||||
if stream_id as usize >= self.streams_no {
|
||||
log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id)));
|
||||
resp.code = VIRTIO_SND_S_BAD_MSG.into();
|
||||
} else {
|
||||
audio_backend.write().unwrap().prepare(stream_id).unwrap();
|
||||
}
|
||||
}
|
||||
ControlMessageKind::PcmRelease => {
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundPcmHeader>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
let stream_id = request.stream_id.into();
|
||||
|
||||
if stream_id as usize >= self.streams_no {
|
||||
log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id)));
|
||||
resp.code = VIRTIO_SND_S_BAD_MSG.into();
|
||||
} else {
|
||||
audio_backend
|
||||
.write()
|
||||
.unwrap()
|
||||
.release(
|
||||
stream_id,
|
||||
ControlMessage {
|
||||
kind: code,
|
||||
code: VIRTIO_SND_S_OK,
|
||||
desc_chain,
|
||||
descriptor: desc_hdr,
|
||||
vring: vring.clone(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// PcmRelease needs to flush IO messages; the audio backend will reply when
|
||||
// it drops the ControlMessage.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ControlMessageKind::PcmStart => {
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundPcmHeader>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
let stream_id = request.stream_id.into();
|
||||
|
||||
if stream_id as usize >= self.streams_no {
|
||||
log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id)));
|
||||
resp.code = VIRTIO_SND_S_BAD_MSG.into();
|
||||
} else {
|
||||
audio_backend.write().unwrap().start(stream_id).unwrap();
|
||||
}
|
||||
}
|
||||
ControlMessageKind::PcmStop => {
|
||||
let request = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundPcmHeader>(desc_request.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
let stream_id = request.stream_id.into();
|
||||
|
||||
if stream_id as usize >= self.streams_no {
|
||||
log::error!("{}", Error::from(StreamError::InvalidStreamId(stream_id)));
|
||||
resp.code = VIRTIO_SND_S_BAD_MSG.into();
|
||||
} else {
|
||||
audio_backend.write().unwrap().stop(stream_id).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!(
|
||||
"returned {} for ctrl msg {:?}",
|
||||
match u32::from(resp.code) {
|
||||
v if v == VIRTIO_SND_S_OK => "OK",
|
||||
v if v == VIRTIO_SND_S_BAD_MSG => "BAD_MSG",
|
||||
v if v == VIRTIO_SND_S_NOT_SUPP => "NOT_SUPP",
|
||||
v if v == VIRTIO_SND_S_IO_ERR => "IO_ERR",
|
||||
_ => unreachable!(),
|
||||
},
|
||||
code
|
||||
);
|
||||
desc_chain
|
||||
.memory()
|
||||
.write_obj(resp, desc_hdr.addr())
|
||||
.map_err(|_| Error::DescriptorWriteFailed)?;
|
||||
used_len += desc_hdr.len();
|
||||
|
||||
if vring.add_used(desc_chain.head_index(), used_len).is_err() {
|
||||
log::error!("Couldn't return used descriptors to the ring");
|
||||
}
|
||||
any |= true;
|
||||
}
|
||||
|
||||
// In which cases can happen that we get here and any is false?
|
||||
// PCM_RELEASE and PCM_SET_PARAMS need to be handled asynchronously, therefore
|
||||
// it will be their responsibility to `signal_used_queue`.
|
||||
|
||||
// Send notification if any request was processed
|
||||
if any && vring.signal_used_queue().is_err() {
|
||||
log::error!("Couldn't signal used queue");
|
||||
}
|
||||
|
||||
Ok(!any)
|
||||
}
|
||||
|
||||
fn process_event(&self, _vring: &VringRwLock) -> IoResult<bool> {
|
||||
log::trace!("process_event");
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn process_tx(
|
||||
&self,
|
||||
vring: &VringRwLock,
|
||||
audio_backend: &RwLock<Box<dyn AudioBackend + Send + Sync>>,
|
||||
) -> IoResult<bool> {
|
||||
let requests: Vec<SoundDescriptorChain> = vring
|
||||
.get_mut()
|
||||
.get_queue_mut()
|
||||
.iter(self.mem.as_ref().unwrap().memory())
|
||||
.map_err(|_| Error::DescriptorNotFound)?
|
||||
.collect();
|
||||
|
||||
if requests.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
enum TxState {
|
||||
Ready,
|
||||
WaitingBufferForStreamId(u32),
|
||||
Done,
|
||||
}
|
||||
|
||||
let mut stream_ids = BTreeSet::default();
|
||||
|
||||
for desc_chain in requests {
|
||||
let mut state = TxState::Ready;
|
||||
let mut buffers = vec![];
|
||||
let descriptors: Vec<_> = desc_chain.clone().collect();
|
||||
let message = Arc::new(IOMessage {
|
||||
vring: vring.clone(),
|
||||
status: VIRTIO_SND_S_OK.into(),
|
||||
desc_chain: desc_chain.clone(),
|
||||
descriptor: descriptors.last().cloned().unwrap(),
|
||||
});
|
||||
for descriptor in &descriptors {
|
||||
match state {
|
||||
TxState::Done => {
|
||||
return Err(Error::UnexpectedDescriptorCount(descriptors.len()).into());
|
||||
}
|
||||
TxState::Ready if descriptor.is_write_only() => {
|
||||
if descriptor.len() as usize != size_of::<VirtioSoundPcmStatus>() {
|
||||
return Err(Error::UnexpectedDescriptorSize(
|
||||
size_of::<VirtioSoundPcmStatus>(),
|
||||
descriptor.len(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
state = TxState::Done;
|
||||
}
|
||||
TxState::WaitingBufferForStreamId(stream_id) if descriptor.is_write_only() => {
|
||||
if descriptor.len() as usize != size_of::<VirtioSoundPcmStatus>() {
|
||||
return Err(Error::UnexpectedDescriptorSize(
|
||||
size_of::<VirtioSoundPcmStatus>(),
|
||||
descriptor.len(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let mut streams = self.streams.write().unwrap();
|
||||
for b in std::mem::take(&mut buffers) {
|
||||
streams[stream_id as usize].buffers.push_back(b);
|
||||
}
|
||||
state = TxState::Done;
|
||||
}
|
||||
TxState::Ready
|
||||
if descriptor.len() as usize != size_of::<VirtioSoundPcmXfer>() =>
|
||||
{
|
||||
return Err(Error::UnexpectedDescriptorSize(
|
||||
size_of::<VirtioSoundPcmXfer>(),
|
||||
descriptor.len(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
TxState::Ready => {
|
||||
let xfer = desc_chain
|
||||
.memory()
|
||||
.read_obj::<VirtioSoundPcmXfer>(descriptor.addr())
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
let stream_id: u32 = xfer.stream_id.into();
|
||||
stream_ids.insert(stream_id);
|
||||
|
||||
state = TxState::WaitingBufferForStreamId(stream_id);
|
||||
}
|
||||
TxState::WaitingBufferForStreamId(stream_id)
|
||||
if descriptor.len() as usize == size_of::<VirtioSoundPcmXfer>() =>
|
||||
{
|
||||
return Err(Error::UnexpectedDescriptorSize(
|
||||
u32::from(
|
||||
self.streams.read().unwrap()[stream_id as usize]
|
||||
.params
|
||||
.buffer_bytes,
|
||||
) as usize,
|
||||
descriptor.len(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
TxState::WaitingBufferForStreamId(_stream_id) => {
|
||||
/*
|
||||
Rather than copying the content of a descriptor, buffer keeps a pointer to it.
|
||||
When we copy just after the request is enqueued, the guest's userspace may or
|
||||
may not have updated the buffer contents. Guest driver simply moves buffers
|
||||
from the used ring to the available ring without knowing whether the content
|
||||
has been updated. The device only reads the buffer from guest memory when the
|
||||
audio engine requires it, which is about after a period thus ensuring that the
|
||||
buffer is up-to-date.
|
||||
*/
|
||||
buffers.push(Buffer::new(*descriptor, Arc::clone(&message)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !stream_ids.is_empty() {
|
||||
let b = audio_backend.write().unwrap();
|
||||
for id in stream_ids {
|
||||
b.write(id).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn process_rx(
|
||||
&self,
|
||||
_vring: &VringRwLock,
|
||||
_audio_backend: &RwLock<Box<dyn AudioBackend + Send + Sync>>,
|
||||
) -> IoResult<bool> {
|
||||
log::trace!("process_rx");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VhostUserSoundBackend {
|
||||
threads: Vec<RwLock<VhostUserSoundThread>>,
|
||||
virtio_cfg: VirtioSoundConfig,
|
||||
exit_event: EventFd,
|
||||
audio_backend: RwLock<Box<dyn AudioBackend + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl VhostUserSoundBackend {
|
||||
pub fn new(config: SoundConfig) -> Result<Self> {
|
||||
let streams = vec![
|
||||
Stream {
|
||||
id: 0,
|
||||
direction: VIRTIO_SND_D_OUTPUT,
|
||||
..Stream::default()
|
||||
},
|
||||
Stream {
|
||||
id: 1,
|
||||
direction: VIRTIO_SND_D_INPUT,
|
||||
..Stream::default()
|
||||
},
|
||||
];
|
||||
let streams_no = streams.len();
|
||||
let streams = Arc::new(RwLock::new(streams));
|
||||
let jacks: Arc<RwLock<Vec<VirtioSoundJackInfo>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
let mut positions = [VIRTIO_SND_CHMAP_NONE; VIRTIO_SND_CHMAP_MAX_SIZE];
|
||||
positions[0] = VIRTIO_SND_CHMAP_FL;
|
||||
positions[1] = VIRTIO_SND_CHMAP_FR;
|
||||
let chmaps_info: Vec<VirtioSoundChmapInfo> = vec![
|
||||
VirtioSoundChmapInfo {
|
||||
direction: VIRTIO_SND_D_OUTPUT,
|
||||
channels: 2,
|
||||
positions,
|
||||
..VirtioSoundChmapInfo::default()
|
||||
},
|
||||
VirtioSoundChmapInfo {
|
||||
direction: VIRTIO_SND_D_INPUT,
|
||||
channels: 2,
|
||||
positions,
|
||||
..VirtioSoundChmapInfo::default()
|
||||
},
|
||||
];
|
||||
let chmaps: Arc<RwLock<Vec<VirtioSoundChmapInfo>>> = Arc::new(RwLock::new(chmaps_info));
|
||||
log::trace!("VhostUserSoundBackend::new config {:?}", &config);
|
||||
let threads = if config.multi_thread {
|
||||
vec![
|
||||
RwLock::new(VhostUserSoundThread::new(
|
||||
chmaps.clone(),
|
||||
jacks.clone(),
|
||||
vec![CONTROL_QUEUE_IDX, EVENT_QUEUE_IDX],
|
||||
streams.clone(),
|
||||
streams_no,
|
||||
)?),
|
||||
RwLock::new(VhostUserSoundThread::new(
|
||||
chmaps.clone(),
|
||||
jacks.clone(),
|
||||
vec![TX_QUEUE_IDX],
|
||||
streams.clone(),
|
||||
streams_no,
|
||||
)?),
|
||||
RwLock::new(VhostUserSoundThread::new(
|
||||
chmaps.clone(),
|
||||
jacks.clone(),
|
||||
vec![RX_QUEUE_IDX],
|
||||
streams.clone(),
|
||||
streams_no,
|
||||
)?),
|
||||
]
|
||||
} else {
|
||||
vec![RwLock::new(VhostUserSoundThread::new(
|
||||
chmaps.clone(),
|
||||
jacks.clone(),
|
||||
vec![
|
||||
CONTROL_QUEUE_IDX,
|
||||
EVENT_QUEUE_IDX,
|
||||
TX_QUEUE_IDX,
|
||||
RX_QUEUE_IDX,
|
||||
],
|
||||
streams.clone(),
|
||||
streams_no,
|
||||
)?)]
|
||||
};
|
||||
|
||||
let audio_backend = alloc_audio_backend(config.audio_backend, streams)?;
|
||||
|
||||
Ok(Self {
|
||||
threads,
|
||||
virtio_cfg: VirtioSoundConfig {
|
||||
jacks: 0.into(),
|
||||
streams: 1.into(),
|
||||
chmaps: 1.into(),
|
||||
},
|
||||
exit_event: EventFd::new(EFD_NONBLOCK).map_err(Error::EventFdCreate)?,
|
||||
audio_backend: RwLock::new(audio_backend),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_exit_event(&self) {
|
||||
self.exit_event.write(1).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl VhostUserBackend<VringRwLock, ()> for VhostUserSoundBackend {
|
||||
fn num_queues(&self) -> usize {
|
||||
NUM_QUEUES as usize
|
||||
}
|
||||
|
||||
fn max_queue_size(&self) -> usize {
|
||||
// TODO: Investigate if an alternative value makes any difference.
|
||||
64
|
||||
}
|
||||
|
||||
fn features(&self) -> u64 {
|
||||
1 << VIRTIO_F_VERSION_1
|
||||
| 1 << VIRTIO_F_NOTIFY_ON_EMPTY
|
||||
| 1 << VIRTIO_RING_F_INDIRECT_DESC
|
||||
| 1 << VIRTIO_RING_F_EVENT_IDX
|
||||
| VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits()
|
||||
}
|
||||
|
||||
fn protocol_features(&self) -> VhostUserProtocolFeatures {
|
||||
VhostUserProtocolFeatures::CONFIG | VhostUserProtocolFeatures::MQ
|
||||
}
|
||||
|
||||
fn set_event_idx(&self, enabled: bool) {
|
||||
for thread in self.threads.iter() {
|
||||
thread.write().unwrap().set_event_idx(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_memory(&self, mem: GuestMemoryAtomic<GuestMemoryMmap>) -> IoResult<()> {
|
||||
for thread in self.threads.iter() {
|
||||
thread.write().unwrap().update_memory(mem.clone())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(
|
||||
&self,
|
||||
device_event: u16,
|
||||
evset: EventSet,
|
||||
vrings: &[VringRwLock],
|
||||
thread_id: usize,
|
||||
) -> IoResult<bool> {
|
||||
if evset != EventSet::IN {
|
||||
return Err(Error::HandleEventNotEpollIn.into());
|
||||
}
|
||||
|
||||
self.threads[thread_id].read().unwrap().handle_event(
|
||||
device_event,
|
||||
vrings,
|
||||
&self.audio_backend,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_config(&self, offset: u32, size: u32) -> Vec<u8> {
|
||||
let offset = offset as usize;
|
||||
let size = size as usize;
|
||||
|
||||
let buf = self.virtio_cfg.as_slice();
|
||||
|
||||
if offset + size > buf.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
buf[offset..offset + size].to_vec()
|
||||
}
|
||||
|
||||
fn queues_per_thread(&self) -> Vec<u64> {
|
||||
let mut vec = Vec::with_capacity(self.threads.len());
|
||||
|
||||
for thread in self.threads.iter() {
|
||||
vec.push(thread.read().unwrap().queues_per_thread())
|
||||
}
|
||||
|
||||
vec
|
||||
}
|
||||
|
||||
fn exit_event(&self, _thread_index: usize) -> Option<EventFd> {
|
||||
self.exit_event.try_clone().ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ControlMessage {
|
||||
pub kind: ControlMessageKind,
|
||||
pub code: u32,
|
||||
pub desc_chain: SoundDescriptorChain,
|
||||
pub descriptor: virtio_queue::Descriptor,
|
||||
pub vring: VringRwLock,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ControlMessage {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct(stringify!(ControlMessage))
|
||||
.field("kind", &self.kind)
|
||||
.field("code", &self.code)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ControlMessage {
|
||||
fn drop(&mut self) {
|
||||
log::trace!(
|
||||
"dropping ControlMessage {:?} reply = {}",
|
||||
self.kind,
|
||||
match self.code {
|
||||
virtio_sound::VIRTIO_SND_S_OK => "VIRTIO_SND_S_OK",
|
||||
virtio_sound::VIRTIO_SND_S_BAD_MSG => "VIRTIO_SND_S_BAD_MSG",
|
||||
virtio_sound::VIRTIO_SND_S_NOT_SUPP => "VIRTIO_SND_S_NOT_SUPP",
|
||||
virtio_sound::VIRTIO_SND_S_IO_ERR => "VIRTIO_SND_S_IO_ERR",
|
||||
_ => "other",
|
||||
}
|
||||
);
|
||||
let resp = VirtioSoundHeader {
|
||||
code: self.code.into(),
|
||||
};
|
||||
|
||||
if let Err(err) = self
|
||||
.desc_chain
|
||||
.memory()
|
||||
.write_obj(resp, self.descriptor.addr())
|
||||
{
|
||||
log::error!("Error::DescriptorWriteFailed: {}", err);
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.vring
|
||||
.add_used(self.desc_chain.head_index(), resp.as_slice().len() as u32)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Couldn't add used");
|
||||
}
|
||||
if self.vring.signal_used_queue().is_err() {
|
||||
log::error!("Couldn't signal used queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
317
staging/vhost-device-sound/src/lib.rs
Normal file
317
staging/vhost-device-sound/src/lib.rs
Normal file
@ -0,0 +1,317 @@
|
||||
// Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
|
||||
pub mod audio_backends;
|
||||
pub mod device;
|
||||
pub mod stream;
|
||||
pub mod virtio_sound;
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
io::{Error as IoError, ErrorKind},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use clap::ValueEnum;
|
||||
use log::{info, warn};
|
||||
pub use stream::Stream;
|
||||
use thiserror::Error as ThisError;
|
||||
use vhost::{vhost_user, vhost_user::Listener};
|
||||
use vhost_user_backend::{VhostUserDaemon, VringRwLock, VringT};
|
||||
use virtio_sound::*;
|
||||
use vm_memory::{
|
||||
ByteValued, Bytes, GuestMemoryAtomic, GuestMemoryLoadGuard, GuestMemoryMmap, Le32,
|
||||
};
|
||||
|
||||
use crate::device::VhostUserSoundBackend;
|
||||
|
||||
pub const SUPPORTED_FORMATS: u64 = 1 << VIRTIO_SND_PCM_FMT_U8
|
||||
| 1 << VIRTIO_SND_PCM_FMT_S16
|
||||
| 1 << VIRTIO_SND_PCM_FMT_S24
|
||||
| 1 << VIRTIO_SND_PCM_FMT_S32;
|
||||
|
||||
pub const SUPPORTED_RATES: u64 = 1 << VIRTIO_SND_PCM_RATE_8000
|
||||
| 1 << VIRTIO_SND_PCM_RATE_11025
|
||||
| 1 << VIRTIO_SND_PCM_RATE_16000
|
||||
| 1 << VIRTIO_SND_PCM_RATE_22050
|
||||
| 1 << VIRTIO_SND_PCM_RATE_32000
|
||||
| 1 << VIRTIO_SND_PCM_RATE_44100
|
||||
| 1 << VIRTIO_SND_PCM_RATE_48000;
|
||||
|
||||
use virtio_queue::DescriptorChain;
|
||||
pub type SoundDescriptorChain = DescriptorChain<GuestMemoryLoadGuard<GuestMemoryMmap<()>>>;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Custom error types
|
||||
#[derive(Debug, ThisError)]
|
||||
pub enum Error {
|
||||
#[error("Notification send failed")]
|
||||
SendNotificationFailed,
|
||||
#[error("Descriptor not found")]
|
||||
DescriptorNotFound,
|
||||
#[error("Descriptor read failed")]
|
||||
DescriptorReadFailed,
|
||||
#[error("Descriptor write failed")]
|
||||
DescriptorWriteFailed,
|
||||
#[error("Failed to handle event other than EPOLLIN event")]
|
||||
HandleEventNotEpollIn,
|
||||
#[error("Failed to handle unknown event")]
|
||||
HandleUnknownEvent,
|
||||
#[error("Invalid control message code {0}")]
|
||||
InvalidControlMessage(u32),
|
||||
#[error("Failed to create a new EventFd")]
|
||||
EventFdCreate(IoError),
|
||||
#[error("Request missing data buffer")]
|
||||
SoundReqMissingData,
|
||||
#[error("Audio backend not supported")]
|
||||
AudioBackendNotSupported,
|
||||
#[error("Invalid virtio_snd_hdr size, expected: {0}, found: {1}")]
|
||||
UnexpectedSoundHeaderSize(usize, u32),
|
||||
#[error("Received unexpected write only descriptor at index {0}")]
|
||||
UnexpectedWriteOnlyDescriptor(usize),
|
||||
#[error("Received unexpected readable descriptor at index {0}")]
|
||||
UnexpectedReadableDescriptor(usize),
|
||||
#[error("Invalid descriptor count {0}")]
|
||||
UnexpectedDescriptorCount(usize),
|
||||
#[error("Invalid descriptor size, expected: {0}, found: {1}")]
|
||||
UnexpectedDescriptorSize(usize, u32),
|
||||
#[error("Protocol or device error: {0}")]
|
||||
Stream(stream::Error),
|
||||
#[error("Stream with id {0} not found")]
|
||||
StreamWithIdNotFound(u32),
|
||||
#[error("Channel number not supported: {0}")]
|
||||
ChannelNotSupported(u8),
|
||||
}
|
||||
|
||||
impl From<Error> for IoError {
|
||||
fn from(e: Error) -> Self {
|
||||
Self::new(ErrorKind::Other, e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<stream::Error> for Error {
|
||||
fn from(val: stream::Error) -> Self {
|
||||
Self::Stream(val)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Copy, Default, Debug, Eq, PartialEq)]
|
||||
pub enum BackendType {
|
||||
#[default]
|
||||
Null,
|
||||
#[cfg(feature = "pw-backend")]
|
||||
Pipewire,
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
Alsa,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidControlMessage(u32);
|
||||
|
||||
impl std::fmt::Display for InvalidControlMessage {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "Invalid control message code {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidControlMessage> for crate::Error {
|
||||
fn from(val: InvalidControlMessage) -> Self {
|
||||
Self::InvalidControlMessage(val.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InvalidControlMessage {}
|
||||
|
||||
#[derive(Copy, Debug, Clone, Eq, PartialEq)]
|
||||
#[repr(u32)]
|
||||
pub enum ControlMessageKind {
|
||||
JackInfo = 1,
|
||||
JackRemap = 2,
|
||||
PcmInfo = 0x0100,
|
||||
PcmSetParams = 0x0101,
|
||||
PcmPrepare = 0x0102,
|
||||
PcmRelease = 0x0103,
|
||||
PcmStart = 0x0104,
|
||||
PcmStop = 0x0105,
|
||||
ChmapInfo = 0x0200,
|
||||
}
|
||||
|
||||
impl TryFrom<Le32> for ControlMessageKind {
|
||||
type Error = InvalidControlMessage;
|
||||
|
||||
fn try_from(val: Le32) -> std::result::Result<Self, Self::Error> {
|
||||
Ok(match u32::from(val) {
|
||||
VIRTIO_SND_R_JACK_INFO => Self::JackInfo,
|
||||
VIRTIO_SND_R_JACK_REMAP => Self::JackRemap,
|
||||
VIRTIO_SND_R_PCM_INFO => Self::PcmInfo,
|
||||
VIRTIO_SND_R_PCM_SET_PARAMS => Self::PcmSetParams,
|
||||
VIRTIO_SND_R_PCM_PREPARE => Self::PcmPrepare,
|
||||
VIRTIO_SND_R_PCM_RELEASE => Self::PcmRelease,
|
||||
VIRTIO_SND_R_PCM_START => Self::PcmStart,
|
||||
VIRTIO_SND_R_PCM_STOP => Self::PcmStop,
|
||||
VIRTIO_SND_R_CHMAP_INFO => Self::ChmapInfo,
|
||||
other => return Err(InvalidControlMessage(other)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ControlMessage {
|
||||
pub kind: ControlMessageKind,
|
||||
pub code: u32,
|
||||
pub desc_chain: SoundDescriptorChain,
|
||||
pub descriptor: virtio_queue::Descriptor,
|
||||
pub vring: VringRwLock,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ControlMessage {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct(stringify!(ControlMessage))
|
||||
.field("kind", &self.kind)
|
||||
.field("code", &self.code)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ControlMessage {
|
||||
fn drop(&mut self) {
|
||||
log::trace!(
|
||||
"dropping ControlMessage {:?} reply = {}",
|
||||
self.kind,
|
||||
match self.code {
|
||||
virtio_sound::VIRTIO_SND_S_OK => "VIRTIO_SND_S_OK",
|
||||
virtio_sound::VIRTIO_SND_S_BAD_MSG => "VIRTIO_SND_S_BAD_MSG",
|
||||
virtio_sound::VIRTIO_SND_S_NOT_SUPP => "VIRTIO_SND_S_NOT_SUPP",
|
||||
virtio_sound::VIRTIO_SND_S_IO_ERR => "VIRTIO_SND_S_IO_ERR",
|
||||
_ => "other",
|
||||
}
|
||||
);
|
||||
let resp = VirtioSoundHeader {
|
||||
code: self.code.into(),
|
||||
};
|
||||
|
||||
if let Err(err) = self
|
||||
.desc_chain
|
||||
.memory()
|
||||
.write_obj(resp, self.descriptor.addr())
|
||||
{
|
||||
log::error!("Error::DescriptorWriteFailed: {}", err);
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.vring
|
||||
.add_used(self.desc_chain.head_index(), resp.as_slice().len() as u32)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Couldn't add used");
|
||||
}
|
||||
if self.vring.signal_used_queue().is_err() {
|
||||
log::error!("Couldn't signal used queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// This structure is the public API through which an external program
|
||||
/// is allowed to configure the backend.
|
||||
pub struct SoundConfig {
|
||||
/// vhost-user Unix domain socket
|
||||
socket: String,
|
||||
/// use multiple threads to hanlde the virtqueues
|
||||
multi_thread: bool,
|
||||
/// audio backend variant
|
||||
audio_backend: BackendType,
|
||||
}
|
||||
|
||||
impl SoundConfig {
|
||||
/// Create a new instance of the SoundConfig struct, containing the
|
||||
/// parameters to be fed into the sound-backend server.
|
||||
pub fn new(socket: String, multi_thread: bool, audio_backend: BackendType) -> Self {
|
||||
Self {
|
||||
socket,
|
||||
multi_thread,
|
||||
audio_backend,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the path of the unix domain socket which is listening to
|
||||
/// requests from the guest.
|
||||
pub fn get_socket_path(&self) -> String {
|
||||
String::from(&self.socket)
|
||||
}
|
||||
|
||||
pub fn get_audio_backend(&self) -> BackendType {
|
||||
self.audio_backend
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IOMessage {
|
||||
status: std::sync::atomic::AtomicU32,
|
||||
desc_chain: SoundDescriptorChain,
|
||||
descriptor: virtio_queue::Descriptor,
|
||||
vring: VringRwLock,
|
||||
}
|
||||
|
||||
impl Drop for IOMessage {
|
||||
fn drop(&mut self) {
|
||||
log::trace!("dropping IOMessage");
|
||||
let resp = VirtioSoundPcmStatus {
|
||||
status: self.status.load(std::sync::atomic::Ordering::SeqCst).into(),
|
||||
latency_bytes: 0.into(),
|
||||
};
|
||||
|
||||
if let Err(err) = self
|
||||
.desc_chain
|
||||
.memory()
|
||||
.write_obj(resp, self.descriptor.addr())
|
||||
{
|
||||
log::error!("Error::DescriptorWriteFailed: {}", err);
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.vring
|
||||
.add_used(self.desc_chain.head_index(), resp.as_slice().len() as u32)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Couldn't add used");
|
||||
}
|
||||
if self.vring.signal_used_queue().is_err() {
|
||||
log::error!("Couldn't signal used queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the public API through which an external program starts the
|
||||
/// vhost-device-sound backend server.
|
||||
pub fn start_backend_server(config: SoundConfig) {
|
||||
log::trace!("Using config {:?}", &config);
|
||||
let listener = Listener::new(config.get_socket_path(), true).unwrap();
|
||||
let backend = Arc::new(VhostUserSoundBackend::new(config).unwrap());
|
||||
|
||||
let mut daemon = VhostUserDaemon::new(
|
||||
String::from("vhost-device-sound"),
|
||||
backend.clone(),
|
||||
GuestMemoryAtomic::new(GuestMemoryMmap::new()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
log::trace!("Starting daemon");
|
||||
daemon.start(listener).unwrap();
|
||||
|
||||
match daemon.wait() {
|
||||
Ok(()) => {
|
||||
info!("Stopping cleanly");
|
||||
}
|
||||
Err(vhost_user_backend::Error::HandleRequest(vhost_user::Error::PartialMessage)) => {
|
||||
info!(
|
||||
"vhost-user connection closed with partial message. If the VM is shutting down, \
|
||||
this is expected behavior; otherwise, it might be a bug."
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error running daemon: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// No matter the result, we need to shut down the worker thread.
|
||||
backend.send_exit_event();
|
||||
}
|
||||
92
staging/vhost-device-sound/src/main.rs
Normal file
92
staging/vhost-device-sound/src/main.rs
Normal file
@ -0,0 +1,92 @@
|
||||
// Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
|
||||
// Stefano Garzarella <sgarzare@redhat.com>
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use clap::Parser;
|
||||
use vhost_device_sound::{start_backend_server, BackendType, Error, Result, SoundConfig};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version, about, long_about = None)]
|
||||
struct SoundArgs {
|
||||
/// vhost-user Unix domain socket path.
|
||||
#[clap(long)]
|
||||
socket: String,
|
||||
/// audio backend to be used
|
||||
#[clap(long)]
|
||||
#[clap(value_enum)]
|
||||
backend: BackendType,
|
||||
}
|
||||
|
||||
impl TryFrom<SoundArgs> for SoundConfig {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(cmd_args: SoundArgs) -> Result<Self> {
|
||||
let socket = cmd_args.socket.trim().to_string();
|
||||
|
||||
Ok(SoundConfig::new(socket, false, cmd_args.backend))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let config = SoundConfig::try_from(SoundArgs::parse()).unwrap();
|
||||
|
||||
loop {
|
||||
start_backend_server(config.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::*;
|
||||
use serial_test::serial;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl SoundArgs {
|
||||
fn from_args(socket: &str) -> Self {
|
||||
SoundArgs {
|
||||
socket: socket.to_string(),
|
||||
backend: BackendType::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sound_config_setup() {
|
||||
let args = SoundArgs::from_args("/tmp/vhost-sound.socket");
|
||||
|
||||
let config = SoundConfig::try_from(args);
|
||||
assert!(config.is_ok());
|
||||
|
||||
let config = config.unwrap();
|
||||
assert_eq!(config.get_socket_path(), "/tmp/vhost-sound.socket");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[serial]
|
||||
#[case::null_backend("null", BackendType::Null)]
|
||||
#[cfg_attr(
|
||||
feature = "pw-backend",
|
||||
case::pipewire("pipewire", BackendType::Pipewire)
|
||||
)]
|
||||
#[cfg_attr(feature = "alsa-backend", case::alsa("alsa", BackendType::Alsa))]
|
||||
fn test_cli_backend_arg(#[case] backend_name: &str, #[case] backend: BackendType) {
|
||||
let args: SoundArgs = Parser::parse_from([
|
||||
"",
|
||||
"--socket",
|
||||
"/tmp/vhost-sound.socket ",
|
||||
"--backend",
|
||||
backend_name,
|
||||
]);
|
||||
|
||||
let config = SoundConfig::try_from(args);
|
||||
assert!(config.is_ok());
|
||||
|
||||
let config = config.unwrap();
|
||||
assert_eq!(config.get_audio_backend(), backend);
|
||||
}
|
||||
}
|
||||
284
staging/vhost-device-sound/src/stream.rs
Normal file
284
staging/vhost-device-sound/src/stream.rs
Normal file
@ -0,0 +1,284 @@
|
||||
// Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
|
||||
use thiserror::Error as ThisError;
|
||||
use vm_memory::{Address, Bytes, Le32, Le64};
|
||||
|
||||
use crate::{virtio_sound::*, IOMessage, SUPPORTED_FORMATS, SUPPORTED_RATES};
|
||||
|
||||
/// Stream errors.
|
||||
#[derive(Debug, ThisError)]
|
||||
pub enum Error {
|
||||
#[error("Guest driver request an invalid stream state transition from {0} to {1}.")]
|
||||
InvalidStateTransition(PCMState, PCMState),
|
||||
#[error("Guest requested an invalid stream id: {0}")]
|
||||
InvalidStreamId(u32),
|
||||
#[error("Descriptor read failed")]
|
||||
DescriptorReadFailed,
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// PCM stream state machine.
|
||||
///
|
||||
/// ## 5.14.6.6.1 PCM Command Lifecycle
|
||||
///
|
||||
/// A PCM stream has the following command lifecycle:
|
||||
///
|
||||
/// - `SET PARAMETERS`
|
||||
///
|
||||
/// The driver negotiates the stream parameters (format, transport, etc) with
|
||||
/// the device.
|
||||
///
|
||||
/// Possible valid transitions: `SET PARAMETERS`, `PREPARE`.
|
||||
///
|
||||
/// - `PREPARE`
|
||||
///
|
||||
/// The device prepares the stream (allocates resources, etc).
|
||||
///
|
||||
/// Possible valid transitions: `SET PARAMETERS`, `PREPARE`, `START`,
|
||||
/// `RELEASE`. Output only: the driver transfers data for pre-buffing.
|
||||
///
|
||||
/// - `START`
|
||||
///
|
||||
/// The device starts the stream (unmute, putting into running state, etc).
|
||||
///
|
||||
/// Possible valid transitions: `STOP`.
|
||||
/// The driver transfers data to/from the stream.
|
||||
///
|
||||
/// - `STOP`
|
||||
///
|
||||
/// The device stops the stream (mute, putting into non-running state, etc).
|
||||
///
|
||||
/// Possible valid transitions: `START`, `RELEASE`.
|
||||
///
|
||||
/// - `RELEASE`
|
||||
///
|
||||
/// The device releases the stream (frees resources, etc).
|
||||
///
|
||||
/// Possible valid transitions: `SET PARAMETERS`, `PREPARE`.
|
||||
///
|
||||
/// ```text
|
||||
/// +---------------+ +---------+ +---------+ +-------+ +-------+
|
||||
/// | SetParameters | | Prepare | | Release | | Start | | Stop |
|
||||
/// +---------------+ +---------+ +---------+ +-------+ +-------+
|
||||
/// | | | | |
|
||||
/// |- | | | |
|
||||
/// || | | | |
|
||||
/// |< | | | |
|
||||
/// | | | | |
|
||||
/// |------------->| | | |
|
||||
/// | | | | |
|
||||
/// |<-------------| | | |
|
||||
/// | | | | |
|
||||
/// | |- | | |
|
||||
/// | || | | |
|
||||
/// | |< | | |
|
||||
/// | | | | |
|
||||
/// | |--------------------->| |
|
||||
/// | | | | |
|
||||
/// | |---------->| | |
|
||||
/// | | | | |
|
||||
/// | | | |-------->|
|
||||
/// | | | | |
|
||||
/// | | | |<--------|
|
||||
/// | | | | |
|
||||
/// | | |<-------------------|
|
||||
/// | | | | |
|
||||
/// |<-------------------------| | |
|
||||
/// | | | | |
|
||||
/// | |<----------| | |
|
||||
/// ```
|
||||
#[derive(Debug, Default, Copy, Clone)]
|
||||
pub enum PCMState {
|
||||
#[default]
|
||||
#[doc(alias = "VIRTIO_SND_R_PCM_SET_PARAMS")]
|
||||
SetParameters,
|
||||
#[doc(alias = "VIRTIO_SND_R_PCM_PREPARE")]
|
||||
Prepare,
|
||||
#[doc(alias = "VIRTIO_SND_R_PCM_RELEASE")]
|
||||
Release,
|
||||
#[doc(alias = "VIRTIO_SND_R_PCM_START")]
|
||||
Start,
|
||||
#[doc(alias = "VIRTIO_SND_R_PCM_STOP")]
|
||||
Stop,
|
||||
}
|
||||
|
||||
macro_rules! set_new_state {
|
||||
($new_state_fn:ident, $new_state:expr, $($valid_source_states:tt)*) => {
|
||||
pub fn $new_state_fn(&mut self) -> Result<()> {
|
||||
if !matches!(self, $($valid_source_states)*) {
|
||||
return Err(Error::InvalidStateTransition(*self, $new_state));
|
||||
}
|
||||
*self = $new_state;
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl PCMState {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
set_new_state!(
|
||||
set_parameters,
|
||||
Self::SetParameters,
|
||||
Self::SetParameters | Self::Prepare | Self::Release
|
||||
);
|
||||
|
||||
set_new_state!(
|
||||
prepare,
|
||||
Self::Prepare,
|
||||
Self::SetParameters | Self::Prepare | Self::Release
|
||||
);
|
||||
|
||||
set_new_state!(start, Self::Start, Self::Prepare | Self::Stop);
|
||||
|
||||
set_new_state!(stop, Self::Stop, Self::Start);
|
||||
|
||||
set_new_state!(release, Self::Release, Self::Prepare | Self::Stop);
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PCMState {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use PCMState::*;
|
||||
match *self {
|
||||
SetParameters => {
|
||||
write!(fmt, "VIRTIO_SND_R_PCM_SET_PARAMS")
|
||||
}
|
||||
Prepare => {
|
||||
write!(fmt, "VIRTIO_SND_R_PCM_PREPARE")
|
||||
}
|
||||
Release => {
|
||||
write!(fmt, "VIRTIO_SND_R_PCM_RELEASE")
|
||||
}
|
||||
Start => {
|
||||
write!(fmt, "VIRTIO_SND_R_PCM_START")
|
||||
}
|
||||
Stop => {
|
||||
write!(fmt, "VIRTIO_SND_R_PCM_STOP")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal state of a PCM stream of the VIRTIO Sound device.
|
||||
#[derive(Debug)]
|
||||
pub struct Stream {
|
||||
pub id: usize,
|
||||
pub params: PcmParams,
|
||||
pub formats: Le64,
|
||||
pub rates: Le64,
|
||||
pub direction: u8,
|
||||
pub channels_min: u8,
|
||||
pub channels_max: u8,
|
||||
pub state: PCMState,
|
||||
pub buffers: VecDeque<Buffer>,
|
||||
}
|
||||
|
||||
impl Default for Stream {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
direction: VIRTIO_SND_D_OUTPUT,
|
||||
formats: SUPPORTED_FORMATS.into(),
|
||||
rates: SUPPORTED_RATES.into(),
|
||||
params: PcmParams::default(),
|
||||
channels_min: 1,
|
||||
channels_max: 6,
|
||||
state: Default::default(),
|
||||
buffers: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
#[inline]
|
||||
pub fn supports_format(&self, format: u8) -> bool {
|
||||
let formats: u64 = self.formats.into();
|
||||
(formats & (1_u64 << format)) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn supports_rate(&self, rate: u8) -> bool {
|
||||
let rates: u64 = self.rates.into();
|
||||
(rates & (1_u64 << rate)) != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream params
|
||||
#[derive(Debug)]
|
||||
pub struct PcmParams {
|
||||
/// size of hardware buffer in bytes
|
||||
pub buffer_bytes: Le32,
|
||||
/// size of hardware period in bytes
|
||||
pub period_bytes: Le32,
|
||||
pub features: Le32,
|
||||
pub channels: u8,
|
||||
pub format: u8,
|
||||
pub rate: u8,
|
||||
}
|
||||
|
||||
impl Default for PcmParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer_bytes: 8192.into(),
|
||||
period_bytes: 4096.into(),
|
||||
features: 0.into(),
|
||||
channels: 1,
|
||||
format: VIRTIO_SND_PCM_FMT_S16,
|
||||
rate: VIRTIO_SND_PCM_RATE_44100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
// TODO: to make private and add len usize
|
||||
pub data_descriptor: virtio_queue::Descriptor,
|
||||
pub pos: usize,
|
||||
pub message: Arc<IOMessage>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Buffer {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct(stringify!(Buffer))
|
||||
.field("pos", &self.pos)
|
||||
.field("message", &Arc::as_ptr(&self.message))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new(data_descriptor: virtio_queue::Descriptor, message: Arc<IOMessage>) -> Self {
|
||||
Self {
|
||||
pos: 0,
|
||||
data_descriptor,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume(&self, buf: &mut [u8]) -> Result<u32> {
|
||||
let addr = self.data_descriptor.addr();
|
||||
let offset = self.pos as u64;
|
||||
let len = self
|
||||
.message
|
||||
.desc_chain
|
||||
.memory()
|
||||
.read(
|
||||
buf,
|
||||
addr.checked_add(offset)
|
||||
.expect("invalid guest memory address"),
|
||||
)
|
||||
.map_err(|_| Error::DescriptorReadFailed)?;
|
||||
Ok(len as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Buffer {
|
||||
fn drop(&mut self) {
|
||||
log::trace!("dropping buffer {:?}", self);
|
||||
}
|
||||
}
|
||||
344
staging/vhost-device-sound/src/virtio_sound.rs
Normal file
344
staging/vhost-device-sound/src/virtio_sound.rs
Normal file
@ -0,0 +1,344 @@
|
||||
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
|
||||
use vm_memory::{ByteValued, Le32, Le64};
|
||||
|
||||
// virtqueues
|
||||
|
||||
pub const CONTROL_QUEUE_IDX: u16 = 0;
|
||||
pub const EVENT_QUEUE_IDX: u16 = 1;
|
||||
pub const TX_QUEUE_IDX: u16 = 2;
|
||||
pub const RX_QUEUE_IDX: u16 = 3;
|
||||
pub const NUM_QUEUES: u16 = 4;
|
||||
|
||||
// jack control request types
|
||||
|
||||
pub const VIRTIO_SND_R_JACK_INFO: u32 = 1;
|
||||
pub const VIRTIO_SND_R_JACK_REMAP: u32 = 2;
|
||||
|
||||
// PCM control request types
|
||||
|
||||
pub const VIRTIO_SND_R_PCM_INFO: u32 = 0x0100;
|
||||
pub const VIRTIO_SND_R_PCM_SET_PARAMS: u32 = 0x0101;
|
||||
pub const VIRTIO_SND_R_PCM_PREPARE: u32 = 0x0102;
|
||||
pub const VIRTIO_SND_R_PCM_RELEASE: u32 = 0x0103;
|
||||
pub const VIRTIO_SND_R_PCM_START: u32 = 0x0104;
|
||||
pub const VIRTIO_SND_R_PCM_STOP: u32 = 0x0105;
|
||||
|
||||
// channel map control request types
|
||||
|
||||
pub const VIRTIO_SND_R_CHMAP_INFO: u32 = 0x0200;
|
||||
|
||||
// jack event types
|
||||
|
||||
pub const VIRTIO_SND_EVT_JACK_CONNECTED: u32 = 0x1000;
|
||||
pub const VIRTIO_SND_EVT_JACK_DISCONNECTED: u32 = 0x1001;
|
||||
|
||||
// PCM event types
|
||||
|
||||
pub const VIRTIO_SND_EVT_PCM_PERIOD_ELAPSED: u32 = 0x1100;
|
||||
pub const VIRTIO_SND_EVT_PCM_XRUN: u32 = 0x1101;
|
||||
|
||||
// common status codes
|
||||
|
||||
pub const VIRTIO_SND_S_OK: u32 = 0x8000;
|
||||
pub const VIRTIO_SND_S_BAD_MSG: u32 = 0x8001;
|
||||
pub const VIRTIO_SND_S_NOT_SUPP: u32 = 0x8002;
|
||||
pub const VIRTIO_SND_S_IO_ERR: u32 = 0x8003;
|
||||
|
||||
// device data flow directions
|
||||
|
||||
pub const VIRTIO_SND_D_OUTPUT: u8 = 0;
|
||||
pub const VIRTIO_SND_D_INPUT: u8 = 1;
|
||||
|
||||
// supported jack features
|
||||
|
||||
pub const VIRTIO_SND_JACK_F_REMAP: u32 = 0;
|
||||
|
||||
// supported PCM stream features
|
||||
|
||||
pub const VIRTIO_SND_PCM_F_SHMEM_HOST: u8 = 0;
|
||||
pub const VIRTIO_SND_PCM_F_SHMEM_GUEST: u8 = 1;
|
||||
pub const VIRTIO_SND_PCM_F_MSG_POLLING: u8 = 2;
|
||||
pub const VIRTIO_SND_PCM_F_EVT_SHMEM_PERIODS: u8 = 3;
|
||||
pub const VIRTIO_SND_PCM_F_EVT_XRUNS: u8 = 4;
|
||||
|
||||
// supported PCM sample formats
|
||||
|
||||
pub const VIRTIO_SND_PCM_FMT_IMA_ADPCM: u8 = 0;
|
||||
pub const VIRTIO_SND_PCM_FMT_MU_LAW: u8 = 1;
|
||||
pub const VIRTIO_SND_PCM_FMT_A_LAW: u8 = 2;
|
||||
pub const VIRTIO_SND_PCM_FMT_S8: u8 = 3;
|
||||
pub const VIRTIO_SND_PCM_FMT_U8: u8 = 4;
|
||||
pub const VIRTIO_SND_PCM_FMT_S16: u8 = 5;
|
||||
pub const VIRTIO_SND_PCM_FMT_U16: u8 = 6;
|
||||
pub const VIRTIO_SND_PCM_FMT_S18_3: u8 = 7;
|
||||
pub const VIRTIO_SND_PCM_FMT_U18_3: u8 = 8;
|
||||
pub const VIRTIO_SND_PCM_FMT_S20_3: u8 = 9;
|
||||
pub const VIRTIO_SND_PCM_FMT_U20_3: u8 = 10;
|
||||
pub const VIRTIO_SND_PCM_FMT_S24_3: u8 = 11;
|
||||
pub const VIRTIO_SND_PCM_FMT_U24_3: u8 = 12;
|
||||
pub const VIRTIO_SND_PCM_FMT_S20: u8 = 13;
|
||||
pub const VIRTIO_SND_PCM_FMT_U20: u8 = 14;
|
||||
pub const VIRTIO_SND_PCM_FMT_S24: u8 = 15;
|
||||
pub const VIRTIO_SND_PCM_FMT_U24: u8 = 16;
|
||||
pub const VIRTIO_SND_PCM_FMT_S32: u8 = 17;
|
||||
pub const VIRTIO_SND_PCM_FMT_U32: u8 = 18;
|
||||
pub const VIRTIO_SND_PCM_FMT_FLOAT: u8 = 19;
|
||||
pub const VIRTIO_SND_PCM_FMT_FLOAT64: u8 = 20;
|
||||
// digital formats (width / physical width)
|
||||
pub const VIRTIO_SND_PCM_FMT_DSD_U8: u8 = 21;
|
||||
pub const VIRTIO_SND_PCM_FMT_DSD_U16: u8 = 22;
|
||||
pub const VIRTIO_SND_PCM_FMT_DSD_U32: u8 = 23;
|
||||
pub const VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME: u8 = 24;
|
||||
|
||||
// supported PCM frame rates
|
||||
|
||||
pub const VIRTIO_SND_PCM_RATE_5512: u8 = 0;
|
||||
pub const VIRTIO_SND_PCM_RATE_8000: u8 = 1;
|
||||
pub const VIRTIO_SND_PCM_RATE_11025: u8 = 2;
|
||||
pub const VIRTIO_SND_PCM_RATE_16000: u8 = 3;
|
||||
pub const VIRTIO_SND_PCM_RATE_22050: u8 = 4;
|
||||
pub const VIRTIO_SND_PCM_RATE_32000: u8 = 5;
|
||||
pub const VIRTIO_SND_PCM_RATE_44100: u8 = 6;
|
||||
pub const VIRTIO_SND_PCM_RATE_48000: u8 = 7;
|
||||
pub const VIRTIO_SND_PCM_RATE_64000: u8 = 8;
|
||||
pub const VIRTIO_SND_PCM_RATE_88200: u8 = 9;
|
||||
pub const VIRTIO_SND_PCM_RATE_96000: u8 = 10;
|
||||
pub const VIRTIO_SND_PCM_RATE_176400: u8 = 11;
|
||||
pub const VIRTIO_SND_PCM_RATE_192000: u8 = 12;
|
||||
pub const VIRTIO_SND_PCM_RATE_384000: u8 = 13;
|
||||
|
||||
// standard channel position definition
|
||||
|
||||
pub const VIRTIO_SND_CHMAP_NONE: u8 = 0; /* undefined */
|
||||
pub const VIRTIO_SND_CHMAP_NA: u8 = 1; /* silent */
|
||||
pub const VIRTIO_SND_CHMAP_MONO: u8 = 2; /* mono stream */
|
||||
pub const VIRTIO_SND_CHMAP_FL: u8 = 3; /* front left */
|
||||
pub const VIRTIO_SND_CHMAP_FR: u8 = 4; /* front right */
|
||||
pub const VIRTIO_SND_CHMAP_RL: u8 = 5; /* rear left */
|
||||
pub const VIRTIO_SND_CHMAP_RR: u8 = 6; /* rear right */
|
||||
pub const VIRTIO_SND_CHMAP_FC: u8 = 7; /* front center */
|
||||
pub const VIRTIO_SND_CHMAP_LFE: u8 = 8; /* low frequency (LFE) */
|
||||
pub const VIRTIO_SND_CHMAP_SL: u8 = 9; /* side left */
|
||||
pub const VIRTIO_SND_CHMAP_SR: u8 = 10; /* side right */
|
||||
pub const VIRTIO_SND_CHMAP_RC: u8 = 11; /* rear center */
|
||||
pub const VIRTIO_SND_CHMAP_FLC: u8 = 12; /* front left center */
|
||||
pub const VIRTIO_SND_CHMAP_FRC: u8 = 13; /* front right center */
|
||||
pub const VIRTIO_SND_CHMAP_RLC: u8 = 14; /* rear left center */
|
||||
pub const VIRTIO_SND_CHMAP_RRC: u8 = 15; /* rear right center */
|
||||
pub const VIRTIO_SND_CHMAP_FLW: u8 = 16; /* front left wide */
|
||||
pub const VIRTIO_SND_CHMAP_FRW: u8 = 17; /* front right wide */
|
||||
pub const VIRTIO_SND_CHMAP_FLH: u8 = 18; /* front left high */
|
||||
pub const VIRTIO_SND_CHMAP_FCH: u8 = 19; /* front center high */
|
||||
pub const VIRTIO_SND_CHMAP_FRH: u8 = 20; /* front right high */
|
||||
pub const VIRTIO_SND_CHMAP_TC: u8 = 21; /* top center */
|
||||
pub const VIRTIO_SND_CHMAP_TFL: u8 = 22; /* top front left */
|
||||
pub const VIRTIO_SND_CHMAP_TFR: u8 = 23; /* top front right */
|
||||
pub const VIRTIO_SND_CHMAP_TFC: u8 = 24; /* top front center */
|
||||
pub const VIRTIO_SND_CHMAP_TRL: u8 = 25; /* top rear left */
|
||||
pub const VIRTIO_SND_CHMAP_TRR: u8 = 26; /* top rear right */
|
||||
pub const VIRTIO_SND_CHMAP_TRC: u8 = 27; /* top rear center */
|
||||
pub const VIRTIO_SND_CHMAP_TFLC: u8 = 28; /* top front left center */
|
||||
pub const VIRTIO_SND_CHMAP_TFRC: u8 = 29; /* top front right center */
|
||||
pub const VIRTIO_SND_CHMAP_TSL: u8 = 34; /* top side left */
|
||||
pub const VIRTIO_SND_CHMAP_TSR: u8 = 35; /* top side right */
|
||||
pub const VIRTIO_SND_CHMAP_LLFE: u8 = 36; /* left LFE */
|
||||
pub const VIRTIO_SND_CHMAP_RLFE: u8 = 37; /* right LFE */
|
||||
pub const VIRTIO_SND_CHMAP_BC: u8 = 38; /* bottom center */
|
||||
pub const VIRTIO_SND_CHMAP_BLC: u8 = 39; /* bottom left center */
|
||||
pub const VIRTIO_SND_CHMAP_BRC: u8 = 40; /* bottom right center */
|
||||
// maximum possible number of channels
|
||||
pub const VIRTIO_SND_CHMAP_MAX_SIZE: usize = 18;
|
||||
|
||||
/// Virtio Sound Configuration
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundConfig {
|
||||
/// total number of all available jacks
|
||||
pub jacks: Le32,
|
||||
/// total number of all available PCM streams
|
||||
pub streams: Le32,
|
||||
/// total number of all available channel maps
|
||||
pub chmaps: Le32,
|
||||
}
|
||||
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundConfig {}
|
||||
|
||||
/// Virtio Sound Request / Response common header
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundHeader {
|
||||
/// request type / response status
|
||||
pub code: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundHeader {}
|
||||
|
||||
/// Virtio Sound event notification
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundEvent {
|
||||
/// PCM stream event type
|
||||
pub hdr: VirtioSoundHeader,
|
||||
/// PCM stream identifier from 0 to streams - 1
|
||||
pub data: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundEvent {}
|
||||
|
||||
/// Virtio Sound request information about any kind of configuration item
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundQueryInfo {
|
||||
/// item request type (VIRTIO_SND_R_*_INFO)
|
||||
pub hdr: VirtioSoundHeader,
|
||||
/// starting identifier for the item
|
||||
pub start_id: Le32,
|
||||
/// number of items for which information is requested
|
||||
pub count: Le32,
|
||||
/// size of the structure containing information for one item
|
||||
pub size: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundQueryInfo {}
|
||||
|
||||
/// Virtio Sound response common information header
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundInfo {
|
||||
/// function group node identifier
|
||||
pub hda_fn_nid: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundInfo {}
|
||||
|
||||
/// Jack control request / Jack common header
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundJackHeader {
|
||||
/// jack request type (VIRTIO_SND_R_JACK_*)
|
||||
pub hdr: VirtioSoundHeader,
|
||||
/// jack identifier
|
||||
pub jack_id: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundJackHeader {}
|
||||
|
||||
/// Jack response information about available jacks
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundJackInfo {
|
||||
/// jack response header type
|
||||
pub hdr: VirtioSoundInfo,
|
||||
/// supported feature bit map (VIRTIO_SND_JACK_F_XXX)
|
||||
pub feature: Le32,
|
||||
/// pin default configuration value
|
||||
pub hda_reg_defconf: Le32,
|
||||
/// pin capabilities value
|
||||
pub hda_reg_caps: Le32,
|
||||
/// current jack connection status
|
||||
pub connected: u8,
|
||||
pub padding: [u8; 7],
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundJackInfo {}
|
||||
|
||||
///If the VIRTIO_SND_JACK_F_REMAP feature bit is set in the jack information
|
||||
/// Remap control request
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundJackRemap {
|
||||
pub hdr: VirtioSoundJackHeader, /* .code = VIRTIO_SND_R_JACK_REMAP */
|
||||
pub association: Le32,
|
||||
pub sequence: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundJackRemap {}
|
||||
|
||||
/// PCM control request / PCM common header
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundPcmHeader {
|
||||
pub hdr: VirtioSoundHeader,
|
||||
pub stream_id: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundPcmHeader {}
|
||||
|
||||
/// PCM response information
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundPcmInfo {
|
||||
pub hdr: VirtioSoundInfo,
|
||||
pub features: Le32, /* 1 << VIRTIO_SND_PCM_F_XXX */
|
||||
pub formats: Le64, /* 1 << VIRTIO_SND_PCM_FMT_XXX */
|
||||
pub rates: Le64, /* 1 << VIRTIO_SND_PCM_RATE_XXX */
|
||||
pub direction: u8,
|
||||
pub channels_min: u8,
|
||||
pub channels_max: u8,
|
||||
|
||||
pub padding: [u8; 5],
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundPcmInfo {}
|
||||
|
||||
/// Set selected stream parameters for the specified stream ID
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSndPcmSetParams {
|
||||
pub hdr: VirtioSoundPcmHeader,
|
||||
pub buffer_bytes: Le32,
|
||||
pub period_bytes: Le32,
|
||||
pub features: Le32, /* 1 << VIRTIO_SND_PCM_F_XXX */
|
||||
pub channels: u8,
|
||||
pub format: u8,
|
||||
pub rate: u8,
|
||||
pub padding: u8,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSndPcmSetParams {}
|
||||
|
||||
/// PCM I/O header
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundPcmXfer {
|
||||
pub stream_id: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundPcmXfer {}
|
||||
|
||||
/// PCM I/O status
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundPcmStatus {
|
||||
pub status: Le32,
|
||||
pub latency_bytes: Le32,
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundPcmStatus {}
|
||||
|
||||
/// channel maps response information
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct VirtioSoundChmapInfo {
|
||||
pub hdr: VirtioSoundInfo,
|
||||
pub direction: u8,
|
||||
pub channels: u8,
|
||||
pub positions: [u8; VIRTIO_SND_CHMAP_MAX_SIZE],
|
||||
}
|
||||
// SAFETY: The layout of the structure is fixed and can be initialized by
|
||||
// reading its content from byte array.
|
||||
unsafe impl ByteValued for VirtioSoundChmapInfo {}
|
||||
Loading…
Reference in New Issue
Block a user