mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-04-28 06:21:23 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f33c48bcc | |||
| 1f48dad71c | |||
| 8066f6d96b | |||
| 5c3bc30a6e | |||
| 90ed05d88d | |||
| f59050ef04 | |||
| bae10edc99 | |||
| cfa2681ba3 | |||
| 03e936ac34 | |||
| f7f96abcbb | |||
| 4d54443593 | |||
| 5afe3dd45b | |||
| bd75ac6190 | |||
| 3e6a8b6e79 | |||
| c1c8deb1b3 | |||
| dc61342af8 | |||
| fded7f7b3f | |||
| ec83680333 | |||
| 16e94e71d3 | |||
| 56040934e9 | |||
| 89975cb124 | |||
| 03e20ffa7e | |||
| 8f7ea09ef5 | |||
| c639721f2a | |||
| 84512efda8 | |||
| 6cf4a9744d | |||
| d9ced4e650 | |||
| c1f3f34169 | |||
| 6c06da7b0d | |||
| 023caa4616 | |||
| fc593f8a8e | |||
| f281ad5a78 | |||
| fe8fec4ea7 | |||
| 7583e72687 | |||
| e4b4a3d198 | |||
| a7af1caace | |||
| c5086189d6 | |||
| ac667f1273 | |||
| fdb79e609f | |||
| 531a49dbdf | |||
| 2a13c25230 | |||
| 408f5317c4 | |||
| 3672d6f43e | |||
| cc3bec0746 |
Generated
+150
-730
File diff suppressed because it is too large
Load Diff
+5
-6
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pwsp"
|
name = "pwsp"
|
||||||
version = "1.2.0"
|
version = "1.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["arabian"]
|
authors = ["arabian"]
|
||||||
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
|
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
|
||||||
@@ -12,19 +12,18 @@ keywords = ["soundpad", "pipewire", "linux", "cli", "gui"]
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
futures = { version = "0.3.31", features = ["thread-pool"] }
|
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
|
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.149"
|
||||||
|
|
||||||
clap = { version = "4.5.53", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] }
|
clap = { version = "4.5.54", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
|
|
||||||
rodio = { version = "0.21.1", default-features = false, features = ["symphonia-all", "playback"] }
|
rodio = { version = "0.21.1", default-features = false, features = ["symphonia-all", "playback"] }
|
||||||
pipewire = "0.9.2"
|
pipewire = "0.9.2"
|
||||||
rfd = "0.16.0"
|
rfd = { version = "0.17.2", default-features = false, features = ["xdg-portal"]}
|
||||||
|
|
||||||
egui = { version = "0.33.3", default-features = false, features = ["default_fonts", "rayon"] }
|
egui = { version = "0.33.3", default-features = false, features = ["default_fonts", "rayon"] }
|
||||||
eframe = { version = "0.33.3", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
|
eframe = { version = "0.33.3", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ three main components:
|
|||||||
* Creating and managing virtual audio devices.
|
* Creating and managing virtual audio devices.
|
||||||
* Linking these devices within the PipeWire graph.
|
* Linking these devices within the PipeWire graph.
|
||||||
* Handling all audio playback.
|
* Handling all audio playback.
|
||||||
* **pwsp-gui**: This is the graphical user interface. It acts as a client that communicates with pwsp-daemon via a *
|
* **pwsp-gui**: This is the graphical user interface. It acts as a client that communicates with pwsp-daemon via a
|
||||||
*UnixSocket**. This is how you interact with your sound collection, control playback, and configure settings.
|
**UnixSocket**. This is how you interact with your sound collection, control playback, and configure settings.
|
||||||
* **pwsp-cli**: This is the command-line interface, also acting as a client. It provides a way to control the daemon
|
* **pwsp-cli**: This is the command-line interface, also acting as a client. It provides a way to control the daemon
|
||||||
without a GUI, allowing for scripting or quick command-based actions.
|
without a GUI, allowing for scripting or quick command-based actions.
|
||||||
|
|
||||||
@@ -44,10 +44,10 @@ three main components:
|
|||||||
|
|
||||||
## **Pre-built Packages**
|
## **Pre-built Packages**
|
||||||
|
|
||||||
You can download pre-built binaries, .deb and .rpm packages from
|
You can download pre-built binaries and .deb packages from
|
||||||
the [releases page](https://github.com/arabianq/pipewire-soundpad/releases).
|
the [releases page](https://github.com/arabianq/pipewire-soundpad/releases).
|
||||||
|
|
||||||
## **Fedora Linux**
|
## **Fedora Linux (and derivatives)**
|
||||||
|
|
||||||
If you're using Fedora, you can install PWSP from a dedicated repository using DNF.
|
If you're using Fedora, you can install PWSP from a dedicated repository using DNF.
|
||||||
|
|
||||||
@@ -137,12 +137,12 @@ You can start the daemon from the terminal or enable the systemd service for aut
|
|||||||
|
|
||||||
### **Using the GUI**
|
### **Using the GUI**
|
||||||
|
|
||||||
1. **Add Sounds**: Click the **"Add Directory"** button and select a folder containing your audio files. The application
|
1. **Add Sounds**: Click the **"+"** button and select a folder containing your audio files. The application
|
||||||
will automatically list all supported files.
|
will automatically list all supported files.
|
||||||
2. **Select Microphone**: In the main application window, select your **physical microphone**. PWSP will automatically
|
2. **Select Microphone**: In the main application window, select your microphone. PWSP will automatically
|
||||||
create a virtual microphone and feed it sound from two sources: **your microphone** and the **audio files**.
|
create a virtual microphone and feed it sound from two sources: **your microphone** and the **audio files**.
|
||||||
3. **Playback**: Click on a file in the list to load it, then use the **"Play"** and **"Pause"** buttons to control
|
3. **Playback**: Click on a file in the list to load it, then use the **"Play"** and **"Pause"** buttons to control
|
||||||
playback.
|
playback. You can also play single file once using **"Play File"** button.
|
||||||
|
|
||||||
### **Using the CLI**
|
### **Using the CLI**
|
||||||
|
|
||||||
@@ -173,6 +173,29 @@ pwsp-cli --help
|
|||||||
pwsp-cli set position 20
|
pwsp-cli set position 20
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### **Hotkeys & Controls**
|
||||||
|
|
||||||
|
#### **Keyboard Shortcuts**
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
| :----------------------- | :--------------------------------------------------- |
|
||||||
|
| **Esc** | Close application |
|
||||||
|
| **Space** | Pause / Resume audio |
|
||||||
|
| **Backspace** | Stop all audio tracks |
|
||||||
|
| **Enter** | Play selected file (stops all other tracks) |
|
||||||
|
| **Ctrl + Enter** | Add selected file to playback (plays simultaneously) |
|
||||||
|
| **Shift + Enter** | Replace the last added track with the selected one |
|
||||||
|
| **I** | Open / Close settings |
|
||||||
|
| **/** | Focus search field |
|
||||||
|
| **Ctrl + ↑ / ↓** | Navigate through files |
|
||||||
|
| **Ctrl + Shift + ↑ / ↓** | Navigate through directories |
|
||||||
|
|
||||||
|
#### **Mouse Controls**
|
||||||
|
|
||||||
|
* **Left Click**: Play track (stops all other tracks).
|
||||||
|
* **Ctrl + Left Click**: Add track (plays simultaneously with current tracks).
|
||||||
|
* **Shift + Left Click**: Replace the last added track with the selected one.
|
||||||
|
|
||||||
# **🤝 Contributing**
|
# **🤝 Contributing**
|
||||||
|
|
||||||
Contributions are welcome\! If you have ideas for improvements or find a bug, feel free to create
|
Contributions are welcome\! If you have ideas for improvements or find a bug, feel free to create
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 70 KiB |
@@ -4,7 +4,7 @@
|
|||||||
%global cargo_install_lib 0
|
%global cargo_install_lib 0
|
||||||
|
|
||||||
Name: pwsp
|
Name: pwsp
|
||||||
Version: 1.2.0
|
Version: 1.4.0
|
||||||
Release: %autorelease
|
Release: %autorelease
|
||||||
Summary: Lets you play audio files through your microphone
|
Summary: Lets you play audio files through your microphone
|
||||||
|
|
||||||
|
|||||||
+70
-22
@@ -36,15 +36,36 @@ enum Actions {
|
|||||||
/// Ping the daemon
|
/// Ping the daemon
|
||||||
Ping,
|
Ping,
|
||||||
/// Pause audio playback
|
/// Pause audio playback
|
||||||
Pause,
|
Pause {
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
/// Resume audio playback
|
/// Resume audio playback
|
||||||
Resume,
|
Resume {
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
/// Toggle pause
|
/// Toggle pause
|
||||||
TogglePause,
|
TogglePause {
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
/// Stop audio playback and clear the queue
|
/// Stop audio playback and clear the queue
|
||||||
Stop,
|
Stop {
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
/// Play a file
|
/// Play a file
|
||||||
Play { file_path: PathBuf },
|
Play {
|
||||||
|
file_path: PathBuf,
|
||||||
|
#[clap(short, long)]
|
||||||
|
concurrent: bool,
|
||||||
|
},
|
||||||
|
/// Toggle loop
|
||||||
|
ToggleLoop {
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
@@ -54,13 +75,19 @@ enum GetCommands {
|
|||||||
/// Playback volume
|
/// Playback volume
|
||||||
Volume,
|
Volume,
|
||||||
/// Playback position (in seconds)
|
/// Playback position (in seconds)
|
||||||
Position,
|
Position {
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
/// Duration of the current file
|
/// Duration of the current file
|
||||||
Duration,
|
Duration {
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
/// Player state (Playing, Paused or Stopped)
|
/// Player state (Playing, Paused or Stopped)
|
||||||
State,
|
State,
|
||||||
/// Current playing file path
|
/// Get all playing tracks
|
||||||
CurrentFilePath,
|
Tracks,
|
||||||
/// Current audio input
|
/// Current audio input
|
||||||
Input,
|
Input,
|
||||||
/// All audio inputs
|
/// All audio inputs
|
||||||
@@ -70,11 +97,25 @@ enum GetCommands {
|
|||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
enum SetCommands {
|
enum SetCommands {
|
||||||
/// Playback volume
|
/// Playback volume
|
||||||
Volume { volume: f32 },
|
Volume {
|
||||||
|
volume: f32,
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
/// Playback position (in seconds)
|
/// Playback position (in seconds)
|
||||||
Position { position: f32 },
|
Position {
|
||||||
|
position: f32,
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
/// Audio input id (see pwsp-cli get inputs)
|
/// Audio input id (see pwsp-cli get inputs)
|
||||||
Input { name: String },
|
Input { name: String },
|
||||||
|
/// Enable or disable loop (true or false)
|
||||||
|
Loop {
|
||||||
|
enabled: String,
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: Option<u32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -86,30 +127,37 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let request = match cli.command {
|
let request = match cli.command {
|
||||||
Commands::Action { action } => match action {
|
Commands::Action { action } => match action {
|
||||||
Actions::Ping => Request::ping(),
|
Actions::Ping => Request::ping(),
|
||||||
Actions::Pause => Request::pause(),
|
Actions::Pause { id } => Request::pause(id),
|
||||||
Actions::Resume => Request::resume(),
|
Actions::Resume { id } => Request::resume(id),
|
||||||
Actions::TogglePause => Request::toggle_pause(),
|
Actions::TogglePause { id } => Request::toggle_pause(id),
|
||||||
Actions::Stop => Request::stop(),
|
Actions::Stop { id } => Request::stop(id),
|
||||||
Actions::Play { file_path } => Request::play(file_path.to_str().unwrap()),
|
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 {
|
Commands::Get { parameter } => match parameter {
|
||||||
GetCommands::IsPaused => Request::get_is_paused(),
|
GetCommands::IsPaused => Request::get_is_paused(),
|
||||||
GetCommands::Volume => Request::get_volume(),
|
GetCommands::Volume => Request::get_volume(),
|
||||||
GetCommands::Position => Request::get_position(),
|
GetCommands::Position { id } => Request::get_position(id),
|
||||||
GetCommands::Duration => Request::get_duration(),
|
GetCommands::Duration { id } => Request::get_duration(id),
|
||||||
GetCommands::State => Request::get_state(),
|
GetCommands::State => Request::get_state(),
|
||||||
GetCommands::CurrentFilePath => Request::get_current_file_path(),
|
GetCommands::Tracks => Request::get_tracks(),
|
||||||
GetCommands::Input => Request::get_input(),
|
GetCommands::Input => Request::get_input(),
|
||||||
GetCommands::Inputs => Request::get_inputs(),
|
GetCommands::Inputs => Request::get_inputs(),
|
||||||
},
|
},
|
||||||
Commands::Set { parameter } => match parameter {
|
Commands::Set { parameter } => match parameter {
|
||||||
SetCommands::Volume { volume } => Request::set_volume(volume),
|
SetCommands::Volume { volume, id } => Request::set_volume(volume, id),
|
||||||
SetCommands::Position { position } => Request::seek(position),
|
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),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = make_request(request).await?;
|
let response = make_request(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e as Box<dyn Error>)?;
|
||||||
println!("{} : {}", response.status, response.message);
|
println!("{} : {}", response.status, response.message);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+33
-3
@@ -9,10 +9,11 @@ use pwsp::{
|
|||||||
pipewire::create_virtual_mic,
|
pipewire::create_virtual_mic,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::{error::Error, fs};
|
use std::{error::Error, fs, time::Duration};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
net::UnixListener,
|
net::UnixListener,
|
||||||
|
time::sleep,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -44,6 +45,27 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
socket_path.to_str().unwrap_or_default()
|
socket_path.to_str().unwrap_or_default()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let commands_loop_handle = tokio::spawn(async {
|
||||||
|
commands_loop(listener).await.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let player_loop_handle = tokio::spawn(async {
|
||||||
|
player_loop().await;
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = commands_loop_handle => {
|
||||||
|
eprint!("Commands loop was finished, stopping program...");
|
||||||
|
}
|
||||||
|
_ = player_loop_handle => {
|
||||||
|
eprint!("Audio Player loop was finished, stopping program...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn commands_loop(listener: UnixListener) -> Result<(), Box<dyn Error>> {
|
||||||
loop {
|
loop {
|
||||||
let (mut stream, _addr) = listener.accept().await?;
|
let (mut stream, _addr) = listener.accept().await?;
|
||||||
|
|
||||||
@@ -64,7 +86,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let request: Request = serde_json::from_slice(&buffer).unwrap();
|
let request: Request = serde_json::from_slice(&buffer).unwrap();
|
||||||
println!("Received request: {:?}", request);
|
|
||||||
// ---------- Read request (end) ----------
|
// ---------- Read request (end) ----------
|
||||||
|
|
||||||
// ---------- Generate response (start) ----------
|
// ---------- Generate response (start) ----------
|
||||||
@@ -89,8 +110,17 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
eprintln!("Failed to write response to client!");
|
eprintln!("Failed to write response to client!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
println!("Sent response: {:?}", response);
|
|
||||||
// ---------- Send response (end) ----------
|
// ---------- Send response (end) ----------
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn player_loop() {
|
||||||
|
loop {
|
||||||
|
let mut audio_player = get_audio_player().await.lock().await;
|
||||||
|
|
||||||
|
audio_player.update().await;
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+176
-44
@@ -4,11 +4,32 @@ use egui::{
|
|||||||
Slider, TextEdit, Ui, Vec2,
|
Slider, TextEdit, Ui, Vec2,
|
||||||
};
|
};
|
||||||
use egui_material_icons::icons;
|
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 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 {
|
impl SoundpadGui {
|
||||||
|
fn get_volume_icon(volume: f32) -> &'static str {
|
||||||
|
if volume > 0.7 {
|
||||||
|
icons::ICON_VOLUME_UP
|
||||||
|
} else if volume <= 0.0 {
|
||||||
|
icons::ICON_VOLUME_OFF
|
||||||
|
} else if volume < 0.3 {
|
||||||
|
icons::ICON_VOLUME_MUTE
|
||||||
|
} else {
|
||||||
|
icons::ICON_VOLUME_DOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn draw_waiting_for_daemon(&mut self, ui: &mut Ui) {
|
pub fn draw_waiting_for_daemon(&mut self, ui: &mut Ui) {
|
||||||
ui.centered_and_justified(|ui| {
|
ui.centered_and_justified(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
@@ -74,11 +95,24 @@ impl SoundpadGui {
|
|||||||
|
|
||||||
fn draw_header(&mut self, ui: &mut Ui) {
|
fn draw_header(&mut self, ui: &mut Ui) {
|
||||||
ui.vertical_centered_justified(|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(
|
ui.label(
|
||||||
RichText::new(
|
RichText::new(
|
||||||
self.audio_player_state
|
track
|
||||||
.current_file_path
|
.path
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_str()
|
.to_str()
|
||||||
@@ -87,34 +121,92 @@ impl SoundpadGui {
|
|||||||
.color(Color32::WHITE)
|
.color(Color32::WHITE)
|
||||||
.family(FontFamily::Monospace),
|
.family(FontFamily::Monospace),
|
||||||
);
|
);
|
||||||
// Media controls
|
if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, &track) {
|
||||||
self.draw_controls(ui);
|
action = Some(act);
|
||||||
|
}
|
||||||
ui.separator();
|
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| {
|
ui.horizontal_top(|ui| {
|
||||||
// ---------- Play Button ----------
|
// ---------- Play Button ----------
|
||||||
let play_button = Button::new(match self.audio_player_state.state {
|
let play_button = Button::new(if track.paused {
|
||||||
PlayerState::Playing => icons::ICON_PAUSE,
|
icons::ICON_PLAY_ARROW
|
||||||
PlayerState::Paused | PlayerState::Stopped => icons::ICON_PLAY_ARROW,
|
} else {
|
||||||
|
icons::ICON_PAUSE
|
||||||
})
|
})
|
||||||
.corner_radius(15.0);
|
.corner_radius(15.0);
|
||||||
|
|
||||||
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
|
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
|
||||||
if play_button_response.clicked() {
|
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(if track.looped {
|
||||||
|
icons::ICON_REPEAT_ONE
|
||||||
|
} else {
|
||||||
|
icons::ICON_REPEAT
|
||||||
|
})
|
||||||
|
.size(18.0),
|
||||||
|
)
|
||||||
|
.frame(false);
|
||||||
|
|
||||||
|
let loop_button_response = ui.add_sized([15.0, 30.0], loop_button);
|
||||||
|
if loop_button_response.clicked() {
|
||||||
|
action = Some(TrackAction::ToggleLoop(track.id));
|
||||||
}
|
}
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
|
|
||||||
// ---------- Position Slider ----------
|
// ---------- Position Slider ----------
|
||||||
let position_slider = Slider::new(
|
let duration = track.duration.unwrap_or(1.0);
|
||||||
&mut self.app_state.position_slider_value,
|
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
|
||||||
0.0..=self.audio_player_state.duration,
|
.show_value(false)
|
||||||
)
|
.step_by(0.01);
|
||||||
.show_value(false)
|
|
||||||
.step_by(1.0);
|
|
||||||
|
|
||||||
let default_slider_width = ui.spacing().slider_width;
|
let default_slider_width = ui.spacing().slider_width;
|
||||||
let position_slider_width = ui.available_width()
|
let position_slider_width = ui.available_width()
|
||||||
@@ -124,49 +216,47 @@ impl SoundpadGui {
|
|||||||
ui.spacing_mut().slider_width = position_slider_width;
|
ui.spacing_mut().slider_width = position_slider_width;
|
||||||
let position_slider_response = ui.add_sized([30.0, 30.0], position_slider);
|
let position_slider_response = ui.add_sized([30.0, 30.0], position_slider);
|
||||||
if position_slider_response.drag_stopped() {
|
if position_slider_response.drag_stopped() {
|
||||||
self.app_state.position_dragged = true;
|
ui_state.position_dragged = true;
|
||||||
}
|
}
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
|
|
||||||
// ---------- Time Label ----------
|
// ---------- Time Label ----------
|
||||||
let time_label = Label::new(
|
let time_label =
|
||||||
RichText::new(format_time_pair(
|
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
|
||||||
self.audio_player_state.position,
|
|
||||||
self.audio_player_state.duration,
|
|
||||||
))
|
|
||||||
.monospace(),
|
|
||||||
);
|
|
||||||
ui.add_sized([30.0, 30.0], time_label);
|
ui.add_sized([30.0, 30.0], time_label);
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
|
|
||||||
// ---------- Volume Icon ----------
|
// ---------- Volume Icon ----------
|
||||||
let volume_icon = if self.audio_player_state.volume > 0.7 {
|
let volume_icon = Self::get_volume_icon(track.volume);
|
||||||
icons::ICON_VOLUME_UP
|
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
||||||
} else if self.audio_player_state.volume == 0.0 {
|
ui.add_sized([30.0, 30.0], volume_label)
|
||||||
icons::ICON_VOLUME_OFF
|
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
|
||||||
} else if self.audio_player_state.volume < 0.3 {
|
|
||||||
icons::ICON_VOLUME_MUTE
|
|
||||||
} else {
|
|
||||||
icons::ICON_VOLUME_DOWN
|
|
||||||
};
|
|
||||||
let volume_icon = Label::new(RichText::new(volume_icon).size(18.0));
|
|
||||||
ui.add_sized([30.0, 25.0], volume_icon);
|
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
|
|
||||||
// ---------- Volume Slider ----------
|
// ---------- 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)
|
.show_value(false)
|
||||||
.step_by(0.01);
|
.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;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
|
||||||
let volume_slider_response = ui.add_sized([30.0, 30.0], volume_slider);
|
let volume_slider_response = ui.add_sized([30.0, 30.0], volume_slider);
|
||||||
if volume_slider_response.drag_stopped() {
|
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) {
|
fn draw_body(&mut self, ui: &mut Ui) {
|
||||||
@@ -300,7 +390,18 @@ impl SoundpadGui {
|
|||||||
let file_button = Button::new(file_button_text).frame(false);
|
let file_button = Button::new(file_button_text).frame(false);
|
||||||
let file_button_response = ui.add(file_button);
|
let file_button_response = ui.add(file_button);
|
||||||
if file_button_response.clicked() {
|
if file_button_response.clicked() {
|
||||||
self.play_file(&entry_path);
|
ui.input(|i| {
|
||||||
|
if i.modifiers.ctrl {
|
||||||
|
self.play_file(&entry_path, true);
|
||||||
|
} else if i.modifiers.shift
|
||||||
|
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
||||||
|
{
|
||||||
|
self.stop(Some(last_track.id));
|
||||||
|
self.play_file(&entry_path, true);
|
||||||
|
} else {
|
||||||
|
self.play_file(&entry_path, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
self.app_state.selected_file = Some(entry_path);
|
self.app_state.selected_file = Some(entry_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +412,7 @@ impl SoundpadGui {
|
|||||||
|
|
||||||
fn draw_footer(&mut self, ui: &mut Ui) {
|
fn draw_footer(&mut self, ui: &mut Ui) {
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
ui.horizontal_top(|ui| {
|
ui.horizontal(|ui| {
|
||||||
// ---------- Microphone selection ----------
|
// ---------- Microphone selection ----------
|
||||||
let mut mics: Vec<(&String, &String)> =
|
let mut mics: Vec<(&String, &String)> =
|
||||||
self.audio_player_state.all_inputs.iter().collect();
|
self.audio_player_state.all_inputs.iter().collect();
|
||||||
@@ -320,6 +421,7 @@ impl SoundpadGui {
|
|||||||
let mut selected_input = self.audio_player_state.current_input.to_owned();
|
let mut selected_input = self.audio_player_state.current_input.to_owned();
|
||||||
let prev_input = selected_input.to_owned();
|
let prev_input = selected_input.to_owned();
|
||||||
ComboBox::from_label("Choose microphone")
|
ComboBox::from_label("Choose microphone")
|
||||||
|
.height(30.0)
|
||||||
.selected_text(
|
.selected_text(
|
||||||
self.audio_player_state
|
self.audio_player_state
|
||||||
.all_inputs
|
.all_inputs
|
||||||
@@ -337,10 +439,40 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
|
|
||||||
|
// ---------- Master Volume Slider ----------
|
||||||
|
let volume_icon = Self::get_volume_icon(self.audio_player_state.volume);
|
||||||
|
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
||||||
|
ui.add_sized([18.0, 18.0], volume_label)
|
||||||
|
.on_hover_text(format!(
|
||||||
|
"Master Volume: {:.0}%",
|
||||||
|
self.audio_player_state.volume * 100.0
|
||||||
|
));
|
||||||
|
|
||||||
|
let should_update_volume = !self.app_state.volume_dragged
|
||||||
|
&& self
|
||||||
|
.app_state
|
||||||
|
.ignore_volume_update_until
|
||||||
|
.map(|t| Instant::now() > t)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if should_update_volume {
|
||||||
|
self.app_state.volume_slider_value = self.audio_player_state.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
let volume_slider_response = ui.add_sized([150.0, 18.0], volume_slider);
|
||||||
|
if volume_slider_response.drag_stopped() {
|
||||||
|
self.app_state.volume_dragged = true;
|
||||||
|
}
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
ui.add_space(ui.available_width() - 18.0 - ui.spacing().item_spacing.x);
|
ui.add_space(ui.available_width() - 18.0 - ui.spacing().item_spacing.x);
|
||||||
|
|
||||||
// ---------- Settings button ----------
|
// ---------- Settings button ----------
|
||||||
let settings_button = Button::new(icons::ICON_SETTINGS).frame(false);
|
let settings_button =
|
||||||
|
Button::new(icons::ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false);
|
||||||
let settings_button_response = ui.add_sized([18.0, 18.0], settings_button);
|
let settings_button_response = ui.add_sized([18.0, 18.0], settings_button);
|
||||||
if settings_button_response.clicked() {
|
if settings_button_response.clicked() {
|
||||||
self.app_state.show_settings = true;
|
self.app_state.show_settings = true;
|
||||||
|
|||||||
+16
-1
@@ -21,7 +21,17 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if i.key_pressed(Key::Enter) && self.app_state.selected_file.is_some() {
|
if i.key_pressed(Key::Enter) && self.app_state.selected_file.is_some() {
|
||||||
self.play_file(&self.app_state.selected_file.clone().unwrap());
|
let path = &self.app_state.selected_file.clone().unwrap();
|
||||||
|
if i.modifiers.ctrl {
|
||||||
|
self.play_file(path, true);
|
||||||
|
} else if i.modifiers.shift
|
||||||
|
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
||||||
|
{
|
||||||
|
self.stop(Some(last_track.id));
|
||||||
|
self.play_file(path, true);
|
||||||
|
} else {
|
||||||
|
self.play_file(path, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.app_state.show_settings {
|
if !self.app_state.show_settings {
|
||||||
@@ -30,6 +40,11 @@ impl SoundpadGui {
|
|||||||
self.play_toggle();
|
self.play_toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop all audio tracks on backspace
|
||||||
|
if i.key_pressed(Key::Backspace) {
|
||||||
|
self.stop(None);
|
||||||
|
}
|
||||||
|
|
||||||
// Focus search field
|
// Focus search field
|
||||||
if i.key_pressed(Key::Slash) && self.app_state.search_field_id.is_some() {
|
if i.key_pressed(Key::Slash) && self.app_state.search_field_id.is_some() {
|
||||||
self.app_state.force_focus_id = self.app_state.search_field_id;
|
self.app_state.force_focus_id = self.app_state.search_field_id;
|
||||||
|
|||||||
+39
-19
@@ -13,13 +13,13 @@ use pwsp::{
|
|||||||
},
|
},
|
||||||
utils::{
|
utils::{
|
||||||
daemon::get_daemon_config,
|
daemon::get_daemon_config,
|
||||||
gui::{get_gui_config, make_request_sync, start_app_state_thread},
|
gui::{get_gui_config, make_request_async, make_request_sync, start_app_state_thread},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use rfd::FileDialog;
|
use rfd::FileDialog;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
error::Error,
|
||||||
|
path::PathBuf,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,26 +56,30 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn play_toggle(&mut self) {
|
pub fn play_toggle(&mut self) {
|
||||||
let mut guard = self.audio_player_state_shared.lock().unwrap();
|
let (new_state, request) = {
|
||||||
guard.state = match guard.state {
|
let guard = self.audio_player_state_shared.lock().unwrap();
|
||||||
PlayerState::Playing => {
|
match guard.state {
|
||||||
make_request_sync(Request::pause()).ok();
|
PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause(None))),
|
||||||
guard.new_state = Some(PlayerState::Paused);
|
PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume(None))),
|
||||||
PlayerState::Paused
|
PlayerState::Stopped => (None, None),
|
||||||
}
|
}
|
||||||
PlayerState::Paused => {
|
|
||||||
make_request_sync(Request::resume()).ok();
|
|
||||||
guard.new_state = Some(PlayerState::Playing);
|
|
||||||
PlayerState::Playing
|
|
||||||
}
|
|
||||||
PlayerState::Stopped => PlayerState::Stopped,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(req) = request {
|
||||||
|
make_request_async(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(state) = new_state {
|
||||||
|
let mut guard = self.audio_player_state_shared.lock().unwrap();
|
||||||
|
guard.new_state = Some(state.clone());
|
||||||
|
guard.state = state;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_file(&mut self) {
|
pub fn open_file(&mut self) {
|
||||||
let file_dialog = FileDialog::new().add_filter("Audio File", &SUPPORTED_EXTENSIONS);
|
let file_dialog = FileDialog::new().add_filter("Audio File", &SUPPORTED_EXTENSIONS);
|
||||||
if let Some(path) = file_dialog.pick_file() {
|
if let Some(path) = file_dialog.pick_file() {
|
||||||
self.play_file(&path);
|
self.play_file(&path, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,12 +116,12 @@ impl SoundpadGui {
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play_file(&mut self, path: &PathBuf) {
|
pub fn play_file(&mut self, path: &PathBuf, concurrent: bool) {
|
||||||
make_request_sync(Request::play(path.to_str().unwrap())).ok();
|
make_request_async(Request::play(path.to_str().unwrap(), concurrent));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_input(&mut self, name: String) {
|
pub fn set_input(&mut self, name: String) {
|
||||||
make_request_sync(Request::set_input(&name)).ok();
|
make_request_async(Request::set_input(&name));
|
||||||
|
|
||||||
if self.config.save_input {
|
if self.config.save_input {
|
||||||
let mut daemon_config = get_daemon_config();
|
let mut daemon_config = get_daemon_config();
|
||||||
@@ -125,6 +129,22 @@ impl SoundpadGui {
|
|||||||
daemon_config.save_to_file().ok();
|
daemon_config.save_to_file().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_loop(&mut self, id: Option<u32>) {
|
||||||
|
make_request_async(Request::toggle_loop(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pause(&mut self, id: Option<u32>) {
|
||||||
|
make_request_async(Request::pause(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume(&mut self, id: Option<u32>) {
|
||||||
|
make_request_async(Request::resume(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&mut self, id: Option<u32>) {
|
||||||
|
make_request_async(Request::stop(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run() -> Result<(), Box<dyn Error>> {
|
pub async fn run() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -155,7 +175,7 @@ pub async fn run() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let config = get_gui_config();
|
let config = get_gui_config();
|
||||||
if config.pause_on_exit {
|
if config.pause_on_exit {
|
||||||
make_request_sync(Request::pause()).ok();
|
make_request_sync(Request::pause(None)).ok();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-35
@@ -3,14 +3,60 @@ use eframe::{App, Frame as EFrame};
|
|||||||
use egui::{CentralPanel, Context};
|
use egui::{CentralPanel, Context};
|
||||||
use pwsp::{
|
use pwsp::{
|
||||||
types::socket::Request,
|
types::socket::Request,
|
||||||
utils::{
|
utils::{daemon::get_daemon_config, gui::make_request_async},
|
||||||
daemon::{get_daemon_config, is_daemon_running},
|
|
||||||
gui::make_request_sync,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
impl App for SoundpadGui {
|
impl App for SoundpadGui {
|
||||||
fn update(&mut self, ctx: &Context, _frame: &mut EFrame) {
|
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_async(Request::seek(pos, Some(id)));
|
||||||
|
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(300));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (id, vol) in volume_requests {
|
||||||
|
make_request_async(Request::set_volume(vol, Some(id)));
|
||||||
|
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(300));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.app_state.volume_dragged {
|
||||||
|
make_request_async(Request::set_volume(
|
||||||
|
self.app_state.volume_slider_value,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
|
||||||
|
self.app_state.volume_dragged = false;
|
||||||
|
self.app_state.ignore_volume_update_until =
|
||||||
|
Some(Instant::now() + Duration::from_millis(300));
|
||||||
|
|
||||||
|
if self.config.save_volume {
|
||||||
|
let mut daemon_config = get_daemon_config();
|
||||||
|
daemon_config.default_volume = Some(self.app_state.volume_slider_value);
|
||||||
|
daemon_config.save_to_file().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let guard = self.audio_player_state_shared.lock().unwrap();
|
let guard = self.audio_player_state_shared.lock().unwrap();
|
||||||
self.audio_player_state = guard.clone();
|
self.audio_player_state = guard.clone();
|
||||||
@@ -29,7 +75,7 @@ impl App for SoundpadGui {
|
|||||||
self.handle_input(ctx);
|
self.handle_input(ctx);
|
||||||
|
|
||||||
CentralPanel::default().show(ctx, |ui| {
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
if !is_daemon_running().unwrap() {
|
if !self.audio_player_state.is_daemon_running {
|
||||||
self.draw_waiting_for_daemon(ui);
|
self.draw_waiting_for_daemon(ui);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -49,36 +95,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);
|
ctx.request_repaint_after_secs(1.0 / 60.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+238
-110
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source};
|
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
error::Error,
|
error::Error,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@@ -22,17 +23,35 @@ pub enum PlayerState {
|
|||||||
Playing,
|
Playing,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TrackInfo {
|
||||||
|
pub id: u32,
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub duration: Option<f32>,
|
||||||
|
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<f32>,
|
||||||
|
pub looped: bool,
|
||||||
|
pub volume: f32,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AudioPlayer {
|
pub struct AudioPlayer {
|
||||||
_stream_handle: OutputStream,
|
pub stream_handle: OutputStream,
|
||||||
sink: Sink,
|
pub tracks: HashMap<u32, PlayingSound>,
|
||||||
|
pub next_id: u32,
|
||||||
|
|
||||||
input_link_sender: Option<pipewire::channel::Sender<Terminate>>,
|
input_link_sender: Option<pipewire::channel::Sender<Terminate>>,
|
||||||
pub current_input_device: Option<AudioDevice>,
|
pub current_input_device: Option<AudioDevice>,
|
||||||
|
|
||||||
pub volume: f32,
|
pub volume: f32, // Master volume
|
||||||
pub duration: Option<f32>,
|
|
||||||
|
|
||||||
pub current_file_path: Option<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioPlayer {
|
impl AudioPlayer {
|
||||||
@@ -48,20 +67,16 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let stream_handle = OutputStreamBuilder::open_default_stream()?;
|
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 {
|
let mut audio_player = AudioPlayer {
|
||||||
_stream_handle: stream_handle,
|
stream_handle,
|
||||||
sink,
|
tracks: HashMap::new(),
|
||||||
|
next_id: 1,
|
||||||
|
|
||||||
input_link_sender: None,
|
input_link_sender: None,
|
||||||
current_input_device: default_input_device.clone(),
|
current_input_device: default_input_device.clone(),
|
||||||
|
|
||||||
volume: default_volume,
|
volume: default_volume,
|
||||||
duration: None,
|
|
||||||
|
|
||||||
current_file_path: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if default_input_device.is_some() {
|
if default_input_device.is_some() {
|
||||||
@@ -75,7 +90,7 @@ impl AudioPlayer {
|
|||||||
if let Some(sender) = &self.input_link_sender {
|
if let Some(sender) = &self.input_link_sender {
|
||||||
match sender.send(Terminate {}) {
|
match sender.send(Terminate {}) {
|
||||||
Ok(_) => println!("Sent terminate signal to link thread"),
|
Ok(_) => println!("Sent terminate signal to link thread"),
|
||||||
Err(_) => println!("Failed to send terminate signal to link thread"),
|
Err(_) => eprintln!("Failed to send terminate signal to link thread"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +99,7 @@ impl AudioPlayer {
|
|||||||
self.abort_link_thread();
|
self.abort_link_thread();
|
||||||
|
|
||||||
if self.current_input_device.is_none() {
|
if self.current_input_device.is_none() {
|
||||||
println!("No input device selected, skipping device linking");
|
eprintln!("No input device selected, skipping device linking");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,102 +114,152 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pwsp_daemon_input.is_none() {
|
if pwsp_daemon_input.is_none() {
|
||||||
println!("Could not find pwsp-daemon input device, skipping device linking");
|
eprintln!("Could not find pwsp-daemon input device, skipping device linking");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let pwsp_daemon_input = pwsp_daemon_input.unwrap();
|
let pwsp_daemon_input = pwsp_daemon_input.unwrap();
|
||||||
|
|
||||||
let current_input_device = self.current_input_device.clone().unwrap();
|
let current_input_device = self.current_input_device.clone().unwrap();
|
||||||
let output_fl = current_input_device
|
|
||||||
.clone()
|
let Some(output_fl) = current_input_device.output_fl.clone() else {
|
||||||
.output_fl
|
eprintln!("Failed to get pwsp-daemon output_fl");
|
||||||
.expect("Failed to get pwsp-daemon output_fl");
|
return Ok(());
|
||||||
let output_fr = current_input_device
|
};
|
||||||
.clone()
|
let Some(output_fr) = current_input_device.output_fr.clone() else {
|
||||||
.output_fr
|
eprintln!("Failed to get pwsp-daemon output_fr");
|
||||||
.expect("Failed to get pwsp-daemon output_fl");
|
return Ok(());
|
||||||
let input_fl = pwsp_daemon_input
|
};
|
||||||
.clone()
|
let Some(input_fl) = pwsp_daemon_input.input_fl.clone() else {
|
||||||
.input_fl
|
eprintln!("Failed to get pwsp-daemon input_fl");
|
||||||
.expect("Failed to get pwsp-daemon input_fl");
|
return Ok(());
|
||||||
let input_fr = pwsp_daemon_input
|
};
|
||||||
.clone()
|
let Some(input_fr) = pwsp_daemon_input.input_fr.clone() else {
|
||||||
.input_fr
|
eprintln!("Failed to get pwsp-daemon input_fr");
|
||||||
.expect("Failed to get pwsp-daemon input_fr");
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
self.input_link_sender = Some(create_link(output_fl, output_fr, input_fl, input_fr)?);
|
self.input_link_sender = Some(create_link(output_fl, output_fr, input_fl, input_fr)?);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pause(&mut self) {
|
pub fn pause(&mut self, id: Option<u32>) {
|
||||||
if self.get_state() == PlayerState::Playing {
|
if let Some(id) = id {
|
||||||
self.sink.pause();
|
if let Some(sound) = self.tracks.get_mut(&id) {
|
||||||
}
|
sound.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(&mut 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(&mut 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<dyn Error>> {
|
|
||||||
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<f32, Box<dyn Error>> {
|
|
||||||
if self.get_state() == PlayerState::Stopped {
|
|
||||||
Err("Nothing is playing right now".into())
|
|
||||||
} else {
|
} else {
|
||||||
match self.duration {
|
for sound in self.tracks.values_mut() {
|
||||||
Some(duration) => Ok(duration),
|
sound.sink.pause();
|
||||||
None => Err("Couldn't determine duration for current file".into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn play(&mut self, file_path: &Path) -> Result<(), Box<dyn Error>> {
|
pub fn resume(&mut self, id: Option<u32>) {
|
||||||
|
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<u32>) {
|
||||||
|
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<u32>) {
|
||||||
|
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<u32>) -> 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<u32>) -> Result<(), Box<dyn Error>> {
|
||||||
|
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<u32>) -> Result<f32, Box<dyn Error>> {
|
||||||
|
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<u32, Box<dyn Error>> {
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
return Err(format!("File does not exist: {}", file_path.display()).into());
|
return Err(format!("File does not exist: {}", file_path.display()).into());
|
||||||
}
|
}
|
||||||
@@ -202,30 +267,93 @@ impl AudioPlayer {
|
|||||||
let file = fs::File::open(file_path)?;
|
let file = fs::File::open(file_path)?;
|
||||||
match Decoder::try_from(file) {
|
match Decoder::try_from(file) {
|
||||||
Ok(source) => {
|
Ok(source) => {
|
||||||
self.current_file_path = Some(file_path.to_path_buf());
|
if !concurrent {
|
||||||
|
self.tracks.clear();
|
||||||
if let Some(duration) = source.total_duration() {
|
|
||||||
self.duration = Some(duration.as_secs_f32());
|
|
||||||
} else {
|
|
||||||
self.duration = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sink.stop();
|
let id = self.next_id;
|
||||||
self.sink.append(source);
|
self.next_id += 1;
|
||||||
self.sink.play();
|
|
||||||
|
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?;
|
self.link_devices().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(id)
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_file_path(&mut self) -> &Option<PathBuf> {
|
pub fn set_loop(&mut self, enabled: bool, id: Option<u32>) {
|
||||||
if self.get_state() == PlayerState::Stopped {
|
if let Some(id) = id {
|
||||||
self.current_file_path = None;
|
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<TrackInfo> {
|
||||||
|
let mut tracks: 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();
|
||||||
|
tracks.sort_by_key(|t| t.id);
|
||||||
|
tracks
|
||||||
|
}
|
||||||
|
|
||||||
|
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<dyn Error>> {
|
pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
|||||||
+109
-31
@@ -12,13 +12,21 @@ pub trait Executable {
|
|||||||
|
|
||||||
pub struct PingCommand {}
|
pub struct PingCommand {}
|
||||||
|
|
||||||
pub struct PauseCommand {}
|
pub struct PauseCommand {
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ResumeCommand {}
|
pub struct ResumeCommand {
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TogglePauseCommand {}
|
pub struct TogglePauseCommand {
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StopCommand {}
|
pub struct StopCommand {
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct IsPausedCommand {}
|
pub struct IsPausedCommand {}
|
||||||
|
|
||||||
@@ -28,21 +36,28 @@ pub struct GetVolumeCommand {}
|
|||||||
|
|
||||||
pub struct SetVolumeCommand {
|
pub struct SetVolumeCommand {
|
||||||
pub volume: Option<f32>,
|
pub volume: Option<f32>,
|
||||||
|
pub id: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetPositionCommand {}
|
pub struct GetPositionCommand {
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SeekCommand {
|
pub struct SeekCommand {
|
||||||
pub position: Option<f32>,
|
pub position: Option<f32>,
|
||||||
|
pub id: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetDurationCommand {}
|
pub struct GetDurationCommand {
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PlayCommand {
|
pub struct PlayCommand {
|
||||||
pub file_path: Option<PathBuf>,
|
pub file_path: Option<PathBuf>,
|
||||||
|
pub concurrent: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetCurrentFilePathCommand {}
|
pub struct GetTracksCommand {}
|
||||||
|
|
||||||
pub struct GetCurrentInputCommand {}
|
pub struct GetCurrentInputCommand {}
|
||||||
|
|
||||||
@@ -52,6 +67,15 @@ pub struct SetCurrentInputCommand {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SetLoopCommand {
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ToggleLoopCommand {
|
||||||
|
pub id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for PingCommand {
|
impl Executable for PingCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
@@ -63,7 +87,7 @@ impl Executable for PingCommand {
|
|||||||
impl Executable for PauseCommand {
|
impl Executable for PauseCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = get_audio_player().await.lock().await;
|
||||||
audio_player.pause();
|
audio_player.pause(self.id);
|
||||||
Response::new(true, "Audio was paused")
|
Response::new(true, "Audio was paused")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +96,7 @@ impl Executable for PauseCommand {
|
|||||||
impl Executable for ResumeCommand {
|
impl Executable for ResumeCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = get_audio_player().await.lock().await;
|
||||||
audio_player.resume();
|
audio_player.resume(self.id);
|
||||||
Response::new(true, "Audio was resumed")
|
Response::new(true, "Audio was resumed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,12 +110,31 @@ impl Executable for TogglePauseCommand {
|
|||||||
return Response::new(false, "Audio is not playing");
|
return Response::new(false, "Audio is not playing");
|
||||||
}
|
}
|
||||||
|
|
||||||
if audio_player.is_paused() {
|
// This logic is a bit tricky with multiple tracks.
|
||||||
audio_player.resume();
|
// If ID is provided, toggle that track.
|
||||||
Response::new(true, "Audio was resumed")
|
// 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 {
|
} else {
|
||||||
audio_player.pause();
|
if audio_player.is_paused() {
|
||||||
Response::new(true, "Audio was paused")
|
audio_player.resume(None);
|
||||||
|
Response::new(true, "Audio was resumed")
|
||||||
|
} else {
|
||||||
|
audio_player.pause(None);
|
||||||
|
Response::new(true, "Audio was paused")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +143,7 @@ impl Executable for TogglePauseCommand {
|
|||||||
impl Executable for StopCommand {
|
impl Executable for StopCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = get_audio_player().await.lock().await;
|
||||||
audio_player.stop();
|
audio_player.stop(self.id);
|
||||||
Response::new(true, "Audio was stopped")
|
Response::new(true, "Audio was stopped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +160,7 @@ impl Executable for IsPausedCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetStateCommand {
|
impl Executable for GetStateCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let audio_player = get_audio_player().await.lock().await;
|
||||||
let state = audio_player.get_state();
|
let state = audio_player.get_state();
|
||||||
Response::new(true, serde_json::to_string(&state).unwrap())
|
Response::new(true, serde_json::to_string(&state).unwrap())
|
||||||
}
|
}
|
||||||
@@ -137,7 +180,7 @@ impl Executable for SetVolumeCommand {
|
|||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
if let Some(volume) = self.volume {
|
if let Some(volume) = self.volume {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
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))
|
Response::new(true, format!("Audio volume was set to {}", volume))
|
||||||
} else {
|
} else {
|
||||||
Response::new(false, "Invalid volume value")
|
Response::new(false, "Invalid volume value")
|
||||||
@@ -148,8 +191,8 @@ impl Executable for SetVolumeCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetPositionCommand {
|
impl Executable for GetPositionCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
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())
|
Response::new(true, position.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,7 +202,7 @@ impl Executable for SeekCommand {
|
|||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
if let Some(position) = self.position {
|
if let Some(position) = self.position {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
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)),
|
Ok(_) => Response::new(true, format!("Audio position was set to {}", position)),
|
||||||
Err(err) => Response::new(false, err.to_string()),
|
Err(err) => Response::new(false, err.to_string()),
|
||||||
}
|
}
|
||||||
@@ -173,7 +216,7 @@ impl Executable for SeekCommand {
|
|||||||
impl Executable for GetDurationCommand {
|
impl Executable for GetDurationCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
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()),
|
Ok(duration) => Response::new(true, duration.to_string()),
|
||||||
Err(err) => Response::new(false, err.to_string()),
|
Err(err) => Response::new(false, err.to_string()),
|
||||||
}
|
}
|
||||||
@@ -185,8 +228,11 @@ impl Executable for PlayCommand {
|
|||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
if let Some(file_path) = &self.file_path {
|
if let Some(file_path) = &self.file_path {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = get_audio_player().await.lock().await;
|
||||||
match audio_player.play(file_path).await {
|
match audio_player
|
||||||
Ok(_) => Response::new(true, format!("Now playing {}", file_path.display())),
|
.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()),
|
Err(err) => Response::new(false, err.to_string()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -196,15 +242,11 @@ impl Executable for PlayCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetCurrentFilePathCommand {
|
impl Executable for GetTracksCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let audio_player = get_audio_player().await.lock().await;
|
||||||
let current_file_path = audio_player.get_current_file_path();
|
let tracks = audio_player.get_tracks();
|
||||||
if let Some(current_file_path) = current_file_path {
|
Response::new(true, serde_json::to_string(&tracks).unwrap())
|
||||||
Response::new(true, current_file_path.to_str().unwrap())
|
|
||||||
} else {
|
|
||||||
Response::new(false, "No file is playing")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,3 +298,39 @@ impl Executable for SetCurrentInputCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Executable for SetLoopCommand {
|
||||||
|
async fn execute(&self) -> Response {
|
||||||
|
let mut audio_player = get_audio_player().await.lock().await;
|
||||||
|
|
||||||
|
match self.enabled {
|
||||||
|
Some(enabled) => {
|
||||||
|
audio_player.set_loop(enabled, self.id);
|
||||||
|
Response::new(true, format!("Loop was set to {}", enabled))
|
||||||
|
}
|
||||||
|
None => Response::new(false, "Invalid enabled value"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Executable for ToggleLoopCommand {
|
||||||
|
async fn execute(&self) -> Response {
|
||||||
|
let mut audio_player = get_audio_player().await.lock().await;
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+22
-11
@@ -1,23 +1,37 @@
|
|||||||
use crate::types::audio_player::PlayerState;
|
use crate::types::audio_player::{PlayerState, TrackInfo};
|
||||||
|
|
||||||
use egui::Id;
|
use egui::Id;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct AppState {
|
pub struct TrackUiState {
|
||||||
pub search_query: String,
|
|
||||||
|
|
||||||
pub position_slider_value: f32,
|
pub position_slider_value: f32,
|
||||||
pub volume_slider_value: f32,
|
pub volume_slider_value: f32,
|
||||||
|
|
||||||
pub position_dragged: bool,
|
pub position_dragged: bool,
|
||||||
pub volume_dragged: bool,
|
pub volume_dragged: bool,
|
||||||
|
|
||||||
|
pub ignore_position_update_until: Option<Instant>,
|
||||||
|
pub ignore_volume_update_until: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub search_query: String,
|
||||||
|
|
||||||
|
pub track_ui_states: HashMap<u32, TrackUiState>,
|
||||||
|
|
||||||
pub show_settings: bool,
|
pub show_settings: bool,
|
||||||
|
pub volume_dragged: bool,
|
||||||
|
|
||||||
|
pub volume_slider_value: f32,
|
||||||
|
|
||||||
|
pub ignore_volume_update_until: Option<Instant>,
|
||||||
|
|
||||||
pub current_dir: Option<PathBuf>,
|
pub current_dir: Option<PathBuf>,
|
||||||
pub dirs: HashSet<PathBuf>,
|
pub dirs: HashSet<PathBuf>,
|
||||||
@@ -33,16 +47,13 @@ pub struct AppState {
|
|||||||
pub struct AudioPlayerState {
|
pub struct AudioPlayerState {
|
||||||
pub state: PlayerState,
|
pub state: PlayerState,
|
||||||
pub new_state: Option<PlayerState>,
|
pub new_state: Option<PlayerState>,
|
||||||
pub current_file_path: PathBuf,
|
|
||||||
|
|
||||||
pub is_paused: bool,
|
pub tracks: Vec<TrackInfo>,
|
||||||
|
|
||||||
pub volume: f32,
|
pub volume: f32, // Master volume
|
||||||
pub new_volume: Option<f32>,
|
|
||||||
pub position: f32,
|
|
||||||
pub new_position: Option<f32>,
|
|
||||||
pub duration: f32,
|
|
||||||
|
|
||||||
pub current_input: String,
|
pub current_input: String,
|
||||||
pub all_inputs: HashMap<String, String>,
|
pub all_inputs: HashMap<String, String>,
|
||||||
|
|
||||||
|
pub is_daemon_running: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-20
@@ -24,24 +24,54 @@ impl Request {
|
|||||||
Request::new("ping", vec![])
|
Request::new("ping", vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pause() -> Self {
|
pub fn pause(id: Option<u32>) -> Self {
|
||||||
Request::new("pause", vec![])
|
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 {
|
pub fn resume(id: Option<u32>) -> Self {
|
||||||
Request::new("resume", vec![])
|
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 {
|
pub fn toggle_pause(id: Option<u32>) -> Self {
|
||||||
Request::new("toggle_pause", vec![])
|
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 {
|
pub fn stop(id: Option<u32>) -> Self {
|
||||||
Request::new("stop", vec![])
|
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 {
|
pub fn play(file_path: &str, concurrent: bool) -> Self {
|
||||||
Request::new("play", vec![("file_path", file_path)])
|
Request::new(
|
||||||
|
"play",
|
||||||
|
vec![
|
||||||
|
("file_path", file_path),
|
||||||
|
("concurrent", &concurrent.to_string()),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_is_paused() -> Self {
|
pub fn get_is_paused() -> Self {
|
||||||
@@ -52,20 +82,32 @@ impl Request {
|
|||||||
Request::new("get_volume", vec![])
|
Request::new("get_volume", vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_position() -> Self {
|
pub fn get_position(id: Option<u32>) -> Self {
|
||||||
Request::new("get_position", vec![])
|
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 {
|
pub fn get_duration(id: Option<u32>) -> Self {
|
||||||
Request::new("get_duration", vec![])
|
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 {
|
pub fn get_state() -> Self {
|
||||||
Request::new("get_state", vec![])
|
Request::new("get_state", vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_file_path() -> Self {
|
pub fn get_tracks() -> Self {
|
||||||
Request::new("get_current_file_path", vec![])
|
Request::new("get_tracks", vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_input() -> Self {
|
pub fn get_input() -> Self {
|
||||||
@@ -76,17 +118,43 @@ impl Request {
|
|||||||
Request::new("get_inputs", vec![])
|
Request::new("get_inputs", vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_volume(volume: f32) -> Self {
|
pub fn set_volume(volume: f32, id: Option<u32>) -> Self {
|
||||||
Request::new("set_volume", vec![("volume", &volume.to_string())])
|
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 {
|
pub fn seek(position: f32, id: Option<u32>) -> Self {
|
||||||
Request::new("seek", vec![("position", &position.to_string())])
|
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 {
|
pub fn set_input(name: &str) -> Self {
|
||||||
Request::new("set_input", vec![("input_name", name)])
|
Request::new("set_input", vec![("input_name", name)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_loop(enabled: &str, id: Option<u32>) -> 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 toggle_loop(id: Option<u32>) -> 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
+31
-10
@@ -3,12 +3,14 @@ use crate::types::{commands::*, socket::Request};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
|
pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
|
||||||
|
let id = request.args.get("id").and_then(|s| s.parse::<u32>().ok());
|
||||||
|
|
||||||
match request.name.as_str() {
|
match request.name.as_str() {
|
||||||
"ping" => Some(Box::new(PingCommand {})),
|
"ping" => Some(Box::new(PingCommand {})),
|
||||||
"pause" => Some(Box::new(PauseCommand {})),
|
"pause" => Some(Box::new(PauseCommand { id })),
|
||||||
"resume" => Some(Box::new(ResumeCommand {})),
|
"resume" => Some(Box::new(ResumeCommand { id })),
|
||||||
"toggle_pause" => Some(Box::new(TogglePauseCommand {})),
|
"toggle_pause" => Some(Box::new(TogglePauseCommand { id })),
|
||||||
"stop" => Some(Box::new(StopCommand {})),
|
"stop" => Some(Box::new(StopCommand { id })),
|
||||||
"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 {})),
|
"get_volume" => Some(Box::new(GetVolumeCommand {})),
|
||||||
@@ -19,9 +21,9 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
|
|||||||
.unwrap_or(&String::new())
|
.unwrap_or(&String::new())
|
||||||
.parse::<f32>()
|
.parse::<f32>()
|
||||||
.ok();
|
.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" => {
|
"seek" => {
|
||||||
let position = request
|
let position = request
|
||||||
.args
|
.args
|
||||||
@@ -29,9 +31,9 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
|
|||||||
.unwrap_or(&String::new())
|
.unwrap_or(&String::new())
|
||||||
.parse::<f32>()
|
.parse::<f32>()
|
||||||
.ok();
|
.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" => {
|
"play" => {
|
||||||
let file_path = request
|
let file_path = request
|
||||||
.args
|
.args
|
||||||
@@ -39,15 +41,34 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
|
|||||||
.unwrap_or(&String::new())
|
.unwrap_or(&String::new())
|
||||||
.parse::<PathBuf>()
|
.parse::<PathBuf>()
|
||||||
.ok();
|
.ok();
|
||||||
Some(Box::new(PlayCommand { file_path }))
|
let concurrent = request
|
||||||
|
.args
|
||||||
|
.get("concurrent")
|
||||||
|
.unwrap_or(&String::new())
|
||||||
|
.parse::<bool>()
|
||||||
|
.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_input" => Some(Box::new(GetCurrentInputCommand {})),
|
||||||
"get_inputs" => Some(Box::new(GetAllInputsCommand {})),
|
"get_inputs" => Some(Box::new(GetAllInputsCommand {})),
|
||||||
"set_input" => {
|
"set_input" => {
|
||||||
let name = Some(request.args.get("input_name").unwrap_or(&String::new())).cloned();
|
let name = Some(request.args.get("input_name").unwrap_or(&String::new())).cloned();
|
||||||
Some(Box::new(SetCurrentInputCommand { name }))
|
Some(Box::new(SetCurrentInputCommand { name }))
|
||||||
}
|
}
|
||||||
|
"set_loop" => {
|
||||||
|
let enabled = request
|
||||||
|
.args
|
||||||
|
.get("enabled")
|
||||||
|
.unwrap_or(&String::new())
|
||||||
|
.parse::<bool>()
|
||||||
|
.ok();
|
||||||
|
Some(Box::new(SetLoopCommand { enabled, id }))
|
||||||
|
}
|
||||||
|
"toggle_loop" => Some(Box::new(ToggleLoopCommand { id })),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -47,7 +47,7 @@ pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pwsp_daemon_output.is_none() {
|
if pwsp_daemon_output.is_none() {
|
||||||
println!("Could not find pwsp-daemon output device, skipping device linking");
|
eprintln!("Could not find pwsp-daemon output device, skipping device linking");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pwsp_daemon_input.is_none() {
|
if pwsp_daemon_input.is_none() {
|
||||||
println!("Could not find pwsp-daemon input device, skipping device linking");
|
eprintln!("Could not find pwsp-daemon input device, skipping device linking");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ pub async fn wait_for_daemon() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn make_request(request: Request) -> Result<Response, Box<dyn Error>> {
|
pub async fn make_request(request: Request) -> Result<Response, Box<dyn Error + Send + Sync>> {
|
||||||
let socket_path = get_runtime_dir().join("daemon.sock");
|
let socket_path = get_runtime_dir().join("daemon.sock");
|
||||||
let mut stream = UnixStream::connect(socket_path).await?;
|
let mut stream = UnixStream::connect(socket_path).await?;
|
||||||
|
|
||||||
|
|||||||
+48
-50
@@ -1,16 +1,15 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
types::{
|
types::{
|
||||||
audio_player::PlayerState,
|
audio_player::{PlayerState, TrackInfo},
|
||||||
config::GuiConfig,
|
config::GuiConfig,
|
||||||
gui::AudioPlayerState,
|
gui::AudioPlayerState,
|
||||||
socket::{Request, Response},
|
socket::{Request, Response},
|
||||||
},
|
},
|
||||||
utils::daemon::{make_request, wait_for_daemon},
|
utils::daemon::{is_daemon_running, make_request},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
error::Error,
|
error::Error,
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
use tokio::time::{Duration, sleep};
|
use tokio::time::{Duration, sleep};
|
||||||
@@ -24,7 +23,17 @@ pub fn get_gui_config() -> GuiConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_request_sync(request: Request) -> Result<Response, Box<dyn Error>> {
|
pub fn make_request_sync(request: Request) -> Result<Response, Box<dyn Error>> {
|
||||||
futures::executor::block_on(make_request(request))
|
tokio::task::block_in_place(|| {
|
||||||
|
tokio::runtime::Handle::current()
|
||||||
|
.block_on(make_request(request))
|
||||||
|
.map_err(|e| e as Box<dyn Error>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_request_async(request: Request) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
make_request(request).await.ok();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_time_pair(position: f32, duration: f32) -> String {
|
pub fn format_time_pair(position: f32, duration: f32) -> String {
|
||||||
@@ -40,54 +49,57 @@ pub fn format_time_pair(position: f32, duration: f32) -> String {
|
|||||||
|
|
||||||
pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerState>>) {
|
pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerState>>) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let sleep_duration = Duration::from_millis(100);
|
let sleep_duration = Duration::from_secs_f32(1.0 / 60.0);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
wait_for_daemon().await.ok();
|
let is_running = is_daemon_running().unwrap_or(false);
|
||||||
|
|
||||||
|
if !is_running {
|
||||||
|
{
|
||||||
|
let mut guard = audio_player_state_shared.lock().unwrap();
|
||||||
|
guard.is_daemon_running = false;
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let state_req = Request::get_state();
|
let state_req = Request::get_state();
|
||||||
let file_path_req = Request::get_current_file_path();
|
let tracks_req = Request::get_tracks();
|
||||||
let is_paused_req = Request::get_is_paused();
|
|
||||||
let volume_req = Request::get_volume();
|
let volume_req = Request::get_volume();
|
||||||
let position_req = Request::get_position();
|
|
||||||
let duration_req = Request::get_duration();
|
|
||||||
let current_input_req = Request::get_input();
|
let current_input_req = Request::get_input();
|
||||||
let all_inputs_req = Request::get_inputs();
|
let all_inputs_req = Request::get_inputs();
|
||||||
|
|
||||||
let state_res = make_request(state_req).await.unwrap_or_default();
|
let (state_res, tracks_res, volume_res, current_input_res, all_inputs_res) = tokio::join!(
|
||||||
let file_path_res = make_request(file_path_req).await.unwrap_or_default();
|
make_request(state_req),
|
||||||
let is_paused_res = make_request(is_paused_req).await.unwrap_or_default();
|
make_request(tracks_req),
|
||||||
let volume_res = make_request(volume_req).await.unwrap_or_default();
|
make_request(volume_req),
|
||||||
let position_res = make_request(position_req).await.unwrap_or_default();
|
make_request(current_input_req),
|
||||||
let duration_res = make_request(duration_req).await.unwrap_or_default();
|
make_request(all_inputs_req),
|
||||||
let current_input_res = make_request(current_input_req).await.unwrap_or_default();
|
);
|
||||||
let all_inputs_res = make_request(all_inputs_req).await.unwrap_or_default();
|
|
||||||
|
let state_res = state_res.unwrap_or_default();
|
||||||
|
let tracks_res = tracks_res.unwrap_or_default();
|
||||||
|
let volume_res = volume_res.unwrap_or_default();
|
||||||
|
let current_input_res = current_input_res.unwrap_or_default();
|
||||||
|
let all_inputs_res = all_inputs_res.unwrap_or_default();
|
||||||
|
|
||||||
let state = match state_res.status {
|
let state = match state_res.status {
|
||||||
true => serde_json::from_str::<PlayerState>(&state_res.message).unwrap(),
|
true => serde_json::from_str::<PlayerState>(&state_res.message).unwrap(),
|
||||||
false => PlayerState::default(),
|
false => PlayerState::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let file_path = match file_path_res.status {
|
let tracks = match tracks_res.status {
|
||||||
true => PathBuf::from(file_path_res.message),
|
true => {
|
||||||
false => PathBuf::new(),
|
serde_json::from_str::<Vec<TrackInfo>>(&tracks_res.message).unwrap_or_default()
|
||||||
};
|
}
|
||||||
let is_paused = match is_paused_res.status {
|
false => vec![],
|
||||||
true => is_paused_res.message == "true",
|
|
||||||
false => false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume = match volume_res.status {
|
let volume = match volume_res.status {
|
||||||
true => volume_res.message.parse::<f32>().unwrap(),
|
true => volume_res.message.parse::<f32>().unwrap(),
|
||||||
false => 0.0,
|
false => 0.0,
|
||||||
};
|
};
|
||||||
let position = match position_res.status {
|
|
||||||
true => position_res.message.parse::<f32>().unwrap(),
|
|
||||||
false => 0.0,
|
|
||||||
};
|
|
||||||
let duration = match duration_res.status {
|
|
||||||
true => duration_res.message.parse::<f32>().unwrap(),
|
|
||||||
false => 0.0,
|
|
||||||
};
|
|
||||||
let current_input = match current_input_res.status {
|
let current_input = match current_input_res.status {
|
||||||
true => current_input_res
|
true => current_input_res
|
||||||
.message
|
.message
|
||||||
@@ -127,25 +139,11 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
|
|||||||
}
|
}
|
||||||
None => state,
|
None => state,
|
||||||
};
|
};
|
||||||
guard.current_file_path = file_path;
|
guard.tracks = tracks.clone();
|
||||||
guard.is_paused = is_paused;
|
guard.volume = volume;
|
||||||
guard.volume = match guard.new_volume {
|
|
||||||
Some(new_volume) => {
|
|
||||||
guard.new_volume = None;
|
|
||||||
new_volume
|
|
||||||
}
|
|
||||||
None => 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.current_input = current_input;
|
||||||
guard.all_inputs = all_inputs;
|
guard.all_inputs = all_inputs;
|
||||||
|
guard.is_daemon_running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
sleep(sleep_duration).await;
|
sleep(sleep_duration).await;
|
||||||
|
|||||||
@@ -165,8 +165,8 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
input_device.input_fr = Some(port)
|
input_device.input_fr = Some(port)
|
||||||
}
|
}
|
||||||
"capture_MONO" => {
|
"capture_MONO" => {
|
||||||
input_device.input_fl = Some(port.clone());
|
input_device.output_fl = Some(port.clone());
|
||||||
input_device.input_fr = Some(port);
|
input_device.output_fr = Some(port);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user