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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Turning a FLAC stream back into samples.//!//! `Decoder.init` reads the signature and every metadata block, leaving the//! stream on its first frame; `next` then yields one frame at a time until the//! stream ends, and `finish` checks what the streaminfo block promised about//! the whole of it.//!//! Sample values are handed out as `i32`, which is what the format's bit//! depths need. Everything in between is computed in `i64`: a side channel is//! one bit deeper than the frame it is in, and a prediction is wider still, so//! the specification's own advice is to leave no room for overflow to matter//! (RFC 9639, Appendix A).
const std = @import("std");const Allocator = std.mem.Allocator;const Md5 = std.crypto.hash.Md5;
const bits = @import("bits.zig");const format = @import("format.zig");const frame_mod = @import("frame.zig");const metadata = @import("metadata.zig");
const FrameHeader = frame_mod.FrameHeader;const SubframeHeader = frame_mod.SubframeHeader;const StreamInfo = metadata.StreamInfo;const limits = format.limits;
pub const Error = bits.BitReader.Error || metadata.ReadError || frame_mod.ReadError || SubframeHeader.ReadErrorSet || Allocator.Error || error{ /// The two bits naming a residual coding method were 0b10 or 0b11. ReservedResidualCoding, /// A partition order the block size cannot be divided by, or partitions /// too small to hold the predictor's warm-up samples. MalformedResidual, /// A predictor coefficient precision of 0b1111, which is forbidden, or a /// negative prediction shift, which the format no longer allows /// (RFC 9639, Appendix B.4). MalformedPredictor, /// More wasted bits than the subframe has bits. MalformedSubframe, /// The check over the whole frame did not match. FrameCrcMismatch, /// The frame states a channel count, bit depth or sample rate other than /// the one the streaminfo block states. The format permits this and a /// decoder is permitted to refuse it; this one refuses, because nothing /// downstream of it -- the MD5 over the whole stream least of all -- has /// anywhere to put the change (RFC 9639, Section 9). ChangingStreamProperties, /// A frame declared more samples than the streaminfo block allows for. BlockTooLarge, /// Decoding finished, but the audio was not what the streaminfo block's /// MD5 says it should have been. Md5Mismatch, /// Decoding finished having produced a different number of samples than /// the streaminfo block promised. WrongSampleCount, /// `initFromBlocks` was handed a list that does not begin with a /// streaminfo block. Nothing can be decoded without one, and the format /// requires it to come first (RFC 9639, Section 8.1). MissingStreamInfo, /// A subframe produced a sample too large for the bit depth it is coded /// at, or stereo decorrelation produced one too large for the frame's. /// /// A residual sample is bounded by the format, but a prediction is not /// (RFC 9639, Appendix A.4), so a stream can be written whose samples grow /// without bound however wide the arithmetic. No conforming stream does: /// a subframe's samples fit the depth the subframe codes at, by /// definition. Checking each one as it is produced is what keeps the /// arithmetic that produces the next one inside 64 bits. SampleOutOfRange,};
pub const Options = struct { /// Hash the decoded audio and compare it with the streaminfo block's MD5 /// when `finish` is called. A stream whose MD5 is all zeroes does not /// state one, and is not checked either way. verify_md5: bool = true,};
/// One decoded frame. Both slices belong to the decoder and are overwritten by/// the next call to `next`.pub const Frame = struct { header: FrameHeader, /// One slice per channel, each `header.block_size` samples long, in the /// channel order the frame header names (RFC 9639, Section 9.1.3). Any /// stereo decorrelation has already been undone. channels: []const []const i32, /// Where this frame's header starts, counted from the first byte of the /// first frame header. This is what a seek point stores. offset: u64,};
pub const Decoder = struct { gpa: Allocator, reader: bits.BitReader, options: Options,
/// Every metadata block the stream carried, in order, the first of which /// is always the streaminfo block. blocks: []metadata.Block, streaminfo: StreamInfo,
/// Subframe samples, one row of `capacity` per channel, before stereo /// decorrelation is undone. residual: []i64, /// Decoded samples, laid out the same way. samples: []i32, /// Rows of `samples`, so that a `Frame` can hand out slices. rows: [][]const i32, capacity: u32,
md5: Md5, md5_scratch: []u8, samples_decoded: u64, finished: bool,
/// Bytes of ID3v2 tag stepped over ahead of the signature, which is /// almost always zero and is only here so that `audioOffset` can be right /// about the files where it is not. id3_bytes: u32,
pub fn init(gpa: Allocator, input: *std.Io.Reader, options: Options) Error!Decoder { const id3_bytes = try metadata.skipId3v2(input); const blocks = try metadata.readAll(gpa, input); // From here on the blocks belong to `d`, and `d.deinit` is what frees // them. Nothing between this line and the `errdefer` below can fail, // so there is no window in which they would leak -- and no second // owner to free them twice. const streaminfo = blocks[0].body.streaminfo; var d: Decoder = .{ .gpa = gpa, .reader = .init(input), .options = options, .blocks = blocks, .streaminfo = streaminfo, .residual = &.{}, .samples = &.{}, .rows = &.{}, .capacity = 0, .md5 = Md5.init(.{}), .md5_scratch = &.{}, .samples_decoded = 0, .finished = false, .id3_bytes = id3_bytes, }; errdefer d.deinit(); try d.reserve(streaminfo.max_block_size); return d; }
/// Build a decoder around metadata blocks that came from somewhere other /// than the front of a native stream, and frames that will be handed over /// one at a time with `decodeFrame`. /// /// This is what a container mapping needs. An Ogg FLAC stream carries the /// streaminfo block inside its first packet and every other metadata block /// as a packet of its own, so by the time a decoder can be built the blocks /// have already been read and there is no stream left in front of the /// frames (see `src/ogg.zig`). /// /// Takes ownership of `blocks`, which `deinit` frees, exactly as `init` /// does with the ones it read. pub fn initFromBlocks(gpa: Allocator, blocks: []metadata.Block, options: Options) Error!Decoder { if (blocks.len == 0 or blocks[0].body != .streaminfo) { metadata.freeBlocks(gpa, blocks); return error.MissingStreamInfo; } const streaminfo = blocks[0].body.streaminfo; var d: Decoder = .{ .gpa = gpa, // Nothing will be read through this: every frame arrives as its // own reader. `next` on such a decoder reports the end of the // stream, which is the truthful answer. .reader = .init(std.Io.Reader.ending), .options = options, .blocks = blocks, .streaminfo = streaminfo, .residual = &.{}, .samples = &.{}, .rows = &.{}, .capacity = 0, .md5 = Md5.init(.{}), .md5_scratch = &.{}, .samples_decoded = 0, .finished = false, .id3_bytes = 0, }; errdefer d.deinit(); try d.reserve(streaminfo.max_block_size); return d; }
pub fn deinit(d: *Decoder) void { metadata.freeBlocks(d.gpa, d.blocks); d.gpa.free(d.residual); d.gpa.free(d.samples); d.gpa.free(d.rows); d.gpa.free(d.md5_scratch); d.* = undefined; }
/// The Vorbis comment block, if the stream has one. pub fn vorbisComment(d: *const Decoder) ?metadata.VorbisComment { for (d.blocks) |block| switch (block.body) { .vorbis_comment => |c| return c, else => {}, }; return null; }
/// Where the first frame header starts, counted from the beginning of the /// stream: the signature and every metadata block. A `Frame.offset` and a /// seek point's offset are both counted from there, so this is what turns /// one into a position in the file. pub fn audioOffset(d: *const Decoder) u64 { var offset: u64 = @as(u64, d.id3_bytes) + format.signature.len; for (d.blocks) |block| { // These blocks were read from the stream, so their lengths are the // ones it stated and cannot be out of range. offset += 4 + (block.bodyLength() catch unreachable); } return offset; }
/// The seek table, if the stream has one. pub fn seekTable(d: *const Decoder) ?[]const metadata.SeekPoint { for (d.blocks) |block| switch (block.body) { .seek_table => |points| return points, else => {}, }; return null; }
fn reserve(d: *Decoder, block_size: u32) Error!void { if (block_size <= d.capacity) return; if (block_size > limits.max_block_size) return error.BlockTooLarge; const channels = d.streaminfo.channels; d.gpa.free(d.residual); d.residual = &.{}; d.gpa.free(d.samples); d.samples = &.{}; d.gpa.free(d.rows); d.rows = &.{}; d.gpa.free(d.md5_scratch); d.md5_scratch = &.{};
d.residual = try d.gpa.alloc(i64, @as(usize, block_size) * channels); d.samples = try d.gpa.alloc(i32, @as(usize, block_size) * channels); d.rows = try d.gpa.alloc([]const i32, channels); d.md5_scratch = try d.gpa.alloc(u8, @as(usize, block_size) * channels * d.streaminfo.bytesPerSample()); d.capacity = block_size; }
/// Decode the next frame, or return null at the end of the stream. pub fn next(d: *Decoder) Error!?Frame { return d.decodeFrame(d.reader.input, d.reader.pos); }
/// Decode one frame out of `input`, as if it were the next thing in the /// stream, and call it `offset` bytes from the first frame header. /// /// This is the entry point a container mapping wants: Ogg FLAC hands over /// one frame per packet, so there is a fresh reader for each and no /// continuous stream for `next` to walk. Everything the decoder accumulates /// across a stream -- the MD5, the sample count, the bounds every frame is /// checked against -- carries over from one call to the next, so `finish` /// still checks the whole of it. /// /// Returns null if `input` is empty. Bytes left in `input` afterwards are /// not an error here: a caller who requires one frame per reader, as the /// Ogg mapping does, checks for them itself. pub fn decodeFrame(d: *Decoder, input: *std.Io.Reader, offset: u64) Error!?Frame { // A stream ends on a frame boundary, so there is either another sync // code here or nothing at all. _ = input.peekByte() catch |err| switch (err) { error.EndOfStream => return null, error.ReadFailed => return error.ReadFailed, };
// Safe to start the bit reader over: a frame ends byte-aligned, on the // two bytes of its footer, so there is never a partial byte to carry // across this point. d.reader = .init(input); d.reader.pos = offset; d.reader.resetCrc(); const header = try FrameHeader.read(&d.reader);
const channels = header.channels.channelCount(); if (channels != d.streaminfo.channels) return error.ChangingStreamProperties; if (header.bits_per_sample) |bps| { if (bps != d.streaminfo.bits_per_sample) return error.ChangingStreamProperties; } if (header.sample_rate) |rate| { if (rate != d.streaminfo.sample_rate) return error.ChangingStreamProperties; } if (header.block_size > d.streaminfo.max_block_size) return error.BlockTooLarge; try d.reserve(header.block_size);
const block_size = header.block_size; for (0..channels) |channel| { const row = d.residual[channel * d.capacity ..][0..block_size]; const bps = header.subframeBitsPerSample(d.streaminfo.bits_per_sample, channel); try d.decodeSubframe(row, bps); }
d.reader.alignToByte(); const expected = d.reader.crc16.final(); if (try d.reader.take(16) != expected) return error.FrameCrcMismatch;
try d.undoDecorrelation(header.channels, block_size); for (d.rows, 0..) |*row, channel| { row.* = d.samples[channel * d.capacity ..][0..block_size]; }
d.samples_decoded += block_size; if (d.options.verify_md5) d.hashFrame(block_size);
return .{ .header = header, .channels = d.rows, .offset = offset }; }
/// Check what the streaminfo block promised about the stream as a whole. /// Calling this before the stream has run out is a mistake the decoder /// cannot detect; it will simply report a sample count that is short. pub fn finish(d: *Decoder) Error!void { d.finished = true; if (d.streaminfo.total_samples != 0 and d.samples_decoded != d.streaminfo.total_samples) return error.WrongSampleCount; if (!d.options.verify_md5) return; // All zeroes is how the format says the MD5 is not known. if (std.mem.allEqual(u8, &d.streaminfo.md5, 0)) return; var digest: [Md5.digest_length]u8 = undefined; d.md5.final(&digest); if (!std.mem.eql(u8, &digest, &d.streaminfo.md5)) return error.Md5Mismatch; }
fn decodeSubframe(d: *Decoder, out: []i64, bps: u8) Error!void { const header = try SubframeHeader.read(&d.reader); if (header.wasted_bits >= bps) return error.MalformedSubframe; const depth: u7 = @intCast(bps - header.wasted_bits); const r = &d.reader;
switch (header.type) { .constant => @memset(out, try r.takeSigned(depth)), .verbatim => for (out) |*sample| { sample.* = try r.takeSigned(depth); }, .fixed => |order| { if (order > out.len) return error.MalformedResidual; for (out[0..order]) |*sample| sample.* = try r.takeSigned(depth); try d.decodeResidual(out, order); try restoreFixed(out, order, depth); }, .lpc => |order| { if (order > out.len) return error.MalformedResidual; for (out[0..order]) |*sample| sample.* = try r.takeSigned(depth);
const precision: u7 = @intCast(try r.take(4) + 1); // A stored value of 0b1111, and so a precision of 16, is // forbidden (RFC 9639, Section 9.2.6). if (precision > limits.max_qlp_precision) return error.MalformedPredictor; const shift = try r.takeSigned(5); if (shift < 0) return error.MalformedPredictor;
var coefficients: [limits.max_lpc_order]i64 = undefined; for (coefficients[0..order]) |*c| c.* = try r.takeSigned(precision);
try d.decodeResidual(out, order); try restoreLpc(out, coefficients[0..order], @intCast(shift), depth); }, }
if (header.wasted_bits != 0) { for (out) |*sample| sample.* <<= header.wasted_bits; } }
/// Fill `out[order..]` with residual samples (RFC 9639, Section 9.2.7). fn decodeResidual(d: *Decoder, out: []i64, order: u6) Error!void { const r = &d.reader; const parameter_bits: u7 = switch (try r.take(2)) { 0b00 => 4, 0b01 => 5, else => return error.ReservedResidualCoding, }; const escape: u64 = (@as(u64, 1) << @intCast(parameter_bits)) - 1;
const partition_order: u4 = @intCast(try r.take(4)); const partitions = @as(u32, 1) << partition_order; const block_size = out.len; if (block_size % partitions != 0) return error.MalformedResidual; const per_partition = block_size >> partition_order; if (per_partition <= order) return error.MalformedResidual;
var index: usize = order; for (0..partitions) |partition| { const count = if (partition == 0) per_partition - order else per_partition; const parameter = try r.take(parameter_bits); const slice = out[index..][0..count]; index += count;
if (parameter == escape) { // The partition is stored unencoded, at a width the next five // bits give; a width of zero means every sample is zero. const width: u7 = @intCast(try r.take(5)); for (slice) |*sample| sample.* = try r.takeSigned(width); } else { const k: u7 = @intCast(parameter); for (slice) |*sample| { const msb = try r.takeUnary(); const folded = (@as(u64, msb) << @intCast(k)) | try r.take(k); // Undo the zigzag: even values halve, odd values halve and // flip, which is the same as xor with the negated low bit. sample.* = @bitCast((folded >> 1) ^ (0 -% (folded & 1))); } } } }
/// Undo the stereo decorrelation the frame header names, narrowing to the /// sample type the caller sees (RFC 9639, Section 4.2). /// /// A side channel is one bit deeper than the frame, so undoing the /// decorrelation can produce a value wider than the frame's own depth if /// the two subframes did not come from one signal. No conforming stream /// does that; a stream that does is refused here rather than silently /// wrapping. fn undoDecorrelation( d: *Decoder, assignment: format.ChannelAssignment, block_size: u32, ) error{SampleOutOfRange}!void { const bounds: Bounds = .of(@intCast(d.streaminfo.bits_per_sample)); switch (assignment) { .independent => |count| for (0..count) |channel| { const from = d.residual[channel * d.capacity ..][0..block_size]; const to = d.samples[channel * d.capacity ..][0..block_size]; for (from, to) |value, *sample| sample.* = try bounds.narrow(value); }, .left_side => { const left = d.residual[0..block_size]; const side = d.residual[d.capacity..][0..block_size]; const out_left = d.samples[0..block_size]; const out_right = d.samples[d.capacity..][0..block_size]; for (left, side, out_left, out_right) |l, s, *ol, *orr| { ol.* = try bounds.narrow(l); orr.* = try bounds.narrow(l - s); } }, .right_side => { const side = d.residual[0..block_size]; const right = d.residual[d.capacity..][0..block_size]; const out_left = d.samples[0..block_size]; const out_right = d.samples[d.capacity..][0..block_size]; for (side, right, out_left, out_right) |s, rr, *ol, *orr| { ol.* = try bounds.narrow(s + rr); orr.* = try bounds.narrow(rr); } }, .mid_side => { const mid_row = d.residual[0..block_size]; const side_row = d.residual[d.capacity..][0..block_size]; const out_left = d.samples[0..block_size]; const out_right = d.samples[d.capacity..][0..block_size]; for (mid_row, side_row, out_left, out_right) |m, s, *ol, *orr| { // The bit the mid channel lost to its right shift is the // low bit of the side channel, which must have the same // parity. const mid = (m << 1) | (s & 1); ol.* = try bounds.narrow((mid + s) >> 1); orr.* = try bounds.narrow((mid - s) >> 1); } }, } }
/// Add this frame to the running MD5, which is taken over the samples /// interleaved, each sign-extended to a whole number of bytes and stored /// little-endian (RFC 9639, Section 8.2). fn hashFrame(d: *Decoder, block_size: u32) void { const channels = d.streaminfo.channels; const width = d.streaminfo.bytesPerSample(); const message = d.md5_scratch[0 .. @as(usize, block_size) * channels * width]; var at: usize = 0; for (0..block_size) |sample| { for (0..channels) |channel| { const value: u32 = @bitCast(d.samples[channel * d.capacity + sample]); for (0..width) |byte| { message[at] = @truncate(value >> @intCast(byte * 8)); at += 1; } } } d.md5.update(message); }};
/// The largest and smallest sample a subframe coded at `depth` bits can hold.const Bounds = struct { high: i64, low: i64,
fn of(depth: u7) Bounds { const magnitude = @as(i64, 1) << @intCast(depth - 1); return .{ .high = magnitude - 1, .low = -magnitude }; }
fn check(b: Bounds, value: i64) error{SampleOutOfRange}!void { if (value > b.high or value < b.low) return error.SampleOutOfRange; }
fn narrow(b: Bounds, value: i64) error{SampleOutOfRange}!i32 { try b.check(value); return @intCast(value); }};
/// Add back the prediction one of the five fixed predictors made (RFC 9639,/// Section 9.2.5, Table 20). The first `order` entries are warm-up samples and/// the rest arrive as residuals.////// Each restored sample is checked before the next one uses it, which is what/// bounds every multiplication and sum here to what an `i64` holds.fn restoreFixed(samples: []i64, order: u3, depth: u7) error{SampleOutOfRange}!void { const bounds: Bounds = .of(depth); switch (order) { 0 => {}, 1 => for (samples[1..], 0..) |*sample, i| { sample.* += samples[i]; try bounds.check(sample.*); }, 2 => for (samples[2..], 0..) |*sample, i| { sample.* += 2 * samples[i + 1] - samples[i]; try bounds.check(sample.*); }, 3 => for (samples[3..], 0..) |*sample, i| { sample.* += 3 * samples[i + 2] - 3 * samples[i + 1] + samples[i]; try bounds.check(sample.*); }, 4 => for (samples[4..], 0..) |*sample, i| { sample.* += 4 * samples[i + 3] - 6 * samples[i + 2] + 4 * samples[i + 1] - samples[i]; try bounds.check(sample.*); }, else => unreachable, }}
/// Add back the prediction the subframe's own predictor made (RFC 9639,/// Section 9.2.6). The coefficients run backwards in time: the first one/// belongs to the sample immediately before the one being predicted.fn restoreLpc( samples: []i64, coefficients: []const i64, shift: u6, depth: u7,) error{SampleOutOfRange}!void { const bounds: Bounds = .of(depth); const order = coefficients.len; for (samples[order..], order..) |*sample, i| { var prediction: i64 = 0; for (coefficients, 0..) |coefficient, j| { prediction += coefficient * samples[i - 1 - j]; } sample.* += prediction >> shift; try bounds.check(sample.*); }}
const testing = std.testing;
/// Decode a whole stream, returning its interleaved samples.fn decodeAll(gpa: Allocator, stream: []const u8) ![]i32 { var input: std.Io.Reader = .fixed(stream); var d: Decoder = try .init(gpa, &input, .{}); defer d.deinit();
var out: std.ArrayList(i32) = .empty; errdefer out.deinit(gpa); while (try d.next()) |frame| { for (0..frame.header.block_size) |sample| { for (frame.channels) |channel| try out.append(gpa, channel[sample]); } } try d.finish(); return out.toOwnedSlice(gpa);}
test "RFC 9639 example 1: verbatim subframes with wasted bits" { const gpa = testing.allocator; const samples = try decodeAll(gpa, @embedFile("tests/rfc9639/example1.flac")); defer gpa.free(samples);
// Appendix D.1.4 works both samples out by hand: "the value is then // 0b011000 11111101 00, or 25588", and 651 shifted left by its four // wasted bits, "or 10416". try testing.expectEqualSlices(i32, &.{ 25588, 10416 }, samples);}
test "RFC 9639 example 2: fixed predictors and side-right stereo" { const gpa = testing.allocator; const samples = try decodeAll(gpa, @embedFile("tests/rfc9639/example2.flac")); defer gpa.free(samples);
// Appendix D.2.9 prints the decoded samples the way the MD5 is taken over // them: interleaved, byte-aligned, little-endian and signed. These are // exactly the bytes it prints, so lining the decoded samples up the same // way is a check on all nineteen of them at once. const expected = [_]u8{ 0x84, 0x28, 0xb6, 0x17, 0x79, 0x46, 0x31, 0x29, 0x5e, 0x3a, 0x27, 0x22, 0xd4, 0x45, 0xd1, 0x28, 0x0b, 0x3d, 0xb7, 0x23, 0xeb, 0x45, 0xdf, 0x28, 0x72, 0x3f, 0x1e, 0x25, 0x9d, 0x46, 0x49, 0x29, 0xb8, 0x41, 0x70, 0x26, 0x57, 0x47, 0xb8, 0x29, 0x8f, 0x43, 0x81, 0x27, 0xae, 0xc7, 0x14, 0xdf, 0x9f, 0xc4, 0x41, 0xdd, 0x54, 0xc7, 0xe4, 0xde, 0xa5, 0xc4, 0x40, 0xdd, 0x1e, 0xc6, 0x33, 0xde, 0x82, 0xc3, 0x90, 0xdc, 0x0b, 0xc4, 0x02, 0xdd, 0x4a, 0xc1, 0x3e, 0xdb, };
try testing.expectEqual(@as(usize, expected.len / 2), samples.len); var actual: [expected.len]u8 = undefined; for (samples, 0..) |sample, i| { std.mem.writeInt(i16, actual[i * 2 ..][0..2], @intCast(sample), .little); } try testing.expectEqualSlices(u8, &expected, &actual);}
test "RFC 9639 example 3: a linear predictor and a partitioned residual" { const gpa = testing.allocator; const samples = try decodeAll(gpa, @embedFile("tests/rfc9639/example3.flac")); defer gpa.free(samples); try testing.expectEqual(@as(usize, 24), samples.len); try testing.expectEqual(@as(i32, 0), samples[0]); try testing.expectEqual(@as(i32, 79), samples[1]); try testing.expectEqual(@as(i32, 111), samples[2]);}
test "a frame whose check does not match is refused" { const gpa = testing.allocator; var stream = (@embedFile("tests/rfc9639/example3.flac")).*; // Flip a bit in the middle of the audio frame. stream[0x40] ^= 0x01; try testing.expectError(error.FrameCrcMismatch, decodeAll(gpa, &stream));}
test "a stream whose MD5 does not match is refused" { const gpa = testing.allocator; var stream = (@embedFile("tests/rfc9639/example1.flac")).*; // Flip a bit of the MD5, which is the last 16 bytes of the streaminfo // block's 34-byte body and so starts at 0x1a. stream[0x1a] ^= 0x01; try testing.expectError(error.Md5Mismatch, decodeAll(gpa, &stream));}