feat(lib, daemon, cli, gui): add volume multiplier (#175)

* initial implementation

* rename DaemonConfig.volume_multiplier to default_volume_multiplier

* add volume_multiplier to the FullState

* add pwsp-gui integration
This commit is contained in:
Tarasov Aleksandr
2026-07-24 05:33:51 +03:00
committed by GitHub
parent a33d023950
commit 65ae8589db
11 changed files with 171 additions and 10 deletions
+6
View File
@@ -88,6 +88,8 @@ enum GetCommands {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Volume multiplier for all tracks
VolumeMultiplier,
/// Playback position (in seconds) /// Playback position (in seconds)
Position { Position {
#[clap(short, long)] #[clap(short, long)]
@@ -124,6 +126,8 @@ enum SetCommands {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Volume multiplier for all tracks
VolumeMultiplier { volume: f32 },
/// Playback position (in seconds) /// Playback position (in seconds)
Position { Position {
position: f32, position: f32,
@@ -177,6 +181,7 @@ async fn main() -> Result<()> {
Commands::Get { parameter } => match parameter { Commands::Get { parameter } => match parameter {
GetCommands::IsPaused => Request::get_is_paused(), GetCommands::IsPaused => Request::get_is_paused(),
GetCommands::Volume { id } => Request::get_volume(id), GetCommands::Volume { id } => Request::get_volume(id),
GetCommands::VolumeMultiplier => Request::get_volume_multiplier(),
GetCommands::Position { id } => Request::get_position(id), GetCommands::Position { id } => Request::get_position(id),
GetCommands::Duration { id } => Request::get_duration(id), GetCommands::Duration { id } => Request::get_duration(id),
GetCommands::State => Request::get_state(), GetCommands::State => Request::get_state(),
@@ -190,6 +195,7 @@ async fn main() -> Result<()> {
}, },
Commands::Set { parameter } => match parameter { Commands::Set { parameter } => match parameter {
SetCommands::Volume { volume, id } => Request::set_volume(volume, id), 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::Position { position, id } => Request::seek(position, id),
SetCommands::Input { name } => Request::set_input(&name), SetCommands::Input { name } => Request::set_input(&name),
SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id), SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id),
+22
View File
@@ -217,6 +217,28 @@ kz = "Әрқашан дыбыс деңгейін есте сақтау"
he = "זכור תמיד עוצמת קול" he = "זכור תמיד עוצמת קול"
pt-BR = "Lembrar volume" 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] [gui.settings.remember_mic]
en = "Always remember microphone" en = "Always remember microphone"
ru = "Всегда запоминать микрофон" ru = "Всегда запоминать микрофон"
+18
View File
@@ -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 // Sync audio player state
{ {
let mut guard = self let mut guard = self
+39 -1
View File
@@ -1,5 +1,5 @@
use crate::gui::SoundpadGui; 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 egui_material_icons::icons::ICON_ARROW_BACK;
use pwsp_lib::types::config::PreferredTheme; use pwsp_lib::types::config::PreferredTheme;
use rust_i18n::t; use rust_i18n::t;
@@ -34,6 +34,10 @@ impl SoundpadGui {
&mut self.config.save_volume, &mut self.config.save_volume,
t!("gui.settings.remember_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 = let save_input_response =
ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic")); ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic"));
let save_scale_response = ui.checkbox( let save_scale_response = ui.checkbox(
@@ -46,6 +50,7 @@ impl SoundpadGui {
); );
if save_volume_response.changed() if save_volume_response.changed()
|| save_volume_multiplier_response.changed()
|| save_input_response.changed() || save_input_response.changed()
|| save_scale_response.changed() || save_scale_response.changed()
|| pause_on_exit_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.with_layout(Layout::bottom_up(Align::Min), |ui| {
ui.label(t!( ui.label(t!(
"gui.settings.version", "gui.settings.version",
+18 -9
View File
@@ -40,6 +40,7 @@ pub struct FullState {
pub state: PlayerState, pub state: PlayerState,
pub tracks: Vec<TrackInfo>, pub tracks: Vec<TrackInfo>,
pub volume: f32, pub volume: f32,
pub volume_multiplier: f32,
pub current_input: String, pub current_input: String,
pub all_inputs: HashMap<String, String>, pub all_inputs: HashMap<String, String>,
} }
@@ -63,16 +64,19 @@ pub struct AudioPlayer {
pub input_device_name: Option<String>, pub input_device_name: Option<String>,
pub volume: f32, // Master volume pub volume: f32, // Master volume
pub volume_multiplier: f32,
} }
impl AudioPlayer { impl AudioPlayer {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
let (default_input_name, default_volume) = with_daemon_config(|c| { 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_input_name.clone(),
) c.default_volume.unwrap_or(1.0),
}); c.default_volume_multiplier.unwrap_or(1.0),
)
});
let mut audio_player = AudioPlayer { let mut audio_player = AudioPlayer {
stream_handle: None, stream_handle: None,
@@ -84,6 +88,7 @@ impl AudioPlayer {
input_device_name: default_input_name, input_device_name: default_input_name,
volume: default_volume, volume: default_volume,
volume_multiplier: default_volume_multiplier,
}; };
if audio_player.input_device_name.is_some() { if audio_player.input_device_name.is_some() {
@@ -266,12 +271,16 @@ impl AudioPlayer {
if let Some(id) = id { if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) { if let Some(sound) = self.tracks.get_mut(&id) {
sound.volume = volume; sound.volume = volume;
sound.sink.set_volume(self.volume * volume); sound
.sink
.set_volume(self.volume * sound.volume * self.volume_multiplier);
} }
} else { } else {
self.volume = volume; self.volume = volume;
for sound in self.tracks.values_mut() { 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"))? .ok_or_else(|| anyhow::anyhow!("stream_handle is unexpectedly missing"))?
.mixer(); .mixer();
let sink = Player::connect_new(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.append(source);
sink.play(); sink.play();
+39
View File
@@ -46,11 +46,17 @@ pub struct GetVolumeCommand {
pub id: Option<u32>, pub id: Option<u32>,
} }
pub struct GetVolumeMultiplierCommand {}
pub struct SetVolumeCommand { pub struct SetVolumeCommand {
pub volume: Option<f32>, pub volume: Option<f32>,
pub id: Option<u32>, pub id: Option<u32>,
} }
pub struct SetVolumeMultiplierCommand {
pub volume_multiplier: Option<f32>,
}
pub struct GetPositionCommand { pub struct GetPositionCommand {
pub id: Option<u32>, pub id: Option<u32>,
} }
@@ -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] #[async_trait]
impl Executable for SetVolumeCommand { impl Executable for SetVolumeCommand {
async fn execute(&self) -> Response { 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] #[async_trait]
impl Executable for GetPositionCommand { impl Executable for GetPositionCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -516,6 +554,7 @@ impl Executable for GetFullStateCommand {
state: audio_player.get_state(), state: audio_player.get_state(),
tracks: audio_player.get_tracks(), tracks: audio_player.get_tracks(),
volume: audio_player.volume, volume: audio_player.volume,
volume_multiplier: audio_player.volume_multiplier,
current_input: current_input_nick, current_input: current_input_nick,
all_inputs, all_inputs,
}; };
+3
View File
@@ -17,6 +17,7 @@ use std::{
pub struct DaemonConfig { pub struct DaemonConfig {
pub default_input_name: Option<String>, pub default_input_name: Option<String>,
pub default_volume: Option<f32>, pub default_volume: Option<f32>,
pub default_volume_multiplier: Option<f32>,
} }
impl DaemonConfig { impl DaemonConfig {
@@ -73,6 +74,7 @@ pub struct GuiConfig {
pub left_panel_width: f32, pub left_panel_width: f32,
pub save_volume: bool, pub save_volume: bool,
pub save_volume_multiplier: bool,
pub save_input: bool, pub save_input: bool,
pub save_scale_factor: bool, pub save_scale_factor: bool,
pub pause_on_exit: bool, pub pause_on_exit: bool,
@@ -117,6 +119,7 @@ impl Default for GuiConfig {
left_panel_width: 280.0, left_panel_width: 280.0,
save_volume: false, save_volume: false,
save_volume_multiplier: false,
save_input: false, save_input: false,
save_scale_factor: false, save_scale_factor: false,
pause_on_exit: false, pause_on_exit: false,
+4
View File
@@ -31,13 +31,16 @@ pub struct AppState {
pub show_settings: bool, pub show_settings: bool,
pub volume_dragged: bool, pub volume_dragged: bool,
pub volume_multiplier_dragged: bool,
pub force_focus_search: bool, pub force_focus_search: bool,
pub volume_slider_value: f32, pub volume_slider_value: f32,
pub volume_multiplier_slider_value: f32,
pub search_field_id: Option<Id>, pub search_field_id: Option<Id>,
pub ignore_volume_update_until: Option<Instant>, pub ignore_volume_update_until: Option<Instant>,
pub ignore_volume_multiplier_update_until: Option<Instant>,
pub current_dir: Option<PathBuf>, pub current_dir: Option<PathBuf>,
pub dirs: Vec<PathBuf>, pub dirs: Vec<PathBuf>,
@@ -65,6 +68,7 @@ pub struct AudioPlayerState {
pub tracks: Vec<TrackInfo>, pub tracks: Vec<TrackInfo>,
pub volume: f32, // Master volume pub volume: f32, // Master volume
pub volume_multiplier: f32,
pub current_input: String, pub current_input: String,
pub all_inputs: HashMap<String, String>, pub all_inputs: HashMap<String, String>,
+11
View File
@@ -95,6 +95,10 @@ impl Request {
Request::new("get_volume", args) Request::new("get_volume", args)
} }
pub fn get_volume_multiplier() -> Self {
Request::new("get_volume_multiplier", vec![])
}
pub fn get_position(id: Option<u32>) -> Self { pub fn get_position(id: Option<u32>) -> Self {
let mut args = vec![]; let mut args = vec![];
let id_str; let id_str;
@@ -139,6 +143,13 @@ impl Request {
Request::new("set_volume".to_string(), args) 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<u32>) -> Self { pub fn seek(position: f32, id: Option<u32>) -> Self {
let mut args = vec![("position".to_string(), position.to_string())]; let mut args = vec![("position".to_string(), position.to_string())];
if let Some(id) = id { if let Some(id) = id {
+10
View File
@@ -15,6 +15,7 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
"is_paused" => Some(Box::new(IsPausedCommand {})), "is_paused" => Some(Box::new(IsPausedCommand {})),
"get_state" => Some(Box::new(GetStateCommand {})), "get_state" => Some(Box::new(GetStateCommand {})),
"get_volume" => Some(Box::new(GetVolumeCommand { id })), "get_volume" => Some(Box::new(GetVolumeCommand { id })),
"get_volume_multiplier" => Some(Box::new(GetVolumeMultiplierCommand {})),
"set_volume" => { "set_volume" => {
let volume = request let volume = request
.args .args
@@ -24,6 +25,15 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
.ok(); .ok();
Some(Box::new(SetVolumeCommand { volume, id })) Some(Box::new(SetVolumeCommand { volume, id }))
} }
"set_volume_multiplier" => {
let volume_multiplier = request
.args
.get("volume_multiplier")
.unwrap_or(&String::new())
.parse::<f32>()
.ok();
Some(Box::new(SetVolumeMultiplierCommand { volume_multiplier }))
}
"get_position" => Some(Box::new(GetPositionCommand { id })), "get_position" => Some(Box::new(GetPositionCommand { id })),
"seek" => { "seek" => {
let position = request let position = request
+1
View File
@@ -116,6 +116,7 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
}; };
guard.tracks = full_state.tracks; guard.tracks = full_state.tracks;
guard.volume = full_state.volume; guard.volume = full_state.volume;
guard.volume_multiplier = full_state.volume_multiplier;
guard.current_input = full_state guard.current_input = full_state
.current_input .current_input
.split(" - ") .split(" - ")