mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-04-28 14:31:23 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 949307fcf8 | |||
| 2a8fcca06b | |||
| 5c4b8f4b45 | |||
| 70c7e3789b | |||
| 5367a3daae | |||
| 42c0170044 | |||
| cb56cb3a04 |
Generated
+226
-230
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pwsp"
|
name = "pwsp"
|
||||||
version = "1.7.0"
|
version = "1.7.2"
|
||||||
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."
|
||||||
@@ -12,13 +12,13 @@ keywords = ["soundpad", "pipewire", "linux", "cli", "gui"]
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.51.1", features = ["full"] }
|
tokio = { version = "1.52.1", features = ["full"] }
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
|
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
|
||||||
clap = { version = "4.6.0", default-features = false, features = [
|
clap = { version = "4.6.1", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"suggestions",
|
"suggestions",
|
||||||
"help",
|
"help",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ chats on platforms like **Discord, Zoom, or Teamspeak**.
|
|||||||
* **Collapsible Audio Tracks**: You can collapse every audio track to save space.
|
* **Collapsible Audio Tracks**: You can collapse every audio track to save space.
|
||||||
* **Drag and Drop Directories**: Reorder your sound directories easily using drag and drop.
|
* **Drag and Drop Directories**: Reorder your sound directories easily using drag and drop.
|
||||||
* **Automatic Device Detection**: PWSP automatically detects when an input device is connected or disconnected and handles linking/unlinking.
|
* **Automatic Device Detection**: PWSP automatically detects when an input device is connected or disconnected and handles linking/unlinking.
|
||||||
|
* **Global Hotkeys**: Assign custom keyboard shortcuts to any sound file (or action) to trigger playback instantly, even when the application is not in focus.
|
||||||
|
|
||||||
|
|
||||||
# **⚙️ How It Works**
|
# **⚙️ How It Works**
|
||||||
|
|
||||||
@@ -38,8 +40,8 @@ three main components:
|
|||||||
* Creating and managing virtual audio devices.
|
* Creating and managing virtual audio devices.
|
||||||
* Linking these devices within the PipeWire graph.
|
* Linking these devices within the PipeWire graph.
|
||||||
* Handling all audio playback.
|
* Handling all audio playback.
|
||||||
|
* **UnixSocket**. This is how you interact with your sound collection, control playback, and configure settings.
|
||||||
* **pwsp-gui**: This is the graphical user interface. It acts as a client that communicates with pwsp-daemon via a
|
* **pwsp-gui**: This is the graphical user interface. It acts as a client that communicates with pwsp-daemon via a
|
||||||
**UnixSocket**. This is how you interact with your sound collection, control playback, and configure settings.
|
|
||||||
* **pwsp-cli**: This is the command-line interface, also acting as a client. It provides a way to control the daemon
|
* **pwsp-cli**: This is the command-line interface, also acting as a client. It provides a way to control the daemon
|
||||||
without a GUI, allowing for scripting or quick command-based actions.
|
without a GUI, allowing for scripting or quick command-based actions.
|
||||||
|
|
||||||
|
|||||||
@@ -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.0
|
pkgver = 1.7.2
|
||||||
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.0.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.7.0/pwsp-v1.7.0-linux-x64.zip
|
source = pwsp-bin-1.7.2.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.7.2/pwsp-v1.7.2-linux-x64.zip
|
||||||
source = pipewire-soundpad-1.7.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.0.tar.gz
|
source = pipewire-soundpad-1.7.2.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.2.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.0
|
pkgver=1.7.2
|
||||||
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.0
|
pkgver = 1.7.2
|
||||||
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.0.tar.gz
|
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.2.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.0
|
pkgver=1.7.2
|
||||||
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')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
%global cargo_install_lib 0
|
%global cargo_install_lib 0
|
||||||
|
|
||||||
Name: pwsp
|
Name: pwsp
|
||||||
Version: 1.7.0
|
Version: 1.7.2
|
||||||
Release: %autorelease
|
Release: %autorelease
|
||||||
Summary: Lets you play audio files through your microphone
|
Summary: Lets you play audio files through your microphone
|
||||||
|
|
||||||
|
|||||||
+19
-1
@@ -70,8 +70,10 @@ enum Actions {
|
|||||||
},
|
},
|
||||||
/// Play a sound by hotkey slot name
|
/// Play a sound by hotkey slot name
|
||||||
PlayHotkey { slot: String },
|
PlayHotkey { slot: String },
|
||||||
/// Remove a hotkey slot
|
/// Remove the hotkey slot
|
||||||
ClearHotkey { slot: String },
|
ClearHotkey { slot: String },
|
||||||
|
/// Clear the key chord for a hotkey slot
|
||||||
|
ClearHotkeyKey { slot: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
@@ -135,6 +137,12 @@ enum SetCommands {
|
|||||||
Hotkey { slot: String, file_path: PathBuf },
|
Hotkey { slot: String, file_path: PathBuf },
|
||||||
/// Set the key chord for a hotkey slot (e.g. "Ctrl+Alt+1")
|
/// Set the key chord for a hotkey slot (e.g. "Ctrl+Alt+1")
|
||||||
HotkeyKey { slot: String, key_chord: String },
|
HotkeyKey { slot: String, key_chord: String },
|
||||||
|
/// Atomically set the action and key chord for a hotkey slot
|
||||||
|
HotkeyActionAndKey {
|
||||||
|
slot: String,
|
||||||
|
action: String,
|
||||||
|
key_chord: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -158,6 +166,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
Actions::ToggleLoop { id } => Request::toggle_loop(id),
|
Actions::ToggleLoop { id } => Request::toggle_loop(id),
|
||||||
Actions::PlayHotkey { slot } => Request::play_hotkey(&slot),
|
Actions::PlayHotkey { slot } => Request::play_hotkey(&slot),
|
||||||
Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot),
|
Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot),
|
||||||
|
Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot),
|
||||||
},
|
},
|
||||||
Commands::Get { parameter } => match parameter {
|
Commands::Get { parameter } => match parameter {
|
||||||
GetCommands::IsPaused => Request::get_is_paused(),
|
GetCommands::IsPaused => Request::get_is_paused(),
|
||||||
@@ -183,6 +192,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
SetCommands::HotkeyKey { slot, key_chord } => {
|
SetCommands::HotkeyKey { slot, key_chord } => {
|
||||||
Request::set_hotkey_key(&slot, &key_chord)
|
Request::set_hotkey_key(&slot, &key_chord)
|
||||||
}
|
}
|
||||||
|
SetCommands::HotkeyActionAndKey {
|
||||||
|
slot,
|
||||||
|
action,
|
||||||
|
key_chord,
|
||||||
|
} => Request::set_hotkey_action_and_key(
|
||||||
|
&slot,
|
||||||
|
&serde_json::from_str::<Request>(&action)?,
|
||||||
|
&key_chord,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+18
-27
@@ -1,10 +1,10 @@
|
|||||||
use pwsp::{
|
use pwsp::{
|
||||||
types::socket::{Request, Response},
|
types::socket::{MAX_MESSAGE_SIZE, Request, Response},
|
||||||
utils::{
|
utils::{
|
||||||
commands::parse_command,
|
commands::parse_command,
|
||||||
daemon::{
|
daemon::{
|
||||||
create_runtime_dir, get_audio_player, get_daemon_config, get_runtime_dir,
|
create_runtime_dir, get_audio_player, get_daemon_config, get_runtime_dir,
|
||||||
is_daemon_running, link_player_to_virtual_mic,
|
is_daemon_running,
|
||||||
},
|
},
|
||||||
global_hotkeys::start_global_hotkey_listener,
|
global_hotkeys::start_global_hotkey_listener,
|
||||||
pipewire::create_virtual_mic,
|
pipewire::create_virtual_mic,
|
||||||
@@ -32,21 +32,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
eprintln!("Failed to initialize audio player: {}", err);
|
eprintln!("Failed to initialize audio player: {}", err);
|
||||||
} // Initialize audio player
|
} // Initialize audio player
|
||||||
|
|
||||||
tokio::spawn(async {
|
|
||||||
let max_retries = 60;
|
|
||||||
for i in 0..=max_retries {
|
|
||||||
match link_player_to_virtual_mic().await {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("Successfully linked player to virtual mic.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => println!("{e}\t{i}/{max_retries}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(1000)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::spawn(async {
|
tokio::spawn(async {
|
||||||
start_global_hotkey_listener().await;
|
start_global_hotkey_listener().await;
|
||||||
});
|
});
|
||||||
@@ -105,7 +90,7 @@ async fn commands_loop(listener: UnixListener) -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
let request_len = u32::from_le_bytes(len_bytes) as usize;
|
let request_len = u32::from_le_bytes(len_bytes) as usize;
|
||||||
|
|
||||||
if request_len > 10 * 1024 * 1024 {
|
if request_len > MAX_MESSAGE_SIZE {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Failed to read message from client: request too large ({} bytes)!",
|
"Failed to read message from client: request too large ({} bytes)!",
|
||||||
request_len
|
request_len
|
||||||
@@ -174,19 +159,25 @@ async fn commands_loop(listener: UnixListener) -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn player_loop() {
|
async fn player_loop() {
|
||||||
|
let mut device_check_counter: u32 = 0;
|
||||||
loop {
|
loop {
|
||||||
match get_audio_player().await {
|
let is_idle = match get_audio_player().await {
|
||||||
Ok(player_mutex) => {
|
Ok(player_mutex) => {
|
||||||
let mut audio_player = player_mutex.lock().await;
|
let mut audio_player = player_mutex.lock().await;
|
||||||
audio_player.update().await;
|
let check_devices = device_check_counter == 0;
|
||||||
|
audio_player.update(check_devices).await;
|
||||||
|
audio_player.tracks.is_empty()
|
||||||
}
|
}
|
||||||
Err(_err) => {
|
Err(_err) => true,
|
||||||
// To avoid spamming logs every 100ms when audio player fails to init
|
};
|
||||||
// we can just sleep, or you might prefer to print the error.
|
|
||||||
// Assuming it failed to initialize, no player update is possible.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
if is_idle {
|
||||||
|
device_check_counter = 0;
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
} else {
|
||||||
|
// Check devices every ~5 seconds (50 * 100ms) while playing
|
||||||
|
device_check_counter = (device_check_counter + 1) % 50;
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-105
@@ -7,57 +7,15 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
/// Convert an egui Key + Modifiers to a normalized chord string like "Ctrl+Shift+A".
|
/// Convert an egui Key + Modifiers to a normalized chord string like "Ctrl+Shift+A".
|
||||||
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
|
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
|
||||||
let key_name = match key {
|
let key_name = key.name();
|
||||||
Key::A => "A",
|
let is_valid = (key_name.len() == 1
|
||||||
Key::B => "B",
|
&& key_name.chars().next().unwrap().is_ascii_alphanumeric())
|
||||||
Key::C => "C",
|
|| (key_name.starts_with('F')
|
||||||
Key::D => "D",
|
&& key_name.len() > 1
|
||||||
Key::E => "E",
|
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
||||||
Key::F => "F",
|
if !is_valid {
|
||||||
Key::G => "G",
|
return None;
|
||||||
Key::H => "H",
|
}
|
||||||
Key::I => "I",
|
|
||||||
Key::J => "J",
|
|
||||||
Key::K => "K",
|
|
||||||
Key::L => "L",
|
|
||||||
Key::M => "M",
|
|
||||||
Key::N => "N",
|
|
||||||
Key::O => "O",
|
|
||||||
Key::P => "P",
|
|
||||||
Key::Q => "Q",
|
|
||||||
Key::R => "R",
|
|
||||||
Key::S => "S",
|
|
||||||
Key::T => "T",
|
|
||||||
Key::U => "U",
|
|
||||||
Key::V => "V",
|
|
||||||
Key::W => "W",
|
|
||||||
Key::X => "X",
|
|
||||||
Key::Y => "Y",
|
|
||||||
Key::Z => "Z",
|
|
||||||
Key::Num0 => "0",
|
|
||||||
Key::Num1 => "1",
|
|
||||||
Key::Num2 => "2",
|
|
||||||
Key::Num3 => "3",
|
|
||||||
Key::Num4 => "4",
|
|
||||||
Key::Num5 => "5",
|
|
||||||
Key::Num6 => "6",
|
|
||||||
Key::Num7 => "7",
|
|
||||||
Key::Num8 => "8",
|
|
||||||
Key::Num9 => "9",
|
|
||||||
Key::F1 => "F1",
|
|
||||||
Key::F2 => "F2",
|
|
||||||
Key::F3 => "F3",
|
|
||||||
Key::F4 => "F4",
|
|
||||||
Key::F5 => "F5",
|
|
||||||
Key::F6 => "F6",
|
|
||||||
Key::F7 => "F7",
|
|
||||||
Key::F8 => "F8",
|
|
||||||
Key::F9 => "F9",
|
|
||||||
Key::F10 => "F10",
|
|
||||||
Key::F11 => "F11",
|
|
||||||
Key::F12 => "F12",
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Require at least one modifier for hotkey chords (ignoring command/Super due to Wayland/Niri bug)
|
// Require at least one modifier for hotkey chords (ignoring command/Super due to Wayland/Niri bug)
|
||||||
if !modifiers.ctrl && !modifiers.alt && !modifiers.shift {
|
if !modifiers.ctrl && !modifiers.alt && !modifiers.shift {
|
||||||
@@ -100,57 +58,18 @@ pub fn parse_chord(chord: &str) -> Option<(Modifiers, Key)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = match parts[parts.len() - 1] {
|
let key_name = parts[parts.len() - 1];
|
||||||
"A" => Key::A,
|
let is_valid = (key_name.len() == 1
|
||||||
"B" => Key::B,
|
&& key_name.chars().next().unwrap().is_ascii_alphanumeric())
|
||||||
"C" => Key::C,
|
|| (key_name.starts_with('F')
|
||||||
"D" => Key::D,
|
&& key_name.len() > 1
|
||||||
"E" => Key::E,
|
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
||||||
"F" => Key::F,
|
|
||||||
"G" => Key::G,
|
if !is_valid {
|
||||||
"H" => Key::H,
|
return None;
|
||||||
"I" => Key::I,
|
}
|
||||||
"J" => Key::J,
|
|
||||||
"K" => Key::K,
|
let key = Key::from_name(key_name)?;
|
||||||
"L" => Key::L,
|
|
||||||
"M" => Key::M,
|
|
||||||
"N" => Key::N,
|
|
||||||
"O" => Key::O,
|
|
||||||
"P" => Key::P,
|
|
||||||
"Q" => Key::Q,
|
|
||||||
"R" => Key::R,
|
|
||||||
"S" => Key::S,
|
|
||||||
"T" => Key::T,
|
|
||||||
"U" => Key::U,
|
|
||||||
"V" => Key::V,
|
|
||||||
"W" => Key::W,
|
|
||||||
"X" => Key::X,
|
|
||||||
"Y" => Key::Y,
|
|
||||||
"Z" => Key::Z,
|
|
||||||
"0" => Key::Num0,
|
|
||||||
"1" => Key::Num1,
|
|
||||||
"2" => Key::Num2,
|
|
||||||
"3" => Key::Num3,
|
|
||||||
"4" => Key::Num4,
|
|
||||||
"5" => Key::Num5,
|
|
||||||
"6" => Key::Num6,
|
|
||||||
"7" => Key::Num7,
|
|
||||||
"8" => Key::Num8,
|
|
||||||
"9" => Key::Num9,
|
|
||||||
"F1" => Key::F1,
|
|
||||||
"F2" => Key::F2,
|
|
||||||
"F3" => Key::F3,
|
|
||||||
"F4" => Key::F4,
|
|
||||||
"F5" => Key::F5,
|
|
||||||
"F6" => Key::F6,
|
|
||||||
"F7" => Key::F7,
|
|
||||||
"F8" => Key::F8,
|
|
||||||
"F9" => Key::F9,
|
|
||||||
"F10" => Key::F10,
|
|
||||||
"F11" => Key::F11,
|
|
||||||
"F12" => Key::F12,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some((modifiers, key))
|
Some((modifiers, key))
|
||||||
}
|
}
|
||||||
@@ -221,16 +140,21 @@ impl SoundpadGui {
|
|||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
let action = Request::play(&file_path.to_string_lossy(), false);
|
let action = Request::play(&file_path.to_string_lossy(), false);
|
||||||
make_request_async(Request::set_hotkey_action(&slot_name, &action));
|
|
||||||
make_request_async(Request::set_hotkey_key(&slot_name, &chord));
|
make_request_async(Request::set_hotkey_action_and_key(
|
||||||
|
&slot_name, &action, &chord,
|
||||||
|
));
|
||||||
|
|
||||||
self.app_state
|
self.app_state
|
||||||
.hotkey_config
|
.hotkey_config
|
||||||
.set_slot(slot_name.clone(), action);
|
.set_slot(slot_name.clone(), action);
|
||||||
self.app_state
|
self.app_state
|
||||||
.hotkey_config
|
.hotkey_config
|
||||||
.set_key_chord(&slot_name, Some(chord));
|
.set_key_chord(&slot_name, Some(chord.clone()));
|
||||||
}
|
}
|
||||||
self.app_state.hotkey_capture_active = false;
|
self.app_state.hotkey_capture_active = false;
|
||||||
|
self.app_state.assigning_hotkey_slot = None;
|
||||||
|
self.app_state.assigning_hotkey_for_file = None;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-29
@@ -2,7 +2,7 @@ use crate::{
|
|||||||
types::pipewire::{DeviceType, Terminate},
|
types::pipewire::{DeviceType, Terminate},
|
||||||
utils::{
|
utils::{
|
||||||
daemon::get_daemon_config,
|
daemon::get_daemon_config,
|
||||||
pipewire::{create_link, get_device},
|
pipewire::{create_link, get_device, link_player_to_virtual_mic},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source};
|
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source};
|
||||||
@@ -53,11 +53,12 @@ pub struct PlayingSound {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct AudioPlayer {
|
pub struct AudioPlayer {
|
||||||
pub stream_handle: MixerDeviceSink,
|
stream_handle: Option<MixerDeviceSink>,
|
||||||
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<pipewire::channel::Sender<Terminate>>,
|
||||||
|
player_link_sender: Option<pipewire::channel::Sender<Terminate>>,
|
||||||
pub input_device_name: Option<String>,
|
pub input_device_name: Option<String>,
|
||||||
|
|
||||||
pub volume: f32, // Master volume
|
pub volume: f32, // Master volume
|
||||||
@@ -68,14 +69,13 @@ impl AudioPlayer {
|
|||||||
let daemon_config = get_daemon_config();
|
let daemon_config = get_daemon_config();
|
||||||
let default_volume = daemon_config.default_volume.unwrap_or(1.0);
|
let default_volume = daemon_config.default_volume.unwrap_or(1.0);
|
||||||
|
|
||||||
let stream_handle = DeviceSinkBuilder::open_default_sink()?;
|
|
||||||
|
|
||||||
let mut audio_player = AudioPlayer {
|
let mut audio_player = AudioPlayer {
|
||||||
stream_handle,
|
stream_handle: None,
|
||||||
tracks: HashMap::new(),
|
tracks: HashMap::new(),
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
|
|
||||||
input_link_sender: None,
|
input_link_sender: None,
|
||||||
|
player_link_sender: None,
|
||||||
input_device_name: daemon_config.default_input_name.clone(),
|
input_device_name: daemon_config.default_input_name.clone(),
|
||||||
|
|
||||||
volume: default_volume,
|
volume: default_volume,
|
||||||
@@ -88,18 +88,58 @@ impl AudioPlayer {
|
|||||||
Ok(audio_player)
|
Ok(audio_player)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_stream(&mut self) -> Result<&MixerDeviceSink, Box<dyn Error>> {
|
||||||
|
if self.stream_handle.is_none() {
|
||||||
|
let mut sink = DeviceSinkBuilder::open_default_sink()?;
|
||||||
|
sink.log_on_drop(false);
|
||||||
|
self.stream_handle = Some(sink);
|
||||||
|
}
|
||||||
|
Ok(self.stream_handle.as_ref().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_stream(&mut self) {
|
||||||
|
if self.stream_handle.is_some() {
|
||||||
|
self.stream_handle = None;
|
||||||
|
self.abort_player_link_thread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn abort_link_thread(&mut self) {
|
fn abort_link_thread(&mut self) {
|
||||||
if let Some(sender) = &self.input_link_sender {
|
if let Some(sender) = &self.input_link_sender {
|
||||||
match sender.send(Terminate {}) {
|
if let Ok(_) = sender.send(Terminate {}) {
|
||||||
Ok(_) => {
|
println!("Sent terminate signal to input link thread");
|
||||||
println!("Sent terminate signal to link thread");
|
self.input_link_sender = None;
|
||||||
self.input_link_sender = None;
|
} else {
|
||||||
}
|
eprintln!("Failed to send terminate signal to input link thread");
|
||||||
Err(_) => eprintln!("Failed to send terminate signal to link thread"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn abort_player_link_thread(&mut self) {
|
||||||
|
if let Some(sender) = &self.player_link_sender {
|
||||||
|
if let Ok(_) = sender.send(Terminate {}) {
|
||||||
|
println!("Sent terminate signal to player link thread");
|
||||||
|
self.player_link_sender = None;
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to send terminate signal to player link thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn link_player(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
|
if self.player_link_sender.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match link_player_to_virtual_mic().await {
|
||||||
|
Ok(sender) => {
|
||||||
|
self.player_link_sender = Some(sender);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn link_devices(&mut self) -> Result<(), Box<dyn Error>> {
|
async fn link_devices(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
self.abort_link_thread();
|
self.abort_link_thread();
|
||||||
|
|
||||||
@@ -179,6 +219,9 @@ impl AudioPlayer {
|
|||||||
} else {
|
} else {
|
||||||
self.tracks.clear();
|
self.tracks.clear();
|
||||||
}
|
}
|
||||||
|
if self.tracks.is_empty() {
|
||||||
|
self.drop_stream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_paused(&self) -> bool {
|
pub fn is_paused(&self) -> bool {
|
||||||
@@ -299,12 +342,16 @@ impl AudioPlayer {
|
|||||||
self.tracks.clear();
|
self.tracks.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.ensure_stream()?;
|
||||||
|
self.link_player().await.ok();
|
||||||
|
|
||||||
let id = self.next_id;
|
let id = self.next_id;
|
||||||
self.next_id += 1;
|
self.next_id += 1;
|
||||||
|
|
||||||
let duration = source.total_duration().map(|d| d.as_secs_f32());
|
let duration = source.total_duration().map(|d| d.as_secs_f32());
|
||||||
|
|
||||||
let sink = Player::connect_new(self.stream_handle.mixer());
|
let mixer = self.stream_handle.as_ref().unwrap().mixer();
|
||||||
|
let sink = Player::connect_new(mixer);
|
||||||
sink.set_volume(self.volume); // Default volume is 1.0 * master
|
sink.set_volume(self.volume); // Default volume is 1.0 * master
|
||||||
sink.append(source);
|
sink.append(source);
|
||||||
sink.play();
|
sink.play();
|
||||||
@@ -358,20 +405,26 @@ impl AudioPlayer {
|
|||||||
tracks
|
tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&mut self) {
|
pub async fn update(&mut self, check_devices: bool) {
|
||||||
if let Some(input_device_name) = &self.input_device_name {
|
if check_devices {
|
||||||
// Unlink devices if selected input device was removed
|
if let Some(input_device_name) = &self.input_device_name {
|
||||||
if self.input_link_sender.is_some() && get_device(input_device_name).await.is_err() {
|
// Unlink devices if selected input device was removed
|
||||||
// Selected input device was removed
|
if self.input_link_sender.is_some() && get_device(input_device_name).await.is_err()
|
||||||
eprintln!(
|
{
|
||||||
"Selected input device {} was removed, unlinking devices",
|
eprintln!(
|
||||||
input_device_name
|
"Selected input device {} was removed, unlinking devices",
|
||||||
);
|
input_device_name
|
||||||
self.abort_link_thread();
|
);
|
||||||
|
self.abort_link_thread();
|
||||||
|
}
|
||||||
|
// Link devices if not linked
|
||||||
|
else if self.input_link_sender.is_none() {
|
||||||
|
self.link_devices().await.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Link devices if not linked
|
|
||||||
else if self.input_link_sender.is_none() {
|
if self.stream_handle.is_some() && self.player_link_sender.is_none() {
|
||||||
self.link_devices().await.ok();
|
self.link_player().await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,16 +455,22 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for handle in restart_futures {
|
for handle in restart_futures {
|
||||||
if let Ok(Some((id, source))) = handle.await {
|
if let Ok(res) = handle.await {
|
||||||
if let Some(sound) = self.tracks.get_mut(&id) {
|
if let Some((id, source)) = res {
|
||||||
sound.sink.append(source);
|
if let Some(sound) = self.tracks.get_mut(&id) {
|
||||||
sound.sink.play();
|
sound.sink.append(source);
|
||||||
|
sound.sink.play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tracks
|
self.tracks
|
||||||
.retain(|_, sound| !sound.sink.empty() || sound.looped);
|
.retain(|_, sound| !sound.sink.empty() || sound.looped);
|
||||||
|
|
||||||
|
if self.tracks.is_empty() {
|
||||||
|
self.drop_stream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box<dyn Error>> {
|
pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
|||||||
+66
-20
@@ -99,22 +99,28 @@ pub struct SetHotkeyCommand {
|
|||||||
pub file_path: Option<PathBuf>,
|
pub file_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SetHotkeyActionCommand {
|
||||||
|
pub slot: Option<String>,
|
||||||
|
pub action: Option<Request>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SetHotkeyKeyCommand {
|
pub struct SetHotkeyKeyCommand {
|
||||||
pub slot: Option<String>,
|
pub slot: Option<String>,
|
||||||
pub key_chord: Option<String>,
|
pub key_chord: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClearHotkeyCommand {
|
pub struct SetHotkeyActionAndKeyCommand {
|
||||||
pub slot: Option<String>,
|
pub slot: Option<String>,
|
||||||
|
pub action: Option<Request>,
|
||||||
|
pub key_chord: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PlayHotkeyCommand {
|
pub struct PlayHotkeyCommand {
|
||||||
pub slot: Option<String>,
|
pub slot: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SetHotkeyActionCommand {
|
pub struct ClearHotkeyCommand {
|
||||||
pub slot: Option<String>,
|
pub slot: Option<String>,
|
||||||
pub action: Option<Request>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClearHotkeyKeyCommand {
|
pub struct ClearHotkeyKeyCommand {
|
||||||
@@ -553,6 +559,30 @@ impl Executable for SetHotkeyCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Executable for SetHotkeyActionCommand {
|
||||||
|
async fn execute(&self) -> Response {
|
||||||
|
let Some(slot) = &self.slot else {
|
||||||
|
return Response::new(false, "Missing slot name");
|
||||||
|
};
|
||||||
|
let Some(action) = &self.action else {
|
||||||
|
return Response::new(false, "Missing or invalid action");
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = match HotkeyConfig::load() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
config.set_slot(slot.clone(), action.clone());
|
||||||
|
|
||||||
|
match config.save() {
|
||||||
|
Ok(_) => Response::new(true, format!("Hotkey slot '{}' set", slot)),
|
||||||
|
Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for SetHotkeyKeyCommand {
|
impl Executable for SetHotkeyKeyCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
@@ -583,24 +613,41 @@ impl Executable for SetHotkeyKeyCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for ClearHotkeyCommand {
|
impl Executable for SetHotkeyActionAndKeyCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let Some(slot) = &self.slot else {
|
let Some(slot) = &self.slot else {
|
||||||
return Response::new(false, "Missing slot name");
|
return Response::new(false, "Missing slot name");
|
||||||
};
|
};
|
||||||
|
let Some(action) = &self.action else {
|
||||||
|
return Response::new(false, "Missing or invalid action");
|
||||||
|
};
|
||||||
|
let Some(key_chord) = &self.key_chord else {
|
||||||
|
return Response::new(false, "Missing key chord");
|
||||||
|
};
|
||||||
|
|
||||||
let mut config = match HotkeyConfig::load() {
|
let mut config = match HotkeyConfig::load() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)),
|
Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if config.remove_slot(slot) {
|
// Set the action and then the key chord
|
||||||
match config.save() {
|
config.set_slot(slot.clone(), action.clone());
|
||||||
Ok(_) => Response::new(true, format!("Hotkey slot '{}' cleared", slot)),
|
if !config.set_key_chord(slot, Some(key_chord.clone())) {
|
||||||
Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)),
|
return Response::new(
|
||||||
}
|
false,
|
||||||
} else {
|
format!("Slot '{}' not found after setting action", slot),
|
||||||
Response::new(false, format!("Slot '{}' not found", slot))
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match config.save() {
|
||||||
|
Ok(_) => Response::new(
|
||||||
|
true,
|
||||||
|
format!(
|
||||||
|
"Hotkey slot '{}' set with action and key chord '{}'",
|
||||||
|
slot, key_chord
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,25 +679,24 @@ impl Executable for PlayHotkeyCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for SetHotkeyActionCommand {
|
impl Executable for ClearHotkeyCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let Some(slot) = &self.slot else {
|
let Some(slot) = &self.slot else {
|
||||||
return Response::new(false, "Missing slot name");
|
return Response::new(false, "Missing slot name");
|
||||||
};
|
};
|
||||||
let Some(action) = &self.action else {
|
|
||||||
return Response::new(false, "Missing or invalid action");
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut config = match HotkeyConfig::load() {
|
let mut config = match HotkeyConfig::load() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)),
|
Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)),
|
||||||
};
|
};
|
||||||
|
|
||||||
config.set_slot(slot.clone(), action.clone());
|
if config.remove_slot(slot) {
|
||||||
|
match config.save() {
|
||||||
match config.save() {
|
Ok(_) => Response::new(true, format!("Hotkey slot '{}' cleared", slot)),
|
||||||
Ok(_) => Response::new(true, format!("Hotkey slot '{}' set", slot)),
|
Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)),
|
||||||
Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)),
|
}
|
||||||
|
} else {
|
||||||
|
Response::new(false, format!("Slot '{}' not found", slot))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -98,7 +98,6 @@ impl GuiConfig {
|
|||||||
pub struct HotkeySlot {
|
pub struct HotkeySlot {
|
||||||
pub slot: String,
|
pub slot: String,
|
||||||
pub action: Request,
|
pub action: Request,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub key_chord: Option<String>,
|
pub key_chord: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +120,7 @@ impl HotkeyConfig {
|
|||||||
let bytes = fs::read(&path)?;
|
let bytes = fs::read(&path)?;
|
||||||
match serde_json::from_slice::<HotkeyConfig>(&bytes) {
|
match serde_json::from_slice::<HotkeyConfig>(&bytes) {
|
||||||
Ok(config) => Ok(config),
|
Ok(config) => Ok(config),
|
||||||
Err(_) => Ok(HotkeyConfig::default()),
|
Err(e) => Err(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub const MAX_MESSAGE_SIZE: usize = 128 * 1024;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -208,6 +210,18 @@ impl Request {
|
|||||||
pub fn clear_hotkey_key(slot: &str) -> Self {
|
pub fn clear_hotkey_key(slot: &str) -> Self {
|
||||||
Request::new("clear_hotkey_key", vec![("slot", slot)])
|
Request::new("clear_hotkey_key", vec![("slot", slot)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_hotkey_action_and_key(slot: &str, action: &Request, key_chord: &str) -> Self {
|
||||||
|
let action_json = serde_json::to_string(action).unwrap_or_default();
|
||||||
|
Request::new(
|
||||||
|
"set_hotkey_action_and_key",
|
||||||
|
vec![
|
||||||
|
("slot", slot),
|
||||||
|
("action", &action_json),
|
||||||
|
("key_chord", key_chord),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -106,6 +106,19 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
|
|||||||
let slot = request.args.get("slot").cloned();
|
let slot = request.args.get("slot").cloned();
|
||||||
Some(Box::new(ClearHotkeyKeyCommand { slot }))
|
Some(Box::new(ClearHotkeyKeyCommand { slot }))
|
||||||
}
|
}
|
||||||
|
"set_hotkey_action_and_key" => {
|
||||||
|
let slot = request.args.get("slot").cloned();
|
||||||
|
let action = request
|
||||||
|
.args
|
||||||
|
.get("action")
|
||||||
|
.and_then(|s| serde_json::from_str::<Request>(s).ok());
|
||||||
|
let key_chord = request.args.get("key_chord").cloned();
|
||||||
|
Some(Box::new(SetHotkeyActionAndKeyCommand {
|
||||||
|
slot,
|
||||||
|
action,
|
||||||
|
key_chord,
|
||||||
|
}))
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-40
@@ -2,10 +2,10 @@ use crate::{
|
|||||||
types::{
|
types::{
|
||||||
audio_player::AudioPlayer,
|
audio_player::AudioPlayer,
|
||||||
config::DaemonConfig,
|
config::DaemonConfig,
|
||||||
socket::{Request, Response},
|
socket::{MAX_MESSAGE_SIZE, Request, Response},
|
||||||
},
|
},
|
||||||
utils::pipewire::{create_link, get_device},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{error::Error, fs};
|
use std::{error::Error, fs};
|
||||||
@@ -38,44 +38,6 @@ pub fn get_daemon_config() -> DaemonConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> {
|
|
||||||
let pwsp_daemon_output;
|
|
||||||
if let Ok(device) = get_device("pwsp-daemon").await {
|
|
||||||
pwsp_daemon_output = device;
|
|
||||||
} else {
|
|
||||||
return Err(
|
|
||||||
"Could not find alsa_playback.pwsp-daemon device, skipping device linking".into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let pwsp_daemon_input;
|
|
||||||
if let Ok(device) = get_device("pwsp-virtual-mic").await {
|
|
||||||
pwsp_daemon_input = device;
|
|
||||||
} else {
|
|
||||||
return Err("Could not find pwsp-virtual-mic device, skipping device linking".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let output_fl = pwsp_daemon_output
|
|
||||||
.clone()
|
|
||||||
.output_fl
|
|
||||||
.expect("Failed to get pwsp-daemon output_fl");
|
|
||||||
let output_fr = pwsp_daemon_output
|
|
||||||
.clone()
|
|
||||||
.output_fr
|
|
||||||
.expect("Failed to get pwsp-daemon output_fl");
|
|
||||||
let input_fl = pwsp_daemon_input
|
|
||||||
.clone()
|
|
||||||
.input_fl
|
|
||||||
.expect("Failed to get pwsp-daemon input_fl");
|
|
||||||
let input_fr = pwsp_daemon_input
|
|
||||||
.clone()
|
|
||||||
.input_fr
|
|
||||||
.expect("Failed to get pwsp-daemon input_fr");
|
|
||||||
create_link(output_fl, output_fr, input_fl, input_fr)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_runtime_dir() -> PathBuf {
|
pub fn get_runtime_dir() -> PathBuf {
|
||||||
dirs::runtime_dir().unwrap_or(PathBuf::from("/run/pwsp"))
|
dirs::runtime_dir().unwrap_or(PathBuf::from("/run/pwsp"))
|
||||||
}
|
}
|
||||||
@@ -135,6 +97,14 @@ pub async fn make_request(request: Request) -> Result<Response, Box<dyn Error +
|
|||||||
}
|
}
|
||||||
let response_len = u32::from_le_bytes(len_bytes) as usize;
|
let response_len = u32::from_le_bytes(len_bytes) as usize;
|
||||||
|
|
||||||
|
if response_len > MAX_MESSAGE_SIZE {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to read response from daemon: response too large ({} bytes)!",
|
||||||
|
response_len
|
||||||
|
);
|
||||||
|
return Err("Response too large".into());
|
||||||
|
}
|
||||||
|
|
||||||
let mut buffer = vec![0u8; response_len];
|
let mut buffer = vec![0u8; response_len];
|
||||||
if stream.read_exact(&mut buffer).await.is_err() {
|
if stream.read_exact(&mut buffer).await.is_err() {
|
||||||
return Err("Failed to read response".into());
|
return Err("Failed to read response".into());
|
||||||
|
|||||||
@@ -258,6 +258,44 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
|
|||||||
Ok(pw_sender)
|
Ok(pw_sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn link_player_to_virtual_mic()
|
||||||
|
-> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
||||||
|
let pwsp_daemon_output = match get_device("pwsp-daemon").await {
|
||||||
|
Ok(device) => device,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(
|
||||||
|
"Could not find alsa_playback.pwsp-daemon device, skipping device linking".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let pwsp_daemon_input = match get_device("pwsp-virtual-mic").await {
|
||||||
|
Ok(device) => device,
|
||||||
|
Err(_) => {
|
||||||
|
return Err("Could not find pwsp-virtual-mic device, skipping device linking".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_fl = match pwsp_daemon_output.output_fl {
|
||||||
|
Some(port) => port,
|
||||||
|
None => return Err("Failed to get pwsp-daemon output_fl".into()),
|
||||||
|
};
|
||||||
|
let output_fr = match pwsp_daemon_output.output_fr {
|
||||||
|
Some(port) => port,
|
||||||
|
None => return Err("Failed to get pwsp-daemon output_fr".into()),
|
||||||
|
};
|
||||||
|
let input_fl = match pwsp_daemon_input.input_fl {
|
||||||
|
Some(port) => port,
|
||||||
|
None => return Err("Failed to get pwsp-virtual-mic input_fl".into()),
|
||||||
|
};
|
||||||
|
let input_fr = match pwsp_daemon_input.input_fr {
|
||||||
|
Some(port) => port,
|
||||||
|
None => return Err("Failed to get pwsp-virtual-mic input_fr".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
create_link(output_fl, output_fr, input_fl, input_fr)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_link(
|
pub fn create_link(
|
||||||
output_fl: Port,
|
output_fl: Port,
|
||||||
output_fr: Port,
|
output_fr: Port,
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
use rodio::{DeviceSinkBuilder, MixerDeviceSink};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Instant;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
// A mock of AudioPlayer to isolate the play method's blocking behavior.
|
|
||||||
// We only implement the relevant part of the logic that needs optimizing.
|
|
||||||
pub struct AudioPlayerMock {
|
|
||||||
pub tracks: std::collections::HashMap<u32, ()>,
|
|
||||||
pub next_id: u32,
|
|
||||||
pub volume: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AudioPlayerMock {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
AudioPlayerMock {
|
|
||||||
tracks: std::collections::HashMap::new(),
|
|
||||||
next_id: 1,
|
|
||||||
volume: 1.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn play(
|
|
||||||
&mut self,
|
|
||||||
file_path: &Path,
|
|
||||||
concurrent: bool,
|
|
||||||
) -> Result<u32, Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
if !file_path.exists() {
|
|
||||||
return Err(format!("File does not exist: {}", file_path.display()).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_buf = file_path.to_path_buf();
|
|
||||||
let _file = tokio::task::spawn_blocking(move || {
|
|
||||||
// Simulate some blocking work like Decoder::try_from which reads file headers
|
|
||||||
let _f = fs::File::open(&path_buf).unwrap();
|
|
||||||
|
|
||||||
// Emulate the actual time spent reading file and decoding header (which is what Decoder::try_from does)
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100)); // Simulate slow disk/decode
|
|
||||||
_f
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !concurrent {
|
|
||||||
self.tracks.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = self.next_id;
|
|
||||||
self.next_id += 1;
|
|
||||||
self.tracks.insert(id, ());
|
|
||||||
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_performance_blocking() {
|
|
||||||
println!("Setting up mock environment...");
|
|
||||||
|
|
||||||
// Create a dummy file to read
|
|
||||||
let test_file = Path::new("test_dummy.wav");
|
|
||||||
fs::write(test_file, "dummy content").unwrap();
|
|
||||||
|
|
||||||
let player = Arc::new(Mutex::new(AudioPlayerMock::new()));
|
|
||||||
|
|
||||||
println!("Starting benchmark for synchronous behavior in async fn...");
|
|
||||||
|
|
||||||
// We launch a background task that measures event loop latency.
|
|
||||||
// If the main tasks block the executor, this task will suffer high latency.
|
|
||||||
let latency_task = tokio::spawn(async {
|
|
||||||
let mut max_latency = std::time::Duration::from_secs(0);
|
|
||||||
for _ in 0..50 {
|
|
||||||
let start = Instant::now();
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
if elapsed > max_latency {
|
|
||||||
max_latency = elapsed;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
max_latency
|
|
||||||
});
|
|
||||||
|
|
||||||
// Launch multiple play operations
|
|
||||||
let mut tasks = vec![];
|
|
||||||
let start_time = Instant::now();
|
|
||||||
for _ in 0..10 {
|
|
||||||
let player_clone = Arc::clone(&player);
|
|
||||||
let file_path = test_file.to_path_buf();
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
let mut p = player_clone.lock().await;
|
|
||||||
let _ = p.play(&file_path, true).await;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all tasks to finish
|
|
||||||
for t in tasks {
|
|
||||||
let _ = t.await;
|
|
||||||
}
|
|
||||||
let total_time = start_time.elapsed();
|
|
||||||
|
|
||||||
let max_latency = latency_task.await.unwrap();
|
|
||||||
|
|
||||||
println!("Total execution time: {:?}", total_time);
|
|
||||||
println!(
|
|
||||||
"Max event loop latency (blocking indicator): {:?}",
|
|
||||||
max_latency
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
fs::remove_file(test_file).unwrap();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user