Add automatic-volume to legacy (#51919)

## Context

This allows using of Rodio's effects library within our home brewn audio
pipeline. The alternative would be inlining Rodio's effects which is
problematic from a legal stance. We would then have to make clear that
code is not owned by zed-industries while the code would be surrounded
by zed-industries owned code.

This adaptor does incur a slight performance penalty (copying into a
pre-allocated vec and back) however the impact will be immeasurably low.

There is no latency impact.

## How to Review

- Adds an adapter for Rodio effects
- Enables the adapter and effects only when the setting is enabled
-Makes the setting pub(crate) so we can use it from livekit playback

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Added Automatic volume control to calls
This commit is contained in:
Yara 🏳️‍⚧️ authored and GitHub committed 2026-03-19 14:31:56 +01:00
1 parent f1cad55e44
commit 4caff7d747
3 files changed
+93 -3

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ pub const CHANNEL_COUNT: ChannelCount = nz!(2);
mod audio_settings;
pub use audio_settings::AudioSettings;
pub use audio_settings::LIVE_SETTINGS;
mod audio_pipeline;
pub use audio_pipeline::{Audio, VoipParts};
+3 -3
View File
@@ -71,8 +71,8 @@ impl Settings for AudioSettings {
}
/// See docs on [LIVE_SETTINGS]
pub(crate) struct LiveSettings {
pub(crate) auto_microphone_volume: AtomicBool,
pub struct LiveSettings {
pub auto_microphone_volume: AtomicBool,
pub(crate) auto_speaker_volume: AtomicBool,
pub(crate) denoise: AtomicBool,
}
@@ -128,7 +128,7 @@ impl LiveSettings {
/// observer of SettingsStore. Needed because audio playback and recording are
/// real time and must each run in a dedicated OS thread, therefore we can not
/// use the background executor.
pub(crate) static LIVE_SETTINGS: LiveSettings = LiveSettings {
pub static LIVE_SETTINGS: LiveSettings = LiveSettings {
auto_microphone_volume: AtomicBool::new(true),
auto_speaker_volume: AtomicBool::new(true),
denoise: AtomicBool::new(true),
@@ -23,6 +23,8 @@ use livekit::webrtc::{
use log::info;
use parking_lot::Mutex;
use rodio::Source;
use rodio::conversions::SampleTypeConverter;
use rodio::source::{AutomaticGainControlSettings, LimitSettings};
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::cell::RefCell;
@@ -380,6 +382,14 @@ impl AudioStack {
let ten_ms_buffer_size =
(config.channels() as u32 * config.sample_rate() / 100) as usize;
let mut buf: Vec<i16> = Vec::with_capacity(ten_ms_buffer_size);
let mut rodio_effects = RodioEffectsAdaptor::new(buf.len())
.automatic_gain_control(AutomaticGainControlSettings {
target_level: 0.50,
attack_time: Duration::from_secs(1),
release_time: Duration::from_secs(0),
absolute_max_gain: 5.0,
})
.limit(LimitSettings::live_performance());
let stream = device
.build_input_stream_raw(
@@ -411,6 +421,21 @@ impl AudioStack {
sample_rate,
)
.to_owned();
if audio::LIVE_SETTINGS
.auto_microphone_volume
.load(Ordering::Relaxed)
{
rodio_effects
.inner_mut()
.inner_mut()
.fill_buffer_with(&sampled);
sampled.clear();
sampled.extend(SampleTypeConverter::<_, i16>::new(
rodio_effects.by_ref(),
));
}
apm.lock()
.process_stream(
&mut sampled,
@@ -419,6 +444,7 @@ impl AudioStack {
)
.log_err();
buf.clear();
frame_tx
.try_send(TimestampedFrame {
frame: AudioFrame {
@@ -453,6 +479,69 @@ impl AudioStack {
}
}
/// This allows using of Rodio's effects library within our home brewn audio
/// pipeline. The alternative would be inlining Rodio's effects which is
/// problematic from a legal stance. We would then have to make clear that code
/// is not owned by zed-industries while the code would be surrounded by
/// zed-industries owned code.
///
/// This adaptor does incur a slight performance penalty (copying into a
/// pre-allocated vec and back) however the impact will be immeasurably low.
///
/// There is no latency impact.
pub struct RodioEffectsAdaptor {
input: Vec<rodio::Sample>,
pos: usize,
}
impl RodioEffectsAdaptor {
// This implementation incorrect terminology confusing everyone. A normal
// audio frame consists of all samples for one moment in time (one for mono,
// two for stereo). Here a frame of audio refers to a 10ms buffer of samples.
fn new(samples_per_frame: usize) -> Self {
Self {
input: Vec::with_capacity(samples_per_frame),
pos: 0,
}
}
fn fill_buffer_with(&mut self, integer_samples: &[i16]) {
self.input.clear();
self.input.extend(SampleTypeConverter::<_, f32>::new(
integer_samples.iter().copied(),
));
self.pos = 0;
}
}
impl Iterator for RodioEffectsAdaptor {
type Item = rodio::Sample;
fn next(&mut self) -> Option<Self::Item> {
let sample = self.input.get(self.pos)?;
self.pos += 1;
Some(*sample)
}
}
impl rodio::Source for RodioEffectsAdaptor {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
rodio::nz!(2)
}
fn sample_rate(&self) -> rodio::SampleRate {
rodio::nz!(48000)
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Speaker {
pub name: String,