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

This commit is contained in:
csd4ni3l
2026-03-05 21:24:55 +01:00
parent 8d036bca08
commit 4128566048
2 changed files with 74 additions and 61 deletions

View File

@@ -5,7 +5,8 @@ use rodio::{
use serde_json::Value; use serde_json::Value;
use std::process::Command; 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 { fn pactl_list(sink_type: &str) -> Value {
let command_output = Command::new("pactl") 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 { pub fn get_soundboard_sink_index() -> String {
let sinks = pactl_list(sink_type); 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![]) { pub fn get_default_source() -> String {
if sink["index"] let sources = pactl_list("sources");
.as_u64()
.expect("sink index is not a number") let command = Command::new("pactl")
.to_string() .args(&["get-default-source"])
== index .output()
{ .unwrap();
return sink.clone();
}
}
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<Value> { fn find_soundboard_sinks() -> Vec<Value> {
@@ -73,10 +88,11 @@ pub fn list_outputs() -> Vec<(String, String)> {
.iter() .iter()
.filter_map(|sink| { .filter_map(|sink| {
let app_name = sink["properties"]["application.name"].as_str()?; 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"] let binary = sink["properties"]["application.process.binary"]
.as_str() .as_str()
.unwrap_or("Unknown"); .unwrap_or("Unknown");
if APPS_TO_EXCLUDE.contains(&binary) { if APPS_TO_EXCLUDE.contains(&binary) || NODE_NAMES_TO_EXCLUDE.contains(&node_name) {
return None; return None;
} }
let index = sink["index"] let index = sink["index"]
@@ -88,9 +104,9 @@ pub fn list_outputs() -> Vec<(String, String)> {
.collect(); .collect();
} }
pub fn move_index_to_virtualmic(index: String) { pub fn move_output_to_sink(output_index: String, sink_index: String) {
Command::new("pactl") let output = 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...) .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() .output()
.expect("Failed to execute process"); .expect("Failed to execute process");
} }

View File

@@ -57,8 +57,7 @@ struct AppState {
currently_playing: Vec<PlayingSound>, currently_playing: Vec<PlayingSound>,
sound_system: SoundSystem, sound_system: SoundSystem,
virt_outputs: Vec<(String, String)>, virt_outputs: Vec<(String, String)>,
virt_output_index_switch: String, is_virt_output_used: HashMap<String, bool>,
virt_output_index: String,
last_virt_output_update: Instant, last_virt_output_update: Instant,
current_view: String, current_view: String,
youtube_downloader_state: YoutubeDownloaderState youtube_downloader_state: YoutubeDownloaderState
@@ -156,8 +155,7 @@ fn main() {
currently_playing: Vec::new(), currently_playing: Vec::new(),
sound_system: create_virtual_mic(), sound_system: create_virtual_mic(),
virt_outputs: Vec::new(), virt_outputs: Vec::new(),
virt_output_index_switch: String::from("0"), is_virt_output_used: HashMap::new(),
virt_output_index: String::from("999"),
current_view: "main".to_string(), current_view: "main".to_string(),
last_virt_output_update: Instant::now(), last_virt_output_update: Instant::now(),
youtube_downloader_state: YoutubeDownloaderState { youtube_downloader_state: YoutubeDownloaderState {
@@ -181,30 +179,29 @@ fn main() {
} }
fn update(mut app_state: ResMut<AppState>) { fn update(mut app_state: ResMut<AppState>) {
if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 3.0 { #[cfg(target_os = "linux")] {
app_state.last_virt_output_update = Instant::now(); if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 1.5 {
app_state.virt_outputs = list_outputs(); 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() { for virt_output in &app_state.virt_outputs.clone() {
return; 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()) { if app_state.is_virt_output_used[&virt_output.1] {
app_state.virt_output_index_switch = app_state.virt_outputs[0].1.clone(); linux_lib::move_output_to_sink(virt_output.1.clone(), linux_lib::get_soundboard_sink_index());
} }
else {
if app_state.virt_output_index != app_state.virt_output_index_switch { linux_lib::move_output_to_sink(virt_output.1.clone(), linux_lib::get_default_source());
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()); }
} }
} }
fn load_system(mut app_state: ResMut<AppState>) { fn load_system(mut app_state: ResMut<AppState>) {
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); 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); app_state.currently_playing.push(playing_sound);
} }
fn create_virtual_mic_dropdown(ui: &mut Ui, app_state: &mut ResMut<AppState>, available_width: f32, available_height: f32) { fn create_virtual_mic_ui(ui: &mut Ui, app_state: &mut ResMut<AppState>, available_width: f32, available_height: f32) {
#[cfg(target_os = "linux")] { #[cfg(target_os = "linux")] {
let outputs = app_state.virt_outputs.clone(); if app_state.is_virt_output_used.len() != 0 {
let output_index = app_state.virt_output_index.clone(); let outputs = app_state.virt_outputs.clone();
let output_sink = linux_lib::get_sink_by_index("source-outputs", output_index); for output in &outputs {
if let Some(app_name) = output_sink["properties"]["application.name"].as_str() { let current_value = *app_state.is_virt_output_used.get(&output.1).unwrap_or(&false);
egui::ComboBox::from_id_salt("Virtual Mic Output") if ui
.selected_text(app_name.to_string()) .add_sized(
.width(available_width) [available_width, available_height / 30.0],
.height(available_height / 15.0) egui::Button::new(format!("{} - {}", output.0.clone(), current_value)),
.show_ui(ui, |ui| { )
for output in &outputs { .clicked()
ui.selectable_value( {
&mut app_state.virt_output_index_switch, *app_state.is_virt_output_used.entry(output.1.clone()).or_insert(false) = !current_value;
output.1.clone(), }
output.0.clone(), }
);
}
});
} }
else { else {
ui.add(egui::Button::new("No apps found to use.".to_string())); 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<AppState>) {
let available_width = ui.available_width(); let available_width = ui.available_width();
let available_height = ui.available_height(); let available_height = ui.available_height();
ui.label("Virtual Mic Output"); 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 if ui
.add_sized( .add_sized(
@@ -361,7 +355,6 @@ fn main_ui(ctx: &Context, mut app_state: ResMut<AppState>) {
{ {
if let Some(folder) = rfd::FileDialog::new().pick_folder() { if let Some(folder) = rfd::FileDialog::new().pick_folder() {
if let Some(path_str) = folder.to_str() { if let Some(path_str) = folder.to_str() {
println!("Selected: {}", path_str);
app_state.json_data.tabs.push(path_str.to_string()); app_state.json_data.tabs.push(path_str.to_string());
std::fs::write( std::fs::write(
"data.json", "data.json",
@@ -581,7 +574,11 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> 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| { ui.vertical(|ui| {
for playing_sound in &mut app_state.currently_playing { for playing_sound in &mut app_state.currently_playing {
ui.horizontal(|ui| { ui.horizontal(|ui| {