From 194a13ffb5623e2657a1916db4edab314914720a Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Fri, 19 Sep 2025 12:31:54 +0200 Subject: [PATCH] Add denoising & prepare for migrating to new samplerate & channel count (#38493) Uses the previously merged denoising crate (and fixes a bug in it that snug in during refactoring) in the microphone input. 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. --- Cargo.lock | 2 +- assets/settings/default.json | 32 +++- crates/audio/Cargo.toml | 1 + crates/audio/src/audio.rs | 65 +++++-- 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 ++- .../src/livekit_client/playback.rs | 89 +++++++--- .../src/livekit_client/playback/source.rs | 40 +++-- crates/settings/src/settings_content.rs | 44 +++-- tooling/workspace-hack/Cargo.toml | 14 +- 13 files changed, 454 insertions(+), 123 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3acfed9bd7cfa8bc2742bb4f006c38a4f65a1f0e..be3e5b04ca18d56024eabe45f14562fca3d56375 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1405,6 +1405,7 @@ dependencies = [ "async-tar", "collections", "crossbeam", + "denoise", "gpui", "libwebrtc", "log", @@ -20742,7 +20743,6 @@ dependencies = [ "nix 0.29.0", "nix 0.30.1", "nom 7.1.3", - "num", "num-bigint", "num-bigint-dig", "num-complex", diff --git a/assets/settings/default.json b/assets/settings/default.json index 091231521470ebec50cf1351a76063e9205a3d24..d469638ab28ea02eb9b7675296ee9582e2de3ccd 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -413,15 +413,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": { diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index c083c9a659e50aef37acc2cdfc239696bd469c1e..7f2fed80e2315e51fca7d8477b04885998336632 100644 --- a/crates/audio/Cargo.toml +++ b/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" ] } diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index f60ddb87b9615d2da9c2be248ab397c19a463616..dc4d97a8fa47f11f9120cf5144a37ae6fd94bc2a 100644 --- a/crates/audio/src/audio.rs +++ b/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 = nz!(48000); -pub const CHANNEL_COUNT: NonZero = 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 = nz!(16000); +pub const CHANNEL_COUNT: NonZero = 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 = nz!(48000); +pub const LEGACY_CHANNEL_COUNT: NonZero = nz!(2); + pub const REPLAY_DURATION: Duration = Duration::from_secs(30); pub fn init(cx: &mut App) { @@ -106,6 +108,11 @@ 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() @@ -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 (replay, stream) = stream + .possibly_disconnected_channels_to_mono() + .constant_samplerate(SAMPLE_RATE) .limit(LimitSettings::live_performance()) .process_buffer::(move |buffer| { let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample()); @@ -187,15 +201,28 @@ impl Audio { } } }) - .automatic_gain_control(1.0, 4.0, 0.0, 5.0) + .denoise() + .context("Could not set up denoiser")? + .periodic_access(Duration::from_millis(100), move |denoise| { + denoise.set_enabled(LIVE_SETTINGS.denoise.load(Ordering::Relaxed)); + }) + .automatic_gain_control(1.0, 2.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_microphone_volume.load(Ordering::Relaxed)); }) .replayable(REPLAY_DURATION)?; voip_parts .replays .add_voip_stream("local microphone".to_string(), replay); + + 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) + }; + Ok(stream) } @@ -206,9 +233,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(1.0, 2.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 +297,7 @@ impl Audio { pub struct VoipParts { echo_canceller: Arc>, replays: replays::Replays, + legacy_audio_compatible: bool, } #[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))] @@ -277,8 +306,12 @@ impl VoipParts { let (apm, replays) = cx.try_read_default_global::(|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_default(); Ok(Self { + legacy_audio_compatible, echo_canceller: apm, replays, }) diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index 2c9db4989efa5edcf4ef84c4e3031b53980fad51..cba7d45c31f4674be6a69c10ab34f00e0b8cbbd1 100644 --- a/crates/audio/src/audio_settings.rs +++ b/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::(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), }; diff --git a/crates/audio/src/rodio_ext.rs b/crates/audio/src/rodio_ext.rs index e80b00e15a8fdbd3fc438b78a9ca45d0902dcef1..af4cc89252dfdc1498471ec7ac09b56d59b62eca 100644 --- a/crates/audio/src/rodio_ext.rs +++ b/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(self, callback: F) -> ProcessBuffer where @@ -25,6 +36,14 @@ pub trait RodioExt: Source + Sized { duration: Duration, ) -> Result<(Replay, Replayable), ReplayDurationTooShort>; fn take_samples(self, n: usize) -> TakeSamples; + fn denoise(self) -> Result, DenoiserError>; + fn constant_params( + self, + channel_count: ChannelCount, + sample_rate: SampleRate, + ) -> UniformSourceIterator; + fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate; + fn possibly_disconnected_channels_to_mono(self) -> ToMono; } impl RodioExt for S { @@ -101,8 +120,149 @@ impl RodioExt for S { left_to_take: n, } } + fn denoise(self) -> Result, DenoiserError> { + let res = Denoiser::try_new(self); + res + } + fn constant_params( + self, + channel_count: ChannelCount, + sample_rate: SampleRate, + ) -> UniformSourceIterator { + UniformSourceIterator::new(self, channel_count, sample_rate) + } + fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate { + ConstantSampleRate::new(self, sample_rate) + } + fn possibly_disconnected_channels_to_mono(self) -> ToMono { + ToMono::new(self) + } +} + +pub struct ConstantSampleRate { + inner: SampleRateConverter, + channels: ChannelCount, + sample_rate: SampleRate, +} + +impl ConstantSampleRate { + 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 Iterator for ConstantSampleRate { + type Item = rodio::Sample; + + fn next(&mut self) -> Option { + self.inner.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl Source for ConstantSampleRate { + fn current_span_len(&self) -> Option { + None + } + + fn channels(&self) -> ChannelCount { + self.channels + } + + fn sample_rate(&self) -> SampleRate { + self.sample_rate + } + + fn total_duration(&self) -> Option { + 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 { + inner: S, + input_channel_count: ChannelCount, + connected_channels: ChannelCount, + /// running mean of second channel 'volume' + means: [f32; MAX_CHANNELS], +} +impl ToMono { + fn new(input: S) -> Self { + let channels = input + .channels() + .min(const { NonZero::::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 Source for ToMono { + fn current_span_len(&self) -> Option { + None + } + + fn channels(&self) -> ChannelCount { + rodio::nz!(1) + } + + fn sample_rate(&self) -> SampleRate { + self.inner.sample_rate() + } + + fn total_duration(&self) -> Option { + 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 Iterator for ToMono { + type Item = Sample; + + fn next(&mut self) -> Option { + 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 { inner: S, left_to_take: usize, @@ -147,6 +307,7 @@ impl Source for TakeSamples { } } +/// constant source, only works on a single span #[derive(Debug)] struct ReplayQueue { inner: ArrayQueue>, @@ -193,6 +354,7 @@ impl ReplayQueue { } } +/// constant source, only works on a single span pub struct ProcessBuffer where S: Source + Sized, @@ -260,6 +422,7 @@ where } } +/// constant source, only works on a single span pub struct InspectBuffer where S: Source + Sized, @@ -324,6 +487,7 @@ where } } +/// constant source, only works on a single span #[derive(Debug)] pub struct Replayable { inner: S, @@ -375,6 +539,7 @@ impl Source for Replayable { } } +/// constant source, only works on a single span #[derive(Debug)] pub struct Replay { rx: Arc, diff --git a/crates/denoise/src/engine.rs b/crates/denoise/src/engine.rs index 5196b70b5ba02f665385c022a0dfa9cd22c1db9c..be0548c689e3b902342cd1cb6d6d8e29351e8be4 100644 --- a/crates/denoise/src/engine.rs +++ b/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 } diff --git a/crates/denoise/src/lib.rs b/crates/denoise/src/lib.rs index 1422c81a4b915d571d35585447165c04d3695b73..f6cbf0fadf1f216cc6168c2b249f807b557869af 100644 --- a/crates/denoise/src/lib.rs +++ b/crates/denoise/src/lib.rs @@ -84,7 +84,7 @@ impl Denoiser { .spawn(move || { run_neural_denoiser(denoised_tx, input_rx); }) - .unwrap(); + .expect("Should be ablet to spawn threads"); Ok(Self { inner: source, diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index 45e929cb2ec0bebf054497632d614af1975f6397..04e669869ddbf64ffd92cbcad4bf927bfec55cb5 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/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 { + let speaker: Speaker = + serde_urlencoded::from_str(&track.0.name()).unwrap_or_else(|_| Speaker { + name: track.0.name(), + is_staff: false, + legacy_audio_compatible: 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.legacy_audio_compatible { Ok(self.playback.play_remote_audio_track(&track.0)) + } else { + Err(anyhow!("Client version too old to play audio in call")) } } } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index df8b5ea54fb1ce11bf871faa912757bbff1fd7f9..b4cd68e08e4a88f9cb248e3b7ac64fbfca4c39de 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/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,17 @@ pub(crate) struct AudioStack { pub(crate) fn play_remote_audio_track( track: &livekit::track::RemoteAudioTrack, + speaker: Speaker, cx: &mut gpui::App, ) -> Result { + let stream = source::LiveKitStream::new( + cx.background_executor(), + track, + speaker.legacy_audio_compatible, + ); + 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 +62,6 @@ 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, - }); audio::Audio::play_voip_stream(stream, speaker.name, speaker.is_staff, cx) .context("Could not play audio")?; @@ -152,17 +153,32 @@ 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_default(); + + 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 { name: user_name, is_staff, + legacy_audio_compatible, }) .context("Could not encode user information in track name")?; @@ -186,22 +202,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 }) }; @@ -389,25 +415,30 @@ impl AudioStack { } #[derive(Serialize, Deserialize)] -struct Speaker { - name: String, - is_staff: bool, +pub struct Speaker { + pub name: String, + pub is_staff: bool, + pub legacy_audio_compatible: bool, } fn send_to_livekit(frame_tx: UnboundedSender>, 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() diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs index f605b3d517cd816491f0eceadce5ac778ef75d21..cde4b19fda2e053346ad535e7c75b2abda60431a 100644 --- a/crates/livekit_client/src/livekit_client/playback/source.rs +++ b/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 = 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 { diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 43402cae0e6c723b4cc2e94f28c1ba7d0c61c928..b47755be58445e8ba335c6ea64416265d176fc17 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -291,21 +291,43 @@ pub enum TitleBarVisibility { #[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, + /// + /// You need to rejoin a call for this setting to apply + #[serde(rename = "experimental.rodio_audio")] + pub rodio_audio: Option, // 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, /// 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, + /// 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, /// 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, + /// Remove background noises. Works great for typing, cars, dogs, AC. Does + /// not work well on music. + #[serde(rename = "experimental.denoise")] + pub denoise: Option, + /// 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, } /// Control what info is collected by Zed. diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index ec9629685d8366864b92a6160ece623450f72b0c..b50854abd55af883af1e97eac4afd51dbb31df3b 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -90,7 +90,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"] } @@ -229,7 +228,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"] } @@ -308,7 +306,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"] } @@ -338,7 +335,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"] } @@ -369,7 +365,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"] } @@ -399,7 +394,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"] } @@ -442,6 +436,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"] } @@ -483,6 +478,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"] } @@ -522,6 +518,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"] } @@ -563,6 +560,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"] } @@ -587,7 +585,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"] } @@ -613,7 +610,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"] } @@ -655,6 +651,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"] } @@ -696,6 +693,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"] }