feat: first attemp to support playing multiple tracks in parallel

This commit is contained in:
2026-01-24 22:18:42 +03:00
parent c1c8deb1b3
commit 3e6a8b6e79
12 changed files with 673 additions and 351 deletions
+107 -38
View File
@@ -4,9 +4,18 @@ use egui::{
Slider, TextEdit, Ui, Vec2,
};
use egui_material_icons::icons;
use pwsp::types::audio_player::PlayerState;
use pwsp::types::audio_player::TrackInfo;
use pwsp::utils::gui::format_time_pair;
use std::{error::Error, path::PathBuf};
use std::{error::Error, path::PathBuf, time::Instant};
use pwsp::types::gui::AppState;
enum TrackAction {
Pause(u32),
Resume(u32),
ToggleLoop(u32),
Stop(u32),
}
impl SoundpadGui {
pub fn draw_waiting_for_daemon(&mut self, ui: &mut Ui) {
@@ -74,11 +83,24 @@ impl SoundpadGui {
fn draw_header(&mut self, ui: &mut Ui) {
ui.vertical_centered_justified(|ui| {
// Current file name
self.draw_controls(ui);
});
}
fn draw_controls(&mut self, ui: &mut Ui) {
if self.audio_player_state.tracks.is_empty() {
ui.label("No tracks playing");
return;
}
let tracks = self.audio_player_state.tracks.clone();
let mut action = None;
for track in tracks {
ui.label(
RichText::new(
self.audio_player_state
.current_file_path
track
.path
.file_stem()
.unwrap_or_default()
.to_str()
@@ -87,32 +109,76 @@ impl SoundpadGui {
.color(Color32::WHITE)
.family(FontFamily::Monospace),
);
// Media controls
self.draw_controls(ui);
if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, &track) {
action = Some(act);
}
ui.separator();
});
}
if let Some(action) = action {
match action {
TrackAction::Pause(id) => self.pause(Some(id)),
TrackAction::Resume(id) => self.resume(Some(id)),
TrackAction::ToggleLoop(id) => self.toggle_loop(Some(id)),
TrackAction::Stop(id) => self.stop(Some(id)),
}
}
}
fn draw_controls(&mut self, ui: &mut Ui) {
fn draw_track_control(
ui: &mut Ui,
app_state: &mut AppState,
track: &TrackInfo,
) -> Option<TrackAction> {
let ui_state = app_state.track_ui_states.entry(track.id).or_default();
let should_update_position = !ui_state.position_dragged
&& ui_state
.ignore_position_update_until
.map(|t| Instant::now() > t)
.unwrap_or(true);
if should_update_position {
ui_state.position_slider_value = track.position;
}
let should_update_volume = !ui_state.volume_dragged
&& ui_state
.ignore_volume_update_until
.map(|t| Instant::now() > t)
.unwrap_or(true);
if should_update_volume {
ui_state.volume_slider_value = track.volume;
}
let mut action = None;
ui.horizontal_top(|ui| {
// ---------- Play Button ----------
let play_button = Button::new(match self.audio_player_state.state {
PlayerState::Playing => icons::ICON_PAUSE,
PlayerState::Paused | PlayerState::Stopped => icons::ICON_PLAY_ARROW,
let play_button = Button::new(if track.paused {
icons::ICON_PLAY_ARROW
} else {
icons::ICON_PAUSE
})
.corner_radius(15.0);
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
if play_button_response.clicked() {
self.play_toggle();
if track.paused {
action = Some(TrackAction::Resume(track.id));
} else {
action = Some(TrackAction::Pause(track.id));
}
}
// --------------------------------
// ---------- Loop Button ----------
let loop_button = Button::new(
RichText::new(match self.audio_player_state.looped {
true => icons::ICON_REPEAT_ONE,
false => icons::ICON_REPEAT,
RichText::new(if track.looped {
icons::ICON_REPEAT_ONE
} else {
icons::ICON_REPEAT
})
.size(18.0),
)
@@ -120,17 +186,15 @@ impl SoundpadGui {
let loop_button_response = ui.add_sized([15.0, 30.0], loop_button);
if loop_button_response.clicked() {
self.toggle_loop();
action = Some(TrackAction::ToggleLoop(track.id));
}
// --------------------------------
// ---------- Position Slider ----------
let position_slider = Slider::new(
&mut self.app_state.position_slider_value,
0.0..=self.audio_player_state.duration,
)
.show_value(false)
.step_by(0.01);
let duration = track.duration.unwrap_or(1.0);
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
.show_value(false)
.step_by(0.01);
let default_slider_width = ui.spacing().slider_width;
let position_slider_width = ui.available_width()
@@ -140,27 +204,22 @@ impl SoundpadGui {
ui.spacing_mut().slider_width = position_slider_width;
let position_slider_response = ui.add_sized([30.0, 30.0], position_slider);
if position_slider_response.drag_stopped() {
self.app_state.position_dragged = true;
ui_state.position_dragged = true;
}
// --------------------------------
// ---------- Time Label ----------
let time_label = Label::new(
RichText::new(format_time_pair(
self.audio_player_state.position,
self.audio_player_state.duration,
))
.monospace(),
);
let time_label =
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
ui.add_sized([30.0, 30.0], time_label);
// --------------------------------
// ---------- Volume Icon ----------
let volume_icon = if self.audio_player_state.volume > 0.7 {
let volume_icon = if track.volume > 0.7 {
icons::ICON_VOLUME_UP
} else if self.audio_player_state.volume == 0.0 {
} else if track.volume == 0.0 {
icons::ICON_VOLUME_OFF
} else if self.audio_player_state.volume < 0.3 {
} else if track.volume < 0.3 {
icons::ICON_VOLUME_MUTE
} else {
icons::ICON_VOLUME_DOWN
@@ -170,19 +229,29 @@ impl SoundpadGui {
// --------------------------------
// ---------- Volume Slider ----------
let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0)
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
.show_value(false)
.step_by(0.01);
ui.spacing_mut().slider_width = default_slider_width;
ui.spacing_mut().slider_width = default_slider_width - 30.0;
ui.spacing_mut().item_spacing.x = 0.0;
let volume_slider_response = ui.add_sized([30.0, 30.0], volume_slider);
if volume_slider_response.drag_stopped() {
self.app_state.volume_dragged = true;
ui_state.volume_dragged = true;
}
// --------------------------------
// ---------- Stop Button ---------
let stop_button = Button::new(icons::ICON_CLOSE).frame(false);
let stop_button_response = ui.add_sized([30.0, 30.0], stop_button);
if stop_button_response.clicked() {
action = Some(TrackAction::Stop(track.id));
}
// --------------------------------
});
action
}
fn draw_body(&mut self, ui: &mut Ui) {
@@ -316,7 +385,7 @@ impl SoundpadGui {
let file_button = Button::new(file_button_text).frame(false);
let file_button_response = ui.add(file_button);
if file_button_response.clicked() {
self.play_file(&entry_path);
self.play_file(&entry_path, ui.input(|i| i.modifiers.ctrl));
self.app_state.selected_file = Some(entry_path);
}
}
+4 -1
View File
@@ -21,7 +21,10 @@ impl SoundpadGui {
}
if i.key_pressed(Key::Enter) && self.app_state.selected_file.is_some() {
self.play_file(&self.app_state.selected_file.clone().unwrap());
self.play_file(
&self.app_state.selected_file.clone().unwrap(),
i.modifiers.ctrl,
);
}
if !self.app_state.show_settings {
+20 -8
View File
@@ -59,8 +59,8 @@ impl SoundpadGui {
let (new_state, request) = {
let guard = self.audio_player_state_shared.lock().unwrap();
match guard.state {
PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause())),
PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume())),
PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause(None))),
PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume(None))),
PlayerState::Stopped => (None, None),
}
};
@@ -79,7 +79,7 @@ impl SoundpadGui {
pub fn open_file(&mut self) {
let file_dialog = FileDialog::new().add_filter("Audio File", &SUPPORTED_EXTENSIONS);
if let Some(path) = file_dialog.pick_file() {
self.play_file(&path);
self.play_file(&path, false);
}
}
@@ -116,8 +116,8 @@ impl SoundpadGui {
.collect();
}
pub fn play_file(&mut self, path: &PathBuf) {
make_request_sync(Request::play(path.to_str().unwrap())).ok();
pub fn play_file(&mut self, path: &PathBuf, concurrent: bool) {
make_request_sync(Request::play(path.to_str().unwrap(), concurrent)).ok();
}
pub fn set_input(&mut self, name: String) {
@@ -130,8 +130,20 @@ impl SoundpadGui {
}
}
pub fn toggle_loop(&mut self) {
make_request_sync(Request::toggle_loop()).ok();
pub fn toggle_loop(&mut self, id: Option<u32>) {
make_request_sync(Request::toggle_loop(id)).ok();
}
pub fn pause(&mut self, id: Option<u32>) {
make_request_sync(Request::pause(id)).ok();
}
pub fn resume(&mut self, id: Option<u32>) {
make_request_sync(Request::resume(id)).ok();
}
pub fn stop(&mut self, id: Option<u32>) {
make_request_sync(Request::stop(id)).ok();
}
}
@@ -163,7 +175,7 @@ pub async fn run() -> Result<(), Box<dyn Error>> {
Ok(_) => {
let config = get_gui_config();
if config.pause_on_exit {
make_request_sync(Request::pause()).ok();
make_request_sync(Request::pause(None)).ok();
}
Ok(())
}
+33 -34
View File
@@ -3,14 +3,43 @@ use eframe::{App, Frame as EFrame};
use egui::{CentralPanel, Context};
use pwsp::{
types::socket::Request,
utils::{
daemon::{get_daemon_config, is_daemon_running},
gui::make_request_sync,
},
utils::{daemon::is_daemon_running, gui::make_request_sync},
};
use std::time::{Duration, Instant};
impl App for SoundpadGui {
fn update(&mut self, ctx: &Context, _frame: &mut EFrame) {
let mut seek_requests = vec![];
let mut volume_requests = vec![];
for (id, ui_state) in &mut self.app_state.track_ui_states {
if ui_state.position_dragged {
seek_requests.push((*id, ui_state.position_slider_value));
}
if ui_state.volume_dragged {
volume_requests.push((*id, ui_state.volume_slider_value));
ui_state.volume_dragged = false;
}
}
for (id, pos) in seek_requests {
make_request_sync(Request::seek(pos, Some(id))).ok();
if let Some(ui_state) = self.app_state.track_ui_states.get_mut(&id) {
ui_state.position_dragged = false;
ui_state.ignore_position_update_until =
Some(Instant::now() + Duration::from_millis(200));
}
}
for (id, vol) in volume_requests {
make_request_sync(Request::set_volume(vol, Some(id))).ok();
if let Some(ui_state) = self.app_state.track_ui_states.get_mut(&id) {
ui_state.volume_dragged = false;
ui_state.ignore_volume_update_until =
Some(Instant::now() + Duration::from_millis(200));
}
}
{
let guard = self.audio_player_state_shared.lock().unwrap();
self.audio_player_state = guard.clone();
@@ -49,36 +78,6 @@ impl App for SoundpadGui {
}
});
if self.app_state.position_dragged {
make_request_sync(Request::seek(self.app_state.position_slider_value)).ok();
let mut guard = self.audio_player_state_shared.lock().unwrap();
guard.new_position = Some(self.app_state.position_slider_value);
guard.position = self.app_state.position_slider_value;
self.app_state.position_dragged = false;
} else {
self.app_state.position_slider_value = self.audio_player_state.position;
}
if self.app_state.volume_dragged {
let new_volume = self.app_state.volume_slider_value;
make_request_sync(Request::set_volume(new_volume)).ok();
let mut guard = self.audio_player_state_shared.lock().unwrap();
guard.new_volume = Some(self.app_state.volume_slider_value);
guard.volume = self.app_state.volume_slider_value;
self.app_state.volume_dragged = false;
if self.config.save_volume {
let mut daemon_config = get_daemon_config();
daemon_config.default_volume = Some(new_volume);
daemon_config.save_to_file().ok();
}
} else {
self.app_state.volume_slider_value = self.audio_player_state.volume;
}
ctx.request_repaint_after_secs(1.0 / 60.0);
}
}