From e5c03730115e5578567d0f99edf374dc1296f3ee Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Thu, 11 Sep 2025 16:45:42 +0200 Subject: [PATCH] Make rodio audio input compile under windows (#37999) Follow up on https://github.com/zed-industries/zed/pull/37786 adds conditional cmp removing use of libwebrtc on windows/freebsd They cant compile livekit yet. This removes microphone and echo cancellation on those platforms however they can not join calls due to the same cause so it does not matter. Documentation and error handing improvements Release Notes: - N/A --------- Co-authored-by: Richard --- Cargo.lock | 1 + crates/audio/Cargo.toml | 1 + crates/audio/src/audio.rs | 54 ++++++++++++------- crates/audio/src/audio_settings.rs | 5 +- crates/audio/src/rodio_ext.rs | 7 ++- .../src/livekit_client/playback.rs | 2 + .../src/livekit_client/playback/source.rs | 4 +- 7 files changed, 51 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1488874d6b..82e1b202cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1395,6 +1395,7 @@ dependencies = [ "serde", "settings", "smol", + "thiserror 2.0.12", "util", "workspace-hack", ] diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 08e0df424d..85274f6514 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -25,6 +25,7 @@ schemars.workspace = true serde.workspace = true settings.workspace = true smol.workspace = true +thiserror.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index 511d00671a..44f2d5e07d 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,24 +1,26 @@ use anyhow::{Context as _, Result}; use collections::HashMap; -use gpui::{App, AsyncApp, BackgroundExecutor, BorrowAppContext, Global}; -use libwebrtc::native::apm; -use log::info; -use parking_lot::Mutex; +use gpui::{App, BackgroundExecutor, BorrowAppContext, Global}; + +#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))] +mod non_windows_and_freebsd_deps { + pub(super) use gpui::AsyncApp; + pub(super) use libwebrtc::native::apm; + pub(super) use log::info; + pub(super) use parking_lot::Mutex; + pub(super) use rodio::cpal::Sample; + pub(super) use rodio::source::{LimitSettings, UniformSourceIterator}; + pub(super) use std::sync::Arc; +} + +#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))] +use non_windows_and_freebsd_deps::*; + use rodio::{ - Decoder, OutputStream, OutputStreamBuilder, Source, - cpal::Sample, - mixer::Mixer, - nz, - source::{Buffered, LimitSettings, UniformSourceIterator}, + Decoder, OutputStream, OutputStreamBuilder, Source, mixer::Mixer, nz, source::Buffered, }; use settings::Settings; -use std::{ - io::Cursor, - num::NonZero, - path::PathBuf, - sync::{Arc, atomic::Ordering}, - time::Duration, -}; +use std::{io::Cursor, num::NonZero, path::PathBuf, sync::atomic::Ordering, time::Duration}; use util::ResultExt; mod audio_settings; @@ -76,6 +78,7 @@ impl Sound { pub struct Audio { output_handle: Option, output_mixer: Option, + #[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))] pub echo_canceller: Arc>, source_cache: HashMap>>>>, replays: replays::Replays, @@ -86,6 +89,10 @@ impl Default for Audio { Self { output_handle: Default::default(), output_mixer: Default::default(), + #[cfg(not(any( + all(target_os = "windows", target_env = "gnu"), + target_os = "freebsd" + )))] echo_canceller: Arc::new(Mutex::new(apm::AudioProcessingModule::new( true, false, false, false, ))), @@ -110,7 +117,16 @@ impl Audio { mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE)); self.output_mixer = Some(mixer); + // The webrtc apm is not yet compiling for windows & freebsd + #[cfg(not(any( + any(all(target_os = "windows", target_env = "gnu")), + target_os = "freebsd" + )))] let echo_canceller = Arc::clone(&self.echo_canceller); + #[cfg(not(any( + any(all(target_os = "windows", target_env = "gnu")), + target_os = "freebsd" + )))] let source = source.inspect_buffer::(move |buffer| { let mut buf: [i16; _] = buffer.map(|s| s.to_sample()); echo_canceller @@ -139,6 +155,7 @@ impl Audio { self.replays.replays_to_tar(executor) } + #[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))] pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result { let stream = rodio::microphone::MicrophoneBuilder::new() .default_device()? @@ -174,8 +191,7 @@ impl Audio { .periodic_access(Duration::from_millis(100), move |agc_source| { agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed)); }) - .replayable(REPLAY_DURATION) - .expect("REPLAY_DURATION is longer then 100ms"); + .replayable(REPLAY_DURATION)?; voip_parts .replays @@ -249,11 +265,13 @@ impl Audio { } } +#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))] pub struct VoipParts { echo_canceller: Arc>, replays: replays::Replays, } +#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))] impl VoipParts { pub fn new(cx: &AsyncApp) -> anyhow::Result { let (apm, replays) = cx.try_read_default_global::(|audio, _| { diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index 43edb8d60d..ea0ea5f355 100644 --- a/crates/audio/src/audio_settings.rs +++ b/crates/audio/src/audio_settings.rs @@ -59,6 +59,7 @@ impl Settings for AudioSettings { fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } +/// See docs on [LIVE_SETTINGS] pub(crate) struct LiveSettings { pub(crate) control_input_volume: AtomicBool, pub(crate) control_output_volume: AtomicBool, @@ -89,7 +90,9 @@ impl LiveSettings { } /// Allows access to settings from the audio thread. Updated by -/// observer of SettingsStore. +/// observer of SettingsStore. Needed because audio playback and recording are +/// real time and must each run in a dedicated OS thread, therefore we can not +/// use the background executor. pub(crate) static LIVE_SETTINGS: LiveSettings = LiveSettings { control_input_volume: AtomicBool::new(true), control_output_volume: AtomicBool::new(true), diff --git a/crates/audio/src/rodio_ext.rs b/crates/audio/src/rodio_ext.rs index 4e9430a0b9..ba4e4ff055 100644 --- a/crates/audio/src/rodio_ext.rs +++ b/crates/audio/src/rodio_ext.rs @@ -9,7 +9,8 @@ use std::{ use crossbeam::queue::ArrayQueue; use rodio::{ChannelCount, Sample, SampleRate, Source}; -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] +#[error("Replay duration is too short must be >= 100ms")] pub struct ReplayDurationTooShort; pub trait RodioExt: Source + Sized { @@ -338,6 +339,7 @@ impl Iterator for Replayable { fn next(&mut self) -> Option { if let Some(sample) = self.inner.next() { self.buffer.push(sample); + // If the buffer is full send it if self.buffer.len() == self.chunk_size { self.tx.push_normal(std::mem::take(&mut self.buffer)); } @@ -422,6 +424,9 @@ impl Iterator for Replay { return None; } + // The queue does not support blocking on a next item. We want this queue as it + // is quite fast and provides a fixed size. We know how many samples are in a + // buffer so if we do not get one now we must be getting one after `sleep_duration`. std::thread::sleep(self.sleep_duration); } } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index d1b2cee4aa..7c86611310 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -186,6 +186,8 @@ impl AudioStack { let capture_task = if rodio_pipeline { info!("Using experimental.rodio_audio audio pipeline"); let voip_parts = audio::VoipParts::new(cx)?; + // Audio needs to run real-time and should never be paused. That is why we are using a + // normal std::thread and not a background task thread::spawn(move || { // microphone is non send on mac let microphone = audio::Audio::open_microphone(voip_parts)?; diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs index 67bfe79390..f605b3d517 100644 --- a/crates/livekit_client/src/livekit_client/playback/source.rs +++ b/crates/livekit_client/src/livekit_client/playback/source.rs @@ -12,9 +12,7 @@ fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer { let samples = SampleTypeConverter::<_, _>::new(samples); let samples: Vec = samples.collect(); SamplesBuffer::new( - // here be dragons - // NonZero::new(frame.num_channels as u16).expect("audio frame channels is nonzero"), - nz!(2), + nz!(2), // frame always has two channels NonZero::new(frame.sample_rate).expect("audio frame sample rate is nonzero"), samples, )