Rodio audio (#37786)

Adds input to the experimental rodio_audio pipeline.

Enable with:
```json
"audio": {
  "experimental.rodio_audio": true
}
```

Additionally enables automatic volume 
control for incoming audio:
```json
"audio": {
  "experimental.control_output_volume": true
}
```

Release Notes:

- N/A
This commit is contained in:
David Kleingeld
2025-09-10 22:48:33 +02:00
committed by GitHub
parent 14de161d06
commit 95ccce3095
21 changed files with 1274 additions and 139 deletions
+56 -76
View File
@@ -13,6 +13,7 @@ use agent_ui::{AgentDiffToolbar, AgentPanelDelegate};
use anyhow::Context as _;
pub use app_menus::*;
use assets::Assets;
use audio::{AudioSettings, REPLAY_DURATION};
use breadcrumbs::Breadcrumbs;
use client::zed_urls;
use collections::VecDeque;
@@ -59,7 +60,7 @@ use settings::{
initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
update_settings_file,
};
use std::time::{Duration, Instant};
use std::time::Duration;
use std::{
borrow::Cow,
path::{Path, PathBuf},
@@ -128,8 +129,10 @@ actions!(
actions!(
dev,
[
/// Record 10s of audio from your current microphone
CaptureAudio
/// Stores last 30s of audio from zed staff using the experimental rodio
/// audio system (including yourself) on the current call in a tar file
/// in the current working directory.
CaptureRecentAudio,
]
);
@@ -910,8 +913,8 @@ fn register_actions(
}
}
})
.register_action(|workspace, _: &CaptureAudio, window, cx| {
capture_audio(workspace, window, cx);
.register_action(|workspace, _: &CaptureRecentAudio, window, cx| {
capture_recent_audio(workspace, window, cx);
});
#[cfg(not(target_os = "windows"))]
@@ -1845,50 +1848,39 @@ fn open_settings_file(
.detach_and_log_err(cx);
}
fn capture_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
#[derive(Default)]
enum State {
Recording(livekit_client::CaptureInput),
Failed(String),
Finished(PathBuf),
// Used during state switch. Should never occur naturally.
#[default]
Invalid,
}
struct CaptureAudioNotification {
fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
struct CaptureRecentAudioNotification {
focus_handle: gpui::FocusHandle,
start_time: Instant,
state: State,
save_result: Option<Result<(PathBuf, Duration), anyhow::Error>>,
_save_task: Task<anyhow::Result<()>>,
}
impl gpui::EventEmitter<DismissEvent> for CaptureAudioNotification {}
impl gpui::EventEmitter<SuppressEvent> for CaptureAudioNotification {}
impl gpui::Focusable for CaptureAudioNotification {
impl gpui::EventEmitter<DismissEvent> for CaptureRecentAudioNotification {}
impl gpui::EventEmitter<SuppressEvent> for CaptureRecentAudioNotification {}
impl gpui::Focusable for CaptureRecentAudioNotification {
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl workspace::notifications::Notification for CaptureAudioNotification {}
impl workspace::notifications::Notification for CaptureRecentAudioNotification {}
const AUDIO_RECORDING_TIME_SECS: u64 = 10;
impl Render for CaptureAudioNotification {
impl Render for CaptureRecentAudioNotification {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let elapsed = self.start_time.elapsed().as_secs();
let message = match &self.state {
State::Recording(capture) => format!(
"Recording {} seconds of audio from input: '{}'",
AUDIO_RECORDING_TIME_SECS - elapsed,
capture.name,
let message = match &self.save_result {
None => format!(
"Saving up to {} seconds of recent audio",
REPLAY_DURATION.as_secs(),
),
State::Failed(e) => format!("Error capturing audio: {e}"),
State::Finished(path) => format!("Audio recorded to {}", path.display()),
State::Invalid => "Error invalid state".to_string(),
Some(Ok((path, duration))) => format!(
"Saved {} seconds of all audio to {}",
duration.as_secs(),
path.display(),
),
Some(Err(e)) => format!("Error saving audio replays: {e:?}"),
};
NotificationFrame::new()
.with_title(Some("Recording Audio"))
.with_title(Some("Saved Audio"))
.show_suppress_button(false)
.on_close(cx.listener(|_, _, _, cx| {
cx.emit(DismissEvent);
@@ -1897,53 +1889,41 @@ fn capture_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Wor
}
}
impl CaptureAudioNotification {
fn finish(&mut self) {
let state = std::mem::take(&mut self.state);
self.state = if let State::Recording(capture) = state {
match capture.finish() {
Ok(path) => State::Finished(path),
Err(e) => State::Failed(e.to_string()),
impl CaptureRecentAudioNotification {
fn new(cx: &mut Context<Self>) -> Self {
if AudioSettings::get_global(cx).rodio_audio {
let executor = cx.background_executor().clone();
let save_task = cx.default_global::<audio::Audio>().save_replays(executor);
let _save_task = cx.spawn(async move |this, cx| {
let res = save_task.await;
this.update(cx, |this, cx| {
this.save_result = Some(res);
cx.notify();
})
});
Self {
focus_handle: cx.focus_handle(),
_save_task,
save_result: None,
}
} else {
state
};
}
fn new(cx: &mut Context<Self>) -> Self {
cx.spawn(async move |this, cx| {
for _ in 0..10 {
cx.background_executor().timer(Duration::from_secs(1)).await;
this.update(cx, |_, cx| {
cx.notify();
})?;
Self {
focus_handle: cx.focus_handle(),
_save_task: Task::ready(Ok(())),
save_result: Some(Err(anyhow::anyhow!(
"Capturing recent audio is only supported on the experimental rodio audio pipeline"
))),
}
this.update(cx, |this, cx| {
this.finish();
cx.notify();
})?;
anyhow::Ok(())
})
.detach();
let state = match livekit_client::CaptureInput::start() {
Ok(capture_input) => State::Recording(capture_input),
Err(err) => State::Failed(format!("Error starting audio capture: {}", err)),
};
Self {
focus_handle: cx.focus_handle(),
start_time: Instant::now(),
state,
}
}
}
workspace.show_notification(NotificationId::unique::<CaptureAudio>(), cx, |cx| {
cx.new(CaptureAudioNotification::new)
});
workspace.show_notification(
NotificationId::unique::<CaptureRecentAudioNotification>(),
cx,
|cx| cx.new(CaptureRecentAudioNotification::new),
);
}
#[cfg(test)]