#![allow(clippy::module_name_repetitions)]
use ogg::{PacketReader, PacketWriteEndInfo, PacketWriter};
use switchy_fs::sync::File;
use thiserror::Error;
use crate::EncodeInfo;
#[derive(Debug, Error)]
pub enum EncoderError {
#[error("Encoder error")]
AudiopusEncoder(#[from] audiopus::Error),
#[error("Encoder error")]
OpusEncoder(::opus::Error),
}
impl From<::opus::Error> for EncoderError {
fn from(value: ::opus::Error) -> Self {
Self::OpusEncoder(value)
}
}
pub fn encode_audiopus(samples: &[f32]) -> Result<(u32, Vec<u8>), EncoderError> {
use audiopus::{
Application, Bitrate, Channels, Error as OpusError, ErrorCode as OpusErrorCode, SampleRate,
coder::Encoder,
};
let sample_rate = SampleRate::Hz48000;
let mut encoder = Encoder::new(sample_rate, Channels::Stereo, Application::Audio)?;
encoder.set_bitrate(Bitrate::Max)?;
#[allow(clippy::cast_sign_loss)]
let frame_size = (sample_rate as i32 / 1000 * 2 * 20) as usize;
let mut output = vec![0u8; samples.len().max(256)];
let mut samples_i = 0;
let mut output_i = 0;
let mut end_buffer = vec![0f32; frame_size];
{
let samples: u32 = samples.len().try_into().unwrap();
let bytes = samples.to_be_bytes();
output[..4].clone_from_slice(&bytes[..4]);
output_i += 4;
}
while samples_i < samples.len() {
match encoder.encode_float(
if samples_i + frame_size < samples.len() {
&samples[samples_i..(samples_i + frame_size)]
} else {
end_buffer[..(samples.len() - samples_i)].clone_from_slice(
&samples[samples_i..((samples.len() - samples_i) + samples_i)],
);
&end_buffer
},
&mut output[output_i + 2..],
) {
Ok(pkt_len) => {
samples_i += frame_size;
let bytes = u16::try_from(pkt_len).unwrap().to_be_bytes();
output[output_i] = bytes[0];
output[output_i + 1] = bytes[1];
output_i += pkt_len + 2;
}
Err(OpusError::Opus(OpusErrorCode::BufferTooSmall)) => {
log::error!(
"Needed to increase buffer size, opus is compressing less well than expected."
);
output.resize(output.len() * 2, 0u8);
}
Err(e) => {
return Err(EncoderError::AudiopusEncoder(e));
}
}
}
output.truncate(output_i);
#[allow(clippy::cast_sign_loss)]
Ok((sample_rate as i32 as u32, output))
}
pub fn encoder_opus() -> Result<::opus::Encoder, EncoderError> {
let encoder =
::opus::Encoder::new(48000, ::opus::Channels::Stereo, ::opus::Application::Audio)?;
Ok(encoder)
}
pub fn encode_opus_float(
encoder: &mut ::opus::Encoder,
input: &[f32],
output: &mut [u8],
) -> Result<EncodeInfo, EncoderError> {
let len = encoder.encode_float(input, output)?;
Ok(EncodeInfo {
output_size: len,
input_consumed: input.len(),
})
}
pub fn read_write_ogg(mut read: std::fs::File, mut write: std::fs::File) {
let mut pck_rdr = PacketReader::new(&mut read);
pck_rdr.delete_unread_packets();
let mut pck_wtr = PacketWriter::new(&mut write);
loop {
let r = pck_rdr.read_packet().unwrap();
match r {
Some(pck) => {
let (inf_d, inf) = if pck.last_in_stream() {
("end_stream", PacketWriteEndInfo::EndStream)
} else if pck.last_in_page() {
("end_page", PacketWriteEndInfo::EndPage)
} else {
("normal", PacketWriteEndInfo::NormalPacket)
};
let stream_serial = pck.stream_serial();
let absgp_page = pck.absgp_page();
log::debug!(
"stream_serial={} absgp_page={} len={} inf_d={inf_d}",
stream_serial,
absgp_page,
pck.data.len()
);
pck_wtr
.write_packet(pck.data, stream_serial, inf, absgp_page)
.unwrap();
}
None => break,
}
}
}
pub fn write_ogg(file: std::fs::File, content: &[u8]) {
let mut writer = PacketWriter::new(file);
if let Err(err) = writer.write_packet(content, 0, PacketWriteEndInfo::EndStream, 0) {
log::error!("Error: {err:?}");
}
}
struct OpusPacket {
content: Vec<u8>,
packet_num: u64,
page_num: u64,
absgp: u64,
info: PacketWriteEndInfo,
}
pub struct OpusWrite<'a> {
packet_writer: PacketWriter<'a, File>,
serial: u32,
absgp: u64,
packet_num: u64,
page_num: u64,
packet: Option<OpusPacket>,
}
pub const OPUS_STREAM_IDENTIFICATION_HEADER: [u8; 19] = [
b'O', b'p', b'u', b's', b'H', b'e', b'a', b'd',
0x01, 0x02, 0x00, 0x00, 0x80, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, ];
pub const OPUS_STREAM_COMMENTS_HEADER: [u8; 23] = [
b'O', b'p', b'u', b's', b'T', b'a', b'g', b's',
0x07, 0x00, 0x00, 0x00, b'E', b'N', b'C', b'O', b'D', b'E', b'R',
0x00, 0x00, 0x00, 0x00, ];
impl OpusWrite<'_> {
#[must_use]
pub fn new(path: &str) -> Self {
let _ = std::fs::remove_file(path);
let file = switchy_fs::sync::OpenOptions::new()
.create(true) .truncate(true)
.write(true)
.open(path)
.unwrap();
let packet_writer = PacketWriter::new(file);
let absgp = 0;
Self {
packet_writer,
serial: 2_873_470_314,
absgp,
packet_num: 0,
page_num: 0,
packet: None,
}
}
}
impl std::io::Write for OpusWrite<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let info = PacketWriteEndInfo::NormalPacket;
let packet = OpusPacket {
content: buf.to_vec(),
info,
absgp: self.absgp,
packet_num: self.packet_num,
page_num: self.page_num,
};
if let Some(packet) = self.packet.replace(packet) {
let info_d = match packet.info {
PacketWriteEndInfo::EndPage => "end_page",
PacketWriteEndInfo::NormalPacket => "normal",
PacketWriteEndInfo::EndStream => "end_stream",
};
log::debug!(
"writing stream_serial={} absgp_page={}, len={}, info_d={} packet_num={} page_num={}",
self.serial,
packet.absgp,
packet.content.len(),
info_d,
packet.packet_num,
packet.page_num
);
self.packet_writer
.write_packet(packet.content, self.serial, packet.info, packet.absgp)
.unwrap();
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
if let Some(packet) = self.packet.take() {
let info = PacketWriteEndInfo::EndStream;
let info_d = match info {
PacketWriteEndInfo::EndPage => "end_page",
PacketWriteEndInfo::NormalPacket => "normal",
PacketWriteEndInfo::EndStream => "end_stream",
};
log::debug!(
"writing stream_serial={} absgp_page={}, len={}, info_d={} packet_num={} page_num={}",
self.serial,
packet.absgp,
packet.content.len(),
info_d,
packet.packet_num,
packet.page_num
);
self.packet_writer
.write_packet(packet.content, self.serial, info, packet.absgp)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test]
fn test_encoder_creation() {
let result = encoder_opus();
assert!(
result.is_ok(),
"Opus encoder should initialize successfully"
);
}
#[test_log::test]
fn test_encode_opus_float_basic() {
let mut encoder = encoder_opus().expect("Failed to create encoder");
let input: Vec<f32> = vec![0.0; 960];
let mut output = vec![0u8; 4000];
let result = encode_opus_float(&mut encoder, &input, &mut output);
assert!(result.is_ok(), "Encoding should succeed");
let info = result.unwrap();
assert!(info.output_size > 0, "Should produce output");
assert_eq!(
info.input_consumed,
input.len(),
"Should report all input consumed"
);
}
#[test_log::test]
fn test_encode_audiopus_packet_framing() {
let samples: Vec<f32> = vec![0.1; 1000];
let result = encode_audiopus(&samples);
assert!(result.is_ok(), "Encoding should succeed");
let (sample_rate, output) = result.unwrap();
assert_eq!(sample_rate, 48000, "Sample rate should be 48kHz");
assert!(
output.len() >= 4,
"Output should contain at least the sample count"
);
let sample_count = u32::from_be_bytes([output[0], output[1], output[2], output[3]]);
#[allow(clippy::cast_possible_truncation)]
let expected_count = samples.len() as u32;
assert_eq!(
sample_count, expected_count,
"Sample count should match input"
);
if output.len() > 4 {
assert!(
output.len() >= 6,
"Should have room for at least one packet length"
);
}
}
#[test_log::test]
fn test_encode_audiopus_multiple_frames() {
let frame_size = 1920;
let samples: Vec<f32> = vec![0.5; frame_size * 3];
let result = encode_audiopus(&samples);
assert!(result.is_ok(), "Encoding should succeed");
let (sample_rate, output) = result.unwrap();
assert_eq!(sample_rate, 48000);
let mut offset = 4; let mut packet_count = 0;
while offset + 2 <= output.len() {
let packet_len = u16::from_be_bytes([output[offset], output[offset + 1]]) as usize;
if packet_len == 0 {
break;
}
offset += 2 + packet_len;
packet_count += 1;
if offset >= output.len() {
break;
}
}
assert!(packet_count >= 1, "Should have encoded at least one packet");
}
#[test_log::test]
fn test_encode_audiopus_empty_input() {
let samples: Vec<f32> = vec![];
let result = encode_audiopus(&samples);
assert!(result.is_ok(), "Empty input should be handled");
let (_sample_rate, output) = result.unwrap();
assert!(output.len() >= 4);
let sample_count = u32::from_be_bytes([output[0], output[1], output[2], output[3]]);
assert_eq!(sample_count, 0);
}
#[test_log::test]
#[allow(clippy::cast_precision_loss)]
fn test_encode_audiopus_varying_amplitudes() {
let samples: Vec<f32> = (0..1920)
.map(|i| {
let t = i as f32 / 48000.0;
(t * 440.0 * std::f32::consts::TAU).sin() * 0.5
})
.collect();
let result = encode_audiopus(&samples);
assert!(result.is_ok(), "Encoding varying amplitudes should succeed");
let (sample_rate, output) = result.unwrap();
assert_eq!(sample_rate, 48000);
let sample_count = u32::from_be_bytes([output[0], output[1], output[2], output[3]]);
#[allow(clippy::cast_possible_truncation)]
let expected = samples.len() as u32;
assert_eq!(sample_count, expected);
}
#[test_log::test]
#[allow(clippy::cast_precision_loss)]
fn test_encode_opus_float_consecutive_calls() {
let mut encoder = encoder_opus().expect("Failed to create encoder");
let frame_size = 960;
let mut total_output = 0;
for i in 0..5 {
let input: Vec<f32> = (0..frame_size)
.map(|j| {
let t = (i * frame_size + j) as f32 / 48000.0;
(t * 440.0 * std::f32::consts::TAU).sin() * 0.3
})
.collect();
let mut output = vec![0u8; 4000];
let result = encode_opus_float(&mut encoder, &input, &mut output);
assert!(
result.is_ok(),
"Consecutive encoding call {} should succeed",
i + 1
);
let info = result.unwrap();
assert!(info.output_size > 0, "Each frame should produce output");
assert_eq!(
info.input_consumed, frame_size,
"Each frame should consume all input"
);
total_output += info.output_size;
}
assert!(total_output > 0, "Total output should be non-zero");
}
#[test_log::test(switchy_async::test(real_fs))]
async fn test_opus_write_creation() {
let temp_dir = switchy_fs::tempdir().expect("Failed to create temp directory");
let temp_file = temp_dir.path().join("test_opus_write.ogg");
let temp_file_str = temp_file.to_string_lossy();
let writer = OpusWrite::new(&temp_file_str);
assert_eq!(writer.serial, 2_873_470_314, "Serial should be initialized");
assert_eq!(writer.absgp, 0, "Initial absgp should be 0");
assert_eq!(writer.packet_num, 0, "Initial packet_num should be 0");
assert_eq!(writer.page_num, 0, "Initial page_num should be 0");
assert!(writer.packet.is_none(), "Initial packet should be None");
}
#[test_log::test(switchy_async::test(real_fs))]
async fn test_opus_write_buffering_behavior() {
use std::io::Write;
let temp_dir = switchy_fs::tempdir().expect("Failed to create temp directory");
let temp_file = temp_dir.path().join("test_opus_buffering.ogg");
let temp_file_str = temp_file.to_string_lossy();
let mut writer = OpusWrite::new(&temp_file_str);
let data1 = vec![1u8; 100];
let result1 = writer.write(&data1);
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), 100);
assert!(writer.packet.is_some(), "First packet should be buffered");
let data2 = vec![2u8; 100];
let result2 = writer.write(&data2);
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), 100);
assert!(writer.packet.is_some(), "Second packet should be buffered");
let flush_result = writer.flush();
assert!(flush_result.is_ok());
assert!(writer.packet.is_none(), "Packet should be written on flush");
}
}