initial.
This commit is contained in:
commit
12ea20f270
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1638
Cargo.lock
generated
Normal file
1638
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "soundboard"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
default-run = "soundboard"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = "0.6.18"
|
||||
axum-template = { version = "0.19.0", features = ["minijinja"] }
|
||||
cpal = "0.15.2"
|
||||
minijinja = { version = "1.0.3", features = ["loader"] }
|
||||
minimp3 = "0.5.1"
|
||||
regex = "1.9.0"
|
||||
rodio = "0.17.1"
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
xxhash-rust = { version = "0.8.6", features = ["xxh3", "const_xxh3"] }
|
BIN
sounds/example/liv_aaaaaaaa.mp3
Normal file
BIN
sounds/example/liv_aaaaaaaa.mp3
Normal file
Binary file not shown.
21
src/bin/script_files.rs
Normal file
21
src/bin/script_files.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use soundboard::soundclips::*;
|
||||
|
||||
fn main() {
|
||||
println!("Starting...");
|
||||
if let Some(sound_clips) = scan_directory_for_clips(
|
||||
"D:/work/workspace/rust_explore/soundboard/sounds/example",
|
||||
&["mp3", "wav"],
|
||||
) {
|
||||
println!("Sound clips found: {}", sound_clips.len());
|
||||
println!(
|
||||
"{}",
|
||||
sound_clips
|
||||
.iter()
|
||||
.map(|clip| format!("{}", clip))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
);
|
||||
} else {
|
||||
println!("No clips found.");
|
||||
}
|
||||
}
|
70
src/bin/test_play.rs
Normal file
70
src/bin/test_play.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use cpal::{Device, Host};
|
||||
use minimp3::{Decoder, Error};
|
||||
use regex::Regex;
|
||||
use std::fs::File;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
|
||||
fn list_devices(host: &Host) -> Vec<String> {
|
||||
let devices = host.output_devices().unwrap();
|
||||
devices.map(|device| device.name().unwrap()).collect()
|
||||
}
|
||||
|
||||
fn find_device_index_regex(devices: Vec<String>, pattern: &str) -> Option<usize> {
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
devices.iter().position(|device| re.is_match(device))
|
||||
}
|
||||
|
||||
fn select_device(host: &Host, i: usize) -> Option<Device> {
|
||||
host.output_devices().unwrap().nth(i)
|
||||
}
|
||||
|
||||
fn find_virtual_audio_cable() -> Option<Device> {
|
||||
let host = cpal::default_host();
|
||||
let devices = list_devices(&host);
|
||||
println!("Devices: {:?}", devices);
|
||||
|
||||
let pattern = ".*VB-Audio Virtual Cable.*";
|
||||
if let Some(index) = find_device_index_regex(devices, pattern) {
|
||||
println!("Selecting Device: \"{}\" at index {:?}", pattern, index);
|
||||
return select_device(&host, index);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn load_and_play_mp3(device: &Device, file_name: &str) {
|
||||
let mut mp3_file = File::open(file_name).unwrap();
|
||||
let mut mp3_data = Vec::new();
|
||||
mp3_file.read_to_end(&mut mp3_data).unwrap();
|
||||
|
||||
// Create a rodio Sink connected to our device
|
||||
let (_stream, stream_handle) = rodio::OutputStream::try_from_device(&device).unwrap();
|
||||
let sink = rodio::Sink::try_new(&stream_handle).unwrap();
|
||||
|
||||
// Iterate over the MP3 frames and play them
|
||||
let mut decoder = Decoder::new(Cursor::new(mp3_data));
|
||||
loop {
|
||||
match decoder.next_frame() {
|
||||
Ok(frame) => {
|
||||
let source = rodio::buffer::SamplesBuffer::new(
|
||||
2,
|
||||
frame.sample_rate.try_into().unwrap(),
|
||||
&*frame.data,
|
||||
);
|
||||
sink.append(source);
|
||||
}
|
||||
Err(Error::Eof) => break,
|
||||
Err(e) => panic!("{:?}", e),
|
||||
}
|
||||
}
|
||||
// Wait for the sound to finish
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let file_name = "sounds/example/liv_aaaaaaaa.mp3";
|
||||
let device = find_virtual_audio_cable().unwrap();
|
||||
|
||||
load_and_play_mp3(&device, file_name);
|
||||
}
|
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod soundclips;
|
||||
pub mod vbplay;
|
75
src/main.rs
Normal file
75
src/main.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use axum::{routing, Router};
|
||||
use axum_template::{engine::Engine, Key, RenderHtml};
|
||||
use minijinja::{path_loader, Environment};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
mod soundclips;
|
||||
mod vbplay;
|
||||
|
||||
type AppEngine = Engine<Environment<'static>>;
|
||||
|
||||
fn play_liv() {
|
||||
let file_name = "sounds/example/liv_aaaaaaaa.mp3";
|
||||
let tx = vbplay::audio_thread();
|
||||
|
||||
tx.send(vbplay::Command::Play(file_name.into())).unwrap();
|
||||
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
|
||||
tx.send(vbplay::Command::Stop).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut jinja = Environment::new();
|
||||
jinja.set_loader(path_loader("templates"));
|
||||
|
||||
// Set the address to run our application on
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
|
||||
// Build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/", routing::get(home))
|
||||
.route("/play/:x", routing::get(play_handler))
|
||||
.with_state(Engine::from(jinja));
|
||||
|
||||
println!("listening on {}", addr);
|
||||
|
||||
// Run it with hyper
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn home(engine: AppEngine) -> impl axum::response::IntoResponse {
|
||||
RenderHtml(Key("index.html".to_owned()), engine, ())
|
||||
}
|
||||
|
||||
async fn root_handler() -> axum::response::Html<&'static str> {
|
||||
let html = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Play</title>
|
||||
<script>
|
||||
function play() {
|
||||
fetch("/play/1");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="play()">Play</button>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
html.into()
|
||||
}
|
||||
|
||||
async fn play_handler(axum::extract::Path(x): axum::extract::Path<u32>) -> String {
|
||||
play_liv();
|
||||
format!("You sent: {}", x)
|
||||
}
|
79
src/soundclips.rs
Normal file
79
src/soundclips.rs
Normal file
@ -0,0 +1,79 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SoundClip {
|
||||
hash: String,
|
||||
file_name: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for SoundClip {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"SoundClip (hash: {}, file_name: {})",
|
||||
self.hash, self.file_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SoundClip {
|
||||
pub fn new(file_name: String) -> Self {
|
||||
Self {
|
||||
hash: encode_filename(&file_name),
|
||||
file_name: file_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> &String {
|
||||
&self.hash
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> &String {
|
||||
&self.file_name
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_filename(file_name: &str) -> String {
|
||||
let hash = xxhash_rust::xxh3::xxh3_64(file_name.as_bytes());
|
||||
let hash_hex = format!("{:x}", hash);
|
||||
hash_hex
|
||||
}
|
||||
|
||||
pub fn scan_directory_for_clips(directory: &str, extensions: &[&str]) -> Option<Vec<SoundClip>> {
|
||||
let mut sound_clips = Vec::new();
|
||||
|
||||
if let Ok(entries) = fs::read_dir(directory) {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if let Some(ext) = path.extension() {
|
||||
if extensions.iter().any(|&e| ext == OsStr::new(e)) {
|
||||
let file_name = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||
/*
|
||||
the calls to unwrap() are more or less "safe" because:
|
||||
|
||||
file_name() only returns None if the path ends in "..", which won't be the case for paths returned by read_dir.
|
||||
to_str() only returns None if the file name isn't valid Unicode, which is quite rare on most modern file systems.
|
||||
*/
|
||||
let sound_clip = SoundClip::new(file_name);
|
||||
sound_clips.push(sound_clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Failed to read directory entry: {}", entry.unwrap_err());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"Failed to read directory: {}",
|
||||
fs::read_dir(directory).unwrap_err()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(sound_clips)
|
||||
}
|
91
src/vbplay.rs
Normal file
91
src/vbplay.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use cpal::{Device, Host};
|
||||
use minimp3::{Decoder, Error};
|
||||
use regex::Regex;
|
||||
use std::fs::File;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
use std::thread;
|
||||
|
||||
pub enum Command {
|
||||
Play(String),
|
||||
Stop,
|
||||
}
|
||||
|
||||
fn list_devices(host: &Host) -> Vec<String> {
|
||||
let devices = host.output_devices().unwrap();
|
||||
devices.map(|device| device.name().unwrap()).collect()
|
||||
}
|
||||
|
||||
fn find_device_index_regex(devices: Vec<String>, pattern: &str) -> Option<usize> {
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
devices.iter().position(|device| re.is_match(device))
|
||||
}
|
||||
|
||||
fn select_device(host: &Host, i: usize) -> Option<Device> {
|
||||
host.output_devices().unwrap().nth(i)
|
||||
}
|
||||
|
||||
fn find_virtual_audio_cable() -> Option<Device> {
|
||||
let host = cpal::default_host();
|
||||
let devices = list_devices(&host);
|
||||
println!("Devices: {:?}", devices);
|
||||
|
||||
let pattern = ".*VB-Audio Virtual Cable.*";
|
||||
if let Some(index) = find_device_index_regex(devices, pattern) {
|
||||
println!("Selecting Device: \"{}\" at index {:?}", pattern, index);
|
||||
return select_device(&host, index);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn audio_thread() -> Sender<Command> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
thread::spawn(move || {
|
||||
let device = find_virtual_audio_cable().unwrap();
|
||||
// Create a rodio Sink connected to our device
|
||||
let (_stream, stream_handle) = rodio::OutputStream::try_from_device(&device).unwrap();
|
||||
let sink = rodio::Sink::try_new(&stream_handle).unwrap();
|
||||
|
||||
for command in rx {
|
||||
match command {
|
||||
Command::Play(file_name) => {
|
||||
play_file(&sink, file_name);
|
||||
}
|
||||
Command::Stop => {
|
||||
sink.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tx
|
||||
}
|
||||
|
||||
fn play_file(sink: &rodio::Sink, file_name: String) {
|
||||
let mut mp3_file = File::open(file_name).unwrap();
|
||||
let mut mp3_data = Vec::new();
|
||||
mp3_file.read_to_end(&mut mp3_data).unwrap();
|
||||
|
||||
// Iterate over the MP3 frames and play them
|
||||
let mut decoder = Decoder::new(Cursor::new(mp3_data));
|
||||
loop {
|
||||
match decoder.next_frame() {
|
||||
Ok(frame) => {
|
||||
let source = rodio::buffer::SamplesBuffer::new(
|
||||
2,
|
||||
frame.sample_rate.try_into().unwrap(),
|
||||
&*frame.data,
|
||||
);
|
||||
sink.append(source);
|
||||
}
|
||||
Err(Error::Eof) => {
|
||||
println!("EOF");
|
||||
break;
|
||||
}
|
||||
Err(e) => panic!("{:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
14
templates/index.html
Normal file
14
templates/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Play</title>
|
||||
<script>
|
||||
function play() {
|
||||
fetch("/play/1");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="play()">Play</button>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user