mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-04-28 06:21:23 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2b83f5c32 | |||
| f01a0e656c | |||
| 6114b9a7f8 | |||
| b8baeb6226 | |||
| 02306b5893 | |||
| 3add499bd7 | |||
| 3c2e943e18 | |||
| 261f83efd4 | |||
| c6577cd5e0 | |||
| 95761f6a5a | |||
| d6effc972e | |||
| 498d0d25af | |||
| c99d0749e3 | |||
| 151f43f1ab | |||
| 077518019f | |||
| 968eba80e6 | |||
| aa77a8d212 | |||
| 4b50645c93 | |||
| 39648f7781 |
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: 1.94.1
|
||||||
|
|
||||||
- name: Extract all binary names
|
- name: Extract all binary names
|
||||||
id: cargo-meta
|
id: cargo-meta
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
flatpak-build:
|
flatpak-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: bilelmoussaoui/flatpak-github-actions:freedesktop-24.08
|
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
|
||||||
options: --privileged
|
options: --privileged
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -116,4 +116,4 @@ jobs:
|
|||||||
manifest-path: packages/flatpak/ru.arabianq.pwsp.yaml
|
manifest-path: packages/flatpak/ru.arabianq.pwsp.yaml
|
||||||
cache: true
|
cache: true
|
||||||
branch: master
|
branch: master
|
||||||
build-bundle: true
|
build-bundle: true
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: 1.94.1
|
||||||
|
|
||||||
- name: Extract all binary names
|
- name: Extract all binary names
|
||||||
id: cargo-meta
|
id: cargo-meta
|
||||||
@@ -158,14 +158,14 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: bilelmoussaoui/flatpak-github-actions:freedesktop-24.08
|
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
|
||||||
options: --privileged
|
options: --privileged
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.prepare.outputs.tag }}
|
ref: ${{ needs.prepare.outputs.tag }}
|
||||||
|
|
||||||
- name: Build Flatpak
|
- name: Build Flatpak
|
||||||
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
|
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
Generated
+526
-169
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pwsp"
|
name = "pwsp"
|
||||||
version = "1.6.2"
|
version = "1.6.3"
|
||||||
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."
|
||||||
@@ -18,7 +18,7 @@ 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.5.60", default-features = false, features = [
|
clap = { version = "4.6.0", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"suggestions",
|
"suggestions",
|
||||||
"help",
|
"help",
|
||||||
@@ -39,18 +39,18 @@ rfd = { version = "0.17.2", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
opener = { version = "0.8.4", features = ["reveal"] }
|
opener = { version = "0.8.4", features = ["reveal"] }
|
||||||
|
|
||||||
egui = { version = "0.33.3", default-features = false, features = [
|
egui = { version = "0.34.1", default-features = false, features = [
|
||||||
"default_fonts",
|
"default_fonts",
|
||||||
"rayon",
|
"rayon",
|
||||||
] }
|
] }
|
||||||
eframe = { version = "0.33.3", default-features = false, features = [
|
eframe = { version = "0.34.1", default-features = false, features = [
|
||||||
"default_fonts",
|
"default_fonts",
|
||||||
"glow",
|
"glow",
|
||||||
"x11",
|
"x11",
|
||||||
"wayland",
|
"wayland",
|
||||||
] }
|
] }
|
||||||
egui_material_icons = "0.5.0"
|
egui_material_icons = "0.6.0"
|
||||||
egui_dnd = "0.14.0"
|
egui_dnd = "0.15.0"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "pwsp-daemon"
|
name = "pwsp-daemon"
|
||||||
|
|||||||
@@ -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.6.2
|
pkgver = 1.6.3
|
||||||
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.6.2.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.6.2/pwsp-v1.6.2-linux-x64.zip
|
source = pwsp-bin-1.6.3.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.6.3/pwsp-v1.6.3-linux-x64.zip
|
||||||
source = pipewire-soundpad-1.6.2.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.6.2.tar.gz
|
source = pipewire-soundpad-1.6.3.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.6.3.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.6.2
|
pkgver=1.6.3
|
||||||
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.6.2
|
pkgver = 1.6.3
|
||||||
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.6.2.tar.gz
|
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.6.3.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.6.2
|
pkgver=1.6.3
|
||||||
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')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
app-id: ru.arabianq.pwsp
|
app-id: ru.arabianq.pwsp
|
||||||
runtime: org.freedesktop.Platform
|
runtime: org.freedesktop.Platform
|
||||||
runtime-version: "24.08"
|
runtime-version: "25.08"
|
||||||
sdk: org.freedesktop.Sdk
|
sdk: org.freedesktop.Sdk
|
||||||
sdk-extensions:
|
sdk-extensions:
|
||||||
- org.freedesktop.Sdk.Extension.rust-stable
|
- org.freedesktop.Sdk.Extension.rust-stable
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
%global cargo_install_lib 0
|
%global cargo_install_lib 0
|
||||||
|
|
||||||
Name: pwsp
|
Name: pwsp
|
||||||
Version: 1.6.2
|
Version: 1.6.3
|
||||||
Release: %autorelease
|
Release: %autorelease
|
||||||
Summary: Lets you play audio files through your microphone
|
Summary: Lets you play audio files through your microphone
|
||||||
|
|
||||||
|
|||||||
+42
-15
@@ -9,6 +9,7 @@ use pwsp::{
|
|||||||
pipewire::create_virtual_mic,
|
pipewire::create_virtual_mic,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::{error::Error, fs, time::Duration};
|
use std::{error::Error, fs, time::Duration};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
@@ -26,18 +27,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
get_daemon_config(); // Initialize daemon config
|
get_daemon_config(); // Initialize daemon config
|
||||||
create_virtual_mic()?;
|
create_virtual_mic()?;
|
||||||
get_audio_player().await; // Initialize audio player
|
if let Err(err) = get_audio_player().await {
|
||||||
|
eprintln!("Failed to initialize audio player: {}", err);
|
||||||
|
} // Initialize audio player
|
||||||
|
|
||||||
let max_retries = 5;
|
tokio::spawn(async {
|
||||||
for i in 0..=max_retries {
|
let max_retries = 60;
|
||||||
match link_player_to_virtual_mic().await {
|
for i in 0..=max_retries {
|
||||||
Ok(_) => break,
|
match link_player_to_virtual_mic().await {
|
||||||
Err(e) => println!("{e}\t{i}/{max_retries}"),
|
Ok(_) => {
|
||||||
|
println!("Successfully linked player to virtual mic.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => println!("{e}\t{i}/{max_retries}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(1000)).await;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
sleep(Duration::from_millis(300 * i)).await;
|
|
||||||
}
|
|
||||||
link_player_to_virtual_mic().await?;
|
|
||||||
|
|
||||||
let runtime_dir = get_runtime_dir();
|
let runtime_dir = get_runtime_dir();
|
||||||
|
|
||||||
@@ -45,11 +52,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
lock_file.lock()?;
|
lock_file.lock()?;
|
||||||
|
|
||||||
let socket_path = runtime_dir.join("daemon.sock");
|
let socket_path = runtime_dir.join("daemon.sock");
|
||||||
if fs::metadata(&socket_path).is_ok() {
|
if let Err(e) = fs::remove_file(&socket_path) {
|
||||||
fs::remove_file(&socket_path)?;
|
if e.kind() != std::io::ErrorKind::NotFound {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let listener = UnixListener::bind(&socket_path)?;
|
let listener = UnixListener::bind(&socket_path)?;
|
||||||
|
fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600))?;
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Daemon started. Listening on {}",
|
"Daemon started. Listening on {}",
|
||||||
socket_path.to_str().unwrap_or_default()
|
socket_path.to_str().unwrap_or_default()
|
||||||
@@ -89,6 +100,14 @@ 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 {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to read message from client: request too large ({} bytes)!",
|
||||||
|
request_len
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut buffer = vec![0u8; request_len];
|
let mut buffer = vec![0u8; request_len];
|
||||||
if stream.read_exact(&mut buffer).await.is_err() {
|
if stream.read_exact(&mut buffer).await.is_err() {
|
||||||
eprintln!("Failed to read message from client!");
|
eprintln!("Failed to read message from client!");
|
||||||
@@ -151,9 +170,17 @@ async fn commands_loop(listener: UnixListener) -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
async fn player_loop() {
|
async fn player_loop() {
|
||||||
loop {
|
loop {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
match get_audio_player().await {
|
||||||
|
Ok(player_mutex) => {
|
||||||
audio_player.update().await;
|
let mut audio_player = player_mutex.lock().await;
|
||||||
|
audio_player.update().await;
|
||||||
|
}
|
||||||
|
Err(_err) => {
|
||||||
|
// 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;
|
sleep(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-16
@@ -19,13 +19,13 @@ enum TrackAction {
|
|||||||
impl SoundpadGui {
|
impl SoundpadGui {
|
||||||
fn get_volume_icon(volume: f32) -> &'static str {
|
fn get_volume_icon(volume: f32) -> &'static str {
|
||||||
if volume > 0.7 {
|
if volume > 0.7 {
|
||||||
ICON_VOLUME_UP
|
ICON_VOLUME_UP.codepoint
|
||||||
} else if volume <= 0.0 {
|
} else if volume <= 0.0 {
|
||||||
ICON_VOLUME_OFF
|
ICON_VOLUME_OFF.codepoint
|
||||||
} else if volume < 0.3 {
|
} else if volume < 0.3 {
|
||||||
ICON_VOLUME_MUTE
|
ICON_VOLUME_MUTE.codepoint
|
||||||
} else {
|
} else {
|
||||||
ICON_VOLUME_DOWN
|
ICON_VOLUME_DOWN.codepoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ impl SoundpadGui {
|
|||||||
let path = item.clone();
|
let path = item.clone();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
handle.ui(ui, |ui| {
|
handle.ui(ui, |ui| {
|
||||||
ui.label(ICON_DRAG_INDICATOR);
|
ui.label(ICON_DRAG_INDICATOR.codepoint);
|
||||||
});
|
});
|
||||||
let name = path
|
let name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -345,7 +345,7 @@ impl SoundpadGui {
|
|||||||
// Context menu
|
// Context menu
|
||||||
dir_button_response.context_menu(|ui| {
|
dir_button_response.context_menu(|ui| {
|
||||||
if ui
|
if ui
|
||||||
.button(format!("{} {}", ICON_OPEN_IN_NEW, "Show"))
|
.button(format!("{} {}", ICON_OPEN_IN_NEW.codepoint, "Show"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
self.open_dir(&path);
|
self.open_dir(&path);
|
||||||
@@ -354,7 +354,7 @@ impl SoundpadGui {
|
|||||||
if ui
|
if ui
|
||||||
.button(format!(
|
.button(format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
ICON_OPEN_IN_BROWSER, "Open in File Manager"
|
ICON_OPEN_IN_BROWSER.codepoint, "Open in File Manager"
|
||||||
))
|
))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
@@ -365,7 +365,10 @@ impl SoundpadGui {
|
|||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
if ui.button(format!("{} {}", ICON_DELETE, "Remove")).clicked() {
|
if ui
|
||||||
|
.button(format!("{} {}", ICON_DELETE.codepoint, "Remove"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
self.app_state.dirs_to_remove.insert(path.clone());
|
self.app_state.dirs_to_remove.insert(path.clone());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -452,20 +455,23 @@ impl SoundpadGui {
|
|||||||
// Context menu
|
// Context menu
|
||||||
file_button_response.context_menu(|ui| {
|
file_button_response.context_menu(|ui| {
|
||||||
if ui
|
if ui
|
||||||
.button(format!("{} {}", ICON_BOLT, "Play Solo"))
|
.button(format!("{} {}", ICON_BOLT.codepoint, "Play Solo"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
self.play_file(&entry_path, false);
|
self.play_file(&entry_path, false);
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
self.app_state.selected_file = Some(entry_path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button(format!("{} {}", ICON_ADD, "Add New")).clicked() {
|
if ui
|
||||||
|
.button(format!("{} {}", ICON_ADD.codepoint, "Add New"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
self.play_file(&entry_path, true);
|
self.play_file(&entry_path, true);
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
self.app_state.selected_file = Some(entry_path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.button(format!("{} {}", ICON_SWAP_HORIZ, "Replace Last"))
|
.button(format!("{} {}", ICON_SWAP_HORIZ.codepoint, "Replace Last"))
|
||||||
.clicked()
|
.clicked()
|
||||||
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
||||||
{
|
{
|
||||||
@@ -479,7 +485,7 @@ impl SoundpadGui {
|
|||||||
if ui
|
if ui
|
||||||
.button(format!(
|
.button(format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
ICON_OPEN_IN_BROWSER, "Show in File Manager"
|
ICON_OPEN_IN_BROWSER.codepoint, "Show in File Manager"
|
||||||
))
|
))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
@@ -498,9 +504,7 @@ impl SoundpadGui {
|
|||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
// ---------- Microphone selection ----------
|
// ---------- Microphone selection ----------
|
||||||
let mut mics: Vec<(&String, &String)> =
|
let mics = &self.audio_player_state.all_inputs_sorted;
|
||||||
self.audio_player_state.all_inputs.iter().collect();
|
|
||||||
mics.sort_by_key(|(k, _)| *k);
|
|
||||||
|
|
||||||
let mut selected_input = self.audio_player_state.current_input.to_owned();
|
let mut selected_input = self.audio_player_state.current_input.to_owned();
|
||||||
let prev_input = selected_input.to_owned();
|
let prev_input = selected_input.to_owned();
|
||||||
@@ -514,7 +518,7 @@ impl SoundpadGui {
|
|||||||
)
|
)
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for (name, nick) in mics {
|
for (name, nick) in mics {
|
||||||
ui.selectable_value(&mut selected_input, name.to_owned(), nick);
|
ui.selectable_value(&mut selected_input, name.clone(), nick);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+38
-44
@@ -57,17 +57,18 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Play selected file on Enter
|
// Play selected file on Enter
|
||||||
if self.key_pressed(ctx, Key::Enter) && self.app_state.selected_file.is_some() {
|
if self.key_pressed(ctx, Key::Enter) {
|
||||||
let path = &self.app_state.selected_file.clone().unwrap();
|
if let Some(path) = self.app_state.selected_file.clone() {
|
||||||
if modifiers.ctrl {
|
if modifiers.ctrl {
|
||||||
self.play_file(path, true);
|
self.play_file(&path, true);
|
||||||
} else if modifiers.shift
|
} else if modifiers.shift
|
||||||
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
||||||
{
|
{
|
||||||
self.stop(Some(last_track.id));
|
self.stop(Some(last_track.id));
|
||||||
self.play_file(path, true);
|
self.play_file(&path, true);
|
||||||
} else {
|
} else {
|
||||||
self.play_file(path, false);
|
self.play_file(&path, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,29 +80,23 @@ impl SoundpadGui {
|
|||||||
let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect();
|
let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect();
|
||||||
dirs.sort();
|
dirs.sort();
|
||||||
|
|
||||||
let current_dir_index: i8;
|
let current_dir_index = self
|
||||||
if let Some(current_dir) = &self.app_state.current_dir {
|
.app_state
|
||||||
if let Some(index) = dirs.iter().position(|x| x == current_dir) {
|
.current_dir
|
||||||
current_dir_index = index as i8;
|
.as_ref()
|
||||||
} else {
|
.and_then(|cd| dirs.iter().position(|x| x == cd));
|
||||||
current_dir_index = -1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current_dir_index = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut new_dir_index: i8;
|
let new_dir_index =
|
||||||
|
match (current_dir_index, arrow_up_pressed, arrow_down_pressed) {
|
||||||
|
(Some(i), true, false) => (i + dirs.len() - 1) % dirs.len(),
|
||||||
|
(Some(i), false, true) => (i + 1) % dirs.len(),
|
||||||
|
(Some(i), true, true) => i,
|
||||||
|
(None, true, _) => dirs.len() - 1,
|
||||||
|
(None, false, true) => 0,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
new_dir_index =
|
self.open_dir(&dirs[new_dir_index]);
|
||||||
current_dir_index - arrow_up_pressed as i8 + arrow_down_pressed as i8;
|
|
||||||
|
|
||||||
if new_dir_index < 0 {
|
|
||||||
new_dir_index = (dirs.len() - 1) as i8;
|
|
||||||
} else if new_dir_index >= dirs.len() as i8 {
|
|
||||||
new_dir_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.open_dir(&dirs[new_dir_index as usize]);
|
|
||||||
} else if self.app_state.current_dir.is_some() {
|
} else if self.app_state.current_dir.is_some() {
|
||||||
let files = self.get_filtered_files();
|
let files = self.get_filtered_files();
|
||||||
|
|
||||||
@@ -113,20 +108,19 @@ impl SoundpadGui {
|
|||||||
.app_state
|
.app_state
|
||||||
.selected_file
|
.selected_file
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|f| files.iter().position(|x| x == f))
|
.and_then(|f| files.iter().position(|x| x == f));
|
||||||
.map(|i| i as i64)
|
|
||||||
.unwrap_or(-1);
|
|
||||||
|
|
||||||
let mut new_files_index =
|
let new_files_index =
|
||||||
current_files_index - arrow_up_pressed as i64 + arrow_down_pressed as i64;
|
match (current_files_index, arrow_up_pressed, arrow_down_pressed) {
|
||||||
|
(Some(i), true, false) => (i + files.len() - 1) % files.len(),
|
||||||
|
(Some(i), false, true) => (i + 1) % files.len(),
|
||||||
|
(Some(i), true, true) => i,
|
||||||
|
(None, true, _) => files.len() - 1,
|
||||||
|
(None, false, true) => 0,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
if new_files_index < 0 {
|
self.app_state.selected_file = Some(files[new_files_index].clone());
|
||||||
new_files_index = (files.len() - 1) as i64;
|
|
||||||
} else if new_files_index >= files.len() as i64 {
|
|
||||||
new_files_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.app_state.selected_file = Some(files[new_files_index as usize].clone());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -58,7 +58,10 @@ impl SoundpadGui {
|
|||||||
|
|
||||||
pub fn play_toggle(&mut self) {
|
pub fn play_toggle(&mut self) {
|
||||||
let (new_state, request) = {
|
let (new_state, request) = {
|
||||||
let guard = self.audio_player_state_shared.lock().unwrap();
|
let guard = self
|
||||||
|
.audio_player_state_shared
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
match guard.state {
|
match guard.state {
|
||||||
PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause(None))),
|
PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause(None))),
|
||||||
PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume(None))),
|
PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume(None))),
|
||||||
@@ -71,7 +74,10 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(state) = new_state {
|
if let Some(state) = new_state {
|
||||||
let mut guard = self.audio_player_state_shared.lock().unwrap();
|
let mut guard = self
|
||||||
|
.audio_player_state_shared
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
guard.new_state = Some(state.clone());
|
guard.new_state = Some(state.clone());
|
||||||
guard.state = state;
|
guard.state = state;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-4
@@ -8,7 +8,7 @@ use pwsp::{
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
impl App for SoundpadGui {
|
impl App for SoundpadGui {
|
||||||
fn update(&mut self, ctx: &Context, _frame: &mut EFrame) {
|
fn logic(&mut self, ctx: &Context, _frame: &mut EFrame) {
|
||||||
// Remove directories
|
// Remove directories
|
||||||
for path in self.app_state.dirs_to_remove.drain() {
|
for path in self.app_state.dirs_to_remove.drain() {
|
||||||
self.app_state.dirs.retain(|x| x != &path);
|
self.app_state.dirs.retain(|x| x != &path);
|
||||||
@@ -77,7 +77,10 @@ impl App for SoundpadGui {
|
|||||||
|
|
||||||
// Sync audio player state
|
// Sync audio player state
|
||||||
{
|
{
|
||||||
let guard = self.audio_player_state_shared.lock().unwrap();
|
let guard = self
|
||||||
|
.audio_player_state_shared
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
self.audio_player_state = guard.clone();
|
self.audio_player_state = guard.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +97,11 @@ impl App for SoundpadGui {
|
|||||||
|
|
||||||
// Handle input
|
// Handle input
|
||||||
self.handle_input(ctx);
|
self.handle_input(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut EFrame) {
|
||||||
// Draw UI
|
// Draw UI
|
||||||
CentralPanel::default().show(ctx, |ui| {
|
CentralPanel::default().show_inside(ui, |ui| {
|
||||||
if !self.audio_player_state.is_daemon_running {
|
if !self.audio_player_state.is_daemon_running {
|
||||||
self.draw_waiting_for_daemon(ui);
|
self.draw_waiting_for_daemon(ui);
|
||||||
return;
|
return;
|
||||||
@@ -111,6 +116,6 @@ impl App for SoundpadGui {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Request repaint
|
// Request repaint
|
||||||
ctx.request_repaint_after_secs(1.0 / 60.0);
|
ui.request_repaint_after_secs(1.0 / 60.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-11
@@ -278,12 +278,22 @@ impl AudioPlayer {
|
|||||||
file_path: &Path,
|
file_path: &Path,
|
||||||
concurrent: bool,
|
concurrent: bool,
|
||||||
) -> Result<u32, Box<dyn Error>> {
|
) -> Result<u32, Box<dyn Error>> {
|
||||||
if !file_path.exists() {
|
let path_buf = file_path.to_path_buf();
|
||||||
return Err(format!("File does not exist: {}", file_path.display()).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = fs::File::open(file_path)?;
|
let decoder_result =
|
||||||
match Decoder::try_from(file) {
|
tokio::task::spawn_blocking(move || -> Result<_, Box<dyn Error + Send + Sync>> {
|
||||||
|
if !path_buf.exists() {
|
||||||
|
return Err(format!("File does not exist: {}", path_buf.display()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = fs::File::open(&path_buf)?;
|
||||||
|
let decoder = Decoder::try_from(file)
|
||||||
|
.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
|
||||||
|
Ok(decoder)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match decoder_result {
|
||||||
Ok(source) => {
|
Ok(source) => {
|
||||||
if !concurrent {
|
if !concurrent {
|
||||||
self.tracks.clear();
|
self.tracks.clear();
|
||||||
@@ -312,7 +322,7 @@ impl AudioPlayer {
|
|||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err as Box<dyn Error>),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,13 +384,28 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut restart_futures = vec![];
|
||||||
|
|
||||||
for id in restarts {
|
for id in restarts {
|
||||||
if let Some(sound) = self.tracks.get_mut(&id) {
|
if let Some(sound) = self.tracks.get(&id) {
|
||||||
if let Ok(file) = fs::File::open(&sound.path) {
|
let path = sound.path.clone();
|
||||||
if let Ok(source) = Decoder::try_from(file) {
|
let handle = tokio::task::spawn_blocking(move || {
|
||||||
sound.sink.append(source);
|
if let Ok(file) = fs::File::open(&path) {
|
||||||
sound.sink.play();
|
if let Ok(source) = Decoder::try_from(file) {
|
||||||
|
return Some((id, source));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
restart_futures.push(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for handle in restart_futures {
|
||||||
|
if let Ok(Some((id, source))) = handle.await {
|
||||||
|
if let Some(sound) = self.tracks.get_mut(&id) {
|
||||||
|
sound.sink.append(source);
|
||||||
|
sound.sink.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-29
@@ -107,7 +107,10 @@ impl Executable for KillCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for PauseCommand {
|
impl Executable for PauseCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
audio_player.pause(self.id);
|
audio_player.pause(self.id);
|
||||||
Response::new(true, "Audio was paused")
|
Response::new(true, "Audio was paused")
|
||||||
}
|
}
|
||||||
@@ -116,7 +119,10 @@ impl Executable for PauseCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for ResumeCommand {
|
impl Executable for ResumeCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
audio_player.resume(self.id);
|
audio_player.resume(self.id);
|
||||||
Response::new(true, "Audio was resumed")
|
Response::new(true, "Audio was resumed")
|
||||||
}
|
}
|
||||||
@@ -125,7 +131,10 @@ impl Executable for ResumeCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for TogglePauseCommand {
|
impl Executable for TogglePauseCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
|
|
||||||
if audio_player.get_state() == PlayerState::Stopped {
|
if audio_player.get_state() == PlayerState::Stopped {
|
||||||
return Response::new(false, "Audio is not playing");
|
return Response::new(false, "Audio is not playing");
|
||||||
@@ -163,7 +172,10 @@ impl Executable for TogglePauseCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for StopCommand {
|
impl Executable for StopCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
audio_player.stop(self.id);
|
audio_player.stop(self.id);
|
||||||
Response::new(true, "Audio was stopped")
|
Response::new(true, "Audio was stopped")
|
||||||
}
|
}
|
||||||
@@ -172,7 +184,10 @@ impl Executable for StopCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for IsPausedCommand {
|
impl Executable for IsPausedCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let audio_player = get_audio_player().await.lock().await;
|
let audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
let is_paused = audio_player.is_paused().to_string();
|
let is_paused = audio_player.is_paused().to_string();
|
||||||
Response::new(true, is_paused)
|
Response::new(true, is_paused)
|
||||||
}
|
}
|
||||||
@@ -181,7 +196,10 @@ impl Executable for IsPausedCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetStateCommand {
|
impl Executable for GetStateCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let audio_player = get_audio_player().await.lock().await;
|
let audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
let state = audio_player.get_state();
|
let state = audio_player.get_state();
|
||||||
match serde_json::to_string(&state) {
|
match serde_json::to_string(&state) {
|
||||||
Ok(json) => Response::new(true, json),
|
Ok(json) => Response::new(true, json),
|
||||||
@@ -193,11 +211,14 @@ impl Executable for GetStateCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetVolumeCommand {
|
impl Executable for GetVolumeCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
let volume = audio_player.get_volume(self.id);
|
let volume = audio_player.get_volume(self.id);
|
||||||
|
|
||||||
if let Some(volume) = volume {
|
if let Some(volume) = volume {
|
||||||
Response::new(true, volume.to_string())
|
Response::new(true, volume.to_string())
|
||||||
} else {
|
} else {
|
||||||
Response::new(false, "Failed to get volume")
|
Response::new(false, "Failed to get volume")
|
||||||
}
|
}
|
||||||
@@ -208,7 +229,10 @@ impl Executable for GetVolumeCommand {
|
|||||||
impl Executable for SetVolumeCommand {
|
impl Executable for SetVolumeCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
if let Some(volume) = self.volume {
|
if let Some(volume) = self.volume {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
audio_player.set_volume(volume, self.id);
|
audio_player.set_volume(volume, self.id);
|
||||||
Response::new(true, format!("Audio volume was set to {}", volume))
|
Response::new(true, format!("Audio volume was set to {}", volume))
|
||||||
} else {
|
} else {
|
||||||
@@ -220,7 +244,10 @@ impl Executable for SetVolumeCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetPositionCommand {
|
impl Executable for GetPositionCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let audio_player = get_audio_player().await.lock().await;
|
let audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
let position = audio_player.get_position(self.id);
|
let position = audio_player.get_position(self.id);
|
||||||
Response::new(true, position.to_string())
|
Response::new(true, position.to_string())
|
||||||
}
|
}
|
||||||
@@ -230,7 +257,10 @@ impl Executable for GetPositionCommand {
|
|||||||
impl Executable for SeekCommand {
|
impl Executable for SeekCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
if let Some(position) = self.position {
|
if let Some(position) = self.position {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
match audio_player.seek(position, self.id) {
|
match audio_player.seek(position, self.id) {
|
||||||
Ok(_) => Response::new(true, format!("Audio position was set to {}", position)),
|
Ok(_) => Response::new(true, format!("Audio position was set to {}", position)),
|
||||||
Err(err) => Response::new(false, err.to_string()),
|
Err(err) => Response::new(false, err.to_string()),
|
||||||
@@ -244,7 +274,10 @@ impl Executable for SeekCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetDurationCommand {
|
impl Executable for GetDurationCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
match audio_player.get_duration(self.id) {
|
match audio_player.get_duration(self.id) {
|
||||||
Ok(duration) => Response::new(true, duration.to_string()),
|
Ok(duration) => Response::new(true, duration.to_string()),
|
||||||
Err(err) => Response::new(false, err.to_string()),
|
Err(err) => Response::new(false, err.to_string()),
|
||||||
@@ -256,7 +289,10 @@ impl Executable for GetDurationCommand {
|
|||||||
impl Executable for PlayCommand {
|
impl Executable for PlayCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
if let Some(file_path) = &self.file_path {
|
if let Some(file_path) = &self.file_path {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
match audio_player
|
match audio_player
|
||||||
.play(file_path, self.concurrent.unwrap_or(false))
|
.play(file_path, self.concurrent.unwrap_or(false))
|
||||||
.await
|
.await
|
||||||
@@ -273,7 +309,10 @@ impl Executable for PlayCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetTracksCommand {
|
impl Executable for GetTracksCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let audio_player = get_audio_player().await.lock().await;
|
let audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
let tracks = audio_player.get_tracks();
|
let tracks = audio_player.get_tracks();
|
||||||
match serde_json::to_string(&tracks) {
|
match serde_json::to_string(&tracks) {
|
||||||
Ok(json) => Response::new(true, json),
|
Ok(json) => Response::new(true, json),
|
||||||
@@ -285,7 +324,10 @@ impl Executable for GetTracksCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for GetCurrentInputCommand {
|
impl Executable for GetCurrentInputCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let audio_player = get_audio_player().await.lock().await;
|
let audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
if let Some(input_device_name) = &audio_player.input_device_name {
|
if let Some(input_device_name) = &audio_player.input_device_name {
|
||||||
if let Ok(input_device) = get_device(input_device_name).await {
|
if let Ok(input_device) = get_device(input_device_name).await {
|
||||||
Response::new(
|
Response::new(
|
||||||
@@ -327,7 +369,10 @@ impl Executable for GetAllInputsCommand {
|
|||||||
impl Executable for SetCurrentInputCommand {
|
impl Executable for SetCurrentInputCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
if let Some(name) = &self.name {
|
if let Some(name) = &self.name {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
match audio_player.set_current_input_device(name).await {
|
match audio_player.set_current_input_device(name).await {
|
||||||
Ok(_) => Response::new(true, "Input device was set"),
|
Ok(_) => Response::new(true, "Input device was set"),
|
||||||
Err(err) => Response::new(false, err.to_string()),
|
Err(err) => Response::new(false, err.to_string()),
|
||||||
@@ -341,7 +386,10 @@ impl Executable for SetCurrentInputCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for SetLoopCommand {
|
impl Executable for SetLoopCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
|
|
||||||
match self.enabled {
|
match self.enabled {
|
||||||
Some(enabled) => {
|
Some(enabled) => {
|
||||||
@@ -356,7 +404,10 @@ impl Executable for SetLoopCommand {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Executable for ToggleLoopCommand {
|
impl Executable for ToggleLoopCommand {
|
||||||
async fn execute(&self) -> Response {
|
async fn execute(&self) -> Response {
|
||||||
let mut audio_player = get_audio_player().await.lock().await;
|
let mut audio_player = match get_audio_player().await {
|
||||||
|
Ok(player) => player.lock().await,
|
||||||
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
|
};
|
||||||
if let Some(id) = self.id {
|
if let Some(id) = self.id {
|
||||||
if let Some(track) = audio_player.tracks.get_mut(&id) {
|
if let Some(track) = audio_player.tracks.get_mut(&id) {
|
||||||
track.looped = !track.looped;
|
track.looped = !track.looped;
|
||||||
@@ -391,20 +442,29 @@ impl Executable for GetFullStateCommand {
|
|||||||
let mut all_inputs = HashMap::new();
|
let mut all_inputs = HashMap::new();
|
||||||
let mut current_input_nick = String::new();
|
let mut current_input_nick = String::new();
|
||||||
|
|
||||||
let audio_player = get_audio_player().await.lock().await;
|
let audio_player = match get_audio_player().await {
|
||||||
let current_input_name = audio_player.input_device_name.as_deref();
|
Ok(player) => player.lock().await,
|
||||||
for device in input_devices {
|
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
|
||||||
if device.name == "pwsp-virtual-mic" {
|
};
|
||||||
continue;
|
if let Some(current_input_name) = &audio_player.input_device_name {
|
||||||
}
|
for device in input_devices {
|
||||||
|
if device.name == "pwsp-virtual-mic" {
|
||||||
if let Some(name) = current_input_name {
|
continue;
|
||||||
if device.name == name {
|
}
|
||||||
|
if device.name == *current_input_name {
|
||||||
current_input_nick = format!("{} - {}", device.name, device.nick);
|
current_input_nick = format!("{} - {}", device.name, device.nick);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
all_inputs.insert(device.name, device.nick);
|
all_inputs.insert(device.name, device.nick);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for device in input_devices {
|
||||||
|
if device.name == "pwsp-virtual-mic" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
all_inputs.insert(device.name, device.nick);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let full_state = FullState {
|
let full_state = FullState {
|
||||||
|
|||||||
+16
-8
@@ -12,10 +12,11 @@ pub struct DaemonConfig {
|
|||||||
impl DaemonConfig {
|
impl DaemonConfig {
|
||||||
pub fn save_to_file(&self) -> Result<(), Box<dyn Error>> {
|
pub fn save_to_file(&self) -> Result<(), Box<dyn Error>> {
|
||||||
let config_path = get_config_path()?.join("daemon.json");
|
let config_path = get_config_path()?.join("daemon.json");
|
||||||
let config_dir = config_path.parent().unwrap();
|
|
||||||
|
|
||||||
if !config_path.exists() {
|
if let Some(config_dir) = config_path.parent() {
|
||||||
fs::create_dir_all(config_dir)?;
|
if !config_path.exists() {
|
||||||
|
fs::create_dir_all(config_dir)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_json = serde_json::to_string_pretty(self)?;
|
let config_json = serde_json::to_string_pretty(self)?;
|
||||||
@@ -26,7 +27,10 @@ impl DaemonConfig {
|
|||||||
pub fn load_from_file() -> Result<DaemonConfig, Box<dyn Error>> {
|
pub fn load_from_file() -> Result<DaemonConfig, Box<dyn Error>> {
|
||||||
let config_path = get_config_path()?.join("daemon.json");
|
let config_path = get_config_path()?.join("daemon.json");
|
||||||
let bytes = fs::read(config_path)?;
|
let bytes = fs::read(config_path)?;
|
||||||
Ok(serde_json::from_slice::<DaemonConfig>(&bytes)?)
|
match serde_json::from_slice::<DaemonConfig>(&bytes) {
|
||||||
|
Ok(config) => Ok(config),
|
||||||
|
Err(_) => Ok(DaemonConfig::default()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +67,11 @@ impl Default for GuiConfig {
|
|||||||
impl GuiConfig {
|
impl GuiConfig {
|
||||||
pub fn save_to_file(&mut self) -> Result<(), Box<dyn Error>> {
|
pub fn save_to_file(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
let config_path = get_config_path()?.join("gui.json");
|
let config_path = get_config_path()?.join("gui.json");
|
||||||
let config_dir = config_path.parent().unwrap();
|
|
||||||
|
|
||||||
if !config_path.exists() {
|
if let Some(config_dir) = config_path.parent() {
|
||||||
fs::create_dir_all(config_dir)?;
|
if !config_path.exists() {
|
||||||
|
fs::create_dir_all(config_dir)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not save scale factor if user does not want to
|
// Do not save scale factor if user does not want to
|
||||||
@@ -82,6 +87,9 @@ impl GuiConfig {
|
|||||||
pub fn load_from_file() -> Result<GuiConfig, Box<dyn Error>> {
|
pub fn load_from_file() -> Result<GuiConfig, Box<dyn Error>> {
|
||||||
let config_path = get_config_path()?.join("gui.json");
|
let config_path = get_config_path()?.join("gui.json");
|
||||||
let bytes = fs::read(config_path)?;
|
let bytes = fs::read(config_path)?;
|
||||||
Ok(serde_json::from_slice::<GuiConfig>(&bytes)?)
|
match serde_json::from_slice::<GuiConfig>(&bytes) {
|
||||||
|
Ok(config) => Ok(config),
|
||||||
|
Err(_) => Ok(GuiConfig::default()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub struct AudioPlayerState {
|
|||||||
|
|
||||||
pub current_input: String,
|
pub current_input: String,
|
||||||
pub all_inputs: HashMap<String, String>,
|
pub all_inputs: HashMap<String, String>,
|
||||||
|
pub all_inputs_sorted: Vec<(String, String)>,
|
||||||
|
|
||||||
pub is_daemon_running: bool,
|
pub is_daemon_running: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -6,6 +6,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
utils::pipewire::{create_link, get_device},
|
utils::pipewire::{create_link, get_device},
|
||||||
};
|
};
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{error::Error, fs};
|
use std::{error::Error, fs};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
@@ -17,11 +18,14 @@ use tokio::{
|
|||||||
|
|
||||||
static AUDIO_PLAYER: OnceCell<Mutex<AudioPlayer>> = OnceCell::const_new();
|
static AUDIO_PLAYER: OnceCell<Mutex<AudioPlayer>> = OnceCell::const_new();
|
||||||
|
|
||||||
pub async fn get_audio_player() -> &'static Mutex<AudioPlayer> {
|
pub async fn get_audio_player() -> Result<&'static Mutex<AudioPlayer>, String> {
|
||||||
AUDIO_PLAYER
|
AUDIO_PLAYER
|
||||||
.get_or_init(|| async {
|
.get_or_try_init(|| async {
|
||||||
println!("Initializing audio player");
|
println!("Initializing audio player");
|
||||||
Mutex::new(AudioPlayer::new().await.unwrap())
|
match AudioPlayer::new().await {
|
||||||
|
Ok(player) => Ok(Mutex::new(player)),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -81,6 +85,7 @@ pub fn create_runtime_dir() -> Result<(), Box<dyn Error>> {
|
|||||||
if !runtime_dir.exists() {
|
if !runtime_dir.exists() {
|
||||||
fs::create_dir_all(&runtime_dir)?;
|
fs::create_dir_all(&runtime_dir)?;
|
||||||
}
|
}
|
||||||
|
fs::set_permissions(&runtime_dir, fs::Permissions::from_mode(0o700))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -55,7 +55,9 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
|
|||||||
|
|
||||||
if !is_running {
|
if !is_running {
|
||||||
{
|
{
|
||||||
let mut guard = audio_player_state_shared.lock().unwrap();
|
let mut guard = audio_player_state_shared
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
guard.is_daemon_running = false;
|
guard.is_daemon_running = false;
|
||||||
}
|
}
|
||||||
sleep(Duration::from_millis(500)).await;
|
sleep(Duration::from_millis(500)).await;
|
||||||
@@ -69,7 +71,9 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
|
|||||||
let full_state: FullState =
|
let full_state: FullState =
|
||||||
serde_json::from_str(&full_state_res.message).unwrap_or_default();
|
serde_json::from_str(&full_state_res.message).unwrap_or_default();
|
||||||
|
|
||||||
let mut guard = audio_player_state_shared.lock().unwrap();
|
let mut guard = audio_player_state_shared
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
|
|
||||||
guard.state = match guard.new_state.clone() {
|
guard.state = match guard.new_state.clone() {
|
||||||
Some(new_state) => {
|
Some(new_state) => {
|
||||||
@@ -86,7 +90,18 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
|
|||||||
.next()
|
.next()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string();
|
.to_string();
|
||||||
guard.all_inputs = full_state.all_inputs;
|
|
||||||
|
if guard.all_inputs != full_state.all_inputs {
|
||||||
|
guard.all_inputs = full_state.all_inputs;
|
||||||
|
let mut sorted: Vec<(String, String)> = guard
|
||||||
|
.all_inputs
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect();
|
||||||
|
sorted.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
guard.all_inputs_sorted = sorted;
|
||||||
|
}
|
||||||
|
|
||||||
guard.is_daemon_running = true;
|
guard.is_daemon_running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-27
@@ -57,17 +57,19 @@ fn parse_global_object(
|
|||||||
};
|
};
|
||||||
// Check if the object is a port
|
// Check if the object is a port
|
||||||
} else if props.get("port.direction").is_some() {
|
} else if props.get("port.direction").is_some() {
|
||||||
let node_id = props.get("node.id").unwrap().parse::<u32>().unwrap();
|
if let (Some(node_id), Some(port_id), Some(port_name)) = (
|
||||||
let port_id = props.get("port.id").unwrap().parse::<u32>().unwrap();
|
props.get("node.id").and_then(|id| id.parse::<u32>().ok()),
|
||||||
let port_name = props.get("port.name").unwrap();
|
props.get("port.id").and_then(|id| id.parse::<u32>().ok()),
|
||||||
|
props.get("port.name"),
|
||||||
|
) {
|
||||||
|
let port = Port {
|
||||||
|
node_id,
|
||||||
|
port_id,
|
||||||
|
name: port_name.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
let port = Port {
|
return (None, Some(port));
|
||||||
node_id,
|
}
|
||||||
port_id,
|
|
||||||
name: port_name.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (None, Some(port));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(None, None)
|
(None, None)
|
||||||
@@ -151,8 +153,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
for port in ports {
|
for port in ports {
|
||||||
let node_id = port.node_id;
|
let node_id = port.node_id;
|
||||||
|
|
||||||
if input_devices.contains_key(&node_id) {
|
if let Some(input_device) = input_devices.get_mut(&node_id) {
|
||||||
let input_device = input_devices.get_mut(&node_id).unwrap();
|
|
||||||
match port.name.as_str() {
|
match port.name.as_str() {
|
||||||
"input_FL" => input_device.input_fl = Some(port),
|
"input_FL" => input_device.input_fl = Some(port),
|
||||||
"input_FR" => input_device.input_fr = Some(port),
|
"input_FR" => input_device.input_fr = Some(port),
|
||||||
@@ -170,8 +171,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else if output_devices.contains_key(&node_id) {
|
} else if let Some(output_device) = output_devices.get_mut(&node_id) {
|
||||||
let output_device = output_devices.get_mut(&node_id).unwrap();
|
|
||||||
match port.name.as_str() {
|
match port.name.as_str() {
|
||||||
"input_FL" => output_device.input_fl = Some(port),
|
"input_FL" => output_device.input_fl = Some(port),
|
||||||
"input_FR" => output_device.input_fr = Some(port),
|
"input_FR" => output_device.input_fr = Some(port),
|
||||||
@@ -206,20 +206,18 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>> {
|
pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>> {
|
||||||
let (mut input_devices, output_devices) = get_all_devices().await?;
|
let (input_devices, output_devices) = get_all_devices().await?;
|
||||||
input_devices.extend(output_devices);
|
|
||||||
|
|
||||||
for device in input_devices {
|
input_devices
|
||||||
if device.name == device_name
|
.into_iter()
|
||||||
|| device.nick == device_name
|
.chain(output_devices)
|
||||||
|| device.name.contains(device_name)
|
.find(|device| {
|
||||||
|| device.nick.contains(device_name)
|
device.name == device_name
|
||||||
{
|
|| device.nick == device_name
|
||||||
return Ok(device);
|
|| device.name.contains(device_name)
|
||||||
}
|
|| device.nick.contains(device_name)
|
||||||
}
|
})
|
||||||
|
.ok_or_else(|| "Device not found".into())
|
||||||
Err("Device not found".into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>> {
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
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