diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 0caed1b..4037368 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -36,17 +36,36 @@ enum Actions { /// Ping the daemon Ping, /// Pause audio playback - Pause, + Pause { + #[clap(short, long)] + id: Option, + }, /// Resume audio playback - Resume, + Resume { + #[clap(short, long)] + id: Option, + }, /// Toggle pause - TogglePause, + TogglePause { + #[clap(short, long)] + id: Option, + }, /// Stop audio playback and clear the queue - Stop, + Stop { + #[clap(short, long)] + id: Option, + }, /// Play a file - Play { file_path: PathBuf }, + Play { + file_path: PathBuf, + #[clap(short, long)] + concurrent: bool, + }, /// Toggle loop - ToggleLoop, + ToggleLoop { + #[clap(short, long)] + id: Option, + }, } #[derive(Subcommand, Debug)] @@ -56,31 +75,47 @@ enum GetCommands { /// Playback volume Volume, /// Playback position (in seconds) - Position, + Position { + #[clap(short, long)] + id: Option, + }, /// Duration of the current file - Duration, + Duration { + #[clap(short, long)] + id: Option, + }, /// Player state (Playing, Paused or Stopped) State, - /// Current playing file path - CurrentFilePath, + /// Get all playing tracks + Tracks, /// Current audio input Input, /// All audio inputs Inputs, - /// Is loop enabled (true or false) - Loop, } #[derive(Subcommand, Debug)] enum SetCommands { /// Playback volume - Volume { volume: f32 }, + Volume { + volume: f32, + #[clap(short, long)] + id: Option, + }, /// Playback position (in seconds) - Position { position: f32 }, + Position { + position: f32, + #[clap(short, long)] + id: Option, + }, /// Audio input id (see pwsp-cli get inputs) Input { name: String }, /// Enable or disable loop (true or false) - Loop { enabled: String }, + Loop { + enabled: String, + #[clap(short, long)] + id: Option, + }, } #[tokio::main] @@ -92,29 +127,31 @@ async fn main() -> Result<(), Box> { let request = match cli.command { Commands::Action { action } => match action { Actions::Ping => Request::ping(), - Actions::Pause => Request::pause(), - Actions::Resume => Request::resume(), - Actions::TogglePause => Request::toggle_pause(), - Actions::Stop => Request::stop(), - Actions::Play { file_path } => Request::play(file_path.to_str().unwrap()), - Actions::ToggleLoop => Request::toggle_loop(), + Actions::Pause { id } => Request::pause(id), + Actions::Resume { id } => Request::resume(id), + Actions::TogglePause { id } => Request::toggle_pause(id), + Actions::Stop { id } => Request::stop(id), + Actions::Play { + file_path, + concurrent, + } => Request::play(file_path.to_str().unwrap(), concurrent), + Actions::ToggleLoop { id } => Request::toggle_loop(id), }, Commands::Get { parameter } => match parameter { GetCommands::IsPaused => Request::get_is_paused(), GetCommands::Volume => Request::get_volume(), - GetCommands::Position => Request::get_position(), - GetCommands::Duration => Request::get_duration(), + GetCommands::Position { id } => Request::get_position(id), + GetCommands::Duration { id } => Request::get_duration(id), GetCommands::State => Request::get_state(), - GetCommands::CurrentFilePath => Request::get_current_file_path(), + GetCommands::Tracks => Request::get_tracks(), GetCommands::Input => Request::get_input(), GetCommands::Inputs => Request::get_inputs(), - GetCommands::Loop => Request::get_loop(), }, Commands::Set { parameter } => match parameter { - SetCommands::Volume { volume } => Request::set_volume(volume), - SetCommands::Position { position } => Request::seek(position), + SetCommands::Volume { volume, id } => Request::set_volume(volume, id), + SetCommands::Position { position, id } => Request::seek(position, id), SetCommands::Input { name } => Request::set_input(&name), - SetCommands::Loop { enabled } => Request::set_loop(&enabled), + SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id), }, }; diff --git a/src/bin/daemon.rs b/src/bin/daemon.rs index 60a6880..af70969 100644 --- a/src/bin/daemon.rs +++ b/src/bin/daemon.rs @@ -1,8 +1,5 @@ use pwsp::{ - types::{ - audio_player::PlayerState, - socket::{Request, Response}, - }, + types::socket::{Request, Response}, utils::{ commands::parse_command, daemon::{ @@ -122,18 +119,7 @@ async fn player_loop() { loop { let mut audio_player = get_audio_player().await.lock().await; - // Start playback again if loop is enabled - let should_play = audio_player.get_state() == PlayerState::Stopped - && audio_player.current_file_path.is_some() - && audio_player.looped; - - if should_play { - let file_path = audio_player.current_file_path.clone().unwrap(); - audio_player - .play(&file_path) - .await - .expect("Something went wrong while trying to play the file"); - } + audio_player.update().await; sleep(Duration::from_millis(100)).await; } diff --git a/src/gui/draw.rs b/src/gui/draw.rs index 738d982..f733be2 100644 --- a/src/gui/draw.rs +++ b/src/gui/draw.rs @@ -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 { + 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); } } diff --git a/src/gui/input.rs b/src/gui/input.rs index b871913..ddce162 100644 --- a/src/gui/input.rs +++ b/src/gui/input.rs @@ -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 { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index ee00657..bb1934c 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -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) { + make_request_sync(Request::toggle_loop(id)).ok(); + } + + pub fn pause(&mut self, id: Option) { + make_request_sync(Request::pause(id)).ok(); + } + + pub fn resume(&mut self, id: Option) { + make_request_sync(Request::resume(id)).ok(); + } + + pub fn stop(&mut self, id: Option) { + make_request_sync(Request::stop(id)).ok(); } } @@ -163,7 +175,7 @@ pub async fn run() -> Result<(), Box> { 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(()) } diff --git a/src/gui/update.rs b/src/gui/update.rs index ac8a83d..935a8d4 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -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); } } diff --git a/src/types/audio_player.rs b/src/types/audio_player.rs index 0c35612..508f686 100644 --- a/src/types/audio_player.rs +++ b/src/types/audio_player.rs @@ -8,6 +8,7 @@ use crate::{ use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source}; use serde::{Deserialize, Serialize}; use std::{ + collections::HashMap, error::Error, fs, path::{Path, PathBuf}, @@ -22,19 +23,35 @@ pub enum PlayerState { Playing, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrackInfo { + pub id: u32, + pub path: PathBuf, + pub duration: Option, + pub position: f32, + pub volume: f32, + pub looped: bool, + pub paused: bool, +} + +pub struct PlayingSound { + pub id: u32, + pub sink: Sink, + pub path: PathBuf, + pub duration: Option, + pub looped: bool, + pub volume: f32, +} + pub struct AudioPlayer { - _stream_handle: OutputStream, - sink: Sink, + pub stream_handle: OutputStream, + pub tracks: HashMap, + pub next_id: u32, input_link_sender: Option>, pub current_input_device: Option, - pub volume: f32, - pub duration: Option, - - pub current_file_path: Option, - - pub looped: bool, + pub volume: f32, // Master volume } impl AudioPlayer { @@ -50,22 +67,16 @@ impl AudioPlayer { } let stream_handle = OutputStreamBuilder::open_default_stream()?; - let sink = Sink::connect_new(stream_handle.mixer()); - sink.set_volume(default_volume); let mut audio_player = AudioPlayer { - _stream_handle: stream_handle, - sink, + stream_handle, + tracks: HashMap::new(), + next_id: 1, input_link_sender: None, current_input_device: default_input_device.clone(), volume: default_volume, - duration: None, - - current_file_path: None, - - looped: false, }; if default_input_device.is_some() { @@ -132,74 +143,123 @@ impl AudioPlayer { Ok(()) } - pub fn pause(&mut self) { - if self.get_state() == PlayerState::Playing { - self.sink.pause(); - } - } - - pub fn resume(&mut self) { - if self.get_state() == PlayerState::Paused { - self.sink.play(); - } - } - - pub fn stop(&mut self) { - self.sink.stop(); - } - - pub fn is_paused(&self) -> bool { - self.sink.is_paused() - } - - pub fn get_state(&self) -> PlayerState { - if self.sink.len() == 0 { - return PlayerState::Stopped; - } - - if self.sink.is_paused() { - return PlayerState::Paused; - } - - PlayerState::Playing - } - - pub fn set_volume(&mut self, volume: f32) { - self.volume = volume; - self.sink.set_volume(volume); - } - - pub fn get_position(&self) -> f32 { - if self.get_state() == PlayerState::Stopped { - return 0.0; - } - - self.sink.get_pos().as_secs_f32() - } - - pub fn seek(&mut self, mut position: f32) -> Result<(), Box> { - if position < 0.0 { - position = 0.0; - } - - match self.sink.try_seek(Duration::from_secs_f32(position)) { - Ok(_) => Ok(()), - Err(err) => Err(err.into()), - } - } - - pub fn get_duration(&mut self) -> Result> { - if self.get_state() == PlayerState::Stopped { - Err("Nothing is playing right now".into()) + pub fn pause(&mut self, id: Option) { + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.sink.pause(); + } } else { - match self.duration { - Some(duration) => Ok(duration), - None => Err("Couldn't determine duration for current file".into()), + for sound in self.tracks.values_mut() { + sound.sink.pause(); } } } - pub async fn play(&mut self, file_path: &Path) -> Result<(), Box> { + pub fn resume(&mut self, id: Option) { + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.sink.play(); + } + } else { + for sound in self.tracks.values_mut() { + sound.sink.play(); + } + } + } + + pub fn stop(&mut self, id: Option) { + if let Some(id) = id { + self.tracks.remove(&id); + } else { + self.tracks.clear(); + } + } + + pub fn is_paused(&self) -> bool { + if self.tracks.is_empty() { + return false; + } + self.tracks.values().all(|s| s.sink.is_paused()) + } + + pub fn get_state(&self) -> PlayerState { + if self.tracks.is_empty() { + return PlayerState::Stopped; + } + + if self + .tracks + .values() + .any(|s| !s.sink.is_paused() && !s.sink.empty()) + { + return PlayerState::Playing; + } + + if self.is_paused() { + return PlayerState::Paused; + } + + PlayerState::Stopped + } + + pub fn set_volume(&mut self, volume: f32, id: Option) { + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.volume = volume; + sound.sink.set_volume(self.volume * volume); + } + } else { + self.volume = volume; + for sound in self.tracks.values_mut() { + sound.sink.set_volume(self.volume * sound.volume); + } + } + } + + pub fn get_position(&self, id: Option) -> f32 { + if let Some(id) = id { + if let Some(sound) = self.tracks.get(&id) { + return sound.sink.get_pos().as_secs_f32(); + } + } else if let Some(sound) = self.tracks.values().last() { + // Fallback to last added track if no ID + return sound.sink.get_pos().as_secs_f32(); + } + 0.0 + } + + pub fn seek(&mut self, position: f32, id: Option) -> Result<(), Box> { + let position = if position < 0.0 { 0.0 } else { position }; + + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.sink.try_seek(Duration::from_secs_f32(position))?; + } + } else { + // Seek all? Or last? Let's seek all for now if no ID provided + for sound in self.tracks.values_mut() { + sound.sink.try_seek(Duration::from_secs_f32(position)).ok(); + } + } + Ok(()) + } + + pub fn get_duration(&mut self, id: Option) -> Result> { + if let Some(id) = id { + if let Some(sound) = self.tracks.get(&id) { + return sound.duration.ok_or("Unknown duration".into()); + } + } else if let Some(sound) = self.tracks.values().last() { + return sound.duration.ok_or("Unknown duration".into()); + } + Err("No track playing".into()) + } + + pub async fn play( + &mut self, + file_path: &Path, + concurrent: bool, + ) -> Result> { if !file_path.exists() { return Err(format!("File does not exist: {}", file_path.display()).into()); } @@ -207,30 +267,90 @@ impl AudioPlayer { let file = fs::File::open(file_path)?; match Decoder::try_from(file) { Ok(source) => { - self.current_file_path = Some(file_path.to_path_buf()); - - if let Some(duration) = source.total_duration() { - self.duration = Some(duration.as_secs_f32()); - } else { - self.duration = None; + if !concurrent { + self.tracks.clear(); } - self.sink.stop(); - self.sink.append(source); - self.sink.play(); + let id = self.next_id; + self.next_id += 1; + + let duration = source.total_duration().map(|d| d.as_secs_f32()); + + let sink = Sink::connect_new(self.stream_handle.mixer()); + sink.set_volume(self.volume); // Default volume is 1.0 * master + sink.append(source); + sink.play(); + + let sound = PlayingSound { + id, + sink, + path: file_path.to_path_buf(), + duration, + looped: false, + volume: 1.0, + }; + + self.tracks.insert(id, sound); + self.link_devices().await?; - Ok(()) + Ok(id) } Err(err) => Err(err.into()), } } - pub fn get_current_file_path(&mut self) -> &Option { - if self.get_state() == PlayerState::Stopped && !self.looped { - self.current_file_path = None; + pub fn set_loop(&mut self, enabled: bool, id: Option) { + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.looped = enabled; + } + } else { + // Set loop for all? Or just last? + // Let's set for all. + for sound in self.tracks.values_mut() { + sound.looped = enabled; + } } - &self.current_file_path + } + + pub fn get_tracks(&self) -> Vec { + self.tracks + .values() + .map(|sound| TrackInfo { + id: sound.id, + path: sound.path.clone(), + duration: sound.duration, + position: sound.sink.get_pos().as_secs_f32(), + volume: sound.volume, + looped: sound.looped, + paused: sound.sink.is_paused(), + }) + .collect() + } + + pub async fn update(&mut self) { + let mut restarts = vec![]; + + for (id, sound) in &self.tracks { + if sound.sink.empty() && sound.looped { + restarts.push(*id); + } + } + + for id in restarts { + if let Some(sound) = self.tracks.get_mut(&id) { + if let Ok(file) = fs::File::open(&sound.path) { + if let Ok(source) = Decoder::try_from(file) { + sound.sink.append(source); + sound.sink.play(); + } + } + } + } + + self.tracks + .retain(|_, sound| !sound.sink.empty() || sound.looped); } pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box> { diff --git a/src/types/commands.rs b/src/types/commands.rs index d6b2a67..d0a66d1 100644 --- a/src/types/commands.rs +++ b/src/types/commands.rs @@ -12,13 +12,21 @@ pub trait Executable { pub struct PingCommand {} -pub struct PauseCommand {} +pub struct PauseCommand { + pub id: Option, +} -pub struct ResumeCommand {} +pub struct ResumeCommand { + pub id: Option, +} -pub struct TogglePauseCommand {} +pub struct TogglePauseCommand { + pub id: Option, +} -pub struct StopCommand {} +pub struct StopCommand { + pub id: Option, +} pub struct IsPausedCommand {} @@ -28,21 +36,28 @@ pub struct GetVolumeCommand {} pub struct SetVolumeCommand { pub volume: Option, + pub id: Option, } -pub struct GetPositionCommand {} +pub struct GetPositionCommand { + pub id: Option, +} pub struct SeekCommand { pub position: Option, + pub id: Option, } -pub struct GetDurationCommand {} +pub struct GetDurationCommand { + pub id: Option, +} pub struct PlayCommand { pub file_path: Option, + pub concurrent: Option, } -pub struct GetCurrentFilePathCommand {} +pub struct GetTracksCommand {} pub struct GetCurrentInputCommand {} @@ -52,13 +67,14 @@ pub struct SetCurrentInputCommand { pub name: Option, } -pub struct GetLoopCommand {} - pub struct SetLoopCommand { pub enabled: Option, + pub id: Option, } -pub struct ToggleLoopCommand {} +pub struct ToggleLoopCommand { + pub id: Option, +} #[async_trait] impl Executable for PingCommand { @@ -71,7 +87,7 @@ impl Executable for PingCommand { impl Executable for PauseCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - audio_player.pause(); + audio_player.pause(self.id); Response::new(true, "Audio was paused") } } @@ -80,7 +96,7 @@ impl Executable for PauseCommand { impl Executable for ResumeCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - audio_player.resume(); + audio_player.resume(self.id); Response::new(true, "Audio was resumed") } } @@ -94,12 +110,31 @@ impl Executable for TogglePauseCommand { return Response::new(false, "Audio is not playing"); } - if audio_player.is_paused() { - audio_player.resume(); - Response::new(true, "Audio was resumed") + // This logic is a bit tricky with multiple tracks. + // If ID is provided, toggle that track. + // If not, toggle global pause state? + // For now, let's just use pause/resume based on global state if no ID. + + if let Some(id) = self.id { + if let Some(track) = audio_player.tracks.get(&id) { + if track.sink.is_paused() { + audio_player.resume(Some(id)); + Response::new(true, "Audio was resumed") + } else { + audio_player.pause(Some(id)); + Response::new(true, "Audio was paused") + } + } else { + Response::new(false, "Track not found") + } } else { - audio_player.pause(); - Response::new(true, "Audio was paused") + if audio_player.is_paused() { + audio_player.resume(None); + Response::new(true, "Audio was resumed") + } else { + audio_player.pause(None); + Response::new(true, "Audio was paused") + } } } } @@ -108,7 +143,7 @@ impl Executable for TogglePauseCommand { impl Executable for StopCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - audio_player.stop(); + audio_player.stop(self.id); Response::new(true, "Audio was stopped") } } @@ -145,7 +180,7 @@ impl Executable for SetVolumeCommand { async fn execute(&self) -> Response { if let Some(volume) = self.volume { let mut audio_player = get_audio_player().await.lock().await; - audio_player.set_volume(volume); + audio_player.set_volume(volume, self.id); Response::new(true, format!("Audio volume was set to {}", volume)) } else { Response::new(false, "Invalid volume value") @@ -157,7 +192,7 @@ impl Executable for SetVolumeCommand { impl Executable for GetPositionCommand { async fn execute(&self) -> Response { let audio_player = get_audio_player().await.lock().await; - let position = audio_player.get_position(); + let position = audio_player.get_position(self.id); Response::new(true, position.to_string()) } } @@ -167,7 +202,7 @@ impl Executable for SeekCommand { async fn execute(&self) -> Response { if let Some(position) = self.position { let mut audio_player = get_audio_player().await.lock().await; - match audio_player.seek(position) { + match audio_player.seek(position, self.id) { Ok(_) => Response::new(true, format!("Audio position was set to {}", position)), Err(err) => Response::new(false, err.to_string()), } @@ -181,7 +216,7 @@ impl Executable for SeekCommand { impl Executable for GetDurationCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - match audio_player.get_duration() { + match audio_player.get_duration(self.id) { Ok(duration) => Response::new(true, duration.to_string()), Err(err) => Response::new(false, err.to_string()), } @@ -193,8 +228,11 @@ impl Executable for PlayCommand { async fn execute(&self) -> Response { if let Some(file_path) = &self.file_path { let mut audio_player = get_audio_player().await.lock().await; - match audio_player.play(file_path).await { - Ok(_) => Response::new(true, format!("Now playing {}", file_path.display())), + match audio_player + .play(file_path, self.concurrent.unwrap_or(false)) + .await + { + Ok(id) => Response::new(true, id.to_string()), Err(err) => Response::new(false, err.to_string()), } } else { @@ -204,15 +242,11 @@ impl Executable for PlayCommand { } #[async_trait] -impl Executable for GetCurrentFilePathCommand { +impl Executable for GetTracksCommand { async fn execute(&self) -> Response { - let mut audio_player = get_audio_player().await.lock().await; - let current_file_path = audio_player.get_current_file_path(); - if let Some(current_file_path) = current_file_path { - Response::new(true, current_file_path.to_str().unwrap()) - } else { - Response::new(false, "No file is playing") - } + let audio_player = get_audio_player().await.lock().await; + let tracks = audio_player.get_tracks(); + Response::new(true, serde_json::to_string(&tracks).unwrap()) } } @@ -265,14 +299,6 @@ impl Executable for SetCurrentInputCommand { } } -#[async_trait] -impl Executable for GetLoopCommand { - async fn execute(&self) -> Response { - let audio_player = get_audio_player().await.lock().await; - Response::new(true, audio_player.looped.to_string()) - } -} - #[async_trait] impl Executable for SetLoopCommand { async fn execute(&self) -> Response { @@ -280,7 +306,7 @@ impl Executable for SetLoopCommand { match self.enabled { Some(enabled) => { - audio_player.looped = enabled; + audio_player.set_loop(enabled, self.id); Response::new(true, format!("Loop was set to {}", enabled)) } None => Response::new(false, "Invalid enabled value"), @@ -292,7 +318,19 @@ impl Executable for SetLoopCommand { impl Executable for ToggleLoopCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - audio_player.looped = !audio_player.looped; - Response::new(true, format!("Loop was set to {}", audio_player.looped)) + if let Some(id) = self.id { + if let Some(track) = audio_player.tracks.get_mut(&id) { + track.looped = !track.looped; + Response::new(true, format!("Loop was set to {}", track.looped)) + } else { + Response::new(false, "Track not found") + } + } else { + // Toggle all? + for track in audio_player.tracks.values_mut() { + track.looped = !track.looped; + } + Response::new(true, "Loop toggled for all tracks") + } } } diff --git a/src/types/gui.rs b/src/types/gui.rs index 0fbe641..82f8b15 100644 --- a/src/types/gui.rs +++ b/src/types/gui.rs @@ -1,21 +1,28 @@ -use crate::types::audio_player::PlayerState; +use crate::types::audio_player::{PlayerState, TrackInfo}; use egui::Id; use std::{ collections::{HashMap, HashSet}, path::PathBuf, + time::Instant, }; +#[derive(Default, Debug)] +pub struct TrackUiState { + pub position_slider_value: f32, + pub volume_slider_value: f32, + pub position_dragged: bool, + pub volume_dragged: bool, + pub ignore_position_update_until: Option, + pub ignore_volume_update_until: Option, +} + #[derive(Default, Debug)] pub struct AppState { pub search_query: String, - pub position_slider_value: f32, - pub volume_slider_value: f32, - - pub position_dragged: bool, - pub volume_dragged: bool, + pub track_ui_states: HashMap, pub show_settings: bool, @@ -33,6 +40,9 @@ pub struct AppState { pub struct AudioPlayerState { pub state: PlayerState, pub new_state: Option, + + pub tracks: Vec, + pub current_file_path: PathBuf, pub is_paused: bool, diff --git a/src/types/socket.rs b/src/types/socket.rs index ac5db2d..1f35810 100644 --- a/src/types/socket.rs +++ b/src/types/socket.rs @@ -24,24 +24,54 @@ impl Request { Request::new("ping", vec![]) } - pub fn pause() -> Self { - Request::new("pause", vec![]) + pub fn pause(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("pause", args) } - pub fn resume() -> Self { - Request::new("resume", vec![]) + pub fn resume(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("resume", args) } - pub fn toggle_pause() -> Self { - Request::new("toggle_pause", vec![]) + pub fn toggle_pause(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("toggle_pause", args) } - pub fn stop() -> Self { - Request::new("stop", vec![]) + pub fn stop(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("stop", args) } - pub fn play(file_path: &str) -> Self { - Request::new("play", vec![("file_path", file_path)]) + pub fn play(file_path: &str, concurrent: bool) -> Self { + Request::new( + "play", + vec![ + ("file_path", file_path), + ("concurrent", &concurrent.to_string()), + ], + ) } pub fn get_is_paused() -> Self { @@ -52,20 +82,32 @@ impl Request { Request::new("get_volume", vec![]) } - pub fn get_position() -> Self { - Request::new("get_position", vec![]) + pub fn get_position(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("get_position", args) } - pub fn get_duration() -> Self { - Request::new("get_duration", vec![]) + pub fn get_duration(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("get_duration", args) } pub fn get_state() -> Self { Request::new("get_state", vec![]) } - pub fn get_current_file_path() -> Self { - Request::new("get_current_file_path", vec![]) + pub fn get_tracks() -> Self { + Request::new("get_tracks", vec![]) } pub fn get_input() -> Self { @@ -76,28 +118,42 @@ impl Request { Request::new("get_inputs", vec![]) } - pub fn set_volume(volume: f32) -> Self { - Request::new("set_volume", vec![("volume", &volume.to_string())]) + pub fn set_volume(volume: f32, id: Option) -> Self { + let mut args = vec![("volume".to_string(), volume.to_string())]; + if let Some(id) = id { + args.push(("id".to_string(), id.to_string())); + } + Request::new("set_volume".to_string(), args) } - pub fn seek(position: f32) -> Self { - Request::new("seek", vec![("position", &position.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 { + args.push(("id".to_string(), id.to_string())); + } + Request::new("seek".to_string(), args) } pub fn set_input(name: &str) -> Self { Request::new("set_input", vec![("input_name", name)]) } - pub fn get_loop() -> Self { - Request::new("get_loop", vec![]) + pub fn set_loop(enabled: &str, id: Option) -> Self { + let mut args = vec![("enabled".to_string(), enabled.to_string())]; + if let Some(id) = id { + args.push(("id".to_string(), id.to_string())); + } + Request::new("set_loop".to_string(), args) } - pub fn set_loop(enabled: &str) -> Self { - Request::new("set_loop", vec![("enabled", enabled)]) - } - - pub fn toggle_loop() -> Self { - Request::new("toggle_loop", vec![]) + pub fn toggle_loop(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("toggle_loop", args) } } diff --git a/src/utils/commands.rs b/src/utils/commands.rs index 46b2889..0e6e7c3 100644 --- a/src/utils/commands.rs +++ b/src/utils/commands.rs @@ -3,12 +3,14 @@ use crate::types::{commands::*, socket::Request}; use std::path::PathBuf; pub fn parse_command(request: &Request) -> Option> { + let id = request.args.get("id").and_then(|s| s.parse::().ok()); + match request.name.as_str() { "ping" => Some(Box::new(PingCommand {})), - "pause" => Some(Box::new(PauseCommand {})), - "resume" => Some(Box::new(ResumeCommand {})), - "toggle_pause" => Some(Box::new(TogglePauseCommand {})), - "stop" => Some(Box::new(StopCommand {})), + "pause" => Some(Box::new(PauseCommand { id })), + "resume" => Some(Box::new(ResumeCommand { id })), + "toggle_pause" => Some(Box::new(TogglePauseCommand { id })), + "stop" => Some(Box::new(StopCommand { id })), "is_paused" => Some(Box::new(IsPausedCommand {})), "get_state" => Some(Box::new(GetStateCommand {})), "get_volume" => Some(Box::new(GetVolumeCommand {})), @@ -19,9 +21,9 @@ pub fn parse_command(request: &Request) -> Option> { .unwrap_or(&String::new()) .parse::() .ok(); - Some(Box::new(SetVolumeCommand { volume })) + Some(Box::new(SetVolumeCommand { volume, id })) } - "get_position" => Some(Box::new(GetPositionCommand {})), + "get_position" => Some(Box::new(GetPositionCommand { id })), "seek" => { let position = request .args @@ -29,9 +31,9 @@ pub fn parse_command(request: &Request) -> Option> { .unwrap_or(&String::new()) .parse::() .ok(); - Some(Box::new(SeekCommand { position })) + Some(Box::new(SeekCommand { position, id })) } - "get_duration" => Some(Box::new(GetDurationCommand {})), + "get_duration" => Some(Box::new(GetDurationCommand { id })), "play" => { let file_path = request .args @@ -39,16 +41,24 @@ pub fn parse_command(request: &Request) -> Option> { .unwrap_or(&String::new()) .parse::() .ok(); - Some(Box::new(PlayCommand { file_path })) + let concurrent = request + .args + .get("concurrent") + .unwrap_or(&String::new()) + .parse::() + .ok(); + Some(Box::new(PlayCommand { + file_path, + concurrent, + })) } - "get_current_file_path" => Some(Box::new(GetCurrentFilePathCommand {})), + "get_tracks" => Some(Box::new(GetTracksCommand {})), "get_input" => Some(Box::new(GetCurrentInputCommand {})), "get_inputs" => Some(Box::new(GetAllInputsCommand {})), "set_input" => { let name = Some(request.args.get("input_name").unwrap_or(&String::new())).cloned(); Some(Box::new(SetCurrentInputCommand { name })) } - "get_loop" => Some(Box::new(GetLoopCommand {})), "set_loop" => { let enabled = request .args @@ -56,9 +66,9 @@ pub fn parse_command(request: &Request) -> Option> { .unwrap_or(&String::new()) .parse::() .ok(); - Some(Box::new(SetLoopCommand { enabled })) + Some(Box::new(SetLoopCommand { enabled, id })) } - "toggle_loop" => Some(Box::new(ToggleLoopCommand {})), + "toggle_loop" => Some(Box::new(ToggleLoopCommand { id })), _ => None, } } diff --git a/src/utils/gui.rs b/src/utils/gui.rs index 81544c7..011f962 100644 --- a/src/utils/gui.rs +++ b/src/utils/gui.rs @@ -1,6 +1,6 @@ use crate::{ types::{ - audio_player::PlayerState, + audio_player::{PlayerState, TrackInfo}, config::GuiConfig, gui::AudioPlayerState, socket::{Request, Response}, @@ -50,56 +50,47 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc serde_json::from_str::(&state_res.message).unwrap(), false => PlayerState::default(), }; - let file_path = match file_path_res.status { - true => PathBuf::from(file_path_res.message), - false => PathBuf::new(), + let tracks = match tracks_res.status { + true => { + serde_json::from_str::>(&tracks_res.message).unwrap_or_default() + } + false => vec![], }; + let is_paused = match is_paused_res.status { true => is_paused_res.message == "true", false => false, @@ -108,14 +99,6 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc volume_res.message.parse::().unwrap(), false => 0.0, }; - let position = match position_res.status { - true => position_res.message.parse::().unwrap(), - false => 0.0, - }; - let duration = match duration_res.status { - true => duration_res.message.parse::().unwrap(), - false => 0.0, - }; let current_input = match current_input_res.status { true => current_input_res .message @@ -144,10 +127,6 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc>(), false => HashMap::new(), }; - let looped = match looped_res.status { - true => looped_res.message.parse::().unwrap_or_default(), - false => false, - }; { let mut guard = audio_player_state_shared.lock().unwrap(); @@ -159,7 +138,19 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc state, }; - guard.current_file_path = file_path; + guard.tracks = tracks.clone(); + if let Some(last_track) = tracks.last() { + guard.current_file_path = last_track.path.clone(); + guard.position = last_track.position; + guard.duration = last_track.duration.unwrap_or(1.0); + guard.looped = last_track.looped; + } else { + guard.current_file_path = PathBuf::new(); + guard.position = 0.0; + guard.duration = 1.0; + guard.looped = false; + } + guard.is_paused = is_paused; guard.volume = match guard.new_volume { Some(new_volume) => { @@ -168,17 +159,8 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc volume, }; - guard.position = match guard.new_position { - Some(new_position) => { - guard.new_position = None; - new_position - } - None => position, - }; - guard.duration = if duration > 0.0 { duration } else { 1.0 }; guard.current_input = current_input; guard.all_inputs = all_inputs; - guard.looped = looped; } sleep(sleep_duration).await;