mirror of
https://github.com/csd4ni3l/soundboard.git
synced 2026-03-10 17:19:24 +01:00
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.
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
"#;
|
"#;
|
||||||
|
|||||||
103
src/main.rs
103
src/main.rs
@@ -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.output_stream = 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(())
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user