99//!
1010//! On platforms without audio support (e.g., musl builds), falls back to
1111//! terminal bell notifications.
12+ //!
13+ //! On Linux, ALSA error messages (e.g., "cannot find card 0") are suppressed
14+ //! during audio initialization to avoid noisy output on headless systems.
1215
1316#[ cfg( feature = "audio" ) ]
1417use std:: io:: Cursor ;
@@ -44,6 +47,96 @@ const COMPLETE_WAV: &[u8] = include_bytes!("sounds/complete.wav");
4447#[ cfg( feature = "audio" ) ]
4548const APPROVAL_WAV : & [ u8 ] = include_bytes ! ( "sounds/approval.wav" ) ;
4649
50+ /// Try to create audio output stream, suppressing ALSA errors on Linux.
51+ ///
52+ /// On Linux, ALSA prints error messages directly to stderr when no audio
53+ /// hardware is available (e.g., "ALSA lib confmisc.c: cannot find card 0").
54+ /// This function suppresses those messages by temporarily redirecting stderr
55+ /// to /dev/null during initialization.
56+ #[ cfg( all( feature = "audio" , target_os = "linux" ) ) ]
57+ fn try_create_output_stream ( ) -> Option < ( rodio:: OutputStream , rodio:: OutputStreamHandle ) > {
58+ use std:: os:: unix:: io:: AsRawFd ;
59+
60+ // Open /dev/null for redirecting stderr
61+ let dev_null = match std:: fs:: File :: open ( "/dev/null" ) {
62+ Ok ( f) => f,
63+ Err ( _) => {
64+ // Can't open /dev/null, try without suppression
65+ return match rodio:: OutputStream :: try_default ( ) {
66+ Ok ( ( stream, handle) ) => Some ( ( stream, handle) ) ,
67+ Err ( e) => {
68+ tracing:: debug!( "Failed to initialize audio output: {}" , e) ;
69+ None
70+ }
71+ } ;
72+ }
73+ } ;
74+
75+ // Save the original stderr file descriptor
76+ // SAFETY: dup is safe to call with a valid file descriptor (2 = stderr)
77+ let original_stderr = unsafe { libc:: dup ( 2 ) } ;
78+ if original_stderr == -1 {
79+ // dup failed, try without suppression
80+ return match rodio:: OutputStream :: try_default ( ) {
81+ Ok ( ( stream, handle) ) => Some ( ( stream, handle) ) ,
82+ Err ( e) => {
83+ tracing:: debug!( "Failed to initialize audio output: {}" , e) ;
84+ None
85+ }
86+ } ;
87+ }
88+
89+ // Redirect stderr to /dev/null
90+ // SAFETY: dup2 is safe with valid file descriptors
91+ let redirect_result = unsafe { libc:: dup2 ( dev_null. as_raw_fd ( ) , 2 ) } ;
92+ drop ( dev_null) ; // Close our handle to /dev/null
93+
94+ if redirect_result == -1 {
95+ // dup2 failed, restore and try without suppression
96+ // SAFETY: close is safe with a valid file descriptor
97+ unsafe { libc:: close ( original_stderr) } ;
98+ return match rodio:: OutputStream :: try_default ( ) {
99+ Ok ( ( stream, handle) ) => Some ( ( stream, handle) ) ,
100+ Err ( e) => {
101+ tracing:: debug!( "Failed to initialize audio output: {}" , e) ;
102+ None
103+ }
104+ } ;
105+ }
106+
107+ // Try to create the audio output stream (ALSA errors will go to /dev/null)
108+ let result = rodio:: OutputStream :: try_default ( ) ;
109+
110+ // Restore the original stderr
111+ // SAFETY: dup2 and close are safe with valid file descriptors
112+ unsafe {
113+ libc:: dup2 ( original_stderr, 2 ) ;
114+ libc:: close ( original_stderr) ;
115+ }
116+
117+ match result {
118+ Ok ( ( stream, handle) ) => Some ( ( stream, handle) ) ,
119+ Err ( e) => {
120+ tracing:: debug!( "Failed to initialize audio output: {}" , e) ;
121+ None
122+ }
123+ }
124+ }
125+
126+ /// Try to create audio output stream (non-Linux platforms).
127+ ///
128+ /// On non-Linux platforms, ALSA is not used, so no stderr suppression is needed.
129+ #[ cfg( all( feature = "audio" , not( target_os = "linux" ) ) ) ]
130+ fn try_create_output_stream ( ) -> Option < ( rodio:: OutputStream , rodio:: OutputStreamHandle ) > {
131+ match rodio:: OutputStream :: try_default ( ) {
132+ Ok ( ( stream, handle) ) => Some ( ( stream, handle) ) ,
133+ Err ( e) => {
134+ tracing:: debug!( "Failed to initialize audio output: {}" , e) ;
135+ None
136+ }
137+ }
138+ }
139+
47140/// Initialize the global sound system.
48141/// Spawns a dedicated audio thread that owns the OutputStream.
49142/// Should be called once at application startup.
@@ -67,14 +160,8 @@ pub fn init() {
67160 thread:: Builder :: new ( )
68161 . name ( "cortex-audio" . to_string ( ) )
69162 . spawn ( move || {
70- // Try to create audio output
71- let output = match rodio:: OutputStream :: try_default ( ) {
72- Ok ( ( stream, handle) ) => Some ( ( stream, handle) ) ,
73- Err ( e) => {
74- tracing:: debug!( "Failed to initialize audio output: {}" , e) ;
75- None
76- }
77- } ;
163+ // Try to create audio output (with ALSA error suppression on Linux)
164+ let output = try_create_output_stream ( ) ;
78165
79166 // Process sound requests
80167 while let Ok ( sound_type) = rx. recv ( ) {
0 commit comments