vorbis

tape
import vorbis from "vorbis";

// a sound effect: the whole file at once
var s: vorbis.Pcm = vorbis.decode_ogg(data) or {
    io.println("cannot decode it: {err}");
    return 1;
};
defer s.free();
play(s.samples, s.rate, s.channels);       // s.frames frames, interleaved f32

// music: decoded as it plays, looping where its tags say
var m: vorbis.OggReader = vorbis.open_ogg(song) or { return 1; };
defer m.free();
let loop_start: i64 = conv.parse_i64(m.comments().find("LOOPSTART") or { "0" }) or { 0 };
var out: [4096]f32;
while (playing) {
    let n: i64 = m.read(out[0..]);          // frames, m.info().channels samples each
    if (n == 0) {
        m.seek(loop_start);
        continue;
    }
    stream.write_f32(out[0..n * m.info().channels]) or { break; };
}

vorbis decodes Vorbis, the audio of .ogg files and of most game music, to f32 samples at full scale ±1.0, interleaved. It decodes all of Vorbis I (both floors, all three residues, coupling, every block size) for 1 to 8 channels at any rate; its samples agree with ffmpeg’s decoder to about 3e-7, and with libvorbis’s to within one 16-bit level. It is pure and sans-I/O: you hand it bytes. A 3 minute stereo song decodes in about a quarter of a second.

A whole file

FunctionDoes
decode_ogg(data) -> PcmThe first Vorbis stream of an Ogg file, decoded
p.free()Give the samples back

A Pcm has rate, channels, frames, samples (frames * channels f32 values) and comments. A few seconds of sound effect is fine this way; minutes of music are tens of MB as f32, so stream those with an OggReader.

Streaming from memory

FunctionDoes
open_ogg(data) -> OggReaderRead the headers of the first Vorbis stream in data (which must outlive the reader)
r.read(out) -> i64Up to out.len / channels frames into out; 0 at the end
r.seek(frame)Go on from frame (0 is the first)
r.position(), r.frames()The next frame read gives; the stream’s length
r.info(), r.comments()The stream’s parameters and tags
r.damaged()Damaged pages and packets skipped so far
r.free()Give the memory back
tape
var r: vorbis.OggReader = vorbis.open_ogg(data) or { return 1; };
defer r.free();
r.seek(r.info().rate * 30);                 // 30 seconds in
let n: i64 = r.read(out[0..]);

The reader gives exactly frames() frames: the padding an encoder puts before the first sample and after the last is cut (the file’s granule positions say how much). Seeking is sample-exact. Damage never stops it: a page that fails its checksum or a packet that cannot be decoded is skipped, and reading goes on with the next.

Tags

comments() is the stream’s comment header: vendor() (the encoder), count(), get(i) ("FIELD=value") and find(field), the value of the first comment with that field name, ignoring case, or null. Games keep their loop points there (LOOPSTART, LOOPLENGTH or LOOPEND, in frames); music has TITLE, ARTIST and the like.

tape
let c: vorbis.Comments = r.comments();
let title: []const u8 = c.find("title") or { "untitled" };
for (i in 0..c.count()) { io.println(c.get(i)); }

parse_comments(packet) reads a comment header packet on its own.

Channels

Channels come in Vorbis’s order, which is not every audio API’s:

ChannelsOrder
1mono
2left, right
3left, centre, right
4front left, front right, rear left, rear right
5front left, centre, front right, rear left, rear right
6 (5.1)front left, centre, front right, rear left, rear right, LFE
7 (6.1)front left, centre, front right, side left, side right, rear centre, LFE
8 (7.1)front left, centre, front right, side left, side right, rear left, rear right, LFE

Info has channels, rate, bitrate_nominal (and _max, _min; 0 when the encoder left them out), block_short and block_long.

Packet by packet

For Vorbis in another container (Matroska and WebM carry the same packets), or to drive ogg yourself:

FunctionDoes
decoder() -> DecoderA decoder; free it
d.header(packet)The three header packets, in order: identification, comment, setup
d.ready()All three are read
d.decode(packet, out) -> i64One audio packet’s samples into out; how many a channel
d.max_samples()The most one packet gives a channel: out must hold that many frames
d.samples_of(packet)How many a packet will give, from its first bits
d.reset()Forget the last block: after a seek or a lost packet
d.info(), d.comments(), d.last_error()The headers’ contents; what went wrong
tape
var d: vorbis.Decoder = vorbis.decoder();
defer d.free();
for (i in 0..3) { d.header(headers[i]) or { io.println(d.last_error()); return 1; }; }
var out: [8192]f32;
let n: i64 = d.decode(packet, out[0..]) or { 0 };

The first packet after the headers (or a reset) gives no samples: a Vorbis block only completes when the next one overlaps it. decode does not cut an encoder’s padding: a packet’s granule position (the samples up to its end) tells you how much of the last packet to keep.

Errors

VorbisError is NotVorbis (not a Vorbis stream, or headers out of order), Unsupported (more than 8 channels, a future Vorbis version), Corrupt (a malformed header or packet; last_error() says where), OutTooSmall or OutOfMemory. Nothing traps on a damaged file.

Last modified: