Skip to content

Commit 65daf44

Browse files
committed
fix: handle high channel count audio devices (e.g., 34-channel interfaces)
Problem: Cap crashed or produced chipmunk audio on systems with professional audio interfaces that report many channels (like RME Digiface with 34 outputs). Root causes: 1. channel_layout() panicked when channel count exceeded 8 (FFmpeg's max) 2. Audio data was processed assuming device's channel count, but FFmpeg can only handle up to 8 channels, causing buffer size mismatches 3. Output audio stream was configured for 34 channels but only 8 channels of data were being sent, causing ~4x playback speed (chipmunk effect) Fixes: - AudioInfo now stores actual device channel count for correct data parsing - Added for_ffmpeg_output() helper that clamps channels to 8 for FFmpeg ops - channel_layout() gracefully handles any channel count by clamping - AudioResampler, AudioPlaybackBuffer, and PrerenderedAudioBuffer all clamp - Output stream config now matches clamped channel count The key insight: we need the real channel count to correctly parse interleaved audio data from the device, but must clamp to 8 channels for FFmpeg processing and audio output streams.
1 parent 58009a5 commit 65daf44

File tree

3 files changed

+46
-16
lines changed

3 files changed

+46
-16
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: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub enum AudioInfoError {
2626
impl AudioInfo {
2727
/// Maximum number of audio channels supported by FFmpeg channel layouts.
2828
/// 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.
2931
pub const MAX_AUDIO_CHANNELS: u16 = 8;
3032

3133
pub const fn new(
@@ -71,18 +73,16 @@ impl AudioInfo {
7173
SupportedBufferSize::Unknown => 1024,
7274
});
7375

74-
let raw_channels = config.channels();
75-
let channels = if Self::channel_layout_raw(raw_channels).is_some() {
76-
raw_channels
77-
} else {
78-
raw_channels.clamp(1, Self::MAX_AUDIO_CHANNELS)
79-
};
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
8081

8182
Self {
8283
sample_format,
8384
sample_rate: config.sample_rate().0,
84-
// we do this here and only here bc we know it's cpal-related
85-
channels: channels.into(),
85+
channels: raw_channels.into(),
8686
time_base: FFRational(1, 1_000_000),
8787
buffer_size,
8888
}
@@ -146,13 +146,15 @@ impl AudioInfo {
146146
packed_data: &[u8],
147147
max_channels: usize,
148148
) -> frame::Audio {
149-
// Handle 0 channels by treating as mono to avoid division by zero
150-
// and unreachable code paths. This can happen with misconfigured audio devices.
151-
let effective_channels = self.channels.max(1);
152-
let out_channels = effective_channels.min(max_channels.max(1));
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);
153155

154156
let sample_size = self.sample_size();
155-
let packed_sample_size = sample_size * effective_channels;
157+
let packed_sample_size = sample_size * input_channels;
156158
let samples = packed_data.len() / packed_sample_size;
157159

158160
let mut frame = frame::Audio::new(
@@ -162,10 +164,10 @@ impl AudioInfo {
162164
);
163165
frame.set_rate(self.sample_rate);
164166

165-
if effective_channels == 1 || (frame.is_packed() && effective_channels <= out_channels) {
167+
if input_channels == 1 || (frame.is_packed() && input_channels <= out_channels) {
166168
// frame is allocated with parameters derived from packed_data, so this is safe
167169
frame.data_mut(0)[0..packed_data.len()].copy_from_slice(packed_data);
168-
} else if frame.is_packed() && effective_channels > out_channels {
170+
} else if frame.is_packed() && input_channels > out_channels {
169171
for (chunk_index, packed_chunk) in packed_data.chunks(packed_sample_size).enumerate() {
170172
let start = chunk_index * sample_size * out_channels;
171173

@@ -208,6 +210,12 @@ impl AudioInfo {
208210
this.channels = this.channels.min(channels as usize);
209211
this
210212
}
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+
}
211219
}
212220

213221
pub enum RawVideoFormat {

0 commit comments

Comments
 (0)