mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-06-19 12:13:32 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a56e1db6b | |||
| 838fc1ce29 | |||
| 622cf39fa2 | |||
| 809b1a8490 | |||
| d1a5275173 | |||
| e67f174a59 | |||
| 6545431ac2 | |||
| 026ef97a72 | |||
| 9f833cc30b | |||
| 410a2c7959 | |||
| c173e602ad | |||
| 3576c634fd | |||
| 5747f39ace | |||
| c501033834 | |||
| c0a27e0c3b | |||
| 3c2882ef1f | |||
| 36aed3f55d | |||
| c48a425bb0 | |||
| 9a5436cd35 | |||
| 2ce243e896 | |||
| 57fb3fd7a3 | |||
| 82b02bf520 | |||
| 3d4dbbe866 | |||
| 70bd3a889a | |||
| a7b5bdd2a9 | |||
| 99fef4a167 | |||
| ae08f7ddc3 |
@@ -244,3 +244,18 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
./dist/pwsp-*.zip
|
./dist/pwsp-*.zip
|
||||||
./dist/*.deb
|
./dist/*.deb
|
||||||
|
|
||||||
|
- 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 --clone-url https://github.com/arabianq/pipewire-soundpad.git \
|
||||||
|
--commit ${{ needs.prepare.outputs.tag }} \
|
||||||
|
--spec packages/rpm/pwsp.spec \
|
||||||
|
arabianq/pwsp
|
||||||
|
|
||||||
|
|||||||
Generated
+10
-10
@@ -776,7 +776,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cpal"
|
name = "cpal"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
source = "git+https://github.com/RustAudio/cpal#d2a268839bf9ada7c2477c8a5f9483c28aa12d0c"
|
source = "git+https://github.com/RustAudio/cpal#004897773f17fa15afbc3270b7cca37cfbbdef2a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa",
|
"alsa",
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
@@ -2306,9 +2306,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.31"
|
version = "0.4.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
@@ -3280,7 +3280,7 @@ checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pwsp-cli"
|
name = "pwsp-cli"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3291,7 +3291,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pwsp-daemon"
|
name = "pwsp-daemon"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3303,7 +3303,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pwsp-gui"
|
name = "pwsp-gui"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"eframe",
|
"eframe",
|
||||||
@@ -3327,7 +3327,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pwsp-lib"
|
name = "pwsp-lib"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3686,7 +3686,7 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rodio"
|
name = "rodio"
|
||||||
version = "0.22.2"
|
version = "0.22.2"
|
||||||
source = "git+https://github.com/arabianq/rodio.git?rev=a634dd471e9d59196e19bf01323fb45f2f899821#a634dd471e9d59196e19bf01323fb45f2f899821"
|
source = "git+https://github.com/arabianq/rodio.git?rev=c6a81b5a46e00a6a682c0c431dff62e86f57d819#c6a81b5a46e00a6a682c0c431dff62e86f57d819"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpal",
|
"cpal",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
@@ -5967,9 +5967,9 @@ checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"yoke-derive",
|
"yoke-derive",
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ members = [
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["arabian"]
|
authors = ["arabian"]
|
||||||
homepage = "https://pwsp.arabianq.ru"
|
homepage = "https://pwsp.arabianq.ru"
|
||||||
@@ -52,7 +52,7 @@ rustix = { version = "1.1.4", features = ["process"] }
|
|||||||
rust-i18n = "4.0.0"
|
rust-i18n = "4.0.0"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
|
|
||||||
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "a634dd471e9d59196e19bf01323fb45f2f899821", default-features = false, features = [
|
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "c6a81b5a46e00a6a682c0c431dff62e86f57d819", default-features = false, features = [
|
||||||
"symphonia-all",
|
"symphonia-all",
|
||||||
"symphonia-libopus",
|
"symphonia-libopus",
|
||||||
"playback",
|
"playback",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pkgbase = pwsp-bin
|
pkgbase = pwsp-bin
|
||||||
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
|
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
|
||||||
pkgver = 1.11.0
|
pkgver = 1.12.0
|
||||||
pkgrel = 1
|
pkgrel = 2
|
||||||
url = https://github.com/arabianq/pipewire-soundpad
|
url = https://github.com/arabianq/pipewire-soundpad
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
arch = aarch64
|
arch = aarch64
|
||||||
@@ -10,11 +10,11 @@ pkgbase = pwsp-bin
|
|||||||
depends = alsa-lib
|
depends = alsa-lib
|
||||||
provides = pwsp
|
provides = pwsp
|
||||||
conflicts = pwsp
|
conflicts = pwsp
|
||||||
source = pipewire-soundpad-1.11.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.11.0.tar.gz
|
source = pipewire-soundpad-1.12.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.0.tar.gz
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
source_x86_64 = pwsp-1.11.0-x86_64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.11.0/pwsp-v1.11.0-linux-x64.zip
|
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
|
||||||
sha256sums_x86_64 = SKIP
|
sha256sums_x86_64 = SKIP
|
||||||
source_aarch64 = pwsp-1.11.0-aarch64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.11.0/pwsp-v1.11.0-linux-arm64.zip
|
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
|
||||||
sha256sums_aarch64 = SKIP
|
sha256sums_aarch64 = SKIP
|
||||||
|
|
||||||
pkgname = pwsp-bin
|
pkgname = pwsp-bin
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
|
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
|
||||||
pkgname=pwsp-bin
|
pkgname=pwsp-bin
|
||||||
_pkgname=pipewire-soundpad
|
_pkgname=pipewire-soundpad
|
||||||
pkgver=1.11.0
|
pkgver=1.12.0
|
||||||
pkgrel=1
|
pkgrel=2
|
||||||
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
|
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
url="https://github.com/arabianq/pipewire-soundpad"
|
url="https://github.com/arabianq/pipewire-soundpad"
|
||||||
@@ -26,9 +26,9 @@ package() {
|
|||||||
install -Dm755 "${srcdir}/pwsp-daemon" "${pkgdir}/usr/bin/pwsp-daemon"
|
install -Dm755 "${srcdir}/pwsp-daemon" "${pkgdir}/usr/bin/pwsp-daemon"
|
||||||
install -Dm755 "${srcdir}/pwsp-gui" "${pkgdir}/usr/bin/pwsp-gui"
|
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/pwsp-gui/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/pwsp-gui/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-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
|
||||||
|
|
||||||
install -Dm644 "$_srcsrc/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
install -Dm644 "$_srcsrc/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = pwsp
|
pkgbase = pwsp
|
||||||
pkgdesc = Lets you play audio files through your microphone
|
pkgdesc = Lets you play audio files through your microphone
|
||||||
pkgver = 1.11.0
|
pkgver = 1.12.0
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/arabianq/pipewire-soundpad
|
url = https://github.com/arabianq/pipewire-soundpad
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
@@ -12,7 +12,7 @@ pkgbase = pwsp
|
|||||||
makedepends = cmake
|
makedepends = cmake
|
||||||
makedepends = pipewire
|
makedepends = pipewire
|
||||||
makedepends = alsa-lib
|
makedepends = alsa-lib
|
||||||
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.11.0.tar.gz
|
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.0.tar.gz
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
|
|
||||||
pkgname = pwsp
|
pkgname = pwsp
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
|
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
|
||||||
pkgsubn=pwsp
|
pkgsubn=pwsp
|
||||||
pkgname=pwsp
|
pkgname=pwsp
|
||||||
pkgver=1.11.0
|
pkgver=1.12.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Lets you play audio files through your microphone"
|
pkgdesc="Lets you play audio files through your microphone"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@
|
|||||||
<name>arabian</name>
|
<name>arabian</name>
|
||||||
</developer>
|
</developer>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="1.11.0" date="2026-06-02" />
|
<release version="1.12.0" date="2026-06-04" />
|
||||||
</releases>
|
</releases>
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
</component>
|
</component>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
# prevent library files from being installed
|
||||||
|
%global cargo_install_lib 0
|
||||||
|
|
||||||
|
# disable debuginfo package generation (debugsourcefiles.list is empty for Rust)
|
||||||
|
%global debug_package %{nil}
|
||||||
|
|
||||||
|
Name: pwsp-git
|
||||||
|
Version: {{{ git describe --tags --always | sed 's/^v//' | sed -E 's/-([0-9]+)-(g[0-9a-f]+)/^git.\1.\2/' }}}
|
||||||
|
Release: 1%{?dist}
|
||||||
|
Summary: Lets you play audio files through your microphone (git version)
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
|
||||||
|
URL: https://github.com/arabianq/pipewire-soundpad
|
||||||
|
VCS: {{{ git_dir_vcs }}}
|
||||||
|
Source: {{{ git_cwd_pack }}}
|
||||||
|
|
||||||
|
|
||||||
|
BuildRequires: rust
|
||||||
|
BuildRequires: cargo
|
||||||
|
BuildRequires: pipewire-devel
|
||||||
|
%if 0%{?suse_version}
|
||||||
|
BuildRequires: alsa-devel
|
||||||
|
BuildRequires: dbus-1-devel
|
||||||
|
%else
|
||||||
|
BuildRequires: alsa-lib-devel
|
||||||
|
BuildRequires: dbus-devel
|
||||||
|
%endif
|
||||||
|
BuildRequires: clang-devel
|
||||||
|
BuildRequires: cmake
|
||||||
|
BuildRequires: pkgconfig
|
||||||
|
%if 0%{?suse_version} && 0%{?suse_version} <= 1500
|
||||||
|
BuildRequires: gcc13-c++
|
||||||
|
%endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Declare compatibility and conflicts with the stable package
|
||||||
|
Provides: pwsp = %{version}-%{release}
|
||||||
|
Conflicts: pwsp
|
||||||
|
|
||||||
|
%global _description %{expand:
|
||||||
|
PWSP lets you play audio files through your microphone. Has both CLI and
|
||||||
|
GUI clients. This is the latest development (git) version.}
|
||||||
|
|
||||||
|
%description %{_description}
|
||||||
|
|
||||||
|
%prep
|
||||||
|
{{{ git_cwd_setup_macro }}}
|
||||||
|
|
||||||
|
|
||||||
|
%build
|
||||||
|
%if 0%{?suse_version} && 0%{?suse_version} <= 1500
|
||||||
|
export CC=gcc-13
|
||||||
|
export CXX=g++-13
|
||||||
|
%endif
|
||||||
|
cargo build --release --locked
|
||||||
|
|
||||||
|
|
||||||
|
%install
|
||||||
|
install -Dm755 target/release/pwsp-cli %{buildroot}%{_bindir}/pwsp-cli
|
||||||
|
install -Dm755 target/release/pwsp-daemon %{buildroot}%{_bindir}/pwsp-daemon
|
||||||
|
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
|
||||||
|
|
||||||
|
%files
|
||||||
|
%license LICENSE
|
||||||
|
%doc README.md
|
||||||
|
%{_bindir}/pwsp-cli
|
||||||
|
%{_bindir}/pwsp-daemon
|
||||||
|
%{_bindir}/pwsp-gui
|
||||||
|
%{_datadir}/applications/pwsp.desktop
|
||||||
|
%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
|
||||||
|
/usr/lib/systemd/user/pwsp-daemon.service
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
{{{ git_dir_changelog }}}
|
||||||
|
|
||||||
+29
-5
@@ -1,10 +1,19 @@
|
|||||||
%bcond check 1
|
|
||||||
|
|
||||||
# prevent library files from being installed
|
# prevent library files from being installed
|
||||||
%global cargo_install_lib 0
|
%global cargo_install_lib 0
|
||||||
|
|
||||||
|
# Fallback macros for systems without rpmautospec (e.g. openSUSE)
|
||||||
|
%{!?autorelease: %global autorelease 1}
|
||||||
|
%{!?autochangelog: %global autochangelog \
|
||||||
|
* Tue Jun 02 2026 Arabian <arabianq@github> - %{version}-%{release}\
|
||||||
|
- Release build}
|
||||||
|
|
||||||
|
|
||||||
|
# disable debuginfo package generation (debugsourcefiles.list is empty for Rust)
|
||||||
|
%global debug_package %{nil}
|
||||||
|
|
||||||
|
|
||||||
Name: pwsp
|
Name: pwsp
|
||||||
Version: 1.11.0
|
Version: 1.12.0
|
||||||
Release: %autorelease
|
Release: %autorelease
|
||||||
Summary: Lets you play audio files through your microphone
|
Summary: Lets you play audio files through your microphone
|
||||||
|
|
||||||
@@ -16,11 +25,21 @@ Source: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags
|
|||||||
BuildRequires: rust
|
BuildRequires: rust
|
||||||
BuildRequires: cargo
|
BuildRequires: cargo
|
||||||
BuildRequires: pipewire-devel
|
BuildRequires: pipewire-devel
|
||||||
|
%if 0%{?suse_version}
|
||||||
|
BuildRequires: alsa-devel
|
||||||
|
BuildRequires: dbus-1-devel
|
||||||
|
%else
|
||||||
BuildRequires: alsa-lib-devel
|
BuildRequires: alsa-lib-devel
|
||||||
|
BuildRequires: dbus-devel
|
||||||
|
%endif
|
||||||
BuildRequires: clang-devel
|
BuildRequires: clang-devel
|
||||||
BuildRequires: cmake
|
BuildRequires: cmake
|
||||||
BuildRequires: dbus-devel
|
BuildRequires: pkgconfig
|
||||||
BuildRequires: pkgconf-pkg-config
|
%if 0%{?suse_version} && 0%{?suse_version} <= 1500
|
||||||
|
BuildRequires: gcc13-c++
|
||||||
|
%endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%global _description %{expand:
|
%global _description %{expand:
|
||||||
PWSP lets you play audio files through your microphone. Has both CLI and
|
PWSP lets you play audio files through your microphone. Has both CLI and
|
||||||
@@ -32,8 +51,13 @@ GUI clients.}
|
|||||||
%autosetup -n pipewire-soundpad-%{version} -p1
|
%autosetup -n pipewire-soundpad-%{version} -p1
|
||||||
|
|
||||||
%build
|
%build
|
||||||
|
%if 0%{?suse_version} && 0%{?suse_version} <= 1500
|
||||||
|
export CC=gcc-13
|
||||||
|
export CXX=g++-13
|
||||||
|
%endif
|
||||||
cargo build --release --locked
|
cargo build --release --locked
|
||||||
|
|
||||||
|
|
||||||
%install
|
%install
|
||||||
install -Dm755 target/release/pwsp-cli %{buildroot}%{_bindir}/pwsp-cli
|
install -Dm755 target/release/pwsp-cli %{buildroot}%{_bindir}/pwsp-cli
|
||||||
install -Dm755 target/release/pwsp-daemon %{buildroot}%{_bindir}/pwsp-daemon
|
install -Dm755 target/release/pwsp-daemon %{buildroot}%{_bindir}/pwsp-daemon
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_daemon_config(); // Initialize daemon config
|
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 {
|
if let Err(err) = get_audio_player().await {
|
||||||
eprintln!("Failed to initialize audio player: {}", err);
|
eprintln!("Failed to initialize audio player: {}", err);
|
||||||
} // Initialize audio player
|
} // Initialize audio player
|
||||||
|
|||||||
@@ -70,6 +70,61 @@ kz = "Жою"
|
|||||||
he = "הסר"
|
he = "הסר"
|
||||||
pt-BR = "Remover"
|
pt-BR = "Remover"
|
||||||
|
|
||||||
|
[gui.context.dirs.sort_by]
|
||||||
|
en = "Sort by"
|
||||||
|
ru = "Сортировка"
|
||||||
|
es = "Ordenar por"
|
||||||
|
fr = "Trier par"
|
||||||
|
zh = "排序方式"
|
||||||
|
ar = "ترتيب حسب"
|
||||||
|
kz = "Сұрыптау"
|
||||||
|
he = "מיין לפי"
|
||||||
|
pt-BR = "Ordenar por"
|
||||||
|
|
||||||
|
[gui.sort.alpha_asc]
|
||||||
|
en = "Alphabetical (A-Z)"
|
||||||
|
ru = "По алфавиту (А-Я)"
|
||||||
|
es = "Alfabético (A-Z)"
|
||||||
|
fr = "Alphabétique (A-Z)"
|
||||||
|
zh = "字母顺序 (A-Z)"
|
||||||
|
ar = "أبجدي (A-Z)"
|
||||||
|
kz = "Әліпби бойынша (А-Я)"
|
||||||
|
he = "אלפביתי (A-Z)"
|
||||||
|
pt-BR = "Alfabético (A-Z)"
|
||||||
|
|
||||||
|
[gui.sort.alpha_desc]
|
||||||
|
en = "Alphabetical (Z-A)"
|
||||||
|
ru = "По алфавиту (Я-А)"
|
||||||
|
es = "Alfabético (Z-A)"
|
||||||
|
fr = "Alphabétique (Z-A)"
|
||||||
|
zh = "字母顺序 (Z-A)"
|
||||||
|
ar = "أبجدي (Z-A)"
|
||||||
|
kz = "Әліпби бойынша (Я-А)"
|
||||||
|
he = "אלפביתי (Z-A)"
|
||||||
|
pt-BR = "Alfabético (Z-A)"
|
||||||
|
|
||||||
|
[gui.sort.date_newest]
|
||||||
|
en = "Date modified (Newest first)"
|
||||||
|
ru = "Дата изменения (Сначала новые)"
|
||||||
|
es = "Fecha de modificación (Más nuevo primero)"
|
||||||
|
fr = "Date de modification (Plus récent en premier)"
|
||||||
|
zh = "修改日期 (最新优先)"
|
||||||
|
ar = "تاريخ التعديل (الأحدث أولاً)"
|
||||||
|
kz = "Өзгертілген күні (Жаңалары бірінші)"
|
||||||
|
he = "תאריך שינוי (החדש ביותר ראשון)"
|
||||||
|
pt-BR = "Data de modificação (Mais novo primeiro)"
|
||||||
|
|
||||||
|
[gui.sort.date_oldest]
|
||||||
|
en = "Date modified (Oldest first)"
|
||||||
|
ru = "Дата изменения (Сначала старые)"
|
||||||
|
es = "Fecha de modificación (Más antiguo primero)"
|
||||||
|
fr = "Date de modification (Plus ancien en premier)"
|
||||||
|
zh = "修改日期 (最旧优先)"
|
||||||
|
ar = "تاريخ التعديل (الأقدم أولاً)"
|
||||||
|
kz = "Өзгертілген күні (Ескілері бірінші)"
|
||||||
|
he = "תאריך שינוי (הישן ביותר ראשון)"
|
||||||
|
pt-BR = "Data de modificação (Mais antigo primeiro)"
|
||||||
|
|
||||||
[gui.context.files.play_solo]
|
[gui.context.files.play_solo]
|
||||||
en = "Play Solo"
|
en = "Play Solo"
|
||||||
ru = "Играть"
|
ru = "Играть"
|
||||||
@@ -125,6 +180,17 @@ kz = "Ыстық пернені тағайындау"
|
|||||||
he = "הקצה מקש קיצור"
|
he = "הקצה מקש קיצור"
|
||||||
pt-BR = "Definir tecla de atalho"
|
pt-BR = "Definir tecla de atalho"
|
||||||
|
|
||||||
|
[gui.context.files.copy_cli_command]
|
||||||
|
en = "Copy PWSP-CLI command"
|
||||||
|
ru = "Скопировать команду для PWSP-CLI"
|
||||||
|
es = "Copiar comando de PWSP-CLI"
|
||||||
|
fr = "Copier la commande PWSP-CLI"
|
||||||
|
zh = "复制 PWSP-CLI 命令"
|
||||||
|
ar = "نسخ أمر PWSP-CLI"
|
||||||
|
kz = "PWSP-CLI командасын көшіру"
|
||||||
|
he = "העתק פקודת PWSP-CLI"
|
||||||
|
pt-BR = "Copiar comando PWSP-CLI"
|
||||||
|
|
||||||
# ----------------
|
# ----------------
|
||||||
# Settings
|
# Settings
|
||||||
# ----------------
|
# ----------------
|
||||||
|
|||||||
+22
-1
@@ -159,6 +159,13 @@ impl SoundpadGui {
|
|||||||
|
|
||||||
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
|
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
|
||||||
let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
|
let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
|
||||||
|
let sort_order = self
|
||||||
|
.app_state
|
||||||
|
.current_dir
|
||||||
|
.as_ref()
|
||||||
|
.map(|d| self.config.get_sort_order(d))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
files.sort_by(|a, b| {
|
files.sort_by(|a, b| {
|
||||||
let a_is_dir = a.is_dir();
|
let a_is_dir = a.is_dir();
|
||||||
let b_is_dir = b.is_dir();
|
let b_is_dir = b.is_dir();
|
||||||
@@ -167,7 +174,7 @@ impl SoundpadGui {
|
|||||||
} else if !a_is_dir && b_is_dir {
|
} else if !a_is_dir && b_is_dir {
|
||||||
Ordering::Greater
|
Ordering::Greater
|
||||||
} else {
|
} else {
|
||||||
a.cmp(b)
|
sort_order.compare(a, b)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -334,5 +341,19 @@ mod tests {
|
|||||||
let filtered_search = gui.get_filtered_files();
|
let filtered_search = gui.get_filtered_files();
|
||||||
assert_eq!(filtered_search.len(), 1);
|
assert_eq!(filtered_search.len(), 1);
|
||||||
assert_eq!(filtered_search[0], file_c);
|
assert_eq!(filtered_search[0], file_c);
|
||||||
|
|
||||||
|
// Test sort order descending
|
||||||
|
gui.app_state.current_dir = Some(PathBuf::from("dummy_dir"));
|
||||||
|
gui.config.dirs_settings.insert(
|
||||||
|
PathBuf::from("dummy_dir"),
|
||||||
|
pwsp_lib::types::config::DirSettings {
|
||||||
|
sort_order: pwsp_lib::types::config::SortOrder::AlphabeticalDesc,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
gui.app_state.search_query = String::new();
|
||||||
|
let filtered_desc = gui.get_filtered_files();
|
||||||
|
assert_eq!(filtered_desc.len(), 2);
|
||||||
|
assert_eq!(filtered_desc[0], file_c);
|
||||||
|
assert_eq!(filtered_desc[1], file_b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ use egui::{
|
|||||||
};
|
};
|
||||||
use egui_dnd::dnd;
|
use egui_dnd::dnd;
|
||||||
use egui_material_icons::icons::*;
|
use egui_material_icons::icons::*;
|
||||||
use pwsp_lib::types::{gui::AppState, gui::AudioPlayerState};
|
use pwsp_lib::types::{
|
||||||
|
config::{GuiConfig, SortOrder},
|
||||||
|
gui::{AppState, AudioPlayerState},
|
||||||
|
};
|
||||||
use rust_i18n::t;
|
use rust_i18n::t;
|
||||||
use std::{cmp::Ordering, path::Path, path::PathBuf};
|
use std::{cmp::Ordering, path::Path, path::PathBuf};
|
||||||
|
|
||||||
@@ -135,6 +138,65 @@ impl SoundpadGui {
|
|||||||
{
|
{
|
||||||
self.app_state.dirs_to_remove.insert(path.clone());
|
self.app_state.dirs_to_remove.insert(path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.label(t!("gui.context.dirs.sort_by"));
|
||||||
|
|
||||||
|
let current_order = self
|
||||||
|
.config
|
||||||
|
.dirs_settings
|
||||||
|
.get(path)
|
||||||
|
.map(|s| s.sort_order)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut new_order = None;
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.radio(
|
||||||
|
current_order == SortOrder::AlphabeticalAsc,
|
||||||
|
t!("gui.sort.alpha_asc"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
new_order = Some(SortOrder::AlphabeticalAsc);
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.radio(
|
||||||
|
current_order == SortOrder::AlphabeticalDesc,
|
||||||
|
t!("gui.sort.alpha_desc"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
new_order = Some(SortOrder::AlphabeticalDesc);
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.radio(
|
||||||
|
current_order == SortOrder::DateModifiedNewest,
|
||||||
|
t!("gui.sort.date_newest"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
new_order = Some(SortOrder::DateModifiedNewest);
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.radio(
|
||||||
|
current_order == SortOrder::DateModifiedOldest,
|
||||||
|
t!("gui.sort.date_oldest"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
new_order = Some(SortOrder::DateModifiedOldest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(order) = new_order {
|
||||||
|
self.config
|
||||||
|
.dirs_settings
|
||||||
|
.entry(path.clone())
|
||||||
|
.or_default()
|
||||||
|
.sort_order = order;
|
||||||
|
self.config.save_to_file().ok();
|
||||||
|
self.app_state.dir_cache.remove(path);
|
||||||
|
self.open_dir(path);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -192,6 +254,7 @@ impl SoundpadGui {
|
|||||||
Self::draw_tree_node(
|
Self::draw_tree_node(
|
||||||
ui,
|
ui,
|
||||||
entry_path,
|
entry_path,
|
||||||
|
&self.config,
|
||||||
&mut self.app_state,
|
&mut self.app_state,
|
||||||
&self.audio_player_state,
|
&self.audio_player_state,
|
||||||
&mut actions,
|
&mut actions,
|
||||||
@@ -226,6 +289,7 @@ impl SoundpadGui {
|
|||||||
fn draw_tree_node_dir(
|
fn draw_tree_node_dir(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
|
config: &GuiConfig,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
audio_player_state: &AudioPlayerState,
|
audio_player_state: &AudioPlayerState,
|
||||||
actions: &mut Vec<FileAction>,
|
actions: &mut Vec<FileAction>,
|
||||||
@@ -247,6 +311,7 @@ impl SoundpadGui {
|
|||||||
read.push(entry.path());
|
read.push(entry.path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let sort_order = config.get_sort_order(&path);
|
||||||
read.sort_by(|a, b| {
|
read.sort_by(|a, b| {
|
||||||
let a_is_dir = a.is_dir();
|
let a_is_dir = a.is_dir();
|
||||||
let b_is_dir = b.is_dir();
|
let b_is_dir = b.is_dir();
|
||||||
@@ -255,7 +320,7 @@ impl SoundpadGui {
|
|||||||
} else if !a_is_dir && b_is_dir {
|
} else if !a_is_dir && b_is_dir {
|
||||||
Ordering::Greater
|
Ordering::Greater
|
||||||
} else {
|
} else {
|
||||||
a.cmp(b)
|
sort_order.compare(a, b)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app_state.dir_cache.insert(path.clone(), read.clone());
|
app_state.dir_cache.insert(path.clone(), read.clone());
|
||||||
@@ -287,7 +352,7 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::draw_tree_node(ui, child, app_state, audio_player_state, actions);
|
Self::draw_tree_node(ui, child, config, app_state, audio_player_state, actions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -412,6 +477,24 @@ impl SoundpadGui {
|
|||||||
actions.push(FileAction::AssignHotkey(path.clone()));
|
actions.push(FileAction::AssignHotkey(path.clone()));
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_FILE_COPY.codepoint,
|
||||||
|
t!("gui.context.files.copy_cli_command")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
ui.ctx().copy_text(format!(
|
||||||
|
"pwsp-cli action play \"{}\"",
|
||||||
|
path.to_string_lossy()
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('"', "\\\"")
|
||||||
|
));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -419,12 +502,13 @@ impl SoundpadGui {
|
|||||||
fn draw_tree_node(
|
fn draw_tree_node(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
|
config: &GuiConfig,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
audio_player_state: &AudioPlayerState,
|
audio_player_state: &AudioPlayerState,
|
||||||
actions: &mut Vec<FileAction>,
|
actions: &mut Vec<FileAction>,
|
||||||
) {
|
) {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
Self::draw_tree_node_dir(ui, path, app_state, audio_player_state, actions);
|
Self::draw_tree_node_dir(ui, path, config, app_state, audio_player_state, actions);
|
||||||
} else {
|
} else {
|
||||||
Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions);
|
Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
types::pipewire::{DeviceType, Terminate},
|
types::pipewire::DeviceType,
|
||||||
utils::{
|
utils::{
|
||||||
daemon::get_daemon_config,
|
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};
|
use anyhow::{Result, anyhow};
|
||||||
@@ -58,8 +58,8 @@ pub struct AudioPlayer {
|
|||||||
pub tracks: HashMap<u32, PlayingSound>,
|
pub tracks: HashMap<u32, PlayingSound>,
|
||||||
pub next_id: u32,
|
pub next_id: u32,
|
||||||
|
|
||||||
input_link_sender: Option<pipewire::channel::Sender<Terminate>>,
|
input_link_sender: Option<PwTerminator>,
|
||||||
player_link_sender: Option<pipewire::channel::Sender<Terminate>>,
|
player_link_sender: Option<PwTerminator>,
|
||||||
pub input_device_name: Option<String>,
|
pub input_device_name: Option<String>,
|
||||||
|
|
||||||
pub volume: f32, // Master volume
|
pub volume: f32, // Master volume
|
||||||
@@ -108,24 +108,16 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn abort_link_thread(&mut self) {
|
fn abort_link_thread(&mut self) {
|
||||||
if let Some(sender) = &self.input_link_sender {
|
if self.input_link_sender.is_some() {
|
||||||
if sender.send(Terminate {}).is_ok() {
|
|
||||||
println!("Sent terminate signal to input link thread");
|
println!("Sent terminate signal to input link thread");
|
||||||
self.input_link_sender = None;
|
self.input_link_sender = None;
|
||||||
} else {
|
|
||||||
eprintln!("Failed to send terminate signal to input link thread");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn abort_player_link_thread(&mut self) {
|
fn abort_player_link_thread(&mut self) {
|
||||||
if let Some(sender) = &self.player_link_sender {
|
if self.player_link_sender.is_some() {
|
||||||
if sender.send(Terminate {}).is_ok() {
|
|
||||||
println!("Sent terminate signal to player link thread");
|
println!("Sent terminate signal to player link thread");
|
||||||
self.player_link_sender = None;
|
self.player_link_sender = None;
|
||||||
} else {
|
|
||||||
eprintln!("Failed to send terminate signal to player link thread");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +179,7 @@ impl AudioPlayer {
|
|||||||
return Ok(());
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, fs, path::PathBuf};
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
collections::HashMap,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize)]
|
#[derive(Default, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -45,6 +51,21 @@ pub enum PreferredTheme {
|
|||||||
Dark,
|
Dark,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum SortOrder {
|
||||||
|
#[default]
|
||||||
|
AlphabeticalAsc,
|
||||||
|
AlphabeticalDesc,
|
||||||
|
DateModifiedNewest,
|
||||||
|
DateModifiedOldest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct DirSettings {
|
||||||
|
pub sort_order: SortOrder,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct GuiConfig {
|
pub struct GuiConfig {
|
||||||
@@ -57,10 +78,38 @@ pub struct GuiConfig {
|
|||||||
pub pause_on_exit: bool,
|
pub pause_on_exit: bool,
|
||||||
|
|
||||||
pub dirs: Vec<PathBuf>,
|
pub dirs: Vec<PathBuf>,
|
||||||
|
pub dirs_settings: HashMap<PathBuf, DirSettings>,
|
||||||
|
|
||||||
pub preferred_theme: PreferredTheme,
|
pub preferred_theme: PreferredTheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SortOrder {
|
||||||
|
pub fn compare(&self, a: &Path, b: &Path) -> Ordering {
|
||||||
|
match self {
|
||||||
|
SortOrder::AlphabeticalAsc => a.cmp(b),
|
||||||
|
SortOrder::AlphabeticalDesc => b.cmp(a),
|
||||||
|
SortOrder::DateModifiedNewest => {
|
||||||
|
let a_time = fs::metadata(a)
|
||||||
|
.and_then(|m| m.modified())
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
let b_time = fs::metadata(b)
|
||||||
|
.and_then(|m| m.modified())
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
b_time.cmp(&a_time)
|
||||||
|
}
|
||||||
|
SortOrder::DateModifiedOldest => {
|
||||||
|
let a_time = fs::metadata(a)
|
||||||
|
.and_then(|m| m.modified())
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
let b_time = fs::metadata(b)
|
||||||
|
.and_then(|m| m.modified())
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
a_time.cmp(&b_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for GuiConfig {
|
impl Default for GuiConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
GuiConfig {
|
GuiConfig {
|
||||||
@@ -75,11 +124,23 @@ impl Default for GuiConfig {
|
|||||||
dirs: vec![ensure_pwsp_audio_dir()],
|
dirs: vec![ensure_pwsp_audio_dir()],
|
||||||
|
|
||||||
preferred_theme: PreferredTheme::System,
|
preferred_theme: PreferredTheme::System,
|
||||||
|
dirs_settings: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GuiConfig {
|
impl GuiConfig {
|
||||||
|
pub fn get_sort_order(&self, path: &Path) -> SortOrder {
|
||||||
|
let mut current = Some(path);
|
||||||
|
while let Some(p) = current {
|
||||||
|
if let Some(settings) = self.dirs_settings.get(p) {
|
||||||
|
return settings.sort_order;
|
||||||
|
}
|
||||||
|
current = p.parent();
|
||||||
|
}
|
||||||
|
SortOrder::default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save_to_file(&mut self) -> Result<()> {
|
pub fn save_to_file(&mut self) -> Result<()> {
|
||||||
let config_path = get_config_path()?.join("gui.json");
|
let config_path = get_config_path()?.join("gui.json");
|
||||||
|
|
||||||
|
|||||||
+276
-252
@@ -1,14 +1,224 @@
|
|||||||
use crate::types::pipewire::{AudioDevice, DeviceType, Port, Terminate};
|
use crate::types::pipewire::{AudioDevice, DeviceType, Port};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use pipewire::{
|
use pipewire::{
|
||||||
context::ContextRc, link::Link, main_loop::MainLoopRc, properties::properties,
|
context::ContextRc, link::Link, main_loop::MainLoopRc, properties::properties,
|
||||||
registry::GlobalObject, spa::utils::dict::DictRef,
|
registry::GlobalObject, spa::utils::dict::DictRef,
|
||||||
};
|
};
|
||||||
use std::{collections::HashMap, thread};
|
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::OnceLock, thread};
|
||||||
use tokio::{
|
use tokio::sync::oneshot;
|
||||||
sync::mpsc,
|
|
||||||
time::{Duration, timeout},
|
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> {
|
pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
|
||||||
pipewire::init();
|
pipewire::init();
|
||||||
@@ -71,127 +281,17 @@ fn parse_global_object(
|
|||||||
(None, None)
|
(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>)> {
|
pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>)> {
|
||||||
// Channels to communicate with pipewire thread
|
let (tx, rx) = oneshot::channel();
|
||||||
let (main_sender, mut main_receiver) = mpsc::channel(10);
|
let manager = get_manager();
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel();
|
manager
|
||||||
let (init_sender, init_receiver) = tokio::sync::oneshot::channel();
|
.sender
|
||||||
|
.send(PwCommand::GetDevices { resp: tx })
|
||||||
// Spawn pipewire thread in background
|
.map_err(|_| anyhow!("Failed to send GetDevices to manager"))?;
|
||||||
let _pw_thread = tokio::spawn(async move {
|
let res = rx
|
||||||
pw_get_global_objects_thread(main_sender, pw_receiver, init_sender).await
|
.await
|
||||||
});
|
.map_err(|e| anyhow!("Failed to receive response: {}", e))?;
|
||||||
|
Ok(res)
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_device(device_name: &str) -> Result<AudioDevice> {
|
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"))
|
.ok_or_else(|| anyhow!("Device not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>> {
|
pub struct PwTerminator {
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
ids: Vec<u32>,
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(pw_sender)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn link_player_to_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>> {
|
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 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 {
|
let pwsp_daemon_output = match get_device("pwsp-daemon").await {
|
||||||
Ok(device) => device,
|
Ok(device) => device,
|
||||||
Err(_) => {
|
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")),
|
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_fl: Port,
|
||||||
output_fr: Port,
|
output_fr: Port,
|
||||||
input_fl: Port,
|
input_fl: Port,
|
||||||
input_fr: Port,
|
input_fr: Port,
|
||||||
) -> Result<pipewire::channel::Sender<Terminate>> {
|
) -> Result<PwTerminator> {
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
let (tx, rx) = oneshot::channel();
|
||||||
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
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 res = rx
|
||||||
let (main_loop, context) = match setup_pipewire_context() {
|
.await
|
||||||
Ok(res) => res,
|
.map_err(|e| anyhow!("Failed to receive response: {}", e))?;
|
||||||
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_fl = properties! {
|
let (id_fl, id_fr) = res.map_err(|e| anyhow!(e))?;
|
||||||
"link.output.node" => format!("{}", output_fl.node_id).as_str(),
|
Ok(PwTerminator {
|
||||||
"link.output.port" => format!("{}", output_fl.port_id).as_str(),
|
ids: vec![id_fl, id_fr],
|
||||||
"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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user