diff --git a/Cargo.lock b/Cargo.lock index 16ad48b..245bc23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/README.md b/README.md index 6e9d27e..5b7c5a4 100644 --- a/README.md +++ b/README.md @@ -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) +## Synopsis + vhost-device-sound --socket --backend + +## Description + A virtio-sound device using the vhost-user protocol. + +## Options + +```text + --socket + vhost-user Unix domain socket path + + --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) diff --git a/staging/vhost-device-sound/rustfmt.toml b/staging/vhost-device-sound/rustfmt.toml new file mode 100644 index 0000000..c6f0942 --- /dev/null +++ b/staging/vhost-device-sound/rustfmt.toml @@ -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 diff --git a/staging/vhost-device-sound/src/audio_backends.rs b/staging/vhost-device-sound/src/audio_backends.rs new file mode 100644 index 0000000..ad67818 --- /dev/null +++ b/staging/vhost-device-sound/src/audio_backends.rs @@ -0,0 +1,58 @@ +// Manos Pitsidianakis +// 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>>, +) -> Result> { + 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))), + } +} diff --git a/staging/vhost-device-sound/src/audio_backends/alsa.rs b/staging/vhost-device-sound/src/audio_backends/alsa.rs new file mode 100644 index 0000000..e158ddd --- /dev/null +++ b/staging/vhost-device-sound/src/audio_backends/alsa.rs @@ -0,0 +1,527 @@ +/// Alsa backend +// +// Manos Pitsidianakis +// 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 = std::result::Result; + +#[derive(Clone, Debug)] +pub struct AlsaBackend { + sender: Arc>>, +} + +#[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>, + stream_id: usize, + streams: &RwLock>, +) -> 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, +) -> AResult { + 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, +) -> AResult { + 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>, + streams: Arc>>, + receiver: &Receiver, + 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::().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>>) -> 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>>, + receiver: Receiver, + ) -> Result<(), Box> { + 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 = msg.desc_chain.clone().collect(); + let desc_request = &descriptors[0]; + let request = msg + .desc_chain + .memory() + .read_obj::(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, + } +} diff --git a/staging/vhost-device-sound/src/audio_backends/null.rs b/staging/vhost-device-sound/src/audio_backends/null.rs new file mode 100644 index 0000000..66edb5e --- /dev/null +++ b/staging/vhost-device-sound/src/audio_backends/null.rs @@ -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>>, +} + +impl NullBackend { + pub fn new(streams: Arc>>) -> 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(()) + } +} diff --git a/staging/vhost-device-sound/src/audio_backends/pipewire.rs b/staging/vhost-device-sound/src/audio_backends/pipewire.rs new file mode 100644 index 0000000..624d5fa --- /dev/null +++ b/staging/vhost-device-sound/src/audio_backends/pipewire.rs @@ -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>>, + thread_loop: ThreadLoop, + pub core: Core, + #[allow(dead_code)] + context: Context, + pub stream_hash: RwLock>, + pub stream_listener: RwLock>>, +} + +impl PwBackend { + pub fn new(stream_params: Arc>>) -> 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 = msg.desc_chain.clone().collect(); + let desc_request = &descriptors[0]; + let request = msg + .desc_chain + .memory() + .read_obj::(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 = 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::() 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(()) + } +} diff --git a/staging/vhost-device-sound/src/device.rs b/staging/vhost-device-sound/src/device.rs new file mode 100644 index 0000000..63cec41 --- /dev/null +++ b/staging/vhost-device-sound/src/device.rs @@ -0,0 +1,816 @@ +// Manos Pitsidianakis +// Stefano Garzarella +// 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>, + event_idx: bool, + chmaps: Arc>>, + jacks: Arc>>, + queue_indexes: Vec, + streams: Arc>>, + streams_no: usize, +} + +type SoundDescriptorChain = DescriptorChain>>; + +impl VhostUserSoundThread { + pub fn new( + chmaps: Arc>>, + jacks: Arc>>, + mut queue_indexes: Vec, + streams: Arc>>, + streams_no: usize, + ) -> Result { + 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) -> IoResult<()> { + self.mem = Some(mem); + Ok(()) + } + + fn handle_event( + &self, + device_event: u16, + vrings: &[VringRwLock], + audio_backend: &RwLock>, + ) -> IoResult { + 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>, + ) -> IoResult { + let requests: Vec = 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::(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::(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::(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::(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::(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::(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::(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::(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::(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 { + log::trace!("process_event"); + Ok(false) + } + + fn process_tx( + &self, + vring: &VringRwLock, + audio_backend: &RwLock>, + ) -> IoResult { + let requests: Vec = 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::() { + return Err(Error::UnexpectedDescriptorSize( + size_of::(), + descriptor.len(), + ) + .into()); + } + state = TxState::Done; + } + TxState::WaitingBufferForStreamId(stream_id) if descriptor.is_write_only() => { + if descriptor.len() as usize != size_of::() { + return Err(Error::UnexpectedDescriptorSize( + size_of::(), + 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::() => + { + return Err(Error::UnexpectedDescriptorSize( + size_of::(), + descriptor.len(), + ) + .into()); + } + TxState::Ready => { + let xfer = desc_chain + .memory() + .read_obj::(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::() => + { + 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>, + ) -> IoResult { + log::trace!("process_rx"); + Ok(false) + } +} + +pub struct VhostUserSoundBackend { + threads: Vec>, + virtio_cfg: VirtioSoundConfig, + exit_event: EventFd, + audio_backend: RwLock>, +} + +impl VhostUserSoundBackend { + pub fn new(config: SoundConfig) -> Result { + 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>> = 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 = 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>> = 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 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) -> 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 { + 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 { + 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 { + 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 { + 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"); + } + } +} diff --git a/staging/vhost-device-sound/src/lib.rs b/staging/vhost-device-sound/src/lib.rs new file mode 100644 index 0000000..d15be90 --- /dev/null +++ b/staging/vhost-device-sound/src/lib.rs @@ -0,0 +1,317 @@ +// Manos Pitsidianakis +// 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>>; +pub type Result = std::result::Result; + +/// 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 for IoError { + fn from(e: Error) -> Self { + Self::new(ErrorKind::Other, e) + } +} + +impl From 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 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 for ControlMessageKind { + type Error = InvalidControlMessage; + + fn try_from(val: Le32) -> std::result::Result { + 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(); +} diff --git a/staging/vhost-device-sound/src/main.rs b/staging/vhost-device-sound/src/main.rs new file mode 100644 index 0000000..8689b39 --- /dev/null +++ b/staging/vhost-device-sound/src/main.rs @@ -0,0 +1,92 @@ +// Manos Pitsidianakis +// Stefano Garzarella +// 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 for SoundConfig { + type Error = Error; + + fn try_from(cmd_args: SoundArgs) -> Result { + 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); + } +} diff --git a/staging/vhost-device-sound/src/stream.rs b/staging/vhost-device-sound/src/stream.rs new file mode 100644 index 0000000..1e07f89 --- /dev/null +++ b/staging/vhost-device-sound/src/stream.rs @@ -0,0 +1,284 @@ +// Manos Pitsidianakis +// 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 = std::result::Result; + +/// 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, +} + +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, +} + +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) -> Self { + Self { + pos: 0, + data_descriptor, + message, + } + } + + pub fn consume(&self, buf: &mut [u8]) -> Result { + 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); + } +} diff --git a/staging/vhost-device-sound/src/virtio_sound.rs b/staging/vhost-device-sound/src/virtio_sound.rs new file mode 100644 index 0000000..252aa76 --- /dev/null +++ b/staging/vhost-device-sound/src/virtio_sound.rs @@ -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 {}