diff --git a/Cargo.lock b/Cargo.lock index f03be68c276..4d23f89e9dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 6ce47ccbc0b..1239adbbacd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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`. diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index d3f59e293c1..9e10f135f6a 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -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 diff --git a/crates/audio/src/audio_pipeline.rs b/crates/audio/src/audio_pipeline.rs index e09f4474af6..a6d33492a42 100644 --- a/crates/audio/src/audio_pipeline.rs +++ b/crates/audio/src/audio_pipeline.rs @@ -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>, pub echo_canceller: EchoCanceller, source_cache: HashMap>>>>, } @@ -54,16 +56,25 @@ pub struct Audio { impl Global for Audio {} impl Audio { - fn ensure_output_exists(&mut self, output_audio_device: Option) -> Result<&Mixer> { + fn ensure_output_exists( + &mut self, + output_audio_device: Option, + 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) -> anyhow::Result { 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) -> anyhow::Result, 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 { pub struct AvailableAudioDevices(pub Vec); 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::