mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-06-19 12:13:32 +00:00
Compare commits
22 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 |
@@ -244,3 +244,18 @@ jobs:
|
||||
files: |
|
||||
./dist/pwsp-*.zip
|
||||
./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]]
|
||||
name = "cpal"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/RustAudio/cpal#d2a268839bf9ada7c2477c8a5f9483c28aa12d0c"
|
||||
source = "git+https://github.com/RustAudio/cpal#004897773f17fa15afbc3270b7cca37cfbbdef2a"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"block2 0.6.2",
|
||||
@@ -2306,9 +2306,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.31"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
|
||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
@@ -3280,7 +3280,7 @@ checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
||||
|
||||
[[package]]
|
||||
name = "pwsp-cli"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -3291,7 +3291,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pwsp-daemon"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -3303,7 +3303,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pwsp-gui"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"eframe",
|
||||
@@ -3327,7 +3327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pwsp-lib"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3686,7 +3686,7 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
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 = [
|
||||
"cpal",
|
||||
"dasp_sample",
|
||||
@@ -5967,9 +5967,9 @@ checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
edition = "2024"
|
||||
authors = ["arabian"]
|
||||
homepage = "https://pwsp.arabianq.ru"
|
||||
@@ -52,7 +52,7 @@ 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 = "a634dd471e9d59196e19bf01323fb45f2f899821", default-features = false, features = [
|
||||
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "c6a81b5a46e00a6a682c0c431dff62e86f57d819", default-features = false, features = [
|
||||
"symphonia-all",
|
||||
"symphonia-libopus",
|
||||
"playback",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pkgbase = pwsp-bin
|
||||
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
|
||||
pkgver = 1.11.0
|
||||
pkgrel = 1
|
||||
pkgver = 1.12.0
|
||||
pkgrel = 2
|
||||
url = https://github.com/arabianq/pipewire-soundpad
|
||||
arch = x86_64
|
||||
arch = aarch64
|
||||
@@ -10,11 +10,11 @@ pkgbase = pwsp-bin
|
||||
depends = alsa-lib
|
||||
provides = 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
|
||||
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
|
||||
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
|
||||
|
||||
pkgname = pwsp-bin
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
|
||||
pkgname=pwsp-bin
|
||||
_pkgname=pipewire-soundpad
|
||||
pkgver=1.11.0
|
||||
pkgrel=1
|
||||
pkgver=1.12.0
|
||||
pkgrel=2
|
||||
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
|
||||
arch=('x86_64' 'aarch64')
|
||||
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-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-gui/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
|
||||
|
||||
install -Dm644 "$_srcsrc/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgbase = pwsp
|
||||
pkgdesc = Lets you play audio files through your microphone
|
||||
pkgver = 1.11.0
|
||||
pkgver = 1.12.0
|
||||
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.11.0.tar.gz
|
||||
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.0.tar.gz
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = pwsp
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
|
||||
pkgsubn=pwsp
|
||||
pkgname=pwsp
|
||||
pkgver=1.11.0
|
||||
pkgver=1.12.0
|
||||
pkgrel=1
|
||||
pkgdesc="Lets you play audio files through your microphone"
|
||||
arch=('x86_64' 'aarch64')
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@
|
||||
<name>arabian</name>
|
||||
</developer>
|
||||
<releases>
|
||||
<release version="1.11.0" date="2026-06-02" />
|
||||
<release version="1.12.0" date="2026-06-04" />
|
||||
</releases>
|
||||
<content_rating type="oars-1.1" />
|
||||
</component>
|
||||
@@ -6,7 +6,7 @@
|
||||
%global debug_package %{nil}
|
||||
|
||||
Name: pwsp-git
|
||||
Version: {{{ git_dir_version }}}
|
||||
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)
|
||||
|
||||
@@ -14,16 +14,27 @@ License: MIT
|
||||
|
||||
URL: https://github.com/arabianq/pipewire-soundpad
|
||||
VCS: {{{ git_dir_vcs }}}
|
||||
Source: {{{ git_dir_pack }}}
|
||||
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: dbus-devel
|
||||
BuildRequires: pkgconf-pkg-config
|
||||
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}
|
||||
@@ -36,11 +47,17 @@ GUI clients. This is the latest development (git) version.}
|
||||
%description %{_description}
|
||||
|
||||
%prep
|
||||
{{{ git_dir_setup_macro }}}
|
||||
{{{ 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
|
||||
@@ -63,3 +80,4 @@ install -Dm644 pwsp-gui/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/
|
||||
|
||||
%changelog
|
||||
{{{ git_dir_changelog }}}
|
||||
|
||||
|
||||
+21
-4
@@ -3,7 +3,9 @@
|
||||
|
||||
# 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}\n- Release build}
|
||||
%{!?autochangelog: %global autochangelog \
|
||||
* Tue Jun 02 2026 Arabian <arabianq@github> - %{version}-%{release}\
|
||||
- Release build}
|
||||
|
||||
|
||||
# disable debuginfo package generation (debugsourcefiles.list is empty for Rust)
|
||||
@@ -11,7 +13,7 @@
|
||||
|
||||
|
||||
Name: pwsp
|
||||
Version: 1.11.0
|
||||
Version: 1.12.0
|
||||
Release: %autorelease
|
||||
Summary: Lets you play audio files through your microphone
|
||||
|
||||
@@ -23,11 +25,21 @@ Source: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags
|
||||
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: dbus-devel
|
||||
BuildRequires: pkgconf-pkg-config
|
||||
BuildRequires: pkgconfig
|
||||
%if 0%{?suse_version} && 0%{?suse_version} <= 1500
|
||||
BuildRequires: gcc13-c++
|
||||
%endif
|
||||
|
||||
|
||||
|
||||
%global _description %{expand:
|
||||
PWSP lets you play audio files through your microphone. Has both CLI and
|
||||
@@ -39,8 +51,13 @@ GUI clients.}
|
||||
%autosetup -n pipewire-soundpad-%{version} -p1
|
||||
|
||||
%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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,6 +70,61 @@ kz = "Жою"
|
||||
he = "הסר"
|
||||
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]
|
||||
en = "Play Solo"
|
||||
ru = "Играть"
|
||||
@@ -125,6 +180,17 @@ kz = "Ыстық пернені тағайындау"
|
||||
he = "הקצה מקש קיצור"
|
||||
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
|
||||
# ----------------
|
||||
|
||||
+22
-1
@@ -159,6 +159,13 @@ impl SoundpadGui {
|
||||
|
||||
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
|
||||
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| {
|
||||
let a_is_dir = a.is_dir();
|
||||
let b_is_dir = b.is_dir();
|
||||
@@ -167,7 +174,7 @@ impl SoundpadGui {
|
||||
} else if !a_is_dir && b_is_dir {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
a.cmp(b)
|
||||
sort_order.compare(a, b)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -334,5 +341,19 @@ mod tests {
|
||||
let filtered_search = gui.get_filtered_files();
|
||||
assert_eq!(filtered_search.len(), 1);
|
||||
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_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 std::{cmp::Ordering, path::Path, path::PathBuf};
|
||||
|
||||
@@ -135,6 +138,65 @@ impl SoundpadGui {
|
||||
{
|
||||
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(
|
||||
ui,
|
||||
entry_path,
|
||||
&self.config,
|
||||
&mut self.app_state,
|
||||
&self.audio_player_state,
|
||||
&mut actions,
|
||||
@@ -226,6 +289,7 @@ impl SoundpadGui {
|
||||
fn draw_tree_node_dir(
|
||||
ui: &mut Ui,
|
||||
path: std::path::PathBuf,
|
||||
config: &GuiConfig,
|
||||
app_state: &mut AppState,
|
||||
audio_player_state: &AudioPlayerState,
|
||||
actions: &mut Vec<FileAction>,
|
||||
@@ -247,6 +311,7 @@ impl SoundpadGui {
|
||||
read.push(entry.path());
|
||||
}
|
||||
}
|
||||
let sort_order = config.get_sort_order(&path);
|
||||
read.sort_by(|a, b| {
|
||||
let a_is_dir = a.is_dir();
|
||||
let b_is_dir = b.is_dir();
|
||||
@@ -255,7 +320,7 @@ impl SoundpadGui {
|
||||
} else if !a_is_dir && b_is_dir {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
a.cmp(b)
|
||||
sort_order.compare(a, b)
|
||||
}
|
||||
});
|
||||
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()));
|
||||
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(
|
||||
ui: &mut Ui,
|
||||
path: std::path::PathBuf,
|
||||
config: &GuiConfig,
|
||||
app_state: &mut AppState,
|
||||
audio_player_state: &AudioPlayerState,
|
||||
actions: &mut Vec<FileAction>,
|
||||
) {
|
||||
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 {
|
||||
Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions);
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
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)]
|
||||
#[serde(default)]
|
||||
@@ -45,6 +51,21 @@ pub enum PreferredTheme {
|
||||
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)]
|
||||
#[serde(default)]
|
||||
pub struct GuiConfig {
|
||||
@@ -57,10 +78,38 @@ pub struct GuiConfig {
|
||||
pub pause_on_exit: bool,
|
||||
|
||||
pub dirs: Vec<PathBuf>,
|
||||
pub dirs_settings: HashMap<PathBuf, DirSettings>,
|
||||
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
GuiConfig {
|
||||
@@ -75,11 +124,23 @@ impl Default for GuiConfig {
|
||||
dirs: vec![ensure_pwsp_audio_dir()],
|
||||
|
||||
preferred_theme: PreferredTheme::System,
|
||||
dirs_settings: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<()> {
|
||||
let config_path = get_config_path()?.join("gui.json");
|
||||
|
||||
|
||||
+273
-249
@@ -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],
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user