use crate::{Result, TranscodeError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioCodecId {
Opus,
Flac,
Vorbis,
Mp3,
Aac,
Pcm,
}
impl AudioCodecId {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Opus => "opus",
Self::Flac => "flac",
Self::Vorbis => "vorbis",
Self::Mp3 => "mp3",
Self::Aac => "aac",
Self::Pcm => "pcm",
}
}
#[must_use]
pub fn is_lossless(self) -> bool {
matches!(self, Self::Flac | Self::Pcm)
}
#[must_use]
pub fn default_bitrate(self) -> u32 {
match self {
Self::Opus => 128_000,
Self::Flac => 0, Self::Vorbis => 128_000,
Self::Mp3 => 192_000,
Self::Aac => 192_000,
Self::Pcm => 0, }
}
}
#[derive(Debug, Clone)]
pub struct AudioOnlyConfig {
pub input_codec: AudioCodecId,
pub output_codec: AudioCodecId,
pub sample_rate: u32,
pub channels: u8,
pub bitrate: u32,
}
impl AudioOnlyConfig {
pub fn new(
input_codec: AudioCodecId,
output_codec: AudioCodecId,
sample_rate: u32,
channels: u8,
bitrate: u32,
) -> Result<Self> {
if channels == 0 || channels > 8 {
return Err(TranscodeError::InvalidInput(format!(
"channels must be 1–8, got {channels}"
)));
}
if sample_rate < 8_000 || sample_rate > 192_000 {
return Err(TranscodeError::InvalidInput(format!(
"sample_rate must be 8000–192000 Hz, got {sample_rate}"
)));
}
Ok(Self {
input_codec,
output_codec,
sample_rate,
channels,
bitrate,
})
}
#[must_use]
pub fn opus_stereo() -> Self {
Self::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 2, 128_000)
.expect("hard-coded opus_stereo config is always valid")
}
#[must_use]
pub fn flac_stereo() -> Self {
Self::new(AudioCodecId::Pcm, AudioCodecId::Flac, 48_000, 2, 0)
.expect("hard-coded flac_stereo config is always valid")
}
}
pub struct AudioOnlyTranscoder {
config: AudioOnlyConfig,
frames_processed: u64,
}
impl AudioOnlyTranscoder {
#[must_use]
pub fn new(config: AudioOnlyConfig) -> Self {
Self {
config,
frames_processed: 0,
}
}
#[must_use]
pub fn config(&self) -> &AudioOnlyConfig {
&self.config
}
#[must_use]
pub fn codec_name(&self) -> &str {
self.config.output_codec.name()
}
#[must_use]
pub fn estimated_bitrate(&self) -> u32 {
if self.config.bitrate == 0 {
self.config.output_codec.default_bitrate()
} else {
self.config.bitrate
}
}
#[must_use]
pub fn frames_processed(&self) -> u64 {
self.frames_processed
}
pub fn transcode_samples(&mut self, input: &[f32]) -> Result<Vec<f32>> {
let ch = self.config.channels as usize;
if ch == 0 {
return Err(TranscodeError::InvalidInput(
"channel count must not be zero".to_string(),
));
}
if input.len() % ch != 0 {
return Err(TranscodeError::InvalidInput(format!(
"input length {} is not a multiple of channel count {}",
input.len(),
ch
)));
}
for (idx, &s) in input.iter().enumerate() {
if s.is_nan() || s.is_infinite() {
return Err(TranscodeError::InvalidInput(format!(
"sample at index {idx} is non-finite: {s}"
)));
}
}
let num_frames = input.len() / ch;
self.frames_processed += num_frames as u64;
let gain = self.codec_gain_factor();
let output: Vec<f32> = input.iter().map(|&s| s * gain).collect();
Ok(output)
}
pub fn reset(&mut self) {
self.frames_processed = 0;
}
pub fn update_config(
&mut self,
input_codec: AudioCodecId,
output_codec: AudioCodecId,
sample_rate: u32,
channels: u8,
bitrate: u32,
) -> Result<()> {
let new_cfg =
AudioOnlyConfig::new(input_codec, output_codec, sample_rate, channels, bitrate)?;
self.config = new_cfg;
Ok(())
}
fn codec_gain_factor(&self) -> f32 {
match self.config.output_codec {
AudioCodecId::Flac | AudioCodecId::Pcm => 1.0, AudioCodecId::Opus => 0.9999,
AudioCodecId::Vorbis => 0.9998,
AudioCodecId::Mp3 => 0.9997,
AudioCodecId::Aac => 0.9996,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codec_id_names() {
assert_eq!(AudioCodecId::Opus.name(), "opus");
assert_eq!(AudioCodecId::Flac.name(), "flac");
assert_eq!(AudioCodecId::Vorbis.name(), "vorbis");
assert_eq!(AudioCodecId::Mp3.name(), "mp3");
assert_eq!(AudioCodecId::Aac.name(), "aac");
assert_eq!(AudioCodecId::Pcm.name(), "pcm");
}
#[test]
fn test_codec_lossless_flag() {
assert!(AudioCodecId::Flac.is_lossless());
assert!(AudioCodecId::Pcm.is_lossless());
assert!(!AudioCodecId::Opus.is_lossless());
assert!(!AudioCodecId::Vorbis.is_lossless());
assert!(!AudioCodecId::Mp3.is_lossless());
assert!(!AudioCodecId::Aac.is_lossless());
}
#[test]
fn test_codec_default_bitrate() {
assert_eq!(AudioCodecId::Flac.default_bitrate(), 0);
assert_eq!(AudioCodecId::Pcm.default_bitrate(), 0);
assert!(AudioCodecId::Opus.default_bitrate() > 0);
assert!(AudioCodecId::Mp3.default_bitrate() > 0);
}
#[test]
fn test_config_valid_creation() {
let cfg = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 2, 128_000);
assert!(cfg.is_ok(), "Valid config should succeed");
let cfg = cfg.expect("already checked");
assert_eq!(cfg.sample_rate, 48_000);
assert_eq!(cfg.channels, 2);
assert_eq!(cfg.bitrate, 128_000);
}
#[test]
fn test_config_invalid_channels_zero() {
let result =
AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 0, 128_000);
assert!(result.is_err(), "channels=0 must fail");
let msg = result.expect_err("expected error").to_string();
assert!(
msg.contains("channels"),
"Error should mention 'channels': {msg}"
);
}
#[test]
fn test_config_invalid_channels_too_many() {
let result =
AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 9, 128_000);
assert!(result.is_err(), "channels=9 must fail");
}
#[test]
fn test_config_invalid_sample_rate_too_low() {
let result = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 7_999, 2, 128_000);
assert!(result.is_err(), "sample_rate=7999 must fail");
let msg = result.expect_err("expected error").to_string();
assert!(
msg.contains("sample_rate"),
"Error should mention 'sample_rate': {msg}"
);
}
#[test]
fn test_config_invalid_sample_rate_too_high() {
let result =
AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 192_001, 2, 128_000);
assert!(result.is_err(), "sample_rate=192001 must fail");
}
#[test]
fn test_config_boundary_sample_rates() {
let low = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Pcm, 8_000, 1, 0);
assert!(low.is_ok(), "sample_rate=8000 should be valid");
let high = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Pcm, 192_000, 1, 0);
assert!(high.is_ok(), "sample_rate=192000 should be valid");
}
#[test]
fn test_config_shortcuts() {
let opus = AudioOnlyConfig::opus_stereo();
assert_eq!(opus.output_codec, AudioCodecId::Opus);
assert_eq!(opus.channels, 2);
assert_eq!(opus.sample_rate, 48_000);
let flac = AudioOnlyConfig::flac_stereo();
assert_eq!(flac.output_codec, AudioCodecId::Flac);
assert_eq!(flac.channels, 2);
assert!(flac.output_codec.is_lossless());
}
#[test]
fn test_transcoder_creation() {
let cfg = AudioOnlyConfig::opus_stereo();
let t = AudioOnlyTranscoder::new(cfg);
assert_eq!(t.codec_name(), "opus");
assert_eq!(t.frames_processed(), 0);
}
#[test]
fn test_transcoder_estimated_bitrate_from_config() {
let cfg = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 2, 256_000)
.expect("valid");
let t = AudioOnlyTranscoder::new(cfg);
assert_eq!(t.estimated_bitrate(), 256_000);
}
#[test]
fn test_transcoder_estimated_bitrate_uses_default_when_zero() {
let cfg = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 2, 0)
.expect("valid");
let t = AudioOnlyTranscoder::new(cfg);
assert_eq!(t.estimated_bitrate(), AudioCodecId::Opus.default_bitrate());
}
#[test]
fn test_transcode_samples_sine_wave() {
let cfg = AudioOnlyConfig::opus_stereo();
let mut t = AudioOnlyTranscoder::new(cfg);
let sample_rate = 48_000.0f32;
let freq = 1_000.0f32;
let num_frames = 100_usize;
let mut input = Vec::with_capacity(num_frames * 2);
for i in 0..num_frames {
let s = 0.5 * (2.0 * std::f32::consts::PI * freq * i as f32 / sample_rate).sin();
input.push(s); input.push(s); }
let result = t.transcode_samples(&input);
assert!(
result.is_ok(),
"transcode_samples must succeed: {:?}",
result.err()
);
let output = result.expect("already checked");
assert_eq!(output.len(), input.len(), "Output length must match input");
assert_eq!(t.frames_processed(), num_frames as u64);
}
#[test]
fn test_transcode_samples_rejects_nan() {
let cfg = AudioOnlyConfig::opus_stereo();
let mut t = AudioOnlyTranscoder::new(cfg);
let input = vec![0.1f32, f32::NAN, 0.3, 0.4];
let result = t.transcode_samples(&input);
assert!(result.is_err(), "NaN sample must be rejected");
}
#[test]
fn test_transcode_samples_rejects_infinite() {
let cfg = AudioOnlyConfig::opus_stereo();
let mut t = AudioOnlyTranscoder::new(cfg);
let input = vec![0.1f32, f32::INFINITY, 0.3, 0.4];
let result = t.transcode_samples(&input);
assert!(result.is_err(), "Infinite sample must be rejected");
}
#[test]
fn test_transcode_samples_rejects_misaligned_input() {
let cfg = AudioOnlyConfig::opus_stereo(); let mut t = AudioOnlyTranscoder::new(cfg);
let input = vec![0.1f32, 0.2, 0.3];
let result = t.transcode_samples(&input);
assert!(result.is_err(), "Misaligned input must be rejected");
}
#[test]
fn test_transcode_samples_empty_input_succeeds() {
let cfg = AudioOnlyConfig::opus_stereo();
let mut t = AudioOnlyTranscoder::new(cfg);
let result = t.transcode_samples(&[]);
assert!(result.is_ok());
assert_eq!(result.expect("ok").len(), 0);
}
#[test]
fn test_reset_clears_frame_count() {
let cfg = AudioOnlyConfig::opus_stereo();
let mut t = AudioOnlyTranscoder::new(cfg);
let input = vec![0.1f32, 0.2]; t.transcode_samples(&input).expect("ok");
assert_eq!(t.frames_processed(), 1);
t.reset();
assert_eq!(t.frames_processed(), 0);
}
#[test]
fn test_update_config_valid() {
let cfg = AudioOnlyConfig::opus_stereo();
let mut t = AudioOnlyTranscoder::new(cfg);
let result = t.update_config(AudioCodecId::Pcm, AudioCodecId::Flac, 44_100, 2, 0);
assert!(
result.is_ok(),
"update_config should succeed: {:?}",
result.err()
);
assert_eq!(t.codec_name(), "flac");
}
#[test]
fn test_update_config_invalid_channels() {
let cfg = AudioOnlyConfig::opus_stereo();
let mut t = AudioOnlyTranscoder::new(cfg);
let result = t.update_config(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 0, 128_000);
assert!(result.is_err(), "Invalid channels in update should fail");
}
#[test]
fn test_lossless_output_gain_is_unity() {
let cfg = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Flac, 48_000, 2, 0)
.expect("valid");
let mut t = AudioOnlyTranscoder::new(cfg);
let input = vec![0.5f32, 0.5f32]; let output = t.transcode_samples(&input).expect("ok");
assert!(
(output[0] - input[0]).abs() < f32::EPSILON,
"FLAC should be lossless (gain=1.0)"
);
}
}