diff --git a/pwsp-cli/src/main.rs b/pwsp-cli/src/main.rs index 0cbf148..35952f0 100644 --- a/pwsp-cli/src/main.rs +++ b/pwsp-cli/src/main.rs @@ -88,6 +88,8 @@ enum GetCommands { #[clap(short, long)] id: Option, }, + /// Volume multiplier for all tracks + VolumeMultiplier, /// Playback position (in seconds) Position { #[clap(short, long)] @@ -124,6 +126,8 @@ enum SetCommands { #[clap(short, long)] id: Option, }, + /// Volume multiplier for all tracks + VolumeMultiplier { volume: f32 }, /// Playback position (in seconds) Position { position: f32, @@ -177,6 +181,7 @@ async fn main() -> Result<()> { Commands::Get { parameter } => match parameter { GetCommands::IsPaused => Request::get_is_paused(), GetCommands::Volume { id } => Request::get_volume(id), + GetCommands::VolumeMultiplier => Request::get_volume_multiplier(), GetCommands::Position { id } => Request::get_position(id), GetCommands::Duration { id } => Request::get_duration(id), GetCommands::State => Request::get_state(), @@ -190,6 +195,7 @@ async fn main() -> Result<()> { }, Commands::Set { parameter } => match parameter { SetCommands::Volume { volume, id } => Request::set_volume(volume, id), + SetCommands::VolumeMultiplier { volume } => Request::set_volume_multiplier(volume), SetCommands::Position { position, id } => Request::seek(position, id), SetCommands::Input { name } => Request::set_input(&name), SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id), diff --git a/pwsp-gui/locales/app.toml b/pwsp-gui/locales/app.toml index 18858ab..4ed9518 100644 --- a/pwsp-gui/locales/app.toml +++ b/pwsp-gui/locales/app.toml @@ -217,6 +217,28 @@ kz = "Әрқашан дыбыс деңгейін есте сақтау" he = "זכור תמיד עוצמת קול" pt-BR = "Lembrar volume" +[gui.settings.remember_volume_multiplier] +en = "Always remember volume multiplier" +ru = "Всегда запоминать множитель громкости" +es = "Recordar siempre el multiplicador de volumen" +fr = "Toujours se souvenir du multiplicateur de volume" +zh = "始终记住音量乘数" +ar = "تذكر ضرب الصوت دائمًا" +kz = "Әрқашан дыбыс деңгейін есте сақтау" +he = "זכור תמיד עוצמת קול" +pt-BR = "Lembrar volume" + +[gui.settings.volume_multiplier] +en = "Volume multiplier" +ru = "Множитель громкости" +es = "Multiplicador de volumen" +fr = "Multiplicateur de volume" +zh = "音量乘数" +ar = "ضرب الصوت" +kz = "Дыбыс деңгейі" +he = "עוצמת קול" +pt-BR = "Volume" + [gui.settings.remember_mic] en = "Always remember microphone" ru = "Всегда запоминать микрофон" diff --git a/pwsp-gui/src/gui/update.rs b/pwsp-gui/src/gui/update.rs index 6760ec7..82bcdc8 100644 --- a/pwsp-gui/src/gui/update.rs +++ b/pwsp-gui/src/gui/update.rs @@ -93,6 +93,24 @@ impl App for SoundpadGui { } } + if self.app_state.volume_multiplier_dragged { + make_request_async(Request::set_volume_multiplier( + self.app_state.volume_multiplier_slider_value, + )); + + self.app_state.volume_multiplier_dragged = false; + self.app_state.ignore_volume_multiplier_update_until = + Some(Instant::now() + Duration::from_millis(300)); + + if self.config.save_volume_multiplier + && let Ok(mut daemon_config) = get_daemon_config() + { + daemon_config.default_volume_multiplier = + Some(self.app_state.volume_multiplier_slider_value); + update_daemon_config(&daemon_config).ok(); + } + } + // Sync audio player state { let mut guard = self diff --git a/pwsp-gui/src/gui/views/settings.rs b/pwsp-gui/src/gui/views/settings.rs index b17633c..d1f9cee 100644 --- a/pwsp-gui/src/gui/views/settings.rs +++ b/pwsp-gui/src/gui/views/settings.rs @@ -1,5 +1,5 @@ use crate::gui::SoundpadGui; -use egui::{Align, Button, Color32, ComboBox, Layout, RichText, Ui}; +use egui::{Align, Button, Color32, ComboBox, Layout, RichText, Slider, Ui}; use egui_material_icons::icons::ICON_ARROW_BACK; use pwsp_lib::types::config::PreferredTheme; use rust_i18n::t; @@ -34,6 +34,10 @@ impl SoundpadGui { &mut self.config.save_volume, t!("gui.settings.remember_volume"), ); + let save_volume_multiplier_response = ui.checkbox( + &mut self.config.save_volume_multiplier, + t!("gui.settings.remember_volume_multiplier"), + ); let save_input_response = ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic")); let save_scale_response = ui.checkbox( @@ -46,6 +50,7 @@ impl SoundpadGui { ); if save_volume_response.changed() + || save_volume_multiplier_response.changed() || save_input_response.changed() || save_scale_response.changed() || pause_on_exit_response.changed() @@ -88,6 +93,39 @@ impl SoundpadGui { } // -------------------------------- + ui.separator(); + + // ----------- Sliders ------------ + // Volume multiplier + let should_update_multiplier = !self.app_state.volume_multiplier_dragged + && self + .app_state + .ignore_volume_multiplier_update_until + .map(|t| std::time::Instant::now() > t) + .unwrap_or(true); + + if should_update_multiplier { + self.app_state.volume_multiplier_slider_value = + self.audio_player_state.volume_multiplier; + } + + ui.horizontal(|ui| { + let slider = Slider::new( + &mut self.app_state.volume_multiplier_slider_value, + 0.01..=3.0, + ); + let response = ui.add(slider); + ui.label(t!("gui.settings.volume_multiplier")); + + if response.changed() { + // This condition is required to avoid spamming requests while dragging, but to allow changing the value via TextEdit + if !response.dragged() || (response.dragged() && response.drag_stopped()) { + self.app_state.volume_multiplier_dragged = true; + } + } + }); + // -------------------------------- + ui.with_layout(Layout::bottom_up(Align::Min), |ui| { ui.label(t!( "gui.settings.version", diff --git a/pwsp-lib/src/types/audio_player.rs b/pwsp-lib/src/types/audio_player.rs index b88d4b0..de87c97 100644 --- a/pwsp-lib/src/types/audio_player.rs +++ b/pwsp-lib/src/types/audio_player.rs @@ -40,6 +40,7 @@ pub struct FullState { pub state: PlayerState, pub tracks: Vec, pub volume: f32, + pub volume_multiplier: f32, pub current_input: String, pub all_inputs: HashMap, } @@ -63,16 +64,19 @@ pub struct AudioPlayer { pub input_device_name: Option, pub volume: f32, // Master volume + pub volume_multiplier: f32, } impl AudioPlayer { pub async fn new() -> Result { - let (default_input_name, default_volume) = with_daemon_config(|c| { - ( - c.default_input_name.clone(), - c.default_volume.unwrap_or(1.0), - ) - }); + let (default_input_name, default_volume, default_volume_multiplier) = + with_daemon_config(|c| { + ( + c.default_input_name.clone(), + c.default_volume.unwrap_or(1.0), + c.default_volume_multiplier.unwrap_or(1.0), + ) + }); let mut audio_player = AudioPlayer { stream_handle: None, @@ -84,6 +88,7 @@ impl AudioPlayer { input_device_name: default_input_name, volume: default_volume, + volume_multiplier: default_volume_multiplier, }; if audio_player.input_device_name.is_some() { @@ -266,12 +271,16 @@ impl AudioPlayer { if let Some(id) = id { if let Some(sound) = self.tracks.get_mut(&id) { sound.volume = volume; - sound.sink.set_volume(self.volume * volume); + sound + .sink + .set_volume(self.volume * sound.volume * self.volume_multiplier); } } else { self.volume = volume; for sound in self.tracks.values_mut() { - sound.sink.set_volume(self.volume * sound.volume); + sound + .sink + .set_volume(self.volume * sound.volume * self.volume_multiplier); } } } @@ -351,7 +360,7 @@ impl AudioPlayer { .ok_or_else(|| anyhow::anyhow!("stream_handle is unexpectedly missing"))? .mixer(); let sink = Player::connect_new(mixer); - sink.set_volume(self.volume); // Default volume is 1.0 * master + sink.set_volume(self.volume * self.volume_multiplier); // Default volume is 1.0 * master sink.append(source); sink.play(); diff --git a/pwsp-lib/src/types/commands.rs b/pwsp-lib/src/types/commands.rs index 225bdf7..b239514 100644 --- a/pwsp-lib/src/types/commands.rs +++ b/pwsp-lib/src/types/commands.rs @@ -46,11 +46,17 @@ pub struct GetVolumeCommand { pub id: Option, } +pub struct GetVolumeMultiplierCommand {} + pub struct SetVolumeCommand { pub volume: Option, pub id: Option, } +pub struct SetVolumeMultiplierCommand { + pub volume_multiplier: Option, +} + pub struct GetPositionCommand { pub id: Option, } @@ -270,6 +276,18 @@ impl Executable for GetVolumeCommand { } } +#[async_trait] +impl Executable for GetVolumeMultiplierCommand { + async fn execute(&self) -> Response { + let audio_player = match get_audio_player().await { + Ok(player) => player.lock().await, + Err(err) => return Response::new(false, format!("Audio player error: {}", err)), + }; + + Response::new(true, audio_player.volume_multiplier.to_string()) + } +} + #[async_trait] impl Executable for SetVolumeCommand { async fn execute(&self) -> Response { @@ -286,6 +304,26 @@ impl Executable for SetVolumeCommand { } } +#[async_trait] +impl Executable for SetVolumeMultiplierCommand { + async fn execute(&self) -> Response { + if let Some(volume_multiplier) = self.volume_multiplier { + let mut audio_player = match get_audio_player().await { + Ok(player) => player.lock().await, + Err(err) => return Response::new(false, format!("Audio player error: {}", err)), + }; + audio_player.volume_multiplier = volume_multiplier; + audio_player.set_volume(volume_multiplier, None); // Reset current volume for all tracks to apply multiplier + Response::new( + true, + format!("Audio volume multiplier was set to {}", volume_multiplier), + ) + } else { + Response::new(false, "Invalid volume multiplier value") + } + } +} + #[async_trait] impl Executable for GetPositionCommand { async fn execute(&self) -> Response { @@ -516,6 +554,7 @@ impl Executable for GetFullStateCommand { state: audio_player.get_state(), tracks: audio_player.get_tracks(), volume: audio_player.volume, + volume_multiplier: audio_player.volume_multiplier, current_input: current_input_nick, all_inputs, }; diff --git a/pwsp-lib/src/types/config.rs b/pwsp-lib/src/types/config.rs index b1ea601..0d2cc99 100644 --- a/pwsp-lib/src/types/config.rs +++ b/pwsp-lib/src/types/config.rs @@ -17,6 +17,7 @@ use std::{ pub struct DaemonConfig { pub default_input_name: Option, pub default_volume: Option, + pub default_volume_multiplier: Option, } impl DaemonConfig { @@ -73,6 +74,7 @@ pub struct GuiConfig { pub left_panel_width: f32, pub save_volume: bool, + pub save_volume_multiplier: bool, pub save_input: bool, pub save_scale_factor: bool, pub pause_on_exit: bool, @@ -117,6 +119,7 @@ impl Default for GuiConfig { left_panel_width: 280.0, save_volume: false, + save_volume_multiplier: false, save_input: false, save_scale_factor: false, pause_on_exit: false, diff --git a/pwsp-lib/src/types/gui.rs b/pwsp-lib/src/types/gui.rs index 9bddfef..956874f 100644 --- a/pwsp-lib/src/types/gui.rs +++ b/pwsp-lib/src/types/gui.rs @@ -31,13 +31,16 @@ pub struct AppState { pub show_settings: bool, pub volume_dragged: bool, + pub volume_multiplier_dragged: bool, pub force_focus_search: bool, pub volume_slider_value: f32, + pub volume_multiplier_slider_value: f32, pub search_field_id: Option, pub ignore_volume_update_until: Option, + pub ignore_volume_multiplier_update_until: Option, pub current_dir: Option, pub dirs: Vec, @@ -65,6 +68,7 @@ pub struct AudioPlayerState { pub tracks: Vec, pub volume: f32, // Master volume + pub volume_multiplier: f32, pub current_input: String, pub all_inputs: HashMap, diff --git a/pwsp-lib/src/types/socket.rs b/pwsp-lib/src/types/socket.rs index b046a0a..841c987 100644 --- a/pwsp-lib/src/types/socket.rs +++ b/pwsp-lib/src/types/socket.rs @@ -95,6 +95,10 @@ impl Request { Request::new("get_volume", args) } + pub fn get_volume_multiplier() -> Self { + Request::new("get_volume_multiplier", vec![]) + } + pub fn get_position(id: Option) -> Self { let mut args = vec![]; let id_str; @@ -139,6 +143,13 @@ impl Request { Request::new("set_volume".to_string(), args) } + pub fn set_volume_multiplier(volume: f32) -> Self { + Request::new( + "set_volume_multiplier", + vec![("volume_multiplier", &volume.to_string())], + ) + } + pub fn seek(position: f32, id: Option) -> Self { let mut args = vec![("position".to_string(), position.to_string())]; if let Some(id) = id { diff --git a/pwsp-lib/src/utils/commands.rs b/pwsp-lib/src/utils/commands.rs index 332dd88..1359d93 100644 --- a/pwsp-lib/src/utils/commands.rs +++ b/pwsp-lib/src/utils/commands.rs @@ -15,6 +15,7 @@ pub fn parse_command(request: &Request) -> Option> { "is_paused" => Some(Box::new(IsPausedCommand {})), "get_state" => Some(Box::new(GetStateCommand {})), "get_volume" => Some(Box::new(GetVolumeCommand { id })), + "get_volume_multiplier" => Some(Box::new(GetVolumeMultiplierCommand {})), "set_volume" => { let volume = request .args @@ -24,6 +25,15 @@ pub fn parse_command(request: &Request) -> Option> { .ok(); Some(Box::new(SetVolumeCommand { volume, id })) } + "set_volume_multiplier" => { + let volume_multiplier = request + .args + .get("volume_multiplier") + .unwrap_or(&String::new()) + .parse::() + .ok(); + Some(Box::new(SetVolumeMultiplierCommand { volume_multiplier })) + } "get_position" => Some(Box::new(GetPositionCommand { id })), "seek" => { let position = request diff --git a/pwsp-lib/src/utils/gui.rs b/pwsp-lib/src/utils/gui.rs index 38de1e1..79dcfc5 100644 --- a/pwsp-lib/src/utils/gui.rs +++ b/pwsp-lib/src/utils/gui.rs @@ -116,6 +116,7 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc