66//! Audio playback is handled in a dedicated thread since rodio's OutputStream
77//! is not Send/Sync. We use a channel-based approach to send sound requests
88//! from any thread to the dedicated audio thread.
9+ //!
10+ //! On platforms without audio support (e.g., musl builds), falls back to
11+ //! terminal bell notifications.
912
10- use std:: io:: { Cursor , Write } ;
11- use std:: sync:: OnceLock ;
13+ use std:: io:: Write ;
14+ #[ cfg( feature = "audio" ) ]
15+ use std:: io:: Cursor ;
16+ #[ cfg( feature = "audio" ) ]
1217use std:: sync:: mpsc;
18+ use std:: sync:: OnceLock ;
19+ #[ cfg( feature = "audio" ) ]
1320use std:: thread;
1421
1522/// Type of sound notification
@@ -23,16 +30,24 @@ pub enum SoundType {
2330
2431/// Channel sender for sound requests (Send + Sync)
2532/// Using sync_channel with capacity of 16 to prevent unbounded growth
33+ #[ cfg( feature = "audio" ) ]
2634static SOUND_SENDER : OnceLock < mpsc:: SyncSender < SoundType > > = OnceLock :: new ( ) ;
2735
36+ /// Track whether sound system has been initialized (for non-audio builds)
37+ #[ cfg( not( feature = "audio" ) ) ]
38+ static SOUND_INITIALIZED : OnceLock < bool > = OnceLock :: new ( ) ;
39+
2840/// Embedded WAV data for response complete sound
41+ #[ cfg( feature = "audio" ) ]
2942const COMPLETE_WAV : & [ u8 ] = include_bytes ! ( "sounds/complete.wav" ) ;
3043/// Embedded WAV data for approval required sound
44+ #[ cfg( feature = "audio" ) ]
3145const APPROVAL_WAV : & [ u8 ] = include_bytes ! ( "sounds/approval.wav" ) ;
3246
3347/// Initialize the global sound system.
3448/// Spawns a dedicated audio thread that owns the OutputStream.
3549/// Should be called once at application startup.
50+ #[ cfg( feature = "audio" ) ]
3651pub fn init ( ) {
3752 // Only initialize once
3853 if SOUND_SENDER . get ( ) . is_some ( ) {
@@ -78,7 +93,17 @@ pub fn init() {
7893 . expect ( "Failed to spawn audio thread" ) ;
7994}
8095
96+ /// Initialize the global sound system (no-op for non-audio builds).
97+ /// Falls back to terminal bell for notifications.
98+ #[ cfg( not( feature = "audio" ) ) ]
99+ pub fn init ( ) {
100+ // Mark as initialized so is_initialized() returns true
101+ let _ = SOUND_INITIALIZED . set ( true ) ;
102+ tracing:: debug!( "Audio support not available, using terminal bell fallback" ) ;
103+ }
104+
81105/// Internal function to play WAV data using a stream handle
106+ #[ cfg( feature = "audio" ) ]
82107fn play_wav_internal (
83108 handle : & rodio:: OutputStreamHandle ,
84109 data : & ' static [ u8 ] ,
@@ -103,6 +128,7 @@ fn emit_terminal_bell() {
103128/// If `enabled` is false or audio is unavailable, this function does nothing.
104129/// Falls back to terminal bell if the sound system is not initialized.
105130/// This function is non-blocking - sound plays in background thread.
131+ #[ cfg( feature = "audio" ) ]
106132pub fn play ( sound_type : SoundType , enabled : bool ) {
107133 if !enabled {
108134 return ;
@@ -121,6 +147,16 @@ pub fn play(sound_type: SoundType, enabled: bool) {
121147 }
122148}
123149
150+ /// Play a notification sound (non-audio build - uses terminal bell).
151+ #[ cfg( not( feature = "audio" ) ) ]
152+ pub fn play ( _sound_type : SoundType , enabled : bool ) {
153+ if !enabled {
154+ return ;
155+ }
156+ // No audio support, use terminal bell
157+ emit_terminal_bell ( ) ;
158+ }
159+
124160/// Play notification for response completion
125161pub fn play_response_complete ( enabled : bool ) {
126162 play ( SoundType :: ResponseComplete , enabled) ;
@@ -133,10 +169,17 @@ pub fn play_approval_required(enabled: bool) {
133169
134170/// Check if the sound system has been initialized.
135171/// Useful for testing and diagnostics.
172+ #[ cfg( feature = "audio" ) ]
136173pub fn is_initialized ( ) -> bool {
137174 SOUND_SENDER . get ( ) . is_some ( )
138175}
139176
177+ /// Check if the sound system has been initialized (non-audio build).
178+ #[ cfg( not( feature = "audio" ) ) ]
179+ pub fn is_initialized ( ) -> bool {
180+ SOUND_INITIALIZED . get ( ) . is_some ( )
181+ }
182+
140183#[ cfg( test) ]
141184mod tests {
142185 use super :: * ;
@@ -183,13 +226,15 @@ mod tests {
183226 }
184227
185228 #[ test]
229+ #[ cfg( feature = "audio" ) ]
186230 fn test_embedded_wav_data_not_empty ( ) {
187231 // Verify that the embedded WAV files are not empty
188232 assert ! ( !COMPLETE_WAV . is_empty( ) , "complete.wav should not be empty" ) ;
189233 assert ! ( !APPROVAL_WAV . is_empty( ) , "approval.wav should not be empty" ) ;
190234 }
191235
192236 #[ test]
237+ #[ cfg( feature = "audio" ) ]
193238 fn test_embedded_wav_data_has_riff_header ( ) {
194239 // WAV files should start with "RIFF" magic bytes
195240 assert ! (
0 commit comments