Something went wrong. Try again.
A FLAC encoder and decoder for Zig, written against RFC 9639, for native FLAC streams and FLAC in Ogg
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! FLAC in an Ogg container: the mapping of RFC 9639 Section 10.1, specified in//! full by Xiph's [Ogg mapping](https://xiph.org/flac/ogg_mapping.html).//!//! Native FLAC carries its own thin transport -- a signature, metadata blocks,//! and frames that find each other by sync code. Ogg is a real transport, and//! brings what that thin one cannot: several logical bitstreams in one file,//! chaining, per-page checksums, and resynchronisation after damage. The//! mapping that gets FLAC there is version 1.0, and it is deliberately the//! simple one of the two that were possible://!//! > The alternative is to treat native FLAC frames as Ogg packets and accept//! > the transport redundancy. It turns out that this is not much of a penalty;//! > a maximum of 12 bytes per frame will be wasted.//!//! So a FLAC frame is an Ogg packet, unchanged, sync code and footer CRC and//! all, and both of this file's types are thin: they move whole frames between//! `Decoder`/`Encoder` and the container, and the codec never learns which one//! it is in.//!//! # The shape of a stream//!//! | Packet | What it is |//! | --- | --- |//! | first | `Identification`: `0x7F`, `"FLAC"`, the mapping version, a count of the header packets to come, then `"fLaC"` and the streaminfo block. Alone on the first page, which is exactly 79 bytes. |//! | next | One native FLAC metadata block each. The first must be a Vorbis comment block. The last finishes its page, so audio starts a fresh one. |//! | rest | One FLAC frame each, so every one of them begins `0xFF`. |//!//! The first byte of every packet is its type, which is what makes the three//! kinds tell themselves apart: `0x7F` opens the stream, `0x01`-`0x7E` and//! `0x81`-`0xFE` are metadata blocks, and `0xFF` is a frame, because a frame//! begins with the sync code. That is also why this reads header packets until//! it meets a `0xFF` rather than trusting the count in the identification//! packet: the count is allowed to be zero for "unknown", and a wrong one is a//! fault in a tag rather than in the audio.//!//! # Decoding//!//! ```zig//! var buffer: [flac.ogg.page_buffer_size]u8 = undefined;//! var d: flac.ogg.Decoder = try .init(gpa, reader, &buffer, .{});//! defer d.deinit();//!//! while (try d.next()) |frame| {//! // frame.channels, exactly as from a native decoder//! }//! try d.finish();//! ```//!//! # Encoding//!//! ```zig//! var e: flac.ogg.Encoder = try .init(gpa, writer, config, .{//! .serial_number = serial, // random: see Encoder.Options//! });//! defer e.deinit();//!//! try e.writeInterleaved(samples);//! try e.finish();//! ```//!//! # What the container changes about the codec's problems//!//! **The streaminfo block need not be patched afterwards.** In a native stream//! the frame sizes, the sample count and the MD5 are not known until the whole//! of it has been seen, so what goes at the head of the file is a placeholder a//! seekable caller writes back over. The mapping says of those same fields that//! they "may be set to zero (indicating 'unknown' in native FLAC), which also//! facilitates single-pass encoding", so leaving them is not a compromise here//! but the blessed answer. `Encoder.writeFirstPage` is for a caller who can//! seek and wants them anyway, and rewrites the whole 79-byte first page --//! because the checksum over it changes with them.//!//! **There is no seek table.** It would be a metadata block pointing at byte//! offsets in a native stream, and a granule position on every page is both//! more accurate and already there. Asking for one is a configuration error.
const std = @import("std");const Allocator = std.mem.Allocator;const assert = std.debug.assert;
const ogg = @import("ogg");
const decoder_mod = @import("decoder.zig");const encoder_mod = @import("encoder.zig");const format = @import("format.zig");const metadata = @import("metadata.zig");
const StreamInfo = metadata.StreamInfo;
/// The four bytes every Ogg page begins with. A file holding a FLAC stream/// starts with either this or `format.signature`, which is how a reader tells/// the two containers apart without being told which it has.pub const capture_pattern = ogg.Page.capture_pattern;
/// What `Decoder` needs a page buffer of. A page is bounded by its own segment/// table, so this is a constant rather than a guess.pub const page_buffer_size = ogg.Page.max_size;
/// The first packet of an Ogg FLAC logical bitstream, which identifies both the/// codec and the mapping.pub const Identification = struct { /// The byte the packet opens with, chosen so that it can be neither a /// metadata block header nor a frame sync code. pub const packet_type: u8 = 0x7f; /// The four bytes after it. Not the same as `format.signature`, which is /// `fLaC` and appears later in the same packet. pub const signature = "FLAC"; /// The mapping this implements. A decoder is expected to read any 1.x /// stream, which is what makes the minor version worth ignoring. pub const major_version: u8 = 1; pub const minor_version: u8 = 0; /// 1 + 4 + 1 + 1 + 2 for the mapping's own header, then 4 + 4 + 34 for the /// native signature and the streaminfo block. pub const encoded_length = 51; /// The whole of the first page: `Page.min_header_size`, one lacing value, /// and the packet. The mapping states this number. pub const page_size = 27 + 1 + encoded_length;
major: u8 = major_version, minor: u8 = minor_version, /// How many header packets follow this one, or zero for "unknown". Not /// relied upon -- see the note at the top of this file. header_packets: u16, streaminfo: StreamInfo,
pub const ParseError = error{ /// The packet is not an Ogg FLAC identification packet: the type byte, /// either signature, or the streaminfo block's own header is wrong. NotOggFlac, /// A major mapping version this does not implement. A 1.x decoder is /// promised any 1.y stream, and promised nothing about 2.x. UnsupportedMapping, /// The packet is shorter than the mapping's fixed 51 bytes. ShortIdentification, } || error{InvalidStreamInfo};
pub fn parse(packet: []const u8) ParseError!Identification { if (packet.len < encoded_length) return error.ShortIdentification; if (packet[0] != packet_type) return error.NotOggFlac; if (!std.mem.eql(u8, packet[1..5], signature)) return error.NotOggFlac; const major = packet[5]; const minor = packet[6]; const header_packets = std.mem.readInt(u16, packet[7..9], .big); if (!std.mem.eql(u8, packet[9..13], format.signature)) return error.NotOggFlac; // The streaminfo block keeps its native four-byte header, so the block // type has to be the streaminfo type and the length has to be the one // fixed length that block has. if (packet[13] & 0x7f != 0) return error.NotOggFlac; if (std.mem.readInt(u24, packet[14..17], .big) != StreamInfo.encoded_length) return error.NotOggFlac; // Checked after the rest, so that a stream of something else is // reported as not being Ogg FLAC rather than as a mapping from the // future. if (major != major_version) return error.UnsupportedMapping;
var body: std.Io.Reader = .fixed(packet[17..encoded_length]); const streaminfo = StreamInfo.read(&body) catch |err| switch (err) { // The reader is over exactly 34 bytes, so it cannot run out. error.EndOfStream, error.ReadFailed => unreachable, error.InvalidStreamInfo => return error.InvalidStreamInfo, }; return .{ .major = major, .minor = minor, .header_packets = header_packets, .streaminfo = streaminfo, }; }
/// Write the packet, which is always `encoded_length` bytes. pub fn write(id: Identification, w: *std.Io.Writer) metadata.WriteError!void { try w.writeByte(packet_type); try w.writeAll(signature); try w.writeByte(id.major); try w.writeByte(id.minor); try w.writeInt(u16, id.header_packets, .big); try w.writeAll(format.signature); // Never the last block: the mapping requires a Vorbis comment packet // after this one, so there is always more metadata to come. try metadata.writeBlock(w, .{ .last = false, .body = .{ .streaminfo = id.streaminfo } }); }};
/// The first byte of a FLAC frame, and so of every audio packet: the sync code/// begins with fourteen ones, which no metadata block header can (RFC 9639,/// Section 9.1.1).const frame_type: u8 = 0xff;
pub const ReadError = decoder_mod.Error || ogg.Demuxer.Error || Identification.ParseError || error{ /// No logical bitstream in the physical one identified itself as Ogg /// FLAC. NoFlacStream, /// A packet held something other than exactly one frame. The mapping is /// one frame per packet, so a packet with bytes left over after a frame /// has been decoded is a stream that cannot be trusted about where the /// next one starts. TrailingBytesInPacket, };
/// Decoding a FLAC stream out of an Ogg container.////// Everything about the audio is the native decoder's, and `flac` is that/// decoder: its `streaminfo`, `blocks`, `vorbisComment()` and `seekTable()`/// describe this stream. What this adds is the container -- finding the FLAC/// logical bitstream among however many the file holds, reading the header/// packets, and handing over one packet per frame.pub const Decoder = struct { pub const Options = struct { /// Passed through to the native decoder. flac: decoder_mod.Options = .{}, /// Passed through to the demuxer. Its limits are what a hostile file /// runs into. demuxer: ogg.Demuxer.Options = .{}, };
gpa: Allocator, demuxer: ogg.Demuxer, /// The native decoder underneath, built from the blocks the header packets /// carried. Read what the stream says about itself from here; call `next` /// on this type rather than on it. flac: decoder_mod.Decoder, /// What the first packet said. identification: Identification, /// The logical bitstream being decoded. A file can hold others, whose pages /// are skipped. serial_number: u32, /// Frame bytes handed over so far, which is what `Frame.offset` counts. audio_bytes: u64, /// Whether a packet has been seen on the page carrying the end-of-stream /// flag. /// /// True is proof the logical bitstream ended where it said it would. False /// is not proof of the opposite: RFC 3533 Section 4 allows the last page to /// be a "nil" one carrying no content, and a packet is what this is counting. /// So it is worth reporting and not worth refusing a file over -- which is /// also why `finish` does not look at it. What does catch a truncated stream /// is the sample count and the MD5, when the stream stated them. ended: bool, /// The first audio packet, which `init` had to read to find out that the /// header packets had finished. The first call to `next` decodes it. pending: ?[]const u8 = null,
/// Read as far as the first audio packet: find the FLAC logical bitstream, /// parse its identification packet, and collect its metadata blocks. /// /// `buffer` is where the demuxer assembles pages; it belongs to the caller /// and must outlive the decoder. `page_buffer_size` is how big it is. pub fn init( gpa: Allocator, input: *std.Io.Reader, buffer: *[page_buffer_size]u8, options: Options, ) ReadError!Decoder { var demuxer: ogg.Demuxer = .init(gpa, input, buffer, options.demuxer); errdefer demuxer.deinit();
// A physical bitstream can carry other codecs, and a chain can carry // other streams; the one wanted is the first whose first packet says it // is Ogg FLAC. const identification, const serial_number = while (try demuxer.next()) |packet| { if (!packet.first) continue; const id = Identification.parse(packet.data) catch |err| switch (err) { // Some other codec's identification packet, or some other // mapping's. Neither is this file being wrong. error.NotOggFlac, error.ShortIdentification => continue, else => |e| return e, }; break .{ id, packet.serial_number }; } else return error.NoFlacStream;
// The streaminfo block came out of the first packet; every other // metadata block is a packet of its own. var blocks: std.ArrayList(metadata.Block) = .empty; // Each block, then the list -- not `metadata.freeBlocks`, which frees // the slice it is given as well and would be freeing an allocation the // list is about to free again. errdefer { for (blocks.items) |block| block.deinit(gpa); blocks.deinit(gpa); } try blocks.append(gpa, .{ .last = false, .body = .{ .streaminfo = identification.streaminfo } });
var first_frame: ?[]const u8 = null; while (try demuxer.next()) |packet| { if (packet.serial_number != serial_number) continue; if (packet.data.len == 0) continue; if (packet.data[0] == frame_type) { first_frame = packet.data; break; } var body: std.Io.Reader = .fixed(packet.data); const block = try metadata.readBlock(gpa, &body); errdefer block.deinit(gpa); try blocks.append(gpa, block); }
const owned = try blocks.toOwnedSlice(gpa); // From here the blocks belong to the native decoder, which frees them // whether it is built or refuses to be. const d: Decoder = .{ .gpa = gpa, .demuxer = demuxer, .flac = try .initFromBlocks(gpa, owned, options.flac), .identification = identification, .serial_number = serial_number, .audio_bytes = 0, .ended = false, // The packet that ended the headers was read to find out that it // was audio, so it is kept rather than thrown away. It borrows the // demuxer's page buffer, which nothing touches until `next`. .pending = first_frame, }; return d; }
pub fn deinit(d: *Decoder) void { d.flac.deinit(); d.demuxer.deinit(); d.* = undefined; }
/// Decode the next frame, or return null at the end of the logical /// bitstream. pub fn next(d: *Decoder) ReadError!?decoder_mod.Frame { const packet = packet: { if (d.pending) |bytes| { d.pending = null; break :packet bytes; } while (try d.demuxer.next()) |packet| { if (packet.serial_number != d.serial_number) continue; if (packet.last) d.ended = true; if (packet.data.len == 0) continue; break :packet packet.data; } return null; };
var input: std.Io.Reader = .fixed(packet); const frame = try d.flac.decodeFrame(&input, d.audio_bytes) orelse return null; // One frame per packet is the mapping. Anything after the frame footer // means the writer and this disagree about the framing, and going on // would be guessing. if (input.bufferedLen() != 0) return error.TrailingBytesInPacket; d.audio_bytes += packet.len; return frame; }
/// Check what the streaminfo block promised about the stream as a whole. pub fn finish(d: *Decoder) ReadError!void { try d.flac.finish(); }};
pub const WriteError = encoder_mod.Error || Allocator.Error;
/// Encoding a FLAC stream into an Ogg container.////// The frames are the native encoder's; this decides nothing about the audio./// What it does is put each one in a packet of its own, put the metadata in the/// packets the mapping asks for, and keep the granule positions right.pub const Encoder = struct { pub const Options = struct { /// Passed through to the native encoder, except that the head it would /// write is suppressed -- the mapping puts the signature, the streaminfo /// block and every other metadata block in packets instead -- and that /// `seek_points` must be zero. flac: encoder_mod.Options = .{},
/// Names the logical bitstream. It has to be unique within the physical /// one, including across a chain, which is why RFC 3533 Section 4 has it /// "created randomly and does not have any connection to the content or /// encoder". /// /// There is no default because this library has no source of entropy: it /// takes a `std.Io.Writer` rather than a `std.Io`, deliberately, so the /// caller is the one that can ask for random bytes. `std.Io.random` is /// where they come from. serial_number: u32,
/// How much a page is filled to before it is closed. See /// `ogg.StreamWriter.Options`. target_page_size: usize = 4096, };
gpa: Allocator, /// Where the pages go. stream: ogg.StreamWriter, page_body: *[ogg.Page.max_body_size]u8, /// Where the native encoder writes. Holds at most one frame at a time: it is /// drained into a packet as soon as the frame is complete. /// /// On the heap because the native encoder holds a pointer to the writer /// inside it, and this type is returned by value: a buffer that moved out /// from under that pointer is a segmentation fault on the first frame. frames: *std.Io.Writer.Allocating, /// The native encoder. `streaminfo` is its, and after `finish` holds the /// true figures for the stream. flac: encoder_mod.Encoder,
/// The first page as it was written, so that `writeFirstPage` can write it /// again with the streaminfo block `finish` worked out. header_packets: u16,
pub fn init( gpa: Allocator, output: *std.Io.Writer, config: encoder_mod.Config, options: Options, ) WriteError!Encoder { if (options.flac.seek_points != 0) return error.InvalidConfiguration;
const page_body = try gpa.create([ogg.Page.max_body_size]u8); errdefer gpa.destroy(page_body);
const frames = try gpa.create(std.Io.Writer.Allocating); frames.* = .init(gpa); errdefer { frames.deinit(); gpa.destroy(frames); }
var e: Encoder = .{ .gpa = gpa, .stream = .init(output, page_body, .{ .serial_number = options.serial_number, .target_body_size = options.target_page_size, }), .page_body = page_body, .frames = frames, .flac = undefined, .header_packets = 0, };
var flac_options = options.flac; flac_options.write_head = false; e.flac = try .init(gpa, &e.frames.writer, config, flac_options); errdefer e.flac.deinit(); // `write_head = false` means nothing was written, so the buffer still // holds nothing and the first thing in it will be the first frame. assert(e.frames.written().len == 0);
try e.writeHead(options.flac.metadata, options.flac.padding); return e; }
pub fn deinit(e: *Encoder) void { e.flac.deinit(); e.frames.deinit(); e.gpa.destroy(e.frames); e.gpa.destroy(e.page_body); e.* = undefined; }
/// The identification packet, alone on the first page, then one packet per /// metadata block, then a page break so that audio starts a fresh page -- /// all three of which the mapping requires rather than merely permits. fn writeHead(e: *Encoder, blocks: []const metadata.Block, padding: u32) WriteError!void { // The first header packet has to be a Vorbis comment block. If the // caller's first block is one it is used; otherwise an empty one goes in // front of whatever they did ask for. const prepend_comment = blocks.len == 0 or blocks[0].body != .vorbis_comment; const count = blocks.len + @intFromBool(prepend_comment) + @intFromBool(padding > 0); if (count > std.math.maxInt(u16)) return error.MetadataTooLong; e.header_packets = @intCast(count);
try e.writeIdentification();
var left = count; if (prepend_comment) { left -= 1; try e.writeHeaderPacket(.{ .last = left == 0, .body = .{ .vorbis_comment = .{ .vendor = "", .fields = &.{} } }, }); } for (blocks) |block| { left -= 1; var copy = block; copy.last = left == 0; try e.writeHeaderPacket(copy); } if (padding > 0) { left -= 1; try e.writeHeaderPacket(.{ .last = left == 0, .body = .{ .padding = padding } }); }
// "the last will finish the page on which it ends, so that the first // audio packet begins a page". try e.stream.flush(); }
fn writeIdentification(e: *Encoder) WriteError!void { var packet: [Identification.encoded_length]u8 = undefined; var w: std.Io.Writer = .fixed(&packet); const id: Identification = .{ .header_packets = e.header_packets, .streaminfo = e.flac.streaminfo, }; // A fixed writer over exactly the right number of bytes, and a // streaminfo block that came from a validated configuration. id.write(&w) catch unreachable; assert(w.end == packet.len); // Header pages carry a granule position of zero, which the mapping // states and which is not the same as "no packet finished here". try e.stream.writePacket(&packet, 0); }
fn writeHeaderPacket(e: *Encoder, block: metadata.Block) WriteError!void { // Straight into the page body rather than through a buffer of its own: a // metadata block can be sixteen megabytes, so it is written to the // frame buffer, handed over, and the buffer emptied again. assert(e.frames.written().len == 0); try metadata.writeBlock(&e.frames.writer, block); try e.stream.writePacket(e.frames.written(), 0); e.frames.clearRetainingCapacity(); }
/// Hand over samples, interleaved. See `Encoder.writeInterleaved`. pub fn writeInterleaved(e: *Encoder, samples: []const i32) WriteError!void { const channels = e.flac.config.channels; if (samples.len % channels != 0) return error.RaggedInput; var at: usize = 0; while (at < samples.len) { // Never more than one frame's worth at a time, so that what the // native encoder writes is one frame and this knows where it ends. const room = @as(usize, e.flac.samplesUntilFrame()) * channels; const take = @min(room, samples.len - at); try e.flac.writeInterleaved(samples[at..][0..take]); at += take; try e.emitFrame(); } }
/// Hand over samples a channel at a time. See `Encoder.writePlanar`. pub fn writePlanar(e: *Encoder, channels: []const []const i32) WriteError!void { if (channels.len != e.flac.config.channels) return error.RaggedInput; const count = channels[0].len; for (channels) |channel| if (channel.len != count) return error.RaggedInput;
var rows: [format.limits.max_channels][]const i32 = undefined; var at: usize = 0; while (at < count) { const take = @min(@as(usize, e.flac.samplesUntilFrame()), count - at); for (channels, 0..) |channel, i| rows[i] = channel[at..][0..take]; try e.flac.writePlanar(rows[0..channels.len]); at += take; try e.emitFrame(); } }
/// Put whatever frame the native encoder has just finished into a packet of /// its own. Nothing to do if it is still filling a block. fn emitFrame(e: *Encoder) WriteError!void { const frame = e.frames.written(); if (frame.len == 0) return; // The granule position of a page is where the stream stands after the // last packet to finish on it, so it is the sample count including this // frame -- the same semantics as Ogg Vorbis, which is what the mapping // says to use. try e.stream.writePacket(frame, @intCast(e.flac.samples_written)); e.frames.clearRetainingCapacity(); }
/// Write the last frame, close the logical bitstream, and work out the /// streaminfo block the stream really wants. Nothing may be written /// afterwards. pub fn finish(e: *Encoder) WriteError!void { try e.flac.finish(); const tail = e.frames.written(); if (tail.len != 0) { // The end-of-stream flag belongs on the page carrying the last // frame, which is what `writeLastPacket` is for. try e.stream.writeLastPacket(tail, @intCast(e.flac.samples_written)); e.frames.clearRetainingCapacity(); } else { try e.stream.finish(); } }
/// The first page as it would be written now: after `finish`, with the frame /// sizes, the sample count and the MD5 that the stream turned out to have. /// /// A caller writing to something it can seek in puts this back over the /// first `Identification.page_size` bytes of the output, which is what /// `zig-flac encode --ogg` does. A caller writing to a pipe leaves the /// zeroes, which the mapping explicitly allows and which no decoder minds. /// /// The whole page has to be written rather than the streaminfo block inside /// it, because the page's checksum covers the block. pub fn writeFirstPage(e: *const Encoder, w: *std.Io.Writer) WriteError!void { var packet: [Identification.encoded_length]u8 = undefined; var into: std.Io.Writer = .fixed(&packet); const id: Identification = .{ .header_packets = e.header_packets, .streaminfo = e.flac.streaminfo, }; id.write(&into) catch unreachable;
const page: ogg.Page = .{ .granule_position = 0, .serial_number = e.stream.serial_number, .sequence_number = 0, .checksum = 0, .header_type = .{ .first = true }, .lacing = &.{@intCast(packet.len)}, .body = &packet, }; try page.write(w); }
/// Bytes handed to the output so far: the whole stream, once `finish` has /// been called. pub fn bytesWritten(e: *const Encoder) u64 { return e.stream.bytesWritten(); }};
const testing = std.testing;
/// The same three quarters of a second of a 440 Hz tone, encoded by ffmpeg/// twice: once into an Ogg FLAC stream and once into a native one. Lossless/// means the two have to decode to the same samples, which is what makes the/// pair worth having over either on its own. `src/tests/ogg/README.md` says how/// they were made.const golden_ogg = @embedFile("tests/ogg/tone.oga");const golden_native = @embedFile("tests/ogg/tone.flac");
fn pageBuffer() !*[page_buffer_size]u8 { return testing.allocator.create([page_buffer_size]u8);}
test "the identification packet of a stream ffmpeg wrote" { // The first page is 79 bytes: 27 of header, one lacing value, and the // 51-byte packet. try testing.expectEqual(@as(usize, 79), Identification.page_size); const packet = golden_ogg[28..][0..Identification.encoded_length]; const id = try Identification.parse(packet);
try testing.expectEqual(@as(u8, 1), id.major); try testing.expectEqual(@as(u8, 0), id.minor); try testing.expectEqual(@as(u16, 1), id.header_packets); try testing.expectEqual(@as(u32, 8000), id.streaminfo.sample_rate); try testing.expectEqual(@as(u8, 1), id.streaminfo.channels); try testing.expectEqual(@as(u8, 16), id.streaminfo.bits_per_sample); try testing.expectEqual(@as(u16, 1024), id.streaminfo.max_block_size);
// The two fields the mapping says may be left unknown, and which a // single-pass encoder does leave unknown. try testing.expectEqual(@as(u64, 0), id.streaminfo.total_samples); try testing.expect(std.mem.allEqual(u8, &id.streaminfo.md5, 0));
// Writing it back reproduces the packet byte for byte. var out: [Identification.encoded_length]u8 = undefined; var w: std.Io.Writer = .fixed(&out); try id.write(&w); try testing.expectEqualSlices(u8, packet, &out);}
test "an identification packet is rejected rather than half believed" { const packet = golden_ogg[28..][0..Identification.encoded_length]; try testing.expectError(error.ShortIdentification, Identification.parse(packet[0..50]));
var broken = packet.*; broken[0] = 0x01; try testing.expectError(error.NotOggFlac, Identification.parse(&broken));
broken = packet.*; broken[2] = 'l'; try testing.expectError(error.NotOggFlac, Identification.parse(&broken));
// The native signature inside the packet, which is a different four bytes // from the one before it. broken = packet.*; broken[9] = 'F'; try testing.expectError(error.NotOggFlac, Identification.parse(&broken));
// A block that is not the streaminfo block, or is not the length a // streaminfo block is. broken = packet.*; broken[13] = 4; try testing.expectError(error.NotOggFlac, Identification.parse(&broken)); broken = packet.*; broken[16] = 33; try testing.expectError(error.NotOggFlac, Identification.parse(&broken));
// A mapping from the future is told apart from something that is not Ogg // FLAC at all. broken = packet.*; broken[5] = 2; try testing.expectError(error.UnsupportedMapping, Identification.parse(&broken));}
/// Decode a whole stream, either way round, and give back its samples.fn decodeAll(stream: []const u8, container: enum { native, ogg }) ![]i32 { var samples: std.ArrayList(i32) = .empty; errdefer samples.deinit(testing.allocator); var input: std.Io.Reader = .fixed(stream);
switch (container) { .native => { var d: decoder_mod.Decoder = try .init(testing.allocator, &input, .{}); defer d.deinit(); while (try d.next()) |frame| { for (0..frame.header.block_size) |sample| { for (frame.channels) |channel| try samples.append(testing.allocator, channel[sample]); } } try d.finish(); }, .ogg => { const buffer = try pageBuffer(); defer testing.allocator.destroy(buffer); var d: Decoder = try .init(testing.allocator, &input, buffer, .{}); defer d.deinit(); while (try d.next()) |frame| { for (0..frame.header.block_size) |sample| { for (frame.channels) |channel| try samples.append(testing.allocator, channel[sample]); } } try d.finish(); }, } return samples.toOwnedSlice(testing.allocator);}
test "the container makes no difference to the audio" { const from_ogg = try decodeAll(golden_ogg, .ogg); defer testing.allocator.free(from_ogg); const from_native = try decodeAll(golden_native, .native); defer testing.allocator.free(from_native);
try testing.expectEqual(@as(usize, 6000), from_ogg.len); try testing.expectEqualSlices(i32, from_native, from_ogg);}
test "what the header packets carried is what the decoder reports" { var input: std.Io.Reader = .fixed(golden_ogg); const buffer = try pageBuffer(); defer testing.allocator.destroy(buffer); var d: Decoder = try .init(testing.allocator, &input, buffer, .{}); defer d.deinit();
// The streaminfo block out of the first packet, and the one header packet // this file has, which the mapping requires to be a Vorbis comment. try testing.expectEqual(@as(usize, 2), d.flac.blocks.len); try testing.expectEqual(@backingInt(metadata.BlockType.streaminfo), d.flac.blocks[0].blockType()); try testing.expectEqual(@backingInt(metadata.BlockType.vorbis_comment), d.flac.blocks[1].blockType()); try testing.expect(d.flac.vorbisComment() != null); try testing.expectEqual(@as(u32, 8000), d.flac.streaminfo.sample_rate);}
/// A tone, so that a round trip has something with structure in it rather than/// silence.fn tone(gpa: Allocator, count: usize, channels: u8) ![]i32 { const samples = try gpa.alloc(i32, count * channels); for (0..count) |i| { for (0..channels) |channel| { const phase = @as(f64, @floatFromInt(i)) * 0.05 * @as(f64, @floatFromInt(channel + 1)); samples[i * channels + channel] = @intFromFloat(@sin(phase) * 8000.0); } } return samples;}
const RoundTrip = struct { stream: []u8, samples: []i32,
fn deinit(rt: RoundTrip) void { testing.allocator.free(rt.stream); testing.allocator.free(rt.samples); }};
/// Encode a tone into an Ogg FLAC stream, patching the first page afterwards so/// that the streaminfo block holds the figures the stream turned out to have.fn encodeTone(count: usize, channels: u8, options: Encoder.Options) !RoundTrip { const samples = try tone(testing.allocator, count, channels); errdefer testing.allocator.free(samples);
var out: std.Io.Writer.Allocating = .init(testing.allocator); errdefer out.deinit(); { var e: Encoder = try .init(testing.allocator, &out.writer, .{ .sample_rate = 44100, .channels = channels, .bits_per_sample = 16, }, options); defer e.deinit(); try e.writeInterleaved(samples); try e.finish();
var patched: std.Io.Writer = .fixed(out.written()[0..Identification.page_size]); try e.writeFirstPage(&patched); } return .{ .stream = try out.toOwnedSlice(), .samples = samples };}
test "a round trip through the container is lossless" { // Block sizes either side of a page, a page size that forces frames to span // pages, and one and two channels. for ([_]struct { count: usize, channels: u8, block: u32, page: usize }{ .{ .count = 5000, .channels = 1, .block = 4096, .page = 4096 }, .{ .count = 5000, .channels = 2, .block = 1024, .page = 4096 }, // Pages smaller than a frame, so that every frame spans several. .{ .count = 9000, .channels = 2, .block = 4096, .page = 1 }, // One frame and a bit, so that the last frame is short. .{ .count = 4097, .channels = 1, .block = 4096, .page = 4096 }, // Exactly one frame, so that there is no short last frame at all. .{ .count = 4096, .channels = 1, .block = 4096, .page = 4096 }, }) |shape| { const rt = try encodeTone(shape.count, shape.channels, .{ .serial_number = 0xfeedface, .target_page_size = shape.page, .flac = .{ .block_size = shape.block, .padding = 0 }, }); defer rt.deinit();
const back = try decodeAll(rt.stream, .ogg); defer testing.allocator.free(back); try testing.expectEqualSlices(i32, rt.samples, back); }}
test "patching the first page is what makes the MD5 checkable" { const rt = try encodeTone(5000, 1, .{ .serial_number = 1, .flac = .{ .padding = 0 } }); defer rt.deinit();
// `decodeAll` calls `finish`, which is what compares the MD5, so this // passing is the check. Without the patch the field is all zeroes and // `finish` has nothing to compare -- so the stream is also checked for // having a non-zero one at all. const id = try Identification.parse(rt.stream[28..][0..Identification.encoded_length]); try testing.expect(!std.mem.allEqual(u8, &id.streaminfo.md5, 0)); try testing.expectEqual(@as(u64, 5000), id.streaminfo.total_samples); try testing.expect(id.streaminfo.max_frame_size != 0);
const back = try decodeAll(rt.stream, .ogg); defer testing.allocator.free(back); try testing.expectEqualSlices(i32, rt.samples, back);}
test "an unpatched stream is legal and decodes without a checkable MD5" { // Which is what a stream written to a pipe is: the mapping says those // fields "may be set to zero", so nothing is wrong with it. const samples = try tone(testing.allocator, 5000, 1); defer testing.allocator.free(samples);
var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); { var e: Encoder = try .init(testing.allocator, &out.writer, .{ .sample_rate = 44100, .channels = 1, .bits_per_sample = 16, }, .{ .serial_number = 2, .flac = .{ .padding = 0 } }); defer e.deinit(); try e.writeInterleaved(samples); try e.finish(); }
const id = try Identification.parse(out.written()[28..][0..Identification.encoded_length]); try testing.expect(std.mem.allEqual(u8, &id.streaminfo.md5, 0)); try testing.expectEqual(@as(u64, 0), id.streaminfo.total_samples);
const back = try decodeAll(out.written(), .ogg); defer testing.allocator.free(back); try testing.expectEqualSlices(i32, samples, back);}
/// Every page of a stream, with its body copied out, so that a test can say/// what the framing came out as.fn pagesOf(stream: []const u8, out: *std.ArrayList(ogg.Page)) !void { var input: std.Io.Reader = .fixed(stream); const buffer = try pageBuffer(); defer testing.allocator.destroy(buffer); var pr: ogg.PageReader = .init(&input, buffer); while (try pr.next()) |page| { try out.append(testing.allocator, .{ .granule_position = page.granule_position, .serial_number = page.serial_number, .sequence_number = page.sequence_number, .checksum = page.checksum, .header_type = page.header_type, .lacing = try testing.allocator.dupe(u8, page.lacing), .body = try testing.allocator.dupe(u8, page.body), }); }}
fn freePages(pages: *std.ArrayList(ogg.Page)) void { for (pages.items) |page| { testing.allocator.free(page.lacing); testing.allocator.free(page.body); } pages.deinit(testing.allocator);}
test "the framing is what the mapping requires" { const rt = try encodeTone(9000, 2, .{ .serial_number = 0x11223344, .flac = .{ .block_size = 4096, .padding = 0 }, }); defer rt.deinit();
var pages: std.ArrayList(ogg.Page) = .empty; defer freePages(&pages); try pagesOf(rt.stream, &pages);
// "This first packet is the only packet in the first page of the stream. // This results in a first Ogg page of exactly 79 bytes." try testing.expect(pages.items[0].header_type.first); try testing.expectEqual(@as(usize, Identification.page_size), pages.items[0].size()); try testing.expectEqualSlices(u8, &.{Identification.encoded_length}, pages.items[0].lacing); try testing.expectEqual(@as(i64, 0), pages.items[0].granule_position);
// "The first of these must be a VORBIS_COMMENT block." try testing.expectEqual(@as(u8, 0x80 | 4), pages.items[1].body[0]); // "The granule position of these first pages containing only headers is // zero." try testing.expectEqual(@as(i64, 0), pages.items[1].granule_position);
// "The first audio packet of the logical stream begins a fresh Ogg page." try testing.expectEqual(@as(u8, frame_type), pages.items[2].body[0]); try testing.expect(!pages.items[2].header_type.continued);
// "The last page is marked 'end of stream' in the page flags", and its // granule position is where the audio got to. const last = pages.items[pages.items.len - 1]; try testing.expect(last.header_type.last); try testing.expectEqual(@as(i64, 9000), last.granule_position);
// Every page belongs to the one logical bitstream, numbered in order. for (pages.items, 0..) |page, i| { try testing.expectEqual(@as(u32, 0x11223344), page.serial_number); try testing.expectEqual(@as(u32, @intCast(i)), page.sequence_number); }}
test "granule positions count the samples decoded so far" { // Small pages and a small block size, so that there are several audio pages // and each carries a position of its own. const rt = try encodeTone(9000, 1, .{ .serial_number = 1, .target_page_size = 1, .flac = .{ .block_size = 1024, .padding = 0 }, }); defer rt.deinit();
var pages: std.ArrayList(ogg.Page) = .empty; defer freePages(&pages); try pagesOf(rt.stream, &pages);
// One page per frame at this page size: 8 full blocks of 1024 and a short // last one of 808. var positions: std.ArrayList(i64) = .empty; defer positions.deinit(testing.allocator); for (pages.items[2..]) |page| try positions.append(testing.allocator, page.granule_position); try testing.expectEqualSlices(i64, &.{ 1024, 2048, 3072, 4096, 5120, 6144, 7168, 8192, 9000 }, positions.items);}
test "a frame spanning pages comes back whole" { // A page holds at most 65025 bytes, so the only frame that can span one is a // frame bigger than that -- which needs eight channels of 24-bit audio that // does not compress. Noise is what does not compress. const channels = 8; const count = 4096 * 2; const samples = try testing.allocator.alloc(i32, count * channels); defer testing.allocator.free(samples); var prng: std.Random.DefaultPrng = .init(0x511ce); const random = prng.random(); for (samples) |*sample| sample.* = random.intRangeAtMost(i32, -(1 << 23), (1 << 23) - 1);
var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); { var e: Encoder = try .init(testing.allocator, &out.writer, .{ .sample_rate = 48000, .channels = channels, .bits_per_sample = 24, }, .{ .serial_number = 1, .flac = .{ .block_size = 4096, .padding = 0 } }); defer e.deinit(); try e.writeInterleaved(samples); try e.finish(); }
var pages: std.ArrayList(ogg.Page) = .empty; defer freePages(&pages); try pagesOf(out.written(), &pages);
// Each frame is around 98 kB of verbatim subframes, so each one fills a // whole page and continues onto the next. var continued: usize = 0; for (pages.items) |page| { if (page.header_type.continued) continued += 1; } try testing.expect(continued >= 2);
const back = try decodeAll(out.written(), .ogg); defer testing.allocator.free(back); try testing.expectEqualSlices(i32, samples, back);}
test "the FLAC stream is found among others" { // A physical bitstream carrying a decoy logical bitstream as well. Its bos // page comes first, so the decoder has to walk past it. const samples = try tone(testing.allocator, 3000, 1); defer testing.allocator.free(samples);
var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); const decoy_body = try testing.allocator.create([ogg.Page.max_body_size]u8); defer testing.allocator.destroy(decoy_body);
var decoy: ogg.StreamWriter = .init(&out.writer, decoy_body, .{ .serial_number = 0xaaaa }); // Every bos page has to precede every other page, so the decoy's goes out // before the encoder is even built. try decoy.writePacket("OpusHead" ++ @as([11]u8, @splat(0x00)), 0); { var e: Encoder = try .init(testing.allocator, &out.writer, .{ .sample_rate = 44100, .channels = 1, .bits_per_sample = 16, }, .{ .serial_number = 0xbbbb, .flac = .{ .padding = 0 } }); defer e.deinit(); try e.writeInterleaved(samples); try e.finish(); } try decoy.writeLastPacket("not a FLAC frame", 1);
var input: std.Io.Reader = .fixed(out.written()); const buffer = try pageBuffer(); defer testing.allocator.destroy(buffer); var d: Decoder = try .init(testing.allocator, &input, buffer, .{}); defer d.deinit(); try testing.expectEqual(@as(u32, 0xbbbb), d.serial_number);
var count: usize = 0; while (try d.next()) |frame| count += frame.header.block_size; try testing.expectEqual(@as(usize, 3000), count); try d.finish();}
test "a stream with no FLAC in it is refused, not guessed at" { var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); const body = try testing.allocator.create([ogg.Page.max_body_size]u8); defer testing.allocator.destroy(body); var w: ogg.StreamWriter = .init(&out.writer, body, .{ .serial_number = 1 }); try w.writePacket("\x01vorbis" ++ @as([23]u8, @splat(0x00)), 0); try w.writeLastPacket("whatever", 1);
var input: std.Io.Reader = .fixed(out.written()); const buffer = try pageBuffer(); defer testing.allocator.destroy(buffer); try testing.expectError(error.NoFlacStream, Decoder.init(testing.allocator, &input, buffer, .{}));}
test "a packet holding more than one frame is refused" { // Two frames in one packet: legal Ogg, and not the mapping, which is one // frame per packet. Built by taking a stream apart into its packets and // putting two of them back as one. const rt = try encodeTone(8192, 1, .{ .serial_number = 1, .flac = .{ .block_size = 4096, .padding = 0 }, }); defer rt.deinit();
var packets: std.ArrayList([]u8) = .empty; defer { for (packets.items) |packet| testing.allocator.free(packet); packets.deinit(testing.allocator); } { var input: std.Io.Reader = .fixed(rt.stream); const buffer = try pageBuffer(); defer testing.allocator.destroy(buffer); var demuxer: ogg.Demuxer = .init(testing.allocator, &input, buffer, .{}); defer demuxer.deinit(); while (try demuxer.next()) |packet| { try packets.append(testing.allocator, try testing.allocator.dupe(u8, packet.data)); } } // The identification packet, one Vorbis comment, and two frames. try testing.expectEqual(@as(usize, 4), packets.items.len);
var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); const body = try testing.allocator.create([ogg.Page.max_body_size]u8); defer testing.allocator.destroy(body); var w: ogg.StreamWriter = .init(&out.writer, body, .{ .serial_number = 1 });
try w.writePacket(packets.items[0], 0); try w.writePacket(packets.items[1], 0); const both = try std.mem.concat(testing.allocator, u8, &.{ packets.items[2], packets.items[3] }); defer testing.allocator.free(both); try w.writeLastPacket(both, 8192);
var input: std.Io.Reader = .fixed(out.written()); const buffer = try pageBuffer(); defer testing.allocator.destroy(buffer); var d: Decoder = try .init(testing.allocator, &input, buffer, .{}); defer d.deinit(); try testing.expectError(error.TrailingBytesInPacket, d.next());}
test "a seek table cannot be asked for" { // It would be a table of byte offsets into a native stream, and the pages // carry granule positions that are both more accurate and already there. var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); try testing.expectError(error.InvalidConfiguration, Encoder.init(testing.allocator, &out.writer, .{ .sample_rate = 44100, .channels = 1, .bits_per_sample = 16, .total_samples = 1000, }, .{ .serial_number = 1, .flac = .{ .seek_points = 8 } }));}
test "the caller's own Vorbis comment is used rather than a second one" { const samples = try tone(testing.allocator, 2000, 1); defer testing.allocator.free(samples);
var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); const fields = [_][]const u8{"TITLE=A tone"}; const blocks = [_]metadata.Block{.{ .body = .{ .vorbis_comment = .{ .vendor = "zig-flac tests", .fields = &fields, } } }}; { var e: Encoder = try .init(testing.allocator, &out.writer, .{ .sample_rate = 44100, .channels = 1, .bits_per_sample = 16, }, .{ .serial_number = 1, .flac = .{ .metadata = &blocks, .padding = 0 } }); defer e.deinit(); try e.writeInterleaved(samples); try e.finish(); }
var input: std.Io.Reader = .fixed(out.written()); const buffer = try pageBuffer(); defer testing.allocator.destroy(buffer); var d: Decoder = try .init(testing.allocator, &input, buffer, .{}); defer d.deinit();
try testing.expectEqual(@as(u16, 1), d.identification.header_packets); try testing.expectEqual(@as(usize, 2), d.flac.blocks.len); const comment = d.flac.vorbisComment().?; try testing.expectEqualStrings("zig-flac tests", comment.vendor); try testing.expectEqualStrings("A tone", comment.get("TITLE").?);}