use std::str::FromStr;
use std::time::Duration;
use bytes::Bytes;
use unsafe_libopus::{
OPUS_APPLICATION_AUDIO, OPUS_OK, OPUS_SET_BITRATE_REQUEST, OpusEncoder, opus_encode_float, opus_encoder_create,
opus_encoder_ctl_impl, opus_encoder_destroy, varargs,
};
use crate::opus;
use crate::{Error, Format};
const MAX_PACKET_BYTES: usize = 4_000;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Codec {
#[default]
Opus,
}
impl Codec {
pub fn as_str(self) -> &'static str {
match self {
Self::Opus => "opus",
}
}
}
impl std::fmt::Display for Codec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Codec {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"opus" => Ok(Self::Opus),
other => Err(Error::Unsupported(format!("unknown codec: {other}"))),
}
}
}
#[derive(Clone, Debug)]
pub struct Input {
pub format: Format,
pub sample_rate: u32,
pub channels: u32,
}
impl Default for Input {
fn default() -> Self {
Self {
format: Format::F32,
sample_rate: 48_000,
channels: 2,
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Config {
pub input: Input,
pub codec: Codec,
pub sample_rate: Option<u32>,
pub channels: Option<u32>,
pub bitrate: Option<u32>,
pub frame_duration: Duration,
}
impl Config {
pub fn new(input: Input) -> Self {
Self {
input,
codec: Codec::default(),
sample_rate: None,
channels: None,
bitrate: None,
frame_duration: Duration::from_millis(20),
}
}
}
pub struct Encoder {
inner: *mut OpusEncoder,
config: Config,
codec_rate: u32,
codec_channels: u32,
frame_size: usize,
scratch: Vec<u8>,
}
unsafe impl Send for Encoder {}
impl Encoder {
pub fn new(config: &Config) -> Result<Self, Error> {
match config.codec {
Codec::Opus => Self::new_opus(config.clone()),
}
}
fn new_opus(config: Config) -> Result<Self, Error> {
let codec_rate = config
.sample_rate
.unwrap_or_else(|| opus::pick_rate(config.input.sample_rate));
opus::validate_rate(codec_rate)?;
let codec_channels = config.channels.unwrap_or(config.input.channels);
if codec_channels != config.input.channels {
return Err(Error::Unsupported(format!(
"channel remapping not implemented (input {}ch, output {codec_channels}ch)",
config.input.channels
)));
}
let channels = opus::validate_channels(codec_channels)?;
let frame_size = opus::frame_size(codec_rate, config.frame_duration)?;
let mut err = 0i32;
let inner = unsafe { opus_encoder_create(codec_rate as i32, channels, OPUS_APPLICATION_AUDIO, &mut err) };
if err != OPUS_OK || inner.is_null() {
return Err(opus::error(err, "opus_encoder_create"));
}
if let Some(b) = config.bitrate {
let rc = unsafe { opus_encoder_ctl_impl(inner, OPUS_SET_BITRATE_REQUEST, varargs![b as i32]) };
if rc != OPUS_OK {
unsafe { opus_encoder_destroy(inner) };
return Err(opus::error(rc, "OPUS_SET_BITRATE"));
}
}
Ok(Self {
inner,
config,
codec_rate,
codec_channels,
frame_size,
scratch: vec![0u8; MAX_PACKET_BYTES],
})
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn codec(&self) -> Codec {
self.config.codec
}
pub fn codec_rate(&self) -> u32 {
self.codec_rate
}
pub fn codec_channels(&self) -> u32 {
self.codec_channels
}
pub fn frame_size(&self) -> usize {
self.frame_size
}
pub fn encode(&mut self, pcm: &[f32]) -> Result<Bytes, Error> {
let expected = self.frame_size * self.codec_channels as usize;
if pcm.len() != expected {
return Err(Error::Misaligned {
got: std::mem::size_of_val(pcm),
expected: expected * std::mem::size_of::<f32>(),
});
}
let n = unsafe {
opus_encode_float(
self.inner,
pcm.as_ptr(),
self.frame_size as i32,
self.scratch.as_mut_ptr(),
self.scratch.len() as i32,
)
};
if n < 0 {
return Err(opus::error(n, "opus_encode_float"));
}
Ok(Bytes::copy_from_slice(&self.scratch[..n as usize]))
}
pub fn catalog(&self) -> hang::catalog::AudioConfig {
let head = moq_mux::codec::opus::Config {
sample_rate: self.codec_rate,
channel_count: self.codec_channels,
}
.encode()
.expect("opus encoder channels validated to mono/stereo");
let mut config =
hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, self.codec_rate, self.codec_channels);
config.bitrate = self.config.bitrate.map(|b| b as u64);
config.description = Some(head);
config.container = hang::catalog::Container::Legacy;
config
}
}
impl Drop for Encoder {
fn drop(&mut self) {
unsafe { opus_encoder_destroy(self.inner) };
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decode::Decoder;
fn sine(freq: f32, sample_rate: u32, channels: u32, frames: usize) -> Vec<f32> {
let mut out = Vec::with_capacity(frames * channels as usize);
for i in 0..frames {
let t = i as f32 / sample_rate as f32;
let v = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.5;
for _ in 0..channels {
out.push(v);
}
}
out
}
fn stereo_48k() -> Input {
Input {
format: Format::F32,
sample_rate: 48_000,
channels: 2,
}
}
#[test]
fn opus_encode_then_decode_keeps_signal_close() {
let mut enc = Encoder::new(&Config {
bitrate: Some(96_000),
..Config::new(stereo_48k())
})
.unwrap();
let cfg = enc.catalog();
let mut dec = Decoder::new(&cfg).unwrap();
let frame = sine(440.0, 48_000, 2, enc.frame_size());
for _ in 0..5 {
let pkt = enc.encode(&frame).unwrap();
let _ = dec.decode(&pkt).unwrap();
}
let pkt = enc.encode(&frame).unwrap();
let decoded = dec.decode(&pkt).unwrap();
assert_eq!(decoded.len(), frame.len());
let energy_in: f32 = frame.iter().map(|s| s * s).sum();
let energy_out: f32 = decoded.iter().map(|s| s * s).sum();
let ratio = energy_out / energy_in;
assert!(
(0.5..2.0).contains(&ratio),
"output energy ratio {ratio:.3} should be close to 1"
);
}
#[test]
fn opus_rejects_unsupported_frame_duration() {
let err = Encoder::new(&Config {
frame_duration: Duration::from_millis(15),
..Config::new(Input::default())
});
assert!(matches!(err, Err(Error::Unsupported(_))));
}
#[test]
fn opus_rejects_misaligned_input() {
let mut enc = Encoder::new(&Config::new(Input::default())).unwrap();
assert!(matches!(enc.encode(&[0.0f32; 100]), Err(Error::Misaligned { .. })));
}
#[test]
fn opus_catalog_includes_opushead() {
let enc = Encoder::new(&Config {
bitrate: Some(64_000),
..Config::new(stereo_48k())
})
.unwrap();
let cfg = enc.catalog();
assert_eq!(cfg.sample_rate, 48_000);
assert_eq!(cfg.channel_count, 2);
assert_eq!(cfg.bitrate, Some(64_000));
let desc = cfg.description.expect("OpusHead should be present");
assert_eq!(desc.len(), 19);
}
#[test]
fn codec_roundtrips_as_str() {
assert_eq!(Codec::Opus.as_str(), "opus");
assert_eq!(Codec::Opus.to_string(), "opus");
assert_eq!("opus".parse::<Codec>().unwrap(), Codec::Opus);
assert!("aac".parse::<Codec>().is_err());
}
#[test]
fn config_sample_rate_overrides_the_codec_rate() {
let enc = Encoder::new(&Config {
sample_rate: Some(24_000),
..Config::new(Input {
sample_rate: 48_000,
channels: 1,
..Input::default()
})
})
.unwrap();
assert_eq!(enc.codec_rate(), 24_000);
assert_eq!(enc.catalog().sample_rate, 24_000);
}
}