#![allow(dead_code)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AudioLayout {
Mono,
Stereo,
TwoPointOne,
Quad,
FivePointZero,
FivePointOne,
SevenPointOne,
}
impl AudioLayout {
#[must_use]
pub fn channel_count(self) -> u8 {
match self {
Self::Mono => 1,
Self::Stereo => 2,
Self::TwoPointOne => 3,
Self::Quad => 4,
Self::FivePointZero => 5,
Self::FivePointOne => 6,
Self::SevenPointOne => 8,
}
}
#[must_use]
pub fn has_lfe(self) -> bool {
matches!(
self,
Self::TwoPointOne | Self::FivePointOne | Self::SevenPointOne
)
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Mono => "mono",
Self::Stereo => "stereo",
Self::TwoPointOne => "2.1",
Self::Quad => "4.0",
Self::FivePointZero => "5.0",
Self::FivePointOne => "5.1",
Self::SevenPointOne => "7.1",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioTranscodeParams {
pub input_layout: AudioLayout,
pub output_layout: AudioLayout,
pub input_sample_rate: u32,
pub output_sample_rate: u32,
pub target_bitrate_bps: u32,
pub gain_linear: f32,
pub normalise_loudness: bool,
}
impl Default for AudioTranscodeParams {
fn default() -> Self {
Self {
input_layout: AudioLayout::Stereo,
output_layout: AudioLayout::Stereo,
input_sample_rate: 48_000,
output_sample_rate: 48_000,
target_bitrate_bps: 128_000,
gain_linear: 1.0,
normalise_loudness: false,
}
}
}
impl AudioTranscodeParams {
#[must_use]
pub fn stereo() -> Self {
Self::default()
}
#[must_use]
pub fn mono_downmix() -> Self {
Self {
output_layout: AudioLayout::Mono,
..Self::default()
}
}
#[must_use]
pub fn is_passthrough(&self) -> bool {
self.input_layout == self.output_layout
&& self.input_sample_rate == self.output_sample_rate
&& (self.gain_linear - 1.0).abs() < f32::EPSILON
&& !self.normalise_loudness
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.input_sample_rate > 0
&& self.output_sample_rate > 0
&& self.target_bitrate_bps > 0
&& self.gain_linear >= 0.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioChannelMap {
input_layout: AudioLayout,
output_layout: AudioLayout,
routes: Vec<(usize, f32)>,
}
impl AudioChannelMap {
#[must_use]
pub fn identity(layout: AudioLayout) -> Self {
let n = layout.channel_count() as usize;
let routes = (0..n).map(|i| (i, 1.0_f32)).collect();
Self {
input_layout: layout,
output_layout: layout,
routes,
}
}
#[must_use]
pub fn stereo_to_mono() -> Self {
Self {
input_layout: AudioLayout::Stereo,
output_layout: AudioLayout::Mono,
routes: vec![(0, 0.707_107), (1, 0.707_107)],
}
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn compute_gain_db(&self, output_channel: usize) -> Option<f64> {
let (_, gain_linear) = self.routes.get(output_channel)?;
if *gain_linear <= 0.0 {
return Some(f64::NEG_INFINITY);
}
let db = 20.0 * f64::from(*gain_linear).log10();
Some(db)
}
pub fn validate_params(&self) -> Result<(), String> {
let expected_out = self.output_layout.channel_count() as usize;
if self.routes.len() < expected_out {
return Err(format!(
"route count {} is less than output channel count {}",
self.routes.len(),
expected_out
));
}
let max_in = self.input_layout.channel_count() as usize;
for (ch_idx, _) in &self.routes {
if *ch_idx >= max_in {
return Err(format!(
"input channel index {ch_idx} out of range (input has {max_in} channels)"
));
}
}
Ok(())
}
#[must_use]
pub fn output_channel_count(&self) -> usize {
self.output_layout.channel_count() as usize
}
pub fn apply(&self, input: &[f32], output: &mut [f32], frame_size: usize) {
let in_ch = self.input_layout.channel_count() as usize;
let out_ch = self.output_layout.channel_count() as usize;
for frame in 0..frame_size {
for (out_idx, (in_idx, gain)) in self.routes.iter().enumerate() {
let in_pos = frame * in_ch + in_idx;
let out_pos = frame * out_ch + out_idx;
if in_pos < input.len() && out_pos < output.len() {
output[out_pos] += input[in_pos] * gain;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mono_channel_count() {
assert_eq!(AudioLayout::Mono.channel_count(), 1);
}
#[test]
fn test_stereo_channel_count() {
assert_eq!(AudioLayout::Stereo.channel_count(), 2);
}
#[test]
fn test_five_one_channel_count() {
assert_eq!(AudioLayout::FivePointOne.channel_count(), 6);
}
#[test]
fn test_seven_one_channel_count() {
assert_eq!(AudioLayout::SevenPointOne.channel_count(), 8);
}
#[test]
fn test_has_lfe() {
assert!(AudioLayout::FivePointOne.has_lfe());
assert!(AudioLayout::TwoPointOne.has_lfe());
assert!(!AudioLayout::Stereo.has_lfe());
assert!(!AudioLayout::Quad.has_lfe());
}
#[test]
fn test_labels_non_empty() {
let layouts = [
AudioLayout::Mono,
AudioLayout::Stereo,
AudioLayout::TwoPointOne,
AudioLayout::Quad,
AudioLayout::FivePointZero,
AudioLayout::FivePointOne,
AudioLayout::SevenPointOne,
];
for l in layouts {
assert!(!l.label().is_empty());
}
}
#[test]
fn test_params_is_passthrough_default() {
let p = AudioTranscodeParams::default();
assert!(p.is_passthrough());
}
#[test]
fn test_params_not_passthrough_different_layout() {
let p = AudioTranscodeParams {
output_layout: AudioLayout::Mono,
..AudioTranscodeParams::default()
};
assert!(!p.is_passthrough());
}
#[test]
fn test_params_not_passthrough_with_gain() {
let p = AudioTranscodeParams {
gain_linear: 1.5,
..AudioTranscodeParams::default()
};
assert!(!p.is_passthrough());
}
#[test]
fn test_params_is_valid_default() {
assert!(AudioTranscodeParams::default().is_valid());
}
#[test]
fn test_params_invalid_zero_sample_rate() {
let p = AudioTranscodeParams {
input_sample_rate: 0,
..AudioTranscodeParams::default()
};
assert!(!p.is_valid());
}
#[test]
fn test_identity_map_validate() {
let m = AudioChannelMap::identity(AudioLayout::Stereo);
assert!(m.validate_params().is_ok());
}
#[test]
fn test_stereo_to_mono_validate() {
let m = AudioChannelMap::stereo_to_mono();
assert!(m.validate_params().is_ok());
}
#[test]
fn test_compute_gain_db_unity() {
let m = AudioChannelMap::identity(AudioLayout::Stereo);
let db = m.compute_gain_db(0).expect("should succeed in test");
assert!(
(db - 0.0).abs() < 1e-9,
"unity gain should be 0 dB, got {db}"
);
}
#[test]
fn test_compute_gain_db_stereo_to_mono() {
let m = AudioChannelMap::stereo_to_mono();
let db = m.compute_gain_db(0).expect("should succeed in test");
assert!((db + 3.01).abs() < 0.1, "expected ~-3 dB, got {db}");
}
#[test]
fn test_compute_gain_db_out_of_range() {
let m = AudioChannelMap::identity(AudioLayout::Mono);
assert!(m.compute_gain_db(5).is_none());
}
#[test]
fn test_apply_identity() {
let m = AudioChannelMap::identity(AudioLayout::Stereo);
let input = vec![0.5_f32, 0.8, 0.3, 0.1];
let mut output = vec![0.0_f32; 4];
m.apply(&input, &mut output, 2);
assert!((output[0] - 0.5).abs() < 1e-6);
assert!((output[1] - 0.8).abs() < 1e-6);
}
#[test]
fn test_output_channel_count() {
let m = AudioChannelMap::stereo_to_mono();
assert_eq!(m.output_channel_count(), 1);
}
}