mirror of
https://github.com/csd4ni3l/soundboard.git
synced 2026-04-17 16:07:22 +02:00
Compare commits
19 Commits
latest
...
e7fbf8f4d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7fbf8f4d8 | ||
|
|
815663b039 | ||
|
|
0ae204ed0f | ||
|
|
d78c3c22c9 | ||
|
|
5a4ebe3467 | ||
|
|
07c0457701 | ||
|
|
58deb34135 | ||
|
|
ff71f6f21a | ||
|
|
ca6fc96f99 | ||
|
|
6480c80cb4 | ||
|
|
320337567d | ||
|
|
83f53f8711 | ||
|
|
21d9f6c08b | ||
|
|
abb7704e21 | ||
|
|
4d065525ff | ||
|
|
73efdbd8b3 | ||
|
|
caa4f7d2d0 | ||
|
|
a42c69c845 | ||
|
|
d5a7dd624b |
821
Cargo.lock
generated
821
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -7,14 +7,26 @@ edition = "2024"
|
|||||||
bevy_egui = "0.38.1"
|
bevy_egui = "0.38.1"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
rfd = "0.16.0"
|
rfd = "0.16.0"
|
||||||
|
ringbuf = "0.4.8"
|
||||||
rodio = { version = "0.21.1", features = ["mp3", "wav", "flac", "vorbis"] }
|
rodio = { version = "0.21.1", features = ["mp3", "wav", "flac", "vorbis"] }
|
||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
serde_json = "1.0.146"
|
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 +34,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"
|
||||||
|
]
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -1,15 +1,23 @@
|
|||||||
|
# csd4ni3l Soundboard
|
||||||
|
|
||||||
Soundboard made in Rust & Bevy. My first Rust project.
|
Soundboard made in Rust & Bevy. My first Rust project.
|
||||||
|
|
||||||
# Support & Requirements
|
## Support & Requirements
|
||||||
- On all OSes, you need to still select the device inside the app you want to use it in.
|
|
||||||
|
|
||||||
## Linux
|
## Linux
|
||||||
|
|
||||||
- Needs the `mold` linker and `clang` to compile fast
|
- Needs the `mold` linker and `clang` to compile fast
|
||||||
- ALSA & PulseAudio/Pipewire-pulse is a requirement
|
- ALSA & PulseAudio/Pipewire-pulse is a requirement
|
||||||
|
- Can use auto-selection of app to use the virtual mic in.
|
||||||
|
- Auto-routes mic to virtual mic by default, so others can also hear you.
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
- Needs the VB-Cable driver (https://vb-audio.com/Cable/)
|
|
||||||
|
- Needs the [VB-Cable driver](https://vb-audio.com/Cable/)
|
||||||
|
- You need to still select the device inside the app you want to use it in.
|
||||||
|
- They only hear the soundboard as of right now, not your actual mic.
|
||||||
|
|
||||||
## MacOS & Other
|
## MacOS & Other
|
||||||
|
|
||||||
- Might work as a music player with the default output device.
|
- Might work as a music player with the default output device.
|
||||||
- Not supported and not planned.
|
- Not supported and not planned.
|
||||||
|
|||||||
@@ -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
|
||||||
210
src/linux_lib.rs
Normal file
210
src/linux_lib.rs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
use rodio::{
|
||||||
|
OutputStream, OutputStreamBuilder,
|
||||||
|
cpal::{self, traits::HostTrait},
|
||||||
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
const APPS_TO_EXCLUDE: [&str; 1] = ["plasmashell"];
|
||||||
|
|
||||||
|
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");
|
||||||
|
if APPS_TO_EXCLUDE.contains(&binary) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
459
src/main.rs
459
src/main.rs
@@ -1,19 +1,24 @@
|
|||||||
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, time::Instant};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use bevy_egui::{
|
use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui::{self, Context, Ui}};
|
||||||
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, OutputStreamBuilder, Sink, Source,
|
||||||
|
cpal::{self, traits::HostTrait},
|
||||||
};
|
};
|
||||||
|
|
||||||
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 +28,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
|
to_remove: bool,
|
||||||
|
#[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,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
@@ -39,113 +46,78 @@ 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,
|
||||||
|
last_virt_output_update: Instant,
|
||||||
|
current_view: 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)
|
};
|
||||||
}).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(),
|
||||||
|
};
|
||||||
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
|
||||||
let script = r#"
|
.default_output_device()
|
||||||
pactl list modules short | grep "Virtual_Microphone" | cut -f1 | xargs -L1 pactl unload-module
|
.expect("Could not get default output device");
|
||||||
pactl list modules short | grep "Virtual_Mic_Source" | cut -f1 | xargs -L1 pactl unload-module
|
SoundSystem {
|
||||||
"#;
|
output_stream: OutputStreamBuilder::from_device(device)
|
||||||
|
.expect("Unable to open device")
|
||||||
let output = Command::new("sh")
|
.open_stream()
|
||||||
.arg("-c")
|
.expect("Failed to open stream"),
|
||||||
.arg(script)
|
// this is actually not needed here, since windows would exit by far. But, cargo doesnt like SoundSystem not getting the normal_output stream so...
|
||||||
.output()
|
#[cfg(target_os = "windows")]
|
||||||
.expect("Failed to execute process");
|
normal_output_stream: OutputStreamBuilder::from_device(device)
|
||||||
|
.expect("Unable to open device")
|
||||||
if output.status.success() {
|
.open_stream()
|
||||||
println!("Modules unloaded successfully.");
|
.expect("Failed to open stream"),
|
||||||
} 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 inside 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 +131,18 @@ 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"),
|
||||||
}
|
current_view: "main".to_string(),
|
||||||
|
last_virt_output_update: Instant::now()
|
||||||
})
|
})
|
||||||
.add_systems(
|
.add_systems(
|
||||||
PreStartup,
|
PreStartup,
|
||||||
@@ -178,12 +151,36 @@ 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),
|
(draw, update_ui_scale_factor_system, update),
|
||||||
)
|
)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_system(mut app_state: ResMut<AppState>) {
|
fn update(mut app_state: ResMut<AppState>) {
|
||||||
|
if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 3.0 {
|
||||||
|
app_state.last_virt_output_update = Instant::now();
|
||||||
|
app_state.virt_outputs = list_outputs();
|
||||||
|
}
|
||||||
|
|
||||||
|
if app_state.virt_outputs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(app_state.virt_output_index == "999".to_string()) {
|
||||||
|
app_state.virt_output_index_switch = app_state.virt_outputs[0].1.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
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>) {
|
||||||
|
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 +206,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,53 +232,89 @@ 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 length = src
|
||||||
let length = virtual_src.total_duration().expect("Could not get source duration").as_secs_f32();
|
.total_duration()
|
||||||
virtual_sink.append(virtual_src);
|
.expect("Could not get source duration")
|
||||||
virtual_sink.play();
|
.as_secs_f32();
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
|
let sink = Sink::connect_new(&app_state.sound_system.output_stream.mixer());
|
||||||
app_state.currently_playing.push(PlayingSound {
|
sink.append(src);
|
||||||
file_path: file_path.clone(),
|
sink.play();
|
||||||
|
|
||||||
|
let playing_sound = PlayingSound {
|
||||||
|
file_path: file_path.clone(),
|
||||||
length,
|
length,
|
||||||
virtual_sink,
|
sink,
|
||||||
// normal_sink
|
to_remove: false,
|
||||||
})
|
#[cfg(target_os = "windows")]
|
||||||
|
normal_sink: {
|
||||||
|
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);
|
||||||
|
normal_sink.play();
|
||||||
|
normal_sink
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
app_state.currently_playing.push(playing_sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
fn create_virtual_mic_dropdown(ui: &mut Ui, app_state: &mut ResMut<AppState>, available_width: f32, available_height: f32) {
|
||||||
let ctx = contexts.ctx_mut()?;
|
#[cfg(target_os = "linux")] {
|
||||||
|
let outputs = app_state.virt_outputs.clone();
|
||||||
|
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() {
|
||||||
|
egui::ComboBox::from_id_salt("Virtual Mic Output")
|
||||||
|
.selected_text(app_name.to_string())
|
||||||
|
.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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ui.add(egui::Button::new("No apps found to use.".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
return;
|
||||||
ui.heading("csd4ni3l Soundboard");
|
}
|
||||||
});
|
#[allow(unreachable_code)]
|
||||||
|
{
|
||||||
|
ui.add(egui::Button::new("Unsupported. Select inside apps.".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main_ui(ctx: &Context, mut app_state: ResMut<AppState>) {
|
||||||
egui::SidePanel::right("tools").show(ctx, |ui| {
|
egui::SidePanel::right("tools").show(ctx, |ui| {
|
||||||
ui.heading("Tools");
|
ui.heading("Tools");
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
|
let available_width = ui.available_width();
|
||||||
let available_height = ui.available_height();
|
let available_height = ui.available_height();
|
||||||
|
ui.label("Virtual Mic Output");
|
||||||
|
create_virtual_mic_dropdown(ui, &mut app_state, available_width, available_height);
|
||||||
|
|
||||||
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 +338,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,56 +349,45 @@ 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()
|
||||||
{
|
{
|
||||||
println!("Youtube downloader!");
|
app_state.current_view = "youtube_downloader".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
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!");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::TopBottomPanel::bottom("currently_playing").show(ctx, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if app_state.sound_system.paused {
|
|
||||||
ui.heading("Paused");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ui.heading("Playing");
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 +396,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 +406,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 +424,90 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> 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
|
fn youtube_downloader_ui(ctx: &Context, app_state: ResMut<AppState>) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ui.heading(format!("Coming Soon! Currently on {} view.", app_state.current_view)); // view is only included here so there is no warning about app_state not being used.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
||||||
|
let ctx = contexts.ctx_mut()?;
|
||||||
|
|
||||||
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
|
ui.heading("csd4ni3l Soundboard");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
egui::TopBottomPanel::bottom("currently_playing").show(ctx, |ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
for playing_sound in &mut app_state.currently_playing {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(format!(
|
||||||
|
"{} - {:.2} / {:.2}",
|
||||||
|
playing_sound.file_path,
|
||||||
|
playing_sound.sink.get_pos().as_secs_f32(),
|
||||||
|
playing_sound.length
|
||||||
|
));
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let available_height = ui.available_height();
|
||||||
|
if ui
|
||||||
|
.add_sized(
|
||||||
|
[
|
||||||
|
available_width / 2 as f32,
|
||||||
|
available_height,
|
||||||
|
],
|
||||||
|
egui::Button::new("Stop"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
playing_sound.to_remove = true;
|
||||||
|
};
|
||||||
|
if ui
|
||||||
|
.add_sized(
|
||||||
|
[
|
||||||
|
available_width / 2 as f32,
|
||||||
|
available_height,
|
||||||
|
],
|
||||||
|
egui::Button::new(if playing_sound.sink.is_paused() {"Resume"} else {"Pause"}),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
if playing_sound.sink.is_paused() {
|
||||||
|
playing_sound.sink.play();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
playing_sound.sink.pause();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let available_height = ui.available_height();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.add_sized(
|
||||||
|
[available_width, available_height / 15.0],
|
||||||
|
egui::Button::new("Stop all"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
app_state.currently_playing.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app_state.currently_playing.retain(|playing_sound| { // retains happen the next cycle, not in the current one because of borrowing and im lazy to fix
|
||||||
|
playing_sound.sink.get_pos().as_secs_f32() <= (playing_sound.length - 0.01) && !playing_sound.to_remove // 0.01 offset needed here because of floating point errors and so its not exact
|
||||||
|
});
|
||||||
|
|
||||||
|
if app_state.current_view == "main".to_string() {
|
||||||
|
main_ui(ctx, app_state);
|
||||||
|
}
|
||||||
|
else if app_state.current_view == "youtube_downloader".to_string() {
|
||||||
|
youtube_downloader_ui(ctx, app_state);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/windows_lib.rs
Normal file
78
src/windows_lib.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use rodio::{
|
||||||
|
OutputStream, OutputStreamBuilder,
|
||||||
|
cpal::{self, traits::{DeviceTrait, StreamTrait, HostTrait}, StreamConfig, SampleRate},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ringbuf::{traits::*, HeapRb};
|
||||||
|
|
||||||
|
fn route_standard_to_virtual(host: &cpal::Host, virtual_mic: &cpal::Device) {
|
||||||
|
let standard_mic = host.default_input_device().expect("Could not get default input device.");
|
||||||
|
|
||||||
|
let config = StreamConfig {
|
||||||
|
channels: 2,
|
||||||
|
sample_rate: SampleRate(48_000),
|
||||||
|
buffer_size: cpal::BufferSize::Default,
|
||||||
|
};
|
||||||
|
let rb = HeapRb::<f32>::new(48_000 * 2);
|
||||||
|
let (mut producer, mut consumer) = rb.split();
|
||||||
|
|
||||||
|
let input_stream = standard_mic.build_input_stream(
|
||||||
|
&config,
|
||||||
|
move |data: &[f32], _| {
|
||||||
|
for &sample in data {
|
||||||
|
let _ = producer.try_push(sample);
|
||||||
|
let _ = producer.try_push(sample);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |err| eprintln!("Input stream error: {err}"),
|
||||||
|
None,
|
||||||
|
).expect("Could not build input stream for standard to virtual mic routing");
|
||||||
|
|
||||||
|
let output_stream = virtual_mic.build_output_stream(
|
||||||
|
&config,
|
||||||
|
move |data: &mut [f32], _| {
|
||||||
|
for sample in data {
|
||||||
|
*sample = consumer.try_pop().unwrap_or(0.0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |err| eprintln!("Output stream error: {err}"),
|
||||||
|
None,
|
||||||
|
).expect("Could not build output stream for standard to virtual mic routing");
|
||||||
|
|
||||||
|
input_stream.play();
|
||||||
|
output_stream.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 VB Cable output device. Is VB Cable Driver installed?");
|
||||||
|
|
||||||
|
route_standard_to_virtual(&host, &virtual_mic);
|
||||||
|
|
||||||
|
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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user