use crate::error::{EdlError, EdlResult};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum AudioChannel {
A1,
A2,
A3,
A4,
A5,
A6,
A7,
A8,
Custom(u8),
}
impl AudioChannel {
#[must_use]
pub const fn number(&self) -> u8 {
match self {
Self::A1 => 1,
Self::A2 => 2,
Self::A3 => 3,
Self::A4 => 4,
Self::A5 => 5,
Self::A6 => 6,
Self::A7 => 7,
Self::A8 => 8,
Self::Custom(n) => *n,
}
}
#[must_use]
pub const fn from_number(n: u8) -> Self {
match n {
1 => Self::A1,
2 => Self::A2,
3 => Self::A3,
4 => Self::A4,
5 => Self::A5,
6 => Self::A6,
7 => Self::A7,
8 => Self::A8,
_ => Self::Custom(n),
}
}
#[must_use]
pub fn track_id(&self) -> String {
match self {
Self::A1 => "A".to_string(),
Self::A2 => "A2".to_string(),
Self::A3 => "A3".to_string(),
Self::A4 => "A4".to_string(),
Self::A5 => "A5".to_string(),
Self::A6 => "A6".to_string(),
Self::A7 => "A7".to_string(),
Self::A8 => "A8".to_string(),
Self::Custom(n) => format!("A{n}"),
}
}
}
impl FromStr for AudioChannel {
type Err = EdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim().to_uppercase();
match trimmed.as_str() {
"A" | "A1" => Ok(Self::A1),
"A2" => Ok(Self::A2),
"A3" => Ok(Self::A3),
"A4" => Ok(Self::A4),
"A5" => Ok(Self::A5),
"A6" => Ok(Self::A6),
"A7" => Ok(Self::A7),
"A8" => Ok(Self::A8),
_ => {
if let Some(rest) = trimmed.strip_prefix('A') {
if let Ok(n) = rest.parse::<u8>() {
if n > 0 {
return Ok(Self::from_number(n));
}
}
}
Err(EdlError::InvalidAudioChannel(s.to_string()))
}
}
}
}
impl fmt::Display for AudioChannel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.track_id())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AudioMapping {
pub source: AudioChannel,
pub destination: AudioChannel,
pub gain_db: i16,
pub pan: i8,
}
impl AudioMapping {
#[must_use]
pub const fn new(source: AudioChannel, destination: AudioChannel) -> Self {
Self {
source,
destination,
gain_db: 0,
pan: 0,
}
}
#[must_use]
pub const fn with_gain(source: AudioChannel, destination: AudioChannel, gain_db: i16) -> Self {
Self {
source,
destination,
gain_db,
pan: 0,
}
}
#[must_use]
pub const fn with_pan(source: AudioChannel, destination: AudioChannel, pan: i8) -> Self {
Self {
source,
destination,
gain_db: 0,
pan,
}
}
pub fn set_gain(&mut self, gain_db: i16) {
self.gain_db = gain_db;
}
pub fn set_pan(&mut self, pan: i8) {
self.pan = pan.clamp(-100, 100);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AudioConfig {
pub channel_count: u8,
pub sample_rate: u32,
pub mappings: Vec<AudioMapping>,
}
impl AudioConfig {
#[must_use]
pub const fn new(channel_count: u8, sample_rate: u32) -> Self {
Self {
channel_count,
sample_rate,
mappings: Vec::new(),
}
}
#[must_use]
pub const fn stereo() -> Self {
Self::new(2, 48000)
}
#[must_use]
pub const fn mono() -> Self {
Self::new(1, 48000)
}
#[must_use]
pub const fn surround_5_1() -> Self {
Self::new(6, 48000)
}
pub fn add_mapping(&mut self, mapping: AudioMapping) {
self.mappings.push(mapping);
}
#[must_use]
pub fn get_mappings_for_source(&self, source: AudioChannel) -> Vec<&AudioMapping> {
self.mappings
.iter()
.filter(|m| m.source == source)
.collect()
}
#[must_use]
pub fn get_mappings_for_destination(&self, destination: AudioChannel) -> Vec<&AudioMapping> {
self.mappings
.iter()
.filter(|m| m.destination == destination)
.collect()
}
pub fn validate(&self) -> EdlResult<()> {
if self.channel_count == 0 {
return Err(EdlError::ValidationError(
"Channel count must be greater than 0".to_string(),
));
}
if self.sample_rate == 0 {
return Err(EdlError::ValidationError(
"Sample rate must be greater than 0".to_string(),
));
}
for mapping in &self.mappings {
if mapping.source.number() > self.channel_count {
return Err(EdlError::ValidationError(format!(
"Source channel {} exceeds channel count {}",
mapping.source.number(),
self.channel_count
)));
}
if mapping.destination.number() > self.channel_count {
return Err(EdlError::ValidationError(format!(
"Destination channel {} exceeds channel count {}",
mapping.destination.number(),
self.channel_count
)));
}
}
Ok(())
}
}
impl Default for AudioConfig {
fn default() -> Self {
Self::stereo()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_channel_parsing() {
assert_eq!(
"A".parse::<AudioChannel>()
.expect("operation should succeed"),
AudioChannel::A1
);
assert_eq!(
"A1".parse::<AudioChannel>()
.expect("operation should succeed"),
AudioChannel::A1
);
assert_eq!(
"A2".parse::<AudioChannel>()
.expect("operation should succeed"),
AudioChannel::A2
);
assert_eq!(
"A8".parse::<AudioChannel>()
.expect("operation should succeed"),
AudioChannel::A8
);
}
#[test]
fn test_audio_channel_number() {
assert_eq!(AudioChannel::A1.number(), 1);
assert_eq!(AudioChannel::A2.number(), 2);
assert_eq!(AudioChannel::A8.number(), 8);
assert_eq!(AudioChannel::Custom(16).number(), 16);
}
#[test]
fn test_audio_channel_from_number() {
assert_eq!(AudioChannel::from_number(1), AudioChannel::A1);
assert_eq!(AudioChannel::from_number(2), AudioChannel::A2);
assert_eq!(AudioChannel::from_number(16), AudioChannel::Custom(16));
}
#[test]
fn test_audio_channel_track_id() {
assert_eq!(AudioChannel::A1.track_id(), "A");
assert_eq!(AudioChannel::A2.track_id(), "A2");
assert_eq!(AudioChannel::Custom(16).track_id(), "A16");
}
#[test]
fn test_audio_mapping() {
let mapping = AudioMapping::new(AudioChannel::A1, AudioChannel::A2);
assert_eq!(mapping.source, AudioChannel::A1);
assert_eq!(mapping.destination, AudioChannel::A2);
assert_eq!(mapping.gain_db, 0);
assert_eq!(mapping.pan, 0);
}
#[test]
fn test_audio_config_stereo() {
let config = AudioConfig::stereo();
assert_eq!(config.channel_count, 2);
assert_eq!(config.sample_rate, 48000);
assert!(config.validate().is_ok());
}
#[test]
fn test_audio_config_mappings() {
let mut config = AudioConfig::stereo();
config.add_mapping(AudioMapping::new(AudioChannel::A1, AudioChannel::A2));
let mappings = config.get_mappings_for_source(AudioChannel::A1);
assert_eq!(mappings.len(), 1);
assert_eq!(mappings[0].destination, AudioChannel::A2);
}
#[test]
fn test_audio_config_validation() {
let mut config = AudioConfig::new(2, 48000);
config.add_mapping(AudioMapping::new(
AudioChannel::Custom(10),
AudioChannel::A1,
));
assert!(config.validate().is_err());
}
}