use alloc::collections::VecDeque;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use super::page::{NO_GRANULE, OggPacket, PacketReader, PageReader, PageWriter};
use crate::packet::Toc;
pub const OPUS_HEAD_MAGIC: [u8; 8] = *b"OpusHead";
pub const OPUS_TAGS_MAGIC: [u8; 8] = *b"OpusTags";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OggOpusError {
NoOpusStream,
InvalidIdHeader,
UnsupportedVersion(u8),
InvalidCommentHeader,
}
impl fmt::Display for OggOpusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OggOpusError::NoOpusStream => f.write_str("no Opus logical bitstream found"),
OggOpusError::InvalidIdHeader => f.write_str("malformed OpusHead identification header"),
OggOpusError::UnsupportedVersion(v) => {
write!(f, "unsupported Ogg Opus encapsulation version {v}")
},
OggOpusError::InvalidCommentHeader => f.write_str("malformed OpusTags comment header"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for OggOpusError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChannelMapping {
Family0,
Table {
family: u8,
stream_count: u8,
coupled_count: u8,
mapping: Vec<u8>,
},
}
impl ChannelMapping {
#[must_use]
pub fn stream_count(&self) -> u8 {
match self {
ChannelMapping::Family0 => 1,
ChannelMapping::Table { stream_count, .. } => *stream_count,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpusHead {
pub version: u8,
pub channel_count: u8,
pub pre_skip: u16,
pub input_sample_rate: u32,
pub output_gain_q8: i16,
pub channel_mapping: ChannelMapping,
}
impl OpusHead {
pub fn parse(data: &[u8]) -> Result<Self, OggOpusError> {
if data.len() < 19 || data[0..8] != OPUS_HEAD_MAGIC {
return Err(OggOpusError::InvalidIdHeader);
}
let version = data[8];
if version >> 4 != 0 {
return Err(OggOpusError::UnsupportedVersion(version));
}
let channel_count = data[9];
if channel_count == 0 {
return Err(OggOpusError::InvalidIdHeader);
}
let pre_skip = u16::from_le_bytes([data[10], data[11]]);
let input_sample_rate = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
let output_gain_q8 = i16::from_le_bytes([data[16], data[17]]);
let family = data[18];
let channel_mapping = if family == 0 {
if channel_count > 2 {
return Err(OggOpusError::InvalidIdHeader);
}
ChannelMapping::Family0
} else {
let table = &data[19..];
if table.len() < 2 + usize::from(channel_count) {
return Err(OggOpusError::InvalidIdHeader);
}
let stream_count = table[0];
let coupled_count = table[1];
if stream_count == 0
|| coupled_count > stream_count
|| usize::from(stream_count) + usize::from(coupled_count) > 255
{
return Err(OggOpusError::InvalidIdHeader);
}
let mapping = table[2..2 + usize::from(channel_count)].to_vec();
let decoded_channels = stream_count + coupled_count;
if mapping.iter().any(|&idx| idx != 255 && idx >= decoded_channels) {
return Err(OggOpusError::InvalidIdHeader);
}
ChannelMapping::Table {
family,
stream_count,
coupled_count,
mapping,
}
};
Ok(OpusHead {
version,
channel_count,
pre_skip,
input_sample_rate,
output_gain_q8,
channel_mapping,
})
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(19);
out.extend_from_slice(&OPUS_HEAD_MAGIC);
out.push(self.version);
out.push(self.channel_count);
out.extend_from_slice(&self.pre_skip.to_le_bytes());
out.extend_from_slice(&self.input_sample_rate.to_le_bytes());
out.extend_from_slice(&self.output_gain_q8.to_le_bytes());
match &self.channel_mapping {
ChannelMapping::Family0 => out.push(0),
ChannelMapping::Table {
family,
stream_count,
coupled_count,
mapping,
} => {
out.push(*family);
out.push(*stream_count);
out.push(*coupled_count);
out.extend_from_slice(mapping);
},
}
out
}
#[must_use]
pub fn family0(channels: u8, pre_skip: u16, input_sample_rate: u32) -> Self {
assert!(channels == 1 || channels == 2, "family 0 allows 1 or 2 channels");
OpusHead {
version: 1,
channel_count: channels,
pre_skip,
input_sample_rate,
output_gain_q8: 0,
channel_mapping: ChannelMapping::Family0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct OpusTags {
pub vendor: Vec<u8>,
pub comments: Vec<Vec<u8>>,
}
impl OpusTags {
pub fn parse(data: &[u8]) -> Result<Self, OggOpusError> {
let mut rest = data
.strip_prefix(&OPUS_TAGS_MAGIC)
.ok_or(OggOpusError::InvalidCommentHeader)?;
let vendor = read_len_prefixed(&mut rest)?.to_vec();
let count = read_u32(&mut rest)? as usize;
if count > rest.len() / 4 {
return Err(OggOpusError::InvalidCommentHeader);
}
let mut comments = Vec::with_capacity(count);
for _ in 0..count {
comments.push(read_len_prefixed(&mut rest)?.to_vec());
}
Ok(OpusTags { vendor, comments })
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&OPUS_TAGS_MAGIC);
out.extend_from_slice(&(self.vendor.len() as u32).to_le_bytes());
out.extend_from_slice(&self.vendor);
out.extend_from_slice(&(self.comments.len() as u32).to_le_bytes());
for c in &self.comments {
out.extend_from_slice(&(c.len() as u32).to_le_bytes());
out.extend_from_slice(c);
}
out
}
pub fn comments_lossy(&self) -> impl Iterator<Item = String> + '_ {
self.comments.iter().map(|c| String::from_utf8_lossy(c).into_owned())
}
#[must_use]
pub fn get(&self, tag: &str) -> Option<String> {
self.comments.iter().find_map(|c| {
let s = String::from_utf8_lossy(c);
let (name, value) = s.split_once('=')?;
name.eq_ignore_ascii_case(tag).then(|| String::from(value))
})
}
pub fn push(&mut self, tag: &str, value: &str) {
let mut c = Vec::with_capacity(tag.len() + 1 + value.len());
c.extend_from_slice(tag.as_bytes());
c.push(b'=');
c.extend_from_slice(value.as_bytes());
self.comments.push(c);
}
}
fn read_u32(rest: &mut &[u8]) -> Result<u32, OggOpusError> {
let (head, tail) = rest.split_at_checked(4).ok_or(OggOpusError::InvalidCommentHeader)?;
*rest = tail;
Ok(u32::from_le_bytes(head.try_into().expect("4-byte slice")))
}
fn read_len_prefixed<'a>(rest: &mut &'a [u8]) -> Result<&'a [u8], OggOpusError> {
let len = read_u32(rest)? as usize;
let (head, tail) = rest.split_at_checked(len).ok_or(OggOpusError::InvalidCommentHeader)?;
*rest = tail;
Ok(head)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AudioPacket {
pub data: Vec<u8>,
pub granule_position: u64,
pub eos: bool,
}
#[derive(Debug, Clone)]
pub struct OggOpusReader<'a> {
head: OpusHead,
tags: OpusTags,
serial: u32,
data: &'a [u8],
packets: PacketReader<'a>,
queue: VecDeque<AudioPacket>,
last_position: u64,
}
impl<'a> OggOpusReader<'a> {
pub fn new(data: &'a [u8]) -> Result<Self, OggOpusError> {
let serial = PageReader::new(data)
.filter(|p| p.bos)
.find(|p| p.body.starts_with(&OPUS_HEAD_MAGIC))
.map(|p| p.serial)
.ok_or(OggOpusError::NoOpusStream)?;
let mut packets = PacketReader::new(data, serial);
let head_pkt = packets.next().ok_or(OggOpusError::NoOpusStream)?;
let head = OpusHead::parse(&head_pkt.data)?;
let tags_pkt = packets.next().ok_or(OggOpusError::InvalidCommentHeader)?;
let tags = OpusTags::parse(&tags_pkt.data)?;
Ok(OggOpusReader {
head,
tags,
serial,
data,
packets,
queue: VecDeque::new(),
last_position: 0,
})
}
#[must_use]
pub const fn head(&self) -> &OpusHead {
&self.head
}
#[must_use]
pub const fn tags(&self) -> &OpusTags {
&self.tags
}
#[must_use]
pub const fn serial(&self) -> u32 {
self.serial
}
#[must_use]
pub fn pcm_duration_48k(&self) -> Option<u64> {
let last = PageReader::new(self.data)
.filter(|p| p.serial == self.serial && p.granule_position != NO_GRANULE)
.map(|p| p.granule_position)
.last()?;
Some(last.saturating_sub(u64::from(self.head.pre_skip)))
}
#[expect(
clippy::should_implement_trait,
reason = "fallible, stateful iteration; a plain method reads better than a fused Iterator"
)]
pub fn next(&mut self) -> Option<AudioPacket> {
loop {
if let Some(pkt) = self.queue.pop_front() {
self.last_position = pkt.granule_position;
return Some(pkt);
}
let mut group: alloc::vec::Vec<OggPacket> = alloc::vec::Vec::new();
for pkt in self.packets.by_ref() {
let anchored = pkt.granule_position != NO_GRANULE;
group.push(pkt);
if anchored {
break;
}
}
let anchor = group.last()?.granule_position;
if anchor != NO_GRANULE {
let mut pos = anchor;
let mut positions: alloc::vec::Vec<u64> = group
.iter()
.rev()
.map(|p| {
let this = pos;
pos = pos.saturating_sub(packet_samples_48k(&p.data).unwrap_or(0));
this
})
.collect();
positions.reverse();
for (pkt, granule_position) in group.into_iter().zip(positions) {
self.queue.push_back(AudioPacket {
data: pkt.data,
granule_position,
eos: pkt.eos,
});
}
} else {
let mut pos = self.last_position;
for pkt in group {
pos += packet_samples_48k(&pkt.data).unwrap_or(0);
self.queue.push_back(AudioPacket {
data: pkt.data,
granule_position: pos,
eos: pkt.eos,
});
}
}
}
}
}
fn packet_samples_48k(packet: &[u8]) -> Option<u64> {
let toc = Toc::new(*packet.first()?);
let per_frame = toc.frame_size().samples_per_channel_48k() as u64;
let frames = match toc.frame_count_code() {
0 => 1,
1 | 2 => 2,
_ => u64::from(*packet.get(1)? & 0x3F),
};
Some(per_frame * frames)
}
#[derive(Debug, Clone)]
pub struct OggOpusWriter {
out: Vec<u8>,
pages: PageWriter,
granule: u64,
finished: bool,
}
impl OggOpusWriter {
#[must_use]
pub fn new(head: &OpusHead, tags: &OpusTags, serial: u32) -> Self {
let mut out = Vec::new();
let mut pages = PageWriter::new(serial);
pages.push(&mut out, &head.to_bytes(), 0, false);
pages.flush(&mut out);
pages.push(&mut out, &tags.to_bytes(), 0, false);
pages.flush(&mut out);
OggOpusWriter {
out,
pages,
granule: u64::from(head.pre_skip),
finished: false,
}
}
pub fn push(&mut self, packet: &[u8], last: bool) {
debug_assert!(!self.finished, "stream already finished");
self.granule += packet_samples_48k(packet).unwrap_or(0);
self.pages.push(&mut self.out, packet, self.granule, last);
if last {
self.finished = true;
}
}
#[must_use]
pub fn finish(mut self) -> Vec<u8> {
if !self.finished {
self.pages.push(&mut self.out, &[], self.granule, true);
}
self.out
}
}