mirror of
https://github.com/csd4ni3l/soundboard.git
synced 2026-03-10 09:09:24 +01:00
Update README to include clang dependency, move virtual mic creation to the app, add length tracking to the app and remove sounds that already ended, move from Systemtime to Instant and virtual mic recreation inside the app if anything bugs out
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
Soundboard made in Rust & Bevy. My first Rust project.
|
Soundboard made in Rust & Bevy. My first Rust project.
|
||||||
|
|
||||||
For compilation on Linux, you will need the mold linker to speed things up.
|
For compilation on Linux, you will need the mold linker and clang to speed things up.
|
||||||
On an arch machine for example, do `sudo pacman -S mold`
|
On an arch machine for example, do `sudo pacman -S mold`
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
#!/bin/bash
|
cargo run
|
||||||
pactl load-module module-null-sink sink_name=VirtualMic sink_properties=device.description="Virtual_Microphone"
|
|
||||||
pactl load-module module-remap-source master=VirtualMic.monitor source_name=VirtualMicSource source_properties=device.description="Virtual_Mic_Source"
|
|
||||||
|
|
||||||
PULSE_SINK=VirtualMic cargo run
|
|
||||||
|
|
||||||
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
|
||||||
7
soundboard.desktop
Normal file
7
soundboard.desktop
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=csd4ni3l Soundboard
|
||||||
|
Comment=A simple soundboard made in Rust
|
||||||
|
Exec=/opt/cssoundboard/run_linux.sh
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Application;
|
||||||
117
src/main.rs
117
src/main.rs
@@ -3,7 +3,7 @@ use bevy::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{collections::HashMap, fs::File, io::BufReader, path::Path};
|
use std::{collections::HashMap, fs::File, io::BufReader, path::Path, process::Command};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ use bevy_egui::{
|
|||||||
EguiContextSettings, EguiContexts, EguiPlugin, EguiPrimaryContextPass, EguiStartupSet, egui,
|
EguiContextSettings, EguiContexts, EguiPlugin, EguiPrimaryContextPass, EguiStartupSet, egui,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, mixer::Mixer, cpal, cpal::traits::{HostTrait, DeviceTrait}};
|
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source, cpal::{self, traits::HostTrait}};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct JSONData {
|
struct JSONData {
|
||||||
@@ -20,11 +20,12 @@ struct JSONData {
|
|||||||
|
|
||||||
struct PlayingSound {
|
struct PlayingSound {
|
||||||
file_path: String,
|
file_path: String,
|
||||||
start_time: f32,
|
length: f32,
|
||||||
|
sink: Sink
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SoundSystem {
|
struct SoundSystem {
|
||||||
sink: Sink,
|
stream_handle: OutputStream,
|
||||||
paused: bool
|
paused: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,19 +38,66 @@ struct AppState {
|
|||||||
sound_system: SoundSystem
|
sound_system: SoundSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"];
|
const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"];
|
||||||
|
|
||||||
fn main() {
|
fn create_virtual_mic() -> OutputStream {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
panic!("Windows is currently unsupported.");
|
||||||
|
}
|
||||||
|
else if cfg!(target_os = "darwin") {
|
||||||
|
panic!("MacOS is and will most likely stay unsupported.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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");
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("PULSE_SINK", "VirtualMic");
|
||||||
|
}
|
||||||
|
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
let virtual_mic = host.default_output_device().expect("Could not get default output device");
|
let virtual_mic = host.default_output_device().expect("Could not get default output device");
|
||||||
|
|
||||||
println!("Using device: {}", virtual_mic.name().unwrap_or_default());
|
return OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream");
|
||||||
|
}
|
||||||
|
|
||||||
let stream_handle = OutputStreamBuilder::from_device(virtual_mic).expect("Unable to open default audio device").open_stream().expect("Failed to open stream");
|
fn recreate_virtual_mic() -> OutputStream {
|
||||||
let mixer = stream_handle.mixer();
|
if cfg!(target_os = "windows") {
|
||||||
let sink = Sink::connect_new(&mixer);
|
panic!("Windows is currently unsupported.");
|
||||||
|
}
|
||||||
|
else if cfg!(target_os = "darwin") {
|
||||||
|
panic!("MacOS is and will most likely stay unsupported.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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
|
||||||
|
"#;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return create_virtual_mic();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let stream_handle = create_virtual_mic();
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.insert_resource(ClearColor(Color::BLACK))
|
.insert_resource(ClearColor(Color::BLACK))
|
||||||
@@ -76,7 +124,7 @@ fn main() {
|
|||||||
current_directory: String::new(),
|
current_directory: String::new(),
|
||||||
currently_playing: Vec::new(),
|
currently_playing: Vec::new(),
|
||||||
sound_system: SoundSystem {
|
sound_system: SoundSystem {
|
||||||
sink,
|
stream_handle,
|
||||||
paused: false
|
paused: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -146,16 +194,14 @@ fn update_ui_scale_factor_system(
|
|||||||
fn play_sound(file_path: String, app_state: &mut AppState) {
|
fn play_sound(file_path: String, app_state: &mut AppState) {
|
||||||
let file = BufReader::new(File::open(&file_path).unwrap());
|
let file = BufReader::new(File::open(&file_path).unwrap());
|
||||||
let src = Decoder::new(file).unwrap();
|
let src = Decoder::new(file).unwrap();
|
||||||
app_state.sound_system.sink.append(src);
|
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());
|
||||||
let start = SystemTime::now();
|
sink.append(src);
|
||||||
let since_the_epoch = start
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.expect("time should go forward");
|
|
||||||
|
|
||||||
app_state.currently_playing.push(PlayingSound {
|
app_state.currently_playing.push(PlayingSound {
|
||||||
file_path: file_path.clone(),
|
file_path: file_path.clone(),
|
||||||
start_time: since_the_epoch.as_secs_f32(),
|
length: length,
|
||||||
|
sink: sink
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +263,35 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Res
|
|||||||
{
|
{
|
||||||
println!("Youtube downloader!");
|
println!("Youtube downloader!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.add_sized(
|
||||||
|
[ui.available_width(), available_height / 15.0],
|
||||||
|
egui::Button::new("Recreate Virtual Mic"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
app_state.currently_playing.clear();
|
||||||
|
app_state.sound_system.stream_handle = recreate_virtual_mic();
|
||||||
|
println!("Recreated Virtual microphone!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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.sink.get_pos().as_secs_f32(), playing_sound.length));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
@@ -267,6 +342,10 @@ 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
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user