From 2a8fcca06b5a3d6642bd97e23e842596d818245a Mon Sep 17 00:00:00 2001 From: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:24:58 +0300 Subject: [PATCH] Fix virtual mic audio linking (#62) * Fix virtual mic audio linking by managing it in AudioPlayer lifecycle - Moved `link_player_to_virtual_mic` to `src/utils/pipewire.rs` and updated it to return a termination sender. - Added `player_link_sender` to `AudioPlayer` to manage the PipeWire link between the daemon and the virtual mic. - Integrated linking logic into `AudioPlayer::play` and `AudioPlayer::update` to ensure the link is established when audio starts playing. - Ensured the link is terminated in `AudioPlayer::drop_stream` when the audio sink is closed. - Removed redundant and potentially failing startup linking loop from the daemon. - Fixed log spam by ensuring `link_player` is only attempted when necessary and errors are handled gracefully. - Maintained compatibility with stable Rust by avoiding unstable features. Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com> * small refactor * refactor --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- src/bin/daemon.rs | 23 ++-------------- src/gui/input.rs | 14 +++++++--- src/types/audio_player.rs | 56 +++++++++++++++++++++++++++++++-------- src/utils/daemon.rs | 42 ++--------------------------- src/utils/pipewire.rs | 38 ++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 76 deletions(-) diff --git a/src/bin/daemon.rs b/src/bin/daemon.rs index c874716..7307d5a 100644 --- a/src/bin/daemon.rs +++ b/src/bin/daemon.rs @@ -1,10 +1,10 @@ use pwsp::{ - types::socket::{Request, Response, MAX_MESSAGE_SIZE}, + types::socket::{MAX_MESSAGE_SIZE, Request, Response}, utils::{ commands::parse_command, daemon::{ 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, pipewire::create_virtual_mic, @@ -32,25 +32,6 @@ async fn main() -> Result<(), Box> { eprintln!("Failed to initialize audio player: {}", err); } // 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) => { - if i == 0 || i == max_retries { - eprintln!("{e} (attempt {i}/{max_retries})"); - } - } - } - - sleep(Duration::from_millis(1000)).await; - } - }); - tokio::spawn(async { start_global_hotkey_listener().await; }); diff --git a/src/gui/input.rs b/src/gui/input.rs index a6c08c4..c8b0dd0 100644 --- a/src/gui/input.rs +++ b/src/gui/input.rs @@ -8,8 +8,11 @@ use std::path::PathBuf; /// Convert an egui Key + Modifiers to a normalized chord string like "Ctrl+Shift+A". fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option { let key_name = key.name(); - let is_valid = (key_name.len() == 1 && key_name.chars().next().unwrap().is_ascii_alphanumeric()) - || (key_name.starts_with('F') && key_name.len() > 1 && key_name[1..].chars().all(|c| c.is_ascii_digit())); + let is_valid = (key_name.len() == 1 + && key_name.chars().next().unwrap().is_ascii_alphanumeric()) + || (key_name.starts_with('F') + && key_name.len() > 1 + && key_name[1..].chars().all(|c| c.is_ascii_digit())); if !is_valid { return None; } @@ -56,8 +59,11 @@ pub fn parse_chord(chord: &str) -> Option<(Modifiers, Key)> { } let key_name = parts[parts.len() - 1]; - let is_valid = (key_name.len() == 1 && key_name.chars().next().unwrap().is_ascii_alphanumeric()) - || (key_name.starts_with('F') && key_name.len() > 1 && key_name[1..].chars().all(|c| c.is_ascii_digit())); + let is_valid = (key_name.len() == 1 + && key_name.chars().next().unwrap().is_ascii_alphanumeric()) + || (key_name.starts_with('F') + && key_name.len() > 1 + && key_name[1..].chars().all(|c| c.is_ascii_digit())); if !is_valid { return None; diff --git a/src/types/audio_player.rs b/src/types/audio_player.rs index 773c9f9..adc7c87 100644 --- a/src/types/audio_player.rs +++ b/src/types/audio_player.rs @@ -2,7 +2,7 @@ use crate::{ types::pipewire::{DeviceType, Terminate}, utils::{ 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}; @@ -58,6 +58,7 @@ pub struct AudioPlayer { pub next_id: u32, input_link_sender: Option>, + player_link_sender: Option>, pub input_device_name: Option, pub volume: f32, // Master volume @@ -74,6 +75,7 @@ impl AudioPlayer { next_id: 1, input_link_sender: None, + player_link_sender: None, input_device_name: daemon_config.default_input_name.clone(), volume: default_volume, @@ -98,21 +100,46 @@ impl AudioPlayer { 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) { if let Some(sender) = &self.input_link_sender { - match sender.send(Terminate {}) { - Ok(_) => { - println!("Sent terminate signal to link thread"); - self.input_link_sender = None; - } - Err(_) => eprintln!("Failed to send terminate signal to link thread"), + if let Ok(_) = sender.send(Terminate {}) { + println!("Sent terminate signal to input link thread"); + self.input_link_sender = None; + } else { + eprintln!("Failed to send terminate signal to input link thread"); } } } + fn abort_player_link_thread(&mut self) { + if let Some(sender) = &self.player_link_sender { + if 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> { + 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> { self.abort_link_thread(); @@ -316,6 +343,7 @@ impl AudioPlayer { } self.ensure_stream()?; + self.link_player().await.ok(); let id = self.next_id; self.next_id += 1; @@ -394,6 +422,10 @@ impl AudioPlayer { self.link_devices().await.ok(); } } + + if self.stream_handle.is_some() && self.player_link_sender.is_none() { + self.link_player().await.ok(); + } } // Handle looped sounds @@ -423,10 +455,12 @@ impl AudioPlayer { } 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(); + if let Ok(res) = handle.await { + if let Some((id, source)) = res { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.sink.append(source); + sound.sink.play(); + } } } } diff --git a/src/utils/daemon.rs b/src/utils/daemon.rs index 6635324..09814d1 100644 --- a/src/utils/daemon.rs +++ b/src/utils/daemon.rs @@ -2,10 +2,10 @@ use crate::{ types::{ audio_player::AudioPlayer, config::DaemonConfig, - socket::{Request, Response, MAX_MESSAGE_SIZE}, + socket::{MAX_MESSAGE_SIZE, Request, Response}, }, - utils::pipewire::{create_link, get_device}, }; + use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; 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> { - 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 { dirs::runtime_dir().unwrap_or(PathBuf::from("/run/pwsp")) } diff --git a/src/utils/pipewire.rs b/src/utils/pipewire.rs index cc75331..0fa54cc 100644 --- a/src/utils/pipewire.rs +++ b/src/utils/pipewire.rs @@ -258,6 +258,44 @@ pub fn create_virtual_mic() -> Result, Box< Ok(pw_sender) } +pub async fn link_player_to_virtual_mic() +-> Result, Box> { + 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( output_fl: Port, output_fr: Port,