Skip to content

Commit d60fc9d

Browse files
fix: rust features to disable sound (#184)
* fix: rust features to disable sound Signed-off-by: Thomas Mauran <thomasmauran@yahoo.com> * chore: remove sound menu item when no sound Signed-off-by: Thomas Mauran <thomasmauran@yahoo.com> * feat: --no-sound command Signed-off-by: Thomas Mauran <thomasmauran@yahoo.com> * docs: add the no-sound to the documentation Signed-off-by: Thomas Mauran <thomasmauran@yahoo.com> --------- Signed-off-by: Thomas Mauran <thomasmauran@yahoo.com>
1 parent 97ae599 commit d60fc9d

File tree

10 files changed

+211
-130
lines changed

10 files changed

+211
-130
lines changed

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ serde_json = "1.0"
2424
reqwest = { version = "0.11", features = ["blocking", "json"] }
2525
tokio = { version = "1", features = ["full"] }
2626
dotenv = "0.15"
27-
rodio = "0.18"
27+
rodio = { version = "0.18", optional = true }
2828

2929
[dev-dependencies]
3030
tempfile = "3.8"
3131

3232
[features]
3333
chess-tui = []
34-
default = ["chess-tui"]
34+
sound = ["rodio"]
35+
default = ["chess-tui", "sound"]
3536

3637
[profile.release]
3738
lto = true

src/app.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,14 +1242,21 @@ impl App {
12421242
self.cycle_skin();
12431243
self.update_config();
12441244
}
1245+
#[cfg(feature = "sound")]
12451246
5 => {
12461247
// Toggle sound
12471248
self.sound_enabled = !self.sound_enabled;
12481249
crate::sound::set_sound_enabled(self.sound_enabled);
12491250
self.update_config();
12501251
}
1252+
#[cfg(feature = "sound")]
12511253
6 => self.toggle_help_popup(),
1254+
#[cfg(feature = "sound")]
12521255
7 => self.current_page = Pages::Credit,
1256+
#[cfg(not(feature = "sound"))]
1257+
5 => self.toggle_help_popup(),
1258+
#[cfg(not(feature = "sound"))]
1259+
6 => self.current_page = Pages::Credit,
12531260
_ => {}
12541261
}
12551262
}

src/handler.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,21 @@ fn handle_page_input(app: &mut App, key_event: KeyEvent) {
307307
/// Handles keyboard input on the home/menu page.
308308
/// Supports navigation through menu items and selection.
309309
fn handle_home_page_events(app: &mut App, key_event: KeyEvent) {
310+
// Number of menu items depends on whether sound feature is enabled
311+
const MENU_ITEMS: u8 = {
312+
#[cfg(feature = "sound")]
313+
{
314+
8 // Local game, Multiplayer, Lichess, Bot, Skin, Sound, Help, About
315+
}
316+
#[cfg(not(feature = "sound"))]
317+
{
318+
7 // Local game, Multiplayer, Lichess, Bot, Skin, Help, About
319+
}
320+
};
321+
310322
match key_event.code {
311-
KeyCode::Up | KeyCode::Char('k') => app.menu_cursor_up(8), // 8 menu items: Local game, Multiplayer, Lichess, Bot, Skin, Sound, Help, About
312-
KeyCode::Down | KeyCode::Char('j') => app.menu_cursor_down(8), // 8 menu items
323+
KeyCode::Up | KeyCode::Char('k') => app.menu_cursor_up(MENU_ITEMS),
324+
KeyCode::Down | KeyCode::Char('j') => app.menu_cursor_down(MENU_ITEMS),
313325
// If on skin selection menu item (index 3), use left/right to cycle skins
314326
KeyCode::Left | KeyCode::Char('h') if app.menu_cursor == 3 => {
315327
app.cycle_skin_backward();

src/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ struct Args {
3030
/// Lichess API token
3131
#[arg(short, long)]
3232
lichess_token: Option<String>,
33+
/// Disable sound effects
34+
#[arg(long)]
35+
no_sound: bool,
3336
}
3437

3538
fn main() -> AppResult<()> {
@@ -178,6 +181,12 @@ fn main() -> AppResult<()> {
178181
app.lichess_token = Some(token.clone());
179182
}
180183

184+
// Command line no-sound flag takes precedence over configuration file
185+
if args.no_sound {
186+
app.sound_enabled = false;
187+
chess_tui::sound::set_sound_enabled(false);
188+
}
189+
181190
// Setup logging
182191
if let Err(e) = logging::setup_logging(&folder_path, &app.log_level) {
183192
eprintln!("Failed to initialize logging: {}", e);
@@ -361,6 +370,11 @@ fn config_create(args: &Args, folder_path: &Path, config_path: &Path) -> AppResu
361370
config.bot_depth = Some(args.depth);
362371
}
363372

373+
// Always update sound_enabled if --no-sound flag is provided via command line (command line takes precedence)
374+
if args.no_sound {
375+
config.sound_enabled = Some(false);
376+
}
377+
364378
let toml_string = toml::to_string(&config)
365379
.expect("Failed to serialize config to TOML. This is a bug, please report it.");
366380
let mut file = File::create(config_path)?;
@@ -395,6 +409,7 @@ mod tests {
395409
engine_path: "test_engine_path".to_string(),
396410
depth: 10,
397411
lichess_token: None,
412+
no_sound: false,
398413
};
399414

400415
let config_dir = config_dir().unwrap();

src/sound.rs

Lines changed: 121 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use rodio::{OutputStream, Sink};
21
use std::sync::atomic::{AtomicBool, Ordering};
32

43
// Global sound enabled state
@@ -9,11 +8,20 @@ static AUDIO_AVAILABLE: AtomicBool = AtomicBool::new(true);
98
/// Check if audio is available and update the availability state
109
/// This should be called at startup to detect if we're in an environment without audio (e.g., Docker)
1110
pub fn check_audio_availability() -> bool {
12-
// Try to create an output stream
13-
// Note: ALSA may print errors to stderr, but we handle the failure gracefully
14-
let available = OutputStream::try_default().is_ok();
15-
AUDIO_AVAILABLE.store(available, Ordering::Relaxed);
16-
available
11+
#[cfg(feature = "sound")]
12+
{
13+
use rodio::OutputStream;
14+
// Try to create an output stream
15+
// Note: ALSA may print errors to stderr, but we handle the failure gracefully
16+
let available = OutputStream::try_default().is_ok();
17+
AUDIO_AVAILABLE.store(available, Ordering::Relaxed);
18+
available
19+
}
20+
#[cfg(not(feature = "sound"))]
21+
{
22+
AUDIO_AVAILABLE.store(false, Ordering::Relaxed);
23+
false
24+
}
1725
}
1826

1927
/// Set whether sounds are enabled
@@ -29,112 +37,124 @@ pub fn is_sound_enabled() -> bool {
2937
/// Plays a move sound when a chess piece is moved.
3038
/// This generates a pleasant, wood-like "click" sound using multiple harmonics.
3139
pub fn play_move_sound() {
32-
if !is_sound_enabled() {
33-
return;
34-
}
35-
// Spawn in a separate thread to avoid blocking the main game loop
36-
std::thread::spawn(|| {
37-
// Try to get an output stream, but don't fail if audio isn't available
38-
let Ok((_stream, stream_handle)) = OutputStream::try_default() else {
39-
return;
40-
};
41-
42-
// Create a sink to play the sound
43-
let Ok(sink) = Sink::try_new(&stream_handle) else {
40+
#[cfg(feature = "sound")]
41+
{
42+
if !is_sound_enabled() {
4443
return;
45-
};
46-
47-
// Generate a pleasant wood-like click sound
48-
// Using a lower fundamental frequency with harmonics for a richer sound
49-
let sample_rate = 44100;
50-
let duration = 0.08; // 80 milliseconds - slightly longer for better perception
51-
let fundamental = 200.0; // Lower frequency for a more pleasant, less harsh sound
52-
53-
let num_samples = (sample_rate as f64 * duration) as usize;
54-
let mut samples = Vec::with_capacity(num_samples);
55-
56-
for i in 0..num_samples {
57-
let t = i as f64 / sample_rate as f64;
58-
59-
// Create a more sophisticated envelope with exponential decay
60-
// Quick attack, smooth decay - like a wood piece being placed
61-
let envelope = if t < duration * 0.1 {
62-
// Quick attack (10% of duration)
63-
(t / (duration * 0.1)).powf(0.5)
64-
} else {
65-
// Exponential decay
66-
let decay_start = duration * 0.1;
67-
let decay_time = t - decay_start;
68-
let decay_duration = duration - decay_start;
69-
(-decay_time * 8.0 / decay_duration).exp()
44+
}
45+
// Spawn in a separate thread to avoid blocking the main game loop
46+
std::thread::spawn(|| {
47+
use rodio::{OutputStream, Sink};
48+
// Try to get an output stream, but don't fail if audio isn't available
49+
let Ok((_stream, stream_handle)) = OutputStream::try_default() else {
50+
return;
7051
};
7152

72-
// Generate a richer sound with harmonics
73-
// Fundamental + 2nd harmonic (octave) + 3rd harmonic (fifth)
74-
let fundamental_wave = (t * fundamental * 2.0 * std::f64::consts::PI).sin();
75-
let harmonic2 = (t * fundamental * 2.0 * 2.0 * std::f64::consts::PI).sin() * 0.3;
76-
let harmonic3 = (t * fundamental * 2.0 * 3.0 * std::f64::consts::PI).sin() * 0.15;
77-
78-
// Combine harmonics with envelope
79-
let sample = (fundamental_wave + harmonic2 + harmonic3) * envelope * 0.25;
80-
81-
// Convert to i16 sample
82-
samples.push((sample * i16::MAX as f64).clamp(i16::MIN as f64, i16::MAX as f64) as i16);
83-
}
53+
// Create a sink to play the sound
54+
let Ok(sink) = Sink::try_new(&stream_handle) else {
55+
return;
56+
};
8457

85-
// Convert to a source that rodio can play
86-
let source = rodio::buffer::SamplesBuffer::new(1, sample_rate, samples);
87-
sink.append(source);
88-
sink.sleep_until_end();
89-
});
58+
// Generate a pleasant wood-like click sound
59+
// Using a lower fundamental frequency with harmonics for a richer sound
60+
let sample_rate = 44100;
61+
let duration = 0.08; // 80 milliseconds - slightly longer for better perception
62+
let fundamental = 200.0; // Lower frequency for a more pleasant, less harsh sound
63+
64+
let num_samples = (sample_rate as f64 * duration) as usize;
65+
let mut samples = Vec::with_capacity(num_samples);
66+
67+
for i in 0..num_samples {
68+
let t = i as f64 / sample_rate as f64;
69+
70+
// Create a more sophisticated envelope with exponential decay
71+
// Quick attack, smooth decay - like a wood piece being placed
72+
let envelope = if t < duration * 0.1 {
73+
// Quick attack (10% of duration)
74+
(t / (duration * 0.1)).powf(0.5)
75+
} else {
76+
// Exponential decay
77+
let decay_start = duration * 0.1;
78+
let decay_time = t - decay_start;
79+
let decay_duration = duration - decay_start;
80+
(-decay_time * 8.0 / decay_duration).exp()
81+
};
82+
83+
// Generate a richer sound with harmonics
84+
// Fundamental + 2nd harmonic (octave) + 3rd harmonic (fifth)
85+
let fundamental_wave = (t * fundamental * 2.0 * std::f64::consts::PI).sin();
86+
let harmonic2 = (t * fundamental * 2.0 * 2.0 * std::f64::consts::PI).sin() * 0.3;
87+
let harmonic3 = (t * fundamental * 2.0 * 3.0 * std::f64::consts::PI).sin() * 0.15;
88+
89+
// Combine harmonics with envelope
90+
let sample = (fundamental_wave + harmonic2 + harmonic3) * envelope * 0.25;
91+
92+
// Convert to i16 sample
93+
samples.push(
94+
(sample * i16::MAX as f64).clamp(i16::MIN as f64, i16::MAX as f64) as i16,
95+
);
96+
}
97+
98+
// Convert to a source that rodio can play
99+
let source = rodio::buffer::SamplesBuffer::new(1, sample_rate, samples);
100+
sink.append(source);
101+
sink.sleep_until_end();
102+
});
103+
}
90104
}
91105

92106
/// Plays a light navigation sound when moving through menu items.
93107
/// This generates a subtle, high-pitched "tick" sound for menu navigation.
94108
pub fn play_menu_nav_sound() {
95-
if !is_sound_enabled() {
96-
return;
97-
}
98-
// Spawn in a separate thread to avoid blocking the main game loop
99-
std::thread::spawn(|| {
100-
// Try to get an output stream, but don't fail if audio isn't available
101-
let Ok((_stream, stream_handle)) = OutputStream::try_default() else {
109+
#[cfg(feature = "sound")]
110+
{
111+
if !is_sound_enabled() {
102112
return;
103-
};
104-
105-
// Create a sink to play the sound
106-
let Ok(sink) = Sink::try_new(&stream_handle) else {
107-
return;
108-
};
109-
110-
// Generate a light, high-pitched tick sound for menu navigation
111-
let sample_rate = 44100;
112-
let duration = 0.04;
113-
let frequency = 600.0;
114-
115-
let num_samples = (sample_rate as f64 * duration) as usize;
116-
let mut samples = Vec::with_capacity(num_samples);
117-
118-
for i in 0..num_samples {
119-
let t = i as f64 / sample_rate as f64;
120-
121-
let envelope = if t < duration * 0.2 {
122-
(t / (duration * 0.2)).powf(0.3)
123-
} else {
124-
let decay_start = duration * 0.2;
125-
let decay_time = t - decay_start;
126-
let decay_duration = duration - decay_start;
127-
(-decay_time * 12.0 / decay_duration).exp()
113+
}
114+
// Spawn in a separate thread to avoid blocking the main game loop
115+
std::thread::spawn(|| {
116+
use rodio::{OutputStream, Sink};
117+
// Try to get an output stream, but don't fail if audio isn't available
118+
let Ok((_stream, stream_handle)) = OutputStream::try_default() else {
119+
return;
128120
};
129121

130-
let sample = (t * frequency * 2.0 * std::f64::consts::PI).sin() * envelope * 0.3;
131-
132-
samples.push((sample * i16::MAX as f64).clamp(i16::MIN as f64, i16::MAX as f64) as i16);
133-
}
122+
// Create a sink to play the sound
123+
let Ok(sink) = Sink::try_new(&stream_handle) else {
124+
return;
125+
};
134126

135-
// Convert to a source that rodio can play
136-
let source = rodio::buffer::SamplesBuffer::new(1, sample_rate, samples);
137-
sink.append(source);
138-
sink.sleep_until_end();
139-
});
127+
// Generate a light, high-pitched tick sound for menu navigation
128+
let sample_rate = 44100;
129+
let duration = 0.04;
130+
let frequency = 600.0;
131+
132+
let num_samples = (sample_rate as f64 * duration) as usize;
133+
let mut samples = Vec::with_capacity(num_samples);
134+
135+
for i in 0..num_samples {
136+
let t = i as f64 / sample_rate as f64;
137+
138+
let envelope = if t < duration * 0.2 {
139+
(t / (duration * 0.2)).powf(0.3)
140+
} else {
141+
let decay_start = duration * 0.2;
142+
let decay_time = t - decay_start;
143+
let decay_duration = duration - decay_start;
144+
(-decay_time * 12.0 / decay_duration).exp()
145+
};
146+
147+
let sample = (t * frequency * 2.0 * std::f64::consts::PI).sin() * envelope * 0.3;
148+
149+
samples.push(
150+
(sample * i16::MAX as f64).clamp(i16::MIN as f64, i16::MAX as f64) as i16,
151+
);
152+
}
153+
154+
// Convert to a source that rodio can play
155+
let source = rodio::buffer::SamplesBuffer::new(1, sample_rate, samples);
156+
sink.append(source);
157+
sink.sleep_until_end();
158+
});
159+
}
140160
}

0 commit comments

Comments
 (0)