use anyhow::{Result, bail};
use audiopus::coder::Encoder as OpusEnc;
use audiopus::{Application, Channels, SampleRate};
use snapcast_proto::SampleFormat;
use super::{EncodedChunk, Encoder};
use crate::AudioData;
pub struct OpusEncoder {
format: SampleFormat,
encoder: OpusEnc,
header: Vec<u8>,
frame_size: usize,
warned: bool,
}
impl OpusEncoder {
pub fn new(format: SampleFormat, _options: &str) -> Result<Self> {
let sample_rate = match format.rate() {
8000 => SampleRate::Hz8000,
12000 => SampleRate::Hz12000,
16000 => SampleRate::Hz16000,
24000 => SampleRate::Hz24000,
48000 => SampleRate::Hz48000,
r => {
tracing::warn!(codec = "opus", sample_rate = r, "unsupported sample rate");
bail!("Opus does not support sample rate {r}");
}
};
let channels = match format.channels() {
1 => Channels::Mono,
2 => Channels::Stereo,
c => {
tracing::warn!(codec = "opus", channels = c, "unsupported channel count");
bail!("Opus does not support {c} channels");
}
};
let encoder = OpusEnc::new(sample_rate, channels, Application::Audio)?;
let mut header = Vec::with_capacity(19);
header.extend_from_slice(b"OpusHead");
header.push(1); header.push(format.channels() as u8);
header.extend_from_slice(&0u16.to_le_bytes()); header.extend_from_slice(&format.rate().to_le_bytes());
header.extend_from_slice(&0u16.to_le_bytes()); header.push(0);
let frame_size = format.rate() as usize / 50;
Ok(Self {
format,
encoder,
header,
frame_size,
warned: false,
})
}
}
impl Encoder for OpusEncoder {
fn name(&self) -> &str {
"opus"
}
fn header(&self) -> &[u8] {
&self.header
}
fn encode(&mut self, input: &AudioData) -> Result<EncodedChunk> {
let pcm = match input {
AudioData::Pcm(data) => std::borrow::Cow::Borrowed(data.as_slice()),
AudioData::F32(samples) => {
if !self.warned {
self.warned = true;
tracing::warn!(
codec = "opus",
"F32 input requires quantization to 16-bit — consider f32lz4 for lossless path"
);
}
std::borrow::Cow::Owned(super::f32_to_pcm(samples, 16))
}
};
let channels = self.format.channels() as usize;
let frame_samples = self.frame_size * channels;
let frame_bytes = frame_samples * 2; let total_frames = pcm.len() / (channels * 2);
tracing::trace!(
codec = "opus",
input_bytes = pcm.len(),
total_frames,
"encode"
);
let mut output = Vec::new();
let mut encode_buf = [0u8; 4096];
for chunk in pcm.chunks(frame_bytes) {
if chunk.len() < frame_bytes {
break;
}
let samples: Vec<i16> = chunk
.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]))
.collect();
match self.encoder.encode(&samples, &mut encode_buf) {
Ok(len) => output.extend_from_slice(&encode_buf[..len]),
Err(e) => {
tracing::warn!(codec = "opus", error = %e, "encode failed");
bail!("Opus encode failed: {e}");
}
}
}
Ok(EncodedChunk { data: output })
}
}