desktop: Add desktop volume controls

The desktop version of Ruffle now has a volume controls window. It can
be accessed through the menu bar (Controls > Volume controls).
It contains a mute button and a slider from 0 to 100.

The volume settings set in the GUI are saved in a new VolumeControls
struct, which is also used to calculate the real volume (adapted for
logarithmic hearing) out of the entered volume and the mute checkbox.
As soon as the volume is changed in the GUI, the real volume will be set
in the player (if the player exists).

The player doesn't set its volume level according to the PlayerOptions
after its creation anymore. Instead, RuffleGui::on_player_created now
gets the player and sets its volume to the real volume set in the GUI.
The volume in the GUI itself defaults to the PlayerOptions value.
This also fixes the issue that the PlayerOptions volume has previously
not been adapted for logarithmic hearing.

The existing ftl files have been adapted (and new ones have been
created) to include the new multilingual text in the menu bar and the
volume controls window.
This commit is contained in:
Kornelius Rohrschneider 2023-08-13 15:07:04 +02:00 committed by Adrian Wielgosik
parent d195da59af
commit a550a998c3
7 changed files with 102 additions and 5 deletions

View File

@ -17,6 +17,7 @@ file-menu-exit = Beenden
controls-menu = Steuerung
controls-menu-suspend = Aussetzen
controls-menu-resume = Fortsetzen
controls-menu-volume = Lautstärke
help-menu = Hilfe
help-menu-join-discord = Discord beitreten
help-menu-report-a-bug = Bug melden...

View File

@ -0,0 +1,3 @@
volume-controls = Lautstärkeeinstellungen
volume-controls-mute = Stummschalten
volume-controls-volume = Lautstärke

View File

@ -17,6 +17,7 @@ file-menu-exit = Exit
controls-menu = Controls
controls-menu-suspend = Suspend
controls-menu-resume = Resume
controls-menu-volume = Volume controls
help-menu = Help
help-menu-join-discord = Join Discord

View File

@ -0,0 +1,3 @@
volume-controls = Volume controls
volume-controls-mute = Mute
volume-controls-volume = Volume

View File

@ -20,6 +20,7 @@ use ruffle_core::debug_ui::Message as DebugMessage;
use ruffle_core::Player;
use std::collections::HashMap;
use std::fs;
use std::sync::MutexGuard;
use sys_locale::get_locale;
use unic_langid::LanguageIdentifier;
use winit::event_loop::EventLoopProxy;
@ -64,6 +65,8 @@ pub const MENU_HEIGHT: u32 = 24;
pub struct RuffleGui {
event_loop: EventLoopProxy<RuffleEvent>,
is_about_visible: bool,
is_volume_visible: bool,
volume_controls: VolumeControls,
is_open_dialog_visible: bool,
context_menu: Vec<ruffle_core::ContextMenuItem>,
open_dialog: OpenDialog,
@ -89,6 +92,8 @@ impl RuffleGui {
Self {
is_about_visible: false,
is_volume_visible: false,
volume_controls: VolumeControls::new(false, default_player_options.volume * 100.0),
is_open_dialog_visible: false,
was_suspended_before_debug: false,
@ -148,6 +153,10 @@ impl RuffleGui {
}
});
}
self.volume_window(egui_ctx, Some(player));
} else {
self.volume_window(egui_ctx, None);
}
if !self.context_menu.is_empty() {
@ -164,7 +173,12 @@ impl RuffleGui {
}
/// Notifies the GUI that a new player was created.
fn on_player_created(&mut self, opt: PlayerOptions, movie_url: Url) {
fn on_player_created(
&mut self,
opt: PlayerOptions,
movie_url: Url,
mut player: MutexGuard<Player>,
) {
self.currently_opened = Some((movie_url.clone(), opt.clone()));
// Update dialog state to reflect the newly-opened movie's options.
@ -175,6 +189,8 @@ impl RuffleGui {
self.event_loop.clone(),
self.locale.clone(),
);
player.set_volume(self.volume_controls.get_real_volume());
}
/// Renders the main menu bar at the top of the window.
@ -255,6 +271,9 @@ impl RuffleGui {
}
}
});
if Button::new(text(&self.locale, "controls-menu-volume")).ui(ui).clicked() {
self.show_volume_screen(ui);
}
});
menu::menu_button(ui, text(&self.locale, "debug-menu"), |ui| {
ui.add_enabled_ui(player.is_some(), |ui| {
@ -306,6 +325,7 @@ impl RuffleGui {
});
}
/// Renders the About Ruffle window.
fn about_window(&mut self, egui_ctx: &egui::Context) {
egui::Window::new(text(&self.locale, "about-ruffle"))
.collapsible(false)
@ -392,6 +412,40 @@ impl RuffleGui {
});
}
/// Renders the volume controls window.
fn volume_window(&mut self, egui_ctx: &egui::Context, player: Option<&mut Player>) {
egui::Window::new(text(&self.locale, "volume-controls"))
.collapsible(false)
.resizable(false)
.anchor(Align2::CENTER_CENTER, egui::Vec2::ZERO)
.open(&mut self.is_volume_visible)
.show(egui_ctx, |ui| {
let mut changed_slider = false;
let changed_checkbox = ui
.checkbox(
&mut self.volume_controls.is_muted,
text(&self.locale, "volume-controls-mute"),
)
.changed();
ui.add_enabled_ui(!self.volume_controls.is_muted, |ui| {
ui.horizontal(|ui| {
ui.label(text(&self.locale, "volume-controls-volume"));
changed_slider = ui
.add(Slider::new(&mut self.volume_controls.volume, 0.0..=100.0))
.changed();
});
});
if changed_checkbox || changed_slider {
if let Some(player) = player {
player.set_volume(self.volume_controls.get_real_volume());
}
}
});
}
/// Renders the right-click context menu.
fn context_menu(&mut self, egui_ctx: &egui::Context) {
let mut item_clicked = false;
@ -483,4 +537,35 @@ impl RuffleGui {
self.is_about_visible = true;
ui.close_menu();
}
fn show_volume_screen(&mut self, ui: &mut egui::Ui) {
self.is_volume_visible = true;
ui.close_menu();
}
}
/// The volume controls of the Ruffle GUI.
pub struct VolumeControls {
is_muted: bool,
volume: f32,
}
impl VolumeControls {
fn new(is_muted: bool, volume: f32) -> Self {
Self { is_muted, volume }
}
/// Calculates the real volume (between 0 and 1) out of the entered volume
/// (between 0 and 100) and the mute checkbox.
///
/// This is also necessary because human hearing is logarithmic:
/// What sounds like half as loud (and should be 50% on the scale) is actually
/// about 1/10th of the real volume.
pub fn get_real_volume(&self) -> f32 {
if !self.is_muted {
(10_f32.powf(81_f32.log10() * self.volume / 100.0) - 1.0) / 80.0
} else {
0.0
}
}
}

View File

@ -182,7 +182,13 @@ impl GuiController {
self.size.height,
);
player.create(&opt, &movie_url, movie_view);
self.gui.on_player_created(opt, movie_url);
self.gui.on_player_created(
opt,
movie_url,
player
.get()
.expect("Player must exist after being created."),
);
}
pub fn render(&mut self, mut player: Option<MutexGuard<Player>>) {

View File

@ -8,7 +8,6 @@ use crate::executor::WinitAsyncExecutor;
use crate::gui::MovieView;
use crate::{CALLSTACK, RENDER_INFO, SWF_INFO};
use anyhow::anyhow;
use ruffle_core::backend::audio::AudioBackend;
use ruffle_core::backend::navigator::{OpenURLMode, SocketMode};
use ruffle_core::config::Letterbox;
use ruffle_core::{LoadBehavior, Player, PlayerBuilder, PlayerEvent, StageAlign, StageScaleMode};
@ -98,8 +97,7 @@ impl ActivePlayer {
let mut builder = PlayerBuilder::new();
match CpalAudioBackend::new() {
Ok(mut audio) => {
audio.set_volume(opt.volume);
Ok(audio) => {
builder = builder.with_audio(audio);
}
Err(e) => {