Compare commits

...

8 Commits

4 changed files with 110 additions and 79 deletions

View File

@@ -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);
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_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()
}
return Value::Null {};
pub fn get_default_source() -> String {
let sources = pactl_list("sources");
let command = Command::new("pactl")
.args(&["get-default-source"])
.output()
.unwrap();
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> {
@@ -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 _ = 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");
}

View File

@@ -57,8 +57,7 @@ struct AppState {
currently_playing: Vec<PlayingSound>,
sound_system: SoundSystem,
virt_outputs: Vec<(String, String)>,
virt_output_index_switch: String,
virt_output_index: String,
is_virt_output_used: HashMap<String, bool>,
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<AppState>) {
if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 3.0 {
#[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();
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_outputs.is_empty() {
return;
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());
}
}
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());
}
}
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);
}
@@ -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<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")] {
if app_state.is_virt_output_used.len() != 0 {
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(),
);
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<AppState>) {
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<AppState>) {
{
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<AppState>) -> Result {
}
});
egui::TopBottomPanel::bottom("currently_playing").show(ctx, |ui| {
let window_height = ctx.content_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| {

View File

@@ -2,8 +2,9 @@ use rodio::{
OutputStream, OutputStreamBuilder,
cpal::{self, traits::{DeviceTrait, StreamTrait, HostTrait}, StreamConfig, SampleRate},
};
use rfd::{MessageButtons, MessageDialog, MessageDialogResult};
use ringbuf::{traits::*, HeapRb};
use std::process;
fn route_standard_to_virtual(host: &cpal::Host, virtual_mic: &cpal::Device) {
let standard_mic = host.default_input_device().expect("Could not get default input device.");
@@ -56,9 +57,9 @@ pub fn create_virtual_mic_windows() -> (OutputStream, OutputStream) {
.ok()
.map(|name| name.contains("CABLE Input") || name.contains("VB-Audio"))
.unwrap_or(false)
})
.expect("Could not get VB Cable output device. Is VB Cable Driver installed?");
});
if let Some(virtual_mic) = virtual_mic {
route_standard_to_virtual(&host, &virtual_mic);
let normal_output = host
@@ -76,3 +77,13 @@ pub fn create_virtual_mic_windows() -> (OutputStream, OutputStream) {
.expect("Failed to open stream"),
);
}
else {
MessageDialog::new()
.set_title("VB Cable Driver not installed.")
.set_description("Could not access VB Cable output device. Is VB Cable Driver installed?")
.set_buttons(MessageButtons::Ok)
.show();
std::process::exit(1);
}
}

View File

@@ -2,6 +2,9 @@ use std::{env::current_dir, fs::{File, exists}, io, process::Command};
use reqwest;
use rfd::{MessageButtons, MessageDialog, MessageDialogResult};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
pub fn get_yt_dlp_path() -> String {
if cfg!(target_os = "windows"){
current_dir().expect("Failed to get current working directory").join("bin").join("yt-dlp.exe").to_string_lossy().to_string()
@@ -40,10 +43,14 @@ pub fn check_and_download_yt_dlp() {
let mut body = reqwest::blocking::get(url).expect("Could not download yt-dlp");
let mut out = File::create(get_yt_dlp_path()).expect("failed to create file");
io::copy(&mut body, &mut out).expect("failed to copy content");
#[cfg(unix)]
out.set_permissions(PermissionsExt::from_mode(0o755));
}
pub fn check_ffmpeg() -> bool{
return std::process::Command::new("ffmpeg").spawn().is_ok();
return std::process::Command::new("ffmpeg").output().is_ok();
}
pub fn check_and_download_ffmpeg() {