#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum AudioFormat {
#[default]
Unknown,
Wav,
Mp3,
Flac,
Ogg,
Aac,
Opus,
Pcm,
}
impl AudioFormat {
pub fn extension(&self) -> &'static str {
match self {
AudioFormat::Unknown => "bin",
AudioFormat::Wav => "wav",
AudioFormat::Mp3 => "mp3",
AudioFormat::Flac => "flac",
AudioFormat::Ogg => "ogg",
AudioFormat::Aac => "aac",
AudioFormat::Opus => "opus",
AudioFormat::Pcm => "pcm",
}
}
pub fn mime_type(&self) -> &'static str {
match self {
AudioFormat::Unknown => "application/octet-stream",
AudioFormat::Wav => "audio/wav",
AudioFormat::Mp3 => "audio/mpeg",
AudioFormat::Flac => "audio/flac",
AudioFormat::Ogg => "audio/ogg",
AudioFormat::Aac => "audio/aac",
AudioFormat::Opus => "audio/opus",
AudioFormat::Pcm => "audio/l16",
}
}
pub fn is_lossy(&self) -> bool {
matches!(self, AudioFormat::Mp3 | AudioFormat::Aac | AudioFormat::Ogg | AudioFormat::Opus)
}
pub fn is_uncompressed(&self) -> bool {
matches!(self, AudioFormat::Wav | AudioFormat::Pcm)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SampleFormat {
U8,
I16,
I24,
I32,
F32,
}
impl SampleFormat {
pub fn bytes_per_sample(&self) -> usize {
match self {
SampleFormat::U8 => 1,
SampleFormat::I16 => 2,
SampleFormat::I24 => 3,
SampleFormat::I32 => 4,
SampleFormat::F32 => 4,
}
}
pub fn to_f32(&self, data: &[u8]) -> Vec<f32> {
match self {
SampleFormat::U8 => data.iter().map(|&b| (b as f32 / 255.0) * 2.0 - 1.0).collect(),
SampleFormat::I16 => data
.as_chunks::<2>()
.0
.iter()
.map(|c| i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0)
.collect(),
SampleFormat::I24 => data
.as_chunks::<3>()
.0
.iter()
.map(|c| {
let val = (c[0] as i32) | ((c[1] as i32) << 8) | ((c[2] as i32) << 16);
((val << 8) >> 8) as f32 / 8388608.0
})
.collect(),
SampleFormat::I32 => data
.as_chunks::<4>()
.0
.iter()
.map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f32 / 2147483648.0)
.collect(),
SampleFormat::F32 => data
.as_chunks::<4>()
.0
.iter()
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_format_extensions() {
assert_eq!(AudioFormat::Wav.extension(), "wav");
assert_eq!(AudioFormat::Mp3.extension(), "mp3");
assert_eq!(AudioFormat::Flac.extension(), "flac");
assert_eq!(AudioFormat::Ogg.extension(), "ogg");
}
#[test]
fn test_audio_format_mime() {
assert_eq!(AudioFormat::Wav.mime_type(), "audio/wav");
assert_eq!(AudioFormat::Mp3.mime_type(), "audio/mpeg");
assert_eq!(AudioFormat::Flac.mime_type(), "audio/flac");
}
#[test]
fn test_sample_format_u8_to_f32() {
let data = vec![0u8, 128, 255];
let samples = SampleFormat::U8.to_f32(&data);
assert!((samples[0] - (-1.0)).abs() < 0.01);
assert!((samples[1] - 0.0).abs() < 0.01);
assert!((samples[2] - 1.0).abs() < 0.01);
}
#[test]
fn test_sample_format_i16_to_f32() {
let data = vec![0u8, 0, 0, 0x80, 0xFF, 0x7F];
let samples = SampleFormat::I16.to_f32(&data);
assert!((samples[0] - 0.0).abs() < 0.01);
assert!((samples[2] - 0.999).abs() < 0.02);
}
#[test]
fn test_sample_format_i24_full_scale() {
let neg = [0x00u8, 0x00, 0x80];
let samples = SampleFormat::I24.to_f32(&neg);
assert!((samples[0] + 1.0).abs() < 1e-6, "got {}", samples[0]);
let pos = [0xFFu8, 0xFF, 0x7F];
let samples = SampleFormat::I24.to_f32(&pos);
assert!((samples[0] - 1.0).abs() < 1e-6, "got {}", samples[0]);
}
#[test]
fn test_sample_format_i24_lsb_and_zero() {
let one = [0x01u8, 0x00, 0x00];
let samples = SampleFormat::I24.to_f32(&one);
assert!((samples[0] - 1.0 / 8388608.0).abs() < 1e-9, "got {}", samples[0]);
let neg_one = [0xFFu8, 0xFF, 0xFF];
let samples = SampleFormat::I24.to_f32(&neg_one);
assert!((samples[0] + 1.0 / 8388608.0).abs() < 1e-9, "got {}", samples[0]);
let zero = [0u8, 0, 0];
let samples = SampleFormat::I24.to_f32(&zero);
assert_eq!(samples[0], 0.0);
}
}