@@ -24,7 +24,11 @@ pub enum AudioInfoError {
2424}
2525
2626impl AudioInfo {
27- pub const MAX_AUDIO_CHANNELS : u16 = 16 ;
27+ /// Maximum number of audio channels supported by FFmpeg channel layouts.
28+ /// Matches the highest channel count in `channel_layout_raw` (7.1 surround = 8 channels).
29+ /// Note: Actual device channel counts may exceed this; we store the real count
30+ /// but clamp to this value when creating FFmpeg frames.
31+ pub const MAX_AUDIO_CHANNELS : u16 = 8 ;
2832
2933 pub const fn new (
3034 sample_format : Sample ,
@@ -69,18 +73,16 @@ impl AudioInfo {
6973 SupportedBufferSize :: Unknown => 1024 ,
7074 } ) ;
7175
72- let raw_channels = config. channels ( ) ;
73- let channels = if Self :: channel_layout_raw ( raw_channels) . is_some ( ) {
74- raw_channels
75- } else {
76- raw_channels. clamp ( 1 , Self :: MAX_AUDIO_CHANNELS )
77- } ;
76+ // Store the actual channel count from the device, even if it exceeds
77+ // MAX_AUDIO_CHANNELS. This ensures audio data parsing works correctly.
78+ // The clamping to supported FFmpeg layouts happens in channel_layout()
79+ // and wrap_frame_with_max_channels() when creating output frames.
80+ let raw_channels = config. channels ( ) . max ( 1 ) ; // At least 1 channel
7881
7982 Self {
8083 sample_format,
8184 sample_rate : config. sample_rate ( ) . 0 ,
82- // we do this here and only here bc we know it's cpal-related
83- channels : channels. into ( ) ,
85+ channels : raw_channels. into ( ) ,
8486 time_base : FFRational ( 1 , 1_000_000 ) ,
8587 buffer_size,
8688 }
@@ -115,7 +117,12 @@ impl AudioInfo {
115117 }
116118
117119 pub fn channel_layout ( & self ) -> ChannelLayout {
118- Self :: channel_layout_raw ( self . channels as u16 ) . unwrap ( )
120+ // Clamp channels to supported range and return appropriate layout.
121+ // This prevents panics when audio devices report unusual channel counts
122+ // (e.g., 0 channels or more than 8 channels).
123+ let clamped_channels = ( self . channels as u16 ) . clamp ( 1 , 8 ) ;
124+ Self :: channel_layout_raw ( clamped_channels)
125+ . unwrap_or ( ChannelLayout :: STEREO )
119126 }
120127
121128 pub fn sample_size ( & self ) -> usize {
@@ -139,10 +146,15 @@ impl AudioInfo {
139146 packed_data : & [ u8 ] ,
140147 max_channels : usize ,
141148 ) -> frame:: Audio {
142- let out_channels = self . channels . min ( max_channels) ;
149+ // Use actual channel count for parsing input data (at least 1 to avoid div by zero)
150+ let input_channels = self . channels . max ( 1 ) ;
151+ // Clamp output channels to both max_channels and MAX_AUDIO_CHANNELS for FFmpeg compatibility
152+ let out_channels = input_channels
153+ . min ( max_channels. max ( 1 ) )
154+ . min ( Self :: MAX_AUDIO_CHANNELS as usize ) ;
143155
144156 let sample_size = self . sample_size ( ) ;
145- let packed_sample_size = sample_size * self . channels ;
157+ let packed_sample_size = sample_size * input_channels ;
146158 let samples = packed_data. len ( ) / packed_sample_size;
147159
148160 let mut frame = frame:: Audio :: new (
@@ -152,12 +164,10 @@ impl AudioInfo {
152164 ) ;
153165 frame. set_rate ( self . sample_rate ) ;
154166
155- if self . channels == 0 {
156- unreachable ! ( )
157- } else if self . channels == 1 || ( frame. is_packed ( ) && self . channels <= out_channels) {
167+ if input_channels == 1 || ( frame. is_packed ( ) && input_channels <= out_channels) {
158168 // frame is allocated with parameters derived from packed_data, so this is safe
159169 frame. data_mut ( 0 ) [ 0 ..packed_data. len ( ) ] . copy_from_slice ( packed_data) ;
160- } else if frame. is_packed ( ) && self . channels > out_channels {
170+ } else if frame. is_packed ( ) && input_channels > out_channels {
161171 for ( chunk_index, packed_chunk) in packed_data. chunks ( packed_sample_size) . enumerate ( ) {
162172 let start = chunk_index * sample_size * out_channels;
163173
@@ -200,6 +210,12 @@ impl AudioInfo {
200210 this. channels = this. channels . min ( channels as usize ) ;
201211 this
202212 }
213+
214+ /// Returns a version of this AudioInfo with channels clamped for FFmpeg compatibility.
215+ /// FFmpeg channel layouts only support up to 8 channels (7.1 surround).
216+ pub fn for_ffmpeg_output ( & self ) -> Self {
217+ self . with_max_channels ( Self :: MAX_AUDIO_CHANNELS )
218+ }
203219}
204220
205221pub enum RawVideoFormat {
@@ -394,5 +410,45 @@ mod tests {
394410 assert_eq ! ( & frame. data( 0 ) [ 0 ..2 ] , & [ 1 , 1 ] ) ;
395411 assert_eq ! ( & frame. data( 1 ) [ 0 ..2 ] , & [ 2 , 2 ] ) ;
396412 }
413+
414+ #[ test]
415+ fn channel_layout_returns_valid_layout_for_supported_counts ( ) {
416+ // Test all supported channel counts (1-8)
417+ for channels in 1 ..=8u16 {
418+ let info = AudioInfo :: new_raw ( Sample :: F32 ( Type :: Planar ) , 48000 , channels) ;
419+ // Should not panic and should return a valid layout
420+ let layout = info. channel_layout ( ) ;
421+ assert ! ( !layout. is_empty( ) ) ;
422+ }
423+ }
424+
425+ #[ test]
426+ fn channel_layout_handles_zero_channels ( ) {
427+ // Zero channels should be clamped to 1 (MONO)
428+ let info = AudioInfo :: new_raw ( Sample :: F32 ( Type :: Planar ) , 48000 , 0 ) ;
429+ let layout = info. channel_layout ( ) ;
430+ assert_eq ! ( layout, ChannelLayout :: MONO ) ;
431+ }
432+
433+ #[ test]
434+ fn channel_layout_handles_excessive_channels ( ) {
435+ // More than 8 channels should be clamped to 8 (7.1 surround)
436+ for channels in [ 9 , 10 , 16 , 32 , 64 ] {
437+ let info = AudioInfo :: new_raw ( Sample :: F32 ( Type :: Planar ) , 48000 , channels) ;
438+ let layout = info. channel_layout ( ) ;
439+ assert_eq ! ( layout, ChannelLayout :: _7POINT1) ;
440+ }
441+ }
442+
443+ #[ test]
444+ fn wrap_frame_handles_zero_channels ( ) {
445+ // Zero channels should be treated as mono to avoid division by zero
446+ let info = AudioInfo :: new_raw ( Sample :: U8 ( Type :: Packed ) , 2 , 0 ) ;
447+ let input = & [ 1 , 2 , 3 , 4 ] ;
448+ // This should not panic
449+ let frame = info. wrap_frame ( input) ;
450+ // With effective_channels = 1, all input should be copied as mono
451+ assert_eq ! ( & frame. data( 0 ) [ 0 ..input. len( ) ] , input) ;
452+ }
397453 }
398454}
0 commit comments