mirror of
https://github.com/csd4ni3l/soundboard.git
synced 2026-03-10 09:09: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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "rodio"
|
||||
version = "0.21.1"
|
||||
@@ -4224,6 +4235,7 @@ dependencies = [
|
||||
"bevy_egui",
|
||||
"rand",
|
||||
"rfd",
|
||||
"ringbuf",
|
||||
"rodio",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2024"
|
||||
bevy_egui = "0.38.1"
|
||||
rand = "0.9.2"
|
||||
rfd = "0.16.0"
|
||||
ringbuf = "0.4.8"
|
||||
rodio = { version = "0.21.1", features = ["mp3", "wav", "flac", "vorbis"] }
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.146"
|
||||
@@ -49,4 +50,4 @@ features = [
|
||||
"bevy_picking",
|
||||
"wayland",
|
||||
"x11"
|
||||
]
|
||||
]
|
||||
|
||||
11
README.md
11
README.md
@@ -1,18 +1,23 @@
|
||||
# csd4ni3l Soundboard
|
||||
|
||||
Soundboard made in Rust & Bevy. My first Rust project.
|
||||
|
||||
# Support & Requirements
|
||||
## Support & Requirements
|
||||
|
||||
## Linux
|
||||
|
||||
- Needs the `mold` linker and `clang` to compile fast
|
||||
- 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
|
||||
- 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
|
||||
|
||||
- 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 bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui};
|
||||
use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui::{self, Context}};
|
||||
|
||||
use egui::ecolor::Color32;
|
||||
|
||||
@@ -29,6 +29,7 @@ struct PlayingSound {
|
||||
file_path: String,
|
||||
length: f32,
|
||||
sink: Sink,
|
||||
to_remove: bool,
|
||||
#[cfg(target_os = "windows")]
|
||||
normal_sink: Sink,
|
||||
}
|
||||
@@ -37,7 +38,6 @@ struct SoundSystem {
|
||||
#[cfg(target_os = "windows")]
|
||||
normal_output_stream: OutputStream,
|
||||
output_stream: OutputStream,
|
||||
paused: bool,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
@@ -50,7 +50,8 @@ struct AppState {
|
||||
virt_outputs: Vec<(String, String)>,
|
||||
virt_output_index_switch: 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"];
|
||||
@@ -62,7 +63,6 @@ fn create_virtual_mic() -> SoundSystem {
|
||||
return SoundSystem {
|
||||
output_stream: virtual_mic,
|
||||
normal_output_stream: normal,
|
||||
paused: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ fn create_virtual_mic() -> SoundSystem {
|
||||
{
|
||||
return SoundSystem {
|
||||
output_stream: linux_lib::create_virtual_mic_linux(),
|
||||
paused: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,7 +90,6 @@ fn create_virtual_mic() -> SoundSystem {
|
||||
.expect("Unable to open device")
|
||||
.open_stream()
|
||||
.expect("Failed to open stream"),
|
||||
paused: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +141,7 @@ fn main() {
|
||||
virt_outputs: Vec::new(),
|
||||
virt_output_index_switch: String::from("0"),
|
||||
virt_output_index: String::from("999"),
|
||||
current_view: "main".to_string(),
|
||||
last_virt_output_update: Instant::now()
|
||||
})
|
||||
.add_systems(
|
||||
@@ -254,6 +253,7 @@ fn play_sound(file_path: String, app_state: &mut AppState) {
|
||||
file_path: file_path.clone(),
|
||||
length,
|
||||
sink,
|
||||
to_remove: false,
|
||||
#[cfg(target_os = "windows")]
|
||||
normal_sink: {
|
||||
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);
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
fn main_ui(mut ctx: &Context, mut app_state: ResMut<AppState>) {
|
||||
egui::SidePanel::right("tools").show(ctx, |ui| {
|
||||
ui.heading("Tools");
|
||||
|
||||
@@ -353,7 +347,7 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
println!("Youtube downloader!");
|
||||
app_state.current_view = "youtube_downloader".to_string();
|
||||
}
|
||||
|
||||
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| {
|
||||
let available_height = ui.available_height();
|
||||
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| {
|
||||
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
|
||||
fn youtube_downloader_ui(mut ctx: &Context, mut app_state: ResMut<AppState>) {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -3,9 +3,50 @@ use rodio::{
|
||||
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) {
|
||||
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")
|
||||
@@ -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?");
|
||||
|
||||
route_standard_to_virtual(virtual_mic);
|
||||
|
||||
let normal_output = host
|
||||
.default_output_device()
|
||||
.expect("Could not get default output device");
|
||||
|
||||
Reference in New Issue
Block a user