diff --git a/Cargo.toml b/Cargo.toml index 8b82b6d..50f24cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "audio-mixer" description = "Mixing audio by the input and output channel layout" -version = "0.1.3" +version = "0.2.0" authors = ["Chun-Min Chang "] license = "MPL-2.0" repository = "https://github.com/mozilla/audio-mixer" diff --git a/src/channel.rs b/src/channel.rs index b329368..d913df3 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -21,6 +21,7 @@ pub enum Channel { TopBackCenter = 16, TopBackRight = 17, Silence = 18, + Discrete = 19, // To be used based on its index } impl Channel { @@ -29,7 +30,7 @@ impl Channel { } pub const fn count() -> usize { - Channel::Silence as usize + 1 + Channel::Discrete as usize + 1 } pub const fn bitmask(self) -> u32 { @@ -58,6 +59,7 @@ bitflags! { const TOP_BACK_CENTER = Channel::TopBackCenter.bitmask(); const TOP_BACK_RIGHT = Channel::TopBackRight.bitmask(); const SILENCE = Channel::Silence.bitmask(); + const DISCRETE = Channel::Discrete.bitmask(); } } diff --git a/src/coefficient.rs b/src/coefficient.rs index 799d502..c7999ee 100644 --- a/src/coefficient.rs +++ b/src/coefficient.rs @@ -9,7 +9,7 @@ const CHANNELS: usize = Channel::count(); #[derive(Debug)] enum Error { - DuplicateNonSilenceChannel, + DuplicateChannel, AsymmetricChannels, } @@ -28,13 +28,15 @@ impl ChannelLayout { }) } - // Except Silence channel, the duplicate channels are not allowed. + // Except Silence and Discrete channels, duplicate channels aren't allowed. fn get_channel_map(channels: &[Channel]) -> Result { let mut map = ChannelMap::empty(); for channel in channels { let bitmask = ChannelMap::from(*channel); - if channel != &Channel::Silence && map.contains(bitmask) { - return Err(Error::DuplicateNonSilenceChannel); + if (channel != &Channel::Silence && channel != &Channel::Discrete) + && map.contains(bitmask) + { + return Err(Error::DuplicateChannel); } map.insert(bitmask); } @@ -83,15 +85,41 @@ where let input_layout = ChannelLayout::new(input_channels).expect("Invalid input layout"); let output_layout = ChannelLayout::new(output_channels).expect("Invalid output layout"); - let mixing_matrix = - Self::build_mixing_matrix(input_layout.channel_map, output_layout.channel_map) - .unwrap_or_else(|_| Self::get_basic_matrix()); - - let coefficient_matrix = Self::pick_coefficients( - &input_layout.channels, - &output_layout.channels, - &mixing_matrix, - ); + // Check if this is a professional audio interface rather than a sound card for playback, in + // which case it is expected to simply pass all the channel through without change. + // Those interfaces only have an explicit mapping for the stereo pair, but have lots of channels. + let mut only_stereo_or_discrete = true; + for channel in output_channels { + if *channel != Channel::Discrete + && *channel != Channel::FrontLeft + && *channel != Channel::FrontRight + { + only_stereo_or_discrete = false; + break; + } + } + let coefficient_matrix = if only_stereo_or_discrete && output_channels.len() > 2 { + let mut matrix = Vec::with_capacity(output_channels.len()); + // Create a diagonal line of 1.0 for input channels + for (output_channel_index, _) in output_channels.iter().enumerate() { + let mut coefficients = Vec::with_capacity(input_channels.len()); + coefficients.resize(input_channels.len(), 0.0); + if output_channel_index < coefficients.len() { + coefficients[output_channel_index] = 1.0; + } + matrix.push(coefficients); + } + matrix + } else { + let mixing_matrix = + Self::build_mixing_matrix(input_layout.channel_map, output_layout.channel_map) + .unwrap_or_else(|_| Self::get_basic_matrix()); + Self::pick_coefficients( + &input_layout.channels, + &output_layout.channels, + &mixing_matrix, + ) + }; let normalized_matrix = Self::normalize(T::max_coefficients_sum(), coefficient_matrix); @@ -430,7 +458,7 @@ impl MixingCoefficient for f32 { type Coef = f32; fn max_coefficients_sum() -> f64 { - f64::from(std::i32::MAX) + f64::from(i32::MAX) } fn coefficient_from_f64(value: f64) -> Self::Coef { @@ -550,12 +578,12 @@ mod test { #[test] fn test_create_with_duplicate_silience_channels_f32() { - test_create_with_duplicate_silience_channels::() + test_create_with_duplicate_channels::() } #[test] fn test_create_with_duplicate_silience_channels_i16() { - test_create_with_duplicate_silience_channels::() + test_create_with_duplicate_channels::() } #[test] @@ -582,7 +610,7 @@ mod test { test_create_with_duplicate_output_channels::() } - fn test_create_with_duplicate_silience_channels() + fn test_create_with_duplicate_channels() where T: MixingCoefficient, T::Coef: Copy, @@ -649,78 +677,155 @@ mod test { } #[test] - fn test_get_redirect_matrix_f32() { - test_get_redirect_matrix::(); + fn test_get_discrete_mapping() { + test_get_discrete_mapping_matrix::(); + test_get_discrete_mapping_matrix::(); } #[test] - fn test_get_redirect_matrix_i16() { - test_get_redirect_matrix::(); + fn test_get_discrete_mapping_too_many_channels() { + test_get_discrete_mapping_matrix_too_many_channels::(); + test_get_discrete_mapping_matrix_too_many_channels::(); } - fn test_get_redirect_matrix() - where + #[test] + fn test_get_regular_mapping_too_many_channels() { + test_get_regular_mapping_matrix_too_many_channels::(); + test_get_regular_mapping_matrix_too_many_channels::(); + } + + // Check that a matrix is diagonal (1.0 on the diagnoal, 0.0 elsewhere). It's valid to have more input or output channels + fn assert_is_diagonal( + coefficients: &Coefficient, + input_channels: usize, + output_channels: usize, + ) where T: MixingCoefficient, T::Coef: Copy + Debug + PartialEq, { - // Create a matrix that only redirect the channels from input side to output side, - // without mixing input audio data to output audio data. - fn compute_redirect_matrix( - input_channels: &[Channel], - output_channels: &[Channel], - ) -> Vec> - where - T: MixingCoefficient, - { - let mut matrix = Vec::with_capacity(output_channels.len()); - for output_channel in output_channels { - let mut row = Vec::with_capacity(input_channels.len()); - for input_channel in input_channels { - row.push( - if input_channel != output_channel - || input_channel == &Channel::Silence - || output_channel == &Channel::Silence - { - 0.0 - } else { - 1.0 - }, - ); + for i in 0..input_channels { + for j in 0..output_channels { + if i == j { + assert_eq!(coefficients.get(i, j), T::coefficient_from_f64(1.0)); + } else { + assert_eq!(coefficients.get(i, j), T::coefficient_from_f64(0.0)); } - matrix.push(row); } - - // Convert the type of the coefficients from f64 to T::Coef. - matrix - .into_iter() - .map(|row| row.into_iter().map(T::coefficient_from_f64).collect()) - .collect() } + println!( + "{:?} = {:?} * {:?}", + output_channels, coefficients.matrix, input_channels + ); + } + fn test_get_discrete_mapping_matrix() + where + T: MixingCoefficient, + T::Coef: Copy + Debug + PartialEq, + { + // typical 5.1 let input_channels = [ Channel::FrontLeft, - Channel::Silence, Channel::FrontRight, Channel::FrontCenter, + Channel::BackLeft, + Channel::BackRight, + Channel::LowFrequency, ]; + // going into 8 channels with a tagged stereo pair and discrete channels let output_channels = [ - Channel::Silence, Channel::FrontLeft, - Channel::Silence, + Channel::FrontRight, + Channel::Discrete, + Channel::Discrete, + Channel::Discrete, + Channel::Discrete, + Channel::Discrete, + Channel::Discrete, + ]; + + // Get a pass-through matrix in the first 6 channels + let coefficients = Coefficient::::create(&input_channels, &output_channels); + assert_is_diagonal::(&coefficients, input_channels.len(), output_channels.len()); + } + + fn test_get_discrete_mapping_matrix_too_many_channels() + where + T: MixingCoefficient, + T::Coef: Copy + Debug + PartialEq, + { + // 5.1.4 + let input_channels = [ + Channel::FrontLeft, + Channel::FrontRight, Channel::FrontCenter, - Channel::BackCenter, + Channel::LowFrequency, + Channel::FrontLeftOfCenter, + Channel::FrontRightOfCenter, + Channel::TopFrontLeft, + Channel::TopFrontRight, + Channel::BackLeft, + Channel::BackRight, + ]; + // going into 8 channels with a tagged stereo pair and discrete channels + let output_channels = [ + Channel::FrontLeft, + Channel::FrontRight, + Channel::Discrete, + Channel::Discrete, + Channel::Discrete, + Channel::Discrete, + Channel::Discrete, + Channel::Discrete, ]; - // Get a redirect matrix since the output layout is asymmetric. - let coefficient = Coefficient::::create(&input_channels, &output_channels); + // First 8 channels are to be played, last two are to be dropped. + let coefficients = Coefficient::::create(&input_channels, &output_channels); + assert_is_diagonal(&coefficients, input_channels.len(), output_channels.len()); + } + + fn test_get_regular_mapping_matrix_too_many_channels() + where + T: MixingCoefficient, + T::Coef: Copy + Debug + PartialEq, + { + // 5.1.4 + let input_channels = [ + Channel::FrontLeft, + Channel::FrontRight, + Channel::FrontCenter, + Channel::LowFrequency, + Channel::FrontLeftOfCenter, + Channel::FrontRightOfCenter, + Channel::TopFrontLeft, + Channel::TopFrontRight, + Channel::BackLeft, + Channel::BackRight, + ]; + // going into a regular 5.1 sound card + let output_channels = [ + Channel::FrontLeft, + Channel::FrontRight, + Channel::FrontCenter, + Channel::LowFrequency, + Channel::BackLeft, + Channel::BackRight, + ]; - let expected = compute_redirect_matrix::(&input_channels, &output_channels); - assert_eq!(coefficient.matrix, expected); + let coefficients = Coefficient::::create(&input_channels, &output_channels); - println!( - "{:?} = {:?} * {:?}", - output_channels, coefficient.matrix, input_channels - ); + // Non-unity gain non-silence coefficients must be present when down mixing. + let mut found_non_unity_non_silence = false; + for row in coefficients.matrix.iter() { + for coeff in row.iter() { + if T::coefficient_from_f64(1.0) != *coeff || T::coefficient_from_f64(0.0) != *coeff + { + found_non_unity_non_silence = true; + break; + } + } + } + assert!(found_non_unity_non_silence); } #[test] @@ -732,7 +837,7 @@ mod test { vec![4.0_f64, 6.0_f64, 10.0_f64], ]; - let mut max_row_sum: f64 = std::f64::MIN; + let mut max_row_sum: f64 = f64::MIN; for row in &m { max_row_sum = max_row_sum.max(row.iter().sum()); } @@ -746,7 +851,7 @@ mod test { let smaller_max = max_row_sum - 0.5_f64; assert!(smaller_max > 0.0_f64); let n = Coefficient::::normalize(smaller_max, m); - let mut max_row_sum: f64 = std::f64::MIN; + let mut max_row_sum: f64 = f64::MIN; for row in &n { max_row_sum = max_row_sum.max(row.iter().sum()); assert!(row.iter().sum::() <= smaller_max);