Skip to content

Commit 7981751

Browse files
Merge pull request #1536 from noeljackson/fix/channel-layout-panic
fix: prevent panic in AudioInfo::channel_layout() for unsupported channel counts
2 parents bff9782 + 65daf44 commit 7981751

File tree

3 files changed

+95
-17
lines changed

3 files changed

+95
-17
lines changed

crates/editor/src/audio.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ impl<T: FromSampleBytes> AudioPlaybackBuffer<T> {
264264
const PROCESSING_SAMPLES_COUNT: u32 = 1024;
265265

266266
pub fn new(data: Vec<AudioSegment>, output_info: AudioInfo) -> Self {
267+
// Clamp output info for FFmpeg compatibility (max 8 channels)
268+
let output_info = output_info.for_ffmpeg_output();
269+
267270
info!(
268271
sample_rate = output_info.sample_rate,
269272
channels = output_info.channels,
@@ -391,6 +394,9 @@ pub struct AudioResampler {
391394

392395
impl AudioResampler {
393396
pub fn new(output_info: AudioInfo) -> Result<Self, MediaError> {
397+
// Clamp output info for FFmpeg compatibility (max 8 channels)
398+
let output_info = output_info.for_ffmpeg_output();
399+
394400
let mut options = Dictionary::new();
395401
options.set("filter_size", "128");
396402
options.set("cutoff", "0.97");
@@ -460,6 +466,10 @@ impl<T: FromSampleBytes> PrerenderedAudioBuffer<T> {
460466
output_info: AudioInfo,
461467
duration_secs: f64,
462468
) -> Self {
469+
// Clamp output info for FFmpeg compatibility (max 8 channels)
470+
// The resampler will produce audio with this channel count
471+
let output_info = output_info.for_ffmpeg_output();
472+
463473
info!(
464474
duration_secs = duration_secs,
465475
sample_rate = output_info.sample_rate,

crates/editor/src/playback.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,13 @@ impl AudioPlayback {
940940
}
941941
};
942942

943+
// Clamp output info for FFmpeg compatibility (max 8 channels)
944+
// This must match what AudioPlaybackBuffer will use internally
945+
base_output_info = base_output_info.for_ffmpeg_output();
946+
947+
// Also update the stream config to match the clamped channels
948+
config.channels = base_output_info.channels as u16;
949+
943950
let sample_rate = base_output_info.sample_rate;
944951
let buffer_size = base_output_info.buffer_size;
945952
let channels = base_output_info.channels;
@@ -1159,8 +1166,13 @@ impl AudioPlayback {
11591166

11601167
let mut output_info = AudioInfo::from_stream_config(&supported_config);
11611168
output_info.sample_format = output_info.sample_format.packed();
1169+
// Clamp output info for FFmpeg compatibility (max 8 channels)
1170+
output_info = output_info.for_ffmpeg_output();
1171+
1172+
let mut config = supported_config.config();
1173+
// Match stream config channels to clamped output info
1174+
config.channels = output_info.channels as u16;
11621175

1163-
let config = supported_config.config();
11641176
let sample_rate = output_info.sample_rate;
11651177

11661178
let playhead = f64::from(start_frame_number) / f64::from(fps);

crates/media-info/src/lib.rs

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ pub enum AudioInfoError {
2424
}
2525

2626
impl 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

205221
pub 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

Comments
 (0)