diff --git a/run_linux.sh b/run_linux.sh index 9dd8e53..c0e52f4 100644 --- a/run_linux.sh +++ b/run_linux.sh @@ -1,4 +1,5 @@ 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_Mic_Source" | cut -f1 | xargs -L1 pactl unload-module \ No newline at end of file diff --git a/src/linux_lib.rs b/src/linux_lib.rs index 8016ada..b7e4259 100644 --- a/src/linux_lib.rs +++ b/src/linux_lib.rs @@ -17,37 +17,45 @@ fn pactl_list(sink_type: &str) -> Value { } } -pub fn get_device_by_index(sink_type: &str, index: String) -> Value { - let devices = pactl_list(sink_type); +pub fn get_sink_by_index(sink_type: &str, index: String) -> Value { + let sinks = pactl_list(sink_type); - for device in devices.as_array().unwrap_or(&vec![]) { - if device["index"].as_u64().expect("Device index is not a number").to_string() == index { - return device.clone(); + 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{}; } -pub fn move_playback_to_sink() { +fn find_soundboard_sinks() -> Vec { let sink_inputs = pactl_list("sink-inputs"); - for device in sink_inputs.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"); - } + 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(), "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)> { let source_outputs = pactl_list("source-outputs"); - return source_outputs.as_array().unwrap_or(&vec![]).iter().filter_map(|device| { - let app_name = device["properties"]["application.name"].as_str()?; - let binary = device["properties"]["application.process.binary"].as_str().unwrap_or("Unknown"); - let index = device["index"].as_u64().expect("Device index is not a number").to_string(); + 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(); } @@ -60,36 +68,42 @@ pub fn move_index_to_virtualmic(index: String) { } 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") .args(&["load-module", "module-null-sink", "sink_name=VirtualMic", "sink_properties=device.description=\"Virtual_Microphone\""]) .output() - .expect("Failed to execute process"); + .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 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") .args(&["set-sink-volume", "VirtualMic", "100%"]) .output() - .expect("Failed to set sink volume"); - Command::new("pactl") - .args(&["set-sink-volume", "VirtualMicSource", "100%"]) - .output() - .expect("Failed to set sink volume"); + .expect("Failed to set volume"); - 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"); + 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 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() { 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 "#; diff --git a/src/main.rs b/src/main.rs index 2472d08..2958986 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,7 @@ mod linux_lib; mod windows_lib; use rodio::{ - Decoder, OutputStream, OutputStreamBuilder, Sink, Source, - cpal::{self, traits::HostTrait}, + Decoder, OutputStream, Sink, Source, cpal::{self, traits::HostTrait}, OutputStreamBuilder }; #[derive(Serialize, Deserialize)] @@ -28,13 +27,15 @@ struct JSONData { struct PlayingSound { file_path: String, length: f32, - virtual_sink: Sink, - // normal_sink: Sink + sink: Sink, + #[cfg(target_os = "windows")] + normal_sink: Sink } struct SoundSystem { - virtual_mic_stream: OutputStream, - // normal_output_stream: OutputStream, + #[cfg(target_os = "windows")] + normal_output_stream: OutputStream, + output_stream: OutputStream, paused: bool, } @@ -52,32 +53,40 @@ struct AppState { const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"]; -fn create_virtual_mic() -> OutputStream { +fn create_virtual_mic() -> SoundSystem { #[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")] - return linux_lib::create_virtual_mic_linux(); + { + return SoundSystem { + output_stream: linux_lib::create_virtual_mic_linux(), + paused: false, + }; + } #[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 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")); + let device = host.default_output_device().expect("Could not get default output device"); + SoundSystem { + output_stream: OutputStreamBuilder::from_device(device) + .expect("Unable to open device") + .open_stream() + .expect("Failed to open stream"), + paused: false, + } } } -fn reload_sound() -> OutputStream { +fn reload_sound() -> SoundSystem { #[cfg(target_os = "linux")] linux_lib::reload_sound(); @@ -96,9 +105,6 @@ fn list_outputs() -> Vec<(String, String)> { } fn main() { - let virtual_mic_stream = create_virtual_mic(); - // let (normal_output_stream, virtual_mic_stream) = create_virtual_mic(); - App::new() .insert_resource(ClearColor(Color::BLACK)) .add_plugins( @@ -123,11 +129,7 @@ fn main() { json_data: JSONData { tabs: Vec::new() }, current_directory: String::new(), currently_playing: Vec::new(), - sound_system: SoundSystem { - virtual_mic_stream, - // normal_output_stream, - paused: false, - }, + sound_system: create_virtual_mic(), virt_outputs: Vec::new(), virt_output_index_switch: String::from("0"), 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) { - let virtual_file = File::open(&file_path).unwrap(); - let virtual_src = Decoder::new(BufReader::new(virtual_file)).unwrap(); - let virtual_sink = Sink::connect_new(&app_state.sound_system.virtual_mic_stream.mixer()); - let length = virtual_src + let file = File::open(&file_path).unwrap(); + let src = Decoder::new(BufReader::new(file)).unwrap(); + let sink = Sink::connect_new(&app_state.sound_system.output_stream.mixer()); + let length = src .total_duration() .expect("Could not get source duration") .as_secs_f32(); - virtual_sink.append(virtual_src); - virtual_sink.play(); + sink.append(src); - // let normal_file = File::open(&file_path).unwrap(); - // let normal_src = Decoder::new(BufReader::new(normal_file)).unwrap(); - // 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")] + { + 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()); + sink2.append(src2); + } + + sink.play(); app_state.currently_playing.push(PlayingSound { file_path: file_path.clone(), length, - virtual_sink, - // normal_sink + sink, + #[cfg(target_os = "windows")] + normal_sink }) } @@ -263,8 +269,8 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut) -> Res #[cfg(target_os = "linux")] { let output_index = app_state.virt_output_index.clone(); - let output_device = linux_lib::get_device_by_index("source-outputs", output_index); - if let Some(app_name) = output_device["properties"]["application.name"].as_str() { + 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(); } } @@ -338,8 +344,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut) -> Res .clicked() { app_state.currently_playing.clear(); - app_state.sound_system.virtual_mic_stream = reload_sound(); - // (app_state.sound_system.normal_output_stream, app_state.sound_system.virtual_mic_stream) = reload_sound(); + app_state.sound_system.output_stream = reload_sound(); println!("Sucessfully reloaded sound system!"); } }); @@ -357,7 +362,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut) -> Res ui.label(format!( "{} - {:.2} / {:.2}", playing_sound.file_path, - playing_sound.virtual_sink.get_pos().as_secs_f32(), + playing_sound.sink.get_pos().as_secs_f32(), playing_sound.length )); } @@ -421,7 +426,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut) -> Res }); 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(()) diff --git a/src/windows_lib.rs b/src/windows_lib.rs index fffdfcd..8778388 100644 --- a/src/windows_lib.rs +++ b/src/windows_lib.rs @@ -1,13 +1,14 @@ 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 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"); - // 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")); + + 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")); } \ No newline at end of file