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.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Turning samples into a FLAC stream.//!//! Encoding a block is a search. For each channel the encoder tries the models//! the format offers -- a constant, the samples verbatim, each of the five//! fixed predictors, and a linear predictor it solves for -- costs each one in//! bits, and writes the cheapest. Stereo adds a second search on top: the same//! block can be written as two channels, as either channel beside their//! difference, or as their average beside their difference, and whichever of//! those four totals fewest bits wins.//!//! Every cost here is exact rather than estimated, because the residual coder//! makes it cheap to be exact. The cost of a Rice partition at parameter k is//! `n * (1 + k)` plus the sum of the folded residuals shifted right by k, and//! that sum is additive: partition sums at the finest partition order add up//! into the sums at every coarser order. So one pass over the residual yields//! the true cost of every parameter at every partition order, and the encoder//! picks the true best rather than one near it.//!//! What is not exact is the linear predictor itself, which is found in//! floating point (see `lpc.zig`) before being quantized to the integers the//! subframe stores. That is analysis, not coding: the residual is computed//! from the quantized predictor with integer arithmetic, so what comes out is//! lossless whatever the analysis proposed.
const std = @import("std");const Allocator = std.mem.Allocator;const Md5 = std.crypto.hash.Md5;const assert = std.debug.assert;
const bits = @import("bits.zig");const format = @import("format.zig");const frame_mod = @import("frame.zig");const lpc = @import("lpc.zig");const metadata = @import("metadata.zig");
const ChannelAssignment = format.ChannelAssignment;const FrameHeader = frame_mod.FrameHeader;const StreamInfo = metadata.StreamInfo;const SubframeHeader = frame_mod.SubframeHeader;const SubframeType = format.SubframeType;const limits = format.limits;
pub const Error = std.Io.Writer.Error || Allocator.Error || metadata.WriteError || frame_mod.WriteError || error{ /// The configuration asks for audio the format cannot describe. InvalidConfiguration, /// A sample was handed in that does not fit the configured bit depth. /// Writing it would not be lossless, so the encoder refuses it. SampleOutOfRange, /// `writeInterleaved` was given a count that is not a whole number of /// interchannel samples. RaggedInput, /// More samples arrived than the 36-bit sample count can hold. TooManySamples,};
/// What the audio is. None of it can be worked out from the samples.pub const Config = struct { sample_rate: u32, channels: u8, bits_per_sample: u8, /// Interchannel samples, if that is known before encoding starts. It ends /// up in the streaminfo block, where zero means "not known"; a caller that /// can seek should leave it at zero and let `finish` fill it in. total_samples: u64 = 0,};
/// A preset, in the spirit of the reference encoder's `-0` through `-8`: how/// hard to look for a good predictor, traded against how long that takes. Each/// one only sets the defaults for the individual options below, every one of/// which can be given instead.pub const Level = enum(u4) { @"0", @"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8",
pub const fastest: Level = .@"0"; pub const default: Level = .@"5"; pub const best: Level = .@"8";};
/// How a two-channel block is split between its two subframes.pub const Stereo = enum { /// Left in one subframe and right in the other, always. independent, /// Cost all four layouts the format allows and write the cheapest. This /// is what makes stereo worth about a tenth of the file. adaptive,};
pub const Options = struct { /// Sets the default for everything below it. level: Level = .default,
/// Interchannel samples per frame. Bigger blocks predict better and pay /// for their headers less often; smaller ones bound how much is lost to a /// damaged frame and how far a decoder must read to start. block_size: ?u32 = null, stereo: ?Stereo = null, /// The highest linear predictor order to solve for, up to the 32 the /// format allows. Zero uses only the five fixed predictors, which is quick /// and surprisingly close. max_lpc_order: ?u6 = null, /// Bits per stored predictor coefficient. Null lets `lpc.defaultPrecision` /// choose from the block size and bit depth. qlp_precision: ?u5 = null, /// How many linear predictor orders to cost in full, taken in the order /// the recursion's own error estimate ranks them. /// /// One is what that estimate alone gives, and it is usually right. It can /// be wrong, because it describes the predictor that was solved for rather /// than the one that gets stored: quantizing the coefficients to a handful /// of bits changes each order by a different amount, and occasionally /// changes which is best. Costing a few of the best-ranked orders in full /// catches that for a fraction of what costing all of them takes. Zero /// costs every order, which is as thorough as this gets. lpc_orders_costed: ?u6 = null, /// Tukey window parameters to solve a predictor against, at most /// `max_apodizations` of them. Each one weighs the block's edges against /// its middle differently and so proposes a different predictor; the /// cheapest of what they propose is written. More windows means a better /// predictor and proportionally more work. apodizations: ?[]const f64 = null, /// The range of Rice partition orders to cost. The streamable subset caps /// this at 8 (RFC 9639, Section 7). min_partition_order: u4 = 0, max_partition_order: ?u4 = null,
/// Metadata blocks to write after the streaminfo block, in this order. /// The encoder writes the signature and a streaminfo block itself. Ignored /// when `write_head` is false, since then it writes neither. metadata: []const metadata.Block = &.{}, /// Bytes of padding to reserve after those, so that tags can be added /// later without rewriting the file. Zero writes no padding block. padding: u32 = 8192, /// How many seek points to reserve, spread evenly over the stream. This /// needs `Config.total_samples`, since the table has to be written before /// the audio it points into. `Encoder.seek_points` holds the finished /// table, which a caller that can seek writes back over the placeholder at /// `Encoder.seek_table_offset`. seek_points: u32 = 0,
/// Write the `fLaC` signature and the metadata blocks at the head of the /// stream, which is what a native FLAC file wants and is the default. /// /// A container that carries those itself turns this off and gets a stream /// of nothing but frames. Ogg FLAC is such a container: its first packet /// holds the signature and the streaminfo block, and every other metadata /// block is a packet of its own (see `src/ogg.zig`). `Encoder.streaminfo` /// holds the block such a caller has to write for itself, and holds the /// true one once `finish` has been called. /// /// A seek table cannot be asked for at the same time: it is a metadata /// block, so there is nowhere for the encoder to put it, and a container /// that has granule positions of its own does not want one. write_head: bool = true,
/// The most windows a predictor search will use. pub const max_apodizations = 4;
fn resolve(o: Options, config: Config) Resolved { const level = o.level; var apodizations: [max_apodizations]f64 = @splat(lpc.default_tukey_parameter); var count: usize = 1; if (o.apodizations) |given| { count = @min(given.len, max_apodizations); for (apodizations[0..count], given[0..count]) |*into, from| into.* = from; } else switch (level) { .@"6", .@"7" => { apodizations[1] = 0.25; count = 2; }, .@"8" => { apodizations[1] = 0.25; apodizations[2] = 0.10; count = 3; }, else => {}, } const lpc_order: u6 = o.max_lpc_order orelse switch (level) { .@"0", .@"1", .@"2" => 0, .@"3" => 6, .@"4", .@"5" => 8, .@"6", .@"7" => 12, .@"8" => 32, }; const block_size: u32 = o.block_size orelse switch (level) { .@"0", .@"1", .@"2" => 1152, else => 4096, }; return .{ .block_size = block_size, .stereo = o.stereo orelse switch (level) { .@"0" => .independent, else => .adaptive, }, .max_lpc_order = lpc_order, .qlp_precision = o.qlp_precision orelse lpc.defaultPrecision(block_size, config.bits_per_sample), .lpc_orders_costed = o.lpc_orders_costed orelse switch (level) { .@"7" => 3, .@"8" => 6, else => 1, }, .apodizations = apodizations, .apodization_count = count, .min_partition_order = o.min_partition_order, .max_partition_order = o.max_partition_order orelse switch (level) { .@"0", .@"1" => 3, .@"2", .@"3", .@"4" => 4, .@"5" => 5, else => 6, }, }; }};
const Resolved = struct { block_size: u32, stereo: Stereo, max_lpc_order: u6, qlp_precision: u5, lpc_orders_costed: u6, apodizations: [Options.max_apodizations]f64, apodization_count: usize, min_partition_order: u4, max_partition_order: u4,};
/// The largest residual the format allows, in absolute value: everything must/// fit a 32-bit signed integer with the most negative value left out, so that/// a decoder can negate any of them (RFC 9639, Section 9.2.7.3).const residual_limit: u64 = 1 << 31;
/// One partition's decision: a Rice parameter, or the escape that stores the/// partition unencoded at a fixed width.const Parameter = struct { escaped: bool, value: u5,};
/// How a residual is split up and coded.const Partitioning = struct { order: u4 = 0, /// 4 or 5, the width of each partition's parameter field. parameter_bits: u3 = 4, /// Everything from the two method bits to the last code word. bits: u64 = 0,};
/// A model for one channel of one block, and what it costs.const Subframe = struct { type: SubframeType = .verbatim, wasted_bits: u6 = 0, /// Only meaningful for `.constant`. constant: i64 = 0, /// Only meaningful for `.lpc`. predictor: lpc.Quantized = .{}, partitioning: Partitioning = .{}, /// The whole subframe, header included. bits: u64 = 0,};
/// One channel's worth of working space, kept from block to block.const Channel = struct { /// The samples this subframe codes, already shifted down by whatever /// wasted bits were found. signal: []i64, /// The residual of the model in `choice`, from `predictorOrder` onwards. /// The format bounds a residual to what a 32-bit signed integer holds, and /// `fixedResidual` and `lpcResidual` refuse a model whose residual does /// not -- so this is as wide as it can need to be, and half the width /// means half the memory traffic in the loops that read it most. residual: []i32, /// Its partition parameters, `1 << choice.partitioning.order` of them. parameters: []Parameter, choice: Subframe,};
pub const Encoder = struct { gpa: Allocator, writer: bits.BitWriter, config: Config, options: Resolved,
/// The streaminfo block. Until `finish` it holds what was known when the /// stream started, which is what was written at the head of it; afterwards /// it holds the true figures. streaminfo: StreamInfo,
/// The seek table, filled in as frames are written. Empty unless /// `Options.seek_points` asked for one. seek_points: []metadata.SeekPoint, /// Where the seek table block's body starts, or null if there is none. seek_table_offset: ?u64, /// Where the first frame header starts, which is what seek point offsets /// are counted from. audio_offset: u64,
/// Samples waiting for a full block, one row of `block_size` per channel. input: []i32, filled: u32,
channels: []Channel, /// Working space shared between models, since only one is costed at a time. scratch_residual: []i32, scratch_parameters: []Parameter, narrow_parameters: []Parameter, wide_parameters: []Parameter, folded: []u32, /// Sums of the folded residual shifted right by every Rice parameter, one /// row of `sum_columns` per partition at the finest partition order. sums: []u64, counts: []u32, magnitudes: []u32, windowed: []f64, autocorrelation: []f64, lpc_coefficients: []f64, lpc_errors: []f64,
md5: Md5, md5_scratch: []u8, samples_written: u64, frames_written: u64, finished: bool,
/// Where the streaminfo block's body starts, which is always 8: four bytes /// of signature and four of block header. pub const streaminfo_body_offset = 8;
/// Rice parameters run from 0 to 30, so a cost row has 31 entries. const sum_columns = 31;
pub fn init(gpa: Allocator, output: *std.Io.Writer, config: Config, options: Options) Error!Encoder { if (config.channels < limits.min_channels or config.channels > limits.max_channels) return error.InvalidConfiguration; if (config.bits_per_sample < limits.min_bits_per_sample or config.bits_per_sample > limits.max_bits_per_sample) return error.InvalidConfiguration; if (config.sample_rate > limits.max_sample_rate) return error.InvalidConfiguration; if (config.total_samples > limits.max_total_samples) return error.InvalidConfiguration;
const resolved = options.resolve(config); if (resolved.block_size < limits.min_block_size or resolved.block_size > limits.max_block_size) return error.InvalidConfiguration; if (resolved.max_lpc_order > limits.max_lpc_order) return error.InvalidConfiguration; if (resolved.qlp_precision < limits.min_qlp_precision or resolved.qlp_precision > limits.max_qlp_precision) return error.InvalidConfiguration; if (resolved.max_partition_order < resolved.min_partition_order) return error.InvalidConfiguration; if (options.seek_points != 0 and config.total_samples == 0) return error.InvalidConfiguration; if (options.seek_points != 0 and !options.write_head) return error.InvalidConfiguration;
const block_size = resolved.block_size; // Two channels get four candidate signals: left, right, mid and side. const candidates: usize = if (config.channels == 2 and resolved.stereo == .adaptive) 4 else config.channels; const partitions: usize = @min(@as(usize, 1) << resolved.max_partition_order, block_size);
var e: Encoder = .{ .gpa = gpa, .writer = .init(output), .config = config, .options = resolved, .streaminfo = .{ .min_block_size = @intCast(block_size), .max_block_size = @intCast(block_size), .sample_rate = config.sample_rate, .channels = config.channels, .bits_per_sample = config.bits_per_sample, .total_samples = config.total_samples, }, .seek_points = &.{}, .seek_table_offset = null, .audio_offset = 0, .input = &.{}, .filled = 0, .channels = &.{}, .scratch_residual = &.{}, .scratch_parameters = &.{}, .narrow_parameters = &.{}, .wide_parameters = &.{}, .folded = &.{}, .sums = &.{}, .counts = &.{}, .magnitudes = &.{}, .windowed = &.{}, .autocorrelation = &.{}, .lpc_coefficients = &.{}, .lpc_errors = &.{}, .md5 = Md5.init(.{}), .md5_scratch = &.{}, .samples_written = 0, .frames_written = 0, .finished = false, }; errdefer e.deinit();
e.channels = try gpa.alloc(Channel, candidates); for (e.channels) |*channel| channel.* = .{ .signal = &.{}, .residual = &.{}, .parameters = &.{}, .choice = .{}, }; for (e.channels) |*channel| { channel.signal = try gpa.alloc(i64, block_size); channel.residual = try gpa.alloc(i32, block_size); channel.parameters = try gpa.alloc(Parameter, partitions); } e.input = try gpa.alloc(i32, block_size * config.channels); e.scratch_residual = try gpa.alloc(i32, block_size); e.scratch_parameters = try gpa.alloc(Parameter, partitions); e.narrow_parameters = try gpa.alloc(Parameter, partitions); e.wide_parameters = try gpa.alloc(Parameter, partitions); e.folded = try gpa.alloc(u32, block_size); e.sums = try gpa.alloc(u64, partitions * sum_columns); e.counts = try gpa.alloc(u32, partitions); e.magnitudes = try gpa.alloc(u32, partitions); e.md5_scratch = try gpa.alloc(u8, block_size * config.channels * e.streaminfo.bytesPerSample()); if (resolved.max_lpc_order > 0) { e.windowed = try gpa.alloc(f64, block_size); e.autocorrelation = try gpa.alloc(f64, @as(usize, resolved.max_lpc_order) + 1); e.lpc_coefficients = try gpa.alloc(f64, @as(usize, resolved.max_lpc_order) * resolved.max_lpc_order); e.lpc_errors = try gpa.alloc(f64, resolved.max_lpc_order); } if (options.seek_points != 0) { e.seek_points = try gpa.alloc(metadata.SeekPoint, options.seek_points); // Spread the points evenly. Neither operand can be zero: both are // checked above. const step = std.math.divCeil(u64, config.total_samples, options.seek_points) catch unreachable; for (e.seek_points, 0..) |*point, i| point.* = .{ .sample = @as(u64, i) * step, .offset = 0, .frame_samples = 0, }; }
try e.writeHead(options); return e; }
pub fn deinit(e: *Encoder) void { for (e.channels) |channel| { e.gpa.free(channel.signal); e.gpa.free(channel.residual); e.gpa.free(channel.parameters); } e.gpa.free(e.channels); e.gpa.free(e.seek_points); e.gpa.free(e.input); e.gpa.free(e.scratch_residual); e.gpa.free(e.scratch_parameters); e.gpa.free(e.narrow_parameters); e.gpa.free(e.wide_parameters); e.gpa.free(e.folded); e.gpa.free(e.sums); e.gpa.free(e.counts); e.gpa.free(e.magnitudes); e.gpa.free(e.windowed); e.gpa.free(e.autocorrelation); e.gpa.free(e.lpc_coefficients); e.gpa.free(e.lpc_errors); e.gpa.free(e.md5_scratch); e.* = undefined; }
/// The signature, a streaminfo block that is mostly a promise, whatever /// the caller wants after it, a seek table if one was asked for, and /// padding. fn writeHead(e: *Encoder, options: Options) Error!void { // A container carrying the head itself leaves the encoder writing // frames and nothing else, starting at offset zero. if (!options.write_head) return;
const w = e.writer.output; try w.writeAll(format.signature); var written: u64 = format.signature.len;
var left = options.metadata.len + @intFromBool(options.seek_points > 0) + @intFromBool(options.padding > 0); try metadata.writeBlock(w, .{ .last = left == 0, .body = .{ .streaminfo = e.streaminfo }, }); written += 4 + StreamInfo.encoded_length;
for (options.metadata) |block| { left -= 1; var copy = block; copy.last = left == 0; try metadata.writeBlock(w, copy); written += 4 + try copy.bodyLength(); } if (options.seek_points > 0) { left -= 1; e.seek_table_offset = written + 4; const block: metadata.Block = .{ .last = left == 0, .body = .{ .seek_table = e.seek_points }, }; try metadata.writeBlock(w, block); written += 4 + try block.bodyLength(); } if (options.padding > 0) { const block: metadata.Block = .{ .last = true, .body = .{ .padding = options.padding } }; try metadata.writeBlock(w, block); written += 4 + try block.bodyLength(); } e.writer.pos = written; e.audio_offset = written; }
/// Hand over samples, interleaved: the first sample of every channel, then /// the second, and so on. The length must be a whole number of those. pub fn writeInterleaved(e: *Encoder, samples: []const i32) Error!void { const channels = e.config.channels; if (samples.len % channels != 0) return error.RaggedInput; try e.checkRange(samples); var at: usize = 0; while (at < samples.len) { const room = e.options.block_size - e.filled; const available: u32 = @intCast((samples.len - at) / channels); const take = @min(room, available); for (0..take) |i| { for (0..channels) |channel| { e.input[channel * e.options.block_size + e.filled + i] = samples[at + i * channels + channel]; } } at += @as(usize, take) * channels; e.filled += take; if (e.filled == e.options.block_size) try e.writeBlock(); } }
/// Hand over samples a channel at a time. Every slice must be the same /// length, and there must be one per channel. pub fn writePlanar(e: *Encoder, channels: []const []const i32) Error!void { if (channels.len != e.config.channels) return error.RaggedInput; const count = channels[0].len; for (channels) |channel| if (channel.len != count) return error.RaggedInput; for (channels) |channel| try e.checkRange(channel);
var at: usize = 0; while (at < count) { const room = e.options.block_size - e.filled; const take: u32 = @intCast(@min(room, count - at)); for (channels, 0..) |channel, index| { const row = e.input[index * e.options.block_size + e.filled ..][0..take]; @memcpy(row, channel[at..][0..take]); } at += take; e.filled += take; if (e.filled == e.options.block_size) try e.writeBlock(); } }
/// Write whatever is left over as a last, short frame, and work out the /// streaminfo block the stream really wants. Nothing may be written /// afterwards. /// /// The block size range is left as it was written, which is what the /// format expects: an encoder "has to write these fields before receiving /// any input audio data and cannot know beforehand what block sizes it /// will use, only between what bounds the block sizes will be chosen" /// (RFC 9639, Section 8.2). pub fn finish(e: *Encoder) Error!void { assert(!e.finished); if (e.filled > 0) try e.writeBlock(); e.finished = true;
// Whatever the caller said up front, this is what the stream holds. // A count given to `Config` only decides how seek points are spread. e.streaminfo.total_samples = e.samples_written; e.md5.final(&e.streaminfo.md5); try e.writer.output.flush(); }
/// Bytes handed to the output so far: the whole stream, once `finish` has /// been called. pub fn bytesWritten(e: *const Encoder) u64 { return e.writer.pos; }
/// How many more interchannel samples the block being filled has room for, /// which is how many can be handed over before another frame is written. /// /// This is what a container mapping needs to know. Ogg FLAC puts each frame /// in a packet of its own, so it has to be handed at most this many samples /// at a time: then whatever the encoder wrote is one frame, and it knows /// where that frame ends without parsing it back (see `src/ogg.zig`). /// Never zero -- a full block is written before this returns. pub fn samplesUntilFrame(e: *const Encoder) u32 { return e.options.block_size - e.filled; }
fn writeBlock(e: *Encoder) Error!void { const block_size = e.filled; assert(block_size > 0); e.filled = 0;
const first_sample = e.samples_written; if (first_sample + block_size > limits.max_total_samples) return error.TooManySamples;
e.hashBlock(block_size); e.samples_written = first_sample + block_size;
const start = e.writer.pos; e.writer.resetCrc(); try e.encodeFrame(block_size); const size: u64 = e.writer.pos - start;
const clamped: u32 = @intCast(@min(size, limits.max_metadata_length)); if (e.frames_written == 0) { e.streaminfo.min_frame_size = clamped; e.streaminfo.max_frame_size = clamped; } else { e.streaminfo.min_frame_size = @min(e.streaminfo.min_frame_size, clamped); e.streaminfo.max_frame_size = @max(e.streaminfo.max_frame_size, clamped); }
// Claim any reserved seek point whose sample falls in this frame. for (e.seek_points) |*point| { if (point.frame_samples != 0) continue; if (point.sample < first_sample or point.sample >= first_sample + block_size) continue; point.* = .{ .sample = first_sample, .offset = start - e.audio_offset, .frame_samples = @intCast(block_size), }; }
e.frames_written += 1; }
/// Every sample must fit the configured bit depth. A sample that does not /// cannot be written and read back unchanged, so it is refused where the /// caller hands it over rather than a block later. fn checkRange(e: *const Encoder, samples: []const i32) Error!void { const depth: u6 = @intCast(e.config.bits_per_sample); const high: i32 = @intCast((@as(i64, 1) << (depth - 1)) - 1); const low: i32 = @intCast(-(@as(i64, 1) << (depth - 1))); for (samples) |sample| { if (sample > high or sample < low) return error.SampleOutOfRange; } }
/// Add this block 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 hashBlock(e: *Encoder, block_size: u32) void { const channels = e.config.channels; const width = e.streaminfo.bytesPerSample(); const message = e.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(e.input[channel * e.options.block_size + sample]); for (0..width) |byte| { message[at] = @truncate(value >> @intCast(byte * 8)); at += 1; } } } e.md5.update(message); }
fn encodeFrame(e: *Encoder, block_size: u32) Error!void { const bps = e.config.bits_per_sample; const assignment = try e.chooseAssignment(block_size, bps);
var header: FrameHeader = .{ // Every block but the last is the configured size, and the last is // allowed to be short without making the stream a variable block // size one (RFC 9639, Section 8.2). .blocking = .fixed, .block_size = block_size, .sample_rate = e.config.sample_rate, .channels = assignment, .bits_per_sample = bps, .number = e.frames_written, }; // The frame header can only state a sample rate or bit depth the // format has a code for. Where it has none the header defers to the // streaminfo block, which is legal but leaves the streamable subset. // Nothing is written until every field has been encoded, so this // second attempt starts from a clean stream. header.write(&e.writer) catch |err| switch (err) { error.UnrepresentableFrameHeader => { header.sample_rate = null; header.bits_per_sample = null; try header.write(&e.writer); }, else => |other| return other, };
for (0..assignment.channelCount()) |index| { const channel = &e.channels[candidateFor(assignment, index)]; try e.writeSubframe(channel, block_size, header.subframeBitsPerSample(bps, index)); }
try e.writer.padToByte(); const crc = e.writer.crc16.final(); try e.writer.put(16, crc); assert(e.writer.isByteAligned()); }
/// Cost every layout the block could be written in and return the best, /// leaving each candidate channel holding the model chosen for it. fn chooseAssignment(e: *Encoder, block_size: u32, bps: u8) Error!ChannelAssignment { const stereo = e.config.channels == 2 and e.options.stereo == .adaptive; if (!stereo) { for (0..e.config.channels) |index| { const channel = &e.channels[index]; const in = e.input[index * e.options.block_size ..][0..block_size]; for (channel.signal[0..block_size], in) |*out, sample| out.* = sample; try e.chooseSubframe(channel, block_size, bps); } return .{ .independent = @intCast(e.config.channels) }; }
const left_in = e.input[0..block_size]; const right_in = e.input[e.options.block_size..][0..block_size]; const left = e.channels[0].signal[0..block_size]; const right = e.channels[1].signal[0..block_size]; const mid = e.channels[2].signal[0..block_size]; const side = e.channels[3].signal[0..block_size]; for (left_in, right_in, left, right, mid, side) |l, r, *ol, *orr, *om, *os| { ol.* = l; orr.* = r; // The mid channel is the average, rounded down; the bit it loses // is recoverable from the side channel's parity (Section 4.2). om.* = (@as(i64, l) + r) >> 1; os.* = @as(i64, l) - r; }
try e.chooseSubframe(&e.channels[0], block_size, bps); try e.chooseSubframe(&e.channels[1], block_size, bps); try e.chooseSubframe(&e.channels[2], block_size, bps); // A side channel holds a difference, so it needs one bit more. try e.chooseSubframe(&e.channels[3], block_size, bps + 1);
const l_bits = e.channels[0].choice.bits; const r_bits = e.channels[1].choice.bits; const m_bits = e.channels[2].choice.bits; const s_bits = e.channels[3].choice.bits;
var best: ChannelAssignment = .{ .independent = 2 }; var best_bits = l_bits + r_bits; if (l_bits + s_bits < best_bits) { best = .left_side; best_bits = l_bits + s_bits; } if (s_bits + r_bits < best_bits) { best = .right_side; best_bits = s_bits + r_bits; } if (m_bits + s_bits < best_bits) best = .mid_side; return best; }
/// Try every model this encoder is configured to try and leave the /// cheapest in `channel.choice`, with its residual and parameters beside /// it. fn chooseSubframe(e: *Encoder, channel: *Channel, block_size: u32, bps: u8) Error!void { const signal = channel.signal[0..block_size];
// Bits that are zero in every sample carry nothing, so the subframe // says how many there were and codes without them (Section 9.2.2). var present: u64 = 0; for (signal) |sample| present |= @bitCast(sample); const wasted: u6 = if (present == 0) 0 else @intCast(@min(@ctz(present), bps - 1)); if (wasted > 0) { for (signal) |*sample| sample.* >>= wasted; } const depth: u7 = @intCast(bps - wasted); const header_bits: u64 = 8 + @as(u64, wasted);
// A block of one value needs one value. const constant = for (signal[1..]) |sample| { if (sample != signal[0]) break false; } else true; if (constant) { channel.choice = .{ .type = .constant, .wasted_bits = wasted, .constant = signal[0], .bits = header_bits + depth, }; return; }
channel.choice = .{ .type = .verbatim, .wasted_bits = wasted, .bits = header_bits + @as(u64, block_size) * depth, };
for (0..limits.max_fixed_order + 1) |order| { if (order >= block_size) break; if (!fixedResidual(signal, @intCast(order), e.scratch_residual[0..block_size])) continue; const partitioning = e.choosePartitioning(block_size, @intCast(order)) orelse continue; const total = header_bits + @as(u64, order) * depth + partitioning.bits; if (total >= channel.choice.bits) continue; channel.choice = .{ .type = .{ .fixed = @intCast(order) }, .wasted_bits = wasted, .partitioning = partitioning, .bits = total, }; e.keep(channel); }
if (e.options.max_lpc_order > 0) try e.tryLinearPredictors(channel, block_size, depth, header_bits, wasted); }
fn tryLinearPredictors( e: *Encoder, channel: *Channel, block_size: u32, depth: u7, header_bits: u64, wasted: u6, ) Error!void { const signal = channel.signal[0..block_size]; const max_order: usize = @min(e.options.max_lpc_order, block_size -| 1); if (max_order == 0) return;
for (e.options.apodizations[0..e.options.apodization_count]) |taper| { lpc.tukeyWindow(e.windowed[0..block_size], signal, taper); lpc.autocorrelation(e.autocorrelation[0 .. max_order + 1], e.windowed[0..block_size]); const solved = lpc.levinson( e.lpc_coefficients, e.lpc_errors, e.autocorrelation[0 .. max_order + 1], max_order, ); if (solved == 0) continue; try e.tryOrders(channel, block_size, depth, header_bits, wasted, max_order, solved); } }
/// Cost the orders the caller's search policy allows, given a solved /// predictor family already in `lpc_coefficients`. fn tryOrders( e: *Encoder, channel: *Channel, block_size: u32, depth: u7, header_bits: u64, wasted: u6, /// The width of a row of `lpc_coefficients`, which is the highest /// order the recursion was asked for -- not the configured maximum, /// which a short block can hold the recursion below. stride: usize, solved: usize, ) Error!void { const signal = channel.signal[0..block_size]; const precision = e.options.qlp_precision;
// Rank the orders by what the recursion's error estimate says they // should cost, then cost that many of them properly. The estimate is // cheap -- it is a by-product of the recursion -- and costing an order // properly is not, so this is where the time goes or does not. var ranked: [limits.max_lpc_order]usize = undefined; var estimates: [limits.max_lpc_order]f64 = undefined; for (0..solved) |i| { const order = i + 1; const per_sample = lpc.expectedBitsPerResidual(e.lpc_errors[i], block_size); estimates[i] = @as(f64, @floatFromInt(order * (@as(u32, depth) + precision))) + @as(f64, @floatFromInt(block_size - order)) * per_sample; ranked[i] = i; } const wanted: usize = if (e.options.lpc_orders_costed == 0) solved else @min(e.options.lpc_orders_costed, solved); // A selection sort over the first `wanted` places, which is the whole // of the sorting needed and is cheaper than ordering the rest. for (0..wanted) |place| { var best = place; for (place + 1..solved) |other| { if (estimates[ranked[other]] < estimates[ranked[best]]) best = other; } std.mem.swap(usize, &ranked[place], &ranked[best]); }
for (ranked[0..wanted]) |i| { const order = i + 1; const coefficients = e.lpc_coefficients[i * stride ..][0..order]; const quantized = lpc.quantize(coefficients, precision) catch continue; if (!lpcResidual(signal, quantized, e.scratch_residual[0..block_size])) continue; const partitioning = e.choosePartitioning(block_size, @intCast(order)) orelse continue; const total = header_bits + @as(u64, order) * depth + 4 + 5 + @as(u64, order) * precision + partitioning.bits; if (total >= channel.choice.bits) continue; channel.choice = .{ .type = .{ .lpc = @intCast(order) }, .wasted_bits = wasted, .predictor = quantized, .partitioning = partitioning, .bits = total, }; e.keep(channel); } }
/// Adopt the residual and parameters the last model produced, by swapping /// them with the channel's rather than copying. fn keep(e: *Encoder, channel: *Channel) void { std.mem.swap([]i32, &channel.residual, &e.scratch_residual); std.mem.swap([]Parameter, &channel.parameters, &e.scratch_parameters); }
/// Cost every Rice parameter at every allowed partition order and keep the /// cheapest, leaving its parameters in `scratch_parameters`. Returns null /// if the block is too short for the predictor's warm-up samples. fn choosePartitioning(e: *Encoder, block_size: u32, order: u6) ?Partitioning { if (block_size <= order) return null;
// The finest partition order the block and the predictor allow: the // block size must divide evenly, and no partition may be swallowed // whole by the warm-up samples (Section 9.2.7). var finest: u4 = 0; for (1..@as(usize, e.options.max_partition_order) + 1) |candidate| { const partitions = @as(u32, 1) << @intCast(candidate); if (block_size % partitions != 0) break; if ((block_size >> @intCast(candidate)) <= order) break; finest = @intCast(candidate); } const lowest = @min(e.options.min_partition_order, finest);
const residual = e.scratch_residual[0..block_size]; for (e.folded[order..block_size], residual[order..]) |*folded, value| { // Zigzag: a residual of v becomes 2v when positive and -2v-1 when // negative, so that small magnitudes get small codes. folded.* = @bitCast((value << 1) ^ (value >> 31)); }
// One row of sums per partition at the finest order, plus the widest // sample in each, which is what the escape code would have to store. // // The rows stop at `limit` rather than running until the sums reach // zero, which is several parameters further on and costs a pass over // the partition each time. See `parameterLimit` for why nothing past // it can win, and why leaving those entries at zero is safe rather // than merely convenient. const finest_partitions = @as(u32, 1) << finest; const per_partition = block_size >> finest; var at: u32 = order; var limit: u7 = 0; for (0..finest_partitions) |partition| { const count = if (partition == 0) per_partition - order else per_partition; e.counts[partition] = count;
var total: u64 = 0; // The widest sample decides what an escaped partition would cost. // Taking the union of every magnitude rather than the largest // width keeps this branchless: bit length is monotonic, so the // union's bit length is the largest of theirs. // // Two unions are needed, not one. The magnitude of -1 is zero // bits, so a partition of nothing but zeroes and -1 unions to zero // magnitude -- and yet -1 still needs the one bit that says it is // not zero. The union of the raw values is what tells those two // partitions apart. var magnitudes: u32 = 0; var present: u32 = 0; for (e.folded[at..][0..count], residual[at..][0..count]) |folded, value| { total += folded; magnitudes |= @as(u32, @bitCast(value ^ (value >> 31))); present |= @as(u32, @bitCast(value)); } e.sums[partition * sum_columns] = total; e.magnitudes[partition] = if (present == 0) 0 else 32 - @clz(magnitudes) + 1; limit = @max(limit, parameterLimit(total, count)); at += count; }
at = order; for (0..finest_partitions) |partition| { const count = e.counts[partition]; const slice = e.folded[at..][0..count]; const row = e.sums[partition * sum_columns ..][0..sum_columns]; for (row[1 .. @as(usize, limit) + 1], 1..) |*sum, k| { var total: u64 = 0; for (slice) |folded| total += folded >> @intCast(k); sum.* = total; } @memset(row[@as(usize, limit) + 1 ..], 0); at += count; }
var best: ?Partitioning = null; var level = finest; while (true) { if (level >= lowest) { const costed = e.costPartitioning(level); if (best == null or costed.bits < best.?.bits) { best = costed; const source = if (costed.parameter_bits == 4) e.narrow_parameters else e.wide_parameters; const count = @as(u32, 1) << level; @memcpy(e.scratch_parameters[0..count], source[0..count]); } } if (level == 0) break; level -= 1; e.mergePartitions(level); } return best; }
/// Halve the number of partitions, adding neighbouring rows together. /// Sums of shifted values are additive, so a coarser partition's costs /// come straight from its two halves without another pass over the data. fn mergePartitions(e: *Encoder, level: u4) void { const partitions = @as(u32, 1) << level; for (0..partitions) |partition| { const left = partition * 2; const right = left + 1; const into = e.sums[partition * sum_columns ..][0..sum_columns]; if (left != partition) @memcpy(into, e.sums[left * sum_columns ..][0..sum_columns]); const from = e.sums[right * sum_columns ..][0..sum_columns]; for (into, from) |*sum, other| sum.* += other; e.counts[partition] = e.counts[left] + e.counts[right]; e.magnitudes[partition] = @max(e.magnitudes[left], e.magnitudes[right]); } }
/// What this partition order costs, under both parameter widths, leaving /// the parameters for each in their own array. fn costPartitioning(e: *Encoder, level: u4) Partitioning { const partitions = @as(u32, 1) << level; var narrow_bits: u64 = 2 + 4 + @as(u64, partitions) * 4; var wide_bits: u64 = 2 + 4 + @as(u64, partitions) * 5;
for (0..partitions) |partition| { const count = e.counts[partition]; const magnitude = e.magnitudes[partition]; const row = e.sums[partition * sum_columns ..][0..sum_columns];
const narrow = bestParameter(row, count, magnitude, 14); e.narrow_parameters[partition] = narrow.parameter; narrow_bits += narrow.bits;
const wide = bestParameter(row, count, magnitude, 30); e.wide_parameters[partition] = wide.parameter; wide_bits += wide.bits; }
return if (wide_bits < narrow_bits) .{ .order = level, .parameter_bits = 5, .bits = wide_bits } else .{ .order = level, .parameter_bits = 4, .bits = narrow_bits }; }
fn writeSubframe(e: *Encoder, channel: *Channel, block_size: u32, bps: u8) Error!void { const choice = channel.choice; const depth: u7 = @intCast(bps - choice.wasted_bits); const w = &e.writer;
const header: SubframeHeader = .{ .type = choice.type, .wasted_bits = choice.wasted_bits }; try header.write(w);
switch (choice.type) { .constant => try w.putSigned(depth, choice.constant), .verbatim => for (channel.signal[0..block_size]) |sample| { try w.putSigned(depth, sample); }, .fixed => |order| { for (channel.signal[0..order]) |sample| try w.putSigned(depth, sample); try e.writeResidual(channel, block_size, order); }, .lpc => |order| { for (channel.signal[0..order]) |sample| try w.putSigned(depth, sample); const q = choice.predictor; try w.put(4, @as(u64, q.precision) - 1); try w.putSigned(5, q.shift); for (q.coefficients[0..order]) |coefficient| { try w.putSigned(q.precision, coefficient); } try e.writeResidual(channel, block_size, order); }, } }
fn writeResidual(e: *Encoder, channel: *Channel, block_size: u32, order: u6) Error!void { const w = &e.writer; const p = channel.choice.partitioning; try w.put(2, if (p.parameter_bits == 4) 0b00 else 0b01); try w.put(4, p.order);
const per_partition = block_size >> p.order; var at: u32 = order; for (0..@as(u32, 1) << p.order) |partition| { const count = if (partition == 0) per_partition - order else per_partition; const slice = channel.residual[at..][0..count]; at += count; const parameter = channel.parameters[partition];
if (parameter.escaped) { try w.put(p.parameter_bits, (@as(u64, 1) << p.parameter_bits) - 1); try w.put(5, parameter.value); for (slice) |value| try w.putSigned(parameter.value, value); } else { try w.put(p.parameter_bits, parameter.value); const k: u7 = parameter.value; for (slice) |value| { const folded: u32 = @bitCast((value << 1) ^ (value >> 31)); try w.putUnary(folded >> @intCast(k)); try w.put(k, folded); } } } }};
/// Which working channel holds the signal a given subframe of a given layout/// codes. The four candidates are left, right, mid and side, in that order.fn candidateFor(assignment: ChannelAssignment, index: usize) usize { return switch (assignment) { .independent => index, .left_side => if (index == 0) 0 else 3, .right_side => if (index == 0) 3 else 1, .mid_side => if (index == 0) 2 else 3, };}
const Costed = struct { parameter: Parameter, bits: u64,};
/// The cheapest way to code one partition: the best Rice parameter up to/// `max_parameter`, or the escape code where storing the residual unencoded is/// cheaper. The returned cost excludes the parameter field itself.fn bestParameter(sums: []const u64, count: u32, magnitude: u32, max_parameter: u5) Costed { var best: Costed = .{ .parameter = .{ .escaped = false, .value = 0 }, .bits = std.math.maxInt(u64) }; for (0..@as(usize, max_parameter) + 1) |k| { // Each sample costs its unary stop bit, k bits of remainder, and one // bit for every step of its most-significant part. const cost = @as(u64, count) * (1 + k) + sums[k]; if (cost < best.bits) best = .{ .parameter = .{ .escaped = false, .value = @intCast(k) }, .bits = cost }; // Past the point where every folded value has been shifted away, a // larger parameter only adds bits. if (sums[k] == 0) break; } // The escape code stores five bits of width and then the residual // unencoded. A width of 32 cannot be stated, so those partitions have no // escape available. if (magnitude <= 31) { const cost = 5 + @as(u64, count) * magnitude; if (cost < best.bits) best = .{ .parameter = .{ .escaped = true, .value = @intCast(magnitude) }, .bits = cost }; } return best;}
/// The largest Rice parameter that could still be the best one for a partition/// of `count` samples whose folded residuals sum to `total`.////// A partition coded at parameter k costs `count * (1 + k)` bits plus the sum/// of its folded residuals shifted right by k. Shifting cannot lose more than/// half a value each time, so that sum is at most `total >> k`; once `2^k`/// passes the mean, the sum has fallen below `count`, and from there every/// further step of k adds `count` bits and saves less than `count`. So the/// cost is strictly increasing past this parameter and the sums for it need/// never be computed.////// This is also why the rows can be left at zero past the limit rather than/// filled in. A zero makes the apparent cost `count * (1 + k)`, which at/// `limit + 1` is already larger than the true cost at the true best/// parameter -- so no such entry can win, and none of them changes the answer/// after partitions are merged either, since merging adds both the counts and/// the sums.fn parameterLimit(total: u64, count: u32) u7 { const mean = total / count; if (mean == 0) return 0; const highest: u7 = @intCast(63 - @clz(mean)); return @min(highest + 1, 30);}
/// Subtract one of the five fixed predictions from every sample past the/// warm-up (RFC 9639, Section 9.2.5). False if any residual falls outside what/// the format allows, in which case the model cannot be used.fn fixedResidual(signal: []const i64, order: u3, out: []i32) bool { // The first `order` entries are warm-up samples, which a subframe stores // unencoded straight from the signal; nothing reads them from here. var largest: u64 = 0; switch (order) { 0 => for (signal, out) |sample, *residual| { largest = @max(largest, @abs(sample)); residual.* = @truncate(sample); }, 1 => for (signal[1..], out[1..], 0..) |sample, *residual, i| { const value = sample - signal[i]; largest = @max(largest, @abs(value)); residual.* = @truncate(value); }, 2 => for (signal[2..], out[2..], 0..) |sample, *residual, i| { const value = sample - 2 * signal[i + 1] + signal[i]; largest = @max(largest, @abs(value)); residual.* = @truncate(value); }, 3 => for (signal[3..], out[3..], 0..) |sample, *residual, i| { const value = sample - 3 * signal[i + 2] + 3 * signal[i + 1] - signal[i]; largest = @max(largest, @abs(value)); residual.* = @truncate(value); }, 4 => for (signal[4..], out[4..], 0..) |sample, *residual, i| { const value = sample - 4 * signal[i + 3] + 6 * signal[i + 2] - 4 * signal[i + 1] + signal[i]; largest = @max(largest, @abs(value)); residual.* = @truncate(value); }, else => unreachable, } // A residual that does not fit is not an error: it means this model cannot // be used, and the caller moves on to one that can. The truncated values // written above are discarded with it. return largest < residual_limit;}
/// Subtract the subframe's own prediction, computed exactly as a decoder will/// compute it (RFC 9639, Section 9.2.6).fn lpcResidual(signal: []const i64, q: lpc.Quantized, out: []i32) bool { const order = q.order; if (order >= signal.len) return false; var largest: u64 = 0; for (signal[order..], out[order..], order..) |sample, *residual, i| { var prediction: i64 = 0; for (q.coefficients[0..order], 0..) |coefficient, j| { prediction += @as(i64, coefficient) * signal[i - 1 - j]; } const value = sample - (prediction >> q.shift); largest = @max(largest, @abs(value)); residual.* = @truncate(value); } return largest < residual_limit;}
const testing = std.testing;const Decoder = @import("decoder.zig").Decoder;
/// Encode `samples`, decode what came out, and insist on getting the same/// samples back. Returns the size of the stream, so that tests can also say/// something about how well it compressed.fn roundTrip( gpa: Allocator, config: Config, options: Options, samples: []const i32,) !usize { var out: std.Io.Writer.Allocating = .init(gpa); defer out.deinit();
{ var e: Encoder = try .init(gpa, &out.writer, config, options); defer e.deinit(); try e.writeInterleaved(samples); try e.finish();
// Put the finished streaminfo block back, which is what a caller with // a seekable destination does. var patched: std.Io.Writer = .fixed(out.written()[Encoder.streaminfo_body_offset..]); try e.streaminfo.write(&patched); }
const stream = out.written(); var input: std.Io.Reader = .fixed(stream); var d: Decoder = try .init(gpa, &input, .{}); defer d.deinit();
try testing.expectEqual(config.sample_rate, d.streaminfo.sample_rate); try testing.expectEqual(config.channels, d.streaminfo.channels); try testing.expectEqual(config.bits_per_sample, d.streaminfo.bits_per_sample); try testing.expectEqual(samples.len / config.channels, d.streaminfo.total_samples);
var at: usize = 0; while (try d.next()) |f| { for (0..f.header.block_size) |sample| { for (f.channels) |channel| { try testing.expectEqual(samples[at], channel[sample]); at += 1; } } } try testing.expectEqual(samples.len, at); // Which also checks the MD5 the encoder wrote against the audio that came // back out of it. try d.finish(); return stream.len;}
/// A signal with something for every model to find: a tone, noise, silence, a/// step, and a run of samples with wasted bits.fn syntheticSignal(gpa: Allocator, count: usize, channels: u8, bits_per_sample: u8) ![]i32 { const samples = try gpa.alloc(i32, count * channels); var prng: std.Random.DefaultPrng = .init(0x666c6163); const random = prng.random(); const amplitude: f64 = @floatFromInt(@as(i64, 1) << @intCast(bits_per_sample - 2));
for (0..count) |i| { const t: f64 = @floatFromInt(i); for (0..channels) |channel| { const phase = t * 0.01 * @as(f64, @floatFromInt(channel + 1)); var value: i64 = switch ((i / 64) % 5) { // A tone, which a linear predictor should model closely. 0 => @intFromFloat(amplitude * @sin(phase)), // Noise, which nothing can predict. 1 => random.intRangeAtMost(i64, -64, 64), // Silence, which is a constant subframe. 2 => 0, // A ramp, which the fixed predictors handle exactly. 3 => @intCast(@mod(@as(i64, @intCast(i)) * 7, 1024)), // A tone whose low bits are all zero. else => @as(i64, @intFromFloat(amplitude * 0.5 * @sin(phase * 2))) & ~@as(i64, 0xf), }; const high = (@as(i64, 1) << @intCast(bits_per_sample - 1)) - 1; value = std.math.clamp(value, -high - 1, high); samples[i * channels + channel] = @intCast(value); } } return samples;}
test "a round trip at every level" { const gpa = testing.allocator; const config: Config = .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 16 }; const samples = try syntheticSignal(gpa, 5000, config.channels, config.bits_per_sample); defer gpa.free(samples);
const raw = samples.len * 2; for (std.enums.values(Level)) |level| { const size = try roundTrip(gpa, config, .{ .level = level, .padding = 0 }, samples); // Every level should at least beat storing the samples as they are. try testing.expect(size < raw); }}
test "a round trip at every bit depth" { const gpa = testing.allocator; for ([_]u8{ 4, 8, 12, 16, 20, 24, 32 }) |bits_per_sample| { const config: Config = .{ .sample_rate = 48000, .channels = 2, .bits_per_sample = bits_per_sample, }; const samples = try syntheticSignal(gpa, 1500, config.channels, bits_per_sample); defer gpa.free(samples); _ = try roundTrip(gpa, config, .{ .level = .@"5", .padding = 0 }, samples); }}
test "a round trip at every channel count" { const gpa = testing.allocator; for (1..limits.max_channels + 1) |channels| { const config: Config = .{ .sample_rate = 44100, .channels = @intCast(channels), .bits_per_sample = 16, }; const samples = try syntheticSignal(gpa, 900, config.channels, config.bits_per_sample); defer gpa.free(samples); _ = try roundTrip(gpa, config, .{ .level = .@"6", .padding = 0 }, samples); }}
test "a round trip at block sizes that do not divide the input" { const gpa = testing.allocator; const config: Config = .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 16 }; for ([_]u32{ 16, 17, 192, 1153, 4096 }) |block_size| { for ([_]usize{ 1, 2, 15, 16, 17, 1000, 4097 }) |count| { const samples = try syntheticSignal(gpa, count, config.channels, config.bits_per_sample); defer gpa.free(samples); _ = try roundTrip(gpa, config, .{ .level = .@"5", .block_size = block_size, .padding = 0, }, samples); } }}
test "a round trip of signals that push each model" { const gpa = testing.allocator; const config: Config = .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 16 }; const count = 3000;
var prng: std.Random.DefaultPrng = .init(0x776176); const random = prng.random(); const kinds = [_][]const u8{ "silence", "extremes", "full-scale noise", "identical channels", "opposed channels" }; for (kinds, 0..) |kind, k| { const samples = try gpa.alloc(i32, count * 2); defer gpa.free(samples); for (0..count) |i| { const pair: [2]i32 = switch (k) { 0 => .{ 0, 0 }, // Alternating extremes, which makes every residual as large as // it can be. 1 => if (i % 2 == 0) .{ 32767, -32768 } else .{ -32768, 32767 }, 2 => .{ random.intRangeAtMost(i32, -32768, 32767), random.intRangeAtMost(i32, -32768, 32767) }, // Both channels the same, so the side channel is all zeroes. 3 => blk: { const v = random.intRangeAtMost(i32, -32768, 32767); break :blk .{ v, v }; }, // Opposed, so the mid channel is nearly all zeroes. else => blk: { const v = random.intRangeAtMost(i32, -32767, 32767); break :blk .{ v, -v }; }, }; samples[i * 2] = pair[0]; samples[i * 2 + 1] = pair[1]; } _ = roundTrip(gpa, config, .{ .level = .@"8", .padding = 0 }, samples) catch |err| { std.debug.print("failed on {s}\n", .{kind}); return err; }; }}
test "a round trip of a 24-bit tone in noise at -8" { // A tone with white noise over it, at 96 kHz. With little left to predict // beyond the noise, the best predictor's coefficients are all small, and at // -8's precision they wanted a shift of 16 or more -- which five signed // bits write as a negative number the format forbids (RFC 9639, Section // 9.2.6). The stream that came out could not be decoded. const gpa = testing.allocator; const config: Config = .{ .sample_rate = 96000, .channels = 2, .bits_per_sample = 24 }; const count = 16384;
var prng: std.Random.DefaultPrng = .init(0x7368696674); const random = prng.random(); const full: f64 = 8388607; const samples = try gpa.alloc(i32, count * 2); defer gpa.free(samples); for (0..count) |i| { const t = @as(f64, @floatFromInt(i)) / 96000; const tones = [2]f64{ 0.4 * @sin(2 * std.math.pi * 440 * t), 0.3 * @sin(2 * std.math.pi * 660 * t) }; const noise = [2]f64{ 0.05, 0.02 }; for (0..2) |channel| { const value = (tones[channel] + noise[channel] * (random.float(f64) * 2 - 1)) * full; samples[i * 2 + channel] = @intFromFloat(@round(value)); } } _ = try roundTrip(gpa, config, .{ .level = .@"8", .padding = 0 }, samples);}
test "the encoder refuses what it cannot encode losslessly" { const gpa = testing.allocator; var out: std.Io.Writer.Allocating = .init(gpa); defer out.deinit();
var e: Encoder = try .init(gpa, &out.writer, .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 8, }, .{ .padding = 0 }); defer e.deinit();
// 128 does not fit eight bits, and writing it would lose a bit. try testing.expectError(error.SampleOutOfRange, e.writeInterleaved(&(@as([16]i32, @splat(0)) ++ [_]i32{ 128, 0 }))); // An odd number of values is not a whole number of interchannel samples. try testing.expectError(error.RaggedInput, e.writeInterleaved(&.{ 1, 2, 3 }));}
test "configurations the format cannot describe are refused" { const gpa = testing.allocator; var out: std.Io.Writer.Allocating = .init(gpa); defer out.deinit();
const good: Config = .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 16 }; const cases = [_]Config{ .{ .sample_rate = 44100, .channels = 0, .bits_per_sample = 16 }, .{ .sample_rate = 44100, .channels = 9, .bits_per_sample = 16 }, .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 3 }, .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 33 }, .{ .sample_rate = 1 << 21, .channels = 2, .bits_per_sample = 16 }, }; for (cases) |config| { try testing.expectError( error.InvalidConfiguration, Encoder.init(gpa, &out.writer, config, .{}), ); } // A block size outside what a streaminfo block can hold. try testing.expectError( error.InvalidConfiguration, Encoder.init(gpa, &out.writer, good, .{ .block_size = 15 }), ); // And a seek table with nothing to spread over. try testing.expectError( error.InvalidConfiguration, Encoder.init(gpa, &out.writer, good, .{ .seek_points = 4 }), );}
test "a stream whose head was never fixed up still decodes" { // What a caller writing to a pipe ends up with: the placeholder streaminfo // block the encoder wrote before it had seen any audio, saying it does not // know the length or the MD5. Both of those are encodings the format // defines, and a decoder must accept them rather than checking against // zero. const gpa = testing.allocator; const config: Config = .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 16 }; const samples = try syntheticSignal(gpa, 2500, config.channels, config.bits_per_sample); defer gpa.free(samples);
var out: std.Io.Writer.Allocating = .init(gpa); defer out.deinit(); { var e: Encoder = try .init(gpa, &out.writer, config, .{ .padding = 0 }); defer e.deinit(); try e.writeInterleaved(samples); try e.finish(); // And no `e.streaminfo.write` back over the head of the stream. }
var input: std.Io.Reader = .fixed(out.written()); var d: Decoder = try .init(gpa, &input, .{}); defer d.deinit(); try testing.expectEqual(@as(u64, 0), d.streaminfo.total_samples); try testing.expect(std.mem.allEqual(u8, &d.streaminfo.md5, 0));
var at: usize = 0; while (try d.next()) |f| { for (0..f.header.block_size) |sample| { for (f.channels) |channel| { try testing.expectEqual(samples[at], channel[sample]); at += 1; } } } try testing.expectEqual(samples.len, at); try d.finish();}
test "metadata written alongside the audio comes back" { const gpa = testing.allocator; var out: std.Io.Writer.Allocating = .init(gpa); defer out.deinit();
const fields = [_][]const u8{ "TITLE=Round trip", "ARTIST=zig-flac" }; const blocks = [_]metadata.Block{.{ .body = .{ .vorbis_comment = .{ .vendor = "zig-flac", .fields = &fields, } } }};
const config: Config = .{ .sample_rate = 44100, .channels = 1, .bits_per_sample = 16, .total_samples = 2000, }; const samples = try syntheticSignal(gpa, 2000, 1, 16); defer gpa.free(samples);
{ var e: Encoder = try .init(gpa, &out.writer, config, .{ .metadata = &blocks, .padding = 64, .seek_points = 4, .block_size = 512, }); defer e.deinit(); try e.writeInterleaved(samples); try e.finish();
var patched: std.Io.Writer = .fixed(out.written()[Encoder.streaminfo_body_offset..]); try e.streaminfo.write(&patched);
var table: std.Io.Writer = .fixed(out.written()[@intCast(e.seek_table_offset.?)..]); for (e.seek_points) |point| { try table.writeInt(u64, point.sample, .big); try table.writeInt(u64, point.offset, .big); try table.writeInt(u16, point.frame_samples, .big); } }
var input: std.Io.Reader = .fixed(out.written()); var d: Decoder = try .init(gpa, &input, .{}); defer d.deinit();
const comment = d.vorbisComment().?; try testing.expectEqualStrings("zig-flac", comment.vendor); try testing.expectEqualStrings("Round trip", comment.get("title").?); try testing.expectEqualStrings("zig-flac", comment.get("ARTIST").?);
const points = d.seekTable().?; try testing.expectEqual(@as(usize, 4), points.len); try testing.expectEqual(@as(u64, 0), points[0].sample); try testing.expectEqual(@as(u64, 0), points[0].offset); for (points) |point| try testing.expect(point.frame_samples != 0); // A seek point names the frame its sample falls in, so following one lands // on a sync code. for (points) |point| { const at: usize = @intCast(d.audioOffset() + point.offset); try testing.expectEqual(@as(u8, 0xff), out.written()[at]); try testing.expectEqual(@as(u8, 0xf8), out.written()[at + 1]); }
while (try d.next()) |_| {} try d.finish();}