code: adding watch command, making rodio play available, switching to fomantic-ui and implementing fullscreen and aspect ratios
This commit is contained in:
parent
09ed01b687
commit
d2e09ecebc
3
Justfile
3
Justfile
@ -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
|
||||||
|
14
src/main.rs
14
src/main.rs
@ -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,
|
||||||
|
@ -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"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user