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"
},
"locked": {
"lastModified": 1762929886,
"narHash": "sha256-TQZ3Ugb1FoHpTSc8KLrzN4njIZU4FemAMHyS4M3mt6s=",
"lastModified": 1775462255,
"narHash": "sha256-YRzdvh6nvMebcgO2nDpr8dqVwKHpp1BBRUHeMxX9UAY=",
"owner": "nix-community",
"repo": "fenix",
"rev": "6998514dce2c365142a0a119a95ef95d89b84086",
"rev": "f90343f1ed330243d4bbdbce51acbd93b776a797",
"type": "github"
},
"original": {
@@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1752689277,
"narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=",
"lastModified": 1769799857,
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
"owner": "nix-community",
"repo": "naersk",
"rev": "0e72363d0938b0208d6c646d10649164c43f4d64",
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
"type": "github"
},
"original": {
@@ -46,11 +46,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1762844143,
"narHash": "sha256-SlybxLZ1/e4T2lb1czEtWVzDCVSTvk9WLwGhmxFmBxI=",
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9da7f1cf7f8a6e2a7cb3001b048546c92a8258b4",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"type": "github"
},
"original": {
@@ -70,11 +70,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1762860488,
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
"lastModified": 1775429583,
"narHash": "sha256-bFC/p7Ywyd9QIr9DbU3Q75c7AcaCm9wVmEvcI3702cY=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
"rev": "38fb8f92ac15853d7fa9fb47fc2d81fdd5cd6c7e",
"type": "github"
},
"original": {
+4 -1
View File
@@ -48,7 +48,10 @@
};
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; [
rustToolchain
alsa-lib
Binary file not shown.
+9 -3
View File
@@ -1,11 +1,15 @@
use crate::{
app::{GameStates, handle_keybindings, threads::AppEvent, view::View},
app::{
GameStates, handle_keybindings,
threads::{AppEvent, AudioCmd},
view::View,
},
cli::Cli,
};
use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect};
use std::{
io::Result,
sync::mpsc::{Receiver, RecvTimeoutError},
sync::mpsc::{Receiver, RecvTimeoutError, Sender},
time::Duration,
};
@@ -15,16 +19,18 @@ pub struct App {
pub window_area: Rect,
pub args: Cli,
pub states: Option<GameStates>,
pub audio_tx: Sender<AudioCmd>,
}
impl App {
pub fn new(args: Cli) -> Self {
pub fn new(args: Cli, audio_tx: Sender<AudioCmd>) -> Self {
Self {
exit: false,
view: args.view,
window_area: Rect::default(),
args: args,
states: None,
audio_tx,
}
}
+4
View File
@@ -1,6 +1,7 @@
use crate::app::{
App, View,
keybindings::{Action, event_to_action},
threads::AudioCmd,
};
use ratatui::crossterm::event::KeyEvent;
@@ -8,6 +9,9 @@ pub fn common_keybindings(app: &mut App, action: Action) {
match action {
Action::Quit | Action::Quit2 => app.exit = true,
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,
/// Zoom the view out.
ZoomOut,
/// Mute music.
Mute,
/// Volume up.
VolumeUp,
/// Volume down.
VolumeDown,
/// Matches any character key; the inner `char` is the actual key pressed.
WildCard(char),
}
@@ -61,6 +67,8 @@ pub enum Group {
Input,
/// Zoom related bindings.
Zoom,
/// Music related bindings.
Music,
/// Quit related bindings.
Quit,
}
@@ -236,6 +244,33 @@ pub static KEYBINDINGS: &[KeyBinding] = &[
symbol: ".",
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 {
action: Action::WildCard('_'),
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 handle_events;
pub use audio::{AudioCmd, SoundrackParts, handle_audio};
pub use events::AppEvent;
pub use handle_events::handle_events;
+3
View File
@@ -17,6 +17,9 @@ const ACTIONS: &[Action] = &[
Action::Up,
Action::Down,
Action::Space,
Action::VolumeUp,
Action::VolumeDown,
Action::Mute,
Action::Quit,
Action::Quit2,
];
+3
View File
@@ -23,6 +23,9 @@ const ACTIONS: &[Action] = &[
Action::ScrollRight,
Action::ZoomIn,
Action::ZoomOut,
Action::VolumeUp,
Action::VolumeDown,
Action::Mute,
Action::Quit,
Action::Quit2,
Action::Esc,
+8
View File
@@ -140,6 +140,14 @@ pub struct Cli {
default_value_t = false
)]
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`
+26 -7
View File
@@ -2,12 +2,15 @@ use ratatui::{Terminal, prelude::CrosstermBackend};
use std::{
io::{Result, Stdout},
sync::mpsc::channel,
thread::{self, JoinHandle},
thread::{
self,
// JoinHandle
},
};
use war_in_tunnels::{
app::{
App,
threads::{AppEvent, handle_events},
threads::{AppEvent, AudioCmd, handle_audio, handle_events},
},
cli::{Cli, get_args},
logs::init_logger,
@@ -24,19 +27,35 @@ fn main() -> Result<()> {
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_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);
});
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);
ratatui::restore();
let _ = app_event_thread.join();
// let _ = app_event_thread.;
// let _ = audio_event_thread.join();
app_result
}