mirror of
https://github.com/csd4ni3l/soundboard.git
synced 2026-03-10 17:19:24 +01:00
Improve README, add untested Windows mic to virtual mic support, remove global pause and add per-sound pause & stop. Add youtube downloader UI and per-view rendering which doesnt do anything yet
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -3905,6 +3905,17 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ringbuf"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rodio"
|
name = "rodio"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -4224,6 +4235,7 @@ dependencies = [
|
|||||||
"bevy_egui",
|
"bevy_egui",
|
||||||
"rand",
|
"rand",
|
||||||
"rfd",
|
"rfd",
|
||||||
|
"ringbuf",
|
||||||
"rodio",
|
"rodio",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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"
|
||||||
|
|||||||
@@ -1,18 +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
|
||||||
|
|
||||||
## 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
151
src/main.rs
151
src/main.rs
@@ -4,7 +4,7 @@ use std::{collections::HashMap, fs::File, io::BufReader, path::Path, time::Insta
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui};
|
use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui::{self, Context}};
|
||||||
|
|
||||||
use egui::ecolor::Color32;
|
use egui::ecolor::Color32;
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ struct PlayingSound {
|
|||||||
file_path: String,
|
file_path: String,
|
||||||
length: f32,
|
length: f32,
|
||||||
sink: Sink,
|
sink: Sink,
|
||||||
|
to_remove: bool,
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
normal_sink: Sink,
|
normal_sink: Sink,
|
||||||
}
|
}
|
||||||
@@ -37,7 +38,6 @@ struct SoundSystem {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
normal_output_stream: OutputStream,
|
normal_output_stream: OutputStream,
|
||||||
output_stream: OutputStream,
|
output_stream: OutputStream,
|
||||||
paused: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
@@ -50,7 +50,8 @@ struct AppState {
|
|||||||
virt_outputs: Vec<(String, String)>,
|
virt_outputs: Vec<(String, String)>,
|
||||||
virt_output_index_switch: String,
|
virt_output_index_switch: String,
|
||||||
virt_output_index: String,
|
virt_output_index: String,
|
||||||
last_virt_output_update: Instant
|
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"];
|
||||||
@@ -62,7 +63,6 @@ fn create_virtual_mic() -> SoundSystem {
|
|||||||
return SoundSystem {
|
return SoundSystem {
|
||||||
output_stream: virtual_mic,
|
output_stream: virtual_mic,
|
||||||
normal_output_stream: normal,
|
normal_output_stream: normal,
|
||||||
paused: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,6 @@ fn create_virtual_mic() -> SoundSystem {
|
|||||||
{
|
{
|
||||||
return SoundSystem {
|
return SoundSystem {
|
||||||
output_stream: linux_lib::create_virtual_mic_linux(),
|
output_stream: linux_lib::create_virtual_mic_linux(),
|
||||||
paused: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +90,6 @@ fn create_virtual_mic() -> SoundSystem {
|
|||||||
.expect("Unable to open device")
|
.expect("Unable to open device")
|
||||||
.open_stream()
|
.open_stream()
|
||||||
.expect("Failed to open stream"),
|
.expect("Failed to open stream"),
|
||||||
paused: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +141,7 @@ fn main() {
|
|||||||
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"),
|
||||||
|
current_view: "main".to_string(),
|
||||||
last_virt_output_update: Instant::now()
|
last_virt_output_update: Instant::now()
|
||||||
})
|
})
|
||||||
.add_systems(
|
.add_systems(
|
||||||
@@ -254,6 +253,7 @@ fn play_sound(file_path: String, app_state: &mut AppState) {
|
|||||||
file_path: file_path.clone(),
|
file_path: file_path.clone(),
|
||||||
length,
|
length,
|
||||||
sink,
|
sink,
|
||||||
|
to_remove: false,
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
normal_sink: {
|
normal_sink: {
|
||||||
let file2 = File::open(&file_path).unwrap();
|
let file2 = File::open(&file_path).unwrap();
|
||||||
@@ -269,13 +269,7 @@ fn play_sound(file_path: String, app_state: &mut AppState) {
|
|||||||
app_state.currently_playing.push(playing_sound);
|
app_state.currently_playing.push(playing_sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
fn main_ui(mut ctx: &Context, mut app_state: ResMut<AppState>) {
|
||||||
let ctx = contexts.ctx_mut()?;
|
|
||||||
|
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
|
||||||
ui.heading("csd4ni3l Soundboard");
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::SidePanel::right("tools").show(ctx, |ui| {
|
egui::SidePanel::right("tools").show(ctx, |ui| {
|
||||||
ui.heading("Tools");
|
ui.heading("Tools");
|
||||||
|
|
||||||
@@ -353,7 +347,7 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
|||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
println!("Youtube downloader!");
|
app_state.current_view = "youtube_downloader".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
@@ -369,51 +363,6 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::TopBottomPanel::bottom("currently_playing").show(ctx, |ui| {
|
|
||||||
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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.add_sized(
|
|
||||||
[available_width, available_height / 15.0],
|
|
||||||
egui::Button::new(if app_state.sound_system.paused {"Resume"} else {"Pause"}),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
app_state.sound_system.paused = !app_state.sound_system.paused;
|
|
||||||
|
|
||||||
if app_state.sound_system.paused {
|
|
||||||
for sound in &app_state.currently_playing {
|
|
||||||
sound.sink.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for sound in &app_state.currently_playing {
|
|
||||||
sound.sink.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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| {
|
||||||
@@ -468,10 +417,90 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
app_state.currently_playing.retain(|playing_sound| {
|
fn youtube_downloader_ui(mut ctx: &Context, mut app_state: ResMut<AppState>) {
|
||||||
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
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ui.heading("Coming Soon!");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,50 @@ use rodio::{
|
|||||||
cpal::{self, traits::DeviceTrait, traits::HostTrait},
|
cpal::{self, traits::DeviceTrait, traits::HostTrait},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use ringbuf::{traits::*, HeapRb};
|
||||||
|
|
||||||
|
fn route_standard_to_virtual(virtual_mic: cpal::Device) {
|
||||||
|
let standard_mic = host.default_output_device();
|
||||||
|
|
||||||
|
let config = StreamConfig {
|
||||||
|
channels: 2,
|
||||||
|
sample_rate: SampleRate(48_000),
|
||||||
|
buffer_size: cpal::BufferSize::Default,
|
||||||
|
};
|
||||||
|
let rb = HeapRb::<i32>::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.push(sample);
|
||||||
|
let _ = producer.push(sample);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |err| eprintln!("Input stream error: {err}"),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let output_stream = virtual_mic.build_output_stream(
|
||||||
|
&config,
|
||||||
|
move |data: &mut [f32], _| {
|
||||||
|
for sample in data {
|
||||||
|
*sample = consumer.pop().unwrap_or(0.0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |err| eprintln!("Output stream error: {err}"),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
input_stream.play()?;
|
||||||
|
output_stream.play()?;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_virtual_mic_windows() -> (OutputStream, OutputStream) {
|
pub fn create_virtual_mic_windows() -> (OutputStream, OutputStream) {
|
||||||
let host = cpal::host_from_id(cpal::HostId::Wasapi)
|
let host = cpal::host_from_id(cpal::HostId::Wasapi)
|
||||||
.expect("Could not initialize audio routing using WasAPI");
|
.expect("Could not initialize audio routing using WasAPI");
|
||||||
|
|
||||||
let virtual_mic = host
|
let virtual_mic = host
|
||||||
.output_devices()
|
.output_devices()
|
||||||
.expect("Could not list Output devices")
|
.expect("Could not list Output devices")
|
||||||
@@ -18,6 +59,8 @@ pub fn create_virtual_mic_windows() -> (OutputStream, OutputStream) {
|
|||||||
})
|
})
|
||||||
.expect("Could not get VB Cable output device. Is VB Cable Driver installed?");
|
.expect("Could not get VB Cable output device. Is VB Cable Driver installed?");
|
||||||
|
|
||||||
|
route_standard_to_virtual(virtual_mic);
|
||||||
|
|
||||||
let normal_output = host
|
let normal_output = host
|
||||||
.default_output_device()
|
.default_output_device()
|
||||||
.expect("Could not get default output device");
|
.expect("Could not get default output device");
|
||||||
|
|||||||
Reference in New Issue
Block a user