Compare commits

..

39 Commits

Author SHA1 Message Date
arabianq 330c3d79d4 cargo update 2026-01-28 03:40:43 +03:00
arabianq dff20daace deps: bump clap to 4.5.55 2026-01-28 03:39:39 +03:00
arabianq cdc44328a8 docs: update README to include new features for collapsible audio tracks, drag and drop directories, and automatic device detection 2026-01-28 03:38:22 +03:00
arabianq ac61a71dcb feat: bump version to 1.5.0 2026-01-28 03:35:04 +03:00
arabianq 71c800c396 docs(assets): update screenshot image 2026-01-28 03:34:32 +03:00
arabianq 577a6d279b fix(daemon): remove unnecessary ExecStartPre sleep command 2026-01-28 03:32:29 +03:00
arabianq 49e01f0318 fix(gui): correct calculation of vertical separator's position 2026-01-28 03:31:54 +03:00
arabianq 5ea9b3b0ba feat(daemon): implementet get full-state command 2026-01-28 02:41:33 +03:00
arabianq ca85d4c369 refactor: remove redundant device linking in play method 2026-01-28 02:28:23 +03:00
arabianq 4499b1d3aa feat(gui): now directories can be reordered using drag and drop 2026-01-28 02:10:36 +03:00
arabianq d385e5356e refactor: simplify device retrieval in link_player_to_virtual_mic function 2026-01-28 01:30:03 +03:00
arabianq b4a0dc6a83 feat: now pwsp will automatically detect when input device is connected/disconnected and properly link/unlink it 2026-01-28 01:26:43 +03:00
arabianq 2e570b3bb0 fix: navigating through files using keyboard now works correctly with filtered files 2026-01-28 00:45:52 +03:00
arabianq ee4554286e refactor: improved filtering functionality 2026-01-28 00:45:20 +03:00
arabianq 2c6f0d932e refactor: refactor input handling for Enter key and directory navigation 2026-01-28 00:34:44 +03:00
arabianq 4e7606fdc6 feat: remove escape key functionality from input handling 2026-01-28 00:28:34 +03:00
arabianq 03df631690 refactor: enhance search field focus functionality and input handling 2026-01-28 00:28:08 +03:00
arabianq 6df826f210 feat: you can now collapse every audio track 2026-01-28 00:03:56 +03:00
arabianq cdf306cfe9 feat: make vertical separator in GUI adjustable 2026-01-27 23:51:14 +03:00
arabianq 74a436b171 fix: add serde default attribute to DaemonConfig and GuiConfig structs 2026-01-27 23:50:50 +03:00
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
21 changed files with 1317 additions and 753 deletions
Generated
+164 -72
View File
@@ -182,7 +182,7 @@ dependencies = [
"bitflags 2.10.0",
"cexpr",
"clang-sys",
"itertools",
"itertools 0.13.0",
"proc-macro2",
"quote",
"regex",
@@ -327,9 +327,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.51"
version = "1.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -354,9 +354,9 @@ dependencies = [
[[package]]
name = "cfg-expr"
version = "0.20.5"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21be0e1ce6cdb2ee7fff840f922fb04ead349e5cfb1e750b769132d44ce04720"
checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a"
dependencies = [
"smallvec",
"target-lexicon",
@@ -396,9 +396,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.54"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785"
dependencies = [
"clap_builder",
"clap_derive",
@@ -406,9 +406,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.54"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61"
dependencies = [
"anstyle",
"clap_lex",
@@ -417,9 +417,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.49"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck",
"proc-macro2",
@@ -429,9 +429,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.6"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "clipboard-win"
@@ -461,6 +461,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "concat-idents"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -782,7 +792,7 @@ dependencies = [
"epaint",
"log",
"profiling",
"thiserror 2.0.17",
"thiserror 2.0.18",
"type-map",
"web-time",
"wgpu",
@@ -810,6 +820,29 @@ dependencies = [
"winit",
]
[[package]]
name = "egui_animation"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3db554dd3784f469d804f7dc25d1b14a2e00f1608d7af60218ccbced720a6e8"
dependencies = [
"egui",
"hello_egui_utils",
"simple-easing",
]
[[package]]
name = "egui_dnd"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f535b8df7ca89f781954feaa505899c8955b550074ecafcaa3c040f67aec3d46"
dependencies = [
"egui",
"egui_animation",
"simple-easing",
"web-time",
]
[[package]]
name = "egui_glow"
version = "0.33.3"
@@ -944,15 +977,15 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "flate2"
version = "1.1.5"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1000,6 +1033,31 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "gethostname"
version = "1.1.0"
@@ -1012,9 +1070,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
@@ -1155,6 +1213,16 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hello_egui_utils"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d09c2c7f3aa61624b1bec320be9029f30e06769f92e39ac99a6d6e01024ae8"
dependencies = [
"concat-idents",
"egui",
]
[[package]]
name = "hermit-abi"
version = "0.5.2"
@@ -1302,6 +1370,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.17"
@@ -1342,9 +1419,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.83"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1364,9 +1441,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.179"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libloading"
@@ -1380,9 +1457,9 @@ dependencies = [
[[package]]
name = "libm"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
@@ -1553,7 +1630,7 @@ dependencies = [
"num-traits",
"once_cell",
"rustc-hash 1.1.0",
"thiserror 2.0.17",
"thiserror 2.0.18",
"unicode-ident",
]
@@ -2092,6 +2169,12 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pipewire"
version = "0.9.2"
@@ -2106,7 +2189,7 @@ dependencies = [
"nix",
"once_cell",
"pipewire-sys",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -2194,9 +2277,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.105"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
@@ -2209,14 +2292,16 @@ checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]]
name = "pwsp"
version = "1.3.1"
version = "1.5.0"
dependencies = [
"async-trait",
"clap",
"dirs",
"eframe",
"egui",
"egui_dnd",
"egui_material_icons",
"itertools 0.14.0",
"pipewire",
"rfd",
"rodio",
@@ -2251,9 +2336,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
@@ -2323,9 +2408,9 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"getrandom 0.2.17",
"libredox",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -2365,9 +2450,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "rfd"
version = "0.17.1"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069d6129dede311430d0dcf1ded88a7affc7a342c2d8e6336043d43ed14dac17"
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
dependencies = [
"block2 0.6.2",
"dispatch2",
@@ -2538,6 +2623,12 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "simple-easing"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1"
[[package]]
name = "slab"
version = "0.4.11"
@@ -2598,7 +2689,7 @@ dependencies = [
"log",
"memmap2",
"rustix 1.1.3",
"thiserror 2.0.17",
"thiserror 2.0.18",
"wayland-backend",
"wayland-client",
"wayland-csd-frame",
@@ -2633,9 +2724,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
@@ -2906,11 +2997,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.17"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl 2.0.17",
"thiserror-impl 2.0.18",
]
[[package]]
@@ -2926,9 +3017,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.17"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
@@ -2989,9 +3080,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.10+spec-1.1.0"
version = "0.9.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
dependencies = [
"indexmap",
"serde_core",
@@ -3136,18 +3227,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
@@ -3158,11 +3249,12 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.56"
version = "0.4.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
dependencies = [
"cfg-if",
"futures-util",
"js-sys",
"once_cell",
"wasm-bindgen",
@@ -3171,9 +3263,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3181,9 +3273,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -3194,9 +3286,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
@@ -3338,9 +3430,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.83"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3425,7 +3517,7 @@ dependencies = [
"raw-window-handle",
"rustc-hash 1.1.0",
"smallvec",
"thiserror 2.0.17",
"thiserror 2.0.18",
"wgpu-core-deps-windows-linux-android",
"wgpu-hal",
"wgpu-types",
@@ -3456,7 +3548,7 @@ dependencies = [
"portable-atomic-util",
"raw-window-handle",
"renderdoc-sys",
"thiserror 2.0.17",
"thiserror 2.0.18",
"wgpu-types",
]
@@ -3470,7 +3562,7 @@ dependencies = [
"bytemuck",
"js-sys",
"log",
"thiserror 2.0.17",
"thiserror 2.0.18",
"web-sys",
]
@@ -3811,9 +3903,9 @@ dependencies = [
[[package]]
name = "wit-bindgen"
version = "0.46.0"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "writeable"
@@ -3909,18 +4001,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.33"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.33"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
dependencies = [
"proc-macro2",
"quote",
@@ -3983,9 +4075,9 @@ dependencies = [
[[package]]
name = "zmij"
version = "1.0.12"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
[[package]]
name = "zune-core"
+6 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "pwsp"
version = "1.3.1"
version = "1.5.0"
edition = "2024"
authors = ["arabian"]
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
@@ -18,16 +18,18 @@ async-trait = "0.1.89"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
clap = { version = "4.5.54", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] }
clap = { version = "4.5.55", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] }
dirs = "6.0.0"
itertools = "0.14.0"
rodio = { version = "0.21.1", default-features = false, features = ["symphonia-all", "playback"] }
pipewire = "0.9.2"
rfd = { version = "0.17.1", default-features = false, features = ["xdg-portal"]}
rfd = { version = "0.17.2", default-features = false, features = ["xdg-portal"]}
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"] }
egui_material_icons = "0.5.0"
egui_dnd = "0.14.0"
[[bin]]
name = "pwsp-daemon"
@@ -56,4 +58,4 @@ assets = [
["assets/pwsp-gui.desktop", "usr/share/applications/pwsp.desktop", "644"],
["assets/icon.png", "usr/share/icons/hicolor/256x256/apps/pwsp.png", "644"],
["assets/pwsp-daemon.service", "usr/lib/systemd/user/pwsp-daemon.service", "644"],
]
]
+32 -7
View File
@@ -24,6 +24,9 @@ chats on platforms like **Discord, Zoom, or Teamspeak**.
* **Position slider** to fast-forward or rewind the audio.
* **Persistent Configuration**: The list of added directories and your selected audio output device are saved
automatically, so you won't need to reconfigure them every time you launch the application.
* **Collapsible Audio Tracks**: You can collapse every audio track to save space.
* **Drag and Drop Directories**: Reorder your sound directories easily using drag and drop.
* **Automatic Device Detection**: PWSP automatically detects when an input device is connected or disconnected and handles linking/unlinking.
# **⚙️ How It Works**
@@ -35,8 +38,8 @@ three main components:
* Creating and managing virtual audio devices.
* Linking these devices within the PipeWire graph.
* Handling all audio playback.
* **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.
* **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.
* **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.
@@ -44,10 +47,10 @@ three main components:
## **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).
## **Fedora Linux**
## **Fedora Linux (and derivatives)**
If you're using Fedora, you can install PWSP from a dedicated repository using DNF.
@@ -137,12 +140,12 @@ You can start the daemon from the terminal or enable the systemd service for aut
### **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.
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**.
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**
@@ -173,6 +176,28 @@ pwsp-cli --help
pwsp-cli set position 20
```
### **Hotkeys & Controls**
#### **Keyboard Shortcuts**
| Key | Action |
| :----------------------- | :--------------------------------------------------- |
| **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**
Contributions are welcome\! If you have ideas for improvements or find a bug, feel free to create
-1
View File
@@ -3,7 +3,6 @@ Description=Pipewire Soundpad Daemon
After=pipewire.service
[Service]
ExecStartPre=/usr/bin/sleep 10
ExecStart=/usr/bin/pwsp-daemon
Restart=no
RuntimeDirectory=pwsp
Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 84 KiB

+1 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0
Name: pwsp
Version: 1.3.1
Version: 1.5.0
Release: %autorelease
Summary: Lets you play audio files through your microphone
+68 -28
View File
@@ -36,17 +36,36 @@ enum Actions {
/// Ping the daemon
Ping,
/// Pause audio playback
Pause,
Pause {
#[clap(short, long)]
id: Option<u32>,
},
/// Resume audio playback
Resume,
Resume {
#[clap(short, long)]
id: Option<u32>,
},
/// Toggle pause
TogglePause,
TogglePause {
#[clap(short, long)]
id: Option<u32>,
},
/// Stop audio playback and clear the queue
Stop,
Stop {
#[clap(short, long)]
id: Option<u32>,
},
/// Play a file
Play { file_path: PathBuf },
Play {
file_path: PathBuf,
#[clap(short, long)]
concurrent: bool,
},
/// Toggle loop
ToggleLoop,
ToggleLoop {
#[clap(short, long)]
id: Option<u32>,
},
}
#[derive(Subcommand, Debug)]
@@ -56,31 +75,49 @@ enum GetCommands {
/// Playback volume
Volume,
/// Playback position (in seconds)
Position,
Position {
#[clap(short, long)]
id: Option<u32>,
},
/// Duration of the current file
Duration,
Duration {
#[clap(short, long)]
id: Option<u32>,
},
/// Player state (Playing, Paused or Stopped)
State,
/// Current playing file path
CurrentFilePath,
/// Get all playing tracks
Tracks,
/// Current audio input
Input,
/// All audio inputs
Inputs,
/// Is loop enabled (true or false)
Loop,
/// Full player state
FullState,
}
#[derive(Subcommand, Debug)]
enum SetCommands {
/// Playback volume
Volume { volume: f32 },
Volume {
volume: f32,
#[clap(short, long)]
id: Option<u32>,
},
/// Playback position (in seconds)
Position { position: f32 },
Position {
position: f32,
#[clap(short, long)]
id: Option<u32>,
},
/// Audio input id (see pwsp-cli get inputs)
Input { name: String },
/// Enable or disable loop (true or false)
Loop { enabled: String },
Loop {
enabled: String,
#[clap(short, long)]
id: Option<u32>,
},
}
#[tokio::main]
@@ -92,29 +129,32 @@ async fn main() -> Result<(), Box<dyn Error>> {
let request = match cli.command {
Commands::Action { action } => match action {
Actions::Ping => Request::ping(),
Actions::Pause => Request::pause(),
Actions::Resume => Request::resume(),
Actions::TogglePause => Request::toggle_pause(),
Actions::Stop => Request::stop(),
Actions::Play { file_path } => Request::play(file_path.to_str().unwrap()),
Actions::ToggleLoop => Request::toggle_loop(),
Actions::Pause { id } => Request::pause(id),
Actions::Resume { id } => Request::resume(id),
Actions::TogglePause { id } => Request::toggle_pause(id),
Actions::Stop { id } => Request::stop(id),
Actions::Play {
file_path,
concurrent,
} => Request::play(file_path.to_str().unwrap(), concurrent),
Actions::ToggleLoop { id } => Request::toggle_loop(id),
},
Commands::Get { parameter } => match parameter {
GetCommands::IsPaused => Request::get_is_paused(),
GetCommands::Volume => Request::get_volume(),
GetCommands::Position => Request::get_position(),
GetCommands::Duration => Request::get_duration(),
GetCommands::Position { id } => Request::get_position(id),
GetCommands::Duration { id } => Request::get_duration(id),
GetCommands::State => Request::get_state(),
GetCommands::CurrentFilePath => Request::get_current_file_path(),
GetCommands::Tracks => Request::get_tracks(),
GetCommands::Input => Request::get_input(),
GetCommands::Inputs => Request::get_inputs(),
GetCommands::Loop => Request::get_loop(),
GetCommands::FullState => Request::get_full_state(),
},
Commands::Set { parameter } => match parameter {
SetCommands::Volume { volume } => Request::set_volume(volume),
SetCommands::Position { position } => Request::seek(position),
SetCommands::Volume { volume, id } => Request::set_volume(volume, id),
SetCommands::Position { position, id } => Request::seek(position, id),
SetCommands::Input { name } => Request::set_input(&name),
SetCommands::Loop { enabled } => Request::set_loop(&enabled),
SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id),
},
};
+2 -16
View File
@@ -1,8 +1,5 @@
use pwsp::{
types::{
audio_player::PlayerState,
socket::{Request, Response},
},
types::socket::{Request, Response},
utils::{
commands::parse_command,
daemon::{
@@ -122,18 +119,7 @@ async fn player_loop() {
loop {
let mut audio_player = get_audio_player().await.lock().await;
// Start playback again if loop is enabled
let should_play = audio_player.get_state() == PlayerState::Stopped
&& audio_player.current_file_path.is_some()
&& audio_player.looped;
if should_play {
let file_path = audio_player.current_file_path.clone().unwrap();
audio_player
.play(&file_path)
.await
.expect("Something went wrong while trying to play the file");
}
audio_player.update().await;
sleep(Duration::from_millis(100)).await;
}
+223 -93
View File
@@ -1,14 +1,36 @@
use crate::gui::{SUPPORTED_EXTENSIONS, SoundpadGui};
use crate::gui::SoundpadGui;
use egui::{
Align, AtomExt, Button, Color32, ComboBox, FontFamily, Label, Layout, RichText, ScrollArea,
Slider, TextEdit, Ui, Vec2,
Align, AtomExt, Button, CollapsingHeader, Color32, ComboBox, CursorIcon, FontFamily, Label,
Layout, RichText, ScrollArea, Sense, Slider, TextEdit, Ui, Vec2,
};
use egui_dnd::dnd;
use egui_material_icons::icons;
use pwsp::types::audio_player::PlayerState;
use pwsp::types::audio_player::TrackInfo;
use pwsp::utils::gui::format_time_pair;
use std::{error::Error, path::PathBuf};
use std::{error::Error, time::Instant};
use pwsp::types::gui::AppState;
enum TrackAction {
Pause(u32),
Resume(u32),
ToggleLoop(u32),
Stop(u32),
}
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) {
ui.centered_and_justified(|ui| {
ui.label(
@@ -74,45 +96,101 @@ impl SoundpadGui {
fn draw_header(&mut self, ui: &mut Ui) {
ui.vertical_centered_justified(|ui| {
// Current file name
ui.label(
RichText::new(
self.audio_player_state
.current_file_path
.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
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 {
CollapsingHeader::new(
RichText::new(
track
.path
.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
)
.color(Color32::WHITE)
.family(FontFamily::Monospace),
)
.color(Color32::WHITE)
.family(FontFamily::Monospace),
);
// Media controls
self.draw_controls(ui);
ui.separator();
.default_open(true)
.show(ui, |ui| {
if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, &track) {
action = Some(act);
}
});
ui.separator();
}
if let Some(action) = action {
match action {
TrackAction::Pause(id) => self.pause(Some(id)),
TrackAction::Resume(id) => self.resume(Some(id)),
TrackAction::ToggleLoop(id) => self.toggle_loop(Some(id)),
TrackAction::Stop(id) => self.stop(Some(id)),
}
}
});
}
fn draw_controls(&mut self, ui: &mut Ui) {
fn draw_track_control(
ui: &mut Ui,
app_state: &mut AppState,
track: &TrackInfo,
) -> Option<TrackAction> {
let ui_state = app_state.track_ui_states.entry(track.id).or_default();
let should_update_position = !ui_state.position_dragged
&& ui_state
.ignore_position_update_until
.map(|t| Instant::now() > t)
.unwrap_or(true);
if should_update_position {
ui_state.position_slider_value = track.position;
}
let should_update_volume = !ui_state.volume_dragged
&& ui_state
.ignore_volume_update_until
.map(|t| Instant::now() > t)
.unwrap_or(true);
if should_update_volume {
ui_state.volume_slider_value = track.volume;
}
let mut action = None;
ui.horizontal_top(|ui| {
// ---------- Play Button ----------
let play_button = Button::new(match self.audio_player_state.state {
PlayerState::Playing => icons::ICON_PAUSE,
PlayerState::Paused | PlayerState::Stopped => icons::ICON_PLAY_ARROW,
let play_button = Button::new(if track.paused {
icons::ICON_PLAY_ARROW
} else {
icons::ICON_PAUSE
})
.corner_radius(15.0);
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
if play_button_response.clicked() {
self.play_toggle();
if track.paused {
action = Some(TrackAction::Resume(track.id));
} else {
action = Some(TrackAction::Pause(track.id));
}
}
// --------------------------------
// ---------- Loop Button ----------
let loop_button = Button::new(
RichText::new(match self.audio_player_state.looped {
true => icons::ICON_REPEAT_ONE,
false => icons::ICON_REPEAT,
RichText::new(if track.looped {
icons::ICON_REPEAT_ONE
} else {
icons::ICON_REPEAT
})
.size(18.0),
)
@@ -120,17 +198,15 @@ impl SoundpadGui {
let loop_button_response = ui.add_sized([15.0, 30.0], loop_button);
if loop_button_response.clicked() {
self.toggle_loop();
action = Some(TrackAction::ToggleLoop(track.id));
}
// --------------------------------
// ---------- Position Slider ----------
let position_slider = Slider::new(
&mut self.app_state.position_slider_value,
0.0..=self.audio_player_state.duration,
)
.show_value(false)
.step_by(0.01);
let duration = track.duration.unwrap_or(1.0);
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
.show_value(false)
.step_by(0.01);
let default_slider_width = ui.spacing().slider_width;
let position_slider_width = ui.available_width()
@@ -140,57 +216,81 @@ impl SoundpadGui {
ui.spacing_mut().slider_width = position_slider_width;
let position_slider_response = ui.add_sized([30.0, 30.0], position_slider);
if position_slider_response.drag_stopped() {
self.app_state.position_dragged = true;
ui_state.position_dragged = true;
}
// --------------------------------
// ---------- Time Label ----------
let time_label = Label::new(
RichText::new(format_time_pair(
self.audio_player_state.position,
self.audio_player_state.duration,
))
.monospace(),
);
let time_label =
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
ui.add_sized([30.0, 30.0], time_label);
// --------------------------------
// ---------- Volume Icon ----------
let volume_icon = if self.audio_player_state.volume > 0.7 {
icons::ICON_VOLUME_UP
} else if self.audio_player_state.volume == 0.0 {
icons::ICON_VOLUME_OFF
} 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, 30.0], volume_icon);
let volume_icon = Self::get_volume_icon(track.volume);
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
ui.add_sized([30.0, 30.0], volume_label)
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
// --------------------------------
// ---------- Volume Slider ----------
let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0)
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
.show_value(false)
.step_by(0.01);
ui.spacing_mut().slider_width = default_slider_width;
ui.spacing_mut().slider_width = default_slider_width - 30.0;
ui.spacing_mut().item_spacing.x = 0.0;
let volume_slider_response = ui.add_sized([30.0, 30.0], volume_slider);
if volume_slider_response.drag_stopped() {
self.app_state.volume_dragged = true;
ui_state.volume_dragged = true;
}
// --------------------------------
// ---------- Stop Button ---------
let stop_button = Button::new(icons::ICON_CLOSE).frame(false);
let stop_button_response = ui.add_sized([30.0, 30.0], stop_button);
if stop_button_response.clicked() {
action = Some(TrackAction::Stop(track.id));
}
// --------------------------------
});
action
}
fn draw_body(&mut self, ui: &mut Ui) {
let dirs_size = Vec2::new(ui.available_width() / 4.0, ui.available_height() - 40.0);
let left_panel_width = self
.config
.left_panel_width
.max(100.0)
.min(ui.available_width() - 100.0);
let dirs_size = Vec2::new(left_panel_width, ui.available_height() - 40.0);
ui.horizontal(|ui| {
self.draw_dirs(ui, dirs_size);
ui.separator();
let (rect, response) = ui.allocate_at_least(
Vec2::new(ui.spacing().item_spacing.x, ui.available_height()),
Sense::click_and_drag(),
);
if ui.is_rect_visible(rect) {
let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
ui.painter().vline(rect.center().x, rect.y_range(), stroke);
}
let vertical_separator_response =
response.on_hover_and_drag_cursor(CursorIcon::ResizeHorizontal);
if vertical_separator_response.dragged() {
self.config.left_panel_width += vertical_separator_response.drag_delta().x;
self.config.left_panel_width = self.config.left_panel_width.clamp(100.0, 500.0);
}
if vertical_separator_response.drag_stopped() {
self.config.save_to_file().ok();
}
let files_size = Vec2::new(ui.available_width(), ui.available_height() - 40.0);
self.draw_files(ui, files_size);
@@ -205,10 +305,14 @@ impl SoundpadGui {
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();
dirs.sort();
for path in dirs.iter() {
let mut dirs = self.app_state.dirs.clone();
dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| {
let path = item.clone();
ui.horizontal(|ui| {
handle.ui(ui, |ui| {
ui.label(icons::ICON_DRAG_INDICATOR);
});
let name = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
@@ -216,7 +320,7 @@ impl SoundpadGui {
let mut dir_button_text = RichText::new(name.clone());
if let Some(current_dir) = &self.app_state.current_dir {
if current_dir.eq(path) {
if current_dir.eq(&path) {
dir_button_text = dir_button_text.color(Color32::WHITE);
}
}
@@ -226,7 +330,7 @@ impl SoundpadGui {
let dir_button_response = ui.add(dir_button);
if dir_button_response.clicked() {
self.open_dir(path);
self.open_dir(&path);
}
let delete_dir_button = Button::new(icons::ICON_DELETE).frame(false);
@@ -236,7 +340,8 @@ impl SoundpadGui {
self.remove_dir(&path.clone());
}
});
}
});
self.app_state.dirs = dirs;
ui.horizontal(|ui| {
let add_dirs_button = Button::new(icons::ICON_ADD).frame(false);
@@ -260,12 +365,17 @@ impl SoundpadGui {
fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) {
ui.vertical(|ui| {
ui.horizontal(|ui| {
let search_field = ui.add_sized(
let search_field_response = ui.add_sized(
[ui.available_width(), 22.0],
TextEdit::singleline(&mut self.app_state.search_query).hint_text("Search..."),
);
self.app_state.search_field_id = Some(search_field.id);
if self.app_state.force_focus_search {
search_field_response.request_focus();
self.app_state.force_focus_search = false;
}
self.app_state.search_field_id = Some(search_field_response.id);
});
ui.separator();
@@ -275,37 +385,15 @@ impl SoundpadGui {
ui.set_min_height(area_size.y);
ui.vertical(|ui| {
let mut files: Vec<PathBuf> = self.app_state.files.iter().cloned().collect();
files.sort();
let files = self.get_filtered_files();
for entry_path in files {
if entry_path.is_dir() {
continue;
}
if !SUPPORTED_EXTENSIONS
.contains(&entry_path.extension().unwrap_or_default().to_str().unwrap())
{
continue;
}
let file_name = entry_path
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
let search_query = self
.app_state
.search_query
.to_lowercase()
.trim()
.to_string();
if !file_name.to_lowercase().contains(search_query.as_str()) {
continue;
}
let mut file_button_text = RichText::new(file_name);
if let Some(current_file) = &self.app_state.selected_file {
if current_file.eq(&entry_path) {
@@ -316,7 +404,18 @@ impl SoundpadGui {
let file_button = Button::new(file_button_text).frame(false);
let file_button_response = ui.add(file_button);
if file_button_response.clicked() {
self.play_file(&entry_path);
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);
}
}
@@ -327,7 +426,7 @@ impl SoundpadGui {
fn draw_footer(&mut self, ui: &mut Ui) {
ui.add_space(5.0);
ui.horizontal_top(|ui| {
ui.horizontal(|ui| {
// ---------- Microphone selection ----------
let mut mics: Vec<(&String, &String)> =
self.audio_player_state.all_inputs.iter().collect();
@@ -336,6 +435,7 @@ impl SoundpadGui {
let mut selected_input = self.audio_player_state.current_input.to_owned();
let prev_input = selected_input.to_owned();
ComboBox::from_label("Choose microphone")
.height(30.0)
.selected_text(
self.audio_player_state
.all_inputs
@@ -353,10 +453,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);
// ---------- 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);
if settings_button_response.clicked() {
self.app_state.show_settings = true;
+92 -84
View File
@@ -1,107 +1,115 @@
use crate::gui::SoundpadGui;
use egui::{Context, Key};
use egui::{Context, Key, Modifiers};
use std::path::PathBuf;
impl SoundpadGui {
fn key_pressed(&self, ctx: &Context, key: Key) -> bool {
ctx.input(|i| i.key_pressed(key))
}
fn modifiers(&self, ctx: &Context) -> Modifiers {
ctx.input(|i| i.modifiers)
}
pub fn handle_input(&mut self, ctx: &Context) {
if ctx.memory(|reader| { reader.focused() }.is_some()) {
return;
let modifiers = self.modifiers(ctx);
// Open/close settings
if self.key_pressed(ctx, Key::I) {
self.app_state.show_settings = !self.app_state.show_settings;
}
ctx.input(|i| {
// Close app on espace
if i.key_pressed(Key::Escape) {
std::process::exit(0);
if !self.app_state.show_settings {
// Pause / resume audio on space
if self.key_pressed(ctx, Key::Space) {
self.play_toggle();
}
// Open/close settings
if i.key_pressed(Key::I) {
self.app_state.show_settings = !self.app_state.show_settings;
// Stop all audio tracks on backspace
if self.key_pressed(ctx, Key::Backspace) {
self.stop(None);
}
if i.key_pressed(Key::Enter) && self.app_state.selected_file.is_some() {
self.play_file(&self.app_state.selected_file.clone().unwrap());
// Focus search field
if self.key_pressed(ctx, Key::Slash) {
self.app_state.force_focus_search = true;
}
if !self.app_state.show_settings {
// Pause / resume audio on space
if i.key_pressed(Key::Space) {
self.play_toggle();
// Play selected file on Enter
if self.key_pressed(ctx, Key::Enter) && self.app_state.selected_file.is_some() {
let path = &self.app_state.selected_file.clone().unwrap();
if modifiers.ctrl {
self.play_file(path, true);
} else if 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);
}
}
// Focus search field
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;
}
// Iterate through dirs and files with Ctrl + Up/Down
let arrow_up_pressed = self.key_pressed(ctx, Key::ArrowUp);
let arrow_down_pressed = self.key_pressed(ctx, Key::ArrowDown);
if modifiers.ctrl && (arrow_up_pressed || arrow_down_pressed) {
if modifiers.shift && !self.app_state.dirs.is_empty() {
let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect();
dirs.sort();
// Iterate through dirs if there are some
if i.modifiers.ctrl {
let arrow_up_pressed = i.key_pressed(Key::ArrowUp);
let arrow_down_pressed = i.key_pressed(Key::ArrowDown);
if arrow_up_pressed || arrow_down_pressed {
if i.modifiers.shift && !self.app_state.dirs.is_empty() {
let mut dirs: Vec<PathBuf> =
self.app_state.dirs.iter().cloned().collect();
dirs.sort();
let current_dir_index: i8;
if let Some(current_dir) = &self.app_state.current_dir {
if let Some(index) = dirs.iter().position(|x| x == current_dir) {
current_dir_index = index as i8;
} else {
current_dir_index = -1;
}
} else {
current_dir_index = -1;
}
let mut new_dir_index: i8;
new_dir_index = current_dir_index - arrow_up_pressed as i8
+ arrow_down_pressed as i8;
if new_dir_index < 0 {
new_dir_index = (dirs.len() - 1) as i8;
} else if new_dir_index >= dirs.len() as i8 {
new_dir_index = 0;
}
self.open_dir(&dirs[new_dir_index as usize]);
} else if self.app_state.current_dir.is_some() {
let mut files: Vec<PathBuf> =
self.app_state.files.iter().cloned().collect();
files.sort();
let current_files_index: i64;
if let Some(selected_file) = &self.app_state.selected_file {
if let Some(index) = files.iter().position(|x| x == selected_file) {
current_files_index = index as i64;
} else {
current_files_index = -1;
}
} else {
current_files_index = -1;
}
let mut new_files_index: i64;
new_files_index = current_files_index - arrow_up_pressed as i64
+ arrow_down_pressed as i64;
if new_files_index < 0 {
new_files_index = (files.len() - 1) as i64;
} else if new_files_index >= files.len() as i64 {
new_files_index = 0;
}
self.app_state.selected_file =
Some(files[new_files_index as usize].clone());
let current_dir_index: i8;
if let Some(current_dir) = &self.app_state.current_dir {
if let Some(index) = dirs.iter().position(|x| x == current_dir) {
current_dir_index = index as i8;
} else {
current_dir_index = -1;
}
} else {
current_dir_index = -1;
}
let mut new_dir_index: i8;
new_dir_index =
current_dir_index - arrow_up_pressed as i8 + arrow_down_pressed as i8;
if new_dir_index < 0 {
new_dir_index = (dirs.len() - 1) as i8;
} else if new_dir_index >= dirs.len() as i8 {
new_dir_index = 0;
}
self.open_dir(&dirs[new_dir_index as usize]);
} else if self.app_state.current_dir.is_some() {
let files = self.get_filtered_files();
if files.is_empty() {
return;
}
let current_files_index = self
.app_state
.selected_file
.as_ref()
.and_then(|f| files.iter().position(|x| x == f))
.map(|i| i as i64)
.unwrap_or(-1);
let mut new_files_index =
current_files_index - arrow_up_pressed as i64 + arrow_down_pressed as i64;
if new_files_index < 0 {
new_files_index = (files.len() - 1) as i64;
} else if new_files_index >= files.len() as i64 {
new_files_index = 0;
}
self.app_state.selected_file = Some(files[new_files_index as usize].clone());
}
}
});
}
// });
}
}
+69 -14
View File
@@ -4,6 +4,7 @@ mod update;
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, Vec2, ViewportBuilder};
use itertools::Itertools;
use pwsp::{
types::{
audio_player::PlayerState,
@@ -13,13 +14,13 @@ use pwsp::{
},
utils::{
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 std::path::PathBuf;
use std::{
error::Error,
path::PathBuf,
sync::{Arc, Mutex},
};
@@ -59,14 +60,14 @@ impl SoundpadGui {
let (new_state, request) = {
let guard = self.audio_player_state_shared.lock().unwrap();
match guard.state {
PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause())),
PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume())),
PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause(None))),
PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume(None))),
PlayerState::Stopped => (None, None),
}
};
if let Some(req) = request {
make_request_sync(req).ok();
make_request_async(req);
}
if let Some(state) = new_state {
@@ -79,7 +80,7 @@ impl SoundpadGui {
pub fn open_file(&mut self) {
let file_dialog = FileDialog::new().add_filter("Audio File", &SUPPORTED_EXTENSIONS);
if let Some(path) = file_dialog.pick_file() {
self.play_file(&path);
self.play_file(&path, false);
}
}
@@ -87,15 +88,16 @@ impl SoundpadGui {
let file_dialog = FileDialog::new();
if let Some(paths) = file_dialog.pick_folders() {
for path in paths {
self.app_state.dirs.insert(path);
self.app_state.dirs.push(path);
}
self.app_state.dirs = self.app_state.dirs.iter().unique().cloned().collect();
self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok();
}
}
pub fn remove_dir(&mut self, path: &PathBuf) {
self.app_state.dirs.remove(path);
self.app_state.dirs.retain(|x| x != path);
if let Some(current_dir) = &self.app_state.current_dir
&& current_dir == path
{
@@ -116,12 +118,12 @@ impl SoundpadGui {
.collect();
}
pub fn play_file(&mut self, path: &PathBuf) {
make_request_sync(Request::play(path.to_str().unwrap())).ok();
pub fn play_file(&mut self, path: &PathBuf, concurrent: bool) {
make_request_async(Request::play(path.to_str().unwrap(), concurrent));
}
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 {
let mut daemon_config = get_daemon_config();
@@ -130,8 +132,61 @@ impl SoundpadGui {
}
}
pub fn toggle_loop(&mut self) {
make_request_sync(Request::toggle_loop()).ok();
pub fn toggle_loop(&mut self, id: Option<u32>) {
make_request_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 fn get_filtered_files(&self) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = self.app_state.files.iter().cloned().collect();
files.sort();
let search_query = self.app_state.search_query.to_lowercase();
let search_query = search_query.trim();
files
.into_iter()
.filter(|entry_path| {
if entry_path.is_dir() {
return false;
}
if !SUPPORTED_EXTENSIONS.contains(
&entry_path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
return false;
}
if !search_query.is_empty() {
let file_name = entry_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if !file_name.to_lowercase().contains(search_query) {
return false;
}
}
true
})
.collect()
}
}
@@ -163,7 +218,7 @@ pub async fn run() -> Result<(), Box<dyn Error>> {
Ok(_) => {
let config = get_gui_config();
if config.pause_on_exit {
make_request_sync(Request::pause()).ok();
make_request_sync(Request::pause(None)).ok();
}
Ok(())
}
+63 -42
View File
@@ -3,19 +3,74 @@ use eframe::{App, Frame as EFrame};
use egui::{CentralPanel, Context};
use pwsp::{
types::socket::Request,
utils::{
daemon::{get_daemon_config, is_daemon_running},
gui::make_request_sync,
},
utils::{daemon::get_daemon_config, gui::make_request_async},
};
use std::time::{Duration, Instant};
impl App for SoundpadGui {
fn update(&mut self, ctx: &Context, _frame: &mut EFrame) {
// Save directories if changed
if !self.config.dirs.eq(&self.app_state.dirs) {
self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok();
}
// Seek and volume requests
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();
}
}
// Sync audio player state
{
let guard = self.audio_player_state_shared.lock().unwrap();
self.audio_player_state = guard.clone();
}
// Handle scale factor changes
let old_scale_factor = self.config.scale_factor;
let new_scale_factor = ctx.zoom_factor().clamp(0.5, 2.0);
@@ -26,10 +81,12 @@ impl App for SoundpadGui {
self.config.save_to_file().ok();
}
// Handle input
self.handle_input(ctx);
// Draw 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);
return;
}
@@ -40,45 +97,9 @@ impl App for SoundpadGui {
}
self.draw(ui).ok();
if let Some(force_focus_id) = self.app_state.force_focus_id {
ui.memory_mut(|reder| {
reder.request_focus(force_focus_id);
});
self.app_state.force_focus_id = None;
}
});
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;
}
// Request repaint
ctx.request_repaint_after_secs(1.0 / 60.0);
}
}
+273 -129
View File
@@ -1,13 +1,14 @@
use crate::{
types::pipewire::{AudioDevice, DeviceType, Terminate},
types::pipewire::{DeviceType, Terminate},
utils::{
daemon::get_daemon_config,
pipewire::{create_link, get_all_devices, get_device},
pipewire::{create_link, get_device},
},
};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
error::Error,
fs,
path::{Path, PathBuf},
@@ -22,53 +23,65 @@ pub enum PlayerState {
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,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct FullState {
pub state: PlayerState,
pub tracks: Vec<TrackInfo>,
pub volume: f32,
pub current_input: String,
pub all_inputs: HashMap<String, String>,
}
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 {
_stream_handle: OutputStream,
sink: Sink,
pub stream_handle: OutputStream,
pub tracks: HashMap<u32, PlayingSound>,
pub next_id: u32,
input_link_sender: Option<pipewire::channel::Sender<Terminate>>,
pub current_input_device: Option<AudioDevice>,
pub input_device_name: Option<String>,
pub volume: f32,
pub duration: Option<f32>,
pub current_file_path: Option<PathBuf>,
pub looped: bool,
pub volume: f32, // Master volume
}
impl AudioPlayer {
pub async fn new() -> Result<Self, Box<dyn Error>> {
let daemon_config = get_daemon_config();
let default_volume = daemon_config.default_volume.unwrap_or(1.0);
let mut default_input_device: Option<AudioDevice> = None;
if let Some(name) = daemon_config.default_input_name
&& let Ok(device) = get_device(&name).await
&& device.device_type == DeviceType::Input
{
default_input_device = Some(device);
}
let stream_handle = OutputStreamBuilder::open_default_stream()?;
let sink = Sink::connect_new(stream_handle.mixer());
sink.set_volume(default_volume);
let mut audio_player = AudioPlayer {
_stream_handle: stream_handle,
sink,
stream_handle,
tracks: HashMap::new(),
next_id: 1,
input_link_sender: None,
current_input_device: default_input_device.clone(),
input_device_name: daemon_config.default_input_name.clone(),
volume: default_volume,
duration: None,
current_file_path: None,
looped: false,
};
if default_input_device.is_some() {
if audio_player.input_device_name.is_some() {
audio_player.link_devices().await?;
}
@@ -78,7 +91,10 @@ impl AudioPlayer {
fn abort_link_thread(&mut self) {
if let Some(sender) = &self.input_link_sender {
match sender.send(Terminate {}) {
Ok(_) => println!("Sent terminate signal to link thread"),
Ok(_) => {
println!("Sent terminate signal to link thread");
self.input_link_sender = None;
}
Err(_) => eprintln!("Failed to send terminate signal to link thread"),
}
}
@@ -87,42 +103,43 @@ impl AudioPlayer {
async fn link_devices(&mut self) -> Result<(), Box<dyn Error>> {
self.abort_link_thread();
if self.current_input_device.is_none() {
let input_device;
if let Some(input_device_name) = &self.input_device_name {
if let Ok(device) = get_device(input_device_name).await {
input_device = device;
} else {
eprintln!(
"Could not find selected input device {}, skipping device linking",
input_device_name
);
return Ok(());
}
} else {
eprintln!("No input device selected, skipping device linking");
return Ok(());
}
let (input_devices, _) = get_all_devices().await?;
let mut pwsp_daemon_input: Option<AudioDevice> = None;
for input_device in input_devices {
if input_device.name == "pwsp-virtual-mic" {
pwsp_daemon_input = Some(input_device);
break;
}
}
if pwsp_daemon_input.is_none() {
eprintln!("Could not find pwsp-daemon input device, skipping device linking");
let daemon_input;
if let Ok(device) = get_device("pwsp-virtual-mic").await {
daemon_input = device;
} else {
eprintln!("Could not find pwsp-virtual-mic device, skipping device linking");
return Ok(());
}
let pwsp_daemon_input = pwsp_daemon_input.unwrap();
let current_input_device = self.current_input_device.clone().unwrap();
let Some(output_fl) = current_input_device.output_fl.clone() else {
let Some(output_fl) = input_device.output_fl.clone() else {
eprintln!("Failed to get pwsp-daemon output_fl");
return Ok(());
};
let Some(output_fr) = current_input_device.output_fr.clone() else {
let Some(output_fr) = input_device.output_fr.clone() else {
eprintln!("Failed to get pwsp-daemon output_fr");
return Ok(());
};
let Some(input_fl) = pwsp_daemon_input.input_fl.clone() else {
let Some(input_fl) = daemon_input.input_fl.clone() else {
eprintln!("Failed to get pwsp-daemon input_fl");
return Ok(());
};
let Some(input_fr) = pwsp_daemon_input.input_fr.clone() else {
let Some(input_fr) = daemon_input.input_fr.clone() else {
eprintln!("Failed to get pwsp-daemon input_fr");
return Ok(());
};
@@ -132,74 +149,123 @@ impl AudioPlayer {
Ok(())
}
pub fn pause(&mut self) {
if self.get_state() == PlayerState::Playing {
self.sink.pause();
}
}
pub fn resume(&mut self) {
if self.get_state() == PlayerState::Paused {
self.sink.play();
}
}
pub fn stop(&mut self) {
self.sink.stop();
}
pub fn is_paused(&self) -> bool {
self.sink.is_paused()
}
pub fn get_state(&self) -> PlayerState {
if self.sink.len() == 0 {
return PlayerState::Stopped;
}
if self.sink.is_paused() {
return PlayerState::Paused;
}
PlayerState::Playing
}
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume;
self.sink.set_volume(volume);
}
pub fn get_position(&self) -> f32 {
if self.get_state() == PlayerState::Stopped {
return 0.0;
}
self.sink.get_pos().as_secs_f32()
}
pub fn seek(&mut self, mut position: f32) -> Result<(), Box<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())
pub fn pause(&mut self, id: Option<u32>) {
if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) {
sound.sink.pause();
}
} else {
match self.duration {
Some(duration) => Ok(duration),
None => Err("Couldn't determine duration for current file".into()),
for sound in self.tracks.values_mut() {
sound.sink.pause();
}
}
}
pub async fn play(&mut self, file_path: &Path) -> Result<(), Box<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() {
return Err(format!("File does not exist: {}", file_path.display()).into());
}
@@ -207,30 +273,108 @@ impl AudioPlayer {
let file = fs::File::open(file_path)?;
match Decoder::try_from(file) {
Ok(source) => {
self.current_file_path = Some(file_path.to_path_buf());
if let Some(duration) = source.total_duration() {
self.duration = Some(duration.as_secs_f32());
} else {
self.duration = None;
if !concurrent {
self.tracks.clear();
}
self.sink.stop();
self.sink.append(source);
self.sink.play();
self.link_devices().await?;
let id = self.next_id;
self.next_id += 1;
Ok(())
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);
Ok(id)
}
Err(err) => Err(err.into()),
}
}
pub fn get_current_file_path(&mut self) -> &Option<PathBuf> {
if self.get_state() == PlayerState::Stopped && !self.looped {
self.current_file_path = None;
pub fn set_loop(&mut self, enabled: bool, id: Option<u32>) {
if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) {
sound.looped = enabled;
}
} else {
// Set loop for all? Or just last?
// Let's set for all.
for sound in self.tracks.values_mut() {
sound.looped = enabled;
}
}
&self.current_file_path
}
pub fn get_tracks(&self) -> Vec<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) {
if let Some(input_device_name) = &self.input_device_name {
// Unlink devices if selected input device was removed
if self.input_link_sender.is_some() && get_device(input_device_name).await.is_err() {
// Selected input device was removed
eprintln!(
"Selected input device {} was removed, unlinking devices",
input_device_name
);
self.abort_link_thread();
}
// Link devices if not linked
else if self.input_link_sender.is_none() {
self.link_devices().await.ok();
}
}
// Handle looped sounds
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>> {
@@ -240,7 +384,7 @@ impl AudioPlayer {
return Err("Selected device is not an input device".into());
}
self.current_input_device = Some(input_device);
self.input_device_name = Some(name.to_string());
self.link_devices().await?;
+135 -51
View File
@@ -1,9 +1,15 @@
use crate::{
types::{audio_player::PlayerState, socket::Response},
utils::{daemon::get_audio_player, pipewire::get_all_devices},
types::{
audio_player::{FullState, PlayerState},
socket::Response,
},
utils::{
daemon::get_audio_player,
pipewire::{get_all_devices, get_device},
},
};
use async_trait::async_trait;
use std::path::PathBuf;
use std::{collections::HashMap, path::PathBuf};
#[async_trait]
pub trait Executable {
@@ -12,13 +18,21 @@ pub trait Executable {
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 {}
@@ -28,21 +42,28 @@ pub struct GetVolumeCommand {}
pub struct SetVolumeCommand {
pub volume: Option<f32>,
pub id: Option<u32>,
}
pub struct GetPositionCommand {}
pub struct GetPositionCommand {
pub id: Option<u32>,
}
pub struct SeekCommand {
pub position: Option<f32>,
pub id: Option<u32>,
}
pub struct GetDurationCommand {}
pub struct GetDurationCommand {
pub id: Option<u32>,
}
pub struct PlayCommand {
pub file_path: Option<PathBuf>,
pub concurrent: Option<bool>,
}
pub struct GetCurrentFilePathCommand {}
pub struct GetTracksCommand {}
pub struct GetCurrentInputCommand {}
@@ -52,13 +73,16 @@ pub struct SetCurrentInputCommand {
pub name: Option<String>,
}
pub struct GetLoopCommand {}
pub struct SetLoopCommand {
pub enabled: Option<bool>,
pub id: Option<u32>,
}
pub struct ToggleLoopCommand {}
pub struct ToggleLoopCommand {
pub id: Option<u32>,
}
pub struct GetFullStateCommand {}
#[async_trait]
impl Executable for PingCommand {
@@ -71,7 +95,7 @@ impl Executable for PingCommand {
impl Executable for PauseCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.pause();
audio_player.pause(self.id);
Response::new(true, "Audio was paused")
}
}
@@ -80,7 +104,7 @@ impl Executable for PauseCommand {
impl Executable for ResumeCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.resume();
audio_player.resume(self.id);
Response::new(true, "Audio was resumed")
}
}
@@ -94,12 +118,31 @@ impl Executable for TogglePauseCommand {
return Response::new(false, "Audio is not playing");
}
if audio_player.is_paused() {
audio_player.resume();
Response::new(true, "Audio was resumed")
// This logic is a bit tricky with multiple tracks.
// If ID is provided, toggle that track.
// If not, toggle global pause state?
// For now, let's just use pause/resume based on global state if no ID.
if let Some(id) = self.id {
if let Some(track) = audio_player.tracks.get(&id) {
if track.sink.is_paused() {
audio_player.resume(Some(id));
Response::new(true, "Audio was resumed")
} else {
audio_player.pause(Some(id));
Response::new(true, "Audio was paused")
}
} else {
Response::new(false, "Track not found")
}
} else {
audio_player.pause();
Response::new(true, "Audio was paused")
if audio_player.is_paused() {
audio_player.resume(None);
Response::new(true, "Audio was resumed")
} else {
audio_player.pause(None);
Response::new(true, "Audio was paused")
}
}
}
}
@@ -108,7 +151,7 @@ impl Executable for TogglePauseCommand {
impl Executable for StopCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.stop();
audio_player.stop(self.id);
Response::new(true, "Audio was stopped")
}
}
@@ -145,7 +188,7 @@ impl Executable for SetVolumeCommand {
async fn execute(&self) -> Response {
if let Some(volume) = self.volume {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.set_volume(volume);
audio_player.set_volume(volume, self.id);
Response::new(true, format!("Audio volume was set to {}", volume))
} else {
Response::new(false, "Invalid volume value")
@@ -157,7 +200,7 @@ impl Executable for SetVolumeCommand {
impl Executable for GetPositionCommand {
async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await;
let position = audio_player.get_position();
let position = audio_player.get_position(self.id);
Response::new(true, position.to_string())
}
}
@@ -167,7 +210,7 @@ impl Executable for SeekCommand {
async fn execute(&self) -> Response {
if let Some(position) = self.position {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.seek(position) {
match audio_player.seek(position, self.id) {
Ok(_) => Response::new(true, format!("Audio position was set to {}", position)),
Err(err) => Response::new(false, err.to_string()),
}
@@ -181,7 +224,7 @@ impl Executable for SeekCommand {
impl Executable for GetDurationCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.get_duration() {
match audio_player.get_duration(self.id) {
Ok(duration) => Response::new(true, duration.to_string()),
Err(err) => Response::new(false, err.to_string()),
}
@@ -193,8 +236,11 @@ impl Executable for PlayCommand {
async fn execute(&self) -> Response {
if let Some(file_path) = &self.file_path {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.play(file_path).await {
Ok(_) => Response::new(true, format!("Now playing {}", file_path.display())),
match audio_player
.play(file_path, self.concurrent.unwrap_or(false))
.await
{
Ok(id) => Response::new(true, id.to_string()),
Err(err) => Response::new(false, err.to_string()),
}
} else {
@@ -204,15 +250,11 @@ impl Executable for PlayCommand {
}
#[async_trait]
impl Executable for GetCurrentFilePathCommand {
impl Executable for GetTracksCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
let current_file_path = audio_player.get_current_file_path();
if let Some(current_file_path) = current_file_path {
Response::new(true, current_file_path.to_str().unwrap())
} else {
Response::new(false, "No file is playing")
}
let audio_player = get_audio_player().await.lock().await;
let tracks = audio_player.get_tracks();
Response::new(true, serde_json::to_string(&tracks).unwrap())
}
}
@@ -220,11 +262,15 @@ impl Executable for GetCurrentFilePathCommand {
impl Executable for GetCurrentInputCommand {
async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await;
if let Some(input_device) = &audio_player.current_input_device {
Response::new(
true,
format!("{} - {}", input_device.name, input_device.nick),
)
if let Some(input_device_name) = &audio_player.input_device_name {
if let Ok(input_device) = get_device(input_device_name).await {
Response::new(
true,
format!("{} - {}", input_device.name, input_device.nick),
)
} else {
Response::new(false, "Failed to get current input device")
}
} else {
Response::new(false, "No input device selected")
}
@@ -265,14 +311,6 @@ impl Executable for SetCurrentInputCommand {
}
}
#[async_trait]
impl Executable for GetLoopCommand {
async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await;
Response::new(true, audio_player.looped.to_string())
}
}
#[async_trait]
impl Executable for SetLoopCommand {
async fn execute(&self) -> Response {
@@ -280,7 +318,7 @@ impl Executable for SetLoopCommand {
match self.enabled {
Some(enabled) => {
audio_player.looped = enabled;
audio_player.set_loop(enabled, self.id);
Response::new(true, format!("Loop was set to {}", enabled))
}
None => Response::new(false, "Invalid enabled value"),
@@ -292,7 +330,53 @@ impl Executable for SetLoopCommand {
impl Executable for ToggleLoopCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.looped = !audio_player.looped;
Response::new(true, format!("Loop was set to {}", audio_player.looped))
if let Some(id) = self.id {
if let Some(track) = audio_player.tracks.get_mut(&id) {
track.looped = !track.looped;
Response::new(true, format!("Loop was set to {}", track.looped))
} else {
Response::new(false, "Track not found")
}
} else {
// Toggle all?
for track in audio_player.tracks.values_mut() {
track.looped = !track.looped;
}
Response::new(true, "Loop toggled for all tracks")
}
}
}
#[async_trait]
impl Executable for GetFullStateCommand {
async fn execute(&self) -> Response {
let (input_devices, _output_devices) = get_all_devices().await.unwrap();
let mut all_inputs = HashMap::new();
let mut current_input_nick = String::new();
let audio_player = get_audio_player().await.lock().await;
for device in input_devices {
if device.name == "pwsp-virtual-mic" {
continue;
}
if let Some(current_input_name) = &audio_player.input_device_name {
if device.name == *current_input_name {
current_input_nick = format!("{} - {}", device.name, device.nick);
}
}
all_inputs.insert(device.name, device.nick);
}
let full_state = FullState {
state: audio_player.get_state(),
tracks: audio_player.get_tracks(),
volume: audio_player.volume,
current_input: current_input_nick,
all_inputs,
};
Response::new(true, serde_json::to_string(&full_state).unwrap())
}
}
+7 -3
View File
@@ -1,8 +1,9 @@
use crate::utils::config::get_config_path;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, error::Error, fs, path::PathBuf};
use std::{error::Error, fs, path::PathBuf};
#[derive(Default, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DaemonConfig {
pub default_input_name: Option<String>,
pub default_volume: Option<f32>,
@@ -30,28 +31,31 @@ impl DaemonConfig {
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GuiConfig {
pub scale_factor: f32,
pub left_panel_width: f32,
pub save_volume: bool,
pub save_input: bool,
pub save_scale_factor: bool,
pub pause_on_exit: bool,
pub dirs: HashSet<PathBuf>,
pub dirs: Vec<PathBuf>,
}
impl Default for GuiConfig {
fn default() -> Self {
GuiConfig {
scale_factor: 1.0,
left_panel_width: 280.0,
save_volume: false,
save_input: false,
save_scale_factor: false,
pause_on_exit: false,
dirs: HashSet::default(),
dirs: vec![],
}
}
}
+26 -16
View File
@@ -1,49 +1,59 @@
use crate::types::audio_player::PlayerState;
use crate::types::audio_player::{PlayerState, TrackInfo};
use egui::Id;
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
time::Instant,
};
#[derive(Default, Debug)]
pub struct AppState {
pub search_query: String,
pub struct TrackUiState {
pub position_slider_value: f32,
pub volume_slider_value: f32,
pub position_dragged: bool,
pub volume_dragged: bool,
pub ignore_position_update_until: Option<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 volume_dragged: bool,
pub force_focus_search: bool,
pub volume_slider_value: f32,
pub search_field_id: Option<Id>,
pub ignore_volume_update_until: Option<Instant>,
pub current_dir: Option<PathBuf>,
pub dirs: HashSet<PathBuf>,
pub dirs: Vec<PathBuf>,
pub selected_file: Option<PathBuf>,
pub files: HashSet<PathBuf>,
pub search_field_id: Option<Id>,
pub force_focus_id: Option<Id>,
}
#[derive(Default, Debug, Clone)]
pub struct AudioPlayerState {
pub state: PlayerState,
pub new_state: Option<PlayerState>,
pub current_file_path: PathBuf,
pub is_paused: bool,
pub looped: bool,
pub tracks: Vec<TrackInfo>,
pub volume: f32,
pub new_volume: Option<f32>,
pub position: f32,
pub new_position: Option<f32>,
pub duration: f32,
pub volume: f32, // Master volume
pub current_input: String,
pub all_inputs: HashMap<String, String>,
pub is_daemon_running: bool,
}
+86 -26
View File
@@ -24,24 +24,54 @@ impl Request {
Request::new("ping", vec![])
}
pub fn pause() -> Self {
Request::new("pause", vec![])
pub fn pause(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("pause", args)
}
pub fn resume() -> Self {
Request::new("resume", vec![])
pub fn resume(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("resume", args)
}
pub fn toggle_pause() -> Self {
Request::new("toggle_pause", vec![])
pub fn toggle_pause(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_pause", args)
}
pub fn stop() -> Self {
Request::new("stop", vec![])
pub fn stop(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("stop", args)
}
pub fn play(file_path: &str) -> Self {
Request::new("play", vec![("file_path", file_path)])
pub fn play(file_path: &str, concurrent: bool) -> Self {
Request::new(
"play",
vec![
("file_path", file_path),
("concurrent", &concurrent.to_string()),
],
)
}
pub fn get_is_paused() -> Self {
@@ -52,20 +82,32 @@ impl Request {
Request::new("get_volume", vec![])
}
pub fn get_position() -> Self {
Request::new("get_position", vec![])
pub fn get_position(id: Option<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("get_position", args)
}
pub fn get_duration() -> Self {
Request::new("get_duration", vec![])
pub fn get_duration(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("get_duration", args)
}
pub fn get_state() -> Self {
Request::new("get_state", vec![])
}
pub fn get_current_file_path() -> Self {
Request::new("get_current_file_path", vec![])
pub fn get_tracks() -> Self {
Request::new("get_tracks", vec![])
}
pub fn get_input() -> Self {
@@ -76,28 +118,46 @@ impl Request {
Request::new("get_inputs", vec![])
}
pub fn set_volume(volume: f32) -> Self {
Request::new("set_volume", vec![("volume", &volume.to_string())])
pub fn set_volume(volume: f32, id: Option<u32>) -> Self {
let mut args = vec![("volume".to_string(), volume.to_string())];
if let Some(id) = id {
args.push(("id".to_string(), id.to_string()));
}
Request::new("set_volume".to_string(), args)
}
pub fn seek(position: f32) -> Self {
Request::new("seek", vec![("position", &position.to_string())])
pub fn seek(position: f32, id: Option<u32>) -> Self {
let mut args = vec![("position".to_string(), position.to_string())];
if let Some(id) = id {
args.push(("id".to_string(), id.to_string()));
}
Request::new("seek".to_string(), args)
}
pub fn set_input(name: &str) -> Self {
Request::new("set_input", vec![("input_name", name)])
}
pub fn get_loop() -> Self {
Request::new("get_loop", vec![])
pub fn set_loop(enabled: &str, id: Option<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 set_loop(enabled: &str) -> Self {
Request::new("set_loop", vec![("enabled", enabled)])
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)
}
pub fn toggle_loop() -> Self {
Request::new("toggle_loop", vec![])
pub fn get_full_state() -> Self {
Request::new("get_full_state", vec![])
}
}
+24 -13
View File
@@ -3,12 +3,14 @@ use crate::types::{commands::*, socket::Request};
use std::path::PathBuf;
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() {
"ping" => Some(Box::new(PingCommand {})),
"pause" => Some(Box::new(PauseCommand {})),
"resume" => Some(Box::new(ResumeCommand {})),
"toggle_pause" => Some(Box::new(TogglePauseCommand {})),
"stop" => Some(Box::new(StopCommand {})),
"pause" => Some(Box::new(PauseCommand { id })),
"resume" => Some(Box::new(ResumeCommand { id })),
"toggle_pause" => Some(Box::new(TogglePauseCommand { id })),
"stop" => Some(Box::new(StopCommand { id })),
"is_paused" => Some(Box::new(IsPausedCommand {})),
"get_state" => Some(Box::new(GetStateCommand {})),
"get_volume" => Some(Box::new(GetVolumeCommand {})),
@@ -19,9 +21,9 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
.unwrap_or(&String::new())
.parse::<f32>()
.ok();
Some(Box::new(SetVolumeCommand { volume }))
Some(Box::new(SetVolumeCommand { volume, id }))
}
"get_position" => Some(Box::new(GetPositionCommand {})),
"get_position" => Some(Box::new(GetPositionCommand { id })),
"seek" => {
let position = request
.args
@@ -29,9 +31,9 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
.unwrap_or(&String::new())
.parse::<f32>()
.ok();
Some(Box::new(SeekCommand { position }))
Some(Box::new(SeekCommand { position, id }))
}
"get_duration" => Some(Box::new(GetDurationCommand {})),
"get_duration" => Some(Box::new(GetDurationCommand { id })),
"play" => {
let file_path = request
.args
@@ -39,16 +41,24 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
.unwrap_or(&String::new())
.parse::<PathBuf>()
.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_inputs" => Some(Box::new(GetAllInputsCommand {})),
"set_input" => {
let name = Some(request.args.get("input_name").unwrap_or(&String::new())).cloned();
Some(Box::new(SetCurrentInputCommand { name }))
}
"get_loop" => Some(Box::new(GetLoopCommand {})),
"set_loop" => {
let enabled = request
.args
@@ -56,9 +66,10 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
.unwrap_or(&String::new())
.parse::<bool>()
.ok();
Some(Box::new(SetLoopCommand { enabled }))
Some(Box::new(SetLoopCommand { enabled, id }))
}
"toggle_loop" => Some(Box::new(ToggleLoopCommand {})),
"toggle_loop" => Some(Box::new(ToggleLoopCommand { id })),
"get_full_state" => Some(Box::new(GetFullStateCommand {})),
_ => None,
}
}
+11 -27
View File
@@ -2,10 +2,9 @@ use crate::{
types::{
audio_player::AudioPlayer,
config::DaemonConfig,
pipewire::AudioDevice,
socket::{Request, Response},
},
utils::pipewire::{create_link, get_all_devices},
utils::pipewire::{create_link, get_device},
};
use std::path::PathBuf;
use std::{error::Error, fs};
@@ -36,37 +35,22 @@ pub fn get_daemon_config() -> DaemonConfig {
}
pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> {
let (input_devices, output_devices) = get_all_devices().await?;
let mut pwsp_daemon_output: Option<AudioDevice> = None;
for output_device in output_devices {
if output_device.name == "alsa_playback.pwsp-daemon" {
pwsp_daemon_output = Some(output_device);
break;
}
}
if pwsp_daemon_output.is_none() {
eprintln!("Could not find pwsp-daemon output device, skipping device linking");
let pwsp_daemon_output;
if let Ok(device) = get_device("alsa_playback.pwsp-daemon").await {
pwsp_daemon_output = device;
} else {
eprintln!("Could not find alsa_playback.pwsp-daemon device, skipping device linking");
return Ok(());
}
let mut pwsp_daemon_input: Option<AudioDevice> = None;
for input_device in input_devices {
if input_device.name == "pwsp-virtual-mic" {
pwsp_daemon_input = Some(input_device);
break;
}
}
if pwsp_daemon_input.is_none() {
eprintln!("Could not find pwsp-daemon input device, skipping device linking");
let pwsp_daemon_input;
if let Ok(device) = get_device("pwsp-virtual-mic").await {
pwsp_daemon_input = device;
} else {
eprintln!("Could not find pwsp-virtual-mic device, skipping device linking");
return Ok(());
}
let pwsp_daemon_output = pwsp_daemon_output.unwrap();
let pwsp_daemon_input = pwsp_daemon_input.unwrap();
let output_fl = pwsp_daemon_output
.clone()
.output_fl
+33 -124
View File
@@ -1,16 +1,14 @@
use crate::{
types::{
audio_player::PlayerState,
audio_player::FullState,
config::GuiConfig,
gui::AudioPlayerState,
socket::{Request, Response},
},
utils::daemon::{make_request, wait_for_daemon},
utils::daemon::{is_daemon_running, make_request},
};
use std::{
collections::HashMap,
error::Error,
path::PathBuf,
sync::{Arc, Mutex},
};
use tokio::time::{Duration, sleep};
@@ -31,6 +29,12 @@ pub fn make_request_sync(request: Request) -> Result<Response, 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 {
fn format_time(seconds: f32) -> String {
let total_seconds = seconds.round() as u32;
@@ -47,109 +51,24 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
let sleep_duration = Duration::from_secs_f32(1.0 / 60.0);
loop {
wait_for_daemon().await.ok();
let is_running = is_daemon_running().unwrap_or(false);
let state_req = Request::get_state();
let file_path_req = Request::get_current_file_path();
let is_paused_req = Request::get_is_paused();
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 all_inputs_req = Request::get_inputs();
let looped_req = Request::get_loop();
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_res,
file_path_res,
is_paused_res,
volume_res,
position_res,
duration_res,
current_input_res,
all_inputs_res,
looped_res,
) = tokio::join!(
make_request(state_req),
make_request(file_path_req),
make_request(is_paused_req),
make_request(volume_req),
make_request(position_req),
make_request(duration_req),
make_request(current_input_req),
make_request(all_inputs_req),
make_request(looped_req),
);
let full_state_req = Request::get_full_state();
let full_state_res = make_request(full_state_req).await.unwrap_or_default();
let state_res = state_res.unwrap_or_default();
let file_path_res = file_path_res.unwrap_or_default();
let is_paused_res = is_paused_res.unwrap_or_default();
let volume_res = volume_res.unwrap_or_default();
let position_res = position_res.unwrap_or_default();
let duration_res = duration_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 looped_res = looped_res.unwrap_or_default();
if full_state_res.status {
let full_state: FullState =
serde_json::from_str(&full_state_res.message).unwrap_or_default();
let state = match state_res.status {
true => serde_json::from_str::<PlayerState>(&state_res.message).unwrap(),
false => PlayerState::default(),
};
let file_path = match file_path_res.status {
true => PathBuf::from(file_path_res.message),
false => PathBuf::new(),
};
let is_paused = match is_paused_res.status {
true => is_paused_res.message == "true",
false => false,
};
let volume = match volume_res.status {
true => volume_res.message.parse::<f32>().unwrap(),
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 {
true => current_input_res
.message
.as_str()
.split(" - ")
.collect::<Vec<&str>>()
.first()
.unwrap()
.to_string(),
false => String::new(),
};
let all_inputs = match all_inputs_res.status {
true => all_inputs_res
.message
.as_str()
.split(';')
.filter_map(|entry| {
let entry = entry.trim();
if entry.is_empty() {
return None;
}
entry
.split_once(" - ")
.map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
})
.collect::<HashMap<String, String>>(),
false => HashMap::new(),
};
let looped = match looped_res.status {
true => looped_res.message.parse::<bool>().unwrap_or_default(),
false => false,
};
{
let mut guard = audio_player_state_shared.lock().unwrap();
guard.state = match guard.new_state.clone() {
@@ -157,28 +76,18 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
guard.new_state = None;
new_state
}
None => state,
None => full_state.state,
};
guard.current_file_path = file_path;
guard.is_paused = is_paused;
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.all_inputs = all_inputs;
guard.looped = looped;
guard.tracks = full_state.tracks;
guard.volume = full_state.volume;
guard.current_input = full_state
.current_input
.split(" - ")
.next()
.unwrap_or_default()
.to_string();
guard.all_inputs = full_state.all_inputs;
guard.is_daemon_running = true;
}
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)
}
"capture_MONO" => {
input_device.input_fl = Some(port.clone());
input_device.input_fr = Some(port);
input_device.output_fl = Some(port.clone());
input_device.output_fr = Some(port);
}
_ => {}
}