From c713f56ac0e2f549fd224b10cdb8b74fa9d38eaf Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Sat, 2 Mar 2024 23:11:46 +0100 Subject: [PATCH] desktop: Allow changing the audio output device --- .../assets/texts/en-US/preferences_dialog.ftl | 5 ++- desktop/src/backends/audio.rs | 25 +++++++++-- desktop/src/gui/preferences_dialog.rs | 45 +++++++++++++++++++ desktop/src/player.rs | 2 +- desktop/src/preferences.rs | 11 +++++ desktop/src/preferences/read.rs | 40 +++++++++++++++++ desktop/src/preferences/write.rs | 24 ++++++++++ 7 files changed, 147 insertions(+), 5 deletions(-) diff --git a/desktop/assets/texts/en-US/preferences_dialog.ftl b/desktop/assets/texts/en-US/preferences_dialog.ftl index 67a7da0be..569918e37 100644 --- a/desktop/assets/texts/en-US/preferences_dialog.ftl +++ b/desktop/assets/texts/en-US/preferences_dialog.ftl @@ -13,4 +13,7 @@ graphics-power = Power Preference graphics-power-low = Low (e.g. iGPU) graphics-power-high = High (e.g. GPU) -language = Language \ No newline at end of file +language = Language + +audio-output-device = Audio Output Device +audio-output-device-default = System Default diff --git a/desktop/src/backends/audio.rs b/desktop/src/backends/audio.rs index b31559a86..27885a3d7 100644 --- a/desktop/src/backends/audio.rs +++ b/desktop/src/backends/audio.rs @@ -1,3 +1,4 @@ +use crate::preferences::GlobalPreferences; use anyhow::{anyhow, Context, Error}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use ruffle_core::backend::audio::{ @@ -16,11 +17,10 @@ pub struct CpalAudioBackend { } impl CpalAudioBackend { - pub fn new() -> Result { + pub fn new(preferences: &GlobalPreferences) -> Result { // Create CPAL audio device. let host = cpal::default_host(); - let device = host - .default_output_device() + let device = get_suitable_output_device(preferences, &host) .ok_or_else(|| anyhow!("No audio devices available"))?; // Create audio stream for device. @@ -89,3 +89,22 @@ impl AudioBackend for CpalAudioBackend { self.stream.pause().expect("Error trying to pause CPAL audio stream. This feature may not be supported by your audio device."); } } + +fn get_suitable_output_device( + preferences: &GlobalPreferences, + host: &cpal::Host, +) -> Option { + // First let's check for any user preference... + if let Some(preferred_device_name) = preferences.output_device_name() { + if let Ok(mut devices) = host.output_devices() { + if let Some(device) = + devices.find(|device| device.name().ok().as_deref() == Some(&preferred_device_name)) + { + return Some(device); + } + } + } + + // Then let's fall back to the device default + host.default_output_device() +} diff --git a/desktop/src/gui/preferences_dialog.rs b/desktop/src/gui/preferences_dialog.rs index 1d3bd58c3..eab325a1f 100644 --- a/desktop/src/gui/preferences_dialog.rs +++ b/desktop/src/gui/preferences_dialog.rs @@ -1,5 +1,6 @@ use crate::gui::{available_languages, optional_text, text}; use crate::preferences::GlobalPreferences; +use cpal::traits::{DeviceTrait, HostTrait}; use egui::{Align2, Button, ComboBox, Grid, Ui, Widget, Window}; use ruffle_render_wgpu::clap::{GraphicsBackend, PowerPreference}; use ruffle_render_wgpu::descriptors::Descriptors; @@ -20,6 +21,10 @@ pub struct PreferencesDialog { language: LanguageIdentifier, language_changed: bool, + + output_device: Option, + available_output_devices: Vec, + output_device_changed: bool, } impl PreferencesDialog { @@ -31,6 +36,16 @@ impl PreferencesDialog { available_backends |= backend_availability(descriptors, wgpu::Backends::METAL); available_backends |= backend_availability(descriptors, wgpu::Backends::DX12); + let audio_host = cpal::default_host(); + let mut available_output_devices = Vec::new(); + if let Ok(devices) = audio_host.output_devices() { + for device in devices { + if let Ok(name) = device.name() { + available_output_devices.push(name); + } + } + } + Self { available_backends, graphics_backend: preferences.graphics_backends(), @@ -44,6 +59,10 @@ impl PreferencesDialog { language: preferences.language(), language_changed: false, + output_device: preferences.output_device_name(), + available_output_devices, + output_device_changed: false, + preferences, } } @@ -67,6 +86,8 @@ impl PreferencesDialog { self.show_graphics_preferences(locale, &locked_text, ui); self.show_language_preferences(locale, ui); + + self.show_audio_preferences(locale, ui); }); if self.restart_required() { @@ -93,6 +114,7 @@ impl PreferencesDialog { fn restart_required(&self) -> bool { self.graphics_backend != self.preferences.graphics_backends() || self.power_preference != self.preferences.graphics_power_preference() + || self.output_device != self.preferences.output_device_name() } fn show_graphics_preferences( @@ -196,6 +218,25 @@ impl PreferencesDialog { ui.end_row(); } + fn show_audio_preferences(&mut self, locale: &LanguageIdentifier, ui: &mut Ui) { + ui.label(text(locale, "audio-output-device")); + + let previous = self.output_device.clone(); + let default = text(locale, "audio-output-device-default"); + ComboBox::from_id_source("audio-output-device") + .selected_text(self.output_device.as_deref().unwrap_or(default.as_ref())) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.output_device, None, default); + for device in &self.available_output_devices { + ui.selectable_value(&mut self.output_device, Some(device.to_string()), device); + } + }); + if self.output_device != previous { + self.output_device_changed = true; + } + ui.end_row(); + } + fn save(&mut self) { if let Err(e) = self.preferences.write_preferences(|preferences| { if self.graphics_backend_changed { @@ -207,6 +248,10 @@ impl PreferencesDialog { if self.language_changed { preferences.set_language(self.language.clone()); } + if self.output_device_changed { + preferences.set_output_device(self.output_device.clone()); + // [NA] TODO: Inform the running player that the device changed + } }) { // [NA] TODO: Better error handling... everywhere in desktop, really tracing::error!("Could not save preferences: {e}"); diff --git a/desktop/src/player.rs b/desktop/src/player.rs index f5747a37e..51aae333b 100644 --- a/desktop/src/player.rs +++ b/desktop/src/player.rs @@ -110,7 +110,7 @@ impl ActivePlayer { ) -> Self { let mut builder = PlayerBuilder::new(); - match CpalAudioBackend::new() { + match CpalAudioBackend::new(&preferences) { Ok(audio) => { builder = builder.with_audio(audio); } diff --git a/desktop/src/preferences.rs b/desktop/src/preferences.rs index 6934ebc16..4b21feeb2 100644 --- a/desktop/src/preferences.rs +++ b/desktop/src/preferences.rs @@ -73,6 +73,15 @@ impl GlobalPreferences { .clone() } + pub fn output_device_name(&self) -> Option { + self.preferences + .lock() + .expect("Preferences is not reentrant") + .values + .output_device + .clone() + } + pub fn write_preferences(&self, fun: impl FnOnce(&mut PreferencesWriter)) -> Result<(), Error> { let mut preferences = self .preferences @@ -99,6 +108,7 @@ pub struct SavedGlobalPreferences { pub graphics_backend: GraphicsBackend, pub graphics_power_preference: PowerPreference, pub language: LanguageIdentifier, + pub output_device: Option, } impl Default for SavedGlobalPreferences { @@ -111,6 +121,7 @@ impl Default for SavedGlobalPreferences { graphics_backend: Default::default(), graphics_power_preference: Default::default(), language: locale, + output_device: None, } } } diff --git a/desktop/src/preferences/read.rs b/desktop/src/preferences/read.rs index 89186fff3..8a4a792de 100644 --- a/desktop/src/preferences/read.rs +++ b/desktop/src/preferences/read.rs @@ -54,6 +54,12 @@ pub fn read_preferences(input: &str) -> (ParseResult, Document) { Err(e) => result.add_warning(format!("Invalid language: {e}")), }; + match parse_item_from_str(document.get("output_device")) { + Ok(Some(value)) => result.result.output_device = Some(value), + Ok(None) => {} + Err(e) => result.add_warning(format!("Invalid output_device: {e}")), + }; + (result, document) } @@ -222,4 +228,38 @@ mod tests { result ); } + + #[test] + fn correct_output_device() { + let result = read_preferences("output_device = \"Speakers\"").0; + + assert_eq!( + ParseResult { + result: SavedGlobalPreferences { + output_device: Some("Speakers".to_string()), + ..Default::default() + }, + warnings: vec![] + }, + result + ); + } + + #[test] + fn invalid_output_device() { + let result = read_preferences("output_device = 5").0; + + assert_eq!( + ParseResult { + result: SavedGlobalPreferences { + output_device: None, + ..Default::default() + }, + warnings: vec![ + "Invalid output_device: expected string but found integer".to_string() + ] + }, + result + ); + } } diff --git a/desktop/src/preferences/write.rs b/desktop/src/preferences/write.rs index 7c32a9db1..5522dfbef 100644 --- a/desktop/src/preferences/write.rs +++ b/desktop/src/preferences/write.rs @@ -24,6 +24,15 @@ impl<'a> PreferencesWriter<'a> { self.0.document["language"] = value(language.to_string()); self.0.values.language = language; } + + pub fn set_output_device(&mut self, name: Option) { + if let Some(name) = &name { + self.0.document["output_device"] = value(name); + } else { + self.0.document.remove("output_device"); + } + self.0.values.output_device = name; + } } #[cfg(test)] @@ -100,4 +109,19 @@ mod tests { "language = \"en-Latn-US-valencia\"\n", ); } + + #[test] + fn set_output_device() { + test( + "", + |writer| writer.set_output_device(Some("Speakers".to_string())), + "output_device = \"Speakers\"\n", + ); + + test( + "output_device = \"Speakers\"", + |writer| writer.set_output_device(None), + "", + ); + } }