Compare commits

..

14 Commits

Author SHA1 Message Date
arabianq 6c59137639 fix icon 2026-06-01 23:37:46 +03:00
arabianq 3693a678ea new icon 2026-06-01 23:20:11 +03:00
arabianq 5511d23c3e deps: update cargo-sources.json 2026-06-01 23:05:02 +03:00
arabianq 818cd8b50d deps: cargo update 2026-06-01 23:04:42 +03:00
arabianq 6f7d631e28 deps: update rodio 2026-06-01 23:03:21 +03:00
arabianq 18904052c7 change version to 1.10.0 2026-06-01 23:01:32 +03:00
Tarasov Aleksandr 6841d8d1c3 refactor(gui): break down monolithic draw_footer into helper methods (#127)
Split the long continuous block in `draw_footer` into smaller,
modular methods (`draw_mic_selection`, `draw_master_volume`,
`draw_hotkeys_button`, `draw_settings_button`) for better
readability and maintainability.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-01 22:56:37 +03:00
Tarasov Aleksandr 105be87222 refactor(daemon): Refactor commands_loop to use handle_connection (#126)
- Extracted the main token processing loop body in `commands_loop` into `handle_connection` to resolve deep nesting and improve code readability.
- Improved request reading logic by using `(&mut stream).take(request_len as u64).read_to_end(&mut buffer)` to strictly bound allocation to `request_len` and prevent initialization overhead.
- Passed `cargo fmt` and `cargo clippy`.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-01 22:54:27 +03:00
Tarasov Aleksandr 0f8abbc443 refactor(daemon): Refactor src/utils/pipewire.rs to flatten deep conditionals and consolidate logic. (#124)
Moved redundant struct initialization into `AudioDevice::new` and unified port mapping assignments in an `add_port` method. This removes nesting using early returns and eliminates an unnecessary clone on the hashmap conversion step.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-01 22:51:58 +03:00
Tarasov Aleksandr 54011e7ff1 fix(daemon): Replace unwrap with safe Option handling in audio_player (#125)
The `play` method in `src/types/audio_player.rs` previously used `.unwrap()`
directly on `self.stream_handle.as_ref()`. This posed a security/stability risk
where if `stream_handle` was uninitialized or became `None` unexpectedly
despite the prior `ensure_stream()` call, it would cause the thread to panic
and potentially crash the application.

This commit replaces the `.unwrap()` call with `.ok_or_else` to safely handle
the `None` case, returning an `anyhow` error instead of panicking, adhering to
the project's no-panic policy.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-01 22:48:08 +03:00
arabianq dac9d53cef deps: update cargo-sources.json 2026-05-28 01:09:33 +03:00
arabianq 9da3799cd3 cargo update 2026-05-28 01:08:52 +03:00
arabianq d66369884c deps: update rodio 2026-05-28 01:08:13 +03:00
Tarasov Aleksandr 5e47e7d6fb feat(gui): support for soundpad:// uri (#123)
* feat(gui): support for soundpad:// uri

* fix: flatpak

* do not open gui when downloading file
2026-05-28 00:58:03 +03:00
21 changed files with 1988 additions and 817 deletions
Generated
+716 -192
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pwsp" name = "pwsp"
version = "1.9.1" version = "1.10.0"
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."
@@ -43,7 +43,7 @@ rustix = { version = "1.1.4", features = ["process"] }
rust-i18n = "4.0.0" rust-i18n = "4.0.0"
sys-locale = "0.3.2" sys-locale = "0.3.2"
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "1a08f281c352622bd82b87b8731585245802d9cf", default-features = false, features = [ rodio = { git = "https://github.com/arabianq/rodio.git", rev = "a634dd471e9d59196e19bf01323fb45f2f899821", default-features = false, features = [
"symphonia-all", "symphonia-all",
"symphonia-libopus", "symphonia-libopus",
"playback", "playback",
@@ -64,6 +64,9 @@ egui_extras = "0.34.1"
egui_material_icons = "0.6.0" egui_material_icons = "0.6.0"
egui_dnd = "0.15.0" egui_dnd = "0.15.0"
reqwest = "0.13.4"
percent-encoding = "2.3.2"
[[bin]] [[bin]]
name = "pwsp-daemon" name = "pwsp-daemon"
path = "src/bin/daemon.rs" path = "src/bin/daemon.rs"
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 79 KiB

+2 -1
View File
@@ -1,8 +1,9 @@
[Desktop Entry] [Desktop Entry]
Name=PWSP (Soundpad) Name=PWSP (Soundpad)
Comment=Let's you play audio files through you microphone Comment=Let's you play audio files through you microphone
Exec=pwsp-gui %u Exec=/usr/bin/pwsp-gui %u
Icon=pwsp Icon=pwsp
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Audio Categories=Audio
MimeType=x-scheme-handler/soundpad;
+3 -3
View File
@@ -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.9.1 pkgver = 1.10.0
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64 arch = x86_64
@@ -9,8 +9,8 @@ depends = pipewire
depends = alsa-lib depends = alsa-lib
provides = pwsp provides = pwsp
conflicts = pwsp conflicts = pwsp
source = pwsp-bin-1.9.1.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.9.1/pwsp-v1.9.1-linux-x64.zip source = pwsp-bin-1.10.0.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.10.0/pwsp-v1.10.0-linux-x64.zip
source = pipewire-soundpad-1.9.1.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.1.tar.gz source = pipewire-soundpad-1.10.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.10.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
sha256sums = SKIP sha256sums = SKIP
+1 -1
View File
@@ -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.9.1 pkgver=1.10.0
pkgrel=1 pkgrel=1
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')
+3 -3
View File
@@ -1,7 +1,7 @@
pkgbase = pwsp pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone pkgdesc = Lets you play audio files through your microphone
pkgver = 1.9.1 pkgver = 1.10.0
pkgrel = 2 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = any arch = any
license = MIT license = MIT
@@ -11,7 +11,7 @@ pkgbase = pwsp
makedepends = cmake makedepends = cmake
makedepends = pipewire makedepends = pipewire
makedepends = alsa-lib makedepends = alsa-lib
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.1.tar.gz source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.10.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
pkgname = pwsp pkgname = pwsp
+2 -2
View File
@@ -1,8 +1,8 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp pkgsubn=pwsp
pkgname=pwsp pkgname=pwsp
pkgver=1.9.1 pkgver=1.10.0
pkgrel=2 pkgrel=1
pkgdesc="Lets you play audio files through your microphone" pkgdesc="Lets you play audio files through your microphone"
arch=('any') arch=('any')
url="https://github.com/arabianq/pipewire-soundpad" url="https://github.com/arabianq/pipewire-soundpad"
File diff suppressed because one or more lines are too long
+10 -5
View File
@@ -2,21 +2,26 @@
import argparse import argparse
import subprocess import subprocess
import sys
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) == 2 and sys.argv[1].startswith("soundpad://"):
subprocess.Popen(["pwsp-gui", sys.argv[1]])
sys.exit(0)
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="PWSP Flatpak", prog="PWSP Flatpak", add_help=True, exit_on_error=True
add_help=True,
exit_on_error=True
) )
subparsers = parser.add_subparsers(dest="command") subparsers = parser.add_subparsers(dest="command")
cli_parser = subparsers.add_parser("cli", add_help=False, prefix_chars=" ") cli_parser = subparsers.add_parser("cli", add_help=False, prefix_chars=" ")
cli_parser.add_argument("args", nargs=argparse.REMAINDER, help="Arguments for pwsp-cli") cli_parser.add_argument(
"args", nargs=argparse.REMAINDER, help="Arguments for pwsp-cli"
)
daemon_parser = subparsers.add_parser("daemon", add_help=True) daemon_parser = subparsers.add_parser("daemon", add_help=True)
daemon_group = daemon_parser.add_mutually_exclusive_group(required=True) daemon_group = daemon_parser.add_mutually_exclusive_group(required=True)
daemon_group.add_argument("--start", action="store_true", help="Start pwps-daemon") daemon_group.add_argument("--start", action="store_true", help="Start pwsp-daemon")
daemon_group.add_argument("--kill", action="store_true", help="Kill pwsp-daemon") daemon_group.add_argument("--kill", action="store_true", help="Kill pwsp-daemon")
args = parser.parse_args() args = parser.parse_args()
@@ -7,3 +7,4 @@ Terminal=false
Type=Application Type=Application
Categories=AudioVideo;Audio; Categories=AudioVideo;Audio;
Keywords=soundpad;pipewire;audio; Keywords=soundpad;pipewire;audio;
MimeType=x-scheme-handler/soundpad;
@@ -25,7 +25,7 @@
<name>arabian</name> <name>arabian</name>
</developer> </developer>
<releases> <releases>
<release version="1.9.1" date="2026-05-22" /> <release version="1.10.0" date="2026-06-01" />
</releases> </releases>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
</component> </component>
+1 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0 %global cargo_install_lib 0
Name: pwsp Name: pwsp
Version: 1.9.1 Version: 1.10.0
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
+17 -8
View File
@@ -15,7 +15,7 @@ use std::os::unix::fs::PermissionsExt;
use std::{fs, time::Duration}; use std::{fs, time::Duration};
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
net::UnixListener, net::{UnixListener, UnixStream},
time::sleep, time::sleep,
}; };
@@ -83,9 +83,15 @@ async fn main() -> Result<()> {
async fn commands_loop(listener: UnixListener) -> Result<()> { async fn commands_loop(listener: UnixListener) -> Result<()> {
loop { loop {
let (mut stream, _addr) = listener.accept().await?; let (stream, _addr) = listener.accept().await?;
tokio::spawn(async move { tokio::spawn(async move {
handle_connection(stream).await;
});
}
}
async fn handle_connection(mut stream: UnixStream) {
// ---------- Read request (start) ---------- // ---------- Read request (start) ----------
let mut len_bytes = [0u8; 4]; let mut len_bytes = [0u8; 4];
if stream.read_exact(&mut len_bytes).await.is_err() { if stream.read_exact(&mut len_bytes).await.is_err() {
@@ -103,8 +109,14 @@ async fn commands_loop(listener: UnixListener) -> Result<()> {
return; return;
} }
let mut buffer = vec![0u8; request_len]; let mut buffer = Vec::new();
if stream.read_exact(&mut buffer).await.is_err() { if (&mut stream)
.take(request_len as u64)
.read_to_end(&mut buffer)
.await
.is_err()
|| buffer.len() != request_len
{
eprintln!("Failed to read message from client!"); eprintln!("Failed to read message from client!");
return; return;
} }
@@ -112,8 +124,7 @@ async fn commands_loop(listener: UnixListener) -> Result<()> {
let request: Request = match serde_json::from_slice(&buffer) { let request: Request = match serde_json::from_slice(&buffer) {
Ok(req) => req, Ok(req) => req,
Err(err) => { Err(err) => {
let response = let response = Response::new(false, format!("Failed to parse request: {}", err));
Response::new(false, format!("Failed to parse request: {}", err));
let response_data = match serde_json::to_vec(&response) { let response_data = match serde_json::to_vec(&response) {
Ok(data) => data, Ok(data) => data,
Err(_) => return, // Should not happen with this simple Response Err(_) => return, // Should not happen with this simple Response
@@ -159,8 +170,6 @@ async fn commands_loop(listener: UnixListener) -> Result<()> {
if response.status && response.message.eq("killed") { if response.status && response.message.eq("killed") {
std::process::exit(0); std::process::exit(0);
} }
});
}
} }
async fn player_loop() { async fn player_loop() {
+17 -11
View File
@@ -8,7 +8,17 @@ impl SoundpadGui {
pub fn draw_footer(&mut self, ui: &mut Ui) { pub fn draw_footer(&mut self, ui: &mut Ui) {
ui.add_space(5.0); ui.add_space(5.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
// ---------- Microphone selection ---------- self.draw_mic_selection(ui);
self.draw_master_volume(ui);
ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0);
self.draw_hotkeys_button(ui);
self.draw_settings_button(ui);
});
}
fn draw_mic_selection(&mut self, ui: &mut Ui) {
let mics = &self.audio_player_state.all_inputs_sorted; let mics = &self.audio_player_state.all_inputs_sorted;
let mut selected_input = self.audio_player_state.current_input.to_owned(); let mut selected_input = self.audio_player_state.current_input.to_owned();
@@ -30,9 +40,9 @@ impl SoundpadGui {
if selected_input != prev_input { if selected_input != prev_input {
self.set_input(selected_input); self.set_input(selected_input);
} }
// -------------------------------- }
// ---------- Master Volume Slider ---------- fn draw_master_volume(&mut self, ui: &mut Ui) {
let volume_icon = Self::get_volume_icon(self.audio_player_state.volume); let volume_icon = Self::get_volume_icon(self.audio_player_state.volume);
let volume_label = Label::new(RichText::new(volume_icon).size(18.0)); let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
ui.add_sized([18.0, 18.0], volume_label) ui.add_sized([18.0, 18.0], volume_label)
@@ -59,11 +69,9 @@ impl SoundpadGui {
if volume_slider_response.drag_stopped() { if volume_slider_response.drag_stopped() {
self.app_state.volume_dragged = true; self.app_state.volume_dragged = true;
} }
// ------------------------------------------ }
ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0); fn draw_hotkeys_button(&mut self, ui: &mut Ui) {
// ---------- Hotkeys button ----------
let hotkeys_button = let hotkeys_button =
Button::new(ICON_KEYBOARD.atom_size(Vec2::new(18.0, 18.0))).frame(false); Button::new(ICON_KEYBOARD.atom_size(Vec2::new(18.0, 18.0))).frame(false);
let hotkeys_button_response = ui.add_sized([18.0, 18.0], hotkeys_button); let hotkeys_button_response = ui.add_sized([18.0, 18.0], hotkeys_button);
@@ -71,16 +79,14 @@ impl SoundpadGui {
self.app_state.show_hotkeys = true; self.app_state.show_hotkeys = true;
} }
hotkeys_button_response.on_hover_text("Hotkeys (H)"); hotkeys_button_response.on_hover_text("Hotkeys (H)");
// -------------------------------- }
// ---------- Settings button ---------- fn draw_settings_button(&mut self, ui: &mut Ui) {
let settings_button = let settings_button =
Button::new(ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false); Button::new(ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false);
let settings_button_response = ui.add_sized([18.0, 18.0], settings_button); let settings_button_response = ui.add_sized([18.0, 18.0], settings_button);
if settings_button_response.clicked() { if settings_button_response.clicked() {
self.app_state.show_settings = true; self.app_state.show_settings = true;
} }
// --------------------------------
});
} }
} }
+48 -2
View File
@@ -1,7 +1,9 @@
mod gui; mod gui;
use anyhow::Result; use anyhow::{Context, Result};
use pwsp::utils::gui::ensure_pwsp_audio_dir;
use rust_i18n::i18n; use rust_i18n::i18n;
use std::{env, path::PathBuf};
i18n!("locales", fallback = "en"); i18n!("locales", fallback = "en");
@@ -10,5 +12,49 @@ async fn main() -> Result<()> {
let locale = sys_locale::get_locale().unwrap_or(String::from("en-US")); let locale = sys_locale::get_locale().unwrap_or(String::from("en-US"));
rust_i18n::set_locale(&locale); rust_i18n::set_locale(&locale);
gui::run().await let args = env::args().skip(1).collect::<Vec<String>>();
if let Some(uri) = args.first() {
match download_audio_from_url(uri).await {
Ok(path) => println!("Successfully downloaded to: {:?}", path),
Err(e) => eprintln!("Error downloading file: {}", e),
}
} else {
gui::run().await?;
}
Ok(())
}
async fn download_audio_from_url(uri: &str) -> Result<PathBuf> {
let prefix = "soundpad://sound/url/";
let target_url = uri
.strip_prefix(prefix)
.ok_or_else(|| anyhow::anyhow!("URI does not containt an expected prefix: {}", prefix))?;
let file_name_encoded = target_url
.split('/')
.next_back()
.unwrap_or("downloaded_audio.mp3");
let file_name = percent_encoding::percent_decode_str(file_name_encoded)
.decode_utf8()
.unwrap_or_else(|_| file_name_encoded.into())
.into_owned();
let save_path = ensure_pwsp_audio_dir().join(file_name);
let response = reqwest::get(target_url)
.await?
.error_for_status()
.context("Failed to fetch file")?;
let bytes = response.bytes().await?;
tokio::fs::write(&save_path, bytes)
.await
.context("Failed to save file to disk")?;
Ok(save_path)
} }
+5 -1
View File
@@ -349,7 +349,11 @@ impl AudioPlayer {
let duration = source.total_duration().map(|d| d.as_secs_f32()); let duration = source.total_duration().map(|d| d.as_secs_f32());
let mixer = self.stream_handle.as_ref().unwrap().mixer(); let mixer = self
.stream_handle
.as_ref()
.ok_or_else(|| anyhow::anyhow!("stream_handle is unexpectedly missing"))?
.mixer();
let sink = Player::connect_new(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);
+5 -2
View File
@@ -1,4 +1,7 @@
use crate::{types::socket::Request, utils::config::get_config_path}; use crate::{
types::socket::Request,
utils::{config::get_config_path, gui::ensure_pwsp_audio_dir},
};
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::PathBuf}; use std::{collections::HashMap, fs, path::PathBuf};
@@ -69,7 +72,7 @@ impl Default for GuiConfig {
save_scale_factor: false, save_scale_factor: false,
pause_on_exit: false, pause_on_exit: false,
dirs: vec![], dirs: vec![ensure_pwsp_audio_dir()],
preferred_theme: PreferredTheme::System, preferred_theme: PreferredTheme::System,
} }
+43
View File
@@ -29,3 +29,46 @@ pub struct AudioDevice {
pub output_fl: Option<Port>, pub output_fl: Option<Port>,
pub output_fr: Option<Port>, pub output_fr: Option<Port>,
} }
impl AudioDevice {
pub fn new(
id: u32,
nick: Option<&str>,
description: Option<&str>,
name: Option<&str>,
device_type: DeviceType,
) -> Self {
Self {
id,
nick: nick
.or(description)
.or(name)
.unwrap_or_default()
.to_string(),
name: name.unwrap_or_default().to_string(),
device_type,
input_fl: None,
input_fr: None,
output_fl: None,
output_fr: None,
}
}
pub fn add_port(&mut self, port: Port) {
match port.name.as_str() {
"input_FL" => self.input_fl = Some(port),
"input_FR" => self.input_fr = Some(port),
"output_FL" | "capture_FL" => self.output_fl = Some(port),
"output_FR" | "capture_FR" => self.output_fr = Some(port),
"input_MONO" => {
self.input_fl = Some(port.clone());
self.input_fr = Some(port);
}
"output_MONO" | "capture_MONO" => {
self.output_fl = Some(port.clone());
self.output_fr = Some(port);
}
_ => {}
}
}
}
+12
View File
@@ -9,6 +9,7 @@ use crate::{
}; };
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use std::{ use std::{
path::PathBuf,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Instant, time::Instant,
}; };
@@ -36,6 +37,17 @@ pub fn make_request_async(request: Request) {
}); });
} }
pub fn ensure_pwsp_audio_dir() -> PathBuf {
let audio_dir = dirs::audio_dir().unwrap_or("~/Music".into());
let pwsp_audio_dir = audio_dir.join("PWSP");
if !pwsp_audio_dir.exists() {
std::fs::create_dir_all(&pwsp_audio_dir).ok();
}
pwsp_audio_dir
}
pub fn format_time_pair(position: f32, duration: f32) -> String { pub fn format_time_pair(position: f32, duration: f32) -> String {
fn format_time(seconds: f32) -> String { fn format_time(seconds: f32) -> String {
let total_seconds = seconds.round() as u32; let total_seconds = seconds.round() as u32;
+31 -76
View File
@@ -20,51 +20,40 @@ pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
fn parse_global_object( fn parse_global_object(
global_object: &GlobalObject<&DictRef>, global_object: &GlobalObject<&DictRef>,
) -> (Option<AudioDevice>, Option<Port>) { ) -> (Option<AudioDevice>, Option<Port>) {
// Only objects with props can be devices/ports let props = match global_object.props {
if let Some(props) = global_object.props { Some(p) => p,
// Only objects with media.class can be devices None => return (None, None),
};
if let Some(media_class) = props.get("media.class") { if let Some(media_class) = props.get("media.class") {
let node_id = global_object.id; let node_id = global_object.id;
let node_nick = props.get("node.nick"); let node_nick = props.get("node.nick");
let node_name = props.get("node.name"); let node_name = props.get("node.name");
let node_description = props.get("node.description"); let node_description = props.get("node.description");
// Check if the device is an input or output if media_class.starts_with("Audio/Source") {
return if media_class.starts_with("Audio/Source") { let input_device = AudioDevice::new(
let input_device = AudioDevice { node_id,
id: node_id, node_nick,
nick: node_nick node_description,
.unwrap_or(node_description.unwrap_or(node_name.unwrap_or_default())) node_name,
.to_string(), DeviceType::Input,
name: node_name.unwrap_or_default().to_string(), );
device_type: DeviceType::Input, return (Some(input_device), None);
input_fl: None,
input_fr: None,
output_fl: None,
output_fr: None,
};
(Some(input_device), None)
} else if media_class.starts_with("Stream/Output/Audio") { } else if media_class.starts_with("Stream/Output/Audio") {
let output_device = AudioDevice { let output_device = AudioDevice::new(
id: node_id, node_id,
nick: node_nick node_nick,
.unwrap_or(node_description.unwrap_or(node_name.unwrap_or_default())) node_description,
.to_string(), node_name,
name: node_name.unwrap_or_default().to_string(), DeviceType::Output,
device_type: DeviceType::Output, );
return (Some(output_device), None);
}
return (None, None);
}
input_fl: None, if props.get("port.direction").is_some()
input_fr: None,
output_fl: None,
output_fr: None,
};
(Some(output_device), None)
} else {
(None, None)
};
// Check if the object is a port
} else if props.get("port.direction").is_some()
&& let (Some(node_id), Some(port_id), Some(port_name)) = ( && let (Some(node_id), Some(port_id), Some(port_name)) = (
props.get("node.id").and_then(|id| id.parse::<u32>().ok()), props.get("node.id").and_then(|id| id.parse::<u32>().ok()),
props.get("port.id").and_then(|id| id.parse::<u32>().ok()), props.get("port.id").and_then(|id| id.parse::<u32>().ok()),
@@ -76,10 +65,9 @@ fn parse_global_object(
port_id, port_id,
name: port_name.to_string(), name: port_name.to_string(),
}; };
return (None, Some(port)); return (None, Some(port));
} }
}
(None, None) (None, None)
} }
@@ -188,47 +176,14 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>)> {
let node_id = port.node_id; let node_id = port.node_id;
if let Some(input_device) = input_devices.get_mut(&node_id) { if let Some(input_device) = input_devices.get_mut(&node_id) {
match port.name.as_str() { input_device.add_port(port);
"input_FL" => input_device.input_fl = Some(port),
"input_FR" => input_device.input_fr = Some(port),
"output_FL" => input_device.output_fl = Some(port),
"output_FR" => input_device.output_fr = Some(port),
"capture_FL" => input_device.output_fl = Some(port),
"capture_FR" => input_device.output_fr = Some(port),
"input_MONO" => {
input_device.input_fl = Some(port.clone());
input_device.input_fr = Some(port)
}
"capture_MONO" => {
input_device.output_fl = Some(port.clone());
input_device.output_fr = Some(port);
}
_ => {}
}
} else if let Some(output_device) = output_devices.get_mut(&node_id) { } else if let Some(output_device) = output_devices.get_mut(&node_id) {
match port.name.as_str() { output_device.add_port(port);
"input_FL" => output_device.input_fl = Some(port),
"input_FR" => output_device.input_fr = Some(port),
"output_FL" => output_device.output_fl = Some(port),
"output_FR" => output_device.output_fr = Some(port),
"capture_FL" => output_device.output_fl = Some(port),
"capture_FR" => output_device.output_fr = Some(port),
"output_MONO" => {
output_device.output_fl = Some(port.clone());
output_device.output_fr = Some(port)
}
"capture_MONO" => {
output_device.output_fl = Some(port.clone());
output_device.output_fr = Some(port)
}
_ => {}
}
} }
} }
let mut input_devices: Vec<AudioDevice> = input_devices.values().cloned().collect(); let mut input_devices: Vec<AudioDevice> = input_devices.into_values().collect();
let mut output_devices: Vec<AudioDevice> = let mut output_devices: Vec<AudioDevice> = output_devices.into_values().collect();
output_devices.values().cloned().collect();
input_devices.sort_by_key(|a| a.id); input_devices.sort_by_key(|a| a.id);
output_devices.sort_by_key(|a| a.id); output_devices.sort_by_key(|a| a.id);