code: adding watch command, making rodio play available, switching to fomantic-ui and implementing fullscreen and aspect ratios

This commit is contained in:
Gabor Körber 2024-01-20 21:09:44 +01:00
parent 09ed01b687
commit d2e09ecebc
4 changed files with 171 additions and 57 deletions

View File

@ -9,3 +9,6 @@ bin args='':
hello: hello:
@echo "Hello, world!" @echo "Hello, world!"
watch:
cargo watch -c -q -w src -w templates -x run

View File

@ -9,7 +9,7 @@ use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use vbplay::{AudioThread, DeviceSelection}; use vbplay::{AudioThread, DeviceSelection, SoundDecoder};
mod soundclips; mod soundclips;
mod state; mod state;
@ -21,14 +21,18 @@ async fn main() {
jinja.set_loader(path_loader("templates")); jinja.set_loader(path_loader("templates"));
let template_engine = Engine::from(jinja); let template_engine = Engine::from(jinja);
let audio = vbplay::audio_thread(DeviceSelection::FindByPattern( let audio = vbplay::audio_thread(
".*VB-Audio Virtual Cable.*".to_owned(), DeviceSelection::FindByPattern(".*VB-Audio Virtual Cable.*".to_owned()),
)); SoundDecoder::Detect,
);
let audio: Arc<Mutex<AudioThread>> = Arc::new(Mutex::new(audio)); let audio: Arc<Mutex<AudioThread>> = Arc::new(Mutex::new(audio));
let app_state = AppState { let app_state = AppState {
engine: template_engine, engine: template_engine,
clips: soundclips::scan_directory_for_clips("E:/sounds/soundboard/all", &["mp3"]) clips: soundclips::scan_directory_for_clips(
"E:/sounds/soundboard/all",
&["mp3", "ogg", "wav", "flac"],
)
.expect("No Soundclips found."), .expect("No Soundclips found."),
player: audio, player: audio,
playback: None, playback: None,

View File

@ -4,11 +4,18 @@ use log::{debug, error, info};
use minimp3::{Decoder, Error}; use minimp3::{Decoder, Error};
use regex::Regex; use regex::Regex;
use std::fs::File; use std::fs::File;
use std::io::Cursor;
use std::io::Read; use std::io::Read;
use std::io::{BufReader, Cursor};
use std::sync::mpsc::{self, Sender}; use std::sync::mpsc::{self, Sender};
use std::thread; use std::thread;
#[derive(Clone)]
pub enum SoundDecoder {
Detect,
Mp3Mini,
Rodio,
}
pub enum Command { pub enum Command {
Play(String), Play(String),
Stop, Stop,
@ -57,7 +64,7 @@ fn select_device_by_id(index: usize) -> Option<Device> {
pub type AudioThread = Sender<Command>; pub type AudioThread = Sender<Command>;
pub fn audio_thread(select_device: DeviceSelection) -> AudioThread { pub fn audio_thread(select_device: DeviceSelection, select_decoder: SoundDecoder) -> AudioThread {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
thread::spawn(move || { thread::spawn(move || {
@ -76,7 +83,7 @@ pub fn audio_thread(select_device: DeviceSelection) -> AudioThread {
for command in rx { for command in rx {
match command { match command {
Command::Play(file_name) => { Command::Play(file_name) => {
play_file(&sink, file_name); play_file(&sink, file_name, &select_decoder);
} }
Command::Stop => { Command::Stop => {
sink.stop(); sink.stop();
@ -88,13 +95,24 @@ pub fn audio_thread(select_device: DeviceSelection) -> AudioThread {
tx tx
} }
fn play_file(sink: &rodio::Sink, file_name: String) { fn detect_decoder(file_name: &str, sound_decoder: &SoundDecoder) -> SoundDecoder {
let mut mp3_file = File::open(file_name).unwrap(); // TODO: File detection via ending or whitelisting?
let mut mp3_data = Vec::new(); // This function MUST NOT return SoundDecoder::Detect
mp3_file.read_to_end(&mut mp3_data).unwrap(); match sound_decoder {
SoundDecoder::Detect => SoundDecoder::Rodio,
other => other.clone(),
}
}
fn play_file(sink: &rodio::Sink, file_name: String, sound_decoder: &SoundDecoder) {
match detect_decoder(&file_name, sound_decoder) {
SoundDecoder::Mp3Mini => {
// MP3 Mini provides low latency playback
let mut sound_file = File::open(file_name).unwrap();
let mut file_data = Vec::new();
sound_file.read_to_end(&mut file_data).unwrap();
// Iterate over the MP3 frames and play them // Iterate over the MP3 frames and play them
let mut decoder = Decoder::new(Cursor::new(mp3_data)); let mut decoder = Decoder::new(Cursor::new(file_data));
loop { loop {
match decoder.next_frame() { match decoder.next_frame() {
Ok(frame) => { Ok(frame) => {
@ -115,4 +133,14 @@ fn play_file(sink: &rodio::Sink, file_name: String) {
} }
} }
} }
}
SoundDecoder::Rodio => {
// Rodio currently supports Ogg, Mp3, WAV and Flac
let file = File::open(&file_name).unwrap();
let source = rodio::Decoder::new(BufReader::new(file)).expect("Failed to open Decoder");
sink.append(source);
//sink.sleep_until_end();
}
SoundDecoder::Detect => error!("This should never happen"),
}
} }

View File

@ -5,15 +5,48 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- UIkit CSS --> <script src="https://unpkg.com/jquery@3.7.1/dist/jquery.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.17.11/dist/css/uikit.min.css" /> <link rel="stylesheet" type="text/css" href="https://unpkg.com/fomantic-ui@2.9.3/dist/semantic.min.css">
<script src="https://unpkg.com/fomantic-ui@2.9.3/dist/semantic.min.js"></script>
<!-- UIkit JS --> <title>Soundboard</title>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.17.11/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.17.11/dist/js/uikit-icons.min.js"></script>
<title>Play</title>
<script> <script>
function setLandscape() {
if (screen.orientation && screen.orientation.lock) {
screen.orientation.lock('landscape').then(() => {
console.log("Landscape mode activated");
}).catch((error) => {
console.error("Landscape mode failed:", error);
});
} else {
console.log("Screen Orientation API not supported");
}
}
function setPortrait() {
if (screen.orientation && screen.orientation.lock) {
screen.orientation.lock('portrait').then(() => {
console.log("Portrait mode activated");
}).catch((error) => {
console.error("Portrait mode failed:", error);
});
} else {
console.log("Screen Orientation API not supported");
}
}
function toggleFullScreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch((error) => {
console.error("Error attempting to enable full-screen mode:", error);
});
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
function play(hash) { function play(hash) {
fetch("/play/" + hash); fetch("/play/" + hash);
} }
@ -22,26 +55,72 @@
fetch("/stop"); fetch("/stop");
} }
</script> </script>
<style type="text/css">
:root {
--left-column-width: 98px;
/* Define a CSS variable for the width */
}
.left-column {
width: var(--left-column-width) !important;
height: 100%;
position: fixed;
/* Fix the position relative to the viewport */
top: 0;
/* Align the top edge with the top of the viewport */
bottom: 0;
/* Align the bottom edge with the bottom of the viewport */
overflow-y: auto;
/* Add scroll to the left column if content overflows */
background-color: rgb(27, 28, 29);
}
.right-column {
margin-left: var(--left-column-width) !important;
}
</style>
</head> </head>
<body> <body>
<div id="main" class="uk-container">
<div id="categories"> <div class="left-column column fixed">
<span class="category">Category 1</span> <div class="ui inverted labeled icon inline vertical compact mini menu" style="min-width: 96.75px;">
<!-- More categories --> <a class="item" onclick="stop()">
<button class="more">More</button> <i class="stop icon"></i>
Stop
</a>
<a class="item" onclick="toggleFullScreen()">
<i class="expand layout icon"></i>
Fullscreen
</a>
<a class="item" onclick="setLandscape()">
<i class="tv icon"></i>
Landscape
</a>
<a class="item" onclick="setPortrait()">
<i class="mobile alternate icon"></i>
Portrait
</a>
</div> </div>
<div class="uk-text-center" uk-grid> </div>
<div id="soundclips"> <div class="right-column column">
<div class="ui basic segment">
<div class="ui wrapped compact wrapping spaced /**buttons">
{% for clip in clips %} {% for clip in clips %}
<button class="uk-button uk-button-default" onclick="play('{{clip.hash}}')">{{clip.file_name}}</button> <button class="ui {{ loop.cycle('red', 'blue', 'green', 'violet', 'orange') }} basic button"
onclick="play('{{clip.hash}}')">{{clip.file_name}}</button>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="player">
<button onclick="stop()">Stop.</button>
</div> </div>
</div> </div>
</body> </body>
</html> </html>