Compare commits

..

159 Commits

Author SHA1 Message Date
arabianq 2f33c48bcc change version to 1.4.0 2026-01-25 00:34:21 +03:00
arabianq 1f48dad71c cargo update 2026-01-25 00:33:49 +03:00
Tarasov Aleksandr 8066f6d96b Merge pull request #8 from arabianq/feature/parallel-sounds
[Feature] Parallel sounds
2026-01-25 00:25:21 +03:00
arabianq 5c3bc30a6e readme: update screenshot 2026-01-25 00:23:42 +03:00
arabianq 90ed05d88d readme: add information about hotkeys and mouse controls 2026-01-25 00:21:53 +03:00
arabianq f59050ef04 feat: shift + enter is now equal to shift + left mouse 2026-01-25 00:20:55 +03:00
arabianq bae10edc99 feat: replace synchronous requests with asynchronous counterparts for improved performance 2026-01-25 00:14:14 +03:00
arabianq cfa2681ba3 feat: enhance file playback controls with shift and ctrl modifiers 2026-01-25 00:00:11 +03:00
arabianq 03e936ac34 feat: sort tracks by their id in AudioPlayer on get_tracks 2026-01-24 23:59:23 +03:00
arabianq f7f96abcbb feat: add functionality to stop all audio tracks with backspace key 2026-01-24 23:46:51 +03:00
arabianq 4d54443593 feat: add master volume slider 2026-01-24 23:45:33 +03:00
arabianq 5afe3dd45b refactor: remove unused requests and variables from start_app_state_thread 2026-01-24 22:36:26 +03:00
arabianq bd75ac6190 refactor: remove unused fields from AudioPlayerState 2026-01-24 22:34:25 +03:00
arabianq 3e6a8b6e79 feat: first attemp to support playing multiple tracks in parallel 2026-01-24 22:18:42 +03:00
arabianq c1c8deb1b3 README: small changes 2026-01-24 20:46:59 +03:00
arabianq dc61342af8 change version to 1.3.2 2026-01-16 16:19:17 +03:00
arabianq fded7f7b3f deps: cargo update 2026-01-16 16:18:44 +03:00
arabianq ec83680333 deps: bump rfd to 0.17.2 2026-01-16 16:18:11 +03:00
arabianq 16e94e71d3 fix: update input device properties to output for capture_MONO 2026-01-16 16:16:27 +03:00
arabianq 56040934e9 cargo update 2026-01-08 02:56:37 +03:00
arabianq 89975cb124 change version to 1.3.1 2026-01-08 02:53:50 +03:00
arabianq 03e20ffa7e deps: bump rfd to 0.17.1 2026-01-08 02:53:16 +03:00
arabianq 8f7ea09ef5 deps: bump clap to 4.5.54 2026-01-08 02:49:54 +03:00
arabianq c639721f2a deps: bump serde_json to 1.0.149 2026-01-08 02:49:11 +03:00
arabianq 84512efda8 deps: bump tokio to 1.49.0 2026-01-08 02:48:23 +03:00
arabianq 6cf4a9744d replace println! with eprintln! for errors 2026-01-08 02:46:44 +03:00
arabianq d9ced4e650 fix: replace .except() with correct error handling 2026-01-08 02:45:07 +03:00
arabianq c1f3f34169 feat: pwsp-gui now uses Request::toggle_loop instead of set Request::set_loop 2026-01-02 04:41:00 +03:00
arabianq 6c06da7b0d feat: implemented toggle-loop 2026-01-02 04:39:48 +03:00
arabianq 023caa4616 change version to 1.3.0 2026-01-02 04:33:00 +03:00
arabianq fc593f8a8e cargo update 2026-01-02 04:24:32 +03:00
arabianq f281ad5a78 deps: bump serde_json to 1.0.148 2026-01-02 04:18:58 +03:00
arabianq fe8fec4ea7 fix: loop -> tokio::select! in pwsp-daemon main loop for less CPU usage 2026-01-02 04:12:49 +03:00
arabianq 7583e72687 fix: remove debug print statements from commands loop 2026-01-02 04:09:21 +03:00
arabianq e4b4a3d198 fix: remove unnecessary mutability in audio player lock 2026-01-02 03:59:56 +03:00
arabianq a7af1caace fix: highly improved GUI performance 2026-01-02 03:59:05 +03:00
arabianq c5086189d6 fix: changed position slider step from 1.0 to 0.01
now it looks fine when playing short files
2026-01-02 03:58:38 +03:00
arabianq ac667f1273 feat: make_request_sync now uses tokio instead of futures 2026-01-02 03:33:20 +03:00
arabianq fdb79e609f fix: deadlock in play_toggle 2026-01-02 03:32:56 +03:00
arabianq 531a49dbdf feat: implemented loop support in pwsp-gui 2026-01-02 03:18:51 +03:00
arabianq 2a13c25230 feat: implemented loop support in pwsp-cli 2026-01-02 02:57:34 +03:00
arabianq 408f5317c4 feat: implemented loop functionality on daemon level 2026-01-02 02:25:30 +03:00
arabianq 3672d6f43e feat: now pwsp-daemon main loop is separated into commands_loop and player_loop 2026-01-02 02:10:58 +03:00
arabianq cc3bec0746 fix: change get_state and get_position methods to take immutable reference 2026-01-02 02:09:10 +03:00
arabianq 180a69bd40 change version to 1.2.0 2025-12-16 21:08:47 +03:00
arabianq 6de51bc11c cargo update 2025-12-16 21:08:47 +03:00
arabianq bc9f086245 fix: do not display files from removed dir 2025-12-16 21:08:47 +03:00
arabianq f84cc03a6c now can select multiple dirs at once 2025-12-16 21:08:47 +03:00
arabianq 96dda1d8a9 fix: incorrect scroll bar rendering 2025-12-16 21:08:47 +03:00
arabianq 87b791ee3a new feature: pause playback when GUI window is closed 2025-12-16 21:08:47 +03:00
arabianq ae5da6d9bd new feature: now can play file without opening any dir 2025-12-16 21:08:47 +03:00
arabianq 546b6fd13f add toggle-pause command to pwsp-cli 2025-12-16 21:08:47 +03:00
arabianq e8d1947f18 change version to 1.1.4 2025-12-16 21:08:47 +03:00
arabianq 9e55cb5f8b cargo update 2025-12-16 21:08:47 +03:00
arabianq 7f57175519 bump clap version to 4.5.53 2025-12-16 21:08:47 +03:00
arabianq b21b32f746 bump egui and eframe to 0.33.3 2025-12-16 21:08:47 +03:00
arabianq 3827d54ba8 change version to 1.1.3 2025-12-16 21:08:47 +03:00
arabianq 9801b0cdf5 cargo update 2025-12-16 21:08:47 +03:00
arabianq 154f33b15b bump rfd to 0.16.0 2025-12-16 21:08:47 +03:00
arabianq fa217c3b25 fix not working with mono input devices 2025-12-16 21:08:47 +03:00
arabianq 1fb8daf389 change version to 1.1.2 2025-12-16 21:08:47 +03:00
arabianq f2213d1def fix incorrect volume slider width 2025-12-16 21:08:47 +03:00
arabianq 6997a6fa71 disable hotkeys when some widget is focused 2025-12-16 21:08:47 +03:00
arabianq e1ae768f26 change version to 1.1.1 2025-12-16 21:08:47 +03:00
arabianq 57c4461952 fix: crash when setting negative position 2025-12-16 21:08:47 +03:00
arabianq 5c46ff4a1e new hotkeys to select dirs, files. 2025-12-16 21:08:47 +03:00
arabianq 9112102c16 cargo update 2025-12-16 21:08:47 +03:00
arabianq 9d1eb8bee9 change version to 1.1.0 2025-12-16 21:08:47 +03:00
arabianq 39fe8a59c7 add 10s timeout before starting pwsp-daemon via systemd service 2025-12-16 21:08:47 +03:00
arabianq 5870f7d440 update dependencies 2025-12-16 21:08:47 +03:00
arabianq 4a060e4669 change version to 1.0.3 2025-12-16 21:08:47 +03:00
arabianq 3981833a3b call pipewire::init in every pipewire thread 2025-12-16 21:08:47 +03:00
arabianq 1ea84dbb28 cargo update 2025-12-16 21:08:47 +03:00
arabianq 195aa56ceb deps: update clap 2025-12-16 21:08:47 +03:00
arabianq b205800a66 deps: update egui, eframe, egui_material_icons 2025-12-16 21:08:47 +03:00
arabianq 5e538db201 fix: now systemd service should wait for pipewire to start 2025-12-16 21:08:47 +03:00
arabianq 0bcd23195f change pwsp version to 1.0.2 2025-12-16 21:08:47 +03:00
arabianq ca0196f927 update dependencies 2025-12-16 21:08:47 +03:00
arabianq 874c8063aa use device name instead of node id to get audio device 2025-12-16 21:08:47 +03:00
arabianq d877f79d5c fix pwsp.spec failed to build 2025-12-16 21:08:47 +03:00
arabianq 3cfd2cb9bd update descriptions in github actions to use new tags format 2025-12-16 21:08:47 +03:00
arabianq bf1f8410c1 change pwsp.spec to use new tags format 2025-12-16 21:08:47 +03:00
arabianq ad7c63617b change package version to 1.0.1 2025-12-16 21:08:47 +03:00
arabianq 00b25dde9d cargo update 2025-12-16 21:08:47 +03:00
arabianq e3118a4b89 bump serde version to 1.0.227 2025-12-16 21:08:47 +03:00
arabianq 23f5ee0d22 change code to work with pipewire 0.9.2 2025-12-16 21:08:47 +03:00
arabianq 5c4df232d5 bump pipewire crate to version 0.9.2 2025-12-16 21:08:47 +03:00
arabianq 7f0e6f7996 change: now, instead of the full path to the file, only its name is displayed at the top 2025-12-16 21:08:47 +03:00
arabianq ab60e6b0ab fix: too large directory names break the interface 2025-12-16 21:08:47 +03:00
arabian 3b3c5c32f9 Delete scripts directory 2025-12-16 21:08:47 +03:00
arabian 74b76dd61d Update README.md 2025-12-16 21:08:47 +03:00
arabian 3b3db608e8 Update release-deb.yml
fix .deb uploading
2025-12-16 21:08:47 +03:00
arabian b1a24cb50e Update release-deb.yml
change name
2025-12-16 21:08:47 +03:00
arabian 1855a8b72c Create release-deb.yml 2025-12-16 21:08:47 +03:00
arabian 42ab513ad9 Update and rename build-release.yml to release-archive.yml 2025-12-16 21:08:47 +03:00
arabian ba04546b17 Update build-release.yml 2025-12-16 21:08:47 +03:00
arabian 6feec98b4c Update build-release.yml
try to fix zip creation
2025-12-16 21:08:47 +03:00
arabian 43e1c60baa Update build-release.yml
change workflow_dispatch description
2025-12-16 21:08:47 +03:00
arabian 2f5fc04244 Update build-release.yml
add build dependencies installation
2025-12-16 21:08:47 +03:00
arabian aa45a49c3a Create build-release.yml 2025-12-16 21:08:47 +03:00
arabian dd8ff8b1bf Update README.md
add installation on Arch Linux from AUR
2025-12-16 21:08:47 +03:00
arabianq 25667ce0f0 update README.md 2025-12-16 21:08:47 +03:00
arabianq ccf48e6426 remove build_rpm.sh and cargo-generate-rpm mentions. Now copr handles rpm builds 2025-12-16 21:08:47 +03:00
arabianq da79be0d22 fix pwsp.spec 2025-12-16 21:08:47 +03:00
arabianq 01afb01198 add missing files to pwsp.spec 2025-12-16 21:08:47 +03:00
arabianq 37723ddc30 specify BuildRequires and %install manually in pwsp.spec 2025-12-16 21:08:47 +03:00
arabianq b672d9bd10 update pwsp.spec file 2025-12-16 21:08:47 +03:00
arabianq fa8a7b7590 update pwsp.spec file 2025-12-16 21:08:47 +03:00
arabianq d3c8706c33 update pwsp.spec file 2025-12-16 21:08:47 +03:00
arabianq 071e805457 fix Source url in pwsp.spec 2025-12-16 21:08:47 +03:00
arabianq 4f30f5047e add pwsp.spec 2025-12-16 21:08:47 +03:00
arabianq e173a27c6e remove generate_rpm_spec.sh 2025-12-16 21:08:47 +03:00
arabianq 5891cc071e use --path . in generate_rpm_spec 2025-12-16 21:08:47 +03:00
arabianq b02acb44fb add rust-pwsp.spec 2025-12-16 21:08:47 +03:00
arabianq c4a01a9f72 add generate_rpm_spec.sh and rust-pwsp.spec 2025-12-16 21:08:47 +03:00
arabianq be5a619b89 fix .rpm configuration 2025-12-16 21:08:47 +03:00
arabianq 00a3aa4ca4 new README 2025-12-16 21:08:47 +03:00
arabianq dee908a347 1.0.0 rewrite 2025-12-16 21:08:47 +03:00
arabianq 4f8681a8c6 clear README 2025-12-16 21:08:47 +03:00
arabianq 675f29f51f remove everything 2025-12-16 21:08:47 +03:00
arabianq 2efe16376c change version to 0.1.8 2025-12-16 21:08:47 +03:00
arabianq 607e30da7c cargo update 2025-12-16 21:08:47 +03:00
arabianq d6f96f3035 better imports 2025-12-16 21:08:47 +03:00
arabianq 81cea863e4 PlayerState::PLAYING -> PlayerState::Playing; PlayerState::PAUSED -> PlayerState::Paused 2025-12-16 21:08:47 +03:00
arabianq ac721d0c4d cargo clippy 2025-12-16 21:08:47 +03:00
arabianq 06f12cd740 bump egui and eframe to 0.32.3 2025-12-16 21:08:47 +03:00
arabianq dda87f3222 change version to 0.1.7 2025-12-16 21:08:47 +03:00
arabianq 8b1c4773c3 update egui and eframe to 0.32.2 2025-12-16 21:08:47 +03:00
arabianq 9b71c105fc change version to 0.1.6 2025-12-16 21:08:47 +03:00
arabianq 82d603cbab update deps 2025-12-16 21:08:47 +03:00
arabianq e6d0e6e128 fix crash when seeking 2025-12-16 21:08:47 +03:00
arabian 4c62d9f1e7 Update README.md 2025-12-16 21:08:47 +03:00
arabianq c40251b2a1 version -> 0.1.5 2025-12-16 21:08:47 +03:00
arabianq b7fa90d37e update dependencies 2025-12-16 21:08:47 +03:00
arabianq e3b787204d rodio -> 0.21.1 2025-12-16 21:08:47 +03:00
arabianq 0a5d61b510 version -> 0.1.4 2025-12-16 21:08:47 +03:00
arabianq ef3cb0f48c fix incorrect dependency for deb package 2025-12-16 21:08:47 +03:00
arabianq 19b5e52ba6 update Cargo.toml 2025-12-16 21:08:47 +03:00
arabianq 6b75654511 egui, eframe -> 0.32.0; egui_material_icons -> 0.4.0 2025-12-16 21:08:47 +03:00
arabianq 22c5ca6384 update build scripts 2025-12-16 21:08:47 +03:00
arabianq eacfaaf8f6 update README 2025-12-16 21:08:47 +03:00
arabianq f3e574e8c9 update screenshot.png 2025-12-16 21:08:47 +03:00
arabianq cc02c6c3c0 new build scripts and .desktop file 2025-12-16 21:08:47 +03:00
arabianq 7ec53f259e version -> 0.1.31 2025-12-16 21:08:47 +03:00
arabianq dddd4c5440 maximum volume -> 1.0 from 5.0 2025-12-16 21:08:47 +03:00
arabianq 1a9836a7d9 version -> 0.1.3 2025-12-16 21:08:47 +03:00
arabianq 26eec0c0ef now use single settings file instead of many; minor refactoring 2025-12-16 21:08:47 +03:00
arabianq 3ba7a79010 new app::run function 2025-12-16 21:08:47 +03:00
arabianq 6ab54d7ec9 move creation of dirs to the separate function 2025-12-16 21:08:47 +03:00
arabianq af4f175e0c split main.rs into main.rs and app.rs 2025-12-16 21:08:47 +03:00
arabianq 9907408541 bump pwsp version to 0.1.2 2025-12-16 21:08:47 +03:00
arabianq b3e0d730da bump edition to 2024 2025-12-16 21:08:47 +03:00
arabianq edfd2ba9b7 fixed setting player position to 0 2025-12-16 21:08:47 +03:00
arabianq f2ce47cdcd bump egui, eframe and rfd versions 2025-12-16 21:08:47 +03:00
arabianq 6b10d365fc v0.1.1 - minor fixes 2025-12-16 21:08:47 +03:00
arabianq da756ccffe Added unlink() function 2025-12-16 21:08:47 +03:00
arabianq 5e99870a71 Update README.md 2025-12-16 21:08:47 +03:00
arabianq ed5d6f63b1 Added Cargo.lock 2025-12-16 21:08:47 +03:00
arabian 89a58c27a0 Create LICENSE 2025-12-16 21:08:47 +03:00
20 changed files with 1220 additions and 1142 deletions
Generated
+183 -754
View File
File diff suppressed because it is too large Load Diff
+7 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pwsp" name = "pwsp"
version = "1.1.3" 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,22 +12,21 @@ 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.51", 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.0", default-features = false, features = ["default_fonts", "rayon"] } egui = { version = "0.33.3", default-features = false, features = ["default_fonts", "rayon"] }
eframe = { version = "0.33.0", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] } eframe = { version = "0.33.3", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
egui_material_icons = "0.5.0" egui_material_icons = "0.5.0"
[[bin]] [[bin]]
+30 -7
View File
@@ -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

+1 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0 %global cargo_install_lib 0
Name: pwsp Name: pwsp
Version: 1.1.3 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
+78 -27
View File
@@ -14,17 +14,17 @@ struct Cli {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Commands { enum Commands {
/// Perform an action (ping, pause, resume, stop, play) /// Perform an action (ping, pause, resume, toggle-pause, stop, play)
Action { Action {
#[clap(subcommand)] #[clap(subcommand)]
action: Actions, action: Actions,
}, },
/// Get information from the player (is paused, volume, position, state) /// Get information from the player (is paused, volume, position, duration, state, current-file-path, input, inputs)
Get { Get {
#[clap(subcommand)] #[clap(subcommand)]
parameter: GetCommands, parameter: GetCommands,
}, },
/// Set information in the player (volume, position) /// Set information in the player (volume, position, input)
Set { Set {
#[clap(subcommand)] #[clap(subcommand)]
parameter: SetCommands, parameter: SetCommands,
@@ -36,13 +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
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)]
@@ -51,14 +74,20 @@ enum GetCommands {
IsPaused, IsPaused,
/// Playback volume /// Playback volume
Volume, Volume,
/// Playback position /// Playback position (in seconds)
Position, Position {
#[clap(short, long)]
id: Option<u32>,
},
/// Duration of the current file /// Duration of the current file
Duration, Duration {
/// Player state #[clap(short, long)]
id: Option<u32>,
},
/// 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
@@ -68,11 +97,25 @@ enum GetCommands {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum SetCommands { enum SetCommands {
/// Playback volume /// Playback volume
Volume { volume: f32 }, Volume {
/// Playback position volume: f32,
Position { position: f32 }, #[clap(short, long)]
/// Input id: Option<u32>,
},
/// Playback position (in seconds)
Position {
position: f32,
#[clap(short, long)]
id: Option<u32>,
},
/// 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]
@@ -84,29 +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::Stop => Request::stop(), Actions::TogglePause { id } => Request::toggle_pause(id),
Actions::Play { file_path } => Request::play(file_path.to_str().unwrap()), 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 { 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
View File
@@ -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;
}
}
+198 -55
View File
@@ -1,14 +1,35 @@
use crate::gui::SoundpadGui; use crate::gui::{SUPPORTED_EXTENSIONS, SoundpadGui};
use egui::{ use egui::{
AtomExt, Button, Color32, ComboBox, FontFamily, Label, RichText, ScrollArea, Slider, TextEdit, Align, AtomExt, Button, Color32, ComboBox, FontFamily, Label, Layout, RichText, ScrollArea,
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(
@@ -48,10 +69,15 @@ impl SoundpadGui {
&mut self.config.save_scale_factor, &mut self.config.save_scale_factor,
"Always remember UI scale factor", "Always remember UI scale factor",
); );
let pause_on_exit_response = ui.checkbox(
&mut self.config.pause_on_exit,
"Pause audio playback when the window is closed",
);
if save_volume_response.changed() if save_volume_response.changed()
|| save_input_response.changed() || save_input_response.changed()
|| save_scale_response.changed() || save_scale_response.changed()
|| pause_on_exit_response.changed()
{ {
self.config.save_to_file().ok(); self.config.save_to_file().ok();
} }
@@ -69,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()
@@ -82,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();
});
} }
fn draw_controls(&mut self, ui: &mut Ui) { 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_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) .show_value(false)
.step_by(1.0); .step_by(0.01);
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()
@@ -119,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) {
@@ -182,6 +277,8 @@ impl SoundpadGui {
ui.set_min_height(area_size.y); ui.set_min_height(area_size.y);
ScrollArea::vertical().id_salt(0).show(ui, |ui| { ScrollArea::vertical().id_salt(0).show(ui, |ui| {
ui.set_min_width(area_size.x);
let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect(); let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect();
dirs.sort(); dirs.sort();
for path in dirs.iter() { for path in dirs.iter() {
@@ -216,10 +313,18 @@ impl SoundpadGui {
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
let add_dir_button = egui::Button::new(icons::ICON_ADD).frame(false); let add_dirs_button = Button::new(icons::ICON_ADD).frame(false);
let add_dir_button_response = ui.add_sized([18.0, 18.0], add_dir_button); let add_dirs_button_response = ui.add_sized([18.0, 18.0], add_dirs_button);
if add_dir_button_response.clicked() { if add_dirs_button_response.clicked() {
self.add_dir(); self.add_dirs();
}
});
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
let play_file_button = Button::new("Play file");
let play_file_button_response = ui.add(play_file_button);
if play_file_button_response.clicked() {
self.open_file();
} }
}); });
}); });
@@ -227,10 +332,6 @@ impl SoundpadGui {
} }
fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) { fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) {
let extensions = [
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "webm", "avi",
];
ui.vertical(|ui| { ui.vertical(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let search_field = ui.add_sized( let search_field = ui.add_sized(
@@ -256,7 +357,7 @@ impl SoundpadGui {
continue; continue;
} }
if !extensions if !SUPPORTED_EXTENSIONS
.contains(&entry_path.extension().unwrap_or_default().to_str().unwrap()) .contains(&entry_path.extension().unwrap_or_default().to_str().unwrap())
{ {
continue; continue;
@@ -289,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);
} }
} }
@@ -300,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();
@@ -309,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
@@ -326,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
View File
@@ -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;
+60 -20
View File
@@ -13,16 +13,20 @@ 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},
}; };
const SUPPORTED_EXTENSIONS: [&str; 11] = [
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "webm", "avi",
];
struct SoundpadGui { struct SoundpadGui {
pub app_state: AppState, pub app_state: AppState,
pub config: GuiConfig, pub config: GuiConfig,
@@ -52,26 +56,39 @@ 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);
} }
pub fn add_dir(&mut self) { 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) {
let file_dialog = FileDialog::new().add_filter("Audio File", &SUPPORTED_EXTENSIONS);
if let Some(path) = file_dialog.pick_file() {
self.play_file(&path, false);
}
}
pub fn add_dirs(&mut self) {
let file_dialog = FileDialog::new(); let file_dialog = FileDialog::new();
if let Some(path) = file_dialog.pick_folder() { if let Some(paths) = file_dialog.pick_folders() {
for path in paths {
self.app_state.dirs.insert(path); self.app_state.dirs.insert(path);
}
self.config.dirs = self.app_state.dirs.clone(); self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok(); self.config.save_to_file().ok();
} }
@@ -83,6 +100,7 @@ impl SoundpadGui {
&& current_dir == path && current_dir == path
{ {
self.app_state.current_dir = None; self.app_state.current_dir = None;
self.app_state.files.clear();
} }
self.config.dirs = self.app_state.dirs.clone(); self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok(); self.config.save_to_file().ok();
@@ -98,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();
@@ -111,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>> {
@@ -138,7 +172,13 @@ pub async fn run() -> Result<(), Box<dyn Error>> {
Ok(Box::new(SoundpadGui::new(&cc.egui_ctx))) Ok(Box::new(SoundpadGui::new(&cc.egui_ctx)))
}), }),
) { ) {
Ok(_) => Ok(()), Ok(_) => {
let config = get_gui_config();
if config.pause_on_exit {
make_request_sync(Request::pause(None)).ok();
}
Ok(())
}
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
} }
} }
+51 -35
View File
@@ -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);
} }
} }
+218 -90
View File
@@ -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();
}
} else {
for sound in self.tracks.values_mut() {
sound.sink.pause();
}
} }
} }
pub fn resume(&mut self) { pub fn resume(&mut self, id: Option<u32>) {
if self.get_state() == PlayerState::Paused { if let Some(id) = id {
self.sink.play(); 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) { pub fn stop(&mut self, id: Option<u32>) {
self.sink.stop(); if let Some(id) = id {
self.tracks.remove(&id);
} else {
self.tracks.clear();
}
} }
pub fn is_paused(&self) -> bool { pub fn is_paused(&self) -> bool {
self.sink.is_paused() if self.tracks.is_empty() {
return false;
}
self.tracks.values().all(|s| s.sink.is_paused())
} }
pub fn get_state(&mut self) -> PlayerState { pub fn get_state(&self) -> PlayerState {
if self.sink.len() == 0 { if self.tracks.is_empty() {
return PlayerState::Stopped; return PlayerState::Stopped;
} }
if self.sink.is_paused() { if self
.tracks
.values()
.any(|s| !s.sink.is_paused() && !s.sink.empty())
{
return PlayerState::Playing;
}
if self.is_paused() {
return PlayerState::Paused; return PlayerState::Paused;
} }
PlayerState::Playing PlayerState::Stopped
} }
pub fn set_volume(&mut self, volume: f32) { pub fn set_volume(&mut self, volume: f32, id: Option<u32>) {
self.volume = volume; if let Some(id) = id {
self.sink.set_volume(volume); if let Some(sound) = self.tracks.get_mut(&id) {
sound.volume = volume;
sound.sink.set_volume(self.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 { self.volume = volume;
Some(duration) => Ok(duration), for sound in self.tracks.values_mut() {
None => Err("Couldn't determine duration for current file".into()), sound.sink.set_volume(self.volume * sound.volume);
} }
} }
} }
pub async fn play(&mut self, file_path: &Path) -> Result<(), Box<dyn Error>> { 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;
} }
&self.current_file_path } else {
// Set loop for all? Or just last?
// Let's set for all.
for sound in self.tracks.values_mut() {
sound.looped = enabled;
}
}
}
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>> {
+125 -26
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
types::socket::Response, types::{audio_player::PlayerState, socket::Response},
utils::{daemon::get_audio_player, pipewire::get_all_devices}, utils::{daemon::get_audio_player, pipewire::get_all_devices},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -12,11 +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 StopCommand {} pub struct TogglePauseCommand {
pub id: Option<u32>,
}
pub struct StopCommand {
pub id: Option<u32>,
}
pub struct IsPausedCommand {} pub struct IsPausedCommand {}
@@ -26,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 {}
@@ -50,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 {
@@ -61,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")
} }
} }
@@ -70,16 +96,54 @@ 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")
} }
} }
#[async_trait]
impl Executable for TogglePauseCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
if audio_player.get_state() == PlayerState::Stopped {
return Response::new(false, "Audio is not playing");
}
// 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 {
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")
}
}
}
}
#[async_trait] #[async_trait]
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")
} }
} }
@@ -96,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())
} }
@@ -116,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")
@@ -127,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())
} }
} }
@@ -138,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()),
} }
@@ -152,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()),
} }
@@ -164,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 {
@@ -175,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")
}
} }
} }
@@ -235,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")
}
}
}
+2
View File
@@ -36,6 +36,7 @@ pub struct GuiConfig {
pub save_volume: bool, pub save_volume: bool,
pub save_input: bool, pub save_input: bool,
pub save_scale_factor: bool, pub save_scale_factor: bool,
pub pause_on_exit: bool,
pub dirs: HashSet<PathBuf>, pub dirs: HashSet<PathBuf>,
} }
@@ -48,6 +49,7 @@ impl Default for GuiConfig {
save_volume: false, save_volume: false,
save_input: false, save_input: false,
save_scale_factor: false, save_scale_factor: false,
pause_on_exit: false,
dirs: HashSet::default(), dirs: HashSet::default(),
} }
+22 -11
View File
@@ -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,
} }
+90 -18
View File
@@ -24,20 +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 stop() -> Self { pub fn toggle_pause(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("toggle_pause", args)
} }
pub fn play(file_path: &str) -> Self { pub fn stop(id: Option<u32>) -> Self {
Request::new("play", vec![("file_path", file_path)]) 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, concurrent: bool) -> Self {
Request::new(
"play",
vec![
("file_path", file_path),
("concurrent", &concurrent.to_string()),
],
)
} }
pub fn get_is_paused() -> Self { pub fn get_is_paused() -> Self {
@@ -48,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 {
@@ -72,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 -9
View File
@@ -3,11 +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 })),
"stop" => Some(Box::new(StopCommand {})), "toggle_pause" => Some(Box::new(TogglePauseCommand { id })),
"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 {})),
@@ -18,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
@@ -28,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
@@ -38,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
View File
@@ -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
View File
@@ -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;
+2 -2
View File
@@ -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);
} }
_ => {} _ => {}
} }