Compare commits

...
66 Commits
Author SHA1 Message Date
Tarasov AleksandrandGitHub ec4e873071 refactor: better systemd service (#161)
* move `pwsp-daemon.service` from `pwsp-gui/assets` to pwsp-daemon/assets + autorestart service + better logging + wait for pipewire until start

* adapt all packages

* improve systemd service calling for .deb package
2026-07-07 21:29:26 +03:00
arabianq a91252d7fb change version to 1.12.4 2026-07-07 19:31:00 +03:00
arabianq 239170df45 cargo update 2026-07-07 19:30:15 +03:00
arabianq 5fef42a2e2 cargo fmt 2026-07-07 19:29:48 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
4f8f663f12 fix(pwsp-gui): fix arbitrary file overwrite via path traversal (#159)
* Fix path traversal vulnerability in audio download logic

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

* std::path::Path -> Path

* add random id to the filename if it's unknown

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 19:29:12 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
6ccc55a1d0 perf(pwsp-gui): optimize gui file filtering performance (#160)
Avoid unnecessary string cloning and allocation in `pwsp-gui/src/gui/mod.rs` when checking if a file matches the search query.

Specifically, we replaced `.to_string_lossy().to_string()` with `.to_string_lossy()` because `.to_lowercase()` directly works on `Cow<str>`, avoiding an intermediate String allocation.

Measured improvement was verified using an isolated benchmark demonstrating roughly 10-15% reduction in execution time.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 19:00:16 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
d266a3f073 fix(pwsp-lib): fixed unhandled panic on missing config directory (#157)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 18:58:43 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d2993592be chore(deps): bump actions/upload-artifact from 4 to 7 (#153)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 16:56:24 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1a53ab62dc chore(deps): bump peaceiris/actions-gh-pages from 3 to 4 (#154)
Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3 to 4.
- [Release notes](https://github.com/peaceiris/actions-gh-pages/releases)
- [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peaceiris/actions-gh-pages
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 16:06:50 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
51fcd94a61 chore(deps): bump actions/checkout from 4 to 7 (#155)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 15:59:53 +03:00
arabianq 7f6a122816 ci(flatpak): use /run/ccache for flatpak builds 2026-07-07 15:50:06 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7ca01ed8cb chore(deps): bump stefanzweifel/git-auto-commit-action from 5 to 7 (#156)
Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 5 to 7.
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v5...v7)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 15:41:31 +03:00
arabianq 07321afb60 docs(README): change pwsp-gui to pwsp 2026-07-05 22:41:49 +03:00
arabianq 8534e516ad docs(README): change arabianq-repo to pwsp for flatpak repo name 2026-07-05 22:41:15 +03:00
arabianq 16ce233f79 packages(flatapak): fix gpg key for flatpak repo 2026-07-05 22:36:20 +03:00
arabianq c2f4658871 packages(flatpak): add flatpakrepo file generation 2026-07-05 22:22:50 +03:00
arabianq 701bdebc88 packages(aur, rpm, deb): add openssl-devel as build dependency 2026-07-05 21:17:55 +03:00
arabianq d4ae78c7b0 ci(rpm): arabianq/pwsp-git -> arabianq/pwsp 2026-07-05 21:04:07 +03:00
arabianq 9e1d50e30c ci(rpm): add workflow for pwsp-git.rpm 2026-07-05 21:01:05 +03:00
arabianq 85b6c17bd1 deps(flatpak): update cargro-sources.json 2026-07-05 21:00:14 +03:00
arabianq cfe478b02d ci(rpm): replaced rustls with native-tls (OpenSSL) so it can build in copr 2026-07-05 20:56:18 +03:00
arabianq 0b23b3ed51 packages(deb): change package.metadata.deb.name to pwsp 2026-07-05 20:55:27 +03:00
arabianq 728d14006c change version to 1.12.3 2026-07-05 20:20:46 +03:00
arabianq f8bb84bbcf docs(readme): fix screenshot 2026-07-05 19:35:56 +03:00
Tarasov AleksandrandGitHub 0dbcc771f5 docs: new readme (#152) 2026-07-05 19:33:20 +03:00
arabianq b3a786a76c deps(flatpak): update cargo-sources.json 2026-07-05 19:11:08 +03:00
arabianq 972e303bff deps: cargo update 2026-07-05 19:07:46 +03:00
arabianq c406704ce5 fix(ci, flatter): fix building nightly repo 2026-07-05 18:56:32 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fd4a35e0e1 chore(deps): bump crazy-max/ghaction-import-gpg from 6 to 7 (#147)
Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 6 to 7.
- [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases)
- [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/v6...v7)

---
updated-dependencies:
- dependency-name: crazy-max/ghaction-import-gpg
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:44:26 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c793d09006 chore(deps): bump actions/setup-python from 5 to 6 (#148)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:44:13 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e4dd6d8b56 chore(deps): bump actions/cache from 4 to 6 (#149)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:43:58 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
457416779a chore(deps): bump softprops/action-gh-release from 2 to 3 (#150)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:43:42 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b0ac5490cc chore(deps): bump actions/download-artifact from 4 to 8 (#151)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:43:27 +03:00
arabianq a728671b01 fix(ci, flatter): fixed building nightly repo 2026-07-05 18:31:38 +03:00
arabianq 08153e4a1c fix(ci, flatter): fixed building nightly repo 2026-07-05 18:22:03 +03:00
arabianq c4699002fd fix(ci, deb): add secion and priority fields to the package.metadata.deb 2026-07-05 18:09:52 +03:00
arabianq dbcbf36df0 ci(deb): add nightly builds 2026-07-05 18:03:22 +03:00
arabianq 873ed9a6db fix github pages 2026-07-05 17:58:38 +03:00
Tarasov AleksandrandGitHub 601a5ce336 ci: refactor and optimize GitHub Actions workflows (#146)
* ci: refactor and optimize GitHub Actions workflows\n\n- **Consolidate build processes (`build.yml`, `release.yml`)**: Merged the standalone `.zip` (linux-build) and `.deb` (deb-build) jobs into a single matrix job (`build-and-package` / `build-release`).
  This prevents compiling the Rust binaries twice per architecture, effectively cutting the build time in half and saving CI runner minutes.
    - **Parallelize and fix Flatpak deployment (`flatter.yml`)**:
      - Converted `x86_64` and `aarch64` flatpak jobs to run concurrently via a matrix strategy, removing the sequential bottleneck.
      - Resolved an issue where GitHub Pages would only host the `aarch64` repository. Introduced a new `prepare-repo` job that downloads the `.flatpak` bundles from both architectures, merges them into a
  single OSTree repository locally, signs it with GPG, and deploys the unified multi-arch repository to GitHub Pages.
      - Configured `flatter.yml` to automatically attach the offline `.flatpak` bundles to GitHub Releases when triggered by a release event.
    - **Toolchain and cleanup**: Switched hardcoded Rust toolchain version (`1.96.0`) to `stable` across all workflows to prevent manual bumps in the future. Simplified bash scripts and removed redundant
  dependency installation steps.

* Extract Copr trigger into a separate job: Copr build now starts immediately in parallel with GitHub Actions builds, eliminating unnecessary waiting time.

* use ubuntu 22.04 to build binaries instead of ubuntu-latest

* fix cache poisoning

* another attemp to fix cache poisoning

* disable cache in setup-rust-toolchain

* use ubuntu-latest

* optimize dependencies in debug profile

* remove --release flag from build.yml and CARGO env vars

* fix flatter names collision

* better flatter key changing

* auto formatting

* add auto updating cargo-sources for dependabot pull requests

* add groups to dependabot.yml

* add github-actions to dependabot

* add ppa deb repo

* add concurrency: gh-pages-deploy
2026-07-05 17:49:38 +03:00
Tarasov AleksandrandGitHub 62a851714e Update funding link in FUNDING.yml 2026-07-02 14:04:03 +03:00
arabianq cc152de59f change version to 1.12.2 2026-07-02 11:11:50 +03:00
arabianq 501ff9bd64 deps(flatpak): update cargo-sources.json 2026-07-01 18:43:49 +03:00
arabianq f83e18b800 cargo update 2026-07-01 18:03:57 +03:00
arabianq ba920473c7 refactor: deprecated CentralPanel::show_inside -> show 2026-07-01 18:03:34 +03:00
arabianq dfb5985e4a fix: egui 0.35.0 changes 2026-07-01 18:03:00 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Tarasov Aleksandr
52dc0e3a09 chore(deps): bump egui_dnd from 0.15.0 to 0.16.0 (#145)
Bumps [egui_dnd](https://github.com/lucasmerlin/hello_egui) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/lucasmerlin/hello_egui/releases)
- [Changelog](https://github.com/lucasmerlin/hello_egui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lucasmerlin/hello_egui/compare/egui_dnd-v0.15.0...egui_dnd-v0.16.0)

---
updated-dependencies:
- dependency-name: egui_dnd
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com>
2026-07-01 17:57:57 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4ce16d5301 chore(deps): bump egui_material_icons from 0.6.0 to 0.7.0 (#142)
Bumps [egui_material_icons](https://github.com/lucasmerlin/hello_egui) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/lucasmerlin/hello_egui/releases)
- [Changelog](https://github.com/lucasmerlin/hello_egui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lucasmerlin/hello_egui/compare/egui_material_icons-v0.6.0...egui_material_icons-v0.7.0)

---
updated-dependencies:
- dependency-name: egui_material_icons
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 17:55:46 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
48cda995ee chore(deps): bump egui_extras from 0.34.3 to 0.35.0 (#144)
Bumps [egui_extras](https://github.com/emilk/egui) from 0.34.3 to 0.35.0.
- [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.3...0.35.0)

---
updated-dependencies:
- dependency-name: egui_extras
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 17:48:00 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7950f75c07 chore(deps): bump eframe from 0.34.3 to 0.35.0 (#143)
Bumps [eframe](https://github.com/emilk/egui) from 0.34.3 to 0.35.0.
- [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.3...0.35.0)

---
updated-dependencies:
- dependency-name: eframe
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 17:43:56 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
38ad8d5bc2 chore(deps): bump egui from 0.34.3 to 0.35.0 (#141)
Bumps [egui](https://github.com/emilk/egui) from 0.34.3 to 0.35.0.
- [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.3...0.35.0)

---
updated-dependencies:
- dependency-name: egui
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 17:41:24 +03:00
arabianq 347dee713c deps: update cargo-sources.json [no ci] 2026-06-26 00:03:42 +03:00
arabianq e6e6293e20 change version to 1.12.1 [no ci] 2026-06-25 23:57:52 +03:00
arabianq a1e53aa536 deps: update cargo-sources.json 2026-06-25 23:43:08 +03:00
arabianq bff50b99f1 deps: update rodio 2026-06-25 23:35:09 +03:00
arabianq 09b86dc108 cargo update 2026-06-25 23:32:58 +03:00
arabianq 30070c439b add more options to FUNDING.yml 2026-06-25 23:30:28 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>arabianq
44c3a5e3a7 chore(deps): bump itertools from 0.14.0 to 0.15.0 (#140)
* chore(deps): bump itertools from 0.14.0 to 0.15.0

Bumps [itertools](https://github.com/rust-itertools/itertools) from 0.14.0 to 0.15.0.
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: itertools
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* deps: update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: arabian <a.tevg@ya.ru>
2026-06-25 23:23:37 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
9a0bc92222 refactor(gui): pre-filter directory contents by supported extension (#136)
*  [performance] pre-filter directory contents by supported extension

💡 **What:** Moved the check for supported audio file extensions from the GUI rendering loop into the directory read/caching layer.
🎯 **Why:** The file extension string parsing and check was executing on every frame of the render loop for every file listed, causing unnecessary CPU overhead. By caching the pre-filtered items, we only execute the check once per directory load.
📊 **Measured Improvement:** In a micro-benchmark simulating 10k files (with 50% matching extensions), the unoptimized loop took ~14.2ms to execute, while the optimized loop takes ~5.8ms. This yields a ~59% speed improvement in the iteration logic over the baseline.

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

*  [performance] pre-filter directory contents by supported extension

💡 **What:** Moved the check for supported audio file extensions from the GUI rendering loop into the directory read/caching layer.
🎯 **Why:** The file extension string parsing and check was executing on every frame of the render loop for every file listed, causing unnecessary CPU overhead. By caching the pre-filtered items, we only execute the check once per directory load.
📊 **Measured Improvement:** In a micro-benchmark simulating 10k files (with 50% matching extensions), the unoptimized loop took ~14.2ms to execute, while the optimized loop takes ~5.8ms. This yields a ~59% speed improvement in the iteration logic over the baseline.

Flatpak cargo-sources.json regenerated to include criterion dependency to fix CI.

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

* chore: remove criterion bench dependency from gui app

This removes the `bench.rs` and `criterion` dependencies, which failed to compile in offline flatpak builds.

CI build is now fixed.

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

* cargo fmt

* deps: update cargo-sources.json

* chore: update cargo lock and flatpak sources

After removing criterion, the cargo lockfile and flatpak sources have been refreshed to properly remove the offline build dependencies issue.

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

* Revert "chore: update cargo lock and flatpak sources"

This reverts commit 27379d8e78.

[no ci]

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-25 23:23:14 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
3f85994f83 refactor(daemon): Consolidate PipeWire operations into a single background actor
Replaces the previous approach of spawning new PipeWire main loops and
awaiting a 100ms timeout for every `get_all_devices` call. Instead,
this introduces a `PipeWireManager` which maintains a single, persistent
background thread with a PipeWire main loop and registry listener. The
actor caches `AudioDevice`s and `Port`s continuously in a `HashMap`,
providing instantaneous responses to device queries via a channel.

This architectural change replaces `Sender<Terminate>` with a simple
`PwTerminator` drop-guard to manage the lifetimes of PipeWire objects
(virtual mic and links) via `PwCommand::DestroyObject` without creating
additional OS threads or PipeWire contexts.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-25 22:58:05 +03:00
arabianq caf8ac19a1 ci: add --no-wait flag to copr in release.yml 2026-06-19 15:31:05 +03:00
arabianq f563680ceb ci: add rust-cache 2026-06-19 15:29:03 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>arabianq
78ca2ddf68 chore(deps): bump opener from 0.8.4 to 0.8.5 (#139)
* chore(deps): bump opener from 0.8.4 to 0.8.5

Bumps [opener](https://github.com/Seeker14491/opener) from 0.8.4 to 0.8.5.
- [Changelog](https://github.com/Seeker14491/opener/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Seeker14491/opener/compare/v0.8.4...v0.8.5)

---
updated-dependencies:
- dependency-name: opener
  dependency-version: 0.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* deps(flatpak): update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: arabian <a.tevg@ya.ru>
2026-06-19 02:47:19 +03:00
arabianq 838fc1ce29 fix: increment pkgrel to 2 for pwsp-bin aur package 2026-06-05 23:52:31 +03:00
arabianq 622cf39fa2 fix: correct asset paths in pwsp-bin aur package 2026-06-05 23:52:06 +03:00
arabianq 809b1a8490 fix: update Copr build command to use correct repository name 2026-06-04 20:51:36 +03:00
arabianq d1a5275173 fix: remove redundant name parameter from Copr build command 2026-06-04 20:42:11 +03:00
33 changed files with 2034 additions and 2689 deletions
+1 -1
View File
@@ -1 +1 @@
custom: ['https://boosty.to/arabian']
custom: ['https://dalink.to/arabianq', 'https://boosty.to/arabian', 'https://sponsr.ru/notultranx/']
+23
View File
@@ -1,6 +1,29 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
groups:
egui-stack:
patterns:
- "egui*"
- "eframe"
serde-stack:
patterns:
- "serde*"
tokio-stack:
patterns:
- "tokio*"
- "reqwest"
clap-stack:
patterns:
- "clap*"
+129 -70
View File
@@ -12,7 +12,7 @@ on:
workflow_dispatch:
jobs:
linux-build:
build-and-package:
strategy:
matrix:
include:
@@ -33,29 +33,30 @@ jobs:
libclang-dev \
libasound2-dev \
libdbus-1-dev \
libssl-dev \
pkg-config
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.96.0
toolchain: stable
cache: false
- name: Rust Cache
uses: swatinem/rust-cache@v2
with:
prefix-key: "v3-${{ matrix.runner }}"
- name: Run tests
run: cargo test --locked
- name: Build all binaries (debug-speed compilation into target/release)
env:
CARGO_PROFILE_RELEASE_OPT_LEVEL: 0
CARGO_PROFILE_RELEASE_DEBUG: "true"
CARGO_PROFILE_RELEASE_STRIP: "false"
CARGO_PROFILE_RELEASE_LTO: "false"
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 256
run: cargo build --release --locked
- name: Build all binaries
run: cargo build --locked
- name: Extract all binary names
id: cargo-meta
@@ -67,7 +68,7 @@ jobs:
echo "$BIN_NAMES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Package all binaries into one archive
- name: Package ZIP archive
shell: bash
run: |
set -euo pipefail
@@ -78,7 +79,7 @@ jobs:
FILES=()
while IFS= read -r BIN; do
[ -z "$BIN" ] && continue
FILES+=("target/release/$BIN")
FILES+=("target/debug/$BIN")
done <<< "${{ steps.cargo-meta.outputs.bin_names }}"
if [ "${#FILES[@]}" -eq 0 ]; then
@@ -86,67 +87,18 @@ jobs:
exit 1
fi
for f in "${FILES[@]}"; do
if [ ! -f "$f" ]; then
echo "Error: expected binary not found: $f" >&2
exit 1
fi
echo "Will add: $f"
done
zip -j "$ARCHIVE_NAME" "${FILES[@]}"
- name: Upload archive as artifact
uses: actions/upload-artifact@v4
- name: Upload ZIP artifact
uses: actions/upload-artifact@v7
with:
name: archive-${{ matrix.arch }}
path: pwsp-*.zip
retention-days: 7
deb-build:
strategy:
matrix:
include:
- arch: x64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
steps:
- name: Install apt deps (dev-libs)
run: |
sudo apt-get update
sudo apt-get install -y \
libpipewire-0.3-dev \
libclang-dev \
libasound2-dev \
libdbus-1-dev \
pkg-config
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.96.0
- name: Build all binaries (debug-speed compilation into target/release)
env:
CARGO_PROFILE_RELEASE_OPT_LEVEL: 0
CARGO_PROFILE_RELEASE_DEBUG: "true"
CARGO_PROFILE_RELEASE_STRIP: "false"
CARGO_PROFILE_RELEASE_LTO: "false"
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 256
run: cargo build --release --locked
- name: Cache cargo-deb
id: cache-cargo-deb
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.cargo/bin/cargo-deb
key: ${{ runner.os }}-${{ runner.arch }}-cargo-deb-v1
@@ -155,20 +107,108 @@ jobs:
if: steps.cache-cargo-deb.outputs.cache-hit != 'true'
run: cargo install --locked cargo-deb
- name: Create .deb package (debug binaries from target/release)
- name: Create .deb package
shell: bash
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
cargo-deb -p pwsp-gui --no-build --no-strip
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
BASE_VERSION=$(grep '^version =' Cargo.toml | head -1 | cut -d'"' -f2)
GIT_REV=$(git rev-parse --short HEAD)
DATE=$(date +%Y%m%d)
DEB_VER="${BASE_VERSION}+git${DATE}.${GIT_REV}"
echo "Building nightly deb version: $DEB_VER"
cargo-deb -p pwsp-gui --no-build --no-strip --profile dev --deb-version "$DEB_VER"
else
cargo-deb -p pwsp-gui --no-build --no-strip --profile dev
fi
- name: Upload .deb(s) as artifacts
uses: actions/upload-artifact@v4
- name: Upload .deb artifact
uses: actions/upload-artifact@v7
with:
name: deb-packages-${{ matrix.arch }}
path: target/debian/*.deb
retention-days: 7
deploy-nightly-apt:
needs: build-and-package
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
concurrency: gh-pages-deploy
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup GPG
id: gpg
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Download DEB artifacts
uses: actions/download-artifact@v8
with:
pattern: deb-packages-*
path: debs
merge-multiple: true
- name: Checkout existing gh-pages
uses: actions/checkout@v7
with:
ref: gh-pages
path: gh-pages-branch
continue-on-error: true
- name: Generate Nightly APT Repository
run: |
sudo apt-get update
sudo apt-get install -y reprepro
mkdir -p apt-repo
if [ -d "gh-pages-branch/apt" ]; then
cp -r gh-pages-branch/apt/* apt-repo/ || true
fi
mkdir -p apt-repo/conf
cat <<EOF > apt-repo/conf/distributions
Origin: arabianq
Label: PipeWire Soundpad
Codename: stable
Architectures: amd64 arm64
Components: main
Description: APT Repository for PWSP
SignWith: ${{ steps.gpg.outputs.fingerprint }}
Origin: arabianq
Label: PipeWire Soundpad
Codename: nightly
Architectures: amd64 arm64
Components: main
Description: Nightly APT Repository for PWSP
SignWith: ${{ steps.gpg.outputs.fingerprint }}
EOF
gpg --armor --export ${{ steps.gpg.outputs.fingerprint }} > apt-repo/pubkey.gpg
cd apt-repo
for deb in ../debs/*.deb; do
if [ -f "$deb" ]; then
echo "Adding $deb to nightly APT repository..."
reprepro includedeb nightly "$deb"
fi
done
- name: Deploy APT Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./apt-repo
destination_dir: apt
keep_files: true
flatpak-build:
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
strategy:
@@ -185,7 +225,7 @@ jobs:
options: --privileged
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
@@ -196,3 +236,22 @@ jobs:
branch: master
build-bundle: true
arch: ${{ matrix.arch }}
trigger-copr-nightly:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Install copr-cli
run: pip install copr-cli
- name: Trigger Copr Build
env:
COPR_CONFIG: ${{ secrets.COPR_CONFIG }}
run: |
mkdir -p ~/.config
echo "$COPR_CONFIG" > ~/.config/copr
copr-cli buildscm --nowait \
--clone-url https://github.com/arabianq/pipewire-soundpad.git \
--commit ${{ github.sha }} \
--spec packages/rpm/pwsp-git.spec \
arabianq/pwsp
+36
View File
@@ -0,0 +1,36 @@
name: Dependabot Flatpak Cargo Sources
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "Cargo.lock"
jobs:
update-cargo-sources:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: write # Needed to push back to the branch
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ github.head_ref }}
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Install dependencies
run: pip install aiohttp toml
- name: Generate cargo-sources.json
run: bash ./scripts/generate-sources.sh
- name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore(flatpak): automatically update cargo-sources.json"
file_pattern: "packages/flatpak/cargo-sources.json"
+99 -56
View File
@@ -21,9 +21,17 @@ on:
default: "stable"
jobs:
flatter-x64:
name: Flatter (x86_64)
runs-on: ubuntu-latest
build-flatpak:
name: Build Flatpak (${{ matrix.arch }})
strategy:
matrix:
include:
- arch: x86_64
runner: ubuntu-latest
- arch: aarch64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
permissions:
contents: read
container:
@@ -32,13 +40,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
ref: ${{ inputs.tag_name || github.ref }}
- name: Setup GPG
id: gpg
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -56,88 +64,123 @@ jobs:
- name: Modify Manifest
run: |
sed -i '/^branch:/d' packages/flatpak/ru.arabianq.pwsp.yaml
sed -i '/^default-branch:/d' packages/flatpak/ru.arabianq.pwsp.yaml
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
- name: Install SDK Extensions
run:
run: |
flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
org.freedesktop.Sdk.Extension.llvm20//25.08
flatpak install -y flathub org.freedesktop.Sdk.Extension.llvm20//25.08
- name: Build Flatpak
uses: andyholmes/flatter@main
with:
files: packages/flatpak/ru.arabianq.pwsp.yaml
gpg-sign: ${{ steps.gpg.outputs.fingerprint }}
upload-bundles: false
upload-bundles: true
upload-pages-artifact: false
arch: x86_64
arch: ${{ matrix.arch }}
cache: true
flatter-arm64:
name: Flatter (aarch64)
needs: flatter-x64
runs-on: ubuntu-24.04-arm
permissions:
contents: read
prepare-repo:
name: Prepare OSTree Repo
needs: build-flatpak
runs-on: ubuntu-latest
concurrency: gh-pages-deploy
container:
image: ghcr.io/andyholmes/flatter/freedesktop:25.08
options: --privileged
permissions:
contents: write # Needed to upload files to GitHub Release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download Flatpak Bundles
uses: actions/download-artifact@v8
with:
ref: ${{ inputs.tag_name || github.ref }}
path: bundles
pattern: ru.arabianq.pwsp-*
- name: Checkout existing gh-pages
uses: actions/checkout@v7
with:
ref: gh-pages
path: gh-pages-branch
continue-on-error: true
- name: Setup GPG
id: gpg
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Set Default Branch
id: set_branch
- name: Update OSTree Repo and Prepare Release Bundles
run: |
if [ "${{ github.event_name }}" == "release" ]; then
echo "branch=stable" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "branch=${{ inputs.build_branch }}" >> $GITHUB_OUTPUT
mkdir -p release-bundles
# Inherit existing OSTree repository to preserve previous branches (e.g. stable vs nightly)
# Only inherit if objects/ exists, otherwise it's a corrupted/empty repo due to Git ignoring empty folders
if [ -d "gh-pages-branch/objects" ]; then
echo "Found existing OSTree repository. Copying..."
cp -r gh-pages-branch pages-repo
# Clean up the .git folder so it doesn't interfere
rm -rf pages-repo/.git
# Recreate empty OSTree directories that Git ignored when pushing previously
mkdir -p pages-repo/refs/heads pages-repo/refs/mirrors pages-repo/refs/remotes \
pages-repo/objects pages-repo/state pages-repo/extensions pages-repo/tmp
else
echo "branch=nightly" >> $GITHUB_OUTPUT
echo "Initializing new OSTree repository..."
mkdir -p pages-repo
ostree init --mode=archive-z2 --repo=pages-repo
fi
- name: Modify Manifest
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
shopt -s nullglob
for dir in bundles/ru.arabianq.pwsp-*; do
arch=$(basename "$dir" | sed 's/ru.arabianq.pwsp-//')
- name: Install SDK Extensions
run:
flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
org.freedesktop.Sdk.Extension.llvm20//25.08
# Find the flatpak file inside the dir
bundle=$(find "$dir" -name "*.flatpak" | head -n 1)
- name: Build Flatpak
uses: andyholmes/flatter@main
if [ -n "$bundle" ]; then
echo "Importing $bundle for architecture $arch..."
flatpak build-import-bundle pages-repo "$bundle"
# Copy with a unique name for GitHub Release
cp "$bundle" "release-bundles/ru.arabianq.pwsp-${arch}.flatpak"
fi
done
# Update summary and sign the repository
flatpak build-update-repo --generate-static-deltas --gpg-sign=${{ steps.gpg.outputs.fingerprint }} pages-repo
# Export GPG public key for manual import
gpg --export ${{ steps.gpg.outputs.fingerprint }} > pages-repo/pwsp.gpg
# Generate a .flatpakrepo file for easy installation
GPG_BASE64=$(gpg --export ${{ steps.gpg.outputs.fingerprint }} | base64 -w 0)
cat <<EOF | sed 's/^ *//' > pages-repo/pwsp.flatpakrepo
[Flatpak Repo]
Title=PipeWire Soundpad
Url=https://arabianq.github.io/pipewire-soundpad/
Homepage=https://github.com/arabianq/pipewire-soundpad
Comment=PipeWire Soundpad Flatpak Repository
Description=Official repository for PWSP
GPGKey=${GPG_BASE64}
EOF
- name: Deploy Flatpak Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
files: packages/flatpak/ru.arabianq.pwsp.yaml
gpg-sign: ${{ steps.gpg.outputs.fingerprint }}
upload-bundles: false
upload-pages-artifact: true
arch: aarch64
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./pages-repo
destination_dir: .
keep_files: true
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: flatter-arm64
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: Upload bundles to GitHub Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v3
with:
files: release-bundles/*.flatpak
tag_name: ${{ github.event.release.tag_name }}
+122 -112
View File
@@ -23,46 +23,32 @@ jobs:
id: tag
run: |
set -euo pipefail
INPUT_TAG="${{ github.event.inputs.tag || '' }}"
if [ -n "$INPUT_TAG" ]; then
echo "Using input tag: $INPUT_TAG"
echo "tag=$INPUT_TAG" >> $GITHUB_OUTPUT
exit 0
fi
EVENT_TAG="${{ github.event.release.tag_name || '' }}"
if [ -n "$EVENT_TAG" ]; then
echo "Using event tag: $EVENT_TAG"
if [ -n "$INPUT_TAG" ]; then
echo "tag=$INPUT_TAG" >> $GITHUB_OUTPUT
elif [ -n "$EVENT_TAG" ]; then
echo "tag=$EVENT_TAG" >> $GITHUB_OUTPUT
exit 0
fi
if [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then
echo "Using GITHUB_REF tag: ${BASH_REMATCH[1]}"
elif [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then
echo "tag=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
exit 0
fi
echo "No tag in input/event/GITHUB_REF — querying latest release via API..."
else
LATEST_JSON=$(curl -sSf -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/${{ github.repository }}/releases/latest" || true)
TAG_NAME=$(echo "$LATEST_JSON" | jq -r '.tag_name // empty')
if [ -n "$TAG_NAME" ]; then
echo "Found latest release tag: $TAG_NAME"
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
exit 0
fi
echo "No tag found"
else
echo "tag=" >> $GITHUB_OUTPUT
fi
fi
- name: Fail if no tag determined
if: ${{ steps.tag.outputs.tag == '' }}
run: |
echo "ERROR: No tag determined. Provide a tag when running manually or ensure a release exists."
echo "ERROR: No tag determined."
exit 1
linux-release:
build-release:
needs: prepare
strategy:
matrix:
@@ -75,7 +61,7 @@ jobs:
runs-on: ${{ matrix.runner }}
steps:
- name: Install apt deps (jq/zip + dev-libs)
- name: Install apt deps
run: |
sudo apt-get update
sudo apt-get install -y \
@@ -84,10 +70,11 @@ jobs:
libclang-dev \
libasound2-dev \
libdbus-1-dev \
libssl-dev \
pkg-config
- name: Checkout code at tag
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0
@@ -95,7 +82,13 @@ jobs:
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.96.0
toolchain: stable
cache: false
- name: Rust Cache
uses: swatinem/rust-cache@v2
with:
prefix-key: "v3-${{ matrix.runner }}"
- name: Extract all binary names
id: cargo-meta
@@ -110,13 +103,12 @@ jobs:
- name: Build all release binaries
run: cargo build --release --locked
- name: Package all binaries into one archive
- name: Package ZIP archive
shell: bash
run: |
set -euo pipefail
TAG="${{ needs.prepare.outputs.tag }}"
ARCHIVE_NAME="pwsp-${TAG}-linux-${{ matrix.arch }}.zip"
echo "Creating archive: $ARCHIVE_NAME"
FILES=()
while IFS= read -r BIN; do
@@ -124,68 +116,18 @@ jobs:
FILES+=("target/release/$BIN")
done <<< "${{ steps.cargo-meta.outputs.bin_names }}"
if [ "${#FILES[@]}" -eq 0 ]; then
echo "Error: no binaries were discovered via cargo metadata." >&2
exit 1
fi
for f in "${FILES[@]}"; do
if [ ! -f "$f" ]; then
echo "Error: expected binary not found: $f" >&2
exit 1
fi
echo "Will add: $f"
done
zip -j "$ARCHIVE_NAME" "${FILES[@]}"
- name: Upload zip archive
uses: actions/upload-artifact@v4
- name: Upload ZIP artifact
uses: actions/upload-artifact@v7
with:
name: zip-archive-${{ matrix.arch }}
path: pwsp-*.zip
retention-days: 1
deb-release:
needs: prepare
strategy:
matrix:
include:
- arch: x64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
steps:
- name: Install apt deps (dev-libs)
run: |
sudo apt-get update
sudo apt-get install -y \
libpipewire-0.3-dev \
libclang-dev \
libasound2-dev \
libdbus-1-dev \
pkg-config
- name: Checkout code at tag
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.96.0
- name: Build all release binaries
run: cargo build --release --locked
- name: Cache cargo-deb
id: cache-cargo-deb
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.cargo/bin/cargo-deb
key: ${{ runner.os }}-${{ runner.arch }}-cargo-deb-v1
@@ -194,50 +136,32 @@ jobs:
if: steps.cache-cargo-deb.outputs.cache-hit != 'true'
run: cargo install --locked cargo-deb
- name: Create .deb package (release binaries)
- name: Create .deb package
shell: bash
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
cargo-deb -p pwsp-gui --no-build
- name: Upload deb package
uses: actions/upload-artifact@v4
- name: Upload DEB artifact
uses: actions/upload-artifact@v7
with:
name: deb-package-${{ matrix.arch }}
path: target/debian/*.deb
retention-days: 1
publish-release:
needs: [prepare, linux-release, deb-release]
needs: [prepare, build-release]
runs-on: ubuntu-latest
steps:
- name: Download zip archive (x64)
uses: actions/download-artifact@v4
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
name: zip-archive-x64
path: ./dist
- name: Download zip archive (arm64)
uses: actions/download-artifact@v4
with:
name: zip-archive-arm64
path: ./dist
- name: Download deb package (x64)
uses: actions/download-artifact@v4
with:
name: deb-package-x64
path: ./dist
- name: Download deb package (arm64)
uses: actions/download-artifact@v4
with:
name: deb-package-arm64
path: ./dist
merge-multiple: true
- name: Upload artifacts to Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ needs.prepare.outputs.tag }}
@@ -245,6 +169,10 @@ jobs:
./dist/pwsp-*.zip
./dist/*.deb
trigger-copr:
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Install copr-cli
run: pip install copr-cli
@@ -254,9 +182,91 @@ jobs:
run: |
mkdir -p ~/.config
echo "$COPR_CONFIG" > ~/.config/copr
copr-cli buildscm --clone-url https://github.com/arabianq/pipewire-soundpad.git \
copr-cli buildscm --nowait \
--clone-url https://github.com/arabianq/pipewire-soundpad.git \
--commit ${{ needs.prepare.outputs.tag }} \
--spec packages/rpm/pwsp.spec \
--name pwsp \
arabianq/pipewire-soundpad
arabianq/pwsp
build-apt-repo:
needs: [prepare, build-release]
runs-on: ubuntu-latest
concurrency: gh-pages-deploy
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup GPG
id: gpg
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Download DEB artifacts
uses: actions/download-artifact@v8
with:
pattern: deb-package-*
path: debs
merge-multiple: true
- name: Checkout existing gh-pages
uses: actions/checkout@v7
with:
ref: gh-pages
path: gh-pages-branch
continue-on-error: true
- name: Generate APT Repository
run: |
sudo apt-get update
sudo apt-get install -y reprepro
mkdir -p apt-repo
# Restore existing repository first so we don't lose old packages
if [ -d "gh-pages-branch/apt" ]; then
cp -r gh-pages-branch/apt/* apt-repo/ || true
fi
# Always rewrite the config and public key to ensure they are up-to-date
mkdir -p apt-repo/conf
cat <<EOF > apt-repo/conf/distributions
Origin: arabianq
Label: PipeWire Soundpad
Codename: stable
Architectures: amd64 arm64
Components: main
Description: APT Repository for PWSP
SignWith: ${{ steps.gpg.outputs.fingerprint }}
Origin: arabianq
Label: PipeWire Soundpad
Codename: nightly
Architectures: amd64 arm64
Components: main
Description: Nightly APT Repository for PWSP
SignWith: ${{ steps.gpg.outputs.fingerprint }}
EOF
gpg --armor --export ${{ steps.gpg.outputs.fingerprint }} > apt-repo/pubkey.gpg
cd apt-repo
for deb in ../debs/*.deb; do
if [ -f "$deb" ]; then
echo "Adding $deb to APT repository..."
reprepro includedeb stable "$deb"
fi
done
- name: Deploy APT Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./apt-repo
destination_dir: apt
keep_files: true
Generated
+365 -876
View File
File diff suppressed because it is too large Load Diff
+15 -10
View File
@@ -8,7 +8,7 @@ members = [
resolver = "2"
[workspace.package]
version = "1.12.0"
version = "1.12.4"
edition = "2024"
authors = ["arabian"]
homepage = "https://pwsp.arabianq.ru"
@@ -39,7 +39,7 @@ clap = { version = "4.6.1", default-features = false, features = [
] }
dirs = "6.0.0"
itertools = "0.14.0"
itertools = "0.15.0"
evdev = { version = "0.13.2", features = ["tokio"] }
rfd = { version = "0.17.2", default-features = false, features = [
"xdg-portal",
@@ -52,34 +52,39 @@ rustix = { version = "1.1.4", features = ["process"] }
rust-i18n = "4.0.0"
sys-locale = "0.3.2"
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "c6a81b5a46e00a6a682c0c431dff62e86f57d819", default-features = false, features = [
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "4746803c2e5f67004264ac946708d47a9faf510c", default-features = false, features = [
"symphonia-all",
"symphonia-libopus",
"playback",
] }
pipewire = "0.10.0"
egui = { version = "0.34.2", default-features = false, features = [
egui = { version = "0.35.0", default-features = false, features = [
"default_fonts",
"rayon",
] }
eframe = { version = "0.34.2", default-features = false, features = [
eframe = { version = "0.35.0", default-features = false, features = [
"default_fonts",
"glow",
"x11",
"wayland",
] }
egui_extras = "0.34.1"
egui_material_icons = "0.6.0"
egui_dnd = "0.15.0"
egui_extras = "0.35.0"
egui_material_icons = "0.7.0"
egui_dnd = "0.16.0"
reqwest = "0.13.4"
reqwest = { version = "0.13.4", default-features = false, features = ["native-tls"] }
percent-encoding = "2.3.2"
[profile.dev]
[profile.dev.package."*"]
opt-level = 3
debug = false
[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"
panic = "abort"
+103 -72
View File
@@ -1,118 +1,149 @@
<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="pwsp-gui/assets/screenshot.png" alt="PWSP Screenshot" width="700"/>
</div>
# PipeWire Soundpad (PWSP) 🎵
## 🌟 Overview
**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.
![PWSP Screenshot](pwsp-gui/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/).
[🇷🇺 Читать на русском](README.ru.md) | [🇺🇸 Read in English](README.md)
## ⚙️ 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.
[![GitHub Actions Build Status](https://img.shields.io/github/actions/workflow/status/arabianq/pipewire-soundpad/build.yml?branch=main&style=flat-square)](https://github.com/arabianq/pipewire-soundpad/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
[![GitHub Release](https://img.shields.io/github/v/release/arabianq/pipewire-soundpad?style=flat-square)](https://github.com/arabianq/pipewire-soundpad/releases/latest)
[![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20PipeWire-blue?style=flat-square)](https://pipewire.org/)
**PipeWire Soundpad (PWSP)** is a modern, low-latency application that lets you play audio files directly through your microphone. Designed specifically for Linux, it leverages the power of PipeWire to achieve native integration without the need for Pulseaudio bridges or complex virtual sinks.
---
## ✨ Features
- **Native PipeWire Integration:** Direct communication with the PipeWire API for the lowest possible latency.
- **Modular Architecture:** Consists of a background `daemon`, a command-line interface (`cli`), and a graphical user interface (`gui`).
- **Modern GUI:** Built with `egui`, supporting both Wayland and X11 seamlessly.
- **Global Hotkeys:** Powered by `evdev`, allowing you to play sounds from anywhere.
- **Broad Audio Support:** Powered by `rodio` and `symphonia` to support a wide range of audio formats.
---
## 🚀 Installation
We provide multiple ways to install PWSP, including stable releases and rolling "nightly" builds directly from the `main` branch.
### 📦 Flatpak (Recommended)
Install PWSP via Flatpak from our custom repository:
Add our official OSTree repository and install the application:
```bash
flatpak remote-add --user --if-not-exists pwsp-repo https://arabianq.github.io/pipewire-soundpad/index.flatpakrepo
# Add the repository
flatpak remote-add --if-not-exists pwsp https://arabianq.github.io/pipewire-soundpad/pwsp.flatpakrepo
# Install stable version
flatpak install --user arabianq-repo ru.arabianq.pwsp//stable
# Install the Stable version
flatpak install pwsp ru.arabianq.pwsp//stable
# Or install the nightly version (latest commit)
flatpak install --user arabianq-repo ru.arabianq.pwsp//nightly
# OR Install the Nightly version (rolling updates)
flatpak install pwsp ru.arabianq.pwsp//nightly
```
### 🐧 Linux Packages
**Fedora (and derivatives):**
### 🟠 Debian / Ubuntu (APT Repository)
We maintain an official APT repository for seamless updates via `apt`:
```bash
# 1. Download the public GPG key
wget -O- https://arabianq.github.io/pipewire-soundpad/apt/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/pwsp.gpg
# 2. Add the repository (Choose STABLE or NIGHTLY)
# For Stable:
echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipewire-soundpad/apt/ stable main" | sudo tee /etc/apt/sources.list.d/pwsp.list
# For Nightly:
# echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipewire-soundpad/apt/ nightly main" | sudo tee /etc/apt/sources.list.d/pwsp.list
# 3. Update and install
sudo apt update
sudo apt install pwsp
```
### 🐧 Fedora / RHEL (COPR)
Available via the Fedora COPR repository:
```bash
sudo dnf copr enable arabianq/pwsp
sudo dnf install pwsp
```
**Arch Linux (AUR):**
### ⚙️ Manual / Standalone
You can manually download `.deb` packages or standalone `.zip` binaries from the [Releases page](https://github.com/arabianq/pipewire-soundpad/releases).
### 🦀 Build from Source
Make sure you have Rust, Cargo, and the required dependencies (`libpipewire-0.3-dev`, `libclang-dev`, `libasound2-dev`, `libdbus-1-dev`) installed.
```bash
paru -S pwsp-bin # or 'pwsp' to build from source
```
**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
cargo install pwsp
# OR clone and build manually:
git clone https://github.com/arabianq/pipewire-soundpad.git
cd pipewire-soundpad
cargo build --release
cargo build --release --locked
```
*(Note: Requires Rust toolchain and PipeWire running on your system).*
The binaries will be located in `target/release/`.
---
## 🎮 Usage
### 1. Start the Daemon
Before using the GUI or CLI, the daemon must be running in the background.
PWSP operates via a background daemon that handles the audio routing.
```bash
# Recommended: Start and enable via systemd (starts on login)
systemctl --user enable --now pwsp-daemon
# Manual start (if not using systemd):
pwsp-daemon &
# Run the daemon
pwsp-daemon
```
### 2. Using the GUI
1. **Add Sounds:** Click the **"+"** button to add a directory containing your audio files.
2. **Select Mic:** Choose your physical microphone from the dropdown. PWSP will instantly create a virtual microphone combining your voice and the soundboard.
3. **Play:** Click any sound to play it, adjust its volume, or assign a hotkey for quick access.
_(Tip: If installed via package managers, a systemd user service is provided. You can enable it with `systemctl --user enable --now pwsp-daemon.service`)_
### 2. Launch the GUI
Simply run the graphical interface to manage and play your sounds:
### 3. Using the CLI
Control the daemon directly from your terminal:
```bash
pwsp-cli action play /path/to/sound.mp3
pwsp-cli get volume
pwsp-cli set position 20
pwsp-cli --help # View all commands
pwsp-gui
```
### 3. Use the CLI
You can also interact with the daemon directly via the command line:
```bash
pwsp-cli play /path/to/sound.mp3
pwsp-cli stop
pwsp-cli status
```
---
## ⌨️ Shortcuts & Controls
## 📚 Documentation & DeepWiki
| Action | Keyboard | Mouse |
| :----------------------------------- | :--------------------- | :------------------- |
| **Play Track** (Stops others) | | `Left Click` |
| **Add Track** (Plays simultaneously) | | `Ctrl + Left Click` |
| **Replace Last Track** | | `Shift + Left Click` |
| **Pause / Resume** | `Space` | |
| **Stop All Tracks** | `Backspace` | |
| **Open / Close Settings** | `I` | |
| **Search** | `/` | |
For advanced configuration, troubleshooting, architecture details, and custom setups, please visit our official Wiki:
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/arabianq/pipewire-soundpad)
---
## 🤝 Contributing
Contributions, issues, and feature requests are welcome! Feel free to check out the [issues page](https://github.com/arabianq/pipewire-soundpad/issues).
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/arabianq/pipewire-soundpad)
Contributions, issues, and feature requests are welcome!
## 📜 License
This project is licensed under the [MIT License](LICENSE).
1. Fork the project.
2. Create your feature branch (`git checkout -b feat/amazing-feature`).
3. Commit your changes (`git commit -m 'Add some amazing feature'`).
4. Push to the branch (`git push origin feat/amazing-feature`).
5. Open a Pull Request.
---
## 📝 License
Distributed under the MIT License. See `LICENSE` for more information.
_Built with ❤️ for the Linux community._
+149
View File
@@ -0,0 +1,149 @@
# PipeWire Soundpad (PWSP) 🎵
![PWSP Screenshot](pwsp-gui/assets/screenshot.png)
[🇷🇺 Читать на русском](README.ru.md) | [🇺🇸 Read in English](README.md)
[![GitHub Actions Build Status](https://img.shields.io/github/actions/workflow/status/arabianq/pipewire-soundpad/build.yml?branch=main&style=flat-square)](https://github.com/arabianq/pipewire-soundpad/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
[![GitHub Release](https://img.shields.io/github/v/release/arabianq/pipewire-soundpad?style=flat-square)](https://github.com/arabianq/pipewire-soundpad/releases/latest)
[![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20PipeWire-blue?style=flat-square)](https://pipewire.org/)
**PipeWire Soundpad (PWSP)** — это современное приложение с низкой задержкой, которое позволяет воспроизводить аудиофайлы прямо в ваш микрофон. Разработано специально для Linux с использованием мощи PipeWire для достижения нативной интеграции без использования мостов Pulseaudio или сложных виртуальных устройств.
---
## ✨ Возможности
- **Нативная интеграция с PipeWire:** Прямое взаимодействие с PipeWire API для минимальной задержки.
- **Модульная архитектура:** Состоит из фонового демона (`daemon`), интерфейса командной строки (`cli`) и графического интерфейса (`gui`).
- **Современный GUI:** Построен на `egui`, плавно работает как на Wayland, так и на X11.
- **Глобальные горячие клавиши:** Работают через `evdev`, позволяя воспроизводить звуки из любого окна.
- **Широкая поддержка форматов:** Использует `rodio` и `symphonia` для поддержки большинства аудиоформатов.
---
## 🚀 Установка
Мы предоставляем несколько способов установки PWSP, включая стабильные релизы и постоянно обновляемые "nightly" сборки напрямую из ветки `main`.
### 📦 Flatpak (Рекомендуется)
Добавьте наш официальный OSTree репозиторий и установите приложение:
```bash
# Добавьте репозиторий
flatpak remote-add --if-not-exists pwsp https://arabianq.github.io/pipewire-soundpad/pwsp.flatpakrepo
# Установите стабильную (Stable) версию
flatpak install pwsp ru.arabianq.pwsp//stable
# ИЛИ установите Nightly версию (самые свежие обновления)
flatpak install pwsp ru.arabianq.pwsp//nightly
```
### 🟠 Debian / Ubuntu (APT Репозиторий)
Мы поддерживаем официальный APT-репозиторий для бесшовных обновлений через `apt`:
```bash
# 1. Скачайте публичный GPG ключ
wget -O- https://arabianq.github.io/pipewire-soundpad/apt/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/pwsp.gpg
# 2. Добавьте репозиторий (Выберите STABLE или NIGHTLY)
# Для Stable:
echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipewire-soundpad/apt/ stable main" | sudo tee /etc/apt/sources.list.d/pwsp.list
# Для Nightly:
# echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipewire-soundpad/apt/ nightly main" | sudo tee /etc/apt/sources.list.d/pwsp.list
# 3. Обновите индексы и установите
sudo apt update
sudo apt install pwsp
```
### 🐧 Fedora / RHEL (COPR)
Доступно через репозиторий Fedora COPR:
```bash
sudo dnf copr enable arabianq/pwsp
sudo dnf install pwsp
```
### ⚙️ Ручная установка
Вы можете вручную скачать пакеты `.deb` или готовые бинарники `.zip` на [странице релизов](https://github.com/arabianq/pipewire-soundpad/releases).
### 🦀 Сборка из исходников
Убедитесь, что у вас установлены Rust, Cargo и необходимые зависимости (`libpipewire-0.3-dev`, `libclang-dev`, `libasound2-dev`, `libdbus-1-dev`).
```bash
git clone https://github.com/arabianq/pipewire-soundpad.git
cd pipewire-soundpad
cargo build --release --locked
```
Собранные бинарники будут находиться в папке `target/release/`.
---
## 🎮 Использование
### 1. Запуск демона
PWSP работает через фоновый демон, который маршрутизирует аудио.
```bash
# Запуск демона
pwsp-daemon
```
_(Подсказка: Если вы установили программу через пакетный менеджер, она включает пользовательский systemd-сервис. Вы можете включить его командой: `systemctl --user enable --now pwsp-daemon.service`)_
### 2. Запуск GUI
Просто запустите графический интерфейс для управления и воспроизведения звуков:
```bash
pwsp-gui
```
### 3. Использование CLI
Вы также можете взаимодействовать с демоном напрямую через командную строку:
```bash
pwsp-cli play /path/to/sound.mp3
pwsp-cli stop
pwsp-cli status
```
---
## 📚 Документация и DeepWiki
Для детальной настройки, решения проблем, описания архитектуры и кастомных конфигураций, пожалуйста, посетите нашу официальную Wiki:
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/arabianq/pipewire-soundpad)
---
## 🤝 Вклад в проект (Contributing)
Будем рады вашей помощи, баг-репортам и идеям!
1. Сделайте Fork проекта.
2. Создайте свою ветку (`git checkout -b feat/amazing-feature`).
3. Закоммитьте изменения (`git commit -m 'Add some amazing feature'`).
4. Запушьте ветку (`git push origin feat/amazing-feature`).
5. Откройте Pull Request.
---
## 📝 Лицензия
Распространяется под лицензией MIT. Подробнее см. в файле `LICENSE`.
_Сделано с ❤️ для сообщества Linux._
+4 -4
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp-bin
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
pkgver = 1.12.0
pkgver = 1.12.4
pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64
@@ -10,11 +10,11 @@ pkgbase = pwsp-bin
depends = alsa-lib
provides = pwsp
conflicts = pwsp
source = pipewire-soundpad-1.12.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.0.tar.gz
source = pipewire-soundpad-1.12.4.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.4.tar.gz
sha256sums = SKIP
source_x86_64 = pwsp-1.12.0-x86_64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.12.0/pwsp-v1.12.0-linux-x64.zip
source_x86_64 = pwsp-1.12.4-x86_64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.12.4/pwsp-v1.12.4-linux-x64.zip
sha256sums_x86_64 = SKIP
source_aarch64 = pwsp-1.12.0-aarch64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.12.0/pwsp-v1.12.0-linux-arm64.zip
source_aarch64 = pwsp-1.12.4-aarch64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.12.4/pwsp-v1.12.4-linux-arm64.zip
sha256sums_aarch64 = SKIP
pkgname = pwsp-bin
+5 -5
View File
@@ -1,13 +1,13 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgname=pwsp-bin
_pkgname=pipewire-soundpad
pkgver=1.12.0
pkgver=1.12.4
pkgrel=1
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
arch=('x86_64' 'aarch64')
url="https://github.com/arabianq/pipewire-soundpad"
license=('MIT')
depends=('pipewire' 'alsa-lib')
depends=('pipewire' 'alsa-lib' 'openssl')
provides=('pwsp')
conflicts=('pwsp')
@@ -26,9 +26,9 @@ package() {
install -Dm755 "${srcdir}/pwsp-daemon" "${pkgdir}/usr/bin/pwsp-daemon"
install -Dm755 "${srcdir}/pwsp-gui" "${pkgdir}/usr/bin/pwsp-gui"
install -Dm644 "$_srcsrc/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop"
install -Dm644 "$_srcsrc/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "$_srcsrc/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
install -Dm644 "$_srcsrc/pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop"
install -Dm644 "$_srcsrc/pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "$_srcsrc/pwsp-daemon/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
install -Dm644 "$_srcsrc/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}
+2 -2
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone
pkgver = 1.12.0
pkgver = 1.12.4
pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64
@@ -12,7 +12,7 @@ pkgbase = pwsp
makedepends = cmake
makedepends = pipewire
makedepends = alsa-lib
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.0.tar.gz
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.4.tar.gz
sha256sums = SKIP
pkgname = pwsp
+3 -2
View File
@@ -1,12 +1,13 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp
pkgname=pwsp
pkgver=1.12.0
pkgver=1.12.4
pkgrel=1
pkgdesc="Lets you play audio files through your microphone"
arch=('x86_64' 'aarch64')
url="https://github.com/arabianq/pipewire-soundpad"
license=('MIT')
depends=('pipewire' 'alsa-lib' 'openssl')
makedepends=(clang rust cargo cmake pipewire alsa-lib)
source=("$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP')
@@ -43,5 +44,5 @@ package() {
install -Dm644 "pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop"
install -Dm644 "pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "pwsp-gui/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
install -Dm644 "pwsp-daemon/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
}
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@
<name>arabian</name>
</developer>
<releases>
<release version="1.12.0" date="2026-06-04" />
<release version="1.12.4" date="2026-07-07" />
</releases>
<content_rating type="oars-1.1" />
</component>
+1
View File
@@ -30,6 +30,7 @@ modules:
- name: pwsp
buildsystem: simple
build-commands:
- if [ -w /run/ccache ]; then mkdir -p /run/ccache/cargo-target && rm -rf target && ln -s /run/ccache/cargo-target target; fi
- export CARGO_HOME=$PWD/cargo && cargo build --release --offline
- install -Dm755 target/release/pwsp-daemon /app/bin/pwsp-daemon
- install -Dm755 target/release/pwsp-cli /app/bin/pwsp-cli
+12 -1
View File
@@ -23,9 +23,11 @@ BuildRequires: pipewire-devel
%if 0%{?suse_version}
BuildRequires: alsa-devel
BuildRequires: dbus-1-devel
BuildRequires: libopenssl-devel
%else
BuildRequires: alsa-lib-devel
BuildRequires: dbus-devel
BuildRequires: openssl-devel
%endif
BuildRequires: clang-devel
BuildRequires: cmake
@@ -66,7 +68,16 @@ install -Dm755 target/release/pwsp-gui %{buildroot}%{_bindir}/pwsp-gui
install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop
install -Dm644 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
install -Dm644 pwsp-gui/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
install -Dm644 pwsp-daemon/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
%post
%systemd_user_post pwsp-daemon.service
%preun
%systemd_user_preun pwsp-daemon.service
%postun
%systemd_user_postun_with_restart pwsp-daemon.service
%files
%license LICENSE
+13 -2
View File
@@ -13,7 +13,7 @@
Name: pwsp
Version: 1.12.0
Version: 1.12.4
Release: %autorelease
Summary: Lets you play audio files through your microphone
@@ -28,9 +28,11 @@ BuildRequires: pipewire-devel
%if 0%{?suse_version}
BuildRequires: alsa-devel
BuildRequires: dbus-1-devel
BuildRequires: libopenssl-devel
%else
BuildRequires: alsa-lib-devel
BuildRequires: dbus-devel
BuildRequires: openssl-devel
%endif
BuildRequires: clang-devel
BuildRequires: cmake
@@ -66,7 +68,16 @@ install -Dm755 target/release/pwsp-gui %{buildroot}%{_bindir}/pwsp-gui
install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop
install -Dm644 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
install -Dm644 pwsp-gui/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
install -Dm644 pwsp-daemon/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
%post
%systemd_user_post pwsp-daemon.service
%preun
%systemd_user_preun pwsp-daemon.service
%postun
%systemd_user_postun_with_restart pwsp-daemon.service
%files
%license LICENSE
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Pipewire Soundpad Daemon
Requires=pipewire.service
After=pipewire.service
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/pwsp-daemon
Restart=on-failure
RestartSec=3
RuntimeDirectory=pwsp
SyslogIdentifier=pwsp-daemon
[Install]
WantedBy=default.target
+4 -1
View File
@@ -28,7 +28,10 @@ async fn main() -> Result<()> {
}
get_daemon_config(); // Initialize daemon config
create_virtual_mic()?;
// Virtual mic object must be kept alive by some variable until daemon exits
let _virtual_mic = create_virtual_mic().await?;
if let Err(err) = get_audio_player().await {
eprintln!("Failed to initialize audio player: {}", err);
} // Initialize audio player
+5 -1
View File
@@ -35,6 +35,10 @@ reqwest.workspace = true
percent-encoding.workspace = true
[package.metadata.deb]
name = "pwsp"
section = "sound"
priority = "optional"
maintainer-scripts = "maintainer-scripts"
assets = [
[
"target/release/pwsp-daemon",
@@ -62,7 +66,7 @@ assets = [
"644",
],
[
"assets/pwsp-daemon.service",
"../pwsp-daemon/assets/pwsp-daemon.service",
"usr/lib/systemd/user/pwsp-daemon.service",
"644",
],
-11
View File
@@ -1,11 +0,0 @@
[Unit]
Description=Pipewire Soundpad Daemon
After=pipewire.service
[Service]
ExecStart=/usr/bin/pwsp-daemon
Restart=no
RuntimeDirectory=pwsp
[Install]
WantedBy=default.target
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
if [ -x "/usr/bin/systemctl" ]; then
systemctl --global enable pwsp-daemon.service || true
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user daemon-reload" "$username" || true
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user try-restart pwsp-daemon.service" "$username" || true
fi
fi
done
fi
fi
exit 0
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
if [ -x "/usr/bin/systemctl" ]; then
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user daemon-reload" "$username" || true
fi
fi
done
fi
fi
exit 0
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
if [ -x "/usr/bin/systemctl" ]; then
systemctl --global disable pwsp-daemon.service || true
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user stop pwsp-daemon.service" "$username" || true
fi
fi
done
fi
fi
exit 0
+3 -9
View File
@@ -3,7 +3,7 @@ mod update;
mod views;
use anyhow::{Result, anyhow};
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use eframe::{NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, FontData, FontDefinitions, FontFamily, FontTweak, Vec2, ViewportBuilder};
use itertools::Itertools;
use pwsp_lib::{
@@ -199,11 +199,7 @@ impl SoundpadGui {
}
if !search_query.is_empty() {
let file_name = entry_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let file_name = entry_path.file_name().unwrap_or_default().to_string_lossy();
if !file_name.to_lowercase().contains(search_query) {
return false;
@@ -219,7 +215,7 @@ impl SoundpadGui {
fn add_font(font_name: &str, font_bytes: &[u8], fonts: &mut FontDefinitions) -> Result<()> {
let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak {
scale: 1.0,
hinting_override: Some(true),
hinting: Some(true),
..Default::default()
});
@@ -265,9 +261,7 @@ pub async fn run() -> Result<()> {
const ICON: &[u8] = include_bytes!("../../assets/icon.png");
let options = NativeOptions {
vsync: true,
centered: true,
hardware_acceleration: HardwareAcceleration::Preferred,
viewport: ViewportBuilder::default()
.with_app_id("ru.arabianq.pwsp")
+1 -1
View File
@@ -121,7 +121,7 @@ impl App for SoundpadGui {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut EFrame) {
// Draw UI
CentralPanel::default().show_inside(ui, |ui| {
CentralPanel::default().show(ui, |ui| {
if !self.audio_player_state.is_daemon_running {
self.draw_waiting_for_daemon(ui);
return;
+14 -13
View File
@@ -308,7 +308,19 @@ impl SoundpadGui {
let mut read = Vec::new();
if let Ok(entries) = std::fs::read_dir(&path) {
for entry in entries.filter_map(|e| e.ok()) {
read.push(entry.path());
let child_path = entry.path();
if !child_path.is_dir()
&& !crate::gui::SUPPORTED_EXTENSIONS.contains(
&child_path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
)
{
continue;
}
read.push(child_path);
}
}
let sort_order = config.get_sort_order(&path);
@@ -331,17 +343,7 @@ impl SoundpadGui {
let search_query = search_query.trim();
for child in children {
if !child.is_dir() {
if !crate::gui::SUPPORTED_EXTENSIONS.contains(
&child
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
continue;
}
if !search_query.is_empty() {
if !child.is_dir() && !search_query.is_empty() {
let file_name = child
.file_name()
.unwrap_or_default()
@@ -351,7 +353,6 @@ impl SoundpadGui {
continue;
}
}
}
Self::draw_tree_node(ui, child, config, app_state, audio_player_state, actions);
}
});
+25 -9
View File
@@ -1,9 +1,13 @@
mod gui;
use anyhow::{Context, Result};
use anyhow::{Context, Result, anyhow};
use pwsp_lib::utils::gui::ensure_pwsp_audio_dir;
use rust_i18n::i18n;
use std::{env, path::PathBuf};
use std::{
env,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
i18n!("locales", fallback = "en");
@@ -31,19 +35,31 @@ async fn download_audio_from_url(uri: &str) -> Result<PathBuf> {
let target_url = uri
.strip_prefix(prefix)
.ok_or_else(|| anyhow::anyhow!("URI does not containt an expected prefix: {}", prefix))?;
.ok_or_else(|| anyhow!("URI does not containt an expected prefix: {}", prefix))?;
let file_name_encoded = target_url
.split('/')
.next_back()
.unwrap_or("downloaded_audio.mp3");
let file_name_encoded = match target_url.split('/').next_back() {
Some(path) => path.to_string(),
None => {
let id = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went back")
.as_nanos();
format!("downloaded_audio_{}.mp3", id)
}
};
let file_name = percent_encoding::percent_decode_str(file_name_encoded)
let file_name = percent_encoding::percent_decode_str(&file_name_encoded.clone())
.decode_utf8()
.unwrap_or_else(|_| file_name_encoded.into())
.into_owned();
let save_path = ensure_pwsp_audio_dir().join(file_name);
let normalized_file_name = file_name.replace('\\', "/");
let sanitized_file_name = Path::new(&normalized_file_name)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("downloaded_audio.mp3");
let save_path = ensure_pwsp_audio_dir().join(sanitized_file_name);
let response = reqwest::get(target_url)
.await?
+7 -15
View File
@@ -1,8 +1,8 @@
use crate::{
types::pipewire::{DeviceType, Terminate},
types::pipewire::DeviceType,
utils::{
daemon::get_daemon_config,
pipewire::{create_link, get_device, link_player_to_virtual_mic},
pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic},
},
};
use anyhow::{Result, anyhow};
@@ -58,8 +58,8 @@ pub struct AudioPlayer {
pub tracks: HashMap<u32, PlayingSound>,
pub next_id: u32,
input_link_sender: Option<pipewire::channel::Sender<Terminate>>,
player_link_sender: Option<pipewire::channel::Sender<Terminate>>,
input_link_sender: Option<PwTerminator>,
player_link_sender: Option<PwTerminator>,
pub input_device_name: Option<String>,
pub volume: f32, // Master volume
@@ -108,24 +108,16 @@ impl AudioPlayer {
}
fn abort_link_thread(&mut self) {
if let Some(sender) = &self.input_link_sender {
if sender.send(Terminate {}).is_ok() {
if self.input_link_sender.is_some() {
println!("Sent terminate signal to input link thread");
self.input_link_sender = None;
} else {
eprintln!("Failed to send terminate signal to input link thread");
}
}
}
fn abort_player_link_thread(&mut self) {
if let Some(sender) = &self.player_link_sender {
if sender.send(Terminate {}).is_ok() {
if self.player_link_sender.is_some() {
println!("Sent terminate signal to player link thread");
self.player_link_sender = None;
} else {
eprintln!("Failed to send terminate signal to player link thread");
}
}
}
@@ -187,7 +179,7 @@ impl AudioPlayer {
return Ok(());
};
self.input_link_sender = Some(create_link(output_fl, output_fr, input_fl, input_fr)?);
self.input_link_sender = Some(create_link(output_fl, output_fr, input_fl, input_fr).await?);
Ok(())
}
+2 -2
View File
@@ -1,7 +1,7 @@
use anyhow::Result;
use anyhow::{Context, Result};
use std::path::PathBuf;
pub fn get_config_path() -> Result<PathBuf> {
let config_path = dirs::config_dir().expect("Failed to obtain config dir");
let config_path = dirs::config_dir().context("Failed to obtain config dir")?;
Ok(config_path.join("pwsp"))
}
+273 -249
View File
@@ -1,14 +1,224 @@
use crate::types::pipewire::{AudioDevice, DeviceType, Port, Terminate};
use crate::types::pipewire::{AudioDevice, DeviceType, Port};
use anyhow::{Result, anyhow};
use pipewire::{
context::ContextRc, link::Link, main_loop::MainLoopRc, properties::properties,
registry::GlobalObject, spa::utils::dict::DictRef,
};
use std::{collections::HashMap, thread};
use tokio::{
sync::mpsc,
time::{Duration, timeout},
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::OnceLock, thread};
use tokio::sync::oneshot;
pub enum PwCommand {
GetDevices {
resp: oneshot::Sender<(Vec<AudioDevice>, Vec<AudioDevice>)>,
},
CreateVirtualMic {
resp: oneshot::Sender<Result<u32, String>>,
},
CreateLink {
output_fl: Port,
output_fr: Port,
input_fl: Port,
input_fr: Port,
resp: oneshot::Sender<Result<(u32, u32), String>>,
},
DestroyObject {
id: u32,
},
}
struct AppState {
input_devices: HashMap<u32, AudioDevice>,
output_devices: HashMap<u32, AudioDevice>,
ports: HashMap<u32, Port>,
proxies: HashMap<u32, Box<dyn std::any::Any>>,
proxy_id_counter: u32,
ready_tx: Option<std::sync::mpsc::Sender<()>>,
}
pub struct PipewireManager {
pub sender: pipewire::channel::Sender<PwCommand>,
}
static MANAGER: OnceLock<PipewireManager> = OnceLock::new();
pub fn get_manager() -> &'static PipewireManager {
MANAGER.get_or_init(|| {
let (pw_sender, pw_receiver) = pipewire::channel::channel::<PwCommand>();
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
thread::spawn(move || {
let (main_loop, context) = setup_pipewire_context().expect("Failed to setup pipewire");
// Leak main_loop and context so their borrows can be 'static
let main_loop = Box::leak(Box::new(main_loop));
let context = Box::leak(Box::new(context));
// Leak to fix lifetime issues since this thread lives forever
let core = Box::leak(Box::new(
context
.connect(None)
.expect("Failed to connect to pipewire"),
));
let registry = Box::leak(Box::new(
core.get_registry().expect("Failed to get registry"),
));
let state = Rc::new(RefCell::new(AppState {
input_devices: HashMap::new(),
output_devices: HashMap::new(),
ports: HashMap::new(),
proxies: HashMap::new(),
proxy_id_counter: 10000,
ready_tx: Some(ready_tx),
}));
let state_for_registry_add = state.clone();
let state_for_registry_remove = state.clone();
let _listener = registry
.add_listener_local()
.global(move |global| {
let (device, port) = parse_global_object(global);
let mut s = state_for_registry_add.borrow_mut();
if let Some(device) = device {
match device.device_type {
DeviceType::Input => {
s.input_devices.insert(device.id, device);
}
DeviceType::Output => {
s.output_devices.insert(device.id, device);
}
}
} else if let Some(port) = port {
let node_id = port.node_id;
s.ports.insert(port.port_id, port.clone());
if let Some(d) = s.input_devices.get_mut(&node_id) {
d.add_port(port.clone());
} else if let Some(d) = s.output_devices.get_mut(&node_id) {
d.add_port(port);
}
}
})
.global_remove(move |id| {
let mut s = state_for_registry_remove.borrow_mut();
s.input_devices.remove(&id);
s.output_devices.remove(&id);
s.ports.retain(|_, port| port.node_id != id);
s.ports.remove(&id);
})
.register();
// sync to signal ready
let state_for_sync = state.clone();
let _core_listener = core
.add_listener_local()
.done(move |id, _seq| {
if id == 0 {
let mut s = state_for_sync.borrow_mut();
if let Some(tx) = s.ready_tx.take() {
let _ = tx.send(());
}
}
})
.register();
let _pending = core.sync(0).expect("sync failed");
let state_for_cmd = state.clone();
let _receiver = pw_receiver.attach(main_loop.loop_(), move |cmd| {
let mut s = state_for_cmd.borrow_mut();
match cmd {
PwCommand::GetDevices { resp } => {
let mut inputs: Vec<AudioDevice> =
s.input_devices.values().cloned().collect();
let mut outputs: Vec<AudioDevice> =
s.output_devices.values().cloned().collect();
inputs.sort_by_key(|a| a.id);
outputs.sort_by_key(|a| a.id);
let _ = resp.send((inputs, outputs));
}
PwCommand::CreateVirtualMic { resp } => {
let props = properties!(
"factory.name" => "support.null-audio-sink",
"node.name" => "pwsp-virtual-mic",
"node.description" => "PWSP Virtual Mic",
"media.class" => "Audio/Source/Virtual",
"audio.position" => "[ FL FR ]",
"audio.channels" => "2",
"object.linger" => "false",
);
match core.create_object::<pipewire::node::Node>("adapter", &props) {
Ok(node) => {
s.proxy_id_counter += 1;
let id = s.proxy_id_counter;
s.proxies.insert(id, Box::new(node));
let _ = resp.send(Ok(id));
}
Err(e) => {
let _ = resp.send(Err(e.to_string()));
}
}
}
PwCommand::CreateLink {
output_fl,
output_fr,
input_fl,
input_fr,
resp,
} => {
let props_fl = properties! {
"link.output.node" => format!("{}", output_fl.node_id).as_str(),
"link.output.port" => format!("{}", output_fl.port_id).as_str(),
"link.input.node" => format!("{}", input_fl.node_id).as_str(),
"link.input.port" => format!("{}", input_fl.port_id).as_str(),
};
let props_fr = properties! {
"link.output.node" => format!("{}", output_fr.node_id).as_str(),
"link.output.port" => format!("{}", output_fr.port_id).as_str(),
"link.input.node" => format!("{}", input_fr.node_id).as_str(),
"link.input.port" => format!("{}", input_fr.port_id).as_str(),
};
let link_fl = match core.create_object::<Link>("link-factory", &props_fl) {
Ok(link) => link,
Err(e) => {
let _ = resp.send(Err(e.to_string()));
return;
}
};
let link_fr = match core.create_object::<Link>("link-factory", &props_fr) {
Ok(link) => link,
Err(e) => {
let _ = resp.send(Err(e.to_string()));
return;
}
};
s.proxy_id_counter += 1;
let id_fl = s.proxy_id_counter;
s.proxies.insert(id_fl, Box::new(link_fl));
s.proxy_id_counter += 1;
let id_fr = s.proxy_id_counter;
s.proxies.insert(id_fr, Box::new(link_fr));
let _ = resp.send(Ok((id_fl, id_fr)));
}
PwCommand::DestroyObject { id } => {
s.proxies.remove(&id);
}
}
});
main_loop.run();
});
// Wait for the pipewire thread to be fully up and processed initial events
let _ = ready_rx.recv();
PipewireManager { sender: pw_sender }
})
}
pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
pipewire::init();
@@ -71,127 +281,17 @@ fn parse_global_object(
(None, None)
}
async fn pw_get_global_objects_thread(
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
pw_receiver: pipewire::channel::Receiver<Terminate>,
init_sender: tokio::sync::oneshot::Sender<Result<(), String>>,
) {
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
let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone();
move |_| _main_loop.quit()
});
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 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
.add_listener_local()
.global(move |global| {
// Try to parse every global object pipewire finds
let (device, port) = parse_global_object(global);
// Send message to the main thread
let sender_clone = main_sender.clone();
tokio::task::spawn(async move {
sender_clone.send((device, port)).await.ok();
});
})
.register();
// Signal successful initialization
if init_sender.send(Ok(())).is_err() {
return;
}
main_loop.run();
}
pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>)> {
// Channels to communicate with pipewire thread
let (main_sender, mut main_receiver) = mpsc::channel(10);
let (pw_sender, pw_receiver) = pipewire::channel::channel();
let (init_sender, init_receiver) = tokio::sync::oneshot::channel();
// Spawn pipewire thread in background
let _pw_thread = tokio::spawn(async move {
pw_get_global_objects_thread(main_sender, pw_receiver, init_sender).await
});
// Wait for initialization to complete
if let Err(e) = init_receiver.await {
return Err(anyhow!(e));
}
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new();
let mut ports: Vec<Port> = vec![];
loop {
// If we don't receive a message in 100ms, we can assume that pipewire thread is finished
match timeout(Duration::from_millis(100), main_receiver.recv()).await {
Ok(Some((device, port))) => {
if let Some(device) = device {
match device.device_type {
DeviceType::Input => {
input_devices.insert(device.id, device);
}
DeviceType::Output => {
output_devices.insert(device.id, device);
}
}
} else if let Some(port) = port {
ports.push(port);
}
}
Ok(None) | Err(_) => {
// Pipewire thread is finished and we can collect our devices
let _ = pw_sender.send(Terminate {});
for port in ports {
let node_id = port.node_id;
if let Some(input_device) = input_devices.get_mut(&node_id) {
input_device.add_port(port);
} else if let Some(output_device) = output_devices.get_mut(&node_id) {
output_device.add_port(port);
}
}
let mut input_devices: Vec<AudioDevice> = input_devices.into_values().collect();
let mut output_devices: Vec<AudioDevice> = output_devices.into_values().collect();
input_devices.sort_by_key(|a| a.id);
output_devices.sort_by_key(|a| a.id);
return Ok((input_devices, output_devices));
}
}
}
let (tx, rx) = oneshot::channel();
let manager = get_manager();
manager
.sender
.send(PwCommand::GetDevices { resp: tx })
.map_err(|_| anyhow!("Failed to send GetDevices to manager"))?;
let res = rx
.await
.map_err(|e| anyhow!("Failed to receive response: {}", e))?;
Ok(res)
}
pub async fn get_device(device_name: &str) -> Result<AudioDevice> {
@@ -209,65 +309,36 @@ pub async fn get_device(device_name: &str) -> Result<AudioDevice> {
.ok_or_else(|| anyhow!("Device not found"))
}
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<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 (main_loop, context) = match setup_pipewire_context() {
Ok(res) => res,
Err(e) => {
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!(
"factory.name" => "support.null-audio-sink",
"node.name" => "pwsp-virtual-mic",
"node.description" => "PWSP Virtual Mic",
"media.class" => "Audio/Source/Virtual",
"audio.position" => "[ FL FR ]",
"audio.channels" => "2",
"object.linger" => "false", // Destroy the node on app exit
);
let _node = match core.create_object::<pipewire::node::Node>("adapter", &props) {
Ok(node) => node,
Err(e) => {
let _ = init_sender.send(Err(format!("Failed to create virtual mic: {}", e)));
return;
}
};
let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone();
move |_| _main_loop.quit()
});
println!("Virtual mic created");
if init_sender.send(Ok(())).is_err() {
return;
}
main_loop.run();
});
if let Err(e) = init_receiver.recv()? {
return Err(anyhow!(e));
pub struct PwTerminator {
ids: Vec<u32>,
}
Ok(pw_sender)
impl Drop for PwTerminator {
fn drop(&mut self) {
let manager = get_manager();
for id in &self.ids {
let _ = manager.sender.send(PwCommand::DestroyObject { id: *id });
}
}
}
pub async fn link_player_to_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>> {
pub async fn create_virtual_mic() -> Result<PwTerminator> {
let (tx, rx) = oneshot::channel();
let manager = get_manager();
manager
.sender
.send(PwCommand::CreateVirtualMic { resp: tx })
.map_err(|_| anyhow!("Failed to send CreateVirtualMic to manager"))?;
let res = rx
.await
.map_err(|e| anyhow!("Failed to receive response: {}", e))?;
let id = res.map_err(|e| anyhow!(e))?;
Ok(PwTerminator { ids: vec![id] })
}
pub async fn link_player_to_virtual_mic() -> Result<PwTerminator> {
let pwsp_daemon_output = match get_device("pwsp-daemon").await {
Ok(device) => device,
Err(_) => {
@@ -303,81 +374,34 @@ pub async fn link_player_to_virtual_mic() -> Result<pipewire::channel::Sender<Te
None => return Err(anyhow!("Failed to get pwsp-virtual-mic input_fr")),
};
create_link(output_fl, output_fr, input_fl, input_fr)
create_link(output_fl, output_fr, input_fl, input_fr).await
}
pub fn create_link(
pub async fn create_link(
output_fl: Port,
output_fr: Port,
input_fl: Port,
input_fr: Port,
) -> Result<pipewire::channel::Sender<Terminate>> {
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
) -> Result<PwTerminator> {
let (tx, rx) = oneshot::channel();
let manager = get_manager();
manager
.sender
.send(PwCommand::CreateLink {
output_fl,
output_fr,
input_fl,
input_fr,
resp: tx,
})
.map_err(|_| anyhow!("Failed to send CreateLink to manager"))?;
let _pw_thread = thread::spawn(move || {
let (main_loop, context) = match setup_pipewire_context() {
Ok(res) => res,
Err(e) => {
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 res = rx
.await
.map_err(|e| anyhow!("Failed to receive response: {}", e))?;
let props_fl = properties! {
"link.output.node" => format!("{}", output_fl.node_id).as_str(),
"link.output.port" => format!("{}", output_fl.port_id).as_str(),
"link.input.node" => format!("{}", input_fl.node_id).as_str(),
"link.input.port" => format!("{}", input_fl.port_id).as_str(),
};
let props_fr = properties! {
"link.output.node" => format!("{}", output_fr.node_id).as_str(),
"link.output.port" => format!("{}", output_fr.port_id).as_str(),
"link.input.node" => format!("{}", input_fr.node_id).as_str(),
"link.input.port" => format!("{}", input_fr.port_id).as_str(),
};
let _link_fl = match core.create_object::<Link>("link-factory", &props_fl) {
Ok(link) => link,
Err(e) => {
let _ = init_sender.send(Err(format!("Failed to create link FL: {}", e)));
return;
}
};
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 _main_loop = main_loop.clone();
move |_| _main_loop.quit()
});
println!(
"Link created: FL: {}-{} FR: {}-{}",
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();
});
if let Err(e) = init_receiver.recv()? {
return Err(anyhow!(e));
}
Ok(pw_sender)
let (id_fl, id_fr) = res.map_err(|e| anyhow!(e))?;
Ok(PwTerminator {
ids: vec![id_fl, id_fr],
})
}