// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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").?); }