1use anyhow::{Context as _, Result, anyhow};
2use collections::HashMap;
3use gpui::{App, BorrowAppContext, Global};
4use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
5use settings::Settings;
6use std::io::Cursor;
7use util::ResultExt;
8
9mod audio_settings;
10pub use audio_settings::AudioSettings;
11
12pub fn init(cx: &mut App) {
13 AudioSettings::register(cx);
14}
15
16#[derive(Copy, Clone, Eq, Hash, PartialEq)]
17pub enum Sound {
18 Joined,
19 Leave,
20 Mute,
21 Unmute,
22 StartScreenshare,
23 StopScreenshare,
24 AgentDone,
25}
26
27impl Sound {
28 fn file(&self) -> &'static str {
29 match self {
30 Self::Joined => "joined_call",
31 Self::Leave => "leave_call",
32 Self::Mute => "mute",
33 Self::Unmute => "unmute",
34 Self::StartScreenshare => "start_screenshare",
35 Self::StopScreenshare => "stop_screenshare",
36 Self::AgentDone => "agent_done",
37 }
38 }
39}
40
41#[derive(Default)]
42pub struct Audio {
43 output_handle: Option<OutputStream>,
44 source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
45}
46
47impl Global for Audio {}
48
49impl Audio {
50 fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
51 if self.output_handle.is_none() {
52 self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
53 }
54
55 self.output_handle.as_ref()
56 }
57
58 pub fn play_source(
59 source: impl rodio::Source + Send + 'static,
60 cx: &mut App,
61 ) -> anyhow::Result<()> {
62 cx.update_default_global(|this: &mut Self, _cx| {
63 let output_handle = this
64 .ensure_output_exists()
65 .ok_or_else(|| anyhow!("Could not open audio output"))?;
66 output_handle.mixer().add(source);
67 Ok(())
68 })
69 }
70
71 pub fn play_sound(sound: Sound, cx: &mut App) {
72 cx.update_default_global(|this: &mut Self, cx| {
73 let source = this.sound_source(sound, cx).log_err()?;
74 let output_handle = this.ensure_output_exists()?;
75 output_handle.mixer().add(source);
76 Some(())
77 });
78 }
79
80 pub fn end_call(cx: &mut App) {
81 cx.update_default_global(|this: &mut Self, _cx| {
82 this.output_handle.take();
83 });
84 }
85
86 fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
87 if let Some(wav) = self.source_cache.get(&sound) {
88 return Ok(wav.clone());
89 }
90
91 let path = format!("sounds/{}.wav", sound.file());
92 let bytes = cx
93 .asset_source()
94 .load(&path)?
95 .map(anyhow::Ok)
96 .with_context(|| format!("No asset available for path {path}"))??
97 .into_owned();
98 let cursor = Cursor::new(bytes);
99 let source = Decoder::new(cursor)?.buffered();
100
101 self.source_cache.insert(sound, source.clone());
102
103 Ok(source)
104 }
105}