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:
|
||||
@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 std::sync::{Arc, Mutex};
|
||||
|
||||
use vbplay::{AudioThread, DeviceSelection};
|
||||
use vbplay::{AudioThread, DeviceSelection, SoundDecoder};
|
||||
|
||||
mod soundclips;
|
||||
mod state;
|
||||
@ -21,14 +21,18 @@ async fn main() {
|
||||
jinja.set_loader(path_loader("templates"));
|
||||
let template_engine = Engine::from(jinja);
|
||||
|
||||
let audio = vbplay::audio_thread(DeviceSelection::FindByPattern(
|
||||
".*VB-Audio Virtual Cable.*".to_owned(),
|
||||
));
|
||||
let audio = vbplay::audio_thread(
|
||||
DeviceSelection::FindByPattern(".*VB-Audio Virtual Cable.*".to_owned()),
|
||||
SoundDecoder::Detect,
|
||||
);
|
||||
let audio: Arc<Mutex<AudioThread>> = Arc::new(Mutex::new(audio));
|
||||
|
||||
let app_state = AppState {
|
||||
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."),
|
||||
player: audio,
|
||||
playback: None,
|
||||
|
@ -4,11 +4,18 @@ use log::{debug, error, info};
|
||||
use minimp3::{Decoder, Error};
|
||||
use regex::Regex;
|
||||
use std::fs::File;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
use std::io::{BufReader, Cursor};
|
||||
use std::sync::mpsc::{self, Sender};
|
||||
use std::thread;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SoundDecoder {
|
||||
Detect,
|
||||
Mp3Mini,
|
||||
Rodio,
|
||||
}
|
||||
|
||||
pub enum Command {
|
||||
Play(String),
|
||||
Stop,
|
||||
@ -57,7 +64,7 @@ fn select_device_by_id(index: usize) -> Option<Device> {
|
||||
|
||||
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();
|
||||
|
||||
thread::spawn(move || {
|
||||
@ -76,7 +83,7 @@ pub fn audio_thread(select_device: DeviceSelection) -> AudioThread {
|
||||
for command in rx {
|
||||
match command {
|
||||
Command::Play(file_name) => {
|
||||
play_file(&sink, file_name);
|
||||
play_file(&sink, file_name, &select_decoder);
|
||||
}
|
||||
Command::Stop => {
|
||||
sink.stop();
|
||||
@ -88,13 +95,24 @@ pub fn audio_thread(select_device: DeviceSelection) -> AudioThread {
|
||||
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();
|
||||
fn detect_decoder(file_name: &str, sound_decoder: &SoundDecoder) -> SoundDecoder {
|
||||
// TODO: File detection via ending or whitelisting?
|
||||
// This function MUST NOT return SoundDecoder::Detect
|
||||
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
|
||||
let mut decoder = Decoder::new(Cursor::new(mp3_data));
|
||||
let mut decoder = Decoder::new(Cursor::new(file_data));
|
||||
loop {
|
||||
match decoder.next_frame() {
|
||||
Ok(frame) => {
|
||||
@ -116,3 +134,13 @@ 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 name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- UIkit CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.17.11/dist/css/uikit.min.css" />
|
||||
<script src="https://unpkg.com/jquery@3.7.1/dist/jquery.js"></script>
|
||||
<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 -->
|
||||
<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>
|
||||
<title>Soundboard</title>
|
||||
<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) {
|
||||
fetch("/play/" + hash);
|
||||
}
|
||||
@ -22,26 +55,72 @@
|
||||
fetch("/stop");
|
||||
}
|
||||
</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>
|
||||
|
||||
<body>
|
||||
<div id="main" class="uk-container">
|
||||
<div id="categories">
|
||||
<span class="category">Category 1</span>
|
||||
<!-- More categories -->
|
||||
<button class="more">More</button>
|
||||
|
||||
<div class="left-column column fixed">
|
||||
<div class="ui inverted labeled icon inline vertical compact mini menu" style="min-width: 96.75px;">
|
||||
<a class="item" onclick="stop()">
|
||||
<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 class="uk-text-center" uk-grid>
|
||||
<div id="soundclips">
|
||||
</div>
|
||||
<div class="right-column column">
|
||||
|
||||
<div class="ui basic segment">
|
||||
|
||||
<div class="ui wrapped compact wrapping spaced /**buttons">
|
||||
{% 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 %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="player">
|
||||
<button onclick="stop()">Stop.</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user