use std::fs::File;
use std::io::Write;
use std::path::Path;
use ogg::writing::{PacketWriteEndInfo, PacketWriter};
use opus::{Application, Channels, Encoder};
const FRAME_SIZE: usize = 960;
const PRE_SKIP: u16 = 312;
const SAMPLE_RATE: u32 = 48_000;
const BITRATE: i32 = 96_000;
pub struct OpusWriter {
encoder: Encoder,
packets: PacketWriter<'static, File>,
channels: usize,
serial: u32,
pending: Vec<f32>,
granule: u64,
encoded: Vec<u8>,
pub packets_written: u64,
pub bytes_written: u64,
}
impl OpusWriter {
pub fn create(path: impl AsRef<Path>, channels: u16) -> std::io::Result<Self> {
let ch = match channels {
1 => Channels::Mono,
_ => Channels::Stereo,
};
let mut encoder = Encoder::new(SAMPLE_RATE, ch, Application::Voip)
.map_err(|e| std::io::Error::other(format!("opus encoder: {e}")))?;
let _ = encoder.set_bitrate(opus::Bitrate::Bits(BITRATE));
let mut packets = PacketWriter::new(File::create(path)?);
let serial = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0x4469_7363)
| 1;
let channels_u8 = channels.clamp(1, 2) as u8;
packets.write_packet(
opus_head(channels_u8),
serial,
PacketWriteEndInfo::EndPage,
0,
)?;
packets.write_packet(opus_tags(), serial, PacketWriteEndInfo::EndPage, 0)?;
Ok(Self {
encoder,
packets,
channels: channels_u8 as usize,
serial,
pending: Vec::with_capacity(FRAME_SIZE * 2 * 2),
granule: u64::from(PRE_SKIP),
encoded: vec![0u8; 4000],
packets_written: 0,
bytes_written: 0,
})
}
pub fn write(&mut self, samples: &[f32]) -> std::io::Result<()> {
self.pending.extend_from_slice(samples);
let per_frame = FRAME_SIZE * self.channels;
while self.pending.len() >= per_frame {
let frame: Vec<f32> = self.pending.drain(..per_frame).collect();
self.emit(&frame, false)?;
}
Ok(())
}
fn emit(&mut self, frame: &[f32], last: bool) -> std::io::Result<()> {
let n = self
.encoder
.encode_float(frame, &mut self.encoded)
.map_err(|e| std::io::Error::other(format!("opus encode: {e}")))?;
self.granule += FRAME_SIZE as u64;
let info = if last {
PacketWriteEndInfo::EndStream
} else {
PacketWriteEndInfo::EndPage
};
self.packets
.write_packet(self.encoded[..n].to_vec(), self.serial, info, self.granule)?;
self.packets_written += 1;
self.bytes_written += n as u64;
Ok(())
}
pub fn finalize(mut self) -> std::io::Result<()> {
let per_frame = FRAME_SIZE * self.channels;
if !self.pending.is_empty() {
self.pending.resize(per_frame, 0.0);
let frame: Vec<f32> = self.pending.drain(..per_frame).collect();
self.emit(&frame, true)?;
} else {
let silence = vec![0.0f32; per_frame];
self.emit(&silence, true)?;
}
let mut inner = self.packets.into_inner();
inner.flush()?;
inner.sync_all()?;
Ok(())
}
}
fn opus_head(channels: u8) -> Vec<u8> {
let mut v = Vec::with_capacity(19);
v.extend_from_slice(b"OpusHead");
v.push(1); v.push(channels);
v.extend_from_slice(&PRE_SKIP.to_le_bytes());
v.extend_from_slice(&SAMPLE_RATE.to_le_bytes());
v.extend_from_slice(&0i16.to_le_bytes()); v.push(0); v
}
fn opus_tags() -> Vec<u8> {
const VENDOR: &[u8] = b"DiscRec";
let mut v = Vec::with_capacity(8 + 4 + VENDOR.len() + 4);
v.extend_from_slice(b"OpusTags");
v.extend_from_slice(&(VENDOR.len() as u32).to_le_bytes());
v.extend_from_slice(VENDOR);
v.extend_from_slice(&0u32.to_le_bytes()); v
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opus_head_matches_rfc7845_layout() {
let h = opus_head(2);
assert_eq!(&h[..8], b"OpusHead");
assert_eq!(h[8], 1, "version");
assert_eq!(h[9], 2, "channel count");
assert_eq!(u16::from_le_bytes([h[10], h[11]]), PRE_SKIP);
assert_eq!(
u32::from_le_bytes([h[12], h[13], h[14], h[15]]),
SAMPLE_RATE
);
assert_eq!(h[18], 0, "mapping family");
assert_eq!(h.len(), 19);
}
#[test]
fn opus_tags_is_well_formed() {
let t = opus_tags();
assert_eq!(&t[..8], b"OpusTags");
let vlen = u32::from_le_bytes([t[8], t[9], t[10], t[11]]) as usize;
assert_eq!(&t[12..12 + vlen], b"DiscRec");
assert_eq!(
u32::from_le_bytes([t[12 + vlen], t[13 + vlen], t[14 + vlen], t[15 + vlen]]),
0,
"comment count"
);
}
#[test]
fn writes_a_playable_file_and_reports_progress() {
let path = std::env::temp_dir().join("discrec-writer-test.ogg");
let mut w = OpusWriter::create(&path, 2).expect("create");
let mut phase = 0.0f32;
for _ in 0..100 {
let mut block = Vec::with_capacity(480 * 2);
for _ in 0..480 {
phase += 440.0 * std::f32::consts::TAU / SAMPLE_RATE as f32;
let s = (phase.sin()) * 0.2;
block.push(s);
block.push(s);
}
w.write(&block).expect("write");
}
assert!(w.packets_written > 0, "should have emitted audio packets");
assert!(w.bytes_written > 0);
w.finalize().expect("finalize");
let size = std::fs::metadata(&path).expect("stat").len();
assert!(size > 1000, "file suspiciously small: {size} bytes");
let head = std::fs::read(&path).expect("read");
assert_eq!(&head[..4], b"OggS", "must start with an Ogg page");
let _ = std::fs::remove_file(&path);
}
}