1.0.0 rewrite

This commit is contained in:
2025-09-24 22:51:34 +03:00
parent 4f8681a8c6
commit dee908a347
32 changed files with 6973 additions and 7 deletions
+57
View File
@@ -0,0 +1,57 @@
use crate::types::{commands::*, socket::Request};
use std::path::PathBuf;
pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
match request.name.as_str() {
"ping" => Some(Box::new(PingCommand {})),
"pause" => Some(Box::new(PauseCommand {})),
"resume" => Some(Box::new(ResumeCommand {})),
"stop" => Some(Box::new(StopCommand {})),
"is_paused" => Some(Box::new(IsPausedCommand {})),
"get_state" => Some(Box::new(GetStateCommand {})),
"get_volume" => Some(Box::new(GetVolumeCommand {})),
"set_volume" => {
let volume = request
.args
.get("volume")
.unwrap_or(&String::new())
.parse::<f32>()
.ok();
Some(Box::new(SetVolumeCommand { volume }))
}
"get_position" => Some(Box::new(GetPositionCommand {})),
"seek" => {
let position = request
.args
.get("position")
.unwrap_or(&String::new())
.parse::<f32>()
.ok();
Some(Box::new(SeekCommand { position }))
}
"get_duration" => Some(Box::new(GetDurationCommand {})),
"play" => {
let file_path = request
.args
.get("file_path")
.unwrap_or(&String::new())
.parse::<PathBuf>()
.ok();
Some(Box::new(PlayCommand { file_path }))
}
"get_current_file_path" => Some(Box::new(GetCurrentFilePathCommand {})),
"get_input" => Some(Box::new(GetCurrentInputCommand {})),
"get_inputs" => Some(Box::new(GetAllInputsCommand {})),
"set_input" => {
let id = request
.args
.get("input_id")
.unwrap_or(&String::new())
.parse::<u32>()
.ok();
Some(Box::new(SetCurrentInputCommand { id }))
}
_ => None,
}
}
+6
View File
@@ -0,0 +1,6 @@
use std::{error::Error, path::PathBuf};
pub fn get_config_path() -> Result<PathBuf, Box<dyn Error>> {
let config_path = dirs::config_dir().expect("Failed to obtain config dir");
Ok(config_path.join("pwsp"))
}
+156
View File
@@ -0,0 +1,156 @@
use crate::{
types::{
audio_player::AudioPlayer,
config::DaemonConfig,
pipewire::AudioDevice,
socket::{Request, Response},
},
utils::pipewire::{create_link, get_all_devices},
};
use std::path::PathBuf;
use std::{error::Error, fs};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::UnixStream,
sync::{Mutex, OnceCell},
time::{Duration, sleep},
};
static AUDIO_PLAYER: OnceCell<Mutex<AudioPlayer>> = OnceCell::const_new();
pub async fn get_audio_player() -> &'static Mutex<AudioPlayer> {
AUDIO_PLAYER
.get_or_init(|| async {
println!("Initializing audio player");
Mutex::new(AudioPlayer::new().await.unwrap())
})
.await
}
pub fn get_daemon_config() -> DaemonConfig {
DaemonConfig::load_from_file().unwrap_or_else(|_| {
let config = DaemonConfig::default();
config.save_to_file().ok();
config
})
}
pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> {
let (input_devices, output_devices) = get_all_devices().await?;
let mut pwsp_daemon_output: Option<AudioDevice> = None;
for output_device in output_devices {
if output_device.name == "alsa_playback.pwsp-daemon" {
pwsp_daemon_output = Some(output_device);
break;
}
}
if pwsp_daemon_output.is_none() {
println!("Could not find pwsp-daemon output device, skipping device linking");
return Ok(());
}
let mut pwsp_daemon_input: Option<AudioDevice> = None;
for input_device in input_devices {
if input_device.name == "pwsp-virtual-mic" {
pwsp_daemon_input = Some(input_device);
break;
}
}
if pwsp_daemon_input.is_none() {
println!("Could not find pwsp-daemon input device, skipping device linking");
return Ok(());
}
let pwsp_daemon_output = pwsp_daemon_output.unwrap();
let pwsp_daemon_input = pwsp_daemon_input.unwrap();
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"))
}
pub fn create_runtime_dir() -> Result<(), Box<dyn Error>> {
let runtime_dir = get_runtime_dir();
if !runtime_dir.exists() {
fs::create_dir_all(&runtime_dir)?;
}
Ok(())
}
pub fn is_daemon_running() -> Result<bool, Box<dyn Error>> {
let lock_file = fs::File::create(get_runtime_dir().join("daemon.lock"))?;
match lock_file.try_lock() {
Ok(_) => Ok(false),
Err(_) => Ok(true),
}
}
pub async fn wait_for_daemon() -> Result<(), Box<dyn Error>> {
if is_daemon_running()? {
return Ok(());
}
println!("Daemon not found, waiting for it...");
while !is_daemon_running()? {
sleep(Duration::from_millis(100)).await;
}
println!("Found running daemon");
Ok(())
}
pub async fn make_request(request: Request) -> Result<Response, Box<dyn Error>> {
let socket_path = get_runtime_dir().join("daemon.sock");
let mut stream = UnixStream::connect(socket_path).await?;
// ---------- Send request (start) ----------
let request_data = serde_json::to_vec(&request)?;
let request_len = request_data.len() as u32;
if stream.write_all(&request_len.to_le_bytes()).await.is_err() {
return Err("Failed to send request length".into());
};
if stream.write_all(&request_data).await.is_err() {
return Err("Failed to send request".into());
}
// ---------- Send request (end) ----------
// ---------- Read response (start) ----------
let mut len_bytes = [0u8; 4];
if stream.read_exact(&mut len_bytes).await.is_err() {
return Err("Failed to read response length".into());
}
let response_len = u32::from_le_bytes(len_bytes) as usize;
let mut buffer = vec![0u8; response_len];
if stream.read_exact(&mut buffer).await.is_err() {
return Err("Failed to read response".into());
};
// ---------- Read response (end) ----------
Ok(serde_json::from_slice(&buffer)?)
}
+159
View File
@@ -0,0 +1,159 @@
use crate::{
types::{
audio_player::PlayerState,
config::GuiConfig,
gui::AudioPlayerState,
socket::{Request, Response},
},
utils::daemon::{make_request, wait_for_daemon},
};
use std::{
collections::HashMap,
error::Error,
path::PathBuf,
sync::{Arc, Mutex},
};
use tokio::time::{Duration, sleep};
pub fn get_gui_config() -> GuiConfig {
GuiConfig::load_from_file().unwrap_or_else(|_| {
let mut config = GuiConfig::default();
config.save_to_file().ok();
config
})
}
pub fn make_request_sync(request: Request) -> Result<Response, Box<dyn Error>> {
futures::executor::block_on(make_request(request))
}
pub fn format_time_pair(position: f32, duration: f32) -> String {
fn format_time(seconds: f32) -> String {
let total_seconds = seconds.round() as u32;
let minutes = total_seconds / 60;
let secs = total_seconds % 60;
format!("{:02}:{:02}", minutes, secs)
}
format!("{}/{}", format_time(position), format_time(duration))
}
pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerState>>) {
tokio::spawn(async move {
let sleep_duration = Duration::from_millis(100);
loop {
wait_for_daemon().await.ok();
let state_req = Request::get_state();
let file_path_req = Request::get_current_file_path();
let is_paused_req = Request::get_is_paused();
let volume_req = Request::get_volume();
let position_req = Request::get_position();
let duration_req = Request::get_duration();
let current_input_req = Request::get_input();
let all_inputs_req = Request::get_inputs();
let state_res = make_request(state_req).await.unwrap_or_default();
let file_path_res = make_request(file_path_req).await.unwrap_or_default();
let is_paused_res = make_request(is_paused_req).await.unwrap_or_default();
let volume_res = make_request(volume_req).await.unwrap_or_default();
let position_res = make_request(position_req).await.unwrap_or_default();
let duration_res = make_request(duration_req).await.unwrap_or_default();
let current_input_res = make_request(current_input_req).await.unwrap_or_default();
let all_inputs_res = make_request(all_inputs_req).await.unwrap_or_default();
let state = match state_res.status {
true => serde_json::from_str::<PlayerState>(&state_res.message).unwrap(),
false => PlayerState::default(),
};
let file_path = match file_path_res.status {
true => PathBuf::from(file_path_res.message),
false => PathBuf::new(),
};
let is_paused = match is_paused_res.status {
true => is_paused_res.message == "true",
false => false,
};
let volume = match volume_res.status {
true => volume_res.message.parse::<f32>().unwrap(),
false => 0.0,
};
let position = match position_res.status {
true => position_res.message.parse::<f32>().unwrap(),
false => 0.0,
};
let duration = match duration_res.status {
true => duration_res.message.parse::<f32>().unwrap(),
false => 0.0,
};
let current_input = match current_input_res.status {
true => current_input_res
.message
.as_str()
.split(" - ")
.collect::<Vec<&str>>()
.first()
.unwrap()
.to_string()
.parse::<u32>()
.unwrap_or_default(),
false => 0,
};
let all_inputs = match all_inputs_res.status {
true => all_inputs_res
.message
.as_str()
.split(';')
.filter_map(|entry| {
let entry = entry.trim();
if entry.is_empty() {
return None;
}
entry.split_once(" - ").and_then(|(k, v)| {
k.trim()
.parse::<u32>()
.ok()
.map(|key| (key, v.trim().to_string()))
})
})
.collect(),
false => HashMap::new(),
};
{
let mut guard = audio_player_state_shared.lock().unwrap();
guard.state = match guard.new_state.clone() {
Some(new_state) => {
guard.new_state = None;
new_state
}
None => state,
};
guard.current_file_path = file_path;
guard.is_paused = is_paused;
guard.volume = match guard.new_volume {
Some(new_volume) => {
guard.new_volume = None;
new_volume
}
None => volume,
};
guard.position = match guard.new_position {
Some(new_position) => {
guard.new_position = None;
new_position
}
None => position,
};
guard.duration = if duration > 0.0 { duration } else { 1.0 };
guard.current_input = current_input;
guard.all_inputs = all_inputs;
}
sleep(sleep_duration).await;
}
});
}
+5
View File
@@ -0,0 +1,5 @@
pub mod commands;
pub mod config;
pub mod daemon;
pub mod gui;
pub mod pipewire;
+299
View File
@@ -0,0 +1,299 @@
use crate::types::pipewire::{AudioDevice, DeviceType, Port, Terminate};
use pipewire::{
context::Context, link::Link, main_loop::MainLoop, properties::properties,
registry::GlobalObject, spa::utils::dict::DictRef,
};
use std::{collections::HashMap, error::Error, thread};
use tokio::{
sync::mpsc,
time::{Duration, timeout},
};
fn parse_global_object(
global_object: &GlobalObject<&DictRef>,
) -> (Option<AudioDevice>, Option<Port>) {
// Only objects with props can be devices/ports
if let Some(props) = global_object.props {
// Only objects with media.class can be devices
if let Some(media_class) = props.get("media.class") {
let node_id = global_object.id;
let node_nick = props.get("node.nick");
let node_name = props.get("node.name");
let node_description = props.get("node.description");
// Check if the device is an input or output
return if media_class.starts_with("Audio/Source") {
let input_device = AudioDevice {
id: node_id,
nick: node_nick
.unwrap_or(node_description.unwrap_or(node_name.unwrap_or_default()))
.to_string(),
name: node_name.unwrap_or_default().to_string(),
device_type: DeviceType::Input,
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") {
let output_device = AudioDevice {
id: node_id,
nick: node_nick
.unwrap_or(node_description.unwrap_or(node_name.unwrap_or_default()))
.to_string(),
name: node_name.unwrap_or_default().to_string(),
device_type: DeviceType::Output,
input_fl: None,
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 node_id = props.get("node.id").unwrap().parse::<u32>().unwrap();
let port_id = props.get("port.id").unwrap().parse::<u32>().unwrap();
let port_name = props.get("port.name").unwrap();
let port = Port {
node_id,
port_id,
name: port_name.to_string(),
};
return (None, Some(port));
}
}
(None, None)
}
async fn pw_get_global_objects_thread(
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
pw_receiver: pipewire::channel::Receiver<Terminate>,
) {
let main_loop = MainLoop::new(None).expect("Failed to initialize pipewire main loop");
// Stop main loop on Terminate message
let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone();
move |_| _main_loop.quit()
});
let context = Context::new(&main_loop).expect("Failed to create pipewire context");
let core = context
.connect(None)
.expect("Failed to connect to pipewire context");
let registry = core
.get_registry()
.expect("Failed to get registry from pipewire context");
let _listener = registry
.add_listener_local()
.global(move |global| {
// Try to parse every global object pipewire finds
let (device, port) = parse_global_object(global);
// Send message to the main thread
let sender_clone = main_sender.clone();
tokio::task::spawn(async move {
sender_clone.send((device, port)).await.ok();
});
})
.register();
main_loop.run();
}
pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), Box<dyn Error>> {
// Channels to communicate with pipewire thread
let (main_sender, mut main_receiver) = mpsc::channel(10);
let (pw_sender, pw_receiver) = pipewire::channel::channel();
// Spawn pipewire thread in background
let _pw_thread =
tokio::spawn(async move { pw_get_global_objects_thread(main_sender, pw_receiver).await });
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new();
let mut ports: Vec<Port> = vec![];
loop {
// If we don't receive a message in 100ms, we can assume that pipewire thread is finished
match timeout(Duration::from_millis(100), main_receiver.recv()).await {
Ok(Some((device, port))) => {
if let Some(device) = device {
match device.device_type {
DeviceType::Input => {
input_devices.insert(device.id, device);
}
DeviceType::Output => {
output_devices.insert(device.id, device);
}
}
} else if let Some(port) = port {
ports.push(port);
}
}
Ok(None) | Err(_) => {
// Pipewire thread is finished and we can collect our devices
pw_sender
.send(Terminate {})
.expect("Failed to terminate pipewire thread");
for port in ports {
let node_id = port.node_id;
if input_devices.contains_key(&node_id) {
let input_device = input_devices.get_mut(&node_id).unwrap();
match port.name.as_str() {
"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)
}
_ => {}
}
} else if output_devices.contains_key(&node_id) {
let output_device = output_devices.get_mut(&node_id).unwrap();
match port.name.as_str() {
"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 output_devices: Vec<AudioDevice> =
output_devices.values().cloned().collect();
input_devices.sort_by(|a, b| a.id.cmp(&b.id));
output_devices.sort_by(|a, b| a.id.cmp(&b.id));
return Ok((input_devices, output_devices));
}
}
}
}
pub async fn get_device(node_id: u32) -> Result<AudioDevice, Box<dyn Error>> {
let (mut input_devices, output_devices) = get_all_devices().await?;
input_devices.extend(output_devices);
for device in input_devices {
if device.id == node_id {
return Ok(device);
}
}
Err("Device not found".into())
}
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let _pw_thread = thread::spawn(move || {
let main_loop = MainLoop::new(None).expect("Failed to initialize pipewire main loop");
let context = Context::new(&main_loop).expect("Failed to create pipewire context");
let core = context
.connect(None)
.expect("Failed to connect to pipewire context");
let props = properties!(
"factory.name" => "support.null-audio-sink",
"node.name" => "pwsp-virtual-mic",
"node.description" => "PWSP Virtual Mic",
"media.class" => "Audio/Source/Virtual",
"audio.position" => "[ FL FR ]",
"audio.channels" => "2",
"object.linger" => "false", // Destroy the node on app exit
);
let _node = core
.create_object::<pipewire::node::Node>("adapter", &props)
.expect("Failed to create virtual mic");
let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone();
move |_| _main_loop.quit()
});
println!("Virtual mic created");
main_loop.run();
});
Ok(pw_sender)
}
pub fn create_link(
output_fl: Port,
output_fr: Port,
input_fl: Port,
input_fr: Port,
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let _pw_thread = thread::spawn(move || {
let main_loop = MainLoop::new(None).expect("Failed to initialize pipewire main loop");
let context = Context::new(&main_loop).expect("Failed to create pipewire context");
let core = context
.connect(None)
.expect("Failed to connect to pipewire context");
let props_fl = properties! {
"link.output.node" => format!("{}", output_fl.node_id).as_str(),
"link.output.port" => format!("{}", output_fl.port_id).as_str(),
"link.input.node" => format!("{}", input_fl.node_id).as_str(),
"link.input.port" => format!("{}", input_fl.port_id).as_str(),
};
let props_fr = properties! {
"link.output.node" => format!("{}", output_fr.node_id).as_str(),
"link.output.port" => format!("{}", output_fr.port_id).as_str(),
"link.input.node" => format!("{}", input_fr.node_id).as_str(),
"link.input.port" => format!("{}", input_fr.port_id).as_str(),
};
let _link_fl = core
.create_object::<Link>("link-factory", &props_fl)
.expect("Failed to create link FL");
let _link_fr = core
.create_object::<Link>("link-factory", &props_fr)
.expect("Failed to create link FR");
let _receiver = pw_receiver.attach(main_loop.loop_(), {
let _main_loop = main_loop.clone();
move |_| _main_loop.quit()
});
println!(
"Link created: FL: {}-{} FR: {}-{}",
output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id
);
main_loop.run();
});
Ok(pw_sender)
}