audio_test_window.rs

  1use audio::{AudioSettings, CHANNEL_COUNT, RodioExt, SAMPLE_RATE};
  2use cpal::DeviceId;
  3use gpui::{
  4    App, Context, Entity, FocusHandle, Focusable, Render, Size, Tiling, Window, WindowBounds,
  5    WindowKind, WindowOptions, prelude::*, px,
  6};
  7use platform_title_bar::PlatformTitleBar;
  8use release_channel::ReleaseChannel;
  9use rodio::Source;
 10use settings::{AudioInputDeviceName, AudioOutputDeviceName, Settings};
 11use std::{
 12    any::Any,
 13    sync::{
 14        Arc,
 15        atomic::{AtomicBool, Ordering},
 16    },
 17    thread,
 18    time::Duration,
 19};
 20use ui::{Button, ButtonStyle, Label, prelude::*};
 21use util::ResultExt;
 22use workspace::client_side_decorations;
 23
 24use super::audio_input_output_setup::render_audio_device_dropdown;
 25use crate::{SettingsUiFile, update_settings_file};
 26
 27pub struct AudioTestWindow {
 28    title_bar: Option<Entity<PlatformTitleBar>>,
 29    input_device_id: Option<DeviceId>,
 30    output_device_id: Option<DeviceId>,
 31    focus_handle: FocusHandle,
 32    _stop_playback: Option<Box<dyn Any + Send>>,
 33}
 34
 35impl AudioTestWindow {
 36    pub fn new(cx: &mut Context<Self>) -> Self {
 37        let title_bar = if !cfg!(target_os = "macos") {
 38            Some(cx.new(|cx| PlatformTitleBar::new("audio-test-title-bar", cx)))
 39        } else {
 40            None
 41        };
 42
 43        let audio_settings = AudioSettings::get_global(cx);
 44        let input_device_id = audio_settings.input_audio_device.clone();
 45        let output_device_id = audio_settings.output_audio_device.clone();
 46
 47        Self {
 48            title_bar,
 49            input_device_id,
 50            output_device_id,
 51            focus_handle: cx.focus_handle(),
 52            _stop_playback: None,
 53        }
 54    }
 55
 56    fn toggle_testing(&mut self, cx: &mut Context<Self>) {
 57        if let Some(_cb) = self._stop_playback.take() {
 58            cx.notify();
 59            return;
 60        }
 61
 62        if let Some(cb) =
 63            start_test_playback(self.input_device_id.clone(), self.output_device_id.clone()).ok()
 64        {
 65            self._stop_playback = Some(cb);
 66        }
 67
 68        cx.notify();
 69    }
 70}
 71
 72fn start_test_playback(
 73    input_device_id: Option<DeviceId>,
 74    output_device_id: Option<DeviceId>,
 75) -> anyhow::Result<Box<dyn Any + Send>> {
 76    let stop_signal = Arc::new(AtomicBool::new(false));
 77
 78    thread::Builder::new()
 79        .name("AudioTestPlayback".to_string())
 80        .spawn({
 81            let stop_signal = stop_signal.clone();
 82            move || {
 83                let microphone = match open_test_microphone(input_device_id, stop_signal.clone()) {
 84                    Ok(mic) => mic,
 85                    Err(e) => {
 86                        log::error!("Could not open microphone for audio test: {e}");
 87                        return;
 88                    }
 89                };
 90
 91                let Ok(output) = audio::open_output_stream(output_device_id) else {
 92                    log::error!("Could not open output device for audio test");
 93                    return;
 94                };
 95
 96                // let microphone = rx.recv().unwrap();
 97                output.mixer().add(microphone);
 98
 99                // Keep thread (and output device) alive until stop signal
100                while !stop_signal.load(Ordering::Relaxed) {
101                    thread::sleep(Duration::from_millis(100));
102                }
103            }
104        })?;
105
106    Ok(Box::new(util::defer(move || {
107        stop_signal.store(true, Ordering::Relaxed);
108    })))
109}
110
111fn open_test_microphone(
112    input_device_id: Option<DeviceId>,
113    stop_signal: Arc<AtomicBool>,
114) -> anyhow::Result<impl Source> {
115    let stream = audio::open_input_stream(input_device_id)?;
116    let stream = stream
117        .possibly_disconnected_channels_to_mono()
118        .constant_samplerate(SAMPLE_RATE)
119        .constant_params(CHANNEL_COUNT, SAMPLE_RATE)
120        .stoppable()
121        .periodic_access(
122            Duration::from_millis(50),
123            move |stoppable: &mut rodio::source::Stoppable<_>| {
124                if stop_signal.load(Ordering::Relaxed) {
125                    stoppable.stop();
126                }
127            },
128        );
129    Ok(stream)
130}
131
132impl Render for AudioTestWindow {
133    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
134        let is_testing = self._stop_playback.is_some();
135        let button_text = if is_testing {
136            "Stop Testing"
137        } else {
138            "Start Testing"
139        };
140
141        let button_style = if is_testing {
142            ButtonStyle::Tinted(ui::TintColor::Error)
143        } else {
144            ButtonStyle::Filled
145        };
146
147        let weak_entity = cx.entity().downgrade();
148        let input_dropdown = {
149            let weak_entity = weak_entity.clone();
150            render_audio_device_dropdown(
151                "audio-test-input-dropdown",
152                self.input_device_id.clone(),
153                true,
154                move |device_id, window, cx| {
155                    weak_entity
156                        .update(cx, |this, cx| {
157                            this.input_device_id = device_id.clone();
158                            cx.notify();
159                        })
160                        .log_err();
161                    let value: Option<AudioInputDeviceName> =
162                        device_id.map(|id| AudioInputDeviceName(Some(id.to_string())));
163                    update_settings_file(
164                        SettingsUiFile::User,
165                        Some("audio.experimental.input_audio_device"),
166                        window,
167                        cx,
168                        move |settings, _cx| {
169                            settings.audio.get_or_insert_default().input_audio_device = value;
170                        },
171                    )
172                    .log_err();
173                },
174                window,
175                cx,
176            )
177        };
178
179        let output_dropdown = render_audio_device_dropdown(
180            "audio-test-output-dropdown",
181            self.output_device_id.clone(),
182            false,
183            move |device_id, window, cx| {
184                weak_entity
185                    .update(cx, |this, cx| {
186                        this.output_device_id = device_id.clone();
187                        cx.notify();
188                    })
189                    .log_err();
190                let value: Option<AudioOutputDeviceName> =
191                    device_id.map(|id| AudioOutputDeviceName(Some(id.to_string())));
192                update_settings_file(
193                    SettingsUiFile::User,
194                    Some("audio.experimental.output_audio_device"),
195                    window,
196                    cx,
197                    move |settings, _cx| {
198                        settings.audio.get_or_insert_default().output_audio_device = value;
199                    },
200                )
201                .log_err();
202            },
203            window,
204            cx,
205        );
206
207        let content = v_flex()
208            .id("audio-test-window")
209            .track_focus(&self.focus_handle)
210            .size_full()
211            .p_4()
212            .when(cfg!(target_os = "macos"), |this| this.pt_10())
213            .gap_4()
214            .bg(cx.theme().colors().editor_background)
215            .child(
216                v_flex()
217                    .gap_1()
218                    .child(Label::new("Output Device"))
219                    .child(output_dropdown),
220            )
221            .child(
222                v_flex()
223                    .gap_1()
224                    .child(Label::new("Input Device"))
225                    .child(input_dropdown),
226            )
227            .child(
228                h_flex().w_full().justify_center().pt_4().child(
229                    Button::new("test-audio-toggle", button_text)
230                        .style(button_style)
231                        .on_click(cx.listener(|this, _, _, cx| this.toggle_testing(cx))),
232                ),
233            );
234
235        client_side_decorations(
236            v_flex()
237                .size_full()
238                .text_color(cx.theme().colors().text)
239                .children(self.title_bar.clone())
240                .child(content),
241            window,
242            cx,
243            Tiling::default(),
244        )
245    }
246}
247
248impl Focusable for AudioTestWindow {
249    fn focus_handle(&self, _cx: &App) -> FocusHandle {
250        self.focus_handle.clone()
251    }
252}
253
254impl Drop for AudioTestWindow {
255    fn drop(&mut self) {
256        let _ = self._stop_playback.take();
257    }
258}
259
260pub fn open_audio_test_window(_window: &mut Window, cx: &mut App) {
261    let existing = cx
262        .windows()
263        .into_iter()
264        .find_map(|w| w.downcast::<AudioTestWindow>());
265
266    if let Some(existing) = existing {
267        existing
268            .update(cx, |_, window, _| window.activate_window())
269            .log_err();
270        return;
271    }
272
273    let app_id = ReleaseChannel::global(cx).app_id();
274    let window_size = Size {
275        width: px(640.0),
276        height: px(300.0),
277    };
278    let window_min_size = Size {
279        width: px(400.0),
280        height: px(240.0),
281    };
282
283    cx.open_window(
284        WindowOptions {
285            titlebar: Some(gpui::TitlebarOptions {
286                title: Some("Audio Test".into()),
287                appears_transparent: true,
288                traffic_light_position: Some(gpui::point(px(12.0), px(12.0))),
289            }),
290            focus: true,
291            show: true,
292            is_movable: true,
293            kind: WindowKind::Normal,
294            window_background: cx.theme().window_background_appearance(),
295            app_id: Some(app_id.to_owned()),
296            window_decorations: Some(gpui::WindowDecorations::Client),
297            window_bounds: Some(WindowBounds::centered(window_size, cx)),
298            window_min_size: Some(window_min_size),
299            ..Default::default()
300        },
301        |_, cx| cx.new(AudioTestWindow::new),
302    )
303    .log_err();
304}