Fix experimental audio, add denoise, auto volume.Prep migration (#38874)

David Kleingeld created

Uses the previously merged denoising crate (and fixes a bug in it that
snug in during refactoring) to add denoising to the microphone input. 

Adds automatic volume control for microphone and output.

Prepares for migrating to 16kHz SR mono:
The experimental audio path now picks the samplerate and channel count depending on a setting. It can handle incoming streams with both the current (future legacy) and new samplerate & channel count. These are url-encoded into the livekit track name

Release Notes:

- N/A

Change summary

.config/hakari.toml                                         |   2 
Cargo.lock                                                  |  36 
Cargo.toml                                                  |   2 
assets/settings/default.json                                |  32 +
crates/audio/Cargo.toml                                     |   1 
crates/audio/src/audio.rs                                   |  78 ++
crates/audio/src/audio_settings.rs                          |  95 ++-
crates/audio/src/rodio_ext.rs                               | 167 ++++++
crates/denoise/src/engine.rs                                |   6 
crates/denoise/src/lib.rs                                   |   2 
crates/livekit_client/src/livekit_client.rs                 |  20 
crates/livekit_client/src/livekit_client/playback.rs        | 114 +++-
crates/livekit_client/src/livekit_client/playback/source.rs |  40 +
crates/settings/src/settings_content.rs                     |  44 +
tooling/workspace-hack/Cargo.toml                           |  22 
15 files changed, 499 insertions(+), 162 deletions(-)

Detailed changes

.config/hakari.toml 🔗

@@ -26,7 +26,7 @@ third-party = [
     # build of remote_server should not include scap / its x11 dependency
     { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
     # build of remote_server should not need to include on libalsa through rodio
-    { name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
+    { name = "rodio", git = "https://github.com/RustAudio/rodio" },
 ]
 
 [final-excludes]

Cargo.lock 🔗

@@ -1405,6 +1405,7 @@ dependencies = [
  "async-tar",
  "collections",
  "crossbeam",
+ "denoise",
  "gpui",
  "libwebrtc",
  "log",
@@ -2727,7 +2728,7 @@ dependencies = [
  "cap-primitives",
  "cap-std",
  "io-lifetimes",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2756,7 +2757,7 @@ dependencies = [
  "maybe-owned",
  "rustix 1.0.7",
  "rustix-linux-procfs",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
  "winx",
 ]
 
@@ -5486,7 +5487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
 dependencies = [
  "libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -5853,7 +5854,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
 dependencies = [
  "cfg-if",
  "rustix 1.0.7",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -6241,7 +6242,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
 dependencies = [
  "io-lifetimes",
  "rustix 1.0.7",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -8143,7 +8144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
 dependencies = [
  "io-lifetimes",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -8216,7 +8217,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
 dependencies = [
  "hermit-abi 0.5.0",
  "libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -12558,7 +12559,7 @@ dependencies = [
  "once_cell",
  "socket2",
  "tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -13398,7 +13399,7 @@ dependencies = [
 [[package]]
 name = "rodio"
 version = "0.21.1"
-source = "git+https://github.com/RustAudio/rodio?branch=better_wav_output#82514bd1f2c6cfd9a1a885019b26a8ffea75bc5c"
+source = "git+https://github.com/RustAudio/rodio#e2074c6c2acf07b57cf717e076bdda7a9ac6e70b"
 dependencies = [
  "cpal",
  "dasp_sample",
@@ -13640,7 +13641,7 @@ dependencies = [
  "errno 0.3.11",
  "libc",
  "linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -13653,7 +13654,7 @@ dependencies = [
  "errno 0.3.11",
  "libc",
  "linux-raw-sys 0.9.4",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -13775,7 +13776,7 @@ dependencies = [
  "security-framework 3.2.0",
  "security-framework-sys",
  "webpki-root-certs",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -15157,7 +15158,7 @@ dependencies = [
  "cfg-if",
  "libc",
  "psm",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -15850,7 +15851,7 @@ dependencies = [
  "fd-lock",
  "io-lifetimes",
  "rustix 0.38.44",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
  "winx",
 ]
 
@@ -16032,7 +16033,7 @@ dependencies = [
  "getrandom 0.3.2",
  "once_cell",
  "rustix 1.0.7",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -18614,7 +18615,7 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
 dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -19326,7 +19327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
 dependencies = [
  "bitflags 2.9.0",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -19718,7 +19719,6 @@ dependencies = [
  "nix 0.29.0",
  "nix 0.30.1",
  "nom 7.1.3",
- "num",
  "num-bigint",
  "num-bigint-dig",
  "num-complex",

Cargo.toml 🔗

@@ -376,7 +376,7 @@ remote_server = { path = "crates/remote_server" }
 repl = { path = "crates/repl" }
 reqwest_client = { path = "crates/reqwest_client" }
 rich_text = { path = "crates/rich_text" }
-rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
+rodio = { git = "https://github.com/RustAudio/rodio" }
 rope = { path = "crates/rope" }
 rpc = { path = "crates/rpc" }
 rules_library = { path = "crates/rules_library" }

assets/settings/default.json 🔗

@@ -411,15 +411,33 @@
     "experimental.rodio_audio": false,
     // Requires 'rodio_audio: true'
     //
-    // Use the new audio systems automatic gain control for your microphone.
-    // This affects how loud you sound to others.
-    "experimental.control_input_volume": false,
+    // Automatically increase or decrease you microphone's volume. This affects how
+    // loud you sound to others.
+    //
+    // Recommended: off (default)
+    // Microphones are too quite in zed, until everyone is on experimental
+    // audio and has auto speaker volume on this will make you very loud
+    // compared to other speakers.
+    "experimental.auto_microphone_volume": false,
+    // Requires 'rodio_audio: true'
+    //
+    // Automatically increate or decrease the volume of other call members.
+    // This only affects how things sound for you.
+    "experimental.auto_speaker_volume": true,
     // Requires 'rodio_audio: true'
     //
-    // Use the new audio systems automatic gain control on everyone in the
-    // call. This makes call members who are too quite louder and those who are
-    // too loud quieter. This only affects how things sound for you.
-    "experimental.control_output_volume": false
+    // Remove background noises. Works great for typing, cars, dogs, AC. Does
+    // not work well on music.
+    "experimental.denoise": true,
+    // Requires 'rodio_audio: true'
+    //
+    // Use audio parameters compatible with the previous versions of
+    // experimental audio and non-experimental audio. When this is false you
+    // will sound strange to anyone not on the latest experimental audio. In
+    // the future we will migrate by setting this to false
+    //
+    // You need to rejoin a call for this setting to apply
+    "experimental.legacy_audio_compatible": true
   },
   // Scrollbar related settings
   "scrollbar": {

crates/audio/Cargo.toml 🔗

@@ -18,6 +18,7 @@ async-tar.workspace = true
 collections.workspace = true
 crossbeam.workspace = true
 gpui.workspace = true
+denoise = { path = "../denoise" }
 log.workspace = true
 parking_lot.workspace = true
 rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }

crates/audio/src/audio.rs 🔗

@@ -9,7 +9,7 @@ mod non_windows_and_freebsd_deps {
     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 rodio::source::LimitSettings;
     pub(super) use std::sync::Arc;
 }
 
@@ -31,18 +31,20 @@ pub use rodio_ext::RodioExt;
 
 use crate::audio_settings::LIVE_SETTINGS;
 
-// NOTE: We used to use WebRTC's mixer which only supported
-// 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up"
-// for audio output devices like speakers/bluetooth, we just hard-code
-// this; and downsample when we need to.
+// We are migrating to 16kHz sample rate from 48kHz. In the future
+// once we are reasonably sure most users have upgraded we will
+// remove the LEGACY parameters.
 //
-// Since most noise cancelling requires 16kHz we will move to
-// that in the future.
-pub const SAMPLE_RATE: NonZero<u32> = nz!(48000);
-pub const CHANNEL_COUNT: NonZero<u16> = nz!(2);
+// We migrate to 16kHz because it is sufficient for speech and required
+// by the denoiser and future Speech to Text layers.
+pub const SAMPLE_RATE: NonZero<u32> = nz!(16000);
+pub const CHANNEL_COUNT: NonZero<u16> = nz!(1);
 pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
     (SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
 
+pub const LEGACY_SAMPLE_RATE: NonZero<u32> = nz!(48000);
+pub const LEGACY_CHANNEL_COUNT: NonZero<u16> = nz!(2);
+
 pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
 
 pub fn init(cx: &mut App) {
@@ -106,11 +108,16 @@ impl Global for Audio {}
 
 impl Audio {
     fn ensure_output_exists(&mut self) -> Result<&Mixer> {
+        #[cfg(debug_assertions)]
+        log::warn!(
+            "Audio does not sound correct without optimizations. Use a release build to debug audio issues"
+        );
+
         if self.output_handle.is_none() {
-            self.output_handle = Some(
-                OutputStreamBuilder::open_default_stream()
-                    .context("Could not open default output stream")?,
-            );
+            let output_handle = OutputStreamBuilder::open_default_stream()
+                .context("Could not open default output stream")?;
+            info!("Output stream: {:?}", output_handle);
+            self.output_handle = Some(output_handle);
             if let Some(output_handle) = &self.output_handle {
                 let (mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
                 // or the mixer will end immediately as its empty.
@@ -160,13 +167,20 @@ impl Audio {
         let stream = rodio::microphone::MicrophoneBuilder::new()
             .default_device()?
             .default_config()?
-            .prefer_sample_rates([SAMPLE_RATE, SAMPLE_RATE.saturating_mul(nz!(2))])
-            // .prefer_channel_counts([nz!(1), nz!(2)])
+            .prefer_sample_rates([
+                SAMPLE_RATE, // sample rates trivially resamplable to `SAMPLE_RATE`
+                SAMPLE_RATE.saturating_mul(nz!(2)),
+                SAMPLE_RATE.saturating_mul(nz!(3)),
+                SAMPLE_RATE.saturating_mul(nz!(4)),
+            ])
+            .prefer_channel_counts([nz!(1), nz!(2), nz!(3), nz!(4)])
             .prefer_buffer_sizes(512..)
             .open_stream()?;
         info!("Opened microphone: {:?}", stream.config());
 
-        let (replay, stream) = UniformSourceIterator::new(stream, CHANNEL_COUNT, SAMPLE_RATE)
+        let stream = stream
+            .possibly_disconnected_channels_to_mono()
+            .constant_samplerate(SAMPLE_RATE)
             .limit(LimitSettings::live_performance())
             .process_buffer::<BUFFER_SIZE, _>(move |buffer| {
                 let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
@@ -187,15 +201,27 @@ impl Audio {
                     }
                 }
             })
-            .automatic_gain_control(1.0, 4.0, 0.0, 5.0)
+            .denoise()
+            .context("Could not set up denoiser")?
+            .automatic_gain_control(0.90, 1.0, 0.0, 5.0)
             .periodic_access(Duration::from_millis(100), move |agc_source| {
-                agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
-            })
-            .replayable(REPLAY_DURATION)?;
-
+                agc_source
+                    .set_enabled(LIVE_SETTINGS.auto_microphone_volume.load(Ordering::Relaxed));
+                let denoise = agc_source.inner_mut();
+                denoise.set_enabled(LIVE_SETTINGS.denoise.load(Ordering::Relaxed));
+            });
+
+        let stream = if voip_parts.legacy_audio_compatible {
+            stream.constant_params(LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE)
+        } else {
+            stream.constant_params(CHANNEL_COUNT, SAMPLE_RATE)
+        };
+
+        let (replay, stream) = stream.replayable(REPLAY_DURATION)?;
         voip_parts
             .replays
             .add_voip_stream("local microphone".to_string(), replay);
+
         Ok(stream)
     }
 
@@ -206,9 +232,10 @@ impl Audio {
         cx: &mut App,
     ) -> anyhow::Result<()> {
         let (replay_source, source) = source
-            .automatic_gain_control(1.0, 4.0, 0.0, 5.0)
+            .constant_params(CHANNEL_COUNT, SAMPLE_RATE)
+            .automatic_gain_control(0.90, 1.0, 0.0, 5.0)
             .periodic_access(Duration::from_millis(100), move |agc_source| {
-                agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
+                agc_source.set_enabled(LIVE_SETTINGS.auto_speaker_volume.load(Ordering::Relaxed));
             })
             .replayable(REPLAY_DURATION)
             .expect("REPLAY_DURATION is longer than 100ms");
@@ -269,6 +296,7 @@ impl Audio {
 pub struct VoipParts {
     echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
     replays: replays::Replays,
+    legacy_audio_compatible: bool,
 }
 
 #[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
@@ -277,8 +305,12 @@ impl VoipParts {
         let (apm, replays) = cx.try_read_default_global::<Audio, _>(|audio, _| {
             (Arc::clone(&audio.echo_canceller), audio.replays.clone())
         })?;
+        let legacy_audio_compatible =
+            AudioSettings::try_read_global(cx, |settings| settings.legacy_audio_compatible)
+                .unwrap_or(true);
 
         Ok(Self {
+            legacy_audio_compatible,
             echo_canceller: apm,
             replays,
         })

crates/audio/src/audio_settings.rs 🔗

@@ -6,18 +6,38 @@ use settings::{Settings, SettingsStore};
 #[derive(Clone, Debug)]
 pub struct AudioSettings {
     /// Opt into the new audio system.
+    ///
+    /// You need to rejoin a call for this setting to apply
     pub rodio_audio: bool, // default is false
     /// Requires 'rodio_audio: true'
     ///
-    /// Use the new audio systems automatic gain control for your microphone.
-    /// This affects how loud you sound to others.
-    pub control_input_volume: bool,
+    /// Automatically increase or decrease you microphone's volume. This affects how
+    /// loud you sound to others.
+    ///
+    /// Recommended: off (default)
+    /// Microphones are too quite in zed, until everyone is on experimental
+    /// audio and has auto speaker volume on this will make you very loud
+    /// compared to other speakers.
+    pub auto_microphone_volume: bool,
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Automatically increate or decrease the volume of other call members.
+    /// This only affects how things sound for you.
+    pub auto_speaker_volume: bool,
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Remove background noises. Works great for typing, cars, dogs, AC. Does
+    /// not work well on music.
+    pub denoise: bool,
     /// Requires 'rodio_audio: true'
     ///
-    /// Use the new audio systems automatic gain control on everyone in the
-    /// call. This makes call members who are too quite louder and those who are
-    /// too loud quieter. This only affects how things sound for you.
-    pub control_output_volume: bool,
+    /// Use audio parameters compatible with the previous versions of
+    /// experimental audio and non-experimental audio. When this is false you
+    /// will sound strange to anyone not on the latest experimental audio. In
+    /// the future we will migrate by setting this to false
+    ///
+    /// You need to rejoin a call for this setting to apply
+    pub legacy_audio_compatible: bool,
 }
 
 /// Configuration of audio in Zed
@@ -25,46 +45,66 @@ impl Settings for AudioSettings {
     fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let audio = &content.audio.as_ref().unwrap();
         AudioSettings {
-            control_input_volume: audio.control_input_volume.unwrap(),
-            control_output_volume: audio.control_output_volume.unwrap(),
             rodio_audio: audio.rodio_audio.unwrap(),
+            auto_microphone_volume: audio.auto_microphone_volume.unwrap(),
+            auto_speaker_volume: audio.auto_speaker_volume.unwrap(),
+            denoise: audio.denoise.unwrap(),
+            legacy_audio_compatible: audio.legacy_audio_compatible.unwrap(),
         }
     }
-
-    fn import_from_vscode(
-        _vscode: &settings::VsCodeSettings,
-        _current: &mut settings::SettingsContent,
-    ) {
-    }
 }
 
 /// See docs on [LIVE_SETTINGS]
 pub(crate) struct LiveSettings {
-    pub(crate) control_input_volume: AtomicBool,
-    pub(crate) control_output_volume: AtomicBool,
+    pub(crate) auto_microphone_volume: AtomicBool,
+    pub(crate) auto_speaker_volume: AtomicBool,
+    pub(crate) denoise: AtomicBool,
 }
 
 impl LiveSettings {
     pub(crate) fn initialize(&self, cx: &mut App) {
         cx.observe_global::<SettingsStore>(move |cx| {
-            LIVE_SETTINGS.control_input_volume.store(
-                AudioSettings::get_global(cx).control_input_volume,
+            LIVE_SETTINGS.auto_microphone_volume.store(
+                AudioSettings::get_global(cx).auto_microphone_volume,
                 Ordering::Relaxed,
             );
-            LIVE_SETTINGS.control_output_volume.store(
-                AudioSettings::get_global(cx).control_output_volume,
+            LIVE_SETTINGS.auto_speaker_volume.store(
+                AudioSettings::get_global(cx).auto_speaker_volume,
                 Ordering::Relaxed,
             );
+
+            let denoise_enabled = AudioSettings::get_global(cx).denoise;
+            #[cfg(debug_assertions)]
+            {
+                static DENOISE_WARNING_SEND: AtomicBool = AtomicBool::new(false);
+                if denoise_enabled && !DENOISE_WARNING_SEND.load(Ordering::Relaxed) {
+                    DENOISE_WARNING_SEND.store(true, Ordering::Relaxed);
+                    log::warn!("Denoise does not work on debug builds, not enabling")
+                }
+            }
+            #[cfg(not(debug_assertions))]
+            LIVE_SETTINGS
+                .denoise
+                .store(denoise_enabled, Ordering::Relaxed);
         })
         .detach();
 
         let init_settings = AudioSettings::get_global(cx);
         LIVE_SETTINGS
-            .control_input_volume
-            .store(init_settings.control_input_volume, Ordering::Relaxed);
+            .auto_microphone_volume
+            .store(init_settings.auto_microphone_volume, Ordering::Relaxed);
+        LIVE_SETTINGS
+            .auto_speaker_volume
+            .store(init_settings.auto_speaker_volume, Ordering::Relaxed);
+        let denoise_enabled = AudioSettings::get_global(cx).denoise;
+        #[cfg(debug_assertions)]
+        if denoise_enabled {
+            log::warn!("Denoise does not work on debug builds, not enabling")
+        }
+        #[cfg(not(debug_assertions))]
         LIVE_SETTINGS
-            .control_output_volume
-            .store(init_settings.control_output_volume, Ordering::Relaxed);
+            .denoise
+            .store(denoise_enabled, Ordering::Relaxed);
     }
 }
 
@@ -73,6 +113,7 @@ impl LiveSettings {
 /// 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),
+    auto_microphone_volume: AtomicBool::new(true),
+    auto_speaker_volume: AtomicBool::new(true),
+    denoise: AtomicBool::new(true),
 };

crates/audio/src/rodio_ext.rs 🔗

@@ -1,4 +1,5 @@
 use std::{
+    num::NonZero,
     sync::{
         Arc, Mutex,
         atomic::{AtomicBool, Ordering},
@@ -7,12 +8,22 @@ use std::{
 };
 
 use crossbeam::queue::ArrayQueue;
-use rodio::{ChannelCount, Sample, SampleRate, Source};
+use denoise::{Denoiser, DenoiserError};
+use log::warn;
+use rodio::{
+    ChannelCount, Sample, SampleRate, Source, conversions::SampleRateConverter, nz,
+    source::UniformSourceIterator,
+};
+
+const MAX_CHANNELS: usize = 8;
 
 #[derive(Debug, thiserror::Error)]
 #[error("Replay duration is too short must be >= 100ms")]
 pub struct ReplayDurationTooShort;
 
+// These all require constant sources (so the span is infinitely long)
+// this is not guaranteed by rodio however we know it to be true in all our
+// applications. Rodio desperately needs a constant source concept.
 pub trait RodioExt: Source + Sized {
     fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
     where
@@ -25,6 +36,14 @@ pub trait RodioExt: Source + Sized {
         duration: Duration,
     ) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
     fn take_samples(self, n: usize) -> TakeSamples<Self>;
+    fn denoise(self) -> Result<Denoiser<Self>, DenoiserError>;
+    fn constant_params(
+        self,
+        channel_count: ChannelCount,
+        sample_rate: SampleRate,
+    ) -> UniformSourceIterator<Self>;
+    fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self>;
+    fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self>;
 }
 
 impl<S: Source> RodioExt for S {
@@ -101,8 +120,149 @@ impl<S: Source> RodioExt for S {
             left_to_take: n,
         }
     }
+    fn denoise(self) -> Result<Denoiser<Self>, DenoiserError> {
+        let res = Denoiser::try_new(self);
+        res
+    }
+    fn constant_params(
+        self,
+        channel_count: ChannelCount,
+        sample_rate: SampleRate,
+    ) -> UniformSourceIterator<Self> {
+        UniformSourceIterator::new(self, channel_count, sample_rate)
+    }
+    fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self> {
+        ConstantSampleRate::new(self, sample_rate)
+    }
+    fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self> {
+        ToMono::new(self)
+    }
+}
+
+pub struct ConstantSampleRate<S: Source> {
+    inner: SampleRateConverter<S>,
+    channels: ChannelCount,
+    sample_rate: SampleRate,
+}
+
+impl<S: Source> ConstantSampleRate<S> {
+    fn new(source: S, target_rate: SampleRate) -> Self {
+        let input_sample_rate = source.sample_rate();
+        let channels = source.channels();
+        let inner = SampleRateConverter::new(source, input_sample_rate, target_rate, channels);
+        Self {
+            inner,
+            channels,
+            sample_rate: target_rate,
+        }
+    }
+}
+
+impl<S: Source> Iterator for ConstantSampleRate<S> {
+    type Item = rodio::Sample;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.inner.next()
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.inner.size_hint()
+    }
+}
+
+impl<S: Source> Source for ConstantSampleRate<S> {
+    fn current_span_len(&self) -> Option<usize> {
+        None
+    }
+
+    fn channels(&self) -> ChannelCount {
+        self.channels
+    }
+
+    fn sample_rate(&self) -> SampleRate {
+        self.sample_rate
+    }
+
+    fn total_duration(&self) -> Option<Duration> {
+        None // not supported (not used by us)
+    }
+}
+
+const TYPICAL_NOISE_FLOOR: Sample = 1e-3;
+
+/// constant source, only works on a single span
+pub struct ToMono<S> {
+    inner: S,
+    input_channel_count: ChannelCount,
+    connected_channels: ChannelCount,
+    /// running mean of second channel 'volume'
+    means: [f32; MAX_CHANNELS],
+}
+impl<S: Source> ToMono<S> {
+    fn new(input: S) -> Self {
+        let channels = input
+            .channels()
+            .min(const { NonZero::<u16>::new(MAX_CHANNELS as u16).unwrap() });
+        if channels < input.channels() {
+            warn!("Ignoring input channels {}..", channels.get());
+        }
+
+        Self {
+            connected_channels: channels,
+            input_channel_count: channels,
+            inner: input,
+            means: [TYPICAL_NOISE_FLOOR; MAX_CHANNELS],
+        }
+    }
+}
+
+impl<S: Source> Source for ToMono<S> {
+    fn current_span_len(&self) -> Option<usize> {
+        None
+    }
+
+    fn channels(&self) -> ChannelCount {
+        rodio::nz!(1)
+    }
+
+    fn sample_rate(&self) -> SampleRate {
+        self.inner.sample_rate()
+    }
+
+    fn total_duration(&self) -> Option<Duration> {
+        self.inner.total_duration()
+    }
+}
+
+fn update_mean(mean: &mut f32, sample: Sample) {
+    const HISTORY: f32 = 500.0;
+    *mean *= (HISTORY - 1.0) / HISTORY;
+    *mean += sample.abs() / HISTORY;
+}
+
+impl<S: Source> Iterator for ToMono<S> {
+    type Item = Sample;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let mut mono_sample = 0f32;
+        let mut active_channels = 0;
+        for channel in 0..self.input_channel_count.get() as usize {
+            let sample = self.inner.next()?;
+            mono_sample += sample;
+
+            update_mean(&mut self.means[channel], sample);
+            if self.means[channel] > TYPICAL_NOISE_FLOOR / 10.0 {
+                active_channels += 1;
+            }
+        }
+        mono_sample /= self.connected_channels.get() as f32;
+        self.connected_channels = NonZero::new(active_channels).unwrap_or(nz!(1));
+
+        Some(mono_sample)
+    }
 }
 
+/// constant source, only works on a single span
 pub struct TakeSamples<S> {
     inner: S,
     left_to_take: usize,
@@ -147,6 +307,7 @@ impl<S: Source> Source for TakeSamples<S> {
     }
 }
 
+/// constant source, only works on a single span
 #[derive(Debug)]
 struct ReplayQueue {
     inner: ArrayQueue<Vec<Sample>>,
@@ -193,6 +354,7 @@ impl ReplayQueue {
     }
 }
 
+/// constant source, only works on a single span
 pub struct ProcessBuffer<const N: usize, S, F>
 where
     S: Source + Sized,
@@ -260,6 +422,7 @@ where
     }
 }
 
+/// constant source, only works on a single span
 pub struct InspectBuffer<const N: usize, S, F>
 where
     S: Source + Sized,
@@ -324,6 +487,7 @@ where
     }
 }
 
+/// constant source, only works on a single span
 #[derive(Debug)]
 pub struct Replayable<S: Source> {
     inner: S,
@@ -375,6 +539,7 @@ impl<S: Source> Source for Replayable<S> {
     }
 }
 
+/// constant source, only works on a single span
 #[derive(Debug)]
 pub struct Replay {
     rx: Arc<ReplayQueue>,

crates/denoise/src/engine.rs 🔗

@@ -138,13 +138,13 @@ impl Engine {
 
         const SPECTRUM_INPUT: &str = "input_2";
         const MEMORY_INPUT: &str = "input_3";
-        let memory_input =
+        let spectrum =
             Tensor::from_slice::<_, f32>(&self.in_magnitude, (1, 1, FFT_OUT_SIZE), &Device::Cpu)
                 .expect("the in magnitude has enough elements to fill the Tensor");
 
         let inputs = HashMap::from([
-            (MEMORY_INPUT.to_string(), memory_input),
-            (SPECTRUM_INPUT.to_string(), self.spectral_memory.clone()),
+            (SPECTRUM_INPUT.to_string(), spectrum),
+            (MEMORY_INPUT.to_string(), self.spectral_memory.clone()),
         ]);
         inputs
     }

crates/denoise/src/lib.rs 🔗

@@ -84,7 +84,7 @@ impl<S: Source> Denoiser<S> {
             .spawn(move || {
                 run_neural_denoiser(denoised_tx, input_rx);
             })
-            .unwrap();
+            .expect("Should be ablet to spawn threads");
 
         Ok(Self {
             inner: source,

crates/livekit_client/src/livekit_client.rs 🔗

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use anyhow::{Context as _, Result};
+use anyhow::{Context as _, Result, anyhow};
 use audio::AudioSettings;
 use collections::HashMap;
 use futures::{SinkExt, channel::mpsc};
@@ -12,7 +12,10 @@ use settings::Settings;
 
 mod playback;
 
-use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication};
+use crate::{
+    LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication,
+    livekit_client::playback::Speaker,
+};
 pub use playback::AudioStream;
 pub(crate) use playback::{RemoteVideoFrame, play_remote_video_track};
 
@@ -132,11 +135,20 @@ impl Room {
         track: &RemoteAudioTrack,
         cx: &mut App,
     ) -> Result<playback::AudioStream> {
+        let speaker: Speaker =
+            serde_urlencoded::from_str(&track.0.name()).unwrap_or_else(|_| Speaker {
+                name: track.0.name(),
+                is_staff: false,
+                sends_legacy_audio: true,
+            });
+
         if AudioSettings::get_global(cx).rodio_audio {
             info!("Using experimental.rodio_audio audio pipeline for output");
-            playback::play_remote_audio_track(&track.0, cx)
-        } else {
+            playback::play_remote_audio_track(&track.0, speaker, cx)
+        } else if speaker.sends_legacy_audio {
             Ok(self.playback.play_remote_audio_track(&track.0))
+        } else {
+            Err(anyhow!("Client version too old to play audio in call"))
         }
     }
 }

crates/livekit_client/src/livekit_client/playback.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::{Context as _, Result};
 
-use audio::{AudioSettings, CHANNEL_COUNT, SAMPLE_RATE};
+use audio::{AudioSettings, CHANNEL_COUNT, LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE, SAMPLE_RATE};
 use cpal::traits::{DeviceTrait, StreamTrait as _};
 use futures::channel::mpsc::UnboundedSender;
 use futures::{Stream, StreamExt as _};
@@ -43,12 +43,15 @@ pub(crate) struct AudioStack {
 
 pub(crate) fn play_remote_audio_track(
     track: &livekit::track::RemoteAudioTrack,
+    speaker: Speaker,
     cx: &mut gpui::App,
 ) -> Result<AudioStream> {
+    info!("speaker: {speaker:?}");
+    let stream =
+        source::LiveKitStream::new(cx.background_executor(), track, speaker.sends_legacy_audio);
+
     let stop_handle = Arc::new(AtomicBool::new(false));
     let stop_handle_clone = stop_handle.clone();
-    let stream = source::LiveKitStream::new(cx.background_executor(), track);
-
     let stream = stream
         .stoppable()
         .periodic_access(Duration::from_millis(50), move |s| {
@@ -57,10 +60,8 @@ pub(crate) fn play_remote_audio_track(
             }
         });
 
-    let speaker: Speaker = serde_urlencoded::from_str(&track.name()).unwrap_or_else(|_| Speaker {
-        name: track.name(),
-        is_staff: false,
-    });
+    info!("sample_rate: {:?}", stream.sample_rate());
+    info!("channel_count: {:?}", stream.channels());
     audio::Audio::play_voip_stream(stream, speaker.name, speaker.is_staff, cx)
         .context("Could not play audio")?;
 
@@ -96,8 +97,8 @@ impl AudioStack {
         let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed);
         let source = AudioMixerSource {
             ssrc: next_ssrc,
-            sample_rate: SAMPLE_RATE.get(),
-            num_channels: CHANNEL_COUNT.get() as u32,
+            sample_rate: LEGACY_SAMPLE_RATE.get(),
+            num_channels: LEGACY_CHANNEL_COUNT.get() as u32,
             buffer: Arc::default(),
         };
         self.mixer.lock().add_source(source.clone());
@@ -137,9 +138,14 @@ impl AudioStack {
             let apm = self.apm.clone();
             let mixer = self.mixer.clone();
             async move {
-                Self::play_output(apm, mixer, SAMPLE_RATE.get(), CHANNEL_COUNT.get().into())
-                    .await
-                    .log_err();
+                Self::play_output(
+                    apm,
+                    mixer,
+                    LEGACY_SAMPLE_RATE.get(),
+                    LEGACY_CHANNEL_COUNT.get().into(),
+                )
+                .await
+                .log_err();
             }
         }));
         *self._output_task.borrow_mut() = Arc::downgrade(&task);
@@ -152,19 +158,36 @@ impl AudioStack {
         is_staff: bool,
         cx: &AsyncApp,
     ) -> Result<(crate::LocalAudioTrack, AudioStream)> {
-        let source = NativeAudioSource::new(
-            // n.b. this struct's options are always ignored, noise cancellation is provided by apm.
-            AudioSourceOptions::default(),
-            SAMPLE_RATE.get(),
-            CHANNEL_COUNT.get().into(),
-            10,
-        );
+        let legacy_audio_compatible =
+            AudioSettings::try_read_global(cx, |setting| setting.legacy_audio_compatible)
+                .unwrap_or(true);
+
+        let source = if legacy_audio_compatible {
+            NativeAudioSource::new(
+                // n.b. this struct's options are always ignored, noise cancellation is provided by apm.
+                AudioSourceOptions::default(),
+                LEGACY_SAMPLE_RATE.get(),
+                LEGACY_CHANNEL_COUNT.get().into(),
+                10,
+            )
+        } else {
+            NativeAudioSource::new(
+                // n.b. this struct's options are always ignored, noise cancellation is provided by apm.
+                AudioSourceOptions::default(),
+                SAMPLE_RATE.get(),
+                CHANNEL_COUNT.get().into(),
+                10,
+            )
+        };
 
-        let track_name = serde_urlencoded::to_string(Speaker {
+        let speaker = Speaker {
             name: user_name,
             is_staff,
-        })
-        .context("Could not encode user information in track name")?;
+            sends_legacy_audio: legacy_audio_compatible,
+        };
+        log::info!("Microphone speaker: {speaker:?}");
+        let track_name = serde_urlencoded::to_string(speaker)
+            .context("Could not encode user information in track name")?;
 
         let track = track::LocalAudioTrack::create_audio_track(
             &track_name,
@@ -186,22 +209,32 @@ 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
+            // 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::Builder::new()
-                .name("AudioCapture".to_string())
+                .name("MicrophoneToLivekit".to_string())
                 .spawn(move || {
                     // microphone is non send on mac
-                    let microphone = audio::Audio::open_microphone(voip_parts)?;
+                    let microphone = match audio::Audio::open_microphone(voip_parts) {
+                        Ok(m) => m,
+                        Err(e) => {
+                            log::error!("Could not open microphone: {e}");
+                            return;
+                        }
+                    };
                     send_to_livekit(frame_tx, microphone);
-                    Ok::<(), anyhow::Error>(())
                 })
-                .unwrap();
+                .expect("should be able to spawn threads");
             Task::ready(Ok(()))
         } else {
             self.executor.spawn(async move {
-                Self::capture_input(apm, frame_tx, SAMPLE_RATE.get(), CHANNEL_COUNT.get().into())
-                    .await
+                Self::capture_input(
+                    apm,
+                    frame_tx,
+                    LEGACY_SAMPLE_RATE.get(),
+                    LEGACY_CHANNEL_COUNT.get().into(),
+                )
+                .await
             })
         };
 
@@ -388,26 +421,31 @@ impl AudioStack {
     }
 }
 
-#[derive(Serialize, Deserialize)]
-struct Speaker {
-    name: String,
-    is_staff: bool,
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Speaker {
+    pub name: String,
+    pub is_staff: bool,
+    pub sends_legacy_audio: bool,
 }
 
 fn send_to_livekit(frame_tx: UnboundedSender<AudioFrame<'static>>, mut microphone: impl Source) {
     use cpal::Sample;
+    let sample_rate = microphone.sample_rate().get();
+    let num_channels = microphone.channels().get() as u32;
+    let buffer_size = sample_rate / 100 * num_channels;
+
     loop {
         let sampled: Vec<_> = microphone
             .by_ref()
-            .take(audio::BUFFER_SIZE)
+            .take(buffer_size as usize)
             .map(|s| s.to_sample())
             .collect();
 
         if frame_tx
             .unbounded_send(AudioFrame {
-                sample_rate: SAMPLE_RATE.get(),
-                num_channels: CHANNEL_COUNT.get() as u32,
-                samples_per_channel: sampled.len() as u32 / CHANNEL_COUNT.get() as u32,
+                sample_rate,
+                num_channels,
+                samples_per_channel: sampled.len() as u32 / num_channels,
                 data: Cow::Owned(sampled),
             })
             .is_err()

crates/livekit_client/src/livekit_client/playback/source.rs 🔗

@@ -3,17 +3,19 @@ use std::num::NonZero;
 use futures::StreamExt;
 use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame};
 use livekit::track::RemoteAudioTrack;
-use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter, nz};
+use rodio::{
+    ChannelCount, SampleRate, Source, buffer::SamplesBuffer, conversions::SampleTypeConverter,
+};
 
-use audio::{CHANNEL_COUNT, SAMPLE_RATE};
+use audio::{CHANNEL_COUNT, LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE, SAMPLE_RATE};
 
 fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer {
     let samples = frame.data.iter().copied();
     let samples = SampleTypeConverter::<_, _>::new(samples);
     let samples: Vec<f32> = samples.collect();
     SamplesBuffer::new(
-        nz!(2), // frame always has two channels
-        NonZero::new(frame.sample_rate).expect("audio frame sample rate is nonzero"),
+        NonZero::new(frame.num_channels as u16).expect("zero channels is nonsense"),
+        NonZero::new(frame.sample_rate).expect("samplerate zero is nonsense"),
         samples,
     )
 }
@@ -22,14 +24,26 @@ pub struct LiveKitStream {
     // shared_buffer: SharedBuffer,
     inner: rodio::queue::SourcesQueueOutput,
     _receiver_task: gpui::Task<()>,
+    channel_count: ChannelCount,
+    sample_rate: SampleRate,
 }
 
 impl LiveKitStream {
-    pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self {
+    pub fn new(
+        executor: &gpui::BackgroundExecutor,
+        track: &RemoteAudioTrack,
+        legacy: bool,
+    ) -> Self {
+        let (channel_count, sample_rate) = if legacy {
+            (LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE)
+        } else {
+            (CHANNEL_COUNT, SAMPLE_RATE)
+        };
+
         let mut stream = NativeAudioStream::new(
             track.rtc_track(),
-            SAMPLE_RATE.get() as i32,
-            CHANNEL_COUNT.get().into(),
+            sample_rate.get() as i32,
+            channel_count.get().into(),
         );
         let (queue_input, queue_output) = rodio::queue::queue(true);
         // spawn rtc stream
@@ -45,6 +59,8 @@ impl LiveKitStream {
         LiveKitStream {
             _receiver_task: receiver_task,
             inner: queue_output,
+            sample_rate,
+            channel_count,
         }
     }
 }
@@ -63,17 +79,11 @@ impl Source for LiveKitStream {
     }
 
     fn channels(&self) -> rodio::ChannelCount {
-        // This must be hardcoded because the playback source assumes constant
-        // sample rate and channel count. The queue upon which this is build
-        // will however report different counts and rates. Even though we put in
-        // only items with our (constant) CHANNEL_COUNT & SAMPLE_RATE this will
-        // play silence on one channel and at 44100 which is not what our
-        // constants are.
-        CHANNEL_COUNT
+        self.channel_count
     }
 
     fn sample_rate(&self) -> rodio::SampleRate {
-        SAMPLE_RATE // see comment on channels
+        self.sample_rate
     }
 
     fn total_duration(&self) -> Option<std::time::Duration> {

crates/settings/src/settings_content.rs 🔗

@@ -272,21 +272,43 @@ pub struct TitleBarSettingsContent {
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct AudioSettingsContent {
     /// Opt into the new audio system.
-    #[serde(rename = "experimental.rodio_audio", default)]
-    pub rodio_audio: Option<bool>,
+    ///
+    /// You need to rejoin a call for this setting to apply
+    #[serde(rename = "experimental.rodio_audio")]
+    pub rodio_audio: Option<bool>, // default is false
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Automatically increase or decrease you microphone's volume. This affects how
+    /// loud you sound to others.
+    ///
+    /// Recommended: off (default)
+    /// Microphones are too quite in zed, until everyone is on experimental
+    /// audio and has auto speaker volume on this will make you very loud
+    /// compared to other speakers.
+    #[serde(rename = "experimental.auto_microphone_volume")]
+    pub auto_microphone_volume: Option<bool>,
     /// Requires 'rodio_audio: true'
     ///
-    /// Use the new audio systems automatic gain control for your microphone.
-    /// This affects how loud you sound to others.
-    #[serde(rename = "experimental.control_input_volume", default)]
-    pub control_input_volume: Option<bool>,
+    /// Automatically increate or decrease the volume of other call members.
+    /// This only affects how things sound for you.
+    #[serde(rename = "experimental.auto_speaker_volume")]
+    pub auto_speaker_volume: Option<bool>,
     /// Requires 'rodio_audio: true'
     ///
-    /// Use the new audio systems automatic gain control on everyone in the
-    /// call. This makes call members who are too quite louder and those who are
-    /// too loud quieter. This only affects how things sound for you.
-    #[serde(rename = "experimental.control_output_volume", default)]
-    pub control_output_volume: Option<bool>,
+    /// Remove background noises. Works great for typing, cars, dogs, AC. Does
+    /// not work well on music.
+    #[serde(rename = "experimental.denoise")]
+    pub denoise: Option<bool>,
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Use audio parameters compatible with the previous versions of
+    /// experimental audio and non-experimental audio. When this is false you
+    /// will sound strange to anyone not on the latest experimental audio. In
+    /// the future we will migrate by setting this to false
+    ///
+    /// You need to rejoin a call for this setting to apply
+    #[serde(rename = "experimental.legacy_audio_compatible")]
+    pub legacy_audio_compatible: Option<bool>,
 }
 
 /// Control what info is collected by Zed.

tooling/workspace-hack/Cargo.toml 🔗

@@ -88,7 +88,6 @@ mime_guess = { version = "2" }
 miniz_oxide = { version = "0.8", features = ["simd"] }
 nom = { version = "7" }
 num-bigint = { version = "0.4" }
-num-complex = { version = "0.4", features = ["bytemuck"] }
 num-integer = { version = "0.1", features = ["i128"] }
 num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
 num-rational = { version = "0.4", features = ["num-bigint-std"] }
@@ -222,7 +221,6 @@ mime_guess = { version = "2" }
 miniz_oxide = { version = "0.8", features = ["simd"] }
 nom = { version = "7" }
 num-bigint = { version = "0.4" }
-num-complex = { version = "0.4", features = ["bytemuck"] }
 num-integer = { version = "0.1", features = ["i128"] }
 num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
 num-rational = { version = "0.4", features = ["num-bigint-std"] }
@@ -299,7 +297,6 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["http1"
 livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 naga = { version = "25", features = ["msl-out", "wgsl-in"] }
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-num = { version = "0.4" }
 objc2 = { version = "0.6" }
 objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] }
 objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] }
@@ -330,7 +327,6 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["http1"
 livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 naga = { version = "25", features = ["msl-out", "wgsl-in"] }
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-num = { version = "0.4" }
 objc2 = { version = "0.6" }
 objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] }
 objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] }
@@ -362,7 +358,6 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["http1"
 livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 naga = { version = "25", features = ["msl-out", "wgsl-in"] }
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-num = { version = "0.4" }
 objc2 = { version = "0.6" }
 objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] }
 objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] }
@@ -393,7 +388,6 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["http1"
 livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 naga = { version = "25", features = ["msl-out", "wgsl-in"] }
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-num = { version = "0.4" }
 objc2 = { version = "0.6" }
 objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] }
 objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] }
@@ -437,6 +431,7 @@ nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "m
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
 num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
+num-complex = { version = "0.4", features = ["bytemuck"] }
 object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
 proc-macro2 = { version = "1", features = ["span-locations"] }
 prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
@@ -480,6 +475,7 @@ nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "m
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
 num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
+num-complex = { version = "0.4", features = ["bytemuck"] }
 object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
 proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
 prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
@@ -521,6 +517,7 @@ nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "m
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
 num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
+num-complex = { version = "0.4", features = ["bytemuck"] }
 object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
 proc-macro2 = { version = "1", features = ["span-locations"] }
 prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
@@ -564,6 +561,7 @@ nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "m
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
 num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
+num-complex = { version = "0.4", features = ["bytemuck"] }
 object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
 proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
 prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
@@ -590,7 +588,6 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
 getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
 hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
 livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-num = { version = "0.4" }
 prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
 ring = { version = "0.17", features = ["std"] }
 rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "fs", "net"] }
@@ -603,8 +600,8 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti
 winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
 windows-core = { version = "0.61" }
 windows-numerics = { version = "0.2" }
-windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
-windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
+windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
+windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
 windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
 windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
 
@@ -617,7 +614,6 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
 getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
 hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
 livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-num = { version = "0.4" }
 proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
 prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
 ring = { version = "0.17", features = ["std"] }
@@ -631,8 +627,8 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti
 winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
 windows-core = { version = "0.61" }
 windows-numerics = { version = "0.2" }
-windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
-windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
+windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
+windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
 windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
 windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
 
@@ -660,6 +656,7 @@ nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "m
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
 num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
+num-complex = { version = "0.4", features = ["bytemuck"] }
 object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
 proc-macro2 = { version = "1", features = ["span-locations"] }
 prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
@@ -703,6 +700,7 @@ nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "m
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
 num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
+num-complex = { version = "0.4", features = ["bytemuck"] }
 object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
 proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
 prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }