mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-04-28 06:21:23 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4b0b10393 | |||
| 11de96db58 | |||
| 7396c0aef8 | |||
| fc2cd5e2da | |||
| 1a37729cf1 | |||
| 86b38a250e | |||
| 54fa278cea | |||
| db040aa820 | |||
| 04449e7525 | |||
| 9f50809a99 | |||
| 7dda4bc2b1 | |||
| 1569955e12 |
@@ -0,0 +1,88 @@
|
|||||||
|
name: Flatter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: "TAG (empty to build from current branch)"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
build_branch:
|
||||||
|
description: "Flatpak branch to build (stable/nightly)"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- stable
|
||||||
|
- nightly
|
||||||
|
default: "stable"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
flatter:
|
||||||
|
name: Flatter
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
container:
|
||||||
|
image: ghcr.io/andyholmes/flatter/freedesktop:25.08
|
||||||
|
options: --privileged
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.tag_name || github.ref }}
|
||||||
|
|
||||||
|
- name: Setup GPG
|
||||||
|
id: gpg
|
||||||
|
uses: crazy-max/ghaction-import-gpg@v6
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
|
||||||
|
- name: Set Default Branch
|
||||||
|
id: set_branch
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "release" ]; then
|
||||||
|
echo "branch=stable" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
echo "branch=${{ inputs.build_branch }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "branch=nightly" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Modify Manifest
|
||||||
|
run: |
|
||||||
|
echo "branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
|
||||||
|
echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
|
||||||
|
|
||||||
|
- name: Install SDK Extensions
|
||||||
|
run: flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
|
||||||
|
org.freedesktop.Sdk.Extension.llvm20//25.08
|
||||||
|
|
||||||
|
- name: Build Flatpak
|
||||||
|
uses: andyholmes/flatter@main
|
||||||
|
with:
|
||||||
|
files: packages/flatpak/ru.arabianq.pwsp.yaml
|
||||||
|
gpg-sign: ${{ steps.gpg.outputs.fingerprint }}
|
||||||
|
upload-bundles: false
|
||||||
|
upload-pages-artifact: true
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy to GitHub Pages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: flatter
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -174,10 +174,3 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
branch: master
|
branch: master
|
||||||
build-bundle: true
|
build-bundle: true
|
||||||
|
|
||||||
- name: Upload Flatpak to release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
tag_name: ${{ needs.prepare.outputs.tag }}
|
|
||||||
files: ru.arabianq.pwsp.flatpak
|
|
||||||
|
|||||||
Generated
+38
-13
@@ -601,6 +601,15 @@ dependencies = [
|
|||||||
"error-code",
|
"error-code",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmake"
|
||||||
|
version = "0.1.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codespan-reporting"
|
name = "codespan-reporting"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
@@ -729,11 +738,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpal"
|
name = "cpal"
|
||||||
version = "0.17.3"
|
version = "0.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/RustAudio/cpal#e5d618c625e978a105c57edf12f4738c65056cef"
|
||||||
checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa",
|
"alsa",
|
||||||
|
"block2 0.6.2",
|
||||||
"coreaudio-rs",
|
"coreaudio-rs",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
@@ -751,8 +760,6 @@ dependencies = [
|
|||||||
"objc2-core-audio-types",
|
"objc2-core-audio-types",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"windows",
|
"windows",
|
||||||
]
|
]
|
||||||
@@ -2063,12 +2070,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mach2"
|
name = "mach2"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
@@ -2420,6 +2424,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
]
|
]
|
||||||
@@ -2705,6 +2710,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opusic-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc3280fe5b6f97ac1a35a0ac003e2fb0b92f8e4bdf2b2057e1bf9b87acca5696"
|
||||||
|
dependencies = [
|
||||||
|
"cmake",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orbclient"
|
name = "orbclient"
|
||||||
version = "0.3.53"
|
version = "0.3.53"
|
||||||
@@ -3009,7 +3023,7 @@ checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pwsp"
|
name = "pwsp"
|
||||||
version = "1.7.4"
|
version = "1.7.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3235,13 +3249,13 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rodio"
|
name = "rodio"
|
||||||
version = "0.22.2"
|
version = "0.22.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/RustAudio/rodio.git?rev=57ad9d8a9f30398f634fbf8e4e1d53dde7243c21#57ad9d8a9f30398f634fbf8e4e1d53dde7243c21"
|
||||||
checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpal",
|
"cpal",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
"num-rational",
|
"num-rational",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
"symphonia-adapter-libopus",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3598,6 +3612,17 @@ dependencies = [
|
|||||||
"symphonia-metadata",
|
"symphonia-metadata",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-adapter-libopus"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d17450685dda0e87467eddf3e0f9c0b2a1707fc5c3234c111f70d46c6e4494"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"opusic-sys",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "symphonia-bundle-flac"
|
name = "symphonia-bundle-flac"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pwsp"
|
name = "pwsp"
|
||||||
version = "1.7.4"
|
version = "1.7.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["arabian"]
|
authors = ["arabian"]
|
||||||
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
|
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
|
||||||
@@ -29,8 +29,9 @@ clap = { version = "4.6.1", default-features = false, features = [
|
|||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
|
||||||
rodio = { version = "0.22.2", default-features = false, features = [
|
rodio = { git = "https://github.com/RustAudio/rodio.git", rev = "57ad9d8a9f30398f634fbf8e4e1d53dde7243c21", default-features = false, features = [
|
||||||
"symphonia-all",
|
"symphonia-all",
|
||||||
|
"symphonia-libopus",
|
||||||
"playback",
|
"playback",
|
||||||
] }
|
] }
|
||||||
pipewire = "0.9.2"
|
pipewire = "0.9.2"
|
||||||
|
|||||||
@@ -52,6 +52,28 @@ three main components:
|
|||||||
You can download pre-built binaries and .deb packages from
|
You can download pre-built binaries and .deb packages from
|
||||||
the [releases page](https://github.com/arabianq/pipewire-soundpad/releases).
|
the [releases page](https://github.com/arabianq/pipewire-soundpad/releases).
|
||||||
|
|
||||||
|
## **Flatpak**
|
||||||
|
|
||||||
|
You can install PWSP via Flatpak from our custom repository hosted on GitHub Pages.
|
||||||
|
|
||||||
|
Add the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flatpak remote-add --user --if-not-exists arabianq-repo https://arabianq.github.io/pipewire-soundpad/index.flatpakrepo
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the stable version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flatpak install --user arabianq-repo ru.arabianq.pwsp//stable
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install the nightly version (latest commit to `main`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flatpak install --user arabianq-repo ru.arabianq.pwsp//nightly
|
||||||
|
```
|
||||||
|
|
||||||
## **Fedora Linux (and derivatives)**
|
## **Fedora Linux (and derivatives)**
|
||||||
|
|
||||||
If you're using Fedora, you can install PWSP from a dedicated repository using DNF.
|
If you're using Fedora, you can install PWSP from a dedicated repository using DNF.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = pwsp-bin
|
pkgbase = pwsp-bin
|
||||||
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
|
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
|
||||||
pkgver = 1.7.4
|
pkgver = 1.7.5
|
||||||
pkgrel = 2
|
pkgrel = 2
|
||||||
url = https://github.com/arabianq/pipewire-soundpad
|
url = https://github.com/arabianq/pipewire-soundpad
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
@@ -9,8 +9,8 @@ depends = pipewire
|
|||||||
depends = alsa-lib
|
depends = alsa-lib
|
||||||
provides = pwsp
|
provides = pwsp
|
||||||
conflicts = pwsp
|
conflicts = pwsp
|
||||||
source = pwsp-bin-1.7.4.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.7.4/pwsp-v1.7.4-linux-x64.zip
|
source = pwsp-bin-1.7.5.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.7.5/pwsp-v1.7.5-linux-x64.zip
|
||||||
source = pipewire-soundpad-1.7.4.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.4.tar.gz
|
source = pipewire-soundpad-1.7.5.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.5.tar.gz
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
|
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
|
||||||
pkgname=pwsp-bin
|
pkgname=pwsp-bin
|
||||||
_pkgname=pipewire-soundpad
|
_pkgname=pipewire-soundpad
|
||||||
pkgver=1.7.4
|
pkgver=1.7.5
|
||||||
pkgrel=2
|
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')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = pwsp
|
pkgbase = pwsp
|
||||||
pkgdesc = Lets you play audio files through your microphone
|
pkgdesc = Lets you play audio files through your microphone
|
||||||
pkgver = 1.7.4
|
pkgver = 1.7.5
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/arabianq/pipewire-soundpad
|
url = https://github.com/arabianq/pipewire-soundpad
|
||||||
arch = any
|
arch = any
|
||||||
@@ -10,7 +10,7 @@ pkgbase = pwsp
|
|||||||
makedepends = cargo
|
makedepends = cargo
|
||||||
makedepends = pipewire
|
makedepends = pipewire
|
||||||
makedepends = alsa-lib
|
makedepends = alsa-lib
|
||||||
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.4.tar.gz
|
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.5.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.7.4
|
pkgver=1.7.5
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Lets you play audio files through your microphone"
|
pkgdesc="Lets you play audio files through your microphone"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@
|
|||||||
<name>arabian</name>
|
<name>arabian</name>
|
||||||
</developer>
|
</developer>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="1.7.4" date="2026-04-25" />
|
<release version="1.7.5" date="2026-04-26" />
|
||||||
</releases>
|
</releases>
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
</component>
|
</component>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
%global cargo_install_lib 0
|
%global cargo_install_lib 0
|
||||||
|
|
||||||
Name: pwsp
|
Name: pwsp
|
||||||
Version: 1.7.4
|
Version: 1.7.5
|
||||||
Release: %autorelease
|
Release: %autorelease
|
||||||
Summary: Lets you play audio files through your microphone
|
Summary: Lets you play audio files through your microphone
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ BuildRequires: cargo
|
|||||||
BuildRequires: pipewire-devel
|
BuildRequires: pipewire-devel
|
||||||
BuildRequires: alsa-lib-devel
|
BuildRequires: alsa-lib-devel
|
||||||
BuildRequires: clang-devel
|
BuildRequires: clang-devel
|
||||||
|
BuildRequires: cmake
|
||||||
|
|
||||||
%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
|
||||||
|
|||||||
+125
-95
@@ -137,7 +137,22 @@ impl SoundpadGui {
|
|||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.spacing_mut().item_spacing.y = 5.0;
|
ui.spacing_mut().item_spacing.y = 5.0;
|
||||||
|
|
||||||
// --- Header ---
|
self.draw_hotkeys_header(ui);
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
self.draw_hotkeys_search(ui);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
|
let action = self.draw_hotkeys_table(ui);
|
||||||
|
|
||||||
|
if let Some(action) = action {
|
||||||
|
self.handle_hotkey_action(action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_header(&mut self, ui: &mut Ui) {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
||||||
if ui.add(back_button).clicked() {
|
if ui.add(back_button).clicked() {
|
||||||
@@ -148,10 +163,9 @@ impl SoundpadGui {
|
|||||||
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
|
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ui.separator();
|
fn draw_hotkeys_search(&mut self, ui: &mut Ui) {
|
||||||
|
|
||||||
// --- Search and Add Command ---
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| {
|
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| {
|
||||||
let mut selected_cmd = None;
|
let mut selected_cmd = None;
|
||||||
@@ -190,10 +204,9 @@ impl SoundpadGui {
|
|||||||
.desired_width(f32::INFINITY),
|
.desired_width(f32::INFINITY),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ui.separator();
|
fn draw_hotkeys_table(&mut self, ui: &mut Ui) -> Option<HotkeyAction> {
|
||||||
ui.add_space(5.0);
|
|
||||||
|
|
||||||
let conflicts = self.app_state.hotkey_config.find_conflicts();
|
let conflicts = self.app_state.hotkey_config.find_conflicts();
|
||||||
let conflict_slots: std::collections::HashSet<&str> =
|
let conflict_slots: std::collections::HashSet<&str> =
|
||||||
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
|
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
|
||||||
@@ -380,7 +393,10 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(action) = action {
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_hotkey_action(&mut self, action: HotkeyAction) {
|
||||||
match action {
|
match action {
|
||||||
HotkeyAction::Remove(slot) => {
|
HotkeyAction::Remove(slot) => {
|
||||||
make_request_async(Request::clear_hotkey(&slot));
|
make_request_async(Request::clear_hotkey(&slot));
|
||||||
@@ -399,8 +415,6 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_header(&mut self, ui: &mut Ui) {
|
fn draw_header(&mut self, ui: &mut Ui) {
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
@@ -409,10 +423,9 @@ impl SoundpadGui {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tracks = self.audio_player_state.tracks.clone();
|
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
|
|
||||||
for track in tracks {
|
for track in &self.audio_player_state.tracks {
|
||||||
CollapsingHeader::new(
|
CollapsingHeader::new(
|
||||||
RichText::new(
|
RichText::new(
|
||||||
track
|
track
|
||||||
@@ -445,6 +458,99 @@ impl SoundpadGui {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_playback_controls(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
let play_button = Button::new(if track.paused {
|
||||||
|
ICON_PLAY_ARROW
|
||||||
|
} else {
|
||||||
|
ICON_PAUSE
|
||||||
|
})
|
||||||
|
.corner_radius(15.0);
|
||||||
|
|
||||||
|
if ui.add_sized([30.0, 30.0], play_button).clicked() {
|
||||||
|
action = Some(if track.paused {
|
||||||
|
TrackAction::Resume(track.id)
|
||||||
|
} else {
|
||||||
|
TrackAction::Pause(track.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let loop_button = Button::new(
|
||||||
|
RichText::new(if track.looped {
|
||||||
|
ICON_REPEAT_ONE
|
||||||
|
} else {
|
||||||
|
ICON_REPEAT
|
||||||
|
})
|
||||||
|
.size(18.0),
|
||||||
|
)
|
||||||
|
.frame(false);
|
||||||
|
|
||||||
|
if ui.add_sized([15.0, 30.0], loop_button).clicked() {
|
||||||
|
action = Some(TrackAction::ToggleLoop(track.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_position_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ui_state: &mut pwsp::types::gui::TrackUiState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
default_slider_width: f32,
|
||||||
|
) {
|
||||||
|
let duration = track.duration.unwrap_or(1.0);
|
||||||
|
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
|
||||||
|
let position_slider_width = ui.available_width()
|
||||||
|
- (30.0 * 3.0)
|
||||||
|
- default_slider_width
|
||||||
|
- (ui.spacing().item_spacing.x * 6.0);
|
||||||
|
|
||||||
|
ui.spacing_mut().slider_width = position_slider_width;
|
||||||
|
if ui.add_sized([30.0, 30.0], position_slider).drag_stopped() {
|
||||||
|
ui_state.position_dragged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_label =
|
||||||
|
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
|
||||||
|
ui.add_sized([30.0, 30.0], time_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_volume_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ui_state: &mut pwsp::types::gui::TrackUiState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
default_slider_width: f32,
|
||||||
|
) {
|
||||||
|
let volume_icon = Self::get_volume_icon(track.volume);
|
||||||
|
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
||||||
|
ui.add_sized([30.0, 30.0], volume_label)
|
||||||
|
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
|
||||||
|
|
||||||
|
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
|
||||||
|
ui.spacing_mut().slider_width = default_slider_width - 30.0;
|
||||||
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
|
||||||
|
if ui.add_sized([30.0, 30.0], volume_slider).drag_stopped() {
|
||||||
|
ui_state.volume_dragged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_stop_control(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
||||||
|
let stop_button = Button::new(ICON_CLOSE).frame(false);
|
||||||
|
if ui.add_sized([30.0, 30.0], stop_button).clicked() {
|
||||||
|
Some(TrackAction::Stop(track.id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_track_control(
|
fn draw_track_control(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
@@ -475,93 +581,17 @@ impl SoundpadGui {
|
|||||||
let mut action = None;
|
let mut action = None;
|
||||||
|
|
||||||
ui.horizontal_top(|ui| {
|
ui.horizontal_top(|ui| {
|
||||||
// ---------- Play Button ----------
|
if let Some(act) = Self::draw_playback_controls(ui, track) {
|
||||||
let play_button = Button::new(if track.paused {
|
action = Some(act);
|
||||||
ICON_PLAY_ARROW
|
|
||||||
} else {
|
|
||||||
ICON_PAUSE
|
|
||||||
})
|
|
||||||
.corner_radius(15.0);
|
|
||||||
|
|
||||||
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
|
|
||||||
if play_button_response.clicked() {
|
|
||||||
if track.paused {
|
|
||||||
action = Some(TrackAction::Resume(track.id));
|
|
||||||
} else {
|
|
||||||
action = Some(TrackAction::Pause(track.id));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Loop Button ----------
|
|
||||||
let loop_button = Button::new(
|
|
||||||
RichText::new(if track.looped {
|
|
||||||
ICON_REPEAT_ONE
|
|
||||||
} else {
|
|
||||||
ICON_REPEAT
|
|
||||||
})
|
|
||||||
.size(18.0),
|
|
||||||
)
|
|
||||||
.frame(false);
|
|
||||||
|
|
||||||
let loop_button_response = ui.add_sized([15.0, 30.0], loop_button);
|
|
||||||
if loop_button_response.clicked() {
|
|
||||||
action = Some(TrackAction::ToggleLoop(track.id));
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Position Slider ----------
|
|
||||||
let duration = track.duration.unwrap_or(1.0);
|
|
||||||
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
|
|
||||||
.show_value(false)
|
|
||||||
.step_by(0.01);
|
|
||||||
|
|
||||||
let default_slider_width = ui.spacing().slider_width;
|
let default_slider_width = ui.spacing().slider_width;
|
||||||
let position_slider_width = ui.available_width()
|
Self::draw_position_control(ui, ui_state, track, default_slider_width);
|
||||||
- (30.0 * 3.0)
|
Self::draw_volume_control(ui, ui_state, track, default_slider_width);
|
||||||
- default_slider_width
|
|
||||||
- (ui.spacing().item_spacing.x * 6.0);
|
if let Some(act) = Self::draw_stop_control(ui, track) {
|
||||||
ui.spacing_mut().slider_width = position_slider_width;
|
action = Some(act);
|
||||||
let position_slider_response = ui.add_sized([30.0, 30.0], position_slider);
|
|
||||||
if position_slider_response.drag_stopped() {
|
|
||||||
ui_state.position_dragged = true;
|
|
||||||
}
|
}
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Time Label ----------
|
|
||||||
let time_label =
|
|
||||||
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
|
|
||||||
ui.add_sized([30.0, 30.0], time_label);
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Volume Icon ----------
|
|
||||||
let volume_icon = Self::get_volume_icon(track.volume);
|
|
||||||
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
|
||||||
ui.add_sized([30.0, 30.0], volume_label)
|
|
||||||
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Volume Slider ----------
|
|
||||||
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
|
|
||||||
.show_value(false)
|
|
||||||
.step_by(0.01);
|
|
||||||
|
|
||||||
ui.spacing_mut().slider_width = default_slider_width - 30.0;
|
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
|
||||||
|
|
||||||
let volume_slider_response = ui.add_sized([30.0, 30.0], volume_slider);
|
|
||||||
if volume_slider_response.drag_stopped() {
|
|
||||||
ui_state.volume_dragged = true;
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Stop Button ---------
|
|
||||||
let stop_button = Button::new(ICON_CLOSE).frame(false);
|
|
||||||
let stop_button_response = ui.add_sized([30.0, 30.0], stop_button);
|
|
||||||
if stop_button_response.clicked() {
|
|
||||||
action = Some(TrackAction::Stop(track.id));
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
});
|
});
|
||||||
|
|
||||||
action
|
action
|
||||||
|
|||||||
+8
-2
@@ -9,7 +9,10 @@ use std::path::PathBuf;
|
|||||||
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
|
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
|
||||||
let key_name = key.name();
|
let key_name = key.name();
|
||||||
let is_valid = (key_name.len() == 1
|
let is_valid = (key_name.len() == 1
|
||||||
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric()))
|
&& key_name
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_some_and(|c| c.is_ascii_alphanumeric()))
|
||||||
|| (key_name.starts_with('F')
|
|| (key_name.starts_with('F')
|
||||||
&& key_name.len() > 1
|
&& key_name.len() > 1
|
||||||
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
||||||
@@ -60,7 +63,10 @@ pub fn parse_chord(chord: &str) -> Option<(Modifiers, Key)> {
|
|||||||
|
|
||||||
let key_name = parts[parts.len() - 1];
|
let key_name = parts[parts.len() - 1];
|
||||||
let is_valid = (key_name.len() == 1
|
let is_valid = (key_name.len() == 1
|
||||||
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric()))
|
&& key_name
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_some_and(|c| c.is_ascii_alphanumeric()))
|
||||||
|| (key_name.starts_with('F')
|
|| (key_name.starts_with('F')
|
||||||
&& key_name.len() > 1
|
&& key_name.len() > 1
|
||||||
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUPPORTED_EXTENSIONS: [&str; 12] = [
|
const SUPPORTED_EXTENSIONS: [&str; 13] = [
|
||||||
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi",
|
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus",
|
||||||
];
|
];
|
||||||
|
|
||||||
struct SoundpadGui {
|
struct SoundpadGui {
|
||||||
|
|||||||
+112
-33
@@ -9,11 +9,11 @@ use tokio::{
|
|||||||
time::{Duration, timeout},
|
time::{Duration, timeout},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn setup_pipewire_context() -> (MainLoopRc, ContextRc) {
|
pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
|
||||||
pipewire::init();
|
pipewire::init();
|
||||||
let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop");
|
let main_loop = MainLoopRc::new(None).map_err(|e| e.to_string())?;
|
||||||
let context = ContextRc::new(&main_loop, None).expect("Failed to create pipewire context");
|
let context = ContextRc::new(&main_loop, None).map_err(|e| e.to_string())?;
|
||||||
(main_loop, context)
|
Ok((main_loop, context))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_global_object(
|
fn parse_global_object(
|
||||||
@@ -85,8 +85,15 @@ fn parse_global_object(
|
|||||||
async fn pw_get_global_objects_thread(
|
async fn pw_get_global_objects_thread(
|
||||||
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
|
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
|
||||||
pw_receiver: pipewire::channel::Receiver<Terminate>,
|
pw_receiver: pipewire::channel::Receiver<Terminate>,
|
||||||
|
init_sender: std::sync::mpsc::SyncSender<Result<(), String>>,
|
||||||
) {
|
) {
|
||||||
let (main_loop, context) = setup_pipewire_context();
|
let (main_loop, context) = match setup_pipewire_context() {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = init_sender.send(Err(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Stop main loop on Terminate message
|
// Stop main loop on Terminate message
|
||||||
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
||||||
@@ -94,12 +101,24 @@ async fn pw_get_global_objects_thread(
|
|||||||
move |_| _main_loop.quit()
|
move |_| _main_loop.quit()
|
||||||
});
|
});
|
||||||
|
|
||||||
let core = context
|
let core = match context.connect(None) {
|
||||||
.connect(None)
|
Ok(core) => core,
|
||||||
.expect("Failed to connect to pipewire context");
|
Err(e) => {
|
||||||
let registry = core
|
let _ = init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
|
||||||
.get_registry()
|
return;
|
||||||
.expect("Failed to get registry from pipewire context");
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let registry = match core.get_registry() {
|
||||||
|
Ok(registry) => registry,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = init_sender.send(Err(format!(
|
||||||
|
"Failed to get registry from pipewire context: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let _listener = registry
|
let _listener = registry
|
||||||
.add_listener_local()
|
.add_listener_local()
|
||||||
@@ -115,6 +134,11 @@ async fn pw_get_global_objects_thread(
|
|||||||
})
|
})
|
||||||
.register();
|
.register();
|
||||||
|
|
||||||
|
// Signal successful initialization
|
||||||
|
if init_sender.send(Ok(())).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
main_loop.run();
|
main_loop.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +146,17 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
// Channels to communicate with pipewire thread
|
// Channels to communicate with pipewire thread
|
||||||
let (main_sender, mut main_receiver) = mpsc::channel(10);
|
let (main_sender, mut main_receiver) = mpsc::channel(10);
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel();
|
||||||
|
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
||||||
|
|
||||||
// Spawn pipewire thread in background
|
// Spawn pipewire thread in background
|
||||||
let _pw_thread =
|
let _pw_thread = tokio::spawn(async move {
|
||||||
tokio::spawn(async move { pw_get_global_objects_thread(main_sender, pw_receiver).await });
|
pw_get_global_objects_thread(main_sender, pw_receiver, init_sender).await
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for initialization to complete
|
||||||
|
if let Err(e) = init_receiver.recv()? {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
||||||
let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
||||||
@@ -150,9 +181,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
}
|
}
|
||||||
Ok(None) | Err(_) => {
|
Ok(None) | Err(_) => {
|
||||||
// Pipewire thread is finished and we can collect our devices
|
// Pipewire thread is finished and we can collect our devices
|
||||||
pw_sender
|
let _ = pw_sender.send(Terminate {});
|
||||||
.send(Terminate {})
|
|
||||||
.expect("Failed to terminate pipewire thread");
|
|
||||||
|
|
||||||
for port in ports {
|
for port in ports {
|
||||||
let node_id = port.node_id;
|
let node_id = port.node_id;
|
||||||
@@ -226,12 +255,24 @@ pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>
|
|||||||
|
|
||||||
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
||||||
|
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
||||||
|
|
||||||
let _pw_thread = thread::spawn(move || {
|
let _pw_thread = thread::spawn(move || {
|
||||||
let (main_loop, context) = setup_pipewire_context();
|
let (main_loop, context) = match setup_pipewire_context() {
|
||||||
let core = context
|
Ok(res) => res,
|
||||||
.connect(None)
|
Err(e) => {
|
||||||
.expect("Failed to connect to pipewire context");
|
let _ = init_sender.send(Err(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let core = match context.connect(None) {
|
||||||
|
Ok(core) => core,
|
||||||
|
Err(e) => {
|
||||||
|
let _ =
|
||||||
|
init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let props = properties!(
|
let props = properties!(
|
||||||
"factory.name" => "support.null-audio-sink",
|
"factory.name" => "support.null-audio-sink",
|
||||||
@@ -243,9 +284,13 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
|
|||||||
"object.linger" => "false", // Destroy the node on app exit
|
"object.linger" => "false", // Destroy the node on app exit
|
||||||
);
|
);
|
||||||
|
|
||||||
let _node = core
|
let _node = match core.create_object::<pipewire::node::Node>("adapter", &props) {
|
||||||
.create_object::<pipewire::node::Node>("adapter", &props)
|
Ok(node) => node,
|
||||||
.expect("Failed to create virtual mic");
|
Err(e) => {
|
||||||
|
let _ = init_sender.send(Err(format!("Failed to create virtual mic: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
||||||
let _main_loop = main_loop.clone();
|
let _main_loop = main_loop.clone();
|
||||||
@@ -253,9 +298,16 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
|
|||||||
});
|
});
|
||||||
|
|
||||||
println!("Virtual mic created");
|
println!("Virtual mic created");
|
||||||
|
if init_sender.send(Ok(())).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
main_loop.run();
|
main_loop.run();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Err(e) = init_receiver.recv()? {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(pw_sender)
|
Ok(pw_sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,12 +356,24 @@ pub fn create_link(
|
|||||||
input_fr: Port,
|
input_fr: Port,
|
||||||
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
||||||
|
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
||||||
|
|
||||||
let _pw_thread = thread::spawn(move || {
|
let _pw_thread = thread::spawn(move || {
|
||||||
let (main_loop, context) = setup_pipewire_context();
|
let (main_loop, context) = match setup_pipewire_context() {
|
||||||
let core = context
|
Ok(res) => res,
|
||||||
.connect(None)
|
Err(e) => {
|
||||||
.expect("Failed to connect to pipewire context");
|
let _ = init_sender.send(Err(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let core = match context.connect(None) {
|
||||||
|
Ok(core) => core,
|
||||||
|
Err(e) => {
|
||||||
|
let _ =
|
||||||
|
init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let props_fl = properties! {
|
let props_fl = properties! {
|
||||||
"link.output.node" => format!("{}", output_fl.node_id).as_str(),
|
"link.output.node" => format!("{}", output_fl.node_id).as_str(),
|
||||||
@@ -324,12 +388,20 @@ pub fn create_link(
|
|||||||
"link.input.port" => format!("{}", input_fr.port_id).as_str(),
|
"link.input.port" => format!("{}", input_fr.port_id).as_str(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let _link_fl = core
|
let _link_fl = match core.create_object::<Link>("link-factory", &props_fl) {
|
||||||
.create_object::<Link>("link-factory", &props_fl)
|
Ok(link) => link,
|
||||||
.expect("Failed to create link FL");
|
Err(e) => {
|
||||||
let _link_fr = core
|
let _ = init_sender.send(Err(format!("Failed to create link FL: {}", e)));
|
||||||
.create_object::<Link>("link-factory", &props_fr)
|
return;
|
||||||
.expect("Failed to create link FR");
|
}
|
||||||
|
};
|
||||||
|
let _link_fr = match core.create_object::<Link>("link-factory", &props_fr) {
|
||||||
|
Ok(link) => link,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = init_sender.send(Err(format!("Failed to create link FR: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
||||||
let _main_loop = main_loop.clone();
|
let _main_loop = main_loop.clone();
|
||||||
@@ -340,8 +412,15 @@ pub fn create_link(
|
|||||||
"Link created: FL: {}-{} FR: {}-{}",
|
"Link created: FL: {}-{} FR: {}-{}",
|
||||||
output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id
|
output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id
|
||||||
);
|
);
|
||||||
|
if init_sender.send(Ok(())).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
main_loop.run();
|
main_loop.run();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Err(e) = init_receiver.recv()? {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(pw_sender)
|
Ok(pw_sender)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user