From 6fcd4a4154b34e071a94fd7a0f25bdbe861b9d58 Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Fri, 23 Jan 2026 14:20:35 +0100 Subject: [PATCH] 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 --- README.md | 2 +- run_linux.sh | 6 +-- soundboard.desktop | 7 +++ src/main.rs | 117 +++++++++++++++++++++++++++++++++++++-------- 4 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 soundboard.desktop diff --git a/README.md b/README.md index 612aef8..77290bc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ 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` \ No newline at end of file diff --git a/run_linux.sh b/run_linux.sh index 0b2daf7..9dd8e53 100644 --- a/run_linux.sh +++ b/run_linux.sh @@ -1,8 +1,4 @@ -#!/bin/bash -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 +cargo run 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 \ No newline at end of file diff --git a/soundboard.desktop b/soundboard.desktop new file mode 100644 index 0000000..aa7ad4d --- /dev/null +++ b/soundboard.desktop @@ -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; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5b446c1..b4224db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use bevy::{ 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}; @@ -11,7 +11,7 @@ use bevy_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)] struct JSONData { @@ -20,11 +20,12 @@ struct JSONData { struct PlayingSound { file_path: String, - start_time: f32, + length: f32, + sink: Sink } struct SoundSystem { - sink: Sink, + stream_handle: OutputStream, paused: bool } @@ -37,19 +38,66 @@ struct AppState { sound_system: SoundSystem } -use std::time::{SystemTime, UNIX_EPOCH}; - 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 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"); - let mixer = stream_handle.mixer(); - let sink = Sink::connect_new(&mixer); +fn recreate_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 { + 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() .insert_resource(ClearColor(Color::BLACK)) @@ -76,7 +124,7 @@ fn main() { current_directory: String::new(), currently_playing: Vec::new(), sound_system: SoundSystem { - sink, + stream_handle, paused: false } }) @@ -146,16 +194,14 @@ fn update_ui_scale_factor_system( fn play_sound(file_path: String, app_state: &mut AppState) { let file = BufReader::new(File::open(&file_path).unwrap()); let src = Decoder::new(file).unwrap(); - app_state.sound_system.sink.append(src); - - let start = SystemTime::now(); - let since_the_epoch = start - .duration_since(UNIX_EPOCH) - .expect("time should go forward"); + 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()); + sink.append(src); app_state.currently_playing.push(PlayingSound { 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) -> Res { 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| { @@ -267,6 +342,10 @@ fn ui_system(mut contexts: EguiContexts, mut app_state: ResMut) -> 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(()) } \ No newline at end of file