ogg

tape
import ogg from "ogg";

// the packets of the first logical stream of an Ogg file held in memory
var buf: [65536]u8;                     // for packets that span pages
var s: ogg.Stream = ogg.stream(0, buf[0..]);
var chosen: bool = false;
for (p in ogg.pages(data)) {
    if (!chosen && p.bos()) {
        s = ogg.stream(p.serial, buf[0..]);
        chosen = true;
    }
    s.push(p) or { io.println("{err}"); return 1; };
    while (true) {
        let pk: ?ogg.Packet = s.next();
        if (pk == null) { break; }
        handle(pk!.data, pk!.granule);  // a codec's packet; its page's granule position or -1
    }
}

ogg reads the Ogg container: the pages an .ogg or .opus file is made of, and the packets of one logical stream put back together across them. It knows nothing of codecs: vorbis reads its packets through it. It is pure and sans-I/O (bytes in), and freestanding (t0): nothing allocates. A packet lying within one page is a view of that page; one spanning pages is gathered in the buffer you give the stream.

Pages

FunctionDoes
parse_page(data) -> PageThe page at the start of data, its CRC checked
parse_header(data) -> PageThe same without the CRC check (a quick scan of granule positions)
pages(data)The good pages of a buffer in order, for for (p in ogg.pages(data)): garbage and damaged pages stepped over (skipped counts their bytes)
sync(data, start) -> ?i64Where the next capture pattern "OggS" begins, at or after start
crc(data), crc_update(crc, data)Ogg’s CRC-32 (polynomial 0x04c11db7, not reflected, from 0)

A Page has data (all of it), flags, granule (the codec’s position after the last packet that ends on the page; -1 when none does), serial, sequence, crc, segments and header_len; and methods len(), body(), lacing(i), continued(), bos(), eos(), packets() (how many end on it), first_packet() and check().

tape
let p: ogg.Page = ogg.parse_page(data[at..]) or {
    // Truncated: hand it more bytes; Corrupt: no page starts here; BadCrc: damaged
    return 1;
};
if (p.bos() && p.first_packet().len >= 7 && p.first_packet()[0] == 1) { /* "\x01vorbis": Vorbis */ }
at = at + p.len();

parse_page reads only the page’s own bytes: what follows it in data is not looked at. Truncated means the data ends inside the page, so a reader that receives a file in pieces waits for more.

Packets

FunctionDoes
stream(serial, buf) -> StreamPackets of the stream serial; spanning packets gathered in buf
s.push(page)Take the next page (a page of another stream is ignored)
s.next() -> ?PacketThe next whole packet, or null until the next page
s.reset()Forget what is held (after a seek)

A Packet has data, granule (its page’s granule position when it is the last packet ending on the page, else -1), bos, eos, and hole: pages were lost before it (a gap in the page sequence numbers), so the packet before it is missing and a decoder should start afresh.

tape
var buf: [65536]u8;
var s: ogg.Stream = ogg.stream(serial, buf[0..]);
s.push(page) or {
    // TooLarge: a spanning packet did not fit buf; it is dropped
};

A packet stays valid until the next push. Take every packet of a page before pushing the next: what is left is dropped. 64 KB of buf holds the packets of every common stream (a Vorbis setup header is a few KB).

Errors

OggError is Truncated (the data ends inside a page), Corrupt (no page starts there), BadCrc (the page’s checksum does not match) or TooLarge (a packet larger than the stream’s buffer). Nothing traps on any input except Page.lacing with an index outside 0..segments.

Last modified: