Compare commits

...

12 Commits

Author SHA1 Message Date
csd4ni3l
ff71f6f21a Fix currently_playing push not working on Windows because normal_sink is in a different scope 2026-02-05 20:24:50 +01:00
csd4ni3l
ca6fc96f99 Fix some windows compilation issues 2026-02-05 20:04:32 +01:00
csd4ni3l
6480c80cb4 Linux support is done! Now they can hear you & you can speak, and you will not hear yourself. 2026-02-05 18:53:53 +01:00
csd4ni3l
320337567d Fix reload sound system button not replacing sound system but output stream of current sound system 2026-02-05 18:31:06 +01:00
csd4ni3l
83f53f8711 Add normal mic support on Linux by using loopback modules, and remove all unnecessary double stream code which wouldnt work anyway, add Windows normal mic support as well, by using target_os windows blocks and running 2 separate streams. 2026-02-05 18:27:41 +01:00
csd4ni3l
21d9f6c08b Code formatted by Zed automatically, added black fill color for
directory button that is currently selected, removed the current
directory text
2026-02-05 16:26:44 +01:00
csd4ni3l
abb7704e21 Fix typo 2026-01-25 15:55:09 +01:00
csd4ni3l
4d065525ff Fix Windows trying to use linux move_index_to_virtualmic func 2026-01-25 15:53:26 +01:00
csd4ni3l
73efdbd8b3 Add DeviceTrait to fix Windows not being able to access device name 2026-01-25 15:51:19 +01:00
csd4ni3l
caa4f7d2d0 Fix windows missing let keywords 2026-01-25 15:47:49 +01:00
csd4ni3l
a42c69c845 Separate Linux and Windows stuff (I have no idea if Windows even works), add automatic virtual mic support for apps on Linux (impossible on Windows) 2026-01-25 15:42:29 +01:00
csd4ni3l
d5a7dd624b Remove unused bevy features to remove bloat & reduce executable size by 32% (debug) 2026-01-23 21:13:22 +01:00
6 changed files with 403 additions and 955 deletions

819
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,19 @@ serde_json = "1.0.146"
[dependencies.bevy] [dependencies.bevy]
version = "0.17.3" version = "0.17.3"
default-features = false
features = [ features = [
"bevy_winit", "bevy_log",
"bevy_window",
"bevy_winit",
"bevy_render",
"bevy_core_pipeline",
"bevy_sprite",
"bevy_text",
"bevy_ui",
"bevy_asset",
"bevy_picking",
"multi_threaded",
] ]
[profile.dev.package."*"] [profile.dev.package."*"]
@@ -22,5 +33,20 @@ opt-level = 2
debug = false debug = false
[target.'cfg(target_os = "linux")'.dependencies.bevy] [target.'cfg(target_os = "linux")'.dependencies.bevy]
default-features = false
version = "0.17.3" version = "0.17.3"
features = ["wayland", "x11", "bevy_winit"] features = [
"bevy_log",
"bevy_window",
"bevy_winit",
"bevy_render",
"bevy_core_pipeline",
"bevy_sprite",
"bevy_text",
"bevy_ui",
"multi_threaded",
"bevy_asset",
"bevy_picking",
"wayland",
"x11"
]

View File

@@ -1,4 +1,6 @@
cargo run cargo run
pactl list modules short | grep "module-loopback" | cut -f1 | xargs -L1 pactl unload-module
pactl list modules short | grep "Virtual_Microphone" | cut -f1 | xargs -L1 pactl unload-module pactl list modules short | grep "Virtual_Microphone" | cut -f1 | xargs -L1 pactl unload-module
pactl list modules short | grep "Virtual_Mic_Source" | cut -f1 | xargs -L1 pactl unload-module pactl list modules short | grep "Virtual_Mic_Source" | cut -f1 | xargs -L1 pactl unload-module
pactl list modules short | grep "Soundboard_Audio" | cut -f1 | xargs -L1 pactl unload-module

146
src/linux_lib.rs Normal file
View File

@@ -0,0 +1,146 @@
use std::process::Command;
use serde_json::Value;
use rodio::{OutputStream, OutputStreamBuilder, cpal::{self, traits::HostTrait}};
fn pactl_list(sink_type: &str) -> Value {
let command_output = Command::new("pactl")
.args(&["-f", "json", "list", sink_type])
.output()
.expect("Failed to execute process");
if command_output.status.success() {
serde_json::from_str(str::from_utf8(&command_output.stdout).expect("Failed to convert to string")).expect("Failed to parse sink JSON output")
}
else {
Value::Null{}
}
}
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();
}
}
return Value::Null{};
}
fn find_soundboard_sinks() -> Vec<Value> {
let sink_inputs = pactl_list("sink-inputs");
sink_inputs.as_array()
.unwrap_or(&vec![])
.iter()
.filter(|sink| {sink["properties"]["node.name"] == "alsa_playback.soundboard"})
.cloned()
.collect()
}
pub fn move_playback_to_sink() {
let soundboard_sinks = find_soundboard_sinks();
for sink in soundboard_sinks {
let index = sink["index"].as_u64().expect("sink index is not a number").to_string();
Command::new("pactl")
.args(&["move-sink-input", index.as_str(), "SoundboardSink"]) // as_str is needed here as you cannot instantly dereference a growing String (Rust...)
.output()
.expect("Failed to execute process");
}
}
pub fn list_outputs() -> Vec<(String, String)> {
let source_outputs = pactl_list("source-outputs");
return source_outputs.as_array().unwrap_or(&vec![]).iter().filter_map(|sink| {
let app_name = sink["properties"]["application.name"].as_str()?;
let binary = sink["properties"]["application.process.binary"].as_str().unwrap_or("Unknown");
let index = sink["index"].as_u64().expect("sink index is not a number").to_string();
Some((format!("{} ({})", app_name, binary), index))
}).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...)
.output()
.expect("Failed to execute process");
}
pub fn create_virtual_mic_linux() -> OutputStream {
Command::new("pactl")
.args(&["load-module", "module-null-sink", "sink_name=SoundboardSink", "sink_properties=device.description=\"Soundboard_Audio\""])
.output()
.expect("Failed to create SoundboardSink");
Command::new("pactl")
.args(&["load-module", "module-null-sink", "sink_name=VirtualMic", "sink_properties=device.description=\"Virtual_Microphone\""])
.output()
.expect("Failed to create VirtualMic");
Command::new("pactl")
.args(&["load-module", "module-remap-source", "master=VirtualMic.monitor", "source_name=VirtualMicSource", "source_properties=device.description=\"Virtual_Mic_Source\""])
.output()
.expect("Failed to create VirtualMicSource");
// Soundboard audio -> speakers
Command::new("pactl")
.args(&["load-module", "module-loopback", "source=SoundboardSink.monitor", "sink=@DEFAULT_SINK@", "latency_msec=1"])
.output()
.expect("Failed to create soundboard to speakers loopback");
// Soundboard audio -> VirtualMic
Command::new("pactl")
.args(&["load-module", "module-loopback", "source=SoundboardSink.monitor", "sink=VirtualMic", "latency_msec=1"])
.output()
.expect("Failed to create soundboard to VirtualMic loopback");
// Microphone -> VirtualMic ONLY
Command::new("pactl")
.args(&["load-module", "module-loopback", "source=@DEFAULT_SOURCE@", "sink=VirtualMic", "latency_msec=1"])
.output()
.expect("Failed to create microphone loopback");
Command::new("pactl")
.args(&["set-sink-volume", "VirtualMic", "100%"])
.output()
.expect("Failed to set volume");
Command::new("pactl")
.args(&["set-sink-volume", "SoundboardSink", "100%"])
.output()
.expect("Failed to set soundboard volume");
let host = cpal::host_from_id(cpal::HostId::Alsa).expect("Could not initialize ALSA");
let device = host.default_output_device().expect("Could not get default output device");
let stream = OutputStreamBuilder::from_device(device)
.expect("Unable to open VirtualMic")
.open_stream()
.expect("Failed to open stream");
move_playback_to_sink();
return stream;
}
pub fn reload_sound() {
let script = r#"
pactl list modules short | grep "module-loopback" | cut -f1 | xargs -L1 pactl unload-module
pactl list modules short | grep "Virtual_Microphone" | cut -f1 | xargs -L1 pactl unload-module
pactl list modules short | grep "Virtual_Mic_Source" | cut -f1 | xargs -L1 pactl unload-module
pactl list modules short | grep "Soundboard_Audio" | cut -f1 | xargs -L1 pactl unload-module
"#;
let output = Command::new("sh")
.arg("-c")
.arg(script)
.output()
.expect("Failed to execute process");
if output.status.success() {
println!("Modules unloaded successfully.");
} else {
println!("Error: {}", String::from_utf8_lossy(&output.stderr));
}
}

View File

@@ -1,19 +1,23 @@
use bevy::{ use bevy::{log::Level, prelude::*};
log::{Level, LogPlugin},
prelude::*,
};
use std::{collections::HashMap, fs::File, io::BufReader, path::Path, process::Command}; use std::{collections::HashMap, fs::File, io::BufReader, path::Path};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use bevy_egui::{ use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui};
EguiContextSettings, EguiContexts, EguiPlugin, EguiPrimaryContextPass, EguiStartupSet, egui,
use egui::ecolor::Color32;
#[cfg(target_os = "linux")]
mod linux_lib;
#[cfg(target_os = "windows")]
mod windows_lib;
use rodio::{
Decoder, OutputStream, Sink, Source, cpal::{self, traits::HostTrait}, OutputStreamBuilder
}; };
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source, cpal::{self, Device, Host, traits::HostTrait, traits::DeviceTrait}};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct JSONData { struct JSONData {
tabs: Vec<String>, tabs: Vec<String>,
@@ -23,14 +27,16 @@ struct JSONData {
struct PlayingSound { struct PlayingSound {
file_path: String, file_path: String,
length: f32, length: f32,
virtual_sink: Sink, sink: Sink,
// normal_sink: Sink #[cfg(target_os = "windows")]
normal_sink: Sink
} }
struct SoundSystem { struct SoundSystem {
virtual_mic_stream: OutputStream, #[cfg(target_os = "windows")]
// normal_output_stream: OutputStream, normal_output_stream: OutputStream,
paused: bool output_stream: OutputStream,
paused: bool,
} }
#[derive(Resource)] #[derive(Resource)]
@@ -39,113 +45,77 @@ struct AppState {
json_data: JSONData, json_data: JSONData,
current_directory: String, current_directory: String,
currently_playing: Vec<PlayingSound>, currently_playing: Vec<PlayingSound>,
sound_system: SoundSystem sound_system: SoundSystem,
virt_outputs: Vec<(String, String)>,
virt_output_index_switch: String,
virt_output_index: String,
} }
const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"]; const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"];
fn move_playback_to_sink() { fn create_virtual_mic() -> SoundSystem {
let command_output = Command::new("pactl")
.args(&["-f", "json", "list", "sink-inputs"])
.output()
.expect("Failed to execute process");
if command_output.status.success() {
let sink_json: Value = serde_json::from_str(str::from_utf8(&command_output.stdout).expect("Failed to convert to string")).expect("Failed to parse sink JSON output");
for device in sink_json.as_array().unwrap_or(&vec![]) {
if device["properties"]["node.name"] == "alsa_playback.soundboard" {
let index = device["index"].as_u64().expect("Device index is not a number").to_string();
Command::new("pactl")
.args(&["move-sink-input", index.as_str(), "VirtualMic"]) // as_str is needed here as you cannot instantly dereference a growing String (Rust...)
.output()
.expect("Failed to execute process");
}
}
}
}
fn create_virtual_mic() -> OutputStream {
let host: Host;
// let original_host: Host;
// let normal_output: Device;
let virtual_mic: Device;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
host = cpal::host_from_id(cpal::HostId::Wasapi).expect("Could not initialize audio routing using WasAPI"); let (normal, virtual_mic) = windows_lib::create_virtual_mic_windows();
virtual_mic = host.output_devices().expect("Could not list Output devices").find(|device| { return SoundSystem {
device.name().ok().map(|name|{ output_stream: virtual_mic,
name.contains("CABLE Input") || name.contains("VB-Audio") normal_output_stream: normal,
}).unwrap_or(false) paused: false,
}).expect("Could not get default output device"); };
// normal_output = host.default_output_device().expect("Could not get default output device");
return OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream");
// return (OutputStreamBuilder::from_device(normal_output).expect("Unable to open default audio device").open_stream().expect("Failed to open stream"), OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream"));
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
// original_host = cpal::host_from_id(cpal::HostId::Alsa).expect("Could not initialize audio routing using ALSA"); return SoundSystem {
// normal_output = original_host.default_output_device().expect("Could not get default output device"); output_stream: linux_lib::create_virtual_mic_linux(),
paused: false,
Command::new("pactl") };
.args(&["load-module", "module-null-sink", "sink_name=VirtualMic", "sink_properties=device.description=\"Virtual_Microphone\""])
.output()
.expect("Failed to execute process");
Command::new("pactl")
.args(&["load-module", "module-remap-source", "master=VirtualMic.monitor", "source_name=VirtualMicSource", "source_properties=device.description=\"Virtual_Mic_Source\""])
.output()
.expect("Failed to execute process");
host = cpal::host_from_id(cpal::HostId::Alsa).expect("Could not initialize audio routing using ALSA"); // Alsa needed so pulse default works
virtual_mic = host.default_output_device().expect("Could not get default output device");
let virtual_mic_stream = OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream");
move_playback_to_sink();
return virtual_mic_stream;
// return (OutputStreamBuilder::from_device(normal_output).expect("Unable to open default audio device").open_stream().expect("Failed to open stream"), OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream"));
}
#[allow(unreachable_code)] {
println!("Unknown/unsupported OS. Audio support may not work or may route to default output (headset, headphones, etc).");
host = cpal::default_host();
virtual_mic = host.default_output_device().expect("Could not get default output device");
return OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream")
// normal_output = host.default_output_device().expect("Could not get default output device");
// return (OutputStreamBuilder::from_device(normal_output).expect("Unable to open default audio device").open_stream().expect("Failed to open stream"), OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream"));
} }
} #[allow(unreachable_code)]
{
fn reload_sound() -> OutputStream { let host = cpal::default_host();
if cfg!(target_os = "linux"){ let device = host.default_output_device().expect("Could not get default output device");
let script = r#" SoundSystem {
pactl list modules short | grep "Virtual_Microphone" | cut -f1 | xargs -L1 pactl unload-module output_stream: OutputStreamBuilder::from_device(device)
pactl list modules short | grep "Virtual_Mic_Source" | cut -f1 | xargs -L1 pactl unload-module .expect("Unable to open device")
"#; .open_stream()
.expect("Failed to open stream"),
let output = Command::new("sh") // this is actually not needed here, since windows would exit by far. But, cargo doesnt like SoundSystem not getting the normal_output stream so...
.arg("-c") #[cfg(target_os = "windows")]
.arg(script) normal_output_stream: OutputStreamBuilder::from_device(device)
.output() .expect("Unable to open device")
.expect("Failed to execute process"); .open_stream()
.expect("Failed to open stream"),
if output.status.success() { paused: false,
println!("Modules unloaded successfully.");
} else {
println!("Error: {}", String::from_utf8_lossy(&output.stderr));
} }
} }
}
fn reload_sound() -> SoundSystem {
#[cfg(target_os = "linux")]
linux_lib::reload_sound();
return create_virtual_mic(); return create_virtual_mic();
} }
fn main() { fn list_outputs() -> Vec<(String, String)> {
let virtual_mic_stream = create_virtual_mic(); #[cfg(target_os = "windows")]
// let (normal_output_stream, virtual_mic_stream) = create_virtual_mic(); return Vec::from([("Select in apps".to_string(), String::from("9999999"))]);
#[cfg(target_os = "linux")]
return linux_lib::list_outputs();
#[allow(unreachable_code)]
return Vec::new();
}
fn main() {
App::new() App::new()
.insert_resource(ClearColor(Color::BLACK)) .insert_resource(ClearColor(Color::BLACK))
.add_plugins( .add_plugins(
DefaultPlugins DefaultPlugins
.set(LogPlugin { .set(bevy::log::LogPlugin {
filter: "warn,ui=info".to_string(), filter: "warn,ui=info".to_string(),
level: Level::INFO, level: Level::INFO,
..Default::default() ..Default::default()
@@ -159,17 +129,16 @@ fn main() {
..default() ..default()
}), }),
) )
.add_plugins(EguiPlugin::default()) .add_plugins(bevy_egui::EguiPlugin::default())
.insert_resource(AppState { .insert_resource(AppState {
loaded_files: HashMap::new(), loaded_files: HashMap::new(),
json_data: JSONData { tabs: Vec::new() }, json_data: JSONData { tabs: Vec::new() },
current_directory: String::new(), current_directory: String::new(),
currently_playing: Vec::new(), currently_playing: Vec::new(),
sound_system: SoundSystem { sound_system: create_virtual_mic(),
virtual_mic_stream, virt_outputs: Vec::new(),
// normal_output_stream, virt_output_index_switch: String::from("0"),
paused: false virt_output_index: String::from("999"),
}
}) })
.add_systems( .add_systems(
PreStartup, PreStartup,
@@ -178,12 +147,28 @@ fn main() {
.add_systems(Startup, load_system) .add_systems(Startup, load_system)
.add_systems( .add_systems(
EguiPrimaryContextPass, EguiPrimaryContextPass,
(ui_system, update_ui_scale_factor_system), (ui_system, update_ui_scale_factor_system, update_virtualmic),
) )
.run(); .run();
} }
fn update_virtualmic(mut app_state: ResMut<AppState>) {
if app_state.virt_outputs.is_empty() {
return;
}
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>) { fn load_system(mut app_state: ResMut<AppState>) {
app_state.virt_outputs = list_outputs();
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);
} }
@@ -209,7 +194,15 @@ fn load_data(app_state: &mut AppState) {
.filter_map(|entry| { .filter_map(|entry| {
entry.ok().and_then(|e| { entry.ok().and_then(|e| {
let path = e.path(); let path = e.path();
if path.is_file() && ALLOWED_FILE_EXTENSIONS.contains(&path.extension().expect("Could not find extension").to_str().expect("Could not convert extension to string")) { if path.is_file()
&& ALLOWED_FILE_EXTENSIONS.contains(
&path
.extension()
.unwrap_or_default()
.to_str()
.expect("Could not convert extension to string"),
)
{
path.to_str().map(|s| s.to_string()) path.to_str().map(|s| s.to_string())
} else { } else {
None None
@@ -227,33 +220,44 @@ fn setup_camera_system(mut commands: Commands) {
commands.spawn(Camera2d); commands.spawn(Camera2d);
} }
fn update_ui_scale_factor_system( fn update_ui_scale_factor_system(egui_context: Single<(&mut EguiContextSettings, &Camera)>) {
egui_context: Single<(&mut EguiContextSettings, &Camera)>,
) {
let (mut egui_settings, camera) = egui_context.into_inner(); let (mut egui_settings, camera) = egui_context.into_inner();
egui_settings.scale_factor = 1.5 / camera.target_scaling_factor().unwrap_or(1.5); egui_settings.scale_factor = 1.5 / camera.target_scaling_factor().unwrap_or(1.5);
} }
fn play_sound(file_path: String, app_state: &mut AppState) { fn play_sound(file_path: String, app_state: &mut AppState) {
let virtual_file = File::open(&file_path).unwrap(); let file = File::open(&file_path).unwrap();
let virtual_src = Decoder::new(BufReader::new(virtual_file)).unwrap(); let src = Decoder::new(BufReader::new(file)).unwrap();
let virtual_sink = Sink::connect_new(&app_state.sound_system.virtual_mic_stream.mixer()); let sink = Sink::connect_new(&app_state.sound_system.output_stream.mixer());
let length = virtual_src.total_duration().expect("Could not get source duration").as_secs_f32(); let length = src
virtual_sink.append(virtual_src); .total_duration()
virtual_sink.play(); .expect("Could not get source duration")
.as_secs_f32();
// let normal_file = File::open(&file_path).unwrap(); sink.append(src);
// let normal_src = Decoder::new(BufReader::new(normal_file)).unwrap(); sink.play();
// let normal_sink = Sink::connect_new(&app_state.sound_system.normal_output_stream.mixer());
// normal_sink.append(normal_src);
// normal_sink.play();
#[cfg(target_os = "windows")]
app_state.currently_playing.push(PlayingSound { {
file_path: file_path.clone(), let file2 = File::open(&file_path).unwrap();
let src2 = Decoder::new(BufReader::new(file2)).unwrap();
let normal_sink = Sink::connect_new(&app_state.sound_system.normal_output_stream.mixer());
normal_sink.append(src2);
app_state.currently_playing.push(PlayingSound {
file_path: file_path.clone(),
length,
sink,
normal_sink
});
return;
}
app_state.currently_playing.push(PlayingSound {
file_path: file_path.clone(),
length, length,
virtual_sink, sink
// normal_sink
}) })
} }
@@ -269,11 +273,41 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
ui.separator(); ui.separator();
let available_width = ui.available_width();
let available_height = ui.available_height(); let available_height = ui.available_height();
let outputs = app_state.virt_outputs.clone();
#[allow(unused_mut)]
let mut mic_name = "Select inside apps".to_string();
#[cfg(target_os = "linux")]
{
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() {
mic_name = app_name.to_string();
}
}
ui.label("Virtual Mic Output");
egui::ComboBox::from_id_salt("Virtual Mic Output")
.selected_text(mic_name)
.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 ui if ui
.add_sized( .add_sized(
[ui.available_width(), available_height / 15.0], [available_width, available_height / 15.0],
egui::Button::new("Add folder"), egui::Button::new("Add folder"),
) )
.clicked() .clicked()
@@ -297,7 +331,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
if ui if ui
.add_sized( .add_sized(
[ui.available_width(), available_height / 15.0], [available_width, available_height / 15.0],
egui::Button::new("Reload content"), egui::Button::new("Reload content"),
) )
.clicked() .clicked()
@@ -308,7 +342,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
if ui if ui
.add_sized( .add_sized(
[ui.available_width(), available_height / 15.0], [available_width, available_height / 15.0],
egui::Button::new("Youtube downloader"), egui::Button::new("Youtube downloader"),
) )
.clicked() .clicked()
@@ -318,14 +352,13 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
if ui if ui
.add_sized( .add_sized(
[ui.available_width(), available_height / 15.0], [available_width, available_height / 15.0],
egui::Button::new("Reload sound system"), egui::Button::new("Reload sound system"),
) )
.clicked() .clicked()
{ {
app_state.currently_playing.clear(); app_state.currently_playing.clear();
app_state.sound_system.virtual_mic_stream = reload_sound(); app_state.sound_system = reload_sound();
// (app_state.sound_system.normal_output_stream, app_state.sound_system.virtual_mic_stream) = reload_sound();
println!("Sucessfully reloaded sound system!"); println!("Sucessfully reloaded sound system!");
} }
}); });
@@ -334,14 +367,18 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
ui.horizontal(|ui| { ui.horizontal(|ui| {
if app_state.sound_system.paused { if app_state.sound_system.paused {
ui.heading("Paused"); ui.heading("Paused");
} } else {
else {
ui.heading("Playing"); ui.heading("Playing");
} }
ui.vertical(|ui| { ui.vertical(|ui| {
for playing_sound in &app_state.currently_playing { for playing_sound in &app_state.currently_playing {
ui.label(format!("{} - {:.2} / {:.2}", playing_sound.file_path, playing_sound.virtual_sink.get_pos().as_secs_f32(), playing_sound.length)); ui.label(format!(
"{} - {:.2} / {:.2}",
playing_sound.file_path,
playing_sound.sink.get_pos().as_secs_f32(),
playing_sound.length
));
} }
}) })
}); });
@@ -349,15 +386,22 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
let available_height = ui.available_height(); let available_height = ui.available_height();
ui.horizontal(|ui| { ui.horizontal(|ui| {
let available_width = ui.available_width(); let available_width = ui.available_width();
let current_directories = app_state.loaded_files.keys().cloned().collect::<Vec<_>>(); let current_directories = app_state.loaded_files.keys().cloned().collect::<Vec<_>>();
for directory in current_directories.clone() { for directory in current_directories.clone() {
let mut button = egui::Button::new(&directory);
if directory == app_state.current_directory {
button = button.fill(Color32::BLACK);
}
if ui if ui
.add_sized( .add_sized(
[available_width / current_directories.len() as f32, available_height / 15.0], [
egui::Button::new(&directory), available_width / current_directories.len() as f32,
available_height / 15.0,
],
button,
) )
.clicked() .clicked()
{ {
@@ -366,10 +410,6 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
} }
}); });
ui.add_space(available_height / 50.0); ui.add_space(available_height / 50.0);
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
ui.label(egui::RichText::new(format!("The current directory is {}", app_state.current_directory)).font(egui::FontId::proportional(20.0)));
});
ui.add_space(available_height / 50.0);
if app_state.current_directory.chars().count() > 0 { if app_state.current_directory.chars().count() > 0 {
let files = app_state let files = app_state
.loaded_files .loaded_files
@@ -380,10 +420,13 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
for element in files { for element in files {
if let Some(filename) = element.split("/").collect::<Vec<_>>().last() { if let Some(filename) = element.split("/").collect::<Vec<_>>().last() {
if ui.add_sized( if ui
[ui.available_width(), available_height / 15.0], .add_sized(
egui::Button::new(*filename), [ui.available_width(), available_height / 15.0],
).clicked() { egui::Button::new(*filename),
)
.clicked()
{
let path = Path::new(&app_state.current_directory) let path = Path::new(&app_state.current_directory)
.join(filename) .join(filename)
.to_string_lossy() .to_string_lossy()
@@ -395,10 +438,10 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
}); });
} }
}); });
app_state.currently_playing.retain(|playing_sound| { app_state.currently_playing.retain(|playing_sound| {
playing_sound.virtual_sink.get_pos().as_secs_f32() <= (playing_sound.length - 0.01) // 0.01 offset needed here because of floating point errors and so its not exact playing_sound.sink.get_pos().as_secs_f32() <= (playing_sound.length - 0.01) // 0.01 offset needed here because of floating point errors and so its not exact
}); });
Ok(()) Ok(())
} }

14
src/windows_lib.rs Normal file
View File

@@ -0,0 +1,14 @@
use rodio::{OutputStream, OutputStreamBuilder, cpal::{self, traits::HostTrait, traits::DeviceTrait}};
pub fn create_virtual_mic_windows() -> (OutputStream, OutputStream) {
let host = cpal::host_from_id(cpal::HostId::Wasapi).expect("Could not initialize audio routing using WasAPI");
let virtual_mic = host.output_devices().expect("Could not list Output devices").find(|device| {
device.name().ok().map(|name|{
name.contains("CABLE Input") || name.contains("VB-Audio")
}).unwrap_or(false)
}).expect("Could not get default output device");
let normal_output = host.default_output_device().expect("Could not get default output device");
return (OutputStreamBuilder::from_device(normal_output).expect("Unable to open default audio device").open_stream().expect("Failed to open stream"), OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream"));
}