Compare commits

..

29 Commits

Author SHA1 Message Date
arabianq a07025b1f6 change version to 1.8.1 2026-05-15 15:02:15 +03:00
Tarasov Aleksandr c1d145fbc8 Revert "chore(deps): bump tokio from 1.52.1 to 1.52.3 (#99)" (#101)
* Revert "chore(deps): bump tokio from 1.52.1 to 1.52.3 (#99)"

This reverts commit 911417af40.

* deps: update cargo-sources.json
2026-05-15 14:55:52 +03:00
Tarasov Aleksandr 3d4b59761b packages(rpm): add required dependencies 2026-05-13 23:47:25 +03:00
arabianq ca9b5dd517 ci: add missing deps 2026-05-13 23:21:22 +03:00
arabianq 6863c9a6f8 deps: update cargo sources 2026-05-13 23:06:08 +03:00
arabianq 958a3efde5 change version to 1.8.0 2026-05-13 23:06:08 +03:00
Tarasov Aleksandr 30e75e924c fix: Opus audio does not work in mkv files (#95)
* change rodio to the fork with symphonia-adapter-libopus-v0.2.8

* update flatpak sources

* deps: update arabianq/rodio rev
2026-05-13 23:02:31 +03:00
arabianq 2b4b7ea730 cargo update
Adding audio_thread_priority v0.35.1
    Updating cc v1.2.61 -> v1.2.62
    Updating color v0.3.2 -> v0.3.3
    Updating coreaudio-rs v0.14.1 -> v0.14.2
    Updating cpal v0.18.0 (https://github.com/RustAudio/cpal#f938e338) -> #2c7acf8e
      Adding dbus v0.6.5
    Updating egui_extras v0.34.1 -> v0.34.2
    Updating hashbrown v0.17.0 -> v0.17.1
    Updating js-sys v0.3.97 -> v0.3.98
    Updating kurbo v0.13.0 -> v0.13.1
      Adding libdbus-sys v0.2.7
      Adding mach2 v0.4.3
    Updating naga v29.0.1 -> v29.0.3
    Updating no_std_io2 v0.9.3 -> v0.9.4
    Updating normpath v1.5.0 -> v1.5.1
    Updating opusic-sys v0.6.0 -> v0.7.3
    Updating orbclient v0.3.53 -> v0.3.54
    Updating pin-project v1.1.11 -> v1.1.13
    Updating pin-project-internal v1.1.11 -> v1.1.13
      Adding polycool v0.4.0
    Updating profiling v1.0.17 -> v1.0.18
    Updating quick-xml v0.39.2 -> v0.39.4
    Updating redox_syscall v0.7.4 -> v0.7.5
    Updating siphasher v1.0.2 -> v1.0.3
    Updating symphonia-adapter-libopus v0.2.7 -> v0.2.9
    Updating wasm-bindgen v0.2.120 -> v0.2.121
    Updating wasm-bindgen-futures v0.4.70 -> v0.4.71
    Updating wasm-bindgen-macro v0.2.120 -> v0.2.121
    Updating wasm-bindgen-macro-support v0.2.120 -> v0.2.121
    Updating wasm-bindgen-shared v0.2.120 -> v0.2.121
    Updating web-sys v0.3.97 -> v0.3.98
    Updating wgpu v29.0.1 -> v29.0.3
    Updating wgpu-core v29.0.1 -> v29.0.3
    Updating wgpu-core-deps-windows-linux-android v29.0.0 -> v29.0.3
    Updating wgpu-hal v29.0.1 -> v29.0.3
    Updating wgpu-naga-bridge v29.0.1 -> v29.0.3
    Updating wgpu-types v29.0.1 -> v29.0.3
    Updating zerofrom v0.1.7 -> v0.1.8
    Updating zvariant v5.10.1 -> v5.11.0
    Updating zvariant_derive v5.10.1 -> v5.11.0
2026-05-13 23:00:10 +03:00
arabianq dafe67f35f assets: update screenshot.png 2026-05-13 22:59:03 +03:00
arabianq 8fa22ca5b0 docs: update README.md 2026-05-13 22:56:37 +03:00
arabianq d72eaabf54 feat: load system fonts 2026-05-13 22:02:24 +03:00
arabianq 377b218592 deps: update cargo-sources.json for flatpak 2026-05-13 21:45:50 +03:00
dependabot[bot] 911417af40 chore(deps): bump tokio from 1.52.1 to 1.52.3 (#99)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.52.1 to 1.52.3.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.1...tokio-1.52.3)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.52.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 21:44:25 +03:00
dependabot[bot] 573958c05b chore(deps): bump eframe from 0.34.1 to 0.34.2 (#98)
Bumps [eframe](https://github.com/emilk/egui) from 0.34.1 to 0.34.2.
- [Release notes](https://github.com/emilk/egui/releases)
- [Changelog](https://github.com/emilk/egui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/emilk/egui/compare/0.34.1...0.34.2)

---
updated-dependencies:
- dependency-name: eframe
  dependency-version: 0.34.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 21:44:09 +03:00
dependabot[bot] 0bb7ef3f33 chore(deps): bump egui from 0.34.1 to 0.34.2 (#97)
Bumps [egui](https://github.com/emilk/egui) from 0.34.1 to 0.34.2.
- [Release notes](https://github.com/emilk/egui/releases)
- [Changelog](https://github.com/emilk/egui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/emilk/egui/compare/0.34.1...0.34.2)

---
updated-dependencies:
- dependency-name: egui
  dependency-version: 0.34.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 21:43:54 +03:00
Tarasov Aleksandr 10f07cd895 ci: Add Cargo package ecosystem to Dependabot config (#96)
Configured Dependabot to update Cargo packages weekly.
2026-05-12 23:19:45 +03:00
Tarasov Aleksandr b2f2894aa1 perf(gui): remove O(N) allocation in hotkeys table render (#94)
This optimization removes an unnecessary `.cloned()` call inside `draw_hotkeys_table`
which previously forced a clone of every filtered `HotkeySlot` on every frame render.
Instead, we now hold a `Vec<&HotkeySlot>` and only clone `slot.slot` exactly when
a user interaction requires ownership to dispatch a `HotkeyAction`.

This eliminates constant heap allocations of `String` and `Request` components
while scrolling or idling in the hotkeys view.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-01 01:48:43 +03:00
Tarasov Aleksandr e6c8d720d5 update deps and change version to 1.7.6 (#92)
* change version to 1.7.6

* cargo update

cpal v0.18.0 #e5d618c6 -> #f938e338
fax v0.2.6 -> v0.2.7
fax_derive -
idna_adapter v1.2.1 -> v1.2.2
js-sys v0.3.95 -> v0.3.97
wasm-bindgen v0.2.118 -> v0.2.120
wasm-bindgen-futures v0.4.68 -> v0.4.70
wasm-bindgen-macro v0.2.118 -> v0.2.120
wasm-bindgen-macro-support v0.2.118 -> v0.2.120
wasm-bindgen-shared v0.2.118 -> v0.2.120
web-sys v0.3.95 -> v0.3.97
winnow -
zbus v5.14.0 -> v5.15.0
zbus_macros v5.14.0 -> v5.15.0
zbus_names v4.3.1 -> v4.3.2
zvariant v5.10.0 -> v5.10.1
zvariant_derive v5.10.0 -> v5.10.1
zvariant_utils v3.3.0 -> v3.3.1

* deps: update cargo-sources.json
2026-04-28 14:31:27 +03:00
RiDDiX a6d93ff528 fix clippy lints under rust 1.95 (#90) 2026-04-28 14:13:11 +03:00
RiDDiX bcf791d84c fix(packages): add cmake makedepend to aur source pkgbuild (#91) 2026-04-28 14:10:17 +03:00
Tarasov Aleksandr e4b0b10393 refactor(gui): refactor draw_hotkeys to improve code health (#86)
- Break down the monolithic `draw_hotkeys` method into smaller,
  focused component functions: `draw_hotkeys_header`,
  `draw_hotkeys_search`, `draw_hotkeys_table`, and
  `handle_hotkey_action`.
- Improve readability and maintainability of the `src/gui/draw.rs` file
  while preserving identical behavior.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-27 23:07:13 +03:00
Tarasov Aleksandr 11de96db58 refactor: simplify draw_track_control by extracting helper functions (#87)
Extracted distinct UI sections (playback controls, position slider, volume controls, stop button) into their own well-scoped helper functions within `SoundpadGui`. This significantly improves the maintainability and readability of `draw_track_control` while preserving the existing layout structure and state mutation behavior.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-27 23:05:39 +03:00
Tarasov Aleksandr 7396c0aef8 perf(gui): Optimize UI rendering loop by iterating over tracks by reference (#88)
* perf: optimize UI rendering loop by removing unnecessary Vec clone\n\n- Removed `clone()` on `self.audio_player_state.tracks` in `draw_header`\n- Iterated by reference instead of using an owned collection\n- Benchmarked and showed a significant performance improvement (7us -> 87ns)

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* build(flatpak): update cargo-sources.json to include criterion\n\nThe CI failed during the offline flatpak build because the newly added `criterion` dev-dependency was missing from `cargo-sources.json`. Regenerated `packages/flatpak/cargo-sources.json` to fix it.

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* Delete benches/ui_benchmark.rs

* refactor: remove garbage

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-27 23:03:37 +03:00
Tarasov Aleksandr fc2cd5e2da fix(daemon): Remove .expect() panics from PipeWire initialization (#89)
This commit addresses a security and stability vulnerability where failures in PipeWire context setup, connection, or registry acquisition would crash the entire thread (or daemon) via `.expect()` panics.

We now gracefully capture and propagate initialization errors up the call stack. A `sync_channel(0)` is used to signal the success or failure of the initial pipewire setup back to the calling functions (`get_all_devices`, `create_virtual_mic`, `create_link`). This prevents unexpected crashes and improves error resilience.

Also removed unneeded `pw_sender` panics on channel termination by simply dropping/ignoring the result.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-27 22:54:17 +03:00
Tarasov Aleksandr 1a37729cf1 fix: rpm builds
Added cmake as a build requirement for the package.
2026-04-26 19:52:03 +03:00
Tarasov Aleksandr 86b38a250e change version to 1.7.5 (#83)
* deps: rodio v0.22.2 -> 57ad9d8a9f30398f634fbf8e4e1d53dde7243c21 with symphonia-libopus

* change version to 1.7.5

* deps: update cargo-sources.json
2026-04-26 19:32:12 +03:00
Tarasov Aleksandr 54fa278cea feat: opus support (#82)
* deps: rodio v0.22.2 -> 57ad9d8a9f30398f634fbf8e4e1d53dde7243c21 with symphonia-libopus

* deps: update cargo-sources.json

* feat(gui): add .opus file extension support
2026-04-26 19:27:36 +03:00
Tarasov Aleksandr db040aa820 fix(ci): update input descriptions in workflow for clarity (#81) 2026-04-25 19:31:20 +03:00
Tarasov Aleksandr 04449e7525 fix(ci): update workflow inputs for tag and build branch selection (#80) 2026-04-25 19:28:48 +03:00
19 changed files with 1448 additions and 1016 deletions
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
+3 -1
View File
@@ -23,7 +23,9 @@ jobs:
zip jq \ zip jq \
libpipewire-0.3-dev \ libpipewire-0.3-dev \
libclang-dev \ libclang-dev \
libasound2-dev libasound2-dev \
libdbus-1-dev \
pkg-config
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
+18
View File
@@ -6,6 +6,19 @@ on:
release: release:
types: [ published ] types: [ published ]
workflow_dispatch: workflow_dispatch:
inputs:
tag_name:
description: "TAG (empty to build from current branch)"
required: false
type: string
build_branch:
description: "Flatpak branch to build (stable/nightly)"
required: true
type: choice
options:
- stable
- nightly
default: "stable"
jobs: jobs:
flatter: flatter:
@@ -20,6 +33,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ inputs.tag_name || github.ref }}
- name: Setup GPG - name: Setup GPG
id: gpg id: gpg
@@ -33,12 +48,15 @@ jobs:
run: | run: |
if [ "${{ github.event_name }}" == "release" ]; then if [ "${{ github.event_name }}" == "release" ]; then
echo "branch=stable" >> $GITHUB_OUTPUT echo "branch=stable" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "branch=${{ inputs.build_branch }}" >> $GITHUB_OUTPUT
else else
echo "branch=nightly" >> $GITHUB_OUTPUT echo "branch=nightly" >> $GITHUB_OUTPUT
fi fi
- name: Modify Manifest - name: Modify Manifest
run: | run: |
echo "branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
- name: Install SDK Extensions - name: Install SDK Extensions
+3 -1
View File
@@ -74,7 +74,9 @@ jobs:
zip jq \ zip jq \
libpipewire-0.3-dev \ libpipewire-0.3-dev \
libclang-dev \ libclang-dev \
libasound2-dev libasound2-dev \
libdbus-1-dev \
pkg-config
- name: Checkout code at tag - name: Checkout code at tag
uses: actions/checkout@v4 uses: actions/checkout@v4
Generated
+277 -139
View File
@@ -289,6 +289,20 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "audio_thread_priority"
version = "0.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1b4a0adbf971420cca66a75361a1190f34a6762d66361e5cde03dda35d1ed3"
dependencies = [
"cfg-if",
"dbus",
"libc",
"log",
"mach2 0.4.3",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@@ -486,9 +500,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.61" version = "1.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -601,6 +615,15 @@ dependencies = [
"error-code", "error-code",
] ]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.13.1" version = "0.13.1"
@@ -612,9 +635,9 @@ dependencies = [
[[package]] [[package]]
name = "color" name = "color"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
] ]
@@ -714,10 +737,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "coreaudio-rs" name = "core_maths"
version = "0.14.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16dd574a72a021b90c7656c474ea31d11a2f0366a8eff574186e761e0b9e3586" checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
"libm",
]
[[package]]
name = "coreaudio-rs"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"libc", "libc",
@@ -729,17 +761,18 @@ dependencies = [
[[package]] [[package]]
name = "cpal" name = "cpal"
version = "0.17.3" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/RustAudio/cpal#2c7acf8ed42b6523f319145a8be256c446df5939"
checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
dependencies = [ dependencies = [
"alsa", "alsa",
"audio_thread_priority",
"block2 0.6.2",
"coreaudio-rs", "coreaudio-rs",
"dasp_sample", "dasp_sample",
"jni 0.21.1", "jni 0.21.1",
"js-sys", "js-sys",
"libc", "libc",
"mach2", "mach2 0.6.0",
"ndk", "ndk",
"ndk-context", "ndk-context",
"num-derive", "num-derive",
@@ -751,10 +784,9 @@ dependencies = [
"objc2-core-audio-types", "objc2-core-audio-types",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys", "web-sys",
"windows", "windows",
"windows-core",
] ]
[[package]] [[package]]
@@ -815,6 +847,16 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "dbus"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48b5f0f36f1eebe901b0e6bee369a77ed3396334bf3f09abd46454a576f71819"
dependencies = [
"libc",
"libdbus-sys",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "6.0.0" version = "6.0.0"
@@ -897,9 +939,9 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]] [[package]]
name = "ecolor" name = "ecolor"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "137c0ce4ce4152ff7e223a7ce22ee1057cdff61fce0a45c32459c3ccec64868d" checksum = "55f6cc0cb3b84a21232c468db972ebcddd34decbf1ff02cdebffd807c13bbd81"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"emath", "emath",
@@ -907,9 +949,9 @@ dependencies = [
[[package]] [[package]]
name = "eframe" name = "eframe"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6e995b8e434d65aefd12c4519221be3e8f38efd77804ef39ca10553f4ad7063" checksum = "fea3080bfd001aee2223dcb2228d9de7d31f41d7cfb99e8cd70df3ae8083a42f"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytemuck", "bytemuck",
@@ -942,9 +984,9 @@ dependencies = [
[[package]] [[package]]
name = "egui" name = "egui"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f34aaf627da598dfadd64b0fee6101d22e9c451d1e5348157312720b7f459f0f" checksum = "3cbe28ac1a9c0761319aafb9ad37737720cc49d99c13a4a6b990768fa01ffe67"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"ahash", "ahash",
@@ -960,9 +1002,9 @@ dependencies = [
[[package]] [[package]]
name = "egui-wgpu" name = "egui-wgpu"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71033ff78b041c9c363450f4498ff95468ef3ecbcc71a62f67036a6207d98fa4" checksum = "2f311c0b9cdd8a38821ae6f245fbe6e1a3d7d157c77b7d22344f205b317232c8"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytemuck", "bytemuck",
@@ -980,9 +1022,9 @@ dependencies = [
[[package]] [[package]]
name = "egui-winit" name = "egui-winit"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a2881b2bf1a305e413e644af63f836737a33d85077705ff808e88f902ff742" checksum = "e8e97625c2fe0fadc8b92ec690fbf515e15e2efd67ca50e063ad3302ec8ee626"
dependencies = [ dependencies = [
"arboard", "arboard",
"bytemuck", "bytemuck",
@@ -1024,9 +1066,9 @@ dependencies = [
[[package]] [[package]]
name = "egui_extras" name = "egui_extras"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bfc6870c68d3f254e33aca8200095d422e09edacb0f365f79fe23a5ba10963" checksum = "8c609fc87f6c70ffd3afd679cbb294985096d2fc0be33e762ad5614bde4925bc"
dependencies = [ dependencies = [
"ahash", "ahash",
"egui", "egui",
@@ -1038,9 +1080,9 @@ dependencies = [
[[package]] [[package]]
name = "egui_glow" name = "egui_glow"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3b28d39ab6c0cac238190e6cb1e8c9047d02cb470ab942a7a3302e4cb3a8e74" checksum = "6caa4eca47cc2358e2c5ae60843a94118e338f87099c6af4170e6e968e8d77cb"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"egui", "egui",
@@ -1071,9 +1113,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "emath" name = "emath"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a05cd8bdf3b598488c627ca97c7fe8909448ffa26278dd3c7e535cdb554d721" checksum = "a74fbbf7501c430b89df62d102b6bfa02162faaf3e155512c677c9d20f5708d1"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
] ]
@@ -1136,9 +1178,9 @@ dependencies = [
[[package]] [[package]]
name = "epaint" name = "epaint"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04f3017dd67f147a697ee0c8484fb568fd9553e2a0c114be5020dbbc11962841" checksum = "92b452e348c2758115288802ca25f86ee286ce2cfae6643711ce116662311310"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytemuck", "bytemuck",
@@ -1159,9 +1201,9 @@ dependencies = [
[[package]] [[package]]
name = "epaint_default_fonts" name = "epaint_default_fonts"
version = "0.34.1" version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3b85a2bb775a3ab02d077a65cc31575c11b2584581913253cc11ce49f48bba" checksum = "1644e25dbe3d663fd9c2a4181772c64b23361e377e550c10422ecc3a7de1c3c2"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
@@ -1242,23 +1284,9 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]] [[package]]
name = "fax" name = "fax"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
dependencies = [
"fax_derive",
]
[[package]]
name = "fax_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "fdeflate" name = "fdeflate"
@@ -1315,6 +1343,29 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "fontconfig-parser"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
dependencies = [
"roxmltree",
]
[[package]]
name = "fontdb"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
dependencies = [
"fontconfig-parser",
"log",
"memmap2",
"slotmap",
"tinyvec",
"ttf-parser",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.5.0" version = "0.5.0"
@@ -1575,9 +1626,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.0" version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -1714,9 +1765,9 @@ dependencies = [
[[package]] [[package]]
name = "idna_adapter" name = "idna_adapter"
version = "1.2.1" version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [ dependencies = [
"icu_normalizer", "icu_normalizer",
"icu_properties", "icu_properties",
@@ -1776,7 +1827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.0", "hashbrown 0.17.1",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -1891,9 +1942,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.95" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -1909,12 +1960,13 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]] [[package]]
name = "kurbo" name = "kurbo"
version = "0.13.0" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"euclid", "euclid",
"polycool",
"smallvec", "smallvec",
] ]
@@ -1936,6 +1988,15 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libdbus-sys"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
dependencies = [
"pkg-config",
]
[[package]] [[package]]
name = "libflate" name = "libflate"
version = "2.3.0" version = "2.3.0"
@@ -1985,7 +2046,7 @@ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"libc", "libc",
"plain", "plain",
"redox_syscall 0.7.4", "redox_syscall 0.7.5",
] ]
[[package]] [[package]]
@@ -2063,13 +2124,19 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "mach2" name = "mach2"
version = "0.5.0" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "mach2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.0"
@@ -2151,9 +2218,9 @@ dependencies = [
[[package]] [[package]]
name = "naga" name = "naga"
version = "29.0.1" version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2630921705b9b01dcdd0b6864b9562ca3c1951eecd0f0c4f5f04f61e412647" checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-set", "bit-set",
@@ -2230,9 +2297,9 @@ dependencies = [
[[package]] [[package]]
name = "no_std_io2" name = "no_std_io2"
version = "0.9.3" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -2264,9 +2331,9 @@ dependencies = [
[[package]] [[package]]
name = "normpath" name = "normpath"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -2420,6 +2487,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
dependencies = [ dependencies = [
"bitflags 2.11.1",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
] ]
@@ -2706,10 +2774,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "orbclient" name = "opusic-sys"
version = "0.3.53" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12c6933ddbbd16539a7672e697bb8d41ac3a4e99ac43eeb40c07236bd7fcb2dd" checksum = "2804e694ef0de3b4cbb254de565053b7cb48d3398df7fd60c6c62bed40c5372a"
dependencies = [
"cmake",
]
[[package]]
name = "orbclient"
version = "0.3.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a570f6bca41d29acb2139229a7c873ec99bc9a313bd10804081d89bfac8ff329"
dependencies = [ dependencies = [
"libc", "libc",
"libredox", "libredox",
@@ -2819,18 +2896,18 @@ dependencies = [
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.11" version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.1.11" version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2927,6 +3004,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "polycool"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
dependencies = [
"arrayvec",
]
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.13.1" version = "1.13.1"
@@ -3003,13 +3089,13 @@ dependencies = [
[[package]] [[package]]
name = "profiling" name = "profiling"
version = "1.0.17" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
[[package]] [[package]]
name = "pwsp" name = "pwsp"
version = "1.7.4" version = "1.8.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"clap", "clap",
@@ -3027,6 +3113,7 @@ dependencies = [
"rodio", "rodio",
"serde", "serde",
"serde_json", "serde_json",
"system-fonts",
"tokio", "tokio",
] ]
@@ -3044,9 +3131,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.39.2" version = "0.39.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -3149,9 +3236,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.7.4" version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
] ]
@@ -3235,16 +3322,22 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
[[package]] [[package]]
name = "rodio" name = "rodio"
version = "0.22.2" version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/arabianq/rodio.git?rev=1a08f281c352622bd82b87b8731585245802d9cf#1a08f281c352622bd82b87b8731585245802d9cf"
checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb"
dependencies = [ dependencies = [
"cpal", "cpal",
"dasp_sample", "dasp_sample",
"num-rational", "num-rational",
"symphonia", "symphonia",
"symphonia-adapter-libopus",
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
@@ -3440,9 +3533,9 @@ checksum = "c82d449ab1bccfeec125893c6875008206f038d4eb8a09e1e10caf86f44d574e"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
[[package]] [[package]]
name = "skrifa" name = "skrifa"
@@ -3598,6 +3691,17 @@ dependencies = [
"symphonia-metadata", "symphonia-metadata",
] ]
[[package]]
name = "symphonia-adapter-libopus"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bfc8e95f95c23ed1b5328eb66920ad28d9968c797f9c7aa755d4b45a5f47a41"
dependencies = [
"log",
"opusic-sys",
"symphonia-core",
]
[[package]] [[package]]
name = "symphonia-bundle-flac" name = "symphonia-bundle-flac"
version = "0.5.5" version = "0.5.5"
@@ -3792,6 +3896,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "sys-locale"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "system-deps" name = "system-deps"
version = "7.0.8" version = "7.0.8"
@@ -3805,6 +3918,16 @@ dependencies = [
"version-compare", "version-compare",
] ]
[[package]]
name = "system-fonts"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f369feb844d5e08bb4e938c3f88ff359261bc94c7553ddb34e174447120b64d6"
dependencies = [
"fontdb",
"sys-locale",
]
[[package]] [[package]]
name = "tap" name = "tap"
version = "1.0.1" version = "1.0.1"
@@ -3894,6 +4017,21 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.1" version = "1.52.1"
@@ -3934,7 +4072,7 @@ dependencies = [
"toml_datetime", "toml_datetime",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow 1.0.2", "winnow",
] ]
[[package]] [[package]]
@@ -3955,7 +4093,7 @@ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
"toml_parser", "toml_parser",
"winnow 1.0.2", "winnow",
] ]
[[package]] [[package]]
@@ -3964,7 +4102,7 @@ version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow 1.0.2", "winnow",
] ]
[[package]] [[package]]
@@ -4005,6 +4143,15 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
dependencies = [
"core_maths",
]
[[package]] [[package]]
name = "type-map" name = "type-map"
version = "0.5.1" version = "0.5.1"
@@ -4159,9 +4306,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.118" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -4172,9 +4319,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.68" version = "0.4.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -4182,9 +4329,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.118" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -4192,9 +4339,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.118" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -4205,9 +4352,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.118" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -4383,9 +4530,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.95" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -4425,9 +4572,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]] [[package]]
name = "wgpu" name = "wgpu"
version = "29.0.1" version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c239a9a747bbd379590985bac952c2e53cb19873f7072b3370c6a6a8e06837" checksum = "bb3feacc458f7bee8bc1737149b42b6c731aa461039a4264a67bb6681646b250"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags 2.11.1", "bitflags 2.11.1",
@@ -4453,9 +4600,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-core" name = "wgpu-core"
version = "29.0.1" version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e80ac6cf1895df6342f87d975162108f9d98772a0d74bc404ab7304ac29469e" checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-set", "bit-set",
@@ -4484,18 +4631,18 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-core-deps-windows-linux-android" name = "wgpu-core-deps-windows-linux-android"
version = "29.0.0" version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "725d5c006a8c02967b6d93ef04f6537ec4593313e330cfe86d9d3f946eb90f28" checksum = "1bfb01076d0aa08b0ba9bd741e178b5cc440f5abe99d9581323a4c8b5d1a1916"
dependencies = [ dependencies = [
"wgpu-hal", "wgpu-hal",
] ]
[[package]] [[package]]
name = "wgpu-hal" name = "wgpu-hal"
version = "29.0.1" version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a47aef47636562f3937285af4c44b4b5b404b46577471411cc5313a921da7e" checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"cfg-if", "cfg-if",
@@ -4514,9 +4661,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-naga-bridge" name = "wgpu-naga-bridge"
version = "29.0.1" version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4684f4410da0cf95a4cb63bb5edaac022461dedb6adf0b64d0d9b5f6890d51" checksum = "59c654c483f058800972c3645e95388a7eca31bf9fe1933bc20e036588a0be02"
dependencies = [ dependencies = [
"naga", "naga",
"wgpu-types", "wgpu-types",
@@ -4524,9 +4671,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-types" name = "wgpu-types"
version = "29.0.1" version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec2675540fb1a5cfa5ef122d3d5f390e2c75711a0b946410f2d6ac3a0f77d1f6" checksum = "a9bcc31518a0e9735aefebedb5f7a9ef3ed1c42549c9f4c882fa9060ceaac639"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"bytemuck", "bytemuck",
@@ -4937,15 +5084,6 @@ dependencies = [
"xkbcommon-dl", "xkbcommon-dl",
] ]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.2" version = "1.0.2"
@@ -5152,9 +5290,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "5.14.0" version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-executor", "async-executor",
@@ -5179,7 +5317,7 @@ dependencies = [
"uds_windows", "uds_windows",
"uuid", "uuid",
"windows-sys 0.61.2", "windows-sys 0.61.2",
"winnow 0.7.15", "winnow",
"zbus_macros", "zbus_macros",
"zbus_names", "zbus_names",
"zvariant", "zvariant",
@@ -5187,9 +5325,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "5.14.0" version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@@ -5202,12 +5340,12 @@ dependencies = [
[[package]] [[package]]
name = "zbus_names" name = "zbus_names"
version = "4.3.1" version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [ dependencies = [
"serde", "serde",
"winnow 0.7.15", "winnow",
"zvariant", "zvariant",
] ]
@@ -5233,9 +5371,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]
@@ -5336,24 +5474,24 @@ dependencies = [
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "5.10.0" version = "5.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee"
dependencies = [ dependencies = [
"endi", "endi",
"enumflags2", "enumflags2",
"serde", "serde",
"url", "url",
"winnow 0.7.15", "winnow",
"zvariant_derive", "zvariant_derive",
"zvariant_utils", "zvariant_utils",
] ]
[[package]] [[package]]
name = "zvariant_derive" name = "zvariant_derive"
version = "5.10.0" version = "5.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@@ -5364,13 +5502,13 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_utils" name = "zvariant_utils"
version = "3.3.0" version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn", "syn",
"winnow 0.7.15", "winnow",
] ]
+6 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pwsp" name = "pwsp"
version = "1.7.4" version = "1.8.1"
edition = "2024" edition = "2024"
authors = ["arabian"] authors = ["arabian"]
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients." description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
@@ -29,8 +29,9 @@ clap = { version = "4.6.1", default-features = false, features = [
dirs = "6.0.0" dirs = "6.0.0"
itertools = "0.14.0" itertools = "0.14.0"
rodio = { version = "0.22.2", default-features = false, features = [ rodio = { git = "https://github.com/arabianq/rodio.git", rev = "1a08f281c352622bd82b87b8731585245802d9cf", default-features = false, features = [
"symphonia-all", "symphonia-all",
"symphonia-libopus",
"playback", "playback",
] } ] }
pipewire = "0.9.2" pipewire = "0.9.2"
@@ -39,12 +40,13 @@ rfd = { version = "0.17.2", default-features = false, features = [
"xdg-portal", "xdg-portal",
] } ] }
opener = { version = "0.8.4", features = ["reveal"] } opener = { version = "0.8.4", features = ["reveal"] }
system-fonts = "0.1.0"
egui = { version = "0.34.1", default-features = false, features = [ egui = { version = "0.34.2", default-features = false, features = [
"default_fonts", "default_fonts",
"rayon", "rayon",
] } ] }
eframe = { version = "0.34.1", default-features = false, features = [ eframe = { version = "0.34.2", default-features = false, features = [
"default_fonts", "default_fonts",
"glow", "glow",
"x11", "x11",
+75 -192
View File
@@ -1,237 +1,120 @@
# **🎵 Pipewire Soundpad (PWSP)** <div align="center">
<h1>🎵 PipeWire Soundpad (PWSP)</h1>
<p><b>A simple, modern, and powerful soundboard for Linux, written in Rust.</b></p>
<img src="assets/screenshot.png" alt="PWSP Screenshot" width="700"/>
</div>
**PipeWire Soundpad (PWSP)** is a simple yet powerful **soundboard application** written in **Rust**. It provides a ## 🌟 Overview
user-friendly graphical interface for **managing and playing audio files, directing their output directly to the virtual **PipeWire Soundpad (PWSP)** is a graphical soundboard application that routes audio directly to your virtual microphone using **PipeWire**. It provides an intuitive interface for managing your audio collection, making it an ideal tool for gamers, streamers, and anyone looking to inject sound effects into voice chats on platforms like Discord, Zoom, or TeamSpeak.
microphone.** This makes it an ideal tool for gamers, streamers, and anyone looking to inject sound effects into voice
chats on platforms like **Discord, Zoom, or Teamspeak**.
![screenshot.png](assets/screenshot.png) ## ✨ Key Features
* **🎙️ Virtual Microphone Output:** Seamlessly mixes your microphone input with sound effects by automatically managing PipeWire virtual devices.
* **🎵 Multi-Format Support:** Plays popular audio formats including `mp3`, `wav`, `ogg`, `flac`, `mp4`, and `aac`.
* **⚡ Global Hotkeys:** Trigger sounds instantly from anywhere, even when the app is running in the background.
* **📂 Smart Collection Management:** Drag-and-drop folders, quick search, and collapsible tracks to keep your library organized.
* **🎛️ Advanced Playback Controls:** Individual volume sliders, play/pause, position scrubbing, and concurrent multi-track playback.
* **🔌 Plug & Play:** Automatically detects when an input device is connected or disconnected and handles linking/unlinking on the fly.
* **🖥️ Modern GUI:** Clean, responsive, and lightweight interface powered by [egui](https://egui.rs/).
# **🌟 Key Features** ## ⚙️ Architecture
PWSP is built with a client-server model to ensure stability and separation of concerns:
* **`pwsp-daemon`**: The background engine. It runs silently, managing PipeWire virtual devices, audio routing, and playback.
* **`pwsp-gui`**: The graphical interface. Communicates with the daemon via a Unix socket to control playback and settings.
* **`pwsp-cli`**: The command-line tool. Perfect for scripting, hotkey binding, or quick terminal-based control.
* **Multi-Format Support**: Play audio files in popular formats, including _**mp3**_, _**wav**_, _**ogg**_, _**flac**_, ---
_**mp4**_, and _**aac**_.
* **Virtual Microphone Output**: The application routes audio through a virtual device created by PipeWire, allowing
other users to hear the sounds as if you were speaking into your microphone.
* **Modern and Clean GUI**: The interface is built with the [egui](https://egui.rs) library, ensuring an intuitive and
responsive user experience.
* **Sound Collection Management**: Easily add and remove directories containing your audio files. The application scans
these folders and displays all supported files for quick access.
* **Quick Search**: Use the built-in search bar to instantly find any sound file within your library.
* **Detailed Playback Controls**:
* **Play/Pause button**.
* **Volume slider** for individual sound adjustment.
* **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.
* **Global Hotkeys**: Assign custom keyboard shortcuts to any sound file (or action) to trigger playback instantly, even when the application is not in focus.
## 🚀 Installation
# **⚙️ How It Works** ### 📦 Flatpak (Recommended)
Install PWSP via Flatpak from our custom repository:
PWSP is designed with a clear separation of concerns, operating through a client-server architecture. It consists of
three main components:
* **pwsp-daemon**: This is the core of the application. It runs silently in the background, managing all the
heavy-lifting tasks. The daemon is responsible for:
* Creating and managing virtual audio devices.
* Linking these devices within the PipeWire graph.
* Handling all audio playback.
* **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
* **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.
# **🚀 Installation**
## **Pre-built Packages**
You can download pre-built binaries and .deb packages from
the [releases page](https://github.com/arabianq/pipewire-soundpad/releases).
## **Flatpak**
You can install PWSP via Flatpak from our custom repository hosted on GitHub Pages.
Add the repository:
```bash ```bash
flatpak remote-add --user --if-not-exists arabianq-repo https://arabianq.github.io/pipewire-soundpad/index.flatpakrepo flatpak remote-add --user --if-not-exists pwsp-repo https://arabianq.github.io/pipewire-soundpad/index.flatpakrepo
```
Install the stable version: # Install stable version
```bash
flatpak install --user arabianq-repo ru.arabianq.pwsp//stable flatpak install --user arabianq-repo ru.arabianq.pwsp//stable
```
Or install the nightly version (latest commit to `main`): # Or install the nightly version (latest commit)
```bash
flatpak install --user arabianq-repo ru.arabianq.pwsp//nightly flatpak install --user arabianq-repo ru.arabianq.pwsp//nightly
``` ```
## **Fedora Linux (and derivatives)** ### 🐧 Linux Packages
**Fedora (and derivatives):**
If you're using Fedora, you can install PWSP from a dedicated repository using DNF.
Add the repository:
```bash ```bash
sudo dnf copr enable arabianq/pwsp sudo dnf copr enable arabianq/pwsp
```
Update cache:
```bash
sudo dnf makecache
```
Install PWSP:
```bash
sudo dnf install pwsp sudo dnf install pwsp
``` ```
## **Arch Linux** **Arch Linux (AUR):**
There is pwsp package in AUR.
You can install it using yay, paru or any other AUR helper.
```bash ```bash
paru pwsp-bin # or paru pwsp to build it locally paru -S pwsp-bin # or 'pwsp' to build from source
``` ```
## **Installing using cargo** **Debian / Ubuntu:**
Download pre-built `.deb` packages or standalone binaries from the [Releases page](https://github.com/arabianq/pipewire-soundpad/releases).
### 🦀 Cargo / Source Build
```bash ```bash
cargo install pwsp cargo install pwsp
```
## **Building from source** # OR clone and build manually:
git clone https://github.com/arabianq/pipewire-soundpad.git
#### **Requirements**
* **Rust**: Install [Rust](https://www.rust-lang.org/tools/install) (using rustup is recommended).
* **PipeWire**: Ensure that [PipeWire](https://pipewire.org/) is installed and running on your system.
#### **Build Instructions**
Clone the repository:
```bash
git clone https://github.com/arabianq/pipewire-soundpad.git
cd pipewire-soundpad cd pipewire-soundpad
```
Build the project:
```bash
cargo build --release cargo build --release
``` ```
*(Note: Requires Rust toolchain and PipeWire running on your system).*
Now you have three binary files inside ./target/release/: ---
- **pwsp-gui** ## 🎮 Usage
- **pwsp-cli**
- **pwsp-daemon**
# **🎮 Usage** ### 1. Start the Daemon
Before using the GUI or CLI, the daemon must be running in the background.
Before using pwsp-gui or pwsp-cli, you **must** first run the pwsp-daemon in the background.
### **Running the Daemon**
You can start the daemon from the terminal or enable the systemd service for automatic startup.
* **Manual Start:**
```bash ```bash
/path/to/your/pwsp-daemon & # Recommended: Start and enable via systemd (starts on login)
systemctl --user enable --now pwsp-daemon
# Manual start (if not using systemd):
pwsp-daemon &
``` ```
* **Using systemd (recommended):** ### 2. Using the GUI
If you installed PWSP using prebuilt packages, the systemd service is added automatically. 1. **Add Sounds:** Click the **"+"** button to add a directory containing your audio files.
1. **Start the service:** 2. **Select Mic:** Choose your physical microphone from the dropdown. PWSP will instantly create a virtual microphone combining your voice and the soundboard.
```bash 3. **Play:** Click any sound to play it, adjust its volume, or assign a hotkey for quick access.
systemctl --user start pwsp-daemon
```
2. **Enable autostart (starts on login):**
```bash
systemctl --user enable --now pwsp-daemon
```
### **Using the GUI**
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 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. You can also play single file once using **"Play File"** button.
### **Using the CLI**
The pwsp-cli tool allows you to control the daemon from the command line.
* **General Help**: To see a list of all available commands, run:
### 3. Using the CLI
Control the daemon directly from your terminal:
```bash ```bash
pwsp-cli --help pwsp-cli action play /path/to/sound.mp3
pwsp-cli get volume
pwsp-cli set position 20
pwsp-cli --help # View all commands
``` ```
* **Example Commands**: ---
* **Play a file**:
```bash ## ⌨️ Shortcuts & Controls
pwsp-cli action play <file_path>
```
* **Get the current volume**: | Action | Keyboard | Mouse |
| :----------------------------------- | :--------------------- | :------------------- |
| **Play Track** (Stops others) | `Enter` | `Left Click` |
| **Add Track** (Plays simultaneously) | `Ctrl + Enter` | `Ctrl + Left Click` |
| **Replace Last Track** | `Shift + Enter` | `Shift + Left Click` |
| **Pause / Resume** | `Space` | |
| **Stop All Tracks** | `Backspace` | |
| **Open / Close Settings** | `I` | |
| **Search** | `/` | |
| **Navigate Files** | `Ctrl + ↑ / ↓` | |
| **Navigate Directories** | `Ctrl + Shift + ↑ / ↓` | |
```bash ---
pwsp-cli get volume
```
* **Set playback position to 20 seconds**: ## 🤝 Contributing
Contributions, issues, and feature requests are welcome! Feel free to check out the [issues page](https://github.com/arabianq/pipewire-soundpad/issues).
```bash
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
an [issue](https://github.com/arabianq/pipewire-soundpad/issues) or submit
a [pull request](https://github.com/arabianq/pipewire-soundpad/pulls).
# **📜 License**
This project is licensed under
the [MIT License](https://github.com/arabianq/pipewire-soundpad/blob/main/LICENSE).
# **🤖 AI Wiki**
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/arabianq/pipewire-soundpad) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/arabianq/pipewire-soundpad)
## 📜 License
This project is licensed under the [MIT License](LICENSE).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 94 KiB

+4 -4
View File
@@ -1,7 +1,7 @@
pkgbase = pwsp-bin pkgbase = pwsp-bin
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries) pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
pkgver = 1.7.4 pkgver = 1.8.1
pkgrel = 2 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64 arch = x86_64
license = MIT license = MIT
@@ -9,8 +9,8 @@ depends = pipewire
depends = alsa-lib depends = alsa-lib
provides = pwsp provides = pwsp
conflicts = pwsp conflicts = pwsp
source = pwsp-bin-1.7.4.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.7.4/pwsp-v1.7.4-linux-x64.zip source = pwsp-bin-1.8.1.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.8.1/pwsp-v1.8.1-linux-x64.zip
source = pipewire-soundpad-1.7.4.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.4.tar.gz source = pipewire-soundpad-1.8.1.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.8.1.tar.gz
sha256sums = SKIP sha256sums = SKIP
sha256sums = SKIP sha256sums = SKIP
+2 -2
View File
@@ -1,8 +1,8 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgname=pwsp-bin pkgname=pwsp-bin
_pkgname=pipewire-soundpad _pkgname=pipewire-soundpad
pkgver=1.7.4 pkgver=1.8.1
pkgrel=2 pkgrel=1
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)" pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
arch=('x86_64') arch=('x86_64')
url="https://github.com/arabianq/pipewire-soundpad" url="https://github.com/arabianq/pipewire-soundpad"
+3 -2
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone pkgdesc = Lets you play audio files through your microphone
pkgver = 1.7.4 pkgver = 1.8.1
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = any arch = any
@@ -8,9 +8,10 @@ pkgbase = pwsp
makedepends = clang makedepends = clang
makedepends = rust makedepends = rust
makedepends = cargo makedepends = cargo
makedepends = cmake
makedepends = pipewire makedepends = pipewire
makedepends = alsa-lib makedepends = alsa-lib
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.4.tar.gz source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.8.1.tar.gz
sha256sums = SKIP sha256sums = SKIP
pkgname = pwsp pkgname = pwsp
+2 -2
View File
@@ -1,13 +1,13 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp pkgsubn=pwsp
pkgname=pwsp pkgname=pwsp
pkgver=1.7.4 pkgver=1.8.1
pkgrel=1 pkgrel=1
pkgdesc="Lets you play audio files through your microphone" pkgdesc="Lets you play audio files through your microphone"
arch=('any') arch=('any')
url="https://github.com/arabianq/pipewire-soundpad" url="https://github.com/arabianq/pipewire-soundpad"
license=('MIT') license=('MIT')
makedepends=(clang rust cargo pipewire alsa-lib) makedepends=(clang rust cargo cmake pipewire alsa-lib)
source=("$url/archive/refs/tags/v$pkgver.tar.gz") source=("$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP') sha256sums=('SKIP')
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@
<name>arabian</name> <name>arabian</name>
</developer> </developer>
<releases> <releases>
<release version="1.7.4" date="2026-04-25" /> <release version="1.8.1" date="2026-05-15" />
</releases> </releases>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
</component> </component>
+4 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0 %global cargo_install_lib 0
Name: pwsp Name: pwsp
Version: 1.7.4 Version: 1.8.1
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
@@ -18,6 +18,9 @@ BuildRequires: cargo
BuildRequires: pipewire-devel BuildRequires: pipewire-devel
BuildRequires: alsa-lib-devel BuildRequires: alsa-lib-devel
BuildRequires: clang-devel BuildRequires: clang-devel
BuildRequires: cmake
BuildRequires: dbus-devel
BuildRequires: pkgconf-pkg-config
%global _description %{expand: %global _description %{expand:
PWSP lets you play audio files through your microphone. Has both CLI and PWSP lets you play audio files through your microphone. Has both CLI and
+366 -346
View File
@@ -9,10 +9,7 @@ use egui_material_icons::icons::*;
use pwsp::types::socket::Request; use pwsp::types::socket::Request;
use pwsp::types::{audio_player::TrackInfo, gui::AppState}; use pwsp::types::{audio_player::TrackInfo, gui::AppState};
use pwsp::utils::gui::{format_time_pair, make_request_async}; use pwsp::utils::gui::{format_time_pair, make_request_async};
use std::{ use std::{path::Path, time::Instant};
path::{Path, PathBuf},
time::Instant,
};
enum TrackAction { enum TrackAction {
Pause(u32), Pause(u32),
@@ -137,271 +134,278 @@ impl SoundpadGui {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.spacing_mut().item_spacing.y = 5.0; ui.spacing_mut().item_spacing.y = 5.0;
// --- Header --- self.draw_hotkeys_header(ui);
ui.horizontal(|ui| {
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
if ui.add(back_button).clicked() {
self.app_state.show_hotkeys = false;
}
ui.vertical_centered(|ui| {
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
});
});
ui.separator(); ui.separator();
// --- Search and Add Command --- self.draw_hotkeys_search(ui);
ui.horizontal(|ui| {
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| {
let mut selected_cmd = None;
if ui.button("Toggle Pause").clicked() {
selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None)));
}
if ui.button("Stop Playback").clicked() {
selected_cmd = Some(("cmd_stop", Request::stop(None)));
}
if ui.button("Pause Playback").clicked() {
selected_cmd = Some(("cmd_pause", Request::pause(None)));
}
if ui.button("Resume Playback").clicked() {
selected_cmd = Some(("cmd_resume", Request::resume(None)));
}
if ui.button("Toggle Loop").clicked() {
selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None)));
}
if let Some((slot_name, req)) = selected_cmd {
make_request_async(Request::set_hotkey_action(slot_name, &req));
self.app_state
.hotkey_config
.set_slot(slot_name.to_string(), req);
self.app_state.assigning_hotkey_slot = Some(slot_name.to_string());
self.app_state.hotkey_capture_active = true;
ui.close();
}
});
ui.add_space(10.0);
ui.add(
TextEdit::singleline(&mut self.app_state.hotkey_search_query)
.hint_text("Search hotkeys...")
.desired_width(f32::INFINITY),
);
});
ui.separator(); ui.separator();
ui.add_space(5.0); ui.add_space(5.0);
let conflicts = self.app_state.hotkey_config.find_conflicts(); let action = self.draw_hotkeys_table(ui);
let conflict_slots: std::collections::HashSet<&str> =
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
let search = self.app_state.hotkey_search_query.to_lowercase();
let mut action: Option<HotkeyAction> = None;
let slots: Vec<_> = self
.app_state
.hotkey_config
.slots
.iter()
.filter(|s| {
if search.is_empty() {
return true;
}
s.slot.to_lowercase().contains(&search)
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|| s.key_chord
.as_deref()
.unwrap_or("")
.to_lowercase()
.contains(&search)
})
.cloned()
.collect();
let available_width = ui.available_width();
let col_width = (available_width / 4.0).max(80.0);
TableBuilder::new(ui)
.striped(true)
.column(Column::exact(col_width).clip(true)) // Slot
.column(Column::exact(col_width).clip(true)) // Sound / Action name
.column(Column::exact(col_width).clip(true)) // Key Chord
.column(Column::exact(col_width).clip(true)) // Actions
.header(30.0, |mut header| {
header.col(|ui| {
ui.label(
RichText::new("Slot")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Sound")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Key Chord")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Actions")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
})
.body(|mut body| {
if slots.is_empty() {
body.row(30.0, |mut row| {
row.col(|_| {});
row.col(|ui| {
ui.label(
RichText::new("No hotkey slots configured.")
.color(Color32::GRAY),
);
});
row.col(|_| {});
row.col(|_| {});
});
return;
}
for slot in &slots {
body.row(30.0, |mut row| {
// Column 1: Slot
row.col(|ui| {
ui.horizontal(|ui| {
if conflict_slots.contains(slot.slot.as_str()) {
ui.label(
RichText::new(ICON_WARNING.codepoint)
.color(Color32::from_rgb(255, 165, 0)),
)
.on_hover_text("Key chord conflict");
}
ui.add(
Label::new(RichText::new(&slot.slot).monospace())
.truncate(),
);
});
});
// Column 2: Sound / Action name
row.col(|ui| {
let action_name = match slot.action.name.as_str() {
"play" => {
if let Some(file_path_str) =
slot.action.args.get("file_path")
{
Path::new(file_path_str)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
} else {
"Play".to_string()
}
}
"toggle_pause" => "Toggle Pause".to_string(),
"pause" => "Pause Playback".to_string(),
"resume" => "Resume Playback".to_string(),
"stop" => "Stop Playback".to_string(),
"toggle_loop" => "Toggle Loop".to_string(),
other => other.to_string(),
};
ui.add(
Label::new(RichText::new(action_name).monospace()).truncate(),
);
});
// Column 3: Key Chord
row.col(|ui| {
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
ui.add(
Label::new(RichText::new(chord_text).monospace().color(
if slot.key_chord.is_some() {
Color32::from_rgb(100, 200, 100)
} else {
Color32::GRAY
},
))
.truncate(),
);
});
// Column 4: Actions
row.col(|ui| {
ui.horizontal(|ui| {
if ui
.add(Button::new(ICON_DELETE).frame(false))
.on_hover_text("Remove slot")
.clicked()
{
action = Some(HotkeyAction::Remove(slot.slot.clone()));
}
if ui
.add(Button::new(ICON_KEYBOARD).frame(false))
.on_hover_text("Set key chord")
.clicked()
{
action = Some(HotkeyAction::Capture(slot.slot.clone()));
}
if slot.key_chord.is_some()
&& ui
.add(Button::new(ICON_BACKSPACE).frame(false))
.on_hover_text("Clear key chord")
.clicked()
{
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
}
if ui
.add(Button::new(ICON_PLAY_ARROW).frame(false))
.on_hover_text("Play")
.clicked()
{
action = Some(HotkeyAction::Play(slot.slot.clone()));
}
});
});
});
}
});
if let Some(action) = action { if let Some(action) = action {
match action { self.handle_hotkey_action(action);
HotkeyAction::Remove(slot) => {
make_request_async(Request::clear_hotkey(&slot));
self.app_state.hotkey_config.remove_slot(&slot);
}
HotkeyAction::Capture(slot) => {
self.app_state.assigning_hotkey_slot = Some(slot);
self.app_state.hotkey_capture_active = true;
}
HotkeyAction::ClearChord(slot) => {
make_request_async(Request::clear_hotkey_key(&slot));
self.app_state.hotkey_config.set_key_chord(&slot, None);
}
HotkeyAction::Play(slot) => {
self.play_hotkey_slot(&slot);
}
}
} }
}); });
} }
fn draw_hotkeys_header(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
if ui.add(back_button).clicked() {
self.app_state.show_hotkeys = false;
}
ui.vertical_centered(|ui| {
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
});
});
}
fn draw_hotkeys_search(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| {
let mut selected_cmd = None;
if ui.button("Toggle Pause").clicked() {
selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None)));
}
if ui.button("Stop Playback").clicked() {
selected_cmd = Some(("cmd_stop", Request::stop(None)));
}
if ui.button("Pause Playback").clicked() {
selected_cmd = Some(("cmd_pause", Request::pause(None)));
}
if ui.button("Resume Playback").clicked() {
selected_cmd = Some(("cmd_resume", Request::resume(None)));
}
if ui.button("Toggle Loop").clicked() {
selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None)));
}
if let Some((slot_name, req)) = selected_cmd {
make_request_async(Request::set_hotkey_action(slot_name, &req));
self.app_state
.hotkey_config
.set_slot(slot_name.to_string(), req);
self.app_state.assigning_hotkey_slot = Some(slot_name.to_string());
self.app_state.hotkey_capture_active = true;
ui.close();
}
});
ui.add_space(10.0);
ui.add(
TextEdit::singleline(&mut self.app_state.hotkey_search_query)
.hint_text("Search hotkeys...")
.desired_width(f32::INFINITY),
);
});
}
fn draw_hotkeys_table(&mut self, ui: &mut Ui) -> Option<HotkeyAction> {
let conflicts = self.app_state.hotkey_config.find_conflicts();
let conflict_slots: std::collections::HashSet<&str> =
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
let search = self.app_state.hotkey_search_query.to_lowercase();
let mut action: Option<HotkeyAction> = None;
let slots: Vec<_> = self
.app_state
.hotkey_config
.slots
.iter()
.filter(|s| {
if search.is_empty() {
return true;
}
s.slot.to_lowercase().contains(&search)
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|| s.key_chord
.as_deref()
.unwrap_or("")
.to_lowercase()
.contains(&search)
})
.collect();
let available_width = ui.available_width();
let col_width = (available_width / 4.0).max(80.0);
TableBuilder::new(ui)
.striped(true)
.column(Column::exact(col_width).clip(true)) // Slot
.column(Column::exact(col_width).clip(true)) // Sound / Action name
.column(Column::exact(col_width).clip(true)) // Key Chord
.column(Column::exact(col_width).clip(true)) // Actions
.header(30.0, |mut header| {
header.col(|ui| {
ui.label(
RichText::new("Slot")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Sound")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Key Chord")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Actions")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
})
.body(|mut body| {
if slots.is_empty() {
body.row(30.0, |mut row| {
row.col(|_| {});
row.col(|ui| {
ui.label(
RichText::new("No hotkey slots configured.").color(Color32::GRAY),
);
});
row.col(|_| {});
row.col(|_| {});
});
return;
}
for slot in &slots {
body.row(30.0, |mut row| {
// Column 1: Slot
row.col(|ui| {
ui.horizontal(|ui| {
if conflict_slots.contains(slot.slot.as_str()) {
ui.label(
RichText::new(ICON_WARNING.codepoint)
.color(Color32::from_rgb(255, 165, 0)),
)
.on_hover_text("Key chord conflict");
}
ui.add(
Label::new(RichText::new(&slot.slot).monospace()).truncate(),
);
});
});
// Column 2: Sound / Action name
row.col(|ui| {
let action_name = match slot.action.name.as_str() {
"play" => {
if let Some(file_path_str) = slot.action.args.get("file_path") {
Path::new(file_path_str)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
} else {
"Play".to_string()
}
}
"toggle_pause" => "Toggle Pause".to_string(),
"pause" => "Pause Playback".to_string(),
"resume" => "Resume Playback".to_string(),
"stop" => "Stop Playback".to_string(),
"toggle_loop" => "Toggle Loop".to_string(),
other => other.to_string(),
};
ui.add(Label::new(RichText::new(action_name).monospace()).truncate());
});
// Column 3: Key Chord
row.col(|ui| {
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
ui.add(
Label::new(RichText::new(chord_text).monospace().color(
if slot.key_chord.is_some() {
Color32::from_rgb(100, 200, 100)
} else {
Color32::GRAY
},
))
.truncate(),
);
});
// Column 4: Actions
row.col(|ui| {
ui.horizontal(|ui| {
if ui
.add(Button::new(ICON_DELETE).frame(false))
.on_hover_text("Remove slot")
.clicked()
{
action = Some(HotkeyAction::Remove(slot.slot.clone()));
}
if ui
.add(Button::new(ICON_KEYBOARD).frame(false))
.on_hover_text("Set key chord")
.clicked()
{
action = Some(HotkeyAction::Capture(slot.slot.clone()));
}
if slot.key_chord.is_some()
&& ui
.add(Button::new(ICON_BACKSPACE).frame(false))
.on_hover_text("Clear key chord")
.clicked()
{
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
}
if ui
.add(Button::new(ICON_PLAY_ARROW).frame(false))
.on_hover_text("Play")
.clicked()
{
action = Some(HotkeyAction::Play(slot.slot.clone()));
}
});
});
});
}
});
action
}
fn handle_hotkey_action(&mut self, action: HotkeyAction) {
match action {
HotkeyAction::Remove(slot) => {
make_request_async(Request::clear_hotkey(&slot));
self.app_state.hotkey_config.remove_slot(&slot);
}
HotkeyAction::Capture(slot) => {
self.app_state.assigning_hotkey_slot = Some(slot);
self.app_state.hotkey_capture_active = true;
}
HotkeyAction::ClearChord(slot) => {
make_request_async(Request::clear_hotkey_key(&slot));
self.app_state.hotkey_config.set_key_chord(&slot, None);
}
HotkeyAction::Play(slot) => {
self.play_hotkey_slot(&slot);
}
}
}
fn draw_header(&mut self, ui: &mut Ui) { fn draw_header(&mut self, ui: &mut Ui) {
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
if self.audio_player_state.tracks.is_empty() { if self.audio_player_state.tracks.is_empty() {
@@ -409,10 +413,9 @@ impl SoundpadGui {
return; return;
} }
let tracks = self.audio_player_state.tracks.clone();
let mut action = None; let mut action = None;
for track in tracks { for track in &self.audio_player_state.tracks {
CollapsingHeader::new( CollapsingHeader::new(
RichText::new( RichText::new(
track track
@@ -427,7 +430,7 @@ impl SoundpadGui {
) )
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, &track) { if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, track) {
action = Some(act); action = Some(act);
} }
}); });
@@ -445,6 +448,99 @@ impl SoundpadGui {
}); });
} }
fn draw_playback_controls(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
let mut action = None;
let play_button = Button::new(if track.paused {
ICON_PLAY_ARROW
} else {
ICON_PAUSE
})
.corner_radius(15.0);
if ui.add_sized([30.0, 30.0], play_button).clicked() {
action = Some(if track.paused {
TrackAction::Resume(track.id)
} else {
TrackAction::Pause(track.id)
});
}
let loop_button = Button::new(
RichText::new(if track.looped {
ICON_REPEAT_ONE
} else {
ICON_REPEAT
})
.size(18.0),
)
.frame(false);
if ui.add_sized([15.0, 30.0], loop_button).clicked() {
action = Some(TrackAction::ToggleLoop(track.id));
}
action
}
fn draw_position_control(
ui: &mut Ui,
ui_state: &mut pwsp::types::gui::TrackUiState,
track: &TrackInfo,
default_slider_width: f32,
) {
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 position_slider_width = ui.available_width()
- (30.0 * 3.0)
- default_slider_width
- (ui.spacing().item_spacing.x * 6.0);
ui.spacing_mut().slider_width = position_slider_width;
if ui.add_sized([30.0, 30.0], position_slider).drag_stopped() {
ui_state.position_dragged = true;
}
let time_label =
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
ui.add_sized([30.0, 30.0], time_label);
}
fn draw_volume_control(
ui: &mut Ui,
ui_state: &mut pwsp::types::gui::TrackUiState,
track: &TrackInfo,
default_slider_width: f32,
) {
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));
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 - 30.0;
ui.spacing_mut().item_spacing.x = 0.0;
if ui.add_sized([30.0, 30.0], volume_slider).drag_stopped() {
ui_state.volume_dragged = true;
}
}
fn draw_stop_control(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
let stop_button = Button::new(ICON_CLOSE).frame(false);
if ui.add_sized([30.0, 30.0], stop_button).clicked() {
Some(TrackAction::Stop(track.id))
} else {
None
}
}
fn draw_track_control( fn draw_track_control(
ui: &mut Ui, ui: &mut Ui,
app_state: &mut AppState, app_state: &mut AppState,
@@ -475,93 +571,17 @@ impl SoundpadGui {
let mut action = None; let mut action = None;
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
// ---------- Play Button ---------- if let Some(act) = Self::draw_playback_controls(ui, track) {
let play_button = Button::new(if track.paused { action = Some(act);
ICON_PLAY_ARROW
} else {
ICON_PAUSE
})
.corner_radius(15.0);
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
if play_button_response.clicked() {
if track.paused {
action = Some(TrackAction::Resume(track.id));
} else {
action = Some(TrackAction::Pause(track.id));
}
} }
// --------------------------------
// ---------- Loop Button ----------
let loop_button = Button::new(
RichText::new(if track.looped {
ICON_REPEAT_ONE
} else {
ICON_REPEAT
})
.size(18.0),
)
.frame(false);
let loop_button_response = ui.add_sized([15.0, 30.0], loop_button);
if loop_button_response.clicked() {
action = Some(TrackAction::ToggleLoop(track.id));
}
// --------------------------------
// ---------- Position Slider ----------
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 default_slider_width = ui.spacing().slider_width;
let position_slider_width = ui.available_width() Self::draw_position_control(ui, ui_state, track, default_slider_width);
- (30.0 * 3.0) Self::draw_volume_control(ui, ui_state, track, default_slider_width);
- default_slider_width
- (ui.spacing().item_spacing.x * 6.0); if let Some(act) = Self::draw_stop_control(ui, track) {
ui.spacing_mut().slider_width = position_slider_width; action = Some(act);
let position_slider_response = ui.add_sized([30.0, 30.0], position_slider);
if position_slider_response.drag_stopped() {
ui_state.position_dragged = true;
} }
// --------------------------------
// ---------- Time Label ----------
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 = 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 ui_state.volume_slider_value, 0.0..=1.0)
.show_value(false)
.step_by(0.01);
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() {
ui_state.volume_dragged = true;
}
// --------------------------------
// ---------- Stop Button ---------
let stop_button = Button::new(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 action
@@ -836,11 +856,11 @@ impl SoundpadGui {
}); });
} }
fn get_hotkey_badge(&self, path: &PathBuf) -> Option<String> { fn get_hotkey_badge(&self, path: &Path) -> Option<String> {
for slot in &self.app_state.hotkey_config.slots { for slot in &self.app_state.hotkey_config.slots {
if slot.action.name == "play" if slot.action.name == "play"
&& let Some(file_path_str) = slot.action.args.get("file_path") && let Some(file_path_str) = slot.action.args.get("file_path")
&& Path::new(file_path_str) == path.as_path() && Path::new(file_path_str) == path
{ {
if let Some(chord) = &slot.key_chord { if let Some(chord) = &slot.key_chord {
return Some(format!("[{}]", chord)); return Some(format!("[{}]", chord));
+8 -2
View File
@@ -9,7 +9,10 @@ use std::path::PathBuf;
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> { fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
let key_name = key.name(); let key_name = key.name();
let is_valid = (key_name.len() == 1 let is_valid = (key_name.len() == 1
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric())) && key_name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphanumeric()))
|| (key_name.starts_with('F') || (key_name.starts_with('F')
&& key_name.len() > 1 && key_name.len() > 1
&& key_name[1..].chars().all(|c| c.is_ascii_digit())); && key_name[1..].chars().all(|c| c.is_ascii_digit()));
@@ -60,7 +63,10 @@ pub fn parse_chord(chord: &str) -> Option<(Modifiers, Key)> {
let key_name = parts[parts.len() - 1]; let key_name = parts[parts.len() - 1];
let is_valid = (key_name.len() == 1 let is_valid = (key_name.len() == 1
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric())) && key_name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphanumeric()))
|| (key_name.starts_with('F') || (key_name.starts_with('F')
&& key_name.len() > 1 && key_name.len() > 1
&& key_name[1..].chars().all(|c| c.is_ascii_digit())); && key_name[1..].chars().all(|c| c.is_ascii_digit()));
+60 -5
View File
@@ -3,7 +3,7 @@ mod input;
mod update; mod update;
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native}; use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, Vec2, ViewportBuilder}; use egui::{Context, FontData, FontDefinitions, FontFamily, FontTweak, Vec2, ViewportBuilder};
use itertools::Itertools; use itertools::Itertools;
use pwsp::{ use pwsp::{
types::{ types::{
@@ -21,12 +21,14 @@ use pwsp::{
use rfd::FileDialog; use rfd::FileDialog;
use std::{ use std::{
error::Error, error::Error,
path::PathBuf, fs,
path::{Path, PathBuf},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use system_fonts::{FontStyle, FoundFontSource, find_for_locale};
const SUPPORTED_EXTENSIONS: [&str; 12] = [ const SUPPORTED_EXTENSIONS: [&str; 13] = [
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus",
]; ];
struct SoundpadGui { struct SoundpadGui {
@@ -120,7 +122,7 @@ impl SoundpadGui {
} }
} }
pub fn play_file(&mut self, path: &PathBuf, concurrent: bool) { pub fn play_file(&mut self, path: &Path, concurrent: bool) {
make_request_async(Request::play(&path.to_string_lossy(), concurrent)); make_request_async(Request::play(&path.to_string_lossy(), concurrent));
} }
@@ -196,6 +198,54 @@ impl SoundpadGui {
} }
} }
fn add_font(
font_name: &str,
font_bytes: &[u8],
fonts: &mut FontDefinitions,
) -> Result<(), Box<dyn Error>> {
let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak {
scale: 1.0,
hinting_override: Some(true),
..Default::default()
});
fonts
.font_data
.insert(font_name.to_owned(), font_data.into());
fonts
.families
.entry(FontFamily::Proportional)
.or_default()
.insert(0, font_name.to_owned());
fonts
.families
.entry(FontFamily::Monospace)
.or_default()
.insert(0, font_name.to_owned());
Ok(())
}
fn load_system_fonts(fonts: &mut FontDefinitions) -> Result<(), Box<dyn Error>> {
let (_, en_sans) = find_for_locale("en", FontStyle::Sans);
let (_, en_serif) = find_for_locale("en", FontStyle::Serif);
let (_, ja_sans) = find_for_locale("ja", FontStyle::Sans);
let system_fonts = [en_sans, en_serif, ja_sans].concat();
for font in system_fonts.iter().rev() {
let font_bytes = match &font.source {
FoundFontSource::Path(path) => fs::read(path)?,
FoundFontSource::Bytes(bytes) => bytes.to_vec(),
};
add_font(&font.key, &font_bytes, fonts)?;
}
Ok(())
}
pub async fn run() -> Result<(), Box<dyn Error>> { pub async fn run() -> Result<(), Box<dyn Error>> {
const ICON: &[u8] = include_bytes!("../../assets/icon.png"); const ICON: &[u8] = include_bytes!("../../assets/icon.png");
@@ -218,6 +268,11 @@ pub async fn run() -> Result<(), Box<dyn Error>> {
options, options,
Box::new(|cc| { Box::new(|cc| {
egui_material_icons::initialize(&cc.egui_ctx); egui_material_icons::initialize(&cc.egui_ctx);
let mut fonts = FontDefinitions::default();
load_system_fonts(&mut fonts).ok();
cc.egui_ctx.set_fonts(fonts);
Ok(Box::new(SoundpadGui::new(&cc.egui_ctx))) Ok(Box::new(SoundpadGui::new(&cc.egui_ctx)))
}), }),
) { ) {
+114 -35
View File
@@ -9,11 +9,11 @@ use tokio::{
time::{Duration, timeout}, time::{Duration, timeout},
}; };
pub fn setup_pipewire_context() -> (MainLoopRc, ContextRc) { pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
pipewire::init(); pipewire::init();
let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop"); let main_loop = MainLoopRc::new(None).map_err(|e| e.to_string())?;
let context = ContextRc::new(&main_loop, None).expect("Failed to create pipewire context"); let context = ContextRc::new(&main_loop, None).map_err(|e| e.to_string())?;
(main_loop, context) Ok((main_loop, context))
} }
fn parse_global_object( fn parse_global_object(
@@ -85,8 +85,15 @@ fn parse_global_object(
async fn pw_get_global_objects_thread( async fn pw_get_global_objects_thread(
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>, main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
pw_receiver: pipewire::channel::Receiver<Terminate>, pw_receiver: pipewire::channel::Receiver<Terminate>,
init_sender: std::sync::mpsc::SyncSender<Result<(), String>>,
) { ) {
let (main_loop, context) = setup_pipewire_context(); let (main_loop, context) = match setup_pipewire_context() {
Ok(res) => res,
Err(e) => {
let _ = init_sender.send(Err(e));
return;
}
};
// Stop main loop on Terminate message // Stop main loop on Terminate message
let _receiver = pw_receiver.attach(main_loop.loop_(), { let _receiver = pw_receiver.attach(main_loop.loop_(), {
@@ -94,12 +101,24 @@ async fn pw_get_global_objects_thread(
move |_| _main_loop.quit() move |_| _main_loop.quit()
}); });
let core = context let core = match context.connect(None) {
.connect(None) Ok(core) => core,
.expect("Failed to connect to pipewire context"); Err(e) => {
let registry = core let _ = init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
.get_registry() return;
.expect("Failed to get registry from pipewire context"); }
};
let registry = match core.get_registry() {
Ok(registry) => registry,
Err(e) => {
let _ = init_sender.send(Err(format!(
"Failed to get registry from pipewire context: {}",
e
)));
return;
}
};
let _listener = registry let _listener = registry
.add_listener_local() .add_listener_local()
@@ -115,6 +134,11 @@ async fn pw_get_global_objects_thread(
}) })
.register(); .register();
// Signal successful initialization
if init_sender.send(Ok(())).is_err() {
return;
}
main_loop.run(); main_loop.run();
} }
@@ -122,10 +146,17 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
// Channels to communicate with pipewire thread // Channels to communicate with pipewire thread
let (main_sender, mut main_receiver) = mpsc::channel(10); let (main_sender, mut main_receiver) = mpsc::channel(10);
let (pw_sender, pw_receiver) = pipewire::channel::channel(); let (pw_sender, pw_receiver) = pipewire::channel::channel();
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
// Spawn pipewire thread in background // Spawn pipewire thread in background
let _pw_thread = let _pw_thread = tokio::spawn(async move {
tokio::spawn(async move { pw_get_global_objects_thread(main_sender, pw_receiver).await }); pw_get_global_objects_thread(main_sender, pw_receiver, init_sender).await
});
// Wait for initialization to complete
if let Err(e) = init_receiver.recv()? {
return Err(e.into());
}
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new(); let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new(); let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new();
@@ -150,9 +181,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
} }
Ok(None) | Err(_) => { Ok(None) | Err(_) => {
// Pipewire thread is finished and we can collect our devices // Pipewire thread is finished and we can collect our devices
pw_sender let _ = pw_sender.send(Terminate {});
.send(Terminate {})
.expect("Failed to terminate pipewire thread");
for port in ports { for port in ports {
let node_id = port.node_id; let node_id = port.node_id;
@@ -200,8 +229,8 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
let mut output_devices: Vec<AudioDevice> = let mut output_devices: Vec<AudioDevice> =
output_devices.values().cloned().collect(); output_devices.values().cloned().collect();
input_devices.sort_by(|a, b| a.id.cmp(&b.id)); input_devices.sort_by_key(|a| a.id);
output_devices.sort_by(|a, b| a.id.cmp(&b.id)); output_devices.sort_by_key(|a| a.id);
return Ok((input_devices, output_devices)); return Ok((input_devices, output_devices));
} }
@@ -226,12 +255,24 @@ pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> { pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>(); let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
let _pw_thread = thread::spawn(move || { let _pw_thread = thread::spawn(move || {
let (main_loop, context) = setup_pipewire_context(); let (main_loop, context) = match setup_pipewire_context() {
let core = context Ok(res) => res,
.connect(None) Err(e) => {
.expect("Failed to connect to pipewire context"); let _ = init_sender.send(Err(e));
return;
}
};
let core = match context.connect(None) {
Ok(core) => core,
Err(e) => {
let _ =
init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
return;
}
};
let props = properties!( let props = properties!(
"factory.name" => "support.null-audio-sink", "factory.name" => "support.null-audio-sink",
@@ -243,9 +284,13 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
"object.linger" => "false", // Destroy the node on app exit "object.linger" => "false", // Destroy the node on app exit
); );
let _node = core let _node = match core.create_object::<pipewire::node::Node>("adapter", &props) {
.create_object::<pipewire::node::Node>("adapter", &props) Ok(node) => node,
.expect("Failed to create virtual mic"); Err(e) => {
let _ = init_sender.send(Err(format!("Failed to create virtual mic: {}", e)));
return;
}
};
let _receiver = pw_receiver.attach(main_loop.loop_(), { let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone(); let _main_loop = main_loop.clone();
@@ -253,9 +298,16 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
}); });
println!("Virtual mic created"); println!("Virtual mic created");
if init_sender.send(Ok(())).is_err() {
return;
}
main_loop.run(); main_loop.run();
}); });
if let Err(e) = init_receiver.recv()? {
return Err(e.into());
}
Ok(pw_sender) Ok(pw_sender)
} }
@@ -304,12 +356,24 @@ pub fn create_link(
input_fr: Port, input_fr: Port,
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> { ) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>(); let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
let _pw_thread = thread::spawn(move || { let _pw_thread = thread::spawn(move || {
let (main_loop, context) = setup_pipewire_context(); let (main_loop, context) = match setup_pipewire_context() {
let core = context Ok(res) => res,
.connect(None) Err(e) => {
.expect("Failed to connect to pipewire context"); let _ = init_sender.send(Err(e));
return;
}
};
let core = match context.connect(None) {
Ok(core) => core,
Err(e) => {
let _ =
init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
return;
}
};
let props_fl = properties! { let props_fl = properties! {
"link.output.node" => format!("{}", output_fl.node_id).as_str(), "link.output.node" => format!("{}", output_fl.node_id).as_str(),
@@ -324,12 +388,20 @@ pub fn create_link(
"link.input.port" => format!("{}", input_fr.port_id).as_str(), "link.input.port" => format!("{}", input_fr.port_id).as_str(),
}; };
let _link_fl = core let _link_fl = match core.create_object::<Link>("link-factory", &props_fl) {
.create_object::<Link>("link-factory", &props_fl) Ok(link) => link,
.expect("Failed to create link FL"); Err(e) => {
let _link_fr = core let _ = init_sender.send(Err(format!("Failed to create link FL: {}", e)));
.create_object::<Link>("link-factory", &props_fr) return;
.expect("Failed to create link FR"); }
};
let _link_fr = match core.create_object::<Link>("link-factory", &props_fr) {
Ok(link) => link,
Err(e) => {
let _ = init_sender.send(Err(format!("Failed to create link FR: {}", e)));
return;
}
};
let _receiver = pw_receiver.attach(main_loop.loop_(), { let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone(); let _main_loop = main_loop.clone();
@@ -340,8 +412,15 @@ pub fn create_link(
"Link created: FL: {}-{} FR: {}-{}", "Link created: FL: {}-{} FR: {}-{}",
output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id
); );
if init_sender.send(Ok(())).is_err() {
return;
}
main_loop.run(); main_loop.run();
}); });
if let Err(e) = init_receiver.recv()? {
return Err(e.into());
}
Ok(pw_sender) Ok(pw_sender)
} }