From b3747b4bf96293fdcef19940da964401b9dacd25 Mon Sep 17 00:00:00 2001 From: Christina Ying Wang Date: Tue, 10 Feb 2026 21:07:16 -0800 Subject: [PATCH] Add audio input device selector Add a ComboBox in the top bar to select the audio input device at runtime. Users with USB audio interfaces or multiple input devices can now switch without changing their OS default prior to launching the app. - Add selected_device_name field and switch_device()/ available_input_devices()/selected_device_name() methods to Mir - Refactor audio_input() to accept an optional device name - Add "Audio input:" ComboBox in the top bar after latency compensation - Cache the device list to avoid per-frame CoreAudio enumeration, refresh on ComboBox click to pick up newly connected devices Signed-off-by: Christina Ying Wang --- src/bin/main.rs | 33 ++++++++++++++++++++ src/lib/mir.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 0723564c..db2c0df1 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -131,6 +131,8 @@ struct App<'a> { left_panel_expanded: bool, library_newly_opened: bool, insertion_point: InsertionPoint, + selected_device_name: String, + available_devices: Vec, preview_images: HashMap, winit_output: WinitOutput<'a>, app_ui: Option, // Stuff we can't make until we have a window @@ -333,6 +335,8 @@ impl App<'_> { left_panel_expanded: false, library_newly_opened: false, insertion_point: Default::default(), + selected_device_name: "Default".to_owned(), + available_devices: Mir::available_input_devices(), preview_images: Default::default(), winit_output, app_ui: None, @@ -798,6 +802,35 @@ impl App<'_> { .suffix("s") .range(0. ..=1.), ); + ui.label("Audio input:"); + let prev_device = self.selected_device_name.clone(); + let combo = egui::ComboBox::from_id_salt("audio input device") + .selected_text(self.selected_device_name.as_str()) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.selected_device_name, + "Default".to_owned(), + "Default", + ); + for name in &self.available_devices { + ui.selectable_value( + &mut self.selected_device_name, + name.clone(), + name.as_str(), + ); + } + }); + if combo.response.clicked() { + self.available_devices = Mir::available_input_devices(); + } + if self.selected_device_name != prev_device { + let device = if self.selected_device_name == "Default" { + None + } else { + Some(self.selected_device_name.clone()) + }; + self.mir.switch_device(device); + } }); }); diff --git a/src/lib/mir.rs b/src/lib/mir.rs index b6332a37..632f94b9 100644 --- a/src/lib/mir.rs +++ b/src/lib/mir.rs @@ -1,6 +1,6 @@ pub use crate::beat_tracking::AudioLevels; use crate::beat_tracking::{BeatTracker, N_FILTERS, SAMPLE_RATE}; -use cpal::traits::DeviceTrait; +use cpal::traits::{DeviceTrait, HostTrait}; use std::sync::mpsc; use std::time; @@ -31,6 +31,7 @@ pub struct Mir { last_update: Update, pub global_timescale: f32, pub latency_compensation: f32, // Anticipate beats by this many seconds + selected_device_name: Option, } /// Updates sent over a queue @@ -84,13 +85,29 @@ impl Default for Mir { } impl Mir { - fn audio_input(sender: mpsc::SyncSender) -> Result { - use cpal::traits::{HostTrait, StreamTrait}; + fn audio_input( + sender: mpsc::SyncSender, + device_name: Option<&str>, + ) -> Result { + use cpal::traits::StreamTrait; let host = cpal::default_host(); - let device = host - .default_input_device() - .ok_or("No audio input devices found")?; + let device = match device_name { + Some(name) => host + .input_devices() + .map_err(|e| format!("Failed to enumerate input devices: {:?}", e))? + .find(|d| { + d.description() + .ok() + .map(|desc| desc.name().to_string()) + .as_deref() + == Some(name) + }) + .ok_or_else(|| format!("Audio input device '{}' not found", name))?, + None => host + .default_input_device() + .ok_or("No audio input devices found")?, + }; const MIN_USEFUL_BUFFER_SIZE: cpal::FrameCount = 256; // Lower actually would be useful, but CPAL lies about the min size, so this ought to be safe const SAMPLE_RATE_CPAL: cpal::SampleRate = SAMPLE_RATE as u32; @@ -294,7 +311,7 @@ impl Mir { let (sender, receiver) = mpsc::sync_channel(MESSAGE_BUFFER_SIZE); // Set up system audio - let (stream, last_update) = match Self::audio_input(sender) { + let (stream, last_update) = match Self::audio_input(sender, None) { Ok(stream) => ( Some(stream), Update { @@ -330,6 +347,55 @@ impl Mir { last_update, global_timescale: 1., latency_compensation: 0.1, + selected_device_name: None, + } + } + + pub fn available_input_devices() -> Vec { + let host = cpal::default_host(); + match host.input_devices() { + Ok(devices) => devices + .filter_map(|d| d.description().ok().map(|desc| desc.name().to_string())) + .collect(), + Err(e) => { + println!("MIR: Failed to enumerate input devices: {:?}", e); + Vec::new() + } + } + } + + pub fn selected_device_name(&self) -> &str { + match &self.selected_device_name { + Some(name) => name.as_str(), + None => "Default", + } + } + + pub fn switch_device(&mut self, device_name: Option) { + self.selected_device_name = device_name; + + // Drop the current stream + self._stream = None; + + // Create a new channel and stream + const MESSAGE_BUFFER_SIZE: usize = 16; + let (sender, receiver) = mpsc::sync_channel(MESSAGE_BUFFER_SIZE); + + match Self::audio_input(sender, self.selected_device_name.as_deref()) { + Ok(stream) => { + println!( + "MIR: Switched to audio device: {}", + self.selected_device_name() + ); + self._stream = Some(stream); + self.receiver = receiver; + } + Err(e) => { + println!("MIR: Failed to switch audio device: {}", e); + println!("MIR: Proceeding with no audio input"); + self._stream = None; + self.receiver = receiver; + } } }