mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-20 11:46:49 +00:00
audio: Recover from notification output stream failures (#65158)
Closes https://github.com/zed-industries/zed/issues/64962 Update CPAL and rodio to remove the uninterruptible ALSA write retry loop. Discard failed output streams so subsequent notifications can reopen the device. Preserve recoverable underruns, cancel stale error handlers on teardown, and keep existing output buffer defaults. Release Notes: - Fixed audio thread failing to properly stop on errors
This commit is contained in:
parent
976ab33fb4
commit
56303a3372
6 changed files with 149 additions and 48 deletions
58
Cargo.lock
generated
58
Cargo.lock
generated
|
|
@ -665,9 +665,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
|||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3"
|
||||
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.13.1",
|
||||
|
|
@ -677,9 +677,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
|
|
@ -1351,6 +1351,7 @@ dependencies = [
|
|||
"collections",
|
||||
"cpal",
|
||||
"crossbeam-queue",
|
||||
"futures 0.3.34",
|
||||
"gpui",
|
||||
"libwebrtc",
|
||||
"log",
|
||||
|
|
@ -3929,11 +3930,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.13.0"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
|
||||
checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags 2.13.1",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
|
|
@ -3976,17 +3977,17 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
|
||||
version = "0.18.2"
|
||||
source = "git+https://github.com/RustAudio/cpal?rev=e1612d5d98152f8dc2a62e1b51ef7cbf4f7f26b7#e1612d5d98152f8dc2a62e1b51ef7cbf4f7f26b7"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"coreaudio-rs 0.13.0",
|
||||
"block2 0.6.2",
|
||||
"coreaudio-rs 0.14.2",
|
||||
"dasp_sample",
|
||||
"jni 0.21.1",
|
||||
"jni 0.22.4",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2 0.5.0",
|
||||
"mach2",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
|
|
@ -3998,10 +3999,9 @@ dependencies = [
|
|||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4185,7 +4185,7 @@ checksum = "3748e166981fa3ba9dc54ff950ad0ed5ca4aab4b73eae21d9f539659fe806e28"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"mach2 0.6.0",
|
||||
"mach2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4197,7 +4197,7 @@ dependencies = [
|
|||
"cfg-if",
|
||||
"crash-context",
|
||||
"libc",
|
||||
"mach2 0.6.0",
|
||||
"mach2",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
|
|
@ -4209,7 +4209,7 @@ dependencies = [
|
|||
"crash-handler",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.6.0",
|
||||
"mach2",
|
||||
"minidumper",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
|
|
@ -7497,7 +7497,7 @@ dependencies = [
|
|||
"image",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.6.0",
|
||||
"mach2",
|
||||
"metal",
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
|
|
@ -10537,15 +10537,6 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.6.0"
|
||||
|
|
@ -10989,7 +10980,7 @@ dependencies = [
|
|||
"goblin",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.6.0",
|
||||
"mach2",
|
||||
"memmap2",
|
||||
"minidump-common",
|
||||
"process-backend",
|
||||
|
|
@ -11741,6 +11732,7 @@ version = "0.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
|
@ -15311,8 +15303,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/RustAudio/rodio?rev=e50e726ddd0292f6ef9de0dda6b90af4ed1fb66a#e50e726ddd0292f6ef9de0dda6b90af4ed1fb66a"
|
||||
version = "0.22.2"
|
||||
source = "git+https://github.com/RustAudio/rodio?rev=3ee9745862279ce1d18db593155016e7ce024aeb#3ee9745862279ce1d18db593155016e7ce024aeb"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"dasp_sample",
|
||||
|
|
@ -19474,7 +19466,7 @@ dependencies = [
|
|||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.6.0",
|
||||
"mach2",
|
||||
"nix",
|
||||
"path",
|
||||
"percent-encoding",
|
||||
|
|
@ -20025,7 +20017,7 @@ dependencies = [
|
|||
"futures 0.3.34",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.6.0",
|
||||
"mach2",
|
||||
"memfd",
|
||||
"object 0.39.1",
|
||||
"once_cell",
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ remote_connection = { path = "crates/remote_connection" }
|
|||
remote_server = { path = "crates/remote_server" }
|
||||
repl = { path = "crates/repl" }
|
||||
reqwest_client = { path = "crates/reqwest_client" }
|
||||
rodio = { git = "https://github.com/RustAudio/rodio", rev = "e50e726ddd0292f6ef9de0dda6b90af4ed1fb66a", default-features = false, features = ["wav", "playback", "wav_output", "recording"] }
|
||||
rodio = { git = "https://github.com/RustAudio/rodio", rev = "3ee9745862279ce1d18db593155016e7ce024aeb", default-features = false, features = ["wav", "playback", "wav_output", "recording"] }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
sandbox = { path = "crates/sandbox" }
|
||||
|
|
@ -604,7 +604,7 @@ core-services = "1"
|
|||
core-text = "21"
|
||||
core-video = { version = "0.5", default-features = false, features = ["link"] }
|
||||
coreaudio-rs = { version = "0.12", default-features = false }
|
||||
cpal = "0.17"
|
||||
cpal = { git = "https://github.com/RustAudio/cpal", rev = "e1612d5d98152f8dc2a62e1b51ef7cbf4f7f26b7" }
|
||||
crash-handler = "0.8"
|
||||
criterion = { version = "0.8.2", features = ["html_reports"] }
|
||||
perf-event = { package = "perf-event2", version = "0.7.4" }
|
||||
|
|
@ -981,6 +981,7 @@ features = [
|
|||
]
|
||||
|
||||
[patch.crates-io]
|
||||
cpal = { git = "https://github.com/RustAudio/cpal", rev = "e1612d5d98152f8dc2a62e1b51ef7cbf4f7f26b7" }
|
||||
# Unify `tree_sitter_language::LanguageFn` between crates.io grammar crates and
|
||||
# the git tree-sitter above; two instances of the crate would make grammar
|
||||
# constants type-incompatible with `tree_sitter::Language`.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ anyhow.workspace = true
|
|||
collections.workspace = true
|
||||
cpal.workspace = true
|
||||
crossbeam-queue.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
|
@ -25,5 +26,8 @@ settings.workspace = true
|
|||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
|
||||
libwebrtc.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use cpal::{
|
||||
DeviceDescription, DeviceId, default_host,
|
||||
DeviceDescription, DeviceId, Error, ErrorKind, default_host,
|
||||
traits::{DeviceTrait, HostTrait},
|
||||
};
|
||||
use gpui::{App, AsyncApp, BorrowAppContext, Global};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{App, AsyncApp, BorrowAppContext, Global, Task};
|
||||
|
||||
pub(super) use cpal::Sample;
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ pub fn ensure_devices_initialized(cx: &mut App) {
|
|||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
output: Option<(MixerDeviceSink, Mixer)>,
|
||||
output_error_task: Option<Task<()>>,
|
||||
pub echo_canceller: EchoCanceller,
|
||||
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
|
||||
}
|
||||
|
|
@ -54,16 +56,25 @@ pub struct Audio {
|
|||
impl Global for Audio {}
|
||||
|
||||
impl Audio {
|
||||
fn ensure_output_exists(&mut self, output_audio_device: Option<DeviceId>) -> Result<&Mixer> {
|
||||
fn ensure_output_exists(
|
||||
&mut self,
|
||||
output_audio_device: Option<DeviceId>,
|
||||
cx: &mut App,
|
||||
) -> 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.is_none() {
|
||||
let (output_handle, output_mixer) =
|
||||
open_output_stream(output_audio_device, self.echo_canceller.clone())?;
|
||||
self.output = Some((output_handle, output_mixer));
|
||||
let (error_callback, error_task) = output_error_handler(cx);
|
||||
let output = open_output_stream(
|
||||
output_audio_device,
|
||||
self.echo_canceller.clone(),
|
||||
error_callback,
|
||||
)?;
|
||||
self.output = Some(output);
|
||||
self.output_error_task = Some(error_task);
|
||||
}
|
||||
|
||||
Ok(self
|
||||
|
|
@ -78,7 +89,7 @@ impl Audio {
|
|||
cx.update_default_global(|this: &mut Self, cx| {
|
||||
let source = this.sound_source(sound, cx).log_err()?;
|
||||
let output_mixer = this
|
||||
.ensure_output_exists(output_audio_device)
|
||||
.ensure_output_exists(output_audio_device, cx)
|
||||
.context("Could not get output mixer")
|
||||
.log_err()?;
|
||||
|
||||
|
|
@ -88,8 +99,9 @@ impl Audio {
|
|||
}
|
||||
|
||||
pub fn end_call(cx: &mut App) {
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
this.output.take();
|
||||
cx.update_default_global(|audio: &mut Self, _cx| {
|
||||
audio.output_error_task.take();
|
||||
audio.output.take();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -170,6 +182,7 @@ pub fn resolve_device(device_id: Option<&DeviceId>, input: bool) -> anyhow::Resu
|
|||
pub fn open_test_output(device_id: Option<DeviceId>) -> anyhow::Result<MixerDeviceSink> {
|
||||
let device = resolve_device(device_id.as_ref(), false)?;
|
||||
DeviceSinkBuilder::from_device(device)?
|
||||
.with_buffer_size(cpal::BufferSize::Default)
|
||||
.open_stream()
|
||||
.context("Could not open output stream")
|
||||
}
|
||||
|
|
@ -177,9 +190,12 @@ pub fn open_test_output(device_id: Option<DeviceId>) -> anyhow::Result<MixerDevi
|
|||
pub fn open_output_stream(
|
||||
device_id: Option<DeviceId>,
|
||||
mut echo_canceller: EchoCanceller,
|
||||
error_callback: impl FnMut(Error) + Send + 'static,
|
||||
) -> anyhow::Result<(MixerDeviceSink, Mixer)> {
|
||||
let device = resolve_device(device_id.as_ref(), false)?;
|
||||
let mut output_handle = DeviceSinkBuilder::from_device(device)?
|
||||
.with_buffer_size(cpal::BufferSize::Default)
|
||||
.with_error_callback(error_callback)
|
||||
.open_stream()
|
||||
.context("Could not open output stream")?;
|
||||
output_handle.log_on_drop(false);
|
||||
|
|
@ -241,3 +257,91 @@ fn get_available_audio_devices() -> Vec<AudioDeviceInfo> {
|
|||
pub struct AvailableAudioDevices(pub Vec<AudioDeviceInfo>);
|
||||
|
||||
impl Global for AvailableAudioDevices {}
|
||||
|
||||
fn output_error_handler(cx: &mut App) -> (impl FnMut(Error) + Send + 'static + use<>, Task<()>) {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let mut sender = Some(sender);
|
||||
let callback = move |error: Error| {
|
||||
if matches!(
|
||||
error.kind(),
|
||||
ErrorKind::Xrun | ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if let Some(sender) = sender.take() {
|
||||
sender.send(error).ok();
|
||||
}
|
||||
};
|
||||
let task = cx.spawn(async move |cx| {
|
||||
if let Ok(error) = receiver.await {
|
||||
log::error!("Audio output stream failed: {error}");
|
||||
cx.update(Audio::end_call);
|
||||
}
|
||||
});
|
||||
(callback, task)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_output_error_handler(cx: &mut TestAppContext) {
|
||||
for error_kind in [
|
||||
ErrorKind::DeviceNotAvailable,
|
||||
ErrorKind::StreamInvalidated,
|
||||
ErrorKind::BackendError,
|
||||
] {
|
||||
let mut callback = cx.update(install_output_error_handler);
|
||||
for recoverable_kind in [
|
||||
ErrorKind::Xrun,
|
||||
ErrorKind::DeviceChanged,
|
||||
ErrorKind::RealtimeDenied,
|
||||
] {
|
||||
callback(Error::new(recoverable_kind));
|
||||
cx.run_until_parked();
|
||||
cx.update(|cx| assert!(cx.global::<Audio>().output_error_task.is_some()));
|
||||
}
|
||||
|
||||
callback(Error::new(error_kind));
|
||||
callback(Error::new(error_kind));
|
||||
cx.run_until_parked();
|
||||
cx.update(|cx| assert!(cx.global::<Audio>().output_error_task.is_none()));
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_output_error_handler_cancellation(cx: &mut TestAppContext) {
|
||||
for error_before_close in [false, true] {
|
||||
let mut old_callback = cx.update(install_output_error_handler);
|
||||
if error_before_close {
|
||||
old_callback(Error::new(ErrorKind::DeviceNotAvailable));
|
||||
}
|
||||
cx.update(Audio::end_call);
|
||||
|
||||
let mut callback = cx.update(install_output_error_handler);
|
||||
old_callback(Error::new(ErrorKind::DeviceNotAvailable));
|
||||
cx.run_until_parked();
|
||||
cx.update(|cx| assert!(cx.global::<Audio>().output_error_task.is_some()));
|
||||
|
||||
callback(Error::new(ErrorKind::StreamInvalidated));
|
||||
cx.run_until_parked();
|
||||
cx.update(|cx| assert!(cx.global::<Audio>().output_error_task.is_none()));
|
||||
}
|
||||
|
||||
let callback = cx.update(install_output_error_handler);
|
||||
drop(callback);
|
||||
cx.run_until_parked();
|
||||
cx.update(|cx| assert!(cx.global::<Audio>().output_error_task.is_some()));
|
||||
cx.update(Audio::end_call);
|
||||
}
|
||||
|
||||
fn install_output_error_handler(cx: &mut App) -> impl FnMut(Error) + Send + 'static + use<> {
|
||||
let (callback, task) = output_error_handler(cx);
|
||||
cx.update_default_global(|audio: &mut Audio, _cx| {
|
||||
audio.output_error_task = Some(task);
|
||||
});
|
||||
callback
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ impl AudioStack {
|
|||
executor
|
||||
.spawn_with_priority(Priority::RealtimeAudio, async move {
|
||||
let output_stream = output_device.build_output_stream(
|
||||
&output_config.config(),
|
||||
output_config.config(),
|
||||
{
|
||||
move |mut data, _info| {
|
||||
while data.len() > 0 {
|
||||
|
|
@ -340,7 +340,7 @@ impl AudioStack {
|
|||
|
||||
let stream = device
|
||||
.build_input_stream_raw(
|
||||
&config.config(),
|
||||
config.config(),
|
||||
config.sample_format(),
|
||||
move |data, _: &_| {
|
||||
let captured_at = Instant::now();
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ impl CaptureInput {
|
|||
log::info!("Using microphone: {}", name);
|
||||
|
||||
let samples = Arc::new(Mutex::new(Vec::new()));
|
||||
let stream = start_capture(device, config.clone(), samples.clone())?;
|
||||
let stream = start_capture(device, config, samples.clone())?;
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
|
|
@ -58,7 +58,7 @@ fn start_capture(
|
|||
) -> Result<cpal::Stream> {
|
||||
let stream = device
|
||||
.build_input_stream_raw(
|
||||
&config.config(),
|
||||
config.config(),
|
||||
config.sample_format(),
|
||||
move |data, _: &_| {
|
||||
let data = crate::get_sample_data(config.sample_format(), data).log_err();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue