desktop: Allow changing the audio output device
This commit is contained in:
parent
09dfa6427e
commit
c713f56ac0
|
@ -14,3 +14,6 @@ graphics-power-low = Low (e.g. iGPU)
|
||||||
graphics-power-high = High (e.g. GPU)
|
graphics-power-high = High (e.g. GPU)
|
||||||
|
|
||||||
language = Language
|
language = Language
|
||||||
|
|
||||||
|
audio-output-device = Audio Output Device
|
||||||
|
audio-output-device-default = System Default
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::preferences::GlobalPreferences;
|
||||||
use anyhow::{anyhow, Context, Error};
|
use anyhow::{anyhow, Context, Error};
|
||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
use ruffle_core::backend::audio::{
|
use ruffle_core::backend::audio::{
|
||||||
|
@ -16,11 +17,10 @@ pub struct CpalAudioBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CpalAudioBackend {
|
impl CpalAudioBackend {
|
||||||
pub fn new() -> Result<Self, Error> {
|
pub fn new(preferences: &GlobalPreferences) -> Result<Self, Error> {
|
||||||
// Create CPAL audio device.
|
// Create CPAL audio device.
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
let device = host
|
let device = get_suitable_output_device(preferences, &host)
|
||||||
.default_output_device()
|
|
||||||
.ok_or_else(|| anyhow!("No audio devices available"))?;
|
.ok_or_else(|| anyhow!("No audio devices available"))?;
|
||||||
|
|
||||||
// Create audio stream for device.
|
// 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.");
|
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<cpal::Device> {
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::gui::{available_languages, optional_text, text};
|
use crate::gui::{available_languages, optional_text, text};
|
||||||
use crate::preferences::GlobalPreferences;
|
use crate::preferences::GlobalPreferences;
|
||||||
|
use cpal::traits::{DeviceTrait, HostTrait};
|
||||||
use egui::{Align2, Button, ComboBox, Grid, Ui, Widget, Window};
|
use egui::{Align2, Button, ComboBox, Grid, Ui, Widget, Window};
|
||||||
use ruffle_render_wgpu::clap::{GraphicsBackend, PowerPreference};
|
use ruffle_render_wgpu::clap::{GraphicsBackend, PowerPreference};
|
||||||
use ruffle_render_wgpu::descriptors::Descriptors;
|
use ruffle_render_wgpu::descriptors::Descriptors;
|
||||||
|
@ -20,6 +21,10 @@ pub struct PreferencesDialog {
|
||||||
|
|
||||||
language: LanguageIdentifier,
|
language: LanguageIdentifier,
|
||||||
language_changed: bool,
|
language_changed: bool,
|
||||||
|
|
||||||
|
output_device: Option<String>,
|
||||||
|
available_output_devices: Vec<String>,
|
||||||
|
output_device_changed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreferencesDialog {
|
impl PreferencesDialog {
|
||||||
|
@ -31,6 +36,16 @@ impl PreferencesDialog {
|
||||||
available_backends |= backend_availability(descriptors, wgpu::Backends::METAL);
|
available_backends |= backend_availability(descriptors, wgpu::Backends::METAL);
|
||||||
available_backends |= backend_availability(descriptors, wgpu::Backends::DX12);
|
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 {
|
Self {
|
||||||
available_backends,
|
available_backends,
|
||||||
graphics_backend: preferences.graphics_backends(),
|
graphics_backend: preferences.graphics_backends(),
|
||||||
|
@ -44,6 +59,10 @@ impl PreferencesDialog {
|
||||||
language: preferences.language(),
|
language: preferences.language(),
|
||||||
language_changed: false,
|
language_changed: false,
|
||||||
|
|
||||||
|
output_device: preferences.output_device_name(),
|
||||||
|
available_output_devices,
|
||||||
|
output_device_changed: false,
|
||||||
|
|
||||||
preferences,
|
preferences,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +86,8 @@ impl PreferencesDialog {
|
||||||
self.show_graphics_preferences(locale, &locked_text, ui);
|
self.show_graphics_preferences(locale, &locked_text, ui);
|
||||||
|
|
||||||
self.show_language_preferences(locale, ui);
|
self.show_language_preferences(locale, ui);
|
||||||
|
|
||||||
|
self.show_audio_preferences(locale, ui);
|
||||||
});
|
});
|
||||||
|
|
||||||
if self.restart_required() {
|
if self.restart_required() {
|
||||||
|
@ -93,6 +114,7 @@ impl PreferencesDialog {
|
||||||
fn restart_required(&self) -> bool {
|
fn restart_required(&self) -> bool {
|
||||||
self.graphics_backend != self.preferences.graphics_backends()
|
self.graphics_backend != self.preferences.graphics_backends()
|
||||||
|| self.power_preference != self.preferences.graphics_power_preference()
|
|| self.power_preference != self.preferences.graphics_power_preference()
|
||||||
|
|| self.output_device != self.preferences.output_device_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_graphics_preferences(
|
fn show_graphics_preferences(
|
||||||
|
@ -196,6 +218,25 @@ impl PreferencesDialog {
|
||||||
ui.end_row();
|
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) {
|
fn save(&mut self) {
|
||||||
if let Err(e) = self.preferences.write_preferences(|preferences| {
|
if let Err(e) = self.preferences.write_preferences(|preferences| {
|
||||||
if self.graphics_backend_changed {
|
if self.graphics_backend_changed {
|
||||||
|
@ -207,6 +248,10 @@ impl PreferencesDialog {
|
||||||
if self.language_changed {
|
if self.language_changed {
|
||||||
preferences.set_language(self.language.clone());
|
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
|
// [NA] TODO: Better error handling... everywhere in desktop, really
|
||||||
tracing::error!("Could not save preferences: {e}");
|
tracing::error!("Could not save preferences: {e}");
|
||||||
|
|
|
@ -110,7 +110,7 @@ impl ActivePlayer {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut builder = PlayerBuilder::new();
|
let mut builder = PlayerBuilder::new();
|
||||||
|
|
||||||
match CpalAudioBackend::new() {
|
match CpalAudioBackend::new(&preferences) {
|
||||||
Ok(audio) => {
|
Ok(audio) => {
|
||||||
builder = builder.with_audio(audio);
|
builder = builder.with_audio(audio);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,15 @@ impl GlobalPreferences {
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn output_device_name(&self) -> Option<String> {
|
||||||
|
self.preferences
|
||||||
|
.lock()
|
||||||
|
.expect("Preferences is not reentrant")
|
||||||
|
.values
|
||||||
|
.output_device
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write_preferences(&self, fun: impl FnOnce(&mut PreferencesWriter)) -> Result<(), Error> {
|
pub fn write_preferences(&self, fun: impl FnOnce(&mut PreferencesWriter)) -> Result<(), Error> {
|
||||||
let mut preferences = self
|
let mut preferences = self
|
||||||
.preferences
|
.preferences
|
||||||
|
@ -99,6 +108,7 @@ pub struct SavedGlobalPreferences {
|
||||||
pub graphics_backend: GraphicsBackend,
|
pub graphics_backend: GraphicsBackend,
|
||||||
pub graphics_power_preference: PowerPreference,
|
pub graphics_power_preference: PowerPreference,
|
||||||
pub language: LanguageIdentifier,
|
pub language: LanguageIdentifier,
|
||||||
|
pub output_device: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SavedGlobalPreferences {
|
impl Default for SavedGlobalPreferences {
|
||||||
|
@ -111,6 +121,7 @@ impl Default for SavedGlobalPreferences {
|
||||||
graphics_backend: Default::default(),
|
graphics_backend: Default::default(),
|
||||||
graphics_power_preference: Default::default(),
|
graphics_power_preference: Default::default(),
|
||||||
language: locale,
|
language: locale,
|
||||||
|
output_device: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,12 @@ pub fn read_preferences(input: &str) -> (ParseResult, Document) {
|
||||||
Err(e) => result.add_warning(format!("Invalid language: {e}")),
|
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)
|
(result, document)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,4 +228,38 @@ mod tests {
|
||||||
result
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,15 @@ impl<'a> PreferencesWriter<'a> {
|
||||||
self.0.document["language"] = value(language.to_string());
|
self.0.document["language"] = value(language.to_string());
|
||||||
self.0.values.language = language;
|
self.0.values.language = language;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_output_device(&mut self, name: Option<String>) {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
|
@ -100,4 +109,19 @@ mod tests {
|
||||||
"language = \"en-Latn-US-valencia\"\n",
|
"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),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue