Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling mixing with discrete channel hardware #16

Merged
merged 5 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
padenot marked this conversation as resolved.
Show resolved Hide resolved
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
Loading