vorbis
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
| Function | Does |
|---|---|
decode_ogg(data) -> Pcm | The 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
| Function | Does |
|---|---|
open_ogg(data) -> OggReader | Read the headers of the first Vorbis stream in data (which must outlive the reader) |
r.read(out) -> i64 | Up 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 |
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.
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:
| Channels | Order |
|---|---|
| 1 | mono |
| 2 | left, right |
| 3 | left, centre, right |
| 4 | front left, front right, rear left, rear right |
| 5 | front 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:
| Function | Does |
|---|---|
decoder() -> Decoder | A 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) -> i64 | One 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 |
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: