Compare commits

...

2 Commits

4 changed files with 107 additions and 86 deletions

View File

@@ -1,4 +1,5 @@
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

View File

@@ -17,37 +17,45 @@ fn pactl_list(sink_type: &str) -> Value {
} }
} }
pub fn get_device_by_index(sink_type: &str, index: String) -> Value { pub fn get_sink_by_index(sink_type: &str, index: String) -> Value {
let devices = pactl_list(sink_type); let sinks = pactl_list(sink_type);
for device in devices.as_array().unwrap_or(&vec![]) { for sink in sinks.as_array().unwrap_or(&vec![]) {
if device["index"].as_u64().expect("Device index is not a number").to_string() == index { if sink["index"].as_u64().expect("sink index is not a number").to_string() == index {
return device.clone(); return sink.clone();
} }
} }
return Value::Null{}; return Value::Null{};
} }
pub fn move_playback_to_sink() { fn find_soundboard_sinks() -> Vec<Value> {
let sink_inputs = pactl_list("sink-inputs"); let sink_inputs = pactl_list("sink-inputs");
for device in sink_inputs.as_array().unwrap_or(&vec![]) { sink_inputs.as_array()
if device["properties"]["node.name"] == "alsa_playback.soundboard" { .unwrap_or(&vec![])
let index = device["index"].as_u64().expect("Device index is not a number").to_string(); .iter()
Command::new("pactl") .filter(|sink| {sink["properties"]["node.name"] == "alsa_playback.soundboard"})
.args(&["move-sink-input", index.as_str(), "VirtualMic"]) // as_str is needed here as you cannot instantly dereference a growing String (Rust...) .cloned()
.output() .collect()
.expect("Failed to execute process"); }
}
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(), "VirtualMic"]) // 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)> { pub fn list_outputs() -> Vec<(String, String)> {
let source_outputs = pactl_list("source-outputs"); let source_outputs = pactl_list("source-outputs");
return source_outputs.as_array().unwrap_or(&vec![]).iter().filter_map(|device| { return source_outputs.as_array().unwrap_or(&vec![]).iter().filter_map(|sink| {
let app_name = device["properties"]["application.name"].as_str()?; let app_name = sink["properties"]["application.name"].as_str()?;
let binary = device["properties"]["application.process.binary"].as_str().unwrap_or("Unknown"); let binary = sink["properties"]["application.process.binary"].as_str().unwrap_or("Unknown");
let index = device["index"].as_u64().expect("Device index is not a number").to_string(); let index = sink["index"].as_u64().expect("sink index is not a number").to_string();
Some((format!("{} ({})", app_name, binary), index)) Some((format!("{} ({})", app_name, binary), index))
}).collect(); }).collect();
} }
@@ -60,36 +68,42 @@ pub fn move_index_to_virtualmic(index: String) {
} }
pub fn create_virtual_mic_linux() -> OutputStream { pub fn create_virtual_mic_linux() -> OutputStream {
// original_host = cpal::host_from_id(cpal::HostId::Alsa).expect("Could not initialize audio routing using ALSA");
// normal_output = original_host.default_output_device().expect("Could not get default output device");
Command::new("pactl") Command::new("pactl")
.args(&["load-module", "module-null-sink", "sink_name=VirtualMic", "sink_properties=device.description=\"Virtual_Microphone\""]) .args(&["load-module", "module-null-sink", "sink_name=VirtualMic", "sink_properties=device.description=\"Virtual_Microphone\""])
.output() .output()
.expect("Failed to execute process"); .expect("Failed to create VirtualMic");
Command::new("pactl") Command::new("pactl")
.args(&["load-module", "module-remap-source", "master=VirtualMic.monitor", "source_name=VirtualMicSource", "source_properties=device.description=\"Virtual_Mic_Source\""]) .args(&["load-module", "module-remap-source", "master=VirtualMic.monitor", "source_name=VirtualMicSource", "source_properties=device.description=\"Virtual_Mic_Source\""])
.output() .output()
.expect("Failed to execute process"); .expect("Failed to create VirtualMicSource");
Command::new("pactl")
.args(&["load-module", "module-loopback", "source=VirtualMic.monitor", "sink=@DEFAULT_SINK@", "latency_msec=1"])
.output()
.expect("Failed to create loopback");
Command::new("pactl") Command::new("pactl")
.args(&["set-sink-volume", "VirtualMic", "100%"]) .args(&["set-sink-volume", "VirtualMic", "100%"])
.output() .output()
.expect("Failed to set sink volume"); .expect("Failed to set volume");
Command::new("pactl")
.args(&["set-sink-volume", "VirtualMicSource", "100%"]) let host = cpal::host_from_id(cpal::HostId::Alsa).expect("Could not initialize ALSA");
.output() let device = host.default_output_device().expect("Could not get default output device");
.expect("Failed to set sink volume");
let stream = OutputStreamBuilder::from_device(device)
.expect("Unable to open VirtualMic")
.open_stream()
.expect("Failed to open stream");
let host = cpal::host_from_id(cpal::HostId::Alsa).expect("Could not initialize audio routing using ALSA"); // Alsa needed so pulse default works
let 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(); 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")); return stream;
} }
pub fn reload_sound() { pub fn reload_sound() {
let script = r#" 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_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
"#; "#;

View File

@@ -15,8 +15,7 @@ mod linux_lib;
mod windows_lib; mod windows_lib;
use rodio::{ use rodio::{
Decoder, OutputStream, OutputStreamBuilder, Sink, Source, Decoder, OutputStream, Sink, Source, cpal::{self, traits::HostTrait}, OutputStreamBuilder
cpal::{self, traits::HostTrait},
}; };
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -28,13 +27,15 @@ 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,
output_stream: OutputStream,
paused: bool, paused: bool,
} }
@@ -52,32 +53,40 @@ struct AppState {
const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"]; const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"];
fn create_virtual_mic() -> OutputStream { fn create_virtual_mic() -> SoundSystem {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
return windows_lib::create_virtual_mic_windows(); {
let (normal, virtual_mic) = windows_lib::create_virtual_mic_windows();
return SoundSystem {
output_stream: virtual_mic,
normal_output_stream: normal,
paused: false,
};
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
return linux_lib::create_virtual_mic_linux(); {
return SoundSystem {
output_stream: linux_lib::create_virtual_mic_linux(),
paused: false,
};
}
#[allow(unreachable_code)] #[allow(unreachable_code)]
{ {
println!(
"Unknown/unsupported OS. Audio support may not work or may route to default output (headset, headphones, etc)."
);
let host = cpal::default_host(); let host = cpal::default_host();
let virtual_mic = host let device = host.default_output_device().expect("Could not get default output device");
.default_output_device() SoundSystem {
.expect("Could not get default output device"); output_stream: OutputStreamBuilder::from_device(device)
return OutputStreamBuilder::from_device(virtual_mic) .expect("Unable to open device")
.expect("Unable to open default audio device") .open_stream()
.open_stream() .expect("Failed to open stream"),
.expect("Failed to open stream"); paused: false,
// 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"));
} }
} }
fn reload_sound() -> OutputStream { fn reload_sound() -> SoundSystem {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
linux_lib::reload_sound(); linux_lib::reload_sound();
@@ -96,9 +105,6 @@ fn list_outputs() -> Vec<(String, String)> {
} }
fn main() { fn main() {
let virtual_mic_stream = create_virtual_mic();
// let (normal_output_stream, virtual_mic_stream) = create_virtual_mic();
App::new() App::new()
.insert_resource(ClearColor(Color::BLACK)) .insert_resource(ClearColor(Color::BLACK))
.add_plugins( .add_plugins(
@@ -123,11 +129,7 @@ fn main() {
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,
// normal_output_stream,
paused: false,
},
virt_outputs: Vec::new(), virt_outputs: Vec::new(),
virt_output_index_switch: String::from("0"), virt_output_index_switch: String::from("0"),
virt_output_index: String::from("999"), virt_output_index: String::from("999"),
@@ -218,27 +220,31 @@ fn update_ui_scale_factor_system(egui_context: Single<(&mut EguiContextSettings,
} }
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 let length = src
.total_duration() .total_duration()
.expect("Could not get source duration") .expect("Could not get source duration")
.as_secs_f32(); .as_secs_f32();
virtual_sink.append(virtual_src); sink.append(src);
virtual_sink.play();
// let normal_file = File::open(&file_path).unwrap(); #[cfg(target_os = "windows")]
// let normal_src = Decoder::new(BufReader::new(normal_file)).unwrap(); {
// let normal_sink = Sink::connect_new(&app_state.sound_system.normal_output_stream.mixer()); let file2 = File::open(&file_path).unwrap();
// normal_sink.append(normal_src); let src2 = Decoder::new(BufReader::new(file2)).unwrap();
// normal_sink.play(); let normal_sink = Sink::connect_new(&app_state.sound_system.normal_output_stream.mixer());
sink2.append(src2);
}
sink.play();
app_state.currently_playing.push(PlayingSound { app_state.currently_playing.push(PlayingSound {
file_path: file_path.clone(), file_path: file_path.clone(),
length, length,
virtual_sink, sink,
// normal_sink #[cfg(target_os = "windows")]
normal_sink
}) })
} }
@@ -263,8 +269,8 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
let output_index = app_state.virt_output_index.clone(); let output_index = app_state.virt_output_index.clone();
let output_device = linux_lib::get_device_by_index("source-outputs", output_index); let output_sink = linux_lib::get_sink_by_index("source-outputs", output_index);
if let Some(app_name) = output_device["properties"]["application.name"].as_str() { if let Some(app_name) = output_sink["properties"]["application.name"].as_str() {
mic_name = app_name.to_string(); mic_name = app_name.to_string();
} }
} }
@@ -338,8 +344,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
.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!");
} }
}); });
@@ -357,7 +362,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
ui.label(format!( ui.label(format!(
"{} - {:.2} / {:.2}", "{} - {:.2} / {:.2}",
playing_sound.file_path, playing_sound.file_path,
playing_sound.virtual_sink.get_pos().as_secs_f32(), playing_sound.sink.get_pos().as_secs_f32(),
playing_sound.length playing_sound.length
)); ));
} }
@@ -421,7 +426,7 @@ 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(())

View File

@@ -1,13 +1,14 @@
use rodio::{OutputStream, OutputStreamBuilder, cpal::{self, traits::HostTrait, traits::DeviceTrait}}; use rodio::{OutputStream, OutputStreamBuilder, cpal::{self, traits::HostTrait, traits::DeviceTrait}};
pub fn create_virtual_mic_windows() -> OutputStream { 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 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| { let virtual_mic = host.output_devices().expect("Could not list Output devices").find(|device| {
device.name().ok().map(|name|{ device.name().ok().map(|name|{
name.contains("CABLE Input") || name.contains("VB-Audio") name.contains("CABLE Input") || name.contains("VB-Audio")
}).unwrap_or(false) }).unwrap_or(false)
}).expect("Could not get default output device"); }).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"); 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"));
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"));
} }