Skip to content

Commit

Permalink
Merge pull request #16 from mozilla/discrete-channels-fix
Browse files Browse the repository at this point in the history
Handling mixing with discrete channel hardware
  • Loading branch information
padenot authored Apr 22, 2024
2 parents 497d0a2 + d6e21b4 commit 29b5b45
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 69 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <chun.m.chang@gmail.com>"]
license = "MPL-2.0"
repository = "https://github.com/mozilla/audio-mixer"
Expand Down
4 changes: 3 additions & 1 deletion src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum Channel {
TopBackCenter = 16,
TopBackRight = 17,
Silence = 18,
Discrete = 19, // To be used based on its index
}

impl Channel {
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}
}

Expand Down
239 changes: 172 additions & 67 deletions src/coefficient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const CHANNELS: usize = Channel::count();

#[derive(Debug)]
enum Error {
DuplicateNonSilenceChannel,
DuplicateChannel,
AsymmetricChannels,
}

Expand All @@ -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<ChannelMap, Error> {
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);
}
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -550,12 +578,12 @@ mod test {

#[test]
fn test_create_with_duplicate_silience_channels_f32() {
test_create_with_duplicate_silience_channels::<f32>()
test_create_with_duplicate_channels::<f32>()
}

#[test]
fn test_create_with_duplicate_silience_channels_i16() {
test_create_with_duplicate_silience_channels::<i16>()
test_create_with_duplicate_channels::<i16>()
}

#[test]
Expand All @@ -582,7 +610,7 @@ mod test {
test_create_with_duplicate_output_channels::<i16>()
}

fn test_create_with_duplicate_silience_channels<T>()
fn test_create_with_duplicate_channels<T>()
where
T: MixingCoefficient,
T::Coef: Copy,
Expand Down Expand Up @@ -649,78 +677,155 @@ mod test {
}

#[test]
fn test_get_redirect_matrix_f32() {
test_get_redirect_matrix::<f32>();
fn test_get_discrete_mapping() {
test_get_discrete_mapping_matrix::<f32>();
test_get_discrete_mapping_matrix::<i16>();
}

#[test]
fn test_get_redirect_matrix_i16() {
test_get_redirect_matrix::<i16>();
fn test_get_discrete_mapping_too_many_channels() {
test_get_discrete_mapping_matrix_too_many_channels::<i16>();
test_get_discrete_mapping_matrix_too_many_channels::<f32>();
}

fn test_get_redirect_matrix<T>()
where
#[test]
fn test_get_regular_mapping_too_many_channels() {
test_get_regular_mapping_matrix_too_many_channels::<i16>();
test_get_regular_mapping_matrix_too_many_channels::<f32>();
}

// 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<T>(
coefficients: &Coefficient<T>,
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<T>(
input_channels: &[Channel],
output_channels: &[Channel],
) -> Vec<Vec<T::Coef>>
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<T>()
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::<T>::create(&input_channels, &output_channels);
assert_is_diagonal::<T>(&coefficients, input_channels.len(), output_channels.len());
}

fn test_get_discrete_mapping_matrix_too_many_channels<T>()
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::<T>::create(&input_channels, &output_channels);
// First 8 channels are to be played, last two are to be dropped.
let coefficients = Coefficient::<T>::create(&input_channels, &output_channels);
assert_is_diagonal(&coefficients, input_channels.len(), output_channels.len());
}

fn test_get_regular_mapping_matrix_too_many_channels<T>()
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::<T>(&input_channels, &output_channels);
assert_eq!(coefficient.matrix, expected);
let coefficients = Coefficient::<T>::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]
Expand All @@ -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());
}
Expand All @@ -746,7 +851,7 @@ mod test {
let smaller_max = max_row_sum - 0.5_f64;
assert!(smaller_max > 0.0_f64);
let n = Coefficient::<f32>::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::<f64>() <= smaller_max);
Expand Down

0 comments on commit 29b5b45

Please sign in to comment.