Improve README, make bevy wayland & x11 a linux-only dependency, add rust building, fix dead code warnings, try adding normal playing with virtual (didnt work, comments), fix pulse not working sometimes by adding a manual playback move to the virtual mic, make windows, linux and other virtual mic code per-system

This commit is contained in:
csd4ni3l
2026-01-23 18:18:38 +01:00
parent 4fff1d4709
commit b539327672
4 changed files with 188 additions and 49 deletions

View File

@@ -6,26 +6,30 @@ use bevy::{
use std::{collections::HashMap, fs::File, io::BufReader, path::Path, process::Command};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use bevy_egui::{
EguiContextSettings, EguiContexts, EguiPlugin, EguiPrimaryContextPass, EguiStartupSet, egui,
};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source, cpal::{self, traits::HostTrait}};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source, cpal::{self, Device, Host, traits::HostTrait}};
#[derive(Serialize, Deserialize)]
struct JSONData {
tabs: Vec<String>,
}
#[allow(dead_code)]
struct PlayingSound {
file_path: String,
length: f32,
sink: Sink
virtual_sink: Sink,
// normal_sink: Sink
}
struct SoundSystem {
stream_handle: OutputStream,
virtual_mic_stream: OutputStream,
// normal_output_stream: OutputStream,
paused: bool
}
@@ -40,14 +44,48 @@ struct AppState {
const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"];
fn create_virtual_mic() -> OutputStream {
if cfg!(target_os = "windows") {
panic!("Windows is currently unsupported.");
fn move_playback_to_sink() {
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");
}
}
}
else if cfg!(target_os = "macos") {
panic!("MacOS is and will most likely stay unsupported.");
}
else if cfg!(target_os = "linux") {
}
fn create_virtual_mic() -> OutputStream {
let host: Host;
// let original_host: Host;
// let normal_output: Device;
let virtual_mic: Device;
#[cfg(target_os = "windows")]
{
host = cpal::host_from_id(cpal::HostId::Wasapi).expect("Could not initialize audio routing using WasAPI");
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(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")]
{
// 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()
@@ -56,29 +94,27 @@ fn create_virtual_mic() -> OutputStream {
.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"));
}
else {
panic!("I have no idea what OS you are on but it's not mainstream enough.");
}
unsafe {
std::env::set_var("PULSE_SINK", "VirtualMic");
#[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"));
}
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");
}
fn recreate_virtual_mic() -> OutputStream {
if cfg!(target_os = "windows") {
panic!("Windows is currently unsupported.");
}
else if cfg!(target_os = "macos") {
panic!("MacOS is and will most likely stay unsupported.");
}
else if cfg!(target_os = "linux"){
fn reload_sound() -> OutputStream {
if cfg!(target_os = "linux"){
let script = r#"
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
@@ -96,15 +132,13 @@ fn recreate_virtual_mic() -> OutputStream {
println!("Error: {}", String::from_utf8_lossy(&output.stderr));
}
}
else {
panic!("I have no idea what OS you are on but it's not mainstream enough.");
}
return create_virtual_mic();
}
fn main() {
let stream_handle = create_virtual_mic();
let virtual_mic_stream = create_virtual_mic();
// let (normal_output_stream, virtual_mic_stream) = create_virtual_mic();
App::new()
.insert_resource(ClearColor(Color::BLACK))
@@ -131,7 +165,8 @@ fn main() {
current_directory: String::new(),
currently_playing: Vec::new(),
sound_system: SoundSystem {
stream_handle,
virtual_mic_stream,
// normal_output_stream,
paused: false
}
})
@@ -199,16 +234,25 @@ fn update_ui_scale_factor_system(
}
fn play_sound(file_path: String, app_state: &mut AppState) {
let file = BufReader::new(File::open(&file_path).unwrap());
let src = Decoder::new(file).unwrap();
let length = src.total_duration().expect("Could not get source duration").as_secs_f32();
let sink = Sink::connect_new(&app_state.sound_system.stream_handle.mixer());
sink.append(src);
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.total_duration().expect("Could not get source duration").as_secs_f32();
virtual_sink.append(virtual_src);
virtual_sink.play();
// 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();
app_state.currently_playing.push(PlayingSound {
file_path: file_path.clone(),
length: length,
sink: sink
length,
virtual_sink,
// normal_sink
})
}
@@ -274,13 +318,14 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
if ui
.add_sized(
[ui.available_width(), available_height / 15.0],
egui::Button::new("Recreate Virtual Mic"),
egui::Button::new("Reload sound system"),
)
.clicked()
{
app_state.currently_playing.clear();
app_state.sound_system.stream_handle = recreate_virtual_mic();
println!("Recreated Virtual microphone!");
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();
println!("Sucessfully reloaded sound system!");
}
});
@@ -295,7 +340,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
ui.vertical(|ui| {
for playing_sound in &app_state.currently_playing {
ui.label(format!("{} - {:.2} / {:.2}", playing_sound.file_path, playing_sound.sink.get_pos().as_secs_f32(), playing_sound.length));
ui.label(format!("{} - {:.2} / {:.2}", playing_sound.file_path, playing_sound.virtual_sink.get_pos().as_secs_f32(), playing_sound.length));
}
})
});
@@ -351,7 +396,7 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
});
app_state.currently_playing.retain(|playing_sound| {
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
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
});
Ok(())