generated from GarandPLG/rust-flake-template
Add selectable soundtracks via CLI
Introduce a `Soundtrack` enum with Clap `ValueEnum` support and expose it in the CLI as a `--sound-track` option. Extend `AudioCmd` with a `ChangeSoundtrack` variant and refactor audio handling to load and cache soundtracks per‑track using a thread‑safe `RwLock`. Update the `handle_audio` function signature to accept the selected soundtrack, adjust re‑exports, and modify `main` to pass the chosen soundtrack to the audio thread. Also reposition the logging flag in the CLI definition.
This commit is contained in:
+108
-26
@@ -1,3 +1,4 @@
|
|||||||
|
use clap::ValueEnum;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rodio::{
|
use rodio::{
|
||||||
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize, source::Amplify,
|
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize, source::Amplify,
|
||||||
@@ -6,7 +7,8 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::read,
|
fs::read,
|
||||||
io::{BufReader, Cursor},
|
io::{BufReader, Cursor},
|
||||||
sync::mpsc::Receiver,
|
process::exit,
|
||||||
|
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -15,9 +17,15 @@ pub enum AudioCmd {
|
|||||||
VolumeUp,
|
VolumeUp,
|
||||||
VolumeDown,
|
VolumeDown,
|
||||||
ChangePart(SoundrackParts),
|
ChangePart(SoundrackParts),
|
||||||
|
ChangeSoundtrack(Soundtrack),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash)]
|
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, ValueEnum)]
|
||||||
|
pub enum Soundtrack {
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
|
||||||
pub enum SoundrackParts {
|
pub enum SoundrackParts {
|
||||||
Calm,
|
Calm,
|
||||||
Buildup,
|
Buildup,
|
||||||
@@ -25,35 +33,103 @@ pub enum SoundrackParts {
|
|||||||
Outro,
|
Outro,
|
||||||
}
|
}
|
||||||
|
|
||||||
static PRELOADED_DEFAULT_SOUNDTRACK: Lazy<HashMap<SoundrackParts, Vec<u8>>> = Lazy::new(|| {
|
type SoundtrackCache = HashMap<SoundrackParts, Vec<u8>>;
|
||||||
let mut map: HashMap<SoundrackParts, Vec<u8>> = HashMap::new();
|
|
||||||
|
static PRELOADED_SOUNDTRACKS: Lazy<RwLock<HashMap<Soundtrack, Arc<SoundtrackCache>>>> =
|
||||||
|
Lazy::new(|| {
|
||||||
|
let mut map: HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>> = HashMap::new();
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
Soundtrack::Default,
|
||||||
|
Arc::new(load_soundtrack_folder("default")),
|
||||||
|
);
|
||||||
|
|
||||||
|
RwLock::new(map)
|
||||||
|
});
|
||||||
|
|
||||||
|
fn load_soundtrack_folder(folder: &str) -> SoundtrackCache {
|
||||||
|
let base: String = format!("soundtrack/{}", folder);
|
||||||
|
let mut cache: HashMap<SoundrackParts, Vec<u8>> = HashMap::new();
|
||||||
|
|
||||||
macro_rules! load {
|
macro_rules! load {
|
||||||
($part:expr, $path:expr) => {
|
($part:ident, $file:expr) => {
|
||||||
map.insert($part, read($path).expect(concat!("cannot read ", $path)));
|
let path = format!("{}/{}", base, $file);
|
||||||
|
cache.insert(
|
||||||
|
SoundrackParts::$part,
|
||||||
|
read(&path).expect(&format!("cannot read {}", path)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
load!(SoundrackParts::Calm, "soundtrack/default/test.ogg"); // calm.ogg
|
load!(Calm, "test.ogg"); // calm.ogg
|
||||||
load!(SoundrackParts::Buildup, "soundtrack/default/test.ogg"); // buildup.ogg
|
load!(Buildup, "test.ogg"); // buildup.ogg
|
||||||
load!(SoundrackParts::Assault, "soundtrack/default/test.ogg"); // assault.ogg
|
load!(Assault, "test.ogg"); // assault.ogg
|
||||||
load!(SoundrackParts::Outro, "soundtrack/default/test.ogg"); // outro.ogg
|
load!(Outro, "test.ogg"); // outro.ogg
|
||||||
|
|
||||||
map
|
cache
|
||||||
});
|
|
||||||
|
|
||||||
fn load_audio(part: &SoundrackParts) -> Amplify<Decoder<BufReader<Cursor<Vec<u8>>>>> {
|
|
||||||
let data: &Vec<u8> = PRELOADED_DEFAULT_SOUNDTRACK
|
|
||||||
.get(part)
|
|
||||||
.expect("preloaded ogg not found");
|
|
||||||
|
|
||||||
let cursor: Cursor<Vec<u8>> = Cursor::new(data.clone());
|
|
||||||
let reader: BufReader<Cursor<Vec<u8>>> = BufReader::new(cursor);
|
|
||||||
|
|
||||||
Decoder::new(reader).expect("decoder issue").amplify(0.20)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_audio(rx: Receiver<AudioCmd>, mute: bool) {
|
fn load_audio(
|
||||||
|
soundtrack: &Soundtrack,
|
||||||
|
part: &SoundrackParts,
|
||||||
|
) -> Amplify<Decoder<BufReader<Cursor<Vec<u8>>>>> {
|
||||||
|
{
|
||||||
|
let guard: RwLockReadGuard<'_, HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>> =
|
||||||
|
PRELOADED_SOUNDTRACKS
|
||||||
|
.read()
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("RwLock poisoned: {}", e);
|
||||||
|
exit(1);
|
||||||
|
})
|
||||||
|
.expect("lock issue");
|
||||||
|
|
||||||
|
if let Some(soundtrack_cache) = guard.get(soundtrack) {
|
||||||
|
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||||
|
|
||||||
|
return Decoder::new(BufReader::new(Cursor::new(data.clone())))
|
||||||
|
.expect("decoder issue")
|
||||||
|
.amplify(0.20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut guard: RwLockWriteGuard<
|
||||||
|
'_,
|
||||||
|
HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>,
|
||||||
|
> = PRELOADED_SOUNDTRACKS
|
||||||
|
.write()
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("RwLock poisoned: {}", e);
|
||||||
|
exit(1);
|
||||||
|
})
|
||||||
|
.expect("lock issue");
|
||||||
|
|
||||||
|
if let Some(soundtrack_cache) = guard.get(soundtrack) {
|
||||||
|
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||||
|
|
||||||
|
return Decoder::new(BufReader::new(Cursor::new(data.clone())))
|
||||||
|
.expect("decoder issue")
|
||||||
|
.amplify(0.20);
|
||||||
|
}
|
||||||
|
|
||||||
|
let folder_name: &str = match soundtrack {
|
||||||
|
Soundtrack::Default => "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_cache: Arc<HashMap<SoundrackParts, Vec<u8>>> =
|
||||||
|
Arc::new(load_soundtrack_folder(folder_name));
|
||||||
|
|
||||||
|
let data: &Vec<u8> = new_cache.get(&part).expect("part missing after load");
|
||||||
|
|
||||||
|
guard.insert(*soundtrack, Arc::clone(&new_cache));
|
||||||
|
|
||||||
|
Decoder::new(BufReader::new(Cursor::new(data.clone())))
|
||||||
|
.expect("decoder issue")
|
||||||
|
.amplify(0.20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_audio(rx: Receiver<AudioCmd>, mute: bool, soundtrack: Soundtrack) {
|
||||||
let mut handle: MixerDeviceSink = DeviceSinkBuilder::from_default_device()
|
let mut handle: MixerDeviceSink = DeviceSinkBuilder::from_default_device()
|
||||||
.expect("get default device")
|
.expect("get default device")
|
||||||
.with_buffer_size(BufferSize::Fixed(4096))
|
.with_buffer_size(BufferSize::Fixed(4096))
|
||||||
@@ -66,8 +142,9 @@ pub fn handle_audio(rx: Receiver<AudioCmd>, mute: bool) {
|
|||||||
let mut volume: f32 = player.volume();
|
let mut volume: f32 = player.volume();
|
||||||
player.set_volume(if mute { 0.0 } else { volume });
|
player.set_volume(if mute { 0.0 } else { volume });
|
||||||
|
|
||||||
|
let mut current_soundtrack: Soundtrack = soundtrack;
|
||||||
let mut current_part: SoundrackParts = SoundrackParts::Calm;
|
let mut current_part: SoundrackParts = SoundrackParts::Calm;
|
||||||
player.append(load_audio(¤t_part));
|
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
for cmd in rx.try_iter() {
|
for cmd in rx.try_iter() {
|
||||||
@@ -85,14 +162,19 @@ pub fn handle_audio(rx: Receiver<AudioCmd>, mute: bool) {
|
|||||||
}
|
}
|
||||||
AudioCmd::ChangePart(part) => {
|
AudioCmd::ChangePart(part) => {
|
||||||
current_part = part;
|
current_part = part;
|
||||||
player.append(load_audio(¤t_part));
|
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||||
|
player.skip_one();
|
||||||
|
}
|
||||||
|
AudioCmd::ChangeSoundtrack(st) => {
|
||||||
|
current_soundtrack = st;
|
||||||
|
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||||
player.skip_one();
|
player.skip_one();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if player.empty() {
|
if player.empty() {
|
||||||
player.append(load_audio(¤t_part));
|
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ pub mod audio;
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod handle_events;
|
pub mod handle_events;
|
||||||
|
|
||||||
pub use audio::{AudioCmd, SoundrackParts, handle_audio};
|
pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio};
|
||||||
pub use events::AppEvent;
|
pub use events::AppEvent;
|
||||||
pub use handle_events::handle_events;
|
pub use handle_events::handle_events;
|
||||||
|
|||||||
+17
-6
@@ -1,5 +1,6 @@
|
|||||||
use crate::app::{
|
use crate::app::{
|
||||||
states::{GameMode, PerkDecks, ZoomLevel},
|
states::{GameMode, PerkDecks, ZoomLevel},
|
||||||
|
threads::Soundtrack,
|
||||||
view::View,
|
view::View,
|
||||||
};
|
};
|
||||||
use clap::{Error, Parser, error::ErrorKind, value_parser};
|
use clap::{Error, Parser, error::ErrorKind, value_parser};
|
||||||
@@ -133,13 +134,15 @@ pub struct Cli {
|
|||||||
)]
|
)]
|
||||||
pub skill_points_limit: u16,
|
pub skill_points_limit: u16,
|
||||||
|
|
||||||
/// Enable logging to a file (default: disabled).
|
/// Which soundtrack to play (default: default)
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
help = "Enable logging to file (default: disabled)",
|
help = "Soundtrack",
|
||||||
default_value_t = false
|
value_name = "...",
|
||||||
)]
|
default_value_t = Soundtrack::Default,
|
||||||
pub log: bool,
|
value_enum
|
||||||
|
)]
|
||||||
|
pub sound_track: Soundtrack,
|
||||||
|
|
||||||
/// Mute soundtrack (default: disabled).
|
/// Mute soundtrack (default: disabled).
|
||||||
#[arg(
|
#[arg(
|
||||||
@@ -148,6 +151,14 @@ pub struct Cli {
|
|||||||
default_value_t = false
|
default_value_t = false
|
||||||
)]
|
)]
|
||||||
pub mute: bool,
|
pub mute: bool,
|
||||||
|
|
||||||
|
/// Enable logging to a file (default: disabled).
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Enable logging to file (default: disabled)",
|
||||||
|
default_value_t = false
|
||||||
|
)]
|
||||||
|
pub log: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the command line arguments and returns a fully populated `Cli`
|
/// Parses the command line arguments and returns a fully populated `Cli`
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ fn main() -> Result<()> {
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
handle_audio(audio_rx, args.mute);
|
handle_audio(audio_rx, args.mute, args.sound_track);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
|
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
|
||||||
|
|||||||
Reference in New Issue
Block a user