Make audio2

Kirill Bulatov created

Change summary

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(+)

Detailed changes

Cargo.toml 🔗

@@ -4,6 +4,7 @@ members = [
     "crates/ai",
     "crates/assistant",
     "crates/audio",
+    "crates/audio2",
     "crates/auto_update",
     "crates/breadcrumbs",
     "crates/call",

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]

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]

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<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
+
+pub struct SoundRegistry {
+    cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
+    assets: Box<dyn AssetSource>,
+}
+
+impl SoundRegistry {
+    pub fn new(source: impl AssetSource) -> Arc<Self> {
+        Arc::new(Self {
+            cache: Default::default(),
+            assets: Box::new(source),
+        })
+    }
+
+    pub fn global(cx: &AppContext) -> Arc<Self> {
+        cx.global::<Arc<Self>>().clone()
+    }
+
+    pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
+        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::<f32>().buffered();
+
+        self.cache.lock().insert(name.to_string(), source.clone());
+
+        Ok(source)
+    }
+}

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<OutputStream>,
+    output_handle: Option<OutputStreamHandle>,
+}
+
+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::<Self>() {
+            return;
+        }
+
+        cx.update_global::<Self, _, _>(|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::<Self>() {
+            return;
+        }
+
+        cx.update_global::<Self, _, _>(|this, _| {
+            this._output_stream.take();
+            this.output_handle.take();
+        });
+    }
+}

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<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
+
+pub struct SoundRegistry {
+    cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
+    assets: Box<dyn AssetSource>,
+}
+
+impl SoundRegistry {
+    pub fn new(source: impl AssetSource) -> Arc<Self> {
+        Arc::new(Self {
+            cache: Default::default(),
+            assets: Box::new(source),
+        })
+    }
+
+    pub fn global(cx: &AppContext) -> Arc<Self> {
+        cx.global::<Arc<Self>>().clone()
+    }
+
+    pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
+        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::<f32>().buffered();
+
+        self.cache.lock().insert(name.to_string(), source.clone());
+
+        Ok(source)
+    }
+}

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<Box<dyn FnOnce(&mut AudioState) + Send>>,
+}
+
+struct AudioState {
+    _output_stream: Option<OutputStream>,
+    output_handle: Option<OutputStreamHandle>,
+}
+
+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::<Box<dyn FnOnce(&mut AudioState) + Send>>();
+        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();
+    }
+}