use std::str::FromStr;
use std::time::Duration;
use bytes::Bytes;
use unsafe_libopus::{
OPUS_APPLICATION_AUDIO, OPUS_GET_BITRATE_REQUEST, OPUS_GET_LOOKAHEAD_REQUEST, OPUS_OK, OPUS_SET_BITRATE_REQUEST,
OPUS_SET_DTX_REQUEST, OPUS_SET_INBAND_FEC_REQUEST, OpusEncoder, opus_encode_float, opus_encoder_create,
opus_encoder_ctl_impl, opus_encoder_destroy, varargs,
};
use crate::opus;
use crate::pcm;
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,
Pcm,
}
impl Codec {
pub fn as_str(self) -> &'static str {
match self {
Self::Opus => "opus",
Self::Pcm => "pcm",
}
}
}
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),
"pcm" => Ok(Self::Pcm),
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 fec: bool,
pub dtx: bool,
pub frame_duration: Duration,
}
impl Config {
pub fn new(input: Input) -> Self {
Self {
input,
codec: Codec::default(),
sample_rate: None,
channels: None,
bitrate: None,
fec: false,
dtx: false,
frame_duration: Duration::from_millis(20),
}
}
}
pub struct Encoder {
backend: Backend,
config: Config,
codec_rate: u32,
codec_channels: u32,
bitrate: u64,
pre_skip: u16,
frame_size: usize,
}
enum Backend {
Opus(Opus),
Pcm,
}
struct Opus {
inner: *mut OpusEncoder,
scratch: Vec<u8>,
}
unsafe impl Send for Opus {}
impl Encoder {
pub fn new(config: &Config) -> Result<Self, Error> {
match config.codec {
Codec::Opus => Self::new_opus(config.clone()),
Codec::Pcm => Self::new_pcm(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"));
}
let configured = Self::configure_opus(inner, &config, codec_rate, codec_channels);
let (bitrate, pre_skip) = match configured {
Ok(configured) => configured,
Err(err) => {
unsafe { opus_encoder_destroy(inner) };
return Err(err);
}
};
Ok(Self {
backend: Backend::Opus(Opus {
inner,
scratch: vec![0u8; MAX_PACKET_BYTES],
}),
config,
codec_rate,
codec_channels,
bitrate,
pre_skip,
frame_size,
})
}
fn new_pcm(config: Config) -> Result<Self, Error> {
if config.bitrate.is_some() {
return Err(Error::Unsupported(
"pcm bitrate is fixed; leave Config::bitrate unset".into(),
));
}
let codec_rate = config.sample_rate.unwrap_or(config.input.sample_rate);
if codec_rate == 0 {
return Err(Error::Unsupported("pcm sample rate must be greater than zero".into()));
}
let codec_channels = config.channels.unwrap_or(config.input.channels);
if codec_channels == 0 {
return Err(Error::Unsupported("pcm channel count must be greater than zero".into()));
}
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 frame_size = pcm::frame_size(codec_rate, config.frame_duration)?;
pcm::frame_bytes(frame_size, codec_channels)?;
let bitrate = pcm::bitrate(codec_rate, codec_channels)?;
Ok(Self {
backend: Backend::Pcm,
config,
codec_rate,
codec_channels,
bitrate,
pre_skip: 0,
frame_size,
})
}
fn configure_opus(
inner: *mut OpusEncoder,
config: &Config,
codec_rate: u32,
codec_channels: u32,
) -> Result<(u64, u16), Error> {
if let Some(bitrate) = config.bitrate {
Self::set_opus_bitrate(inner, codec_channels, bitrate as u64)?;
}
Self::set_opus_ctl(
inner,
OPUS_SET_INBAND_FEC_REQUEST,
i32::from(config.fec),
"OPUS_SET_INBAND_FEC",
)?;
Self::set_opus_ctl(inner, OPUS_SET_DTX_REQUEST, i32::from(config.dtx), "OPUS_SET_DTX")?;
let bitrate = Self::get_opus_ctl(inner, OPUS_GET_BITRATE_REQUEST, "OPUS_GET_BITRATE")?;
let bitrate = u64::try_from(bitrate)
.map_err(|_| Error::Unsupported(format!("Opus reported negative bitrate {bitrate}")))?;
let lookahead = Self::get_opus_ctl(inner, OPUS_GET_LOOKAHEAD_REQUEST, "OPUS_GET_LOOKAHEAD")?;
let lookahead = u64::try_from(lookahead)
.map_err(|_| Error::Unsupported(format!("Opus reported negative lookahead {lookahead}")))?;
let pre_skip = u16::try_from((lookahead * 48_000) / codec_rate as u64)
.map_err(|_| Error::Unsupported(format!("Opus lookahead {lookahead} does not fit in OpusHead")))?;
Ok((bitrate, pre_skip))
}
fn set_opus_bitrate(inner: *mut OpusEncoder, channels: u32, bitrate: u64) -> Result<(), Error> {
let max = 300_000 * channels as u64;
if !(500..=max).contains(&bitrate) {
return Err(Error::Unsupported(format!(
"Opus bitrate must be between 500 and {max} bits per second for {channels} channel(s), got {bitrate}"
)));
}
Self::set_opus_ctl(inner, OPUS_SET_BITRATE_REQUEST, bitrate as i32, "OPUS_SET_BITRATE")
}
fn set_opus_ctl(inner: *mut OpusEncoder, request: i32, value: i32, name: &'static str) -> Result<(), Error> {
let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![value]) };
if rc != OPUS_OK {
return Err(opus::error(rc, name));
}
Ok(())
}
fn get_opus_ctl(inner: *mut OpusEncoder, request: i32, name: &'static str) -> Result<i32, Error> {
let mut value = 0;
let rc = unsafe { opus_encoder_ctl_impl(inner, request, varargs![&mut value]) };
if rc != OPUS_OK {
return Err(opus::error(rc, name));
}
Ok(value)
}
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 bitrate(&self) -> u64 {
self.bitrate
}
pub fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
let Backend::Opus(opus) = &mut self.backend else {
return Err(Error::Unsupported("pcm bitrate is fixed".into()));
};
if bitrate != self.bitrate {
Self::set_opus_bitrate(opus.inner, self.codec_channels, bitrate)?;
self.bitrate = bitrate;
self.config.bitrate = Some(bitrate as u32);
}
Ok(())
}
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>(),
});
}
match &mut self.backend {
Backend::Opus(opus) => {
let n = unsafe {
opus_encode_float(
opus.inner,
pcm.as_ptr(),
self.frame_size as i32,
opus.scratch.as_mut_ptr(),
opus.scratch.len() as i32,
)
};
if n < 0 {
return Err(crate::opus::error(n, "opus_encode_float"));
}
Ok(Bytes::copy_from_slice(&opus.scratch[..n as usize]))
}
Backend::Pcm => {
let mut payload = Vec::with_capacity(std::mem::size_of_val(pcm));
for sample in pcm {
payload.extend_from_slice(&sample.to_le_bytes());
}
Ok(payload.into())
}
}
}
pub fn catalog(&self) -> hang::catalog::AudioConfig {
match self.config.codec {
Codec::Opus => {
let head = moq_mux::codec::opus::Config::new(self.codec_rate, self.codec_channels)
.with_pre_skip(self.pre_skip)
.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(u64::from);
config.description = Some(head);
config.container = hang::catalog::Container::Legacy;
config
}
Codec::Pcm => {
let mut config = hang::catalog::AudioConfig::new(
hang::catalog::AudioCodec::Pcm,
self.codec_rate,
self.codec_channels,
);
config.bitrate = Some(
pcm::bitrate(self.codec_rate, self.codec_channels)
.expect("pcm encoder bitrate validated at construction"),
);
config.container = hang::catalog::Container::Legacy;
config
}
}
}
}
impl Drop for Opus {
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,
}
}
fn opus_inner(encoder: &Encoder) -> *mut OpusEncoder {
let Backend::Opus(opus) = &encoder.backend else {
panic!("expected Opus encoder");
};
opus.inner
}
#[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);
let head = moq_mux::codec::opus::Config::parse(&mut desc.as_ref()).unwrap();
assert_eq!(head.pre_skip, enc.pre_skip);
assert_eq!(head.pre_skip, 312);
}
#[test]
fn opus_decoder_trims_encoder_lookahead_once() {
let mut enc = Encoder::new(&Config::new(stereo_48k())).unwrap();
let mut dec = Decoder::new(&enc.catalog()).unwrap();
let frame = vec![0.0; enc.frame_size() * enc.codec_channels() as usize];
let first = dec.decode(&enc.encode(&frame).unwrap()).unwrap();
assert_eq!(
first.len(),
(enc.frame_size() - enc.pre_skip as usize) * enc.codec_channels() as usize
);
let second = dec.decode(&enc.encode(&frame).unwrap()).unwrap();
assert_eq!(second.len(), frame.len());
}
#[test]
fn opus_runtime_bitrate_updates_encoder_state() {
let mut enc = Encoder::new(&Config {
bitrate: Some(64_000),
..Config::new(stereo_48k())
})
.unwrap();
enc.set_bitrate(32_000).unwrap();
assert_eq!(enc.bitrate(), 32_000);
assert_eq!(enc.config().bitrate, Some(32_000));
assert_eq!(
Encoder::get_opus_ctl(
opus_inner(&enc),
unsafe_libopus::OPUS_GET_BITRATE_REQUEST,
"OPUS_GET_BITRATE"
)
.unwrap(),
32_000
);
}
#[test]
fn opus_runtime_bitrate_rejects_values_libopus_would_clamp() {
let mut enc = Encoder::new(&Config::new(stereo_48k())).unwrap();
let original = enc.bitrate();
assert!(enc.set_bitrate(1).is_err());
assert!(enc.set_bitrate(600_001).is_err());
assert_eq!(enc.bitrate(), original);
}
#[test]
fn opus_applies_fec_and_dtx_controls() {
let enc = Encoder::new(&Config {
fec: true,
dtx: true,
..Config::new(stereo_48k())
})
.unwrap();
assert_eq!(
Encoder::get_opus_ctl(
opus_inner(&enc),
unsafe_libopus::OPUS_GET_INBAND_FEC_REQUEST,
"OPUS_GET_INBAND_FEC"
)
.unwrap(),
1
);
assert_eq!(
Encoder::get_opus_ctl(opus_inner(&enc), unsafe_libopus::OPUS_GET_DTX_REQUEST, "OPUS_GET_DTX").unwrap(),
1
);
}
#[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_eq!(Codec::Pcm.as_str(), "pcm");
assert_eq!(Codec::Pcm.to_string(), "pcm");
assert_eq!("pcm".parse::<Codec>().unwrap(), Codec::Pcm);
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);
assert_eq!(enc.pre_skip, 312);
}
#[test]
fn pcm_roundtrip_is_lossless() {
let mut enc = Encoder::new(&Config {
codec: Codec::Pcm,
..Config::new(stereo_48k())
})
.unwrap();
let mut dec = Decoder::new(&enc.catalog()).unwrap();
let input = sine(440.0, enc.codec_rate(), enc.codec_channels(), enc.frame_size());
let packet = enc.encode(&input).unwrap();
let output = dec.decode(&packet).unwrap();
assert_eq!(output, input);
}
#[test]
fn pcm_catalog_declares_fixed_bitrate() {
let enc = Encoder::new(&Config {
codec: Codec::Pcm,
..Config::new(stereo_48k())
})
.unwrap();
let catalog = enc.catalog();
assert_eq!(catalog.codec, hang::catalog::AudioCodec::Pcm);
assert_eq!(catalog.bitrate, Some(48_000 * 2 * 32));
assert_eq!(catalog.description, None);
}
#[test]
fn pcm_rejects_runtime_bitrate_change() {
let mut enc = Encoder::new(&Config {
codec: Codec::Pcm,
..Config::new(stereo_48k())
})
.unwrap();
let bitrate = enc.bitrate();
assert!(matches!(enc.set_bitrate(bitrate), Err(Error::Unsupported(_))));
assert_eq!(enc.bitrate(), bitrate);
}
#[test]
fn pcm_rejects_fractional_sample_frame_duration() {
let err = Encoder::new(&Config {
codec: Codec::Pcm,
frame_duration: Duration::from_micros(2_500),
..Config::new(Input {
sample_rate: 44_100,
..Input::default()
})
});
assert!(matches!(err, Err(Error::Unsupported(_))));
}
#[test]
fn pcm_rejects_bitrate_overflow() {
let err = Encoder::new(&Config {
codec: Codec::Pcm,
frame_duration: Duration::from_secs(1),
..Config::new(Input {
sample_rate: u32::MAX,
channels: u32::MAX,
..Input::default()
})
});
assert!(matches!(err, Err(Error::Unsupported(_))));
}
}