par_term/
audio_bell.rs

1use parking_lot::Mutex;
2use rodio::{OutputStream, OutputStreamBuilder, Sink, Source};
3use std::sync::Arc;
4use std::time::Duration;
5
6/// Audio bell manager for playing terminal bell sounds
7pub struct AudioBell {
8    /// Audio output stream handle (kept alive for the duration of the application)
9    stream: Option<OutputStream>,
10    /// Audio sink for playback
11    sink: Option<Arc<Mutex<Sink>>>,
12}
13
14impl Drop for AudioBell {
15    fn drop(&mut self) {
16        // Suppress 'Dropping OutputStream' message by forgetting the stream
17        if let Some(stream) = self.stream.take() {
18            std::mem::forget(stream);
19        }
20    }
21}
22
23impl AudioBell {
24    /// Create a new audio bell manager
25    pub fn new() -> Result<Self, String> {
26        let stream = OutputStreamBuilder::open_default_stream()
27            .map_err(|e| format!("Failed to open audio stream: {}", e))?;
28
29        let sink = Sink::connect_new(stream.mixer());
30
31        Ok(Self {
32            stream: Some(stream),
33            sink: Some(Arc::new(Mutex::new(sink))),
34        })
35    }
36
37    /// Create a dummy/disabled audio bell (safe fallback)
38    pub fn disabled() -> Self {
39        Self {
40            stream: None,
41            sink: None,
42        }
43    }
44
45    /// Play a bell sound with the specified volume (0-100)
46    ///
47    /// # Arguments
48    /// * `volume` - Volume level from 0 to 100. A value of 0 disables the bell sound.
49    pub fn play(&self, volume: u8) {
50        if volume == 0 {
51            return;
52        }
53
54        let sink_arc = match &self.sink {
55            Some(s) => s,
56            None => return, // Audio disabled
57        };
58
59        // Clamp volume to 0-100 range and convert to 0.0-1.0
60        let volume_f32 = (volume.min(100) as f32) / 100.0;
61
62        // Generate a simple beep: 800 Hz sine wave for 100ms
63        let source = rodio::source::SineWave::new(800.0)
64            .take_duration(Duration::from_millis(100))
65            .amplify(volume_f32 * 0.3); // Scale down to avoid being too loud
66
67        let sink = sink_arc.lock();
68        sink.append(source);
69    }
70}
71
72impl Default for AudioBell {
73    fn default() -> Self {
74        Self::new().unwrap_or_else(|e| {
75            log::warn!("Failed to initialize audio bell: {}", e);
76            Self::disabled()
77        })
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_audio_bell_creation() {
87        // Just ensure we can create the audio bell without panicking
88        let bell = AudioBell::new();
89        assert!(bell.is_ok() || bell.is_err());
90    }
91
92    #[test]
93    fn test_audio_bell_default() {
94        // Should not panic even if audio setup fails
95        let _bell = AudioBell::default();
96    }
97
98    #[test]
99    fn test_audio_bell_play_zero_volume() {
100        if let Ok(bell) = AudioBell::new() {
101            // Should not panic with zero volume
102            bell.play(0);
103        }
104    }
105
106    #[test]
107    fn test_audio_bell_play_max_volume() {
108        if let Ok(bell) = AudioBell::new() {
109            // Should not panic with max volume
110            bell.play(100);
111        }
112    }
113
114    #[test]
115    fn test_audio_bell_play_over_max_volume() {
116        if let Ok(bell) = AudioBell::new() {
117            // Should clamp to max volume without panicking
118            bell.play(150);
119        }
120    }
121
122    #[test]
123    fn test_disabled_bell() {
124        let bell = AudioBell::disabled();
125        // Should simply do nothing, not panic
126        bell.play(50);
127    }
128}