From 18930c33fe0b5fc7bc2852547201ed7226f841ad Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 24 Oct 2023 17:15:59 +0200 Subject: [PATCH] Make audio2 --- Cargo.toml | 1 + crates/audio2/Cargo.toml | 24 +++++++ crates/audio2/audio/Cargo.toml | 23 +++++++ crates/audio2/audio/src/assets.rs | 44 +++++++++++++ crates/audio2/audio/src/audio.rs | 81 ++++++++++++++++++++++++ crates/audio2/src/assets.rs | 44 +++++++++++++ crates/audio2/src/audio2.rs | 100 ++++++++++++++++++++++++++++++ 7 files changed, 317 insertions(+) create mode 100644 crates/audio2/Cargo.toml create mode 100644 crates/audio2/audio/Cargo.toml create mode 100644 crates/audio2/audio/src/assets.rs create mode 100644 crates/audio2/audio/src/audio.rs create mode 100644 crates/audio2/src/assets.rs create mode 100644 crates/audio2/src/audio2.rs diff --git a/Cargo.toml b/Cargo.toml index c42feac8fb..030c20e99c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/ai", "crates/assistant", "crates/audio", + "crates/audio2", "crates/auto_update", "crates/breadcrumbs", "crates/call", diff --git a/crates/audio2/Cargo.toml b/crates/audio2/Cargo.toml new file mode 100644 index 0000000000..298142dbef --- /dev/null +++ b/crates/audio2/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "audio2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/audio2.rs" +doctest = false + +[dependencies] +gpui2 = { path = "../gpui2" } +collections = { path = "../collections" } +util = { path = "../util" } + + +rodio ={version = "0.17.1", default-features=false, features = ["wav"]} + +log.workspace = true +futures.workspace = true +anyhow.workspace = true +parking_lot.workspace = true + +[dev-dependencies] diff --git a/crates/audio2/audio/Cargo.toml b/crates/audio2/audio/Cargo.toml new file mode 100644 index 0000000000..36135a1e76 --- /dev/null +++ b/crates/audio2/audio/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "audio" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/audio.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } +collections = { path = "../collections" } +util = { path = "../util" } + +rodio ={version = "0.17.1", default-features=false, features = ["wav"]} + +log.workspace = true + +anyhow.workspace = true +parking_lot.workspace = true + +[dev-dependencies] diff --git a/crates/audio2/audio/src/assets.rs b/crates/audio2/audio/src/assets.rs new file mode 100644 index 0000000000..b58e1f6aee --- /dev/null +++ b/crates/audio2/audio/src/assets.rs @@ -0,0 +1,44 @@ +use std::{io::Cursor, sync::Arc}; + +use anyhow::Result; +use collections::HashMap; +use gpui::{AppContext, AssetSource}; +use rodio::{ + source::{Buffered, SamplesConverter}, + Decoder, Source, +}; + +type Sound = Buffered>>, f32>>; + +pub struct SoundRegistry { + cache: Arc>>, + assets: Box, +} + +impl SoundRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + cache: Default::default(), + assets: Box::new(source), + }) + } + + pub fn global(cx: &AppContext) -> Arc { + cx.global::>().clone() + } + + pub fn get(&self, name: &str) -> Result> { + if let Some(wav) = self.cache.lock().get(name) { + return Ok(wav.clone()); + } + + let path = format!("sounds/{}.wav", name); + let bytes = self.assets.load(&path)?.into_owned(); + let cursor = Cursor::new(bytes); + let source = Decoder::new(cursor)?.convert_samples::().buffered(); + + self.cache.lock().insert(name.to_string(), source.clone()); + + Ok(source) + } +} diff --git a/crates/audio2/audio/src/audio.rs b/crates/audio2/audio/src/audio.rs new file mode 100644 index 0000000000..d80fb6738f --- /dev/null +++ b/crates/audio2/audio/src/audio.rs @@ -0,0 +1,81 @@ +use assets::SoundRegistry; +use gpui::{AppContext, AssetSource}; +use rodio::{OutputStream, OutputStreamHandle}; +use util::ResultExt; + +mod assets; + +pub fn init(source: impl AssetSource, cx: &mut AppContext) { + cx.set_global(SoundRegistry::new(source)); + cx.set_global(Audio::new()); +} + +pub enum Sound { + Joined, + Leave, + Mute, + Unmute, + StartScreenshare, + StopScreenshare, +} + +impl Sound { + fn file(&self) -> &'static str { + match self { + Self::Joined => "joined_call", + Self::Leave => "leave_call", + Self::Mute => "mute", + Self::Unmute => "unmute", + Self::StartScreenshare => "start_screenshare", + Self::StopScreenshare => "stop_screenshare", + } + } +} + +pub struct Audio { + _output_stream: Option, + output_handle: Option, +} + +impl Audio { + pub fn new() -> Self { + Self { + _output_stream: None, + output_handle: None, + } + } + + fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> { + if self.output_handle.is_none() { + let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); + self.output_handle = output_handle; + self._output_stream = _output_stream; + } + + self.output_handle.as_ref() + } + + pub fn play_sound(sound: Sound, cx: &mut AppContext) { + if !cx.has_global::() { + return; + } + + cx.update_global::(|this, cx| { + let output_handle = this.ensure_output_exists()?; + let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; + output_handle.play_raw(source).log_err()?; + Some(()) + }); + } + + pub fn end_call(cx: &mut AppContext) { + if !cx.has_global::() { + return; + } + + cx.update_global::(|this, _| { + this._output_stream.take(); + this.output_handle.take(); + }); + } +} diff --git a/crates/audio2/src/assets.rs b/crates/audio2/src/assets.rs new file mode 100644 index 0000000000..ece0a5f7b8 --- /dev/null +++ b/crates/audio2/src/assets.rs @@ -0,0 +1,44 @@ +use std::{io::Cursor, sync::Arc}; + +use anyhow::Result; +use collections::HashMap; +use gpui2::{AppContext, AssetSource, SharedString}; +use rodio::{ + source::{Buffered, SamplesConverter}, + Decoder, Source, +}; + +type Sound = Buffered>>, f32>>; + +pub struct SoundRegistry { + cache: Arc>>, + assets: Box, +} + +impl SoundRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + cache: Default::default(), + assets: Box::new(source), + }) + } + + pub fn global(cx: &AppContext) -> Arc { + cx.global::>().clone() + } + + pub fn get(&self, name: &str) -> Result> { + if let Some(wav) = self.cache.lock().get(name) { + return Ok(wav.clone()); + } + + let path = format!("sounds/{}.wav", name); + let bytes = self.assets.load(SharedString::from(path))?.into_owned(); + let cursor = Cursor::new(bytes); + let source = Decoder::new(cursor)?.convert_samples::().buffered(); + + self.cache.lock().insert(name.to_string(), source.clone()); + + Ok(source) + } +} diff --git a/crates/audio2/src/audio2.rs b/crates/audio2/src/audio2.rs new file mode 100644 index 0000000000..831ed59515 --- /dev/null +++ b/crates/audio2/src/audio2.rs @@ -0,0 +1,100 @@ +use assets::SoundRegistry; +use futures::{channel::mpsc, StreamExt}; +use gpui2::{AppContext, AssetSource, Executor}; +use rodio::{OutputStream, OutputStreamHandle}; +use util::ResultExt; + +mod assets; + +pub fn init(source: impl AssetSource, cx: &mut AppContext) { + cx.set_global(Audio::new(cx.executor())); + cx.set_global(SoundRegistry::new(source)); +} + +pub enum Sound { + Joined, + Leave, + Mute, + Unmute, + StartScreenshare, + StopScreenshare, +} + +impl Sound { + fn file(&self) -> &'static str { + match self { + Self::Joined => "joined_call", + Self::Leave => "leave_call", + Self::Mute => "mute", + Self::Unmute => "unmute", + Self::StartScreenshare => "start_screenshare", + Self::StopScreenshare => "stop_screenshare", + } + } +} + +pub struct Audio { + tx: mpsc::UnboundedSender>, +} + +struct AudioState { + _output_stream: Option, + output_handle: Option, +} + +impl AudioState { + fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> { + if self.output_handle.is_none() { + let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); + self.output_handle = output_handle; + self._output_stream = _output_stream; + } + + self.output_handle.as_ref() + } + + fn take(&mut self) { + self._output_stream.take(); + self.output_handle.take(); + } +} + +impl Audio { + pub fn new(executor: &Executor) -> Self { + let (tx, mut rx) = mpsc::unbounded::>(); + executor + .spawn_on_main(|| async move { + let mut audio = AudioState { + _output_stream: None, + output_handle: None, + }; + + while let Some(f) = rx.next().await { + (f)(&mut audio); + } + }) + .detach(); + + Self { tx } + } + + pub fn play_sound(&self, sound: Sound, cx: &mut AppContext) { + let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else { + return; + }; + + self.tx + .unbounded_send(Box::new(move |state| { + if let Some(output_handle) = state.ensure_output_exists() { + output_handle.play_raw(source).log_err(); + } + })) + .ok(); + } + + pub fn end_call(&self) { + self.tx + .unbounded_send(Box::new(move |state| state.take())) + .ok(); + } +}