From 4128566048c2c1063456cd5ce31badfba96145e1 Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Thu, 5 Mar 2026 21:24:55 +0100 Subject: [PATCH] Make bottom panel fixed size so content doesn't go up and down when playing new sound effects, add more apps to Linux excludes, add multiple virtual output support by using buttons(on/off) on Linux so you can select the virtual mic for multiple apps at the same time --- src/linux_lib.rs | 52 +++++++++++++++++++----------- src/main.rs | 83 +++++++++++++++++++++++------------------------- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/linux_lib.rs b/src/linux_lib.rs index bc65d82..1ec1593 100644 --- a/src/linux_lib.rs +++ b/src/linux_lib.rs @@ -5,7 +5,8 @@ use rodio::{ use serde_json::Value; use std::process::Command; -const APPS_TO_EXCLUDE: [&str; 1] = ["plasmashell"]; +const APPS_TO_EXCLUDE: [&str; 7] = ["plasmashell", "pavucontrol", "pipewire", "wireplumber", "kwin_wayland", "kwin_x11", "obs"]; +const NODE_NAMES_TO_EXCLUDE: [&str; 2] = ["VirtualMicSource", "SoundboardSink"]; fn pactl_list(sink_type: &str) -> Value { let command_output = Command::new("pactl") @@ -23,21 +24,35 @@ fn pactl_list(sink_type: &str) -> Value { } } -pub fn get_sink_by_index(sink_type: &str, index: String) -> Value { - let sinks = pactl_list(sink_type); +pub fn get_soundboard_sink_index() -> String { + let source_outputs = pactl_list("sinks"); + source_outputs + .as_array() + .unwrap_or(&vec![]) + .iter() + .find(|sink| sink["name"] == "SoundboardSink") + .and_then(|sink| { + Some(sink["index"].to_string()) + }) + .unwrap() +} - for sink in sinks.as_array().unwrap_or(&vec![]) { - if sink["index"] - .as_u64() - .expect("sink index is not a number") - .to_string() - == index - { - return sink.clone(); - } - } +pub fn get_default_source() -> String { + let sources = pactl_list("sources"); + + let command = Command::new("pactl") + .args(&["get-default-source"]) + .output() + .unwrap(); - return Value::Null {}; + let default_source_name = String::from_utf8_lossy(&command.stdout).trim().to_string(); + + sources.as_array() + .unwrap() + .iter() + .find(|sink|{ sink["name"].as_str() == Some(&default_source_name) }) + .and_then(|s|{ Some(s["index"].to_string()) }) + .unwrap() } fn find_soundboard_sinks() -> Vec { @@ -73,10 +88,11 @@ pub fn list_outputs() -> Vec<(String, String)> { .iter() .filter_map(|sink| { let app_name = sink["properties"]["application.name"].as_str()?; + let node_name = sink["properties"]["node.name"].as_str()?; let binary = sink["properties"]["application.process.binary"] .as_str() .unwrap_or("Unknown"); - if APPS_TO_EXCLUDE.contains(&binary) { + if APPS_TO_EXCLUDE.contains(&binary) || NODE_NAMES_TO_EXCLUDE.contains(&node_name) { return None; } let index = sink["index"] @@ -88,9 +104,9 @@ pub fn list_outputs() -> Vec<(String, String)> { .collect(); } -pub fn move_index_to_virtualmic(index: String) { - Command::new("pactl") - .args(&["move-source-output", index.as_str(), "VirtualMicSource"]) // as_str is needed here as you cannot instantly dereference a growing String (Rust...) +pub fn move_output_to_sink(output_index: String, sink_index: String) { + let output = Command::new("pactl") + .args(&["move-source-output", output_index.as_str(), sink_index.as_str()]) // as_str is needed here as you cannot instantly dereference a growing String (Rust...) .output() .expect("Failed to execute process"); } diff --git a/src/main.rs b/src/main.rs index 5b99092..c5bfaef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,8 +57,7 @@ struct AppState { currently_playing: Vec, sound_system: SoundSystem, virt_outputs: Vec<(String, String)>, - virt_output_index_switch: String, - virt_output_index: String, + is_virt_output_used: HashMap, last_virt_output_update: Instant, current_view: String, youtube_downloader_state: YoutubeDownloaderState @@ -156,8 +155,7 @@ fn main() { currently_playing: Vec::new(), sound_system: create_virtual_mic(), virt_outputs: Vec::new(), - virt_output_index_switch: String::from("0"), - virt_output_index: String::from("999"), + is_virt_output_used: HashMap::new(), current_view: "main".to_string(), last_virt_output_update: Instant::now(), youtube_downloader_state: YoutubeDownloaderState { @@ -181,30 +179,29 @@ fn main() { } fn update(mut app_state: ResMut) { - if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 3.0 { - app_state.last_virt_output_update = Instant::now(); - app_state.virt_outputs = list_outputs(); - } + #[cfg(target_os = "linux")] { + if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 1.5 { + app_state.last_virt_output_update = Instant::now(); + app_state.virt_outputs = list_outputs(); + let is_virt_output_used = app_state.is_virt_output_used.clone(); - if app_state.virt_outputs.is_empty() { - return; - } + for virt_output in &app_state.virt_outputs.clone() { + if !is_virt_output_used.contains_key(&virt_output.1) { + app_state.is_virt_output_used.insert(virt_output.1.clone(), false); + } - if !(app_state.virt_output_index == "999".to_string()) { - app_state.virt_output_index_switch = app_state.virt_outputs[0].1.clone(); - } - - if app_state.virt_output_index != app_state.virt_output_index_switch { - app_state.virt_output_index = app_state.virt_output_index_switch.clone(); - #[cfg(target_os = "linux")] - linux_lib::move_index_to_virtualmic(app_state.virt_output_index_switch.clone()); + if app_state.is_virt_output_used[&virt_output.1] { + linux_lib::move_output_to_sink(virt_output.1.clone(), linux_lib::get_soundboard_sink_index()); + } + else { + linux_lib::move_output_to_sink(virt_output.1.clone(), linux_lib::get_default_source()); + } + } + } } } fn load_system(mut app_state: ResMut) { - if !app_state.virt_outputs.is_empty() { - app_state.virt_output_index_switch = app_state.virt_outputs[0].1.clone(); - } load_data(&mut app_state); } @@ -309,25 +306,22 @@ fn play_sound(file_path: String, app_state: &mut AppState) { app_state.currently_playing.push(playing_sound); } -fn create_virtual_mic_dropdown(ui: &mut Ui, app_state: &mut ResMut, available_width: f32, available_height: f32) { +fn create_virtual_mic_ui(ui: &mut Ui, app_state: &mut ResMut, available_width: f32, available_height: f32) { #[cfg(target_os = "linux")] { - let outputs = app_state.virt_outputs.clone(); - let output_index = app_state.virt_output_index.clone(); - let output_sink = linux_lib::get_sink_by_index("source-outputs", output_index); - if let Some(app_name) = output_sink["properties"]["application.name"].as_str() { - egui::ComboBox::from_id_salt("Virtual Mic Output") - .selected_text(app_name.to_string()) - .width(available_width) - .height(available_height / 15.0) - .show_ui(ui, |ui| { - for output in &outputs { - ui.selectable_value( - &mut app_state.virt_output_index_switch, - output.1.clone(), - output.0.clone(), - ); - } - }); + if app_state.is_virt_output_used.len() != 0 { + let outputs = app_state.virt_outputs.clone(); + for output in &outputs { + let current_value = *app_state.is_virt_output_used.get(&output.1).unwrap_or(&false); + if ui + .add_sized( + [available_width, available_height / 30.0], + egui::Button::new(format!("{} - {}", output.0.clone(), current_value)), + ) + .clicked() + { + *app_state.is_virt_output_used.entry(output.1.clone()).or_insert(false) = !current_value; + } + } } else { ui.add(egui::Button::new("No apps found to use.".to_string())); @@ -350,7 +344,7 @@ fn main_ui(ctx: &Context, mut app_state: ResMut) { let available_width = ui.available_width(); let available_height = ui.available_height(); ui.label("Virtual Mic Output"); - create_virtual_mic_dropdown(ui, &mut app_state, available_width, available_height); + create_virtual_mic_ui(ui, &mut app_state, available_width, available_height); if ui .add_sized( @@ -361,7 +355,6 @@ fn main_ui(ctx: &Context, mut app_state: ResMut) { { if let Some(folder) = rfd::FileDialog::new().pick_folder() { if let Some(path_str) = folder.to_str() { - println!("Selected: {}", path_str); app_state.json_data.tabs.push(path_str.to_string()); std::fs::write( "data.json", @@ -581,7 +574,11 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut) -> Result { } }); - egui::TopBottomPanel::bottom("currently_playing").show(ctx, |ui| { + let window_height = ctx.screen_rect().height(); + + egui::TopBottomPanel::bottom("currently_playing") + .exact_height(window_height * 0.1) + .show(ctx, |ui| { ui.vertical(|ui| { for playing_sound in &mut app_state.currently_playing { ui.horizontal(|ui| {