///|
/// Errors raised while parsing Ogg pages and Opus stream headers.
pub(all) suberror OggOpusError {
InvalidOggPage(reason~ : String)
InvalidOpusStream(reason~ : String)
} derive(Debug, Eq)
///|
fn ogg_crc_table() -> FixedArray[UInt32] {
FixedArray::makei(256, index => {
let mut value = index.reinterpret_as_uint() << 24
for _ in 0..<8 {
value = if (value & 0x80000000U) != 0 {
(value << 1) ^ 0x04C11DB7U
} else {
value << 1
}
}
value
})
}
///|
fn ogg_page_crc(page : ArrayView[Byte]) -> UInt32 {
let table = ogg_crc_table()
let mut crc = 0U
for index, byte in page {
let value : Byte = if index >= 22 && index < 26 { 0 } else { byte }
let table_index = ((crc >> 24) ^ value.to_uint()).reinterpret_as_int()
crc = (crc << 8) ^ table[table_index]
}
crc
}
///|
/// The largest packet the reader reassembles. Opus audio packets are a few
/// kilobytes; the bound leaves room for an `OpusTags` header carrying cover
/// art while stopping a stream of continued pages from growing one forever.
const MAX_OGG_PACKET_BYTES : Int32 = 16_777_216
///|
/// Streaming Ogg packet reassembler. Feed arbitrary chunks with `push`, then
/// pull complete packets with `next_packet`.
#doc(hidden)
pub struct OggPacketReader {
priv buffer : Array[Byte]
priv mut cursor : Int32
priv partial_packet : Array[Byte]
priv ready_packets : Array[Bytes]
priv mut ready_cursor : Int32
priv mut stream_serial : UInt32?
priv mut expected_page_sequence : UInt32?
}
///|
/// Create an empty reader; feed it Ogg data with `push` and drain packets
/// with `next_packet`.
#doc(hidden)
pub fn OggPacketReader::new() -> OggPacketReader {
{
buffer: [],
cursor: 0,
partial_packet: [],
ready_packets: [],
ready_cursor: 0,
stream_serial: None,
expected_page_sequence: None,
}
}
///|
/// Add bytes to the streaming input without requiring a page boundary.
#doc(hidden)
pub fn OggPacketReader::push(self : OggPacketReader, bytes : Bytes) -> Unit {
for byte in bytes {
self.buffer.push(byte)
}
}
///|
fn OggPacketReader::take_ready(self : OggPacketReader) -> Bytes? {
if self.ready_cursor >= self.ready_packets.length() {
self.ready_packets.clear()
self.ready_cursor = 0
return None
}
let packet = self.ready_packets[self.ready_cursor]
self.ready_cursor += 1
Some(packet)
}
///|
fn OggPacketReader::parse_page(
self : OggPacketReader,
) -> Bool raise OggOpusError {
let available = self.buffer.length() - self.cursor
if available < 27 {
return false
}
let start = self.cursor
// "OggS", version, header type (continuation is the low bit), granule
// position, serial, page sequence, CRC, segment count
let (continued, serial, page_sequence, expected_crc, segment_count) = match
self.buffer[start:] {
[
u32be(0x4F676753),
u8be(0),
u7be(_),
u1be(continued),
u64le(_),
u32le(serial),
u32le(page_sequence),
u32le(crc),
u8be(segment_count),
..,
] =>
(
continued == 1,
serial,
page_sequence,
crc,
segment_count.reinterpret_as_int(),
)
[u32be(0x4F676753), ..] =>
raise InvalidOggPage(reason="unsupported Ogg bitstream version")
_ => raise InvalidOggPage(reason="missing OggS capture pattern")
}
if available < 27 + segment_count {
return false
}
let mut body_length = 0
for index in 0.. self.stream_serial = Some(serial)
Some(expected) if expected != serial =>
raise InvalidOggPage(
reason="multiple logical Ogg streams are unsupported",
)
Some(_) => ()
}
match self.expected_page_sequence {
None => ()
Some(expected) if expected != page_sequence =>
raise InvalidOggPage(reason="non-contiguous Ogg page sequence")
Some(_) => ()
}
self.expected_page_sequence = Some(page_sequence + 1)
if continued && self.partial_packet.is_empty() {
raise InvalidOggPage(reason="continued page has no preceding packet")
}
if !continued && !self.partial_packet.is_empty() {
raise InvalidOggPage(reason="continued packet is missing its page flag")
}
let mut body_cursor = start + 27 + segment_count
for index in 0.. MAX_OGG_PACKET_BYTES {
raise InvalidOggPage(
reason="packet larger than \{MAX_OGG_PACKET_BYTES} bytes",
)
}
for offset in 0..= self.buffer.length() {
let rest = self.buffer[self.cursor:].to_owned()
self.buffer.clear()
self.buffer.append(rest)
self.cursor = 0
}
true
}
///|
/// Return the next complete Ogg packet, or `None` when more input is needed.
#doc(hidden)
pub fn OggPacketReader::next_packet(
self : OggPacketReader,
) -> Bytes? raise OggOpusError {
if self.take_ready() is Some(packet) {
return Some(packet)
}
while self.parse_page() {
if self.take_ready() is Some(packet) {
return Some(packet)
}
}
None
}
///|
fn OggPacketReader::has_incomplete_input(self : OggPacketReader) -> Bool {
self.cursor < self.buffer.length() || !self.partial_packet.is_empty()
}
///|
/// In-memory Ogg/Opus source. Opus packets must already represent 20 ms audio
/// frames; this demuxer does not decode or resample them.
pub struct OggOpusSource {
priv reader : OggPacketReader
priv input : &@io.Reader?
priv channels_ : Int32
priv pre_skip_ : Int32
}
///|
fn validate_opus_headers(
head : Bytes?,
tags : Bytes?,
) -> (Int32, Int32) raise OggOpusError {
guard head is Some(head) else {
raise InvalidOpusStream(reason="missing OpusHead packet")
}
// "OpusHead", version, channels, pre-skip, input sample rate, output
// gain, channel mapping family
guard head
is [
u64be(0x4F70757348656164),
u8be(version),
u8be(channels),
u16le(pre_skip),
u32le(_),
u16le(_),
u8be(_),
..,
] else {
raise InvalidOpusStream(reason="invalid OpusHead packet")
}
if version != 1 {
raise InvalidOpusStream(reason="unsupported OpusHead version")
}
if channels == 0 {
raise InvalidOpusStream(reason="OpusHead channel count is zero")
}
guard tags is Some(tags) else {
raise InvalidOpusStream(reason="missing OpusTags packet")
}
// "OpusTags"
if !(tags is [u64be(0x4F70757354616773), ..]) {
raise InvalidOpusStream(reason="invalid OpusTags packet")
}
(channels.reinterpret_as_int(), pre_skip.reinterpret_as_int())
}
///|
/// Parse a complete in-memory Ogg Opus file into an audio source, validating
/// its `OpusHead`/`OpusTags` headers.
pub fn OggOpusSource::from_bytes(
bytes : Bytes,
) -> OggOpusSource raise OggOpusError {
let reader = OggPacketReader::new()
reader.push(bytes)
let head = reader.next_packet()
let tags = reader.next_packet()
let (channels, pre_skip) = validate_opus_headers(head, tags)
{ reader, input: None, channels_: channels, pre_skip_: pre_skip, }
}
///|
/// Open an Ogg/Opus source backed by an asynchronous reader. The constructor
/// consumes and validates the `OpusHead` and `OpusTags` packets; audio packets
/// are read lazily by `AudioSource::next_frame`.
///
/// # Example
/// ```mbt nocheck
/// async test {
/// let file = @fs.open("voice.ogg")
/// defer file.close()
/// let source = @voice.OggOpusSource::from_reader(file)
/// let audio : &@voice.AudioSource = source
/// while audio.next_frame() is Some(frame) {
/// play_opus_frame(frame)
/// }
/// }
/// ```
pub async fn OggOpusSource::from_reader(input : &@io.Reader) -> OggOpusSource {
let reader = OggPacketReader::new()
let mut head : Bytes? = None
let mut tags : Bytes? = None
for ;; {
while reader.next_packet() is Some(packet) {
if head is None {
head = Some(packet)
} else {
tags = Some(packet)
break
}
}
if tags is Some(_) {
break
}
match input.read_some(max_len=8192) {
Some(chunk) => reader.push(chunk)
None => break
}
}
let (channels, pre_skip) = validate_opus_headers(head, tags)
{ reader, input: Some(input), channels_: channels, pre_skip_: pre_skip, }
}
///|
/// The channel count declared by the stream's `OpusHead` header.
pub fn OggOpusSource::channels(self : OggOpusSource) -> Int32 {
self.channels_
}
///|
/// Samples (at 48 kHz) to drop before playback, from the `OpusHead` header.
pub fn OggOpusSource::pre_skip(self : OggOpusSource) -> Int32 {
self.pre_skip_
}
///|
pub extend OggOpusSource with AudioSource::{next_frame}
///|
/// Yield the next raw Opus packet, pulling more input on demand for
/// streaming sources; `None` at end of stream.
pub impl AudioSource for OggOpusSource with fn next_frame(self) {
for ;; {
if self.reader.next_packet() is Some(packet) {
return Some(packet)
}
guard self.input is Some(input) else { return None }
match input.read_some(max_len=8192) {
Some(chunk) => self.reader.push(chunk)
None => {
if self.reader.has_incomplete_input() {
raise InvalidOpusStream(reason="truncated ogg stream")
}
return None
}
}
}
}
///|
fn push_u16_le(output : Array[Byte], value : Int32) -> Unit {
output.push(value.to_byte())
output.push((value >> 8).to_byte())
}
///|
fn push_u32_le(output : Array[Byte], value : UInt32) -> Unit {
output.push(value.to_byte())
output.push((value >> 8).to_byte())
output.push((value >> 16).to_byte())
output.push((value >> 24).to_byte())
}
///|
fn push_u64_le(output : Array[Byte], value : UInt64) -> Unit {
for shift in 0..<8 {
output.push((value >> (shift * 8)).to_byte())
}
}
///|
fn opus_packet_segment_count(packet : Bytes) -> Int32 {
packet.length() / 255 + 1
}
///|
fn append_opus_packet_lacing(laces : Array[Byte], packet : Bytes) -> Unit {
let mut remaining = packet.length()
while remaining >= 255 {
laces.push(b'\xFF')
remaining -= 255
}
laces.push(remaining.to_byte())
}
///|
/// Pure in-memory Ogg/Opus muxer. Header pages are available immediately from
/// `take_output`; audio pages become available as packet or lacing limits are
/// reached, and `finish` emits the final EOS page.
pub struct OggOpusWriter {
priv serial : UInt32
priv max_packets_per_page : Int32
priv mut page_sequence : UInt32
priv mut granule : UInt64
priv audio_packets : Array[Bytes]
priv mut audio_segments : Int32
priv output : Array[Byte]
priv mut finished : Bool
}
///|
fn OggOpusWriter::emit_page(
self : OggOpusWriter,
header_type : Byte,
granule : UInt64,
packets : Array[Bytes],
) -> Unit {
let laces : Array[Byte] = []
for packet in packets {
append_opus_packet_lacing(laces, packet)
}
let page : Array[Byte] = [b'O', b'g', b'g', b'S', 0, header_type]
push_u64_le(page, granule)
push_u32_le(page, self.serial)
push_u32_le(page, self.page_sequence)
push_u32_le(page, 0U)
page.push(laces.length().to_byte())
page.append(laces)
for packet in packets {
for byte in packet {
page.push(byte)
}
}
let checksum = ogg_page_crc(page)
page[22] = checksum.to_byte()
page[23] = (checksum >> 8).to_byte()
page[24] = (checksum >> 16).to_byte()
page[25] = (checksum >> 24).to_byte()
self.output.append(page)
self.page_sequence += 1
}
///|
fn OggOpusWriter::flush_audio_page(self : OggOpusWriter, eos~ : Bool) -> Unit {
if self.audio_packets.is_empty() {
if eos {
self.emit_page(b'\x04', self.granule, [])
}
return
}
self.emit_page(
if eos {
b'\x04'
} else {
0
},
self.granule,
self.audio_packets,
)
self.audio_packets.clear()
self.audio_segments = 0
}
///|
/// Create an Ogg/Opus muxer and queue its `OpusHead` and `OpusTags` pages.
///
/// # Example
/// ```mbt nocheck
/// async test {
/// let writer = @voice.OggOpusWriter::new(channels=2, pre_skip=312)
/// for frame in recorded_opus_frames() {
/// writer.write_frame(frame)
/// }
/// writer.finish()
/// let file = @fs.create("recording.ogg")
/// defer file.close()
/// file.write(writer.take_output())
/// }
/// ```
///
/// # Panics
/// Panics if `channels` or `pre_skip` cannot be represented in an Opus header,
/// or if `max_packets_per_page` is not positive.
pub fn OggOpusWriter::new(
channels~ : Int32 = 2,
pre_skip~ : Int32 = 0,
serial~ : UInt32 = 0U,
max_packets_per_page~ : Int32 = 50,
) -> OggOpusWriter {
if channels <= 0 || channels > 255 {
abort("OggOpusWriter channels must be between 1 and 255")
}
if pre_skip < 0 || pre_skip > 65535 {
abort("OggOpusWriter pre_skip must be between 0 and 65535")
}
if max_packets_per_page <= 0 {
abort("OggOpusWriter max_packets_per_page must be positive")
}
let writer = OggOpusWriter::{
serial,
max_packets_per_page,
page_sequence: 0U,
granule: 0UL,
audio_packets: [],
audio_segments: 0,
output: [],
finished: false,
}
let head : Array[Byte] = [
b'O',
b'p',
b'u',
b's',
b'H',
b'e',
b'a',
b'd',
1,
channels.to_byte(),
]
push_u16_le(head, pre_skip)
push_u32_le(head, 48000U)
push_u16_le(head, 0)
head.push(0)
writer.emit_page(b'\x02', 0UL, [Bytes::from_array(head)])
let tags = b"OpusTags\x0B\x00\x00\x00discord.mbt\x00\x00\x00\x00"
writer.emit_page(0, 0UL, [tags])
writer
}
///|
/// Queue one complete Opus packet and advance the 48 kHz granule position.
pub fn OggOpusWriter::write_frame(
self : OggOpusWriter,
opus : Bytes,
samples~ : Int32 = 960,
) -> Unit raise OggOpusError {
if self.finished {
raise InvalidOpusStream(reason="cannot write an Opus frame after finish")
}
if samples < 0 {
raise InvalidOpusStream(reason="Opus frame sample count is negative")
}
let segments = opus_packet_segment_count(opus)
if segments > 255 {
raise InvalidOpusStream(reason="Opus packet exceeds one Ogg page")
}
if !self.audio_packets.is_empty() && self.audio_segments + segments > 255 {
self.flush_audio_page(eos=false)
}
self.audio_packets.push(opus)
self.audio_segments += segments
self.granule += samples.to_uint64()
if self.audio_packets.length() >= self.max_packets_per_page {
self.flush_audio_page(eos=false)
}
}
///|
/// Flush remaining packets in a final EOS page. Repeated calls are no-ops.
pub fn OggOpusWriter::finish(self : OggOpusWriter) -> Unit {
guard !self.finished else { return }
self.flush_audio_page(eos=true)
self.finished = true
}
///|
/// Drain all complete Ogg pages generated so far.
pub fn OggOpusWriter::take_output(self : OggOpusWriter) -> Bytes {
let result = Bytes::from_array(self.output)
self.output.clear()
result
}