Add audio subsystem with mute and volume controls

Update flake.lock dependencies to latest revisions
This commit is contained in:
2026-04-07 01:05:33 +02:00
parent 0f37989f55
commit 8e01c8c33a
12 changed files with 186 additions and 23 deletions
Generated
+12 -12
View File
@@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1762929886, "lastModified": 1775462255,
"narHash": "sha256-TQZ3Ugb1FoHpTSc8KLrzN4njIZU4FemAMHyS4M3mt6s=", "narHash": "sha256-YRzdvh6nvMebcgO2nDpr8dqVwKHpp1BBRUHeMxX9UAY=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "6998514dce2c365142a0a119a95ef95d89b84086", "rev": "f90343f1ed330243d4bbdbce51acbd93b776a797",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -31,11 +31,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1752689277, "lastModified": 1769799857,
"narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=", "narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "0e72363d0938b0208d6c646d10649164c43f4d64", "rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -46,11 +46,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1762844143, "lastModified": 1775036866,
"narHash": "sha256-SlybxLZ1/e4T2lb1czEtWVzDCVSTvk9WLwGhmxFmBxI=", "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9da7f1cf7f8a6e2a7cb3001b048546c92a8258b4", "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -70,11 +70,11 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1762860488, "lastModified": 1775429583,
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=", "narHash": "sha256-bFC/p7Ywyd9QIr9DbU3Q75c7AcaCm9wVmEvcI3702cY=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763", "rev": "38fb8f92ac15853d7fa9fb47fc2d81fdd5cd6c7e",
"type": "github" "type": "github"
}, },
"original": { "original": {
+4 -1
View File
@@ -48,7 +48,10 @@
}; };
devShells.${system}.default = pkgs.mkShell { devShells.${system}.default = pkgs.mkShell {
env.RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; env = {
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; [alsa-lib]);
};
buildInputs = with pkgs; [ buildInputs = with pkgs; [
rustToolchain rustToolchain
alsa-lib alsa-lib
Binary file not shown.
+9 -3
View File
@@ -1,11 +1,15 @@
use crate::{ use crate::{
app::{GameStates, handle_keybindings, threads::AppEvent, view::View}, app::{
GameStates, handle_keybindings,
threads::{AppEvent, AudioCmd},
view::View,
},
cli::Cli, cli::Cli,
}; };
use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect}; use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect};
use std::{ use std::{
io::Result, io::Result,
sync::mpsc::{Receiver, RecvTimeoutError}, sync::mpsc::{Receiver, RecvTimeoutError, Sender},
time::Duration, time::Duration,
}; };
@@ -15,16 +19,18 @@ pub struct App {
pub window_area: Rect, pub window_area: Rect,
pub args: Cli, pub args: Cli,
pub states: Option<GameStates>, pub states: Option<GameStates>,
pub audio_tx: Sender<AudioCmd>,
} }
impl App { impl App {
pub fn new(args: Cli) -> Self { pub fn new(args: Cli, audio_tx: Sender<AudioCmd>) -> Self {
Self { Self {
exit: false, exit: false,
view: args.view, view: args.view,
window_area: Rect::default(), window_area: Rect::default(),
args: args, args: args,
states: None, states: None,
audio_tx,
} }
} }
+4
View File
@@ -1,6 +1,7 @@
use crate::app::{ use crate::app::{
App, View, App, View,
keybindings::{Action, event_to_action}, keybindings::{Action, event_to_action},
threads::AudioCmd,
}; };
use ratatui::crossterm::event::KeyEvent; use ratatui::crossterm::event::KeyEvent;
@@ -8,6 +9,9 @@ pub fn common_keybindings(app: &mut App, action: Action) {
match action { match action {
Action::Quit | Action::Quit2 => app.exit = true, Action::Quit | Action::Quit2 => app.exit = true,
Action::Esc => app.view = View::MainMenu, Action::Esc => app.view = View::MainMenu,
Action::Mute => if let Err(_) = app.audio_tx.send(AudioCmd::Mute) {},
Action::VolumeUp => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeUp) {},
Action::VolumeDown => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeDown) {},
_ => (), _ => (),
} }
} }
+35
View File
@@ -41,6 +41,12 @@ pub enum Action {
ZoomIn, ZoomIn,
/// Zoom the view out. /// Zoom the view out.
ZoomOut, ZoomOut,
/// Mute music.
Mute,
/// Volume up.
VolumeUp,
/// Volume down.
VolumeDown,
/// Matches any character key; the inner `char` is the actual key pressed. /// Matches any character key; the inner `char` is the actual key pressed.
WildCard(char), WildCard(char),
} }
@@ -61,6 +67,8 @@ pub enum Group {
Input, Input,
/// Zoom related bindings. /// Zoom related bindings.
Zoom, Zoom,
/// Music related bindings.
Music,
/// Quit related bindings. /// Quit related bindings.
Quit, Quit,
} }
@@ -236,6 +244,33 @@ pub static KEYBINDINGS: &[KeyBinding] = &[
symbol: ".", symbol: ".",
description: "Zoom out", description: "Zoom out",
}, },
KeyBinding {
action: Action::Mute,
code: KeyCode::Char('m'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "m",
description: "Mute music",
},
KeyBinding {
action: Action::VolumeUp,
code: KeyCode::Char('b'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "b",
description: "Increase music volume",
},
KeyBinding {
action: Action::VolumeDown,
code: KeyCode::Char('n'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "n",
description: "Decrease music volume",
},
KeyBinding { KeyBinding {
action: Action::WildCard('_'), action: Action::WildCard('_'),
code: KeyCode::Char('_'), code: KeyCode::Char('_'),
+80
View File
@@ -0,0 +1,80 @@
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, source::Amplify};
use std::{fs::File, io::BufReader, sync::mpsc::Receiver};
#[derive(Debug)]
pub enum AudioCmd {
Mute,
VolumeUp,
VolumeDown,
}
pub enum SoundrackParts {
Calm,
Buildup,
Assult,
Outro,
}
fn load_audio(part: SoundrackParts) -> Amplify<Decoder<BufReader<File>>> {
let file: File = match part {
SoundrackParts::Calm => File::open("soundtrack/default/test.ogg").expect("open audio file"),
SoundrackParts::Buildup => {
File::open("soundtrack/default/test.ogg").expect("open audio file")
}
SoundrackParts::Assult => {
File::open("soundtrack/default/test.ogg").expect("open audio file")
}
SoundrackParts::Outro => {
File::open("soundtrack/default/test.ogg").expect("open audio file")
}
};
Decoder::try_from(file)
.expect("decoder issue")
.amplify(0.20)
}
pub fn handle_audio(rx: Receiver<AudioCmd>, mute: bool) {
let mut handle: MixerDeviceSink =
DeviceSinkBuilder::open_default_sink().expect("open default audio stream");
handle.log_on_drop(false);
let player: Player = Player::connect_new(&handle.mixer());
let mut volume: f32 = player.volume();
if mute {
player.set_volume(0.0);
} else {
player.set_volume(volume);
}
player.append(load_audio(SoundrackParts::Calm));
loop {
if player.empty() {
player.append(load_audio(SoundrackParts::Calm));
}
for cmd in rx.try_iter() {
match cmd {
AudioCmd::Mute => {
if player.volume() == 0.0 {
player.set_volume(volume);
} else {
player.set_volume(0.0);
}
}
AudioCmd::VolumeUp => {
volume = (volume + 0.1).min(1.0);
player.set_volume(volume);
}
AudioCmd::VolumeDown => {
volume = (volume - 0.1).max(0.0);
player.set_volume(volume);
}
}
}
}
}
+2
View File
@@ -1,5 +1,7 @@
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 events::AppEvent; pub use events::AppEvent;
pub use handle_events::handle_events; pub use handle_events::handle_events;
+3
View File
@@ -17,6 +17,9 @@ const ACTIONS: &[Action] = &[
Action::Up, Action::Up,
Action::Down, Action::Down,
Action::Space, Action::Space,
Action::VolumeUp,
Action::VolumeDown,
Action::Mute,
Action::Quit, Action::Quit,
Action::Quit2, Action::Quit2,
]; ];
+3
View File
@@ -23,6 +23,9 @@ const ACTIONS: &[Action] = &[
Action::ScrollRight, Action::ScrollRight,
Action::ZoomIn, Action::ZoomIn,
Action::ZoomOut, Action::ZoomOut,
Action::VolumeUp,
Action::VolumeDown,
Action::Mute,
Action::Quit, Action::Quit,
Action::Quit2, Action::Quit2,
Action::Esc, Action::Esc,
+8
View File
@@ -140,6 +140,14 @@ pub struct Cli {
default_value_t = false default_value_t = false
)] )]
pub log: bool, pub log: bool,
/// Mute soundtrack (default: disabled).
#[arg(
long,
help = "Mute soundtrack (default: disabled)",
default_value_t = false
)]
pub mute: bool,
} }
/// Parses the command line arguments and returns a fully populated `Cli` /// Parses the command line arguments and returns a fully populated `Cli`
+26 -7
View File
@@ -2,12 +2,15 @@ use ratatui::{Terminal, prelude::CrosstermBackend};
use std::{ use std::{
io::{Result, Stdout}, io::{Result, Stdout},
sync::mpsc::channel, sync::mpsc::channel,
thread::{self, JoinHandle}, thread::{
self,
// JoinHandle
},
}; };
use war_in_tunnels::{ use war_in_tunnels::{
app::{ app::{
App, App,
threads::{AppEvent, handle_events}, threads::{AppEvent, AudioCmd, handle_audio, handle_events},
}, },
cli::{Cli, get_args}, cli::{Cli, get_args},
logs::init_logger, logs::init_logger,
@@ -24,19 +27,35 @@ fn main() -> Result<()> {
init_logger(); init_logger();
} }
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
let mut app: App = App::new(args);
let (app_event_tx, app_event_rx) = channel::<AppEvent>(); let (app_event_tx, app_event_rx) = channel::<AppEvent>();
let app_event_thread: JoinHandle<()> = thread::spawn(move || { // let app_event_thread: JoinHandle<()> = thread::spawn(move || {
// handle_events(app_event_tx);
// });
thread::spawn(move || {
handle_events(app_event_tx); handle_events(app_event_tx);
}); });
let (audio_tx, audio_rx) = channel::<AudioCmd>();
// let audio_event_thread: JoinHandle<()> = thread::spawn(move || {
// handle_audio(audio_rx);
// });
thread::spawn(move || {
handle_audio(audio_rx, args.mute);
});
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
let mut app: App = App::new(args, audio_tx);
let app_result: Result<()> = app.run(&mut terminal, app_event_rx); let app_result: Result<()> = app.run(&mut terminal, app_event_rx);
ratatui::restore(); ratatui::restore();
let _ = app_event_thread.join(); // let _ = app_event_thread.;
// let _ = audio_event_thread.join();
app_result app_result
} }