From 633494ed4627df8e0ff5799f3ec47133054ad21d Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Thu, 9 Apr 2026 02:00:24 +0200 Subject: [PATCH] Add selectable soundtracks via CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/app/threads/audio.rs | 134 +++++++++++++++++++++++++++++++-------- src/app/threads/mod.rs | 2 +- src/cli.rs | 23 +++++-- src/main.rs | 2 +- 4 files changed, 127 insertions(+), 34 deletions(-) diff --git a/src/app/threads/audio.rs b/src/app/threads/audio.rs index c9f9f57..e95b0b1 100644 --- a/src/app/threads/audio.rs +++ b/src/app/threads/audio.rs @@ -1,3 +1,4 @@ +use clap::ValueEnum; use once_cell::sync::Lazy; use rodio::{ Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize, source::Amplify, @@ -6,7 +7,8 @@ use std::{ collections::HashMap, fs::read, io::{BufReader, Cursor}, - sync::mpsc::Receiver, + process::exit, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver}, }; #[derive(Debug)] @@ -15,9 +17,15 @@ pub enum AudioCmd { VolumeUp, VolumeDown, 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 { Calm, Buildup, @@ -25,35 +33,103 @@ pub enum SoundrackParts { Outro, } -static PRELOADED_DEFAULT_SOUNDTRACK: Lazy>> = Lazy::new(|| { - let mut map: HashMap> = HashMap::new(); +type SoundtrackCache = HashMap>; + +static PRELOADED_SOUNDTRACKS: Lazy>>> = + Lazy::new(|| { + let mut map: HashMap>>> = 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> = HashMap::new(); macro_rules! load { - ($part:expr, $path:expr) => { - map.insert($part, read($path).expect(concat!("cannot read ", $path))); + ($part:ident, $file:expr) => { + 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!(SoundrackParts::Buildup, "soundtrack/default/test.ogg"); // buildup.ogg - load!(SoundrackParts::Assault, "soundtrack/default/test.ogg"); // assault.ogg - load!(SoundrackParts::Outro, "soundtrack/default/test.ogg"); // outro.ogg + load!(Calm, "test.ogg"); // calm.ogg + load!(Buildup, "test.ogg"); // buildup.ogg + load!(Assault, "test.ogg"); // assault.ogg + load!(Outro, "test.ogg"); // outro.ogg - map -}); - -fn load_audio(part: &SoundrackParts) -> Amplify>>>> { - let data: &Vec = PRELOADED_DEFAULT_SOUNDTRACK - .get(part) - .expect("preloaded ogg not found"); - - let cursor: Cursor> = Cursor::new(data.clone()); - let reader: BufReader>> = BufReader::new(cursor); - - Decoder::new(reader).expect("decoder issue").amplify(0.20) + cache } -pub fn handle_audio(rx: Receiver, mute: bool) { +fn load_audio( + soundtrack: &Soundtrack, + part: &SoundrackParts, +) -> Amplify>>>> { + { + let guard: RwLockReadGuard<'_, HashMap>>>> = + 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 = 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>>>, + > = 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 = 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>> = + Arc::new(load_soundtrack_folder(folder_name)); + + let data: &Vec = 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, mute: bool, soundtrack: Soundtrack) { let mut handle: MixerDeviceSink = DeviceSinkBuilder::from_default_device() .expect("get default device") .with_buffer_size(BufferSize::Fixed(4096)) @@ -66,8 +142,9 @@ pub fn handle_audio(rx: Receiver, mute: bool) { let mut volume: f32 = player.volume(); player.set_volume(if mute { 0.0 } else { volume }); + let mut current_soundtrack: Soundtrack = soundtrack; let mut current_part: SoundrackParts = SoundrackParts::Calm; - player.append(load_audio(¤t_part)); + player.append(load_audio(¤t_soundtrack, ¤t_part)); loop { for cmd in rx.try_iter() { @@ -85,14 +162,19 @@ pub fn handle_audio(rx: Receiver, mute: bool) { } AudioCmd::ChangePart(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(); } } } if player.empty() { - player.append(load_audio(¤t_part)); + player.append(load_audio(¤t_soundtrack, ¤t_part)); } } } diff --git a/src/app/threads/mod.rs b/src/app/threads/mod.rs index 1f27c25..e2798ce 100644 --- a/src/app/threads/mod.rs +++ b/src/app/threads/mod.rs @@ -2,6 +2,6 @@ pub mod audio; pub mod 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 handle_events::handle_events; diff --git a/src/cli.rs b/src/cli.rs index c3c27e1..f1d0a01 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,6 @@ use crate::app::{ states::{GameMode, PerkDecks, ZoomLevel}, + threads::Soundtrack, view::View, }; use clap::{Error, Parser, error::ErrorKind, value_parser}; @@ -133,13 +134,15 @@ pub struct Cli { )] pub skill_points_limit: u16, - /// Enable logging to a file (default: disabled). + /// Which soundtrack to play (default: default) #[arg( - long, - help = "Enable logging to file (default: disabled)", - default_value_t = false - )] - pub log: bool, + long, + help = "Soundtrack", + value_name = "...", + default_value_t = Soundtrack::Default, + value_enum + )] + pub sound_track: Soundtrack, /// Mute soundtrack (default: disabled). #[arg( @@ -148,6 +151,14 @@ pub struct Cli { default_value_t = false )] 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` diff --git a/src/main.rs b/src/main.rs index 2bc6c03..09b9723 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ fn main() -> Result<()> { // }); thread::spawn(move || { - handle_audio(audio_rx, args.mute); + handle_audio(audio_rx, args.mute, args.sound_track); }); let mut terminal: Terminal> = ratatui::init();