Something went wrong. Try again.
An MPEG audio codec in Zig that decodes Layers I, II and III and encodes Layers II and III.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! A Layer II encoder.//!//! The mirror of `layer12.zig`, and deliberately built on the same tables://! the allocation tables, the quantization classes and the scalefactors are//! read out of the decoder rather than written again here, so the two cannot//! drift apart. What is *not* shared is the interesting part, because it is//! the part the standard does not specify. A decoder has one right answer;//! an encoder has a budget and a choice about how to spend it, and every//! choice made here -- which subbands get bits, how the scalefactors are//! grouped, where the quantizer rounds -- is this encoder's own.//!//! A frame is 1152 samples of each channel://!//! 1. the analysis filterbank turns them into 36 time slots of 32 subbands;//! 2. the psychoacoustic model says how much noise each subband can hide;//! 3. a scalefactor per subband and granule captures its peak;//! 4. bits go to whichever subband is furthest from hiding its own noise,//! one step at a time, until the frame is full;//! 5. the samples are quantized to what they were given and written out.//!//! Constant bit rate, stereo or mono. Joint stereo is not attempted: it//! decides that two channels are alike enough to share their upper subbands,//! which is a judgement this has no way to make well yet, and coding both//! channels in full is always correct if not always thrifty.
const std = @import("std");
const analysis = @import("analysis.zig");const BitWriter = @import("bitwriter.zig");const Header = @import("header.zig");const layer12 = @import("layer12.zig");const psymodel2 = @import("psymodel2.zig");
pub const subbands = layer12.subbands;/// Samples of each channel one frame carries.pub const frame_samples = layer12.max_samples;/// Time slots in a frame, and granules of twelve.const time_slots = frame_samples / subbands;const granules = layer12.max_granules;
/// The largest frame Layer II can produce, in bytes.pub const max_frame_bytes = 1728;
pub const Error = error{ UnsupportedSampleRate, UnsupportedBitRate, UnsupportedChannelCount, /// The output buffer is smaller than the frame the settings ask for. NoSpace, /// The input is not one whole frame of every channel. WrongInputLength,};
pub const Options = struct { sample_rate: u32, channels: u8 = 2, /// Bits per second across both channels. Must be one the format can /// name for this sampling rate; `bitRates` lists them. bitrate: u32,};
/// The bit rates Layer II can name at `sample_rate`, in bits per second.////// The format cannot express an arbitrary rate: it sends an index into a/// table of fourteen, and which table depends on the MPEG version.pub fn bitRates(sample_rate: u32, out: *[14]u32) Error![]const u32 { const found = try describe(sample_rate); var count: usize = 0; for (1..15) |index| { var header = found.header; header.bitrate_index = @intCast(index); out[count] = header.bitRate() orelse continue; count += 1; } return out[0..count];}
const Described = struct { header: Header };
fn describe(sample_rate: u32) Error!Described { const rates = [3]u32{ 44100, 48000, 32000 }; var header: Header = .{ .version = .mpeg1, .layer = .layer2, .crc_protected = false, .bitrate_index = 1, .sample_rate_index = 0, .padding = false, .private = false, .mode = .stereo, .mode_extension = 0, .copyright = false, .original = true, .emphasis = .none, }; for (rates, 0..) |base, index| { if (sample_rate == base) { header.version = .mpeg1; header.sample_rate_index = @intCast(index); return .{ .header = header }; } if (sample_rate == base / 2) { header.version = .mpeg2; header.sample_rate_index = @intCast(index); return .{ .header = header }; } } return Error.UnsupportedSampleRate;}
/// The signal-to-noise ratio each quantization class buys, in decibels.////// A uniform quantizer of `n` levels over the same range as its input has a/// noise power of one part in n^2 of the signal's, so this is 20 log10 n./// The standard tabulates measured figures instead, which differ by a decibel/// or two at the coarse end; it makes no difference here, because what the/// allocation loop needs from this is the order, not the values.const class_snr = blk: { var t: [layer12.class_levels.len]f32 = undefined; for (layer12.class_levels, 0..) |levels, i| { t[i] = @floatCast(20.0 * std.math.log10(@as(f64, @floatFromInt(levels)))); } break :blk t;};
/// Sample bits one subband of one channel costs for a whole frame.fn sampleBits(class: u8) usize { if (class == 0) return 0; if (class < 17) return time_slots * @as(usize, class); const levels = layer12.class_levels[class - 2]; const width = levels + 2 - (levels >> 3); // Three time slots to a code word. return (time_slots / 3) * width;}
/// Which of the three granules transmit a scalefactor, most significant bit/// first, by selector. The decoder reads this same pattern.const scalefactor_pattern = [4]u3{ 0b111, 0b101, 0b100, 0b110 };
pub const Encoder = struct { header: Header, channels: u8, models: [2]psymodel2.Model, filters: [2]analysis.State, /// Carries the fraction of a slot a frame is short, so that padding is /// inserted exactly often enough to hold the stated bit rate. padding_debt: u32, /// Bits and bytes of the frame currently being built. total_bands: u8, /// Which allocation run each subband's code comes from. run_of_band: [subbands]layer12.Allocation,
pub fn init(options: Options) Error!Encoder { if (options.channels == 0 or options.channels > 2) { return Error.UnsupportedChannelCount; } const found = try describe(options.sample_rate); var header = found.header; header.mode = if (options.channels == 1) .mono else .stereo;
var index: ?u4 = null; for (1..15) |candidate| { header.bitrate_index = @intCast(candidate); if (header.bitRate() == options.bitrate) { index = @intCast(candidate); break; } } header.bitrate_index = index orelse return Error.UnsupportedBitRate;
const table = layer12.allocationTable(header); var self: Encoder = .{ .header = header, .channels = options.channels, .models = @splat(psymodel2.Model.init(options.sample_rate)), .filters = @splat(.{}), .padding_debt = 0, .total_bands = table.total, .run_of_band = undefined, }; var band: usize = 0; for (table.runs) |run| { const end = @min(band + run.bands, table.total); while (band < end) : (band += 1) self.run_of_band[band] = run; } return self; }
/// Bytes the next frame will occupy. Varies by one, because most bit /// rates do not divide into a whole number of bytes per frame. pub fn nextFrameBytes(self: *const Encoder) usize { var header = self.header; header.padding = self.paddingWanted(); return header.frameSize().?; }
fn paddingWanted(self: *const Encoder) bool { const rate = self.header.sampleRate(); const numerator = 144 * (self.header.bitRate().?); return self.padding_debt + numerator % rate >= rate; }
/// Encode one frame. /// /// `input` is `frame_samples` samples of each channel, interleaved, in /// the range -1 to 1. Returns the bytes written to `out`. pub fn encodeFrame(self: *Encoder, input: []const f32, out: []u8) Error!usize { const channels: usize = self.channels; if (input.len != frame_samples * channels) return Error.WrongInputLength;
var header = self.header; header.padding = self.paddingWanted(); const size = header.frameSize().?; if (out.len < size) return Error.NoSpace;
// Book the padding, whether or not it was taken this time. const rate = header.sampleRate(); self.padding_debt += 144 * header.bitRate().? % rate; if (self.padding_debt >= rate) self.padding_debt -= rate;
// 1. The filterbank: 36 time slots of 32 subbands per channel. var sub: [2][subbands][time_slots]f32 = undefined; var pcm: [2][frame_samples]f32 = undefined; for (0..channels) |ch| { for (0..frame_samples) |n| pcm[ch][n] = input[n * channels + ch]; for (0..time_slots) |slot| { var one: [subbands]f32 = undefined; analysis.analyze(&self.filters[ch], pcm[ch][slot * subbands ..][0..subbands], &one); for (0..subbands) |band| sub[ch][band][slot] = one[band]; } }
// 2. What the ear will not hear, from the first 1024 samples of the // frame. The standard centres its window on the frame; this does // not, which costs a little accuracy at a transient and nothing at // all elsewhere. var smr: [2][subbands]f32 = undefined; for (0..channels) |ch| { var result: psymodel2.Result = .{}; self.models[ch].analyze(pcm[ch][0..psymodel2.window_len], &result); self.models[ch].signalToMask(&result, &smr[ch]); }
// 3. A scalefactor per subband, channel and granule, and the selector // saying which of the three are actually sent. var scale_index: [subbands][2][granules]u8 = undefined; var selector: [subbands][2]u8 = undefined; var scale_count: [subbands][2]u8 = undefined; for (0..self.total_bands) |band| { for (0..channels) |ch| { for (0..granules) |g| { var peak: f32 = 0; const from = g * (time_slots / granules); for (sub[ch][band][from..][0 .. time_slots / granules]) |value| { peak = @max(peak, @abs(value)); } scale_index[band][ch][g] = scalefactorFor(peak); } const s = &scale_index[band][ch]; const chosen = chooseSelector(s); selector[band][ch] = chosen.selector; scale_count[band][ch] = chosen.count; // The decoder repeats scalefactors the selector does not // send, so the ones not sent must be made to match what it // will use in their place. applySelector(s, chosen.selector); } }
// 4. Spend the frame's bits. var code: [subbands][2]u8 = @splat(@splat(0)); self.allocate(size, &smr, &scale_count, &code);
// 5. Write it. var writer = BitWriter.init(out[0..size]); self.writeHeader(&writer, header); for (0..self.total_bands) |band| { for (0..channels) |ch| { writer.write(code[band][ch], self.run_of_band[band].width); } } for (0..self.total_bands) |band| { for (0..channels) |ch| { if (self.classOf(band, code[band][ch]) == 0) continue; writer.write(selector[band][ch], 2); } } for (0..self.total_bands) |band| { for (0..channels) |ch| { if (self.classOf(band, code[band][ch]) == 0) continue; const present = scalefactor_pattern[selector[band][ch]]; var which: u3 = 4; for (0..granules) |g| { if (present & which != 0) writer.write(scale_index[band][ch][g], 6); which >>= 1; } } } self.writeSamples(&writer, &sub, &code, &scale_index); std.debug.assert(writer.overrun == 0); return size; }
fn classOf(self: *const Encoder, band: usize, index: u8) u8 { const run = self.run_of_band[band]; return layer12.allocation_codes[run.offset + index]; }
fn writeHeader(self: *const Encoder, writer: *BitWriter, header: Header) void { _ = self; writer.write(0x7ff, 11); // sync writer.write(@backingInt(header.version), 2); writer.write(@backingInt(header.layer), 2); writer.write(1, 1); // protection bit is active low: no CRC writer.write(header.bitrate_index, 4); writer.write(header.sample_rate_index, 2); writer.write(@intFromBool(header.padding), 1); writer.write(@intFromBool(header.private), 1); writer.write(@backingInt(header.mode), 2); writer.write(header.mode_extension, 2); writer.write(@intFromBool(header.copyright), 1); writer.write(@intFromBool(header.original), 1); writer.write(@backingInt(header.emphasis), 2); }
/// Give bits to whichever subband is furthest from hiding its own noise, /// one quantization class at a time, until nothing more fits. /// /// The measure is the mask-to-noise ratio: how much better the subband's /// signal-to-noise ratio already is than it needs to be. The subband /// with the smallest is the one a listener will notice first, so it goes /// next; a subband whose signal is under its own masking threshold has a /// large ratio from the start and is never chosen while anything audible /// is still wanting. fn allocate( self: *const Encoder, size: usize, smr: *const [2][subbands]f32, scale_count: *const [subbands][2]u8, code: *[subbands][2]u8, ) void { const channels: usize = self.channels; var left: isize = @intCast(size * 8 - 32); for (0..self.total_bands) |band| { left -= @as(isize, self.run_of_band[band].width) * @as(isize, @intCast(channels)); }
var stuck: [subbands][2]bool = @splat(@splat(false)); while (true) { var best_band: usize = 0; var best_ch: usize = 0; var best: f32 = std.math.inf(f32); var any = false; for (0..self.total_bands) |band| { const width = self.run_of_band[band].width; const limit = (@as(usize, 1) << width) - 1; for (0..channels) |ch| { if (stuck[band][ch] or code[band][ch] >= limit) continue; const class = self.classOf(band, code[band][ch]); const snr: f32 = if (class == 0) 0 else class_snr[class - 2]; const mnr = snr - smr[ch][band]; if (!any or mnr < best) { any = true; best = mnr; best_band = band; best_ch = ch; } } } if (!any) return;
const was = self.classOf(best_band, code[best_band][best_ch]); const now = self.classOf(best_band, code[best_band][best_ch] + 1); var delta: isize = @intCast(sampleBits(now)); delta -= @intCast(sampleBits(was)); // Turning a subband on for the first time also buys its selector // and its scalefactors. if (was == 0 and now != 0) { delta += 2 + 6 * @as(isize, scale_count[best_band][best_ch]); } if (delta <= left) { left -= delta; code[best_band][best_ch] += 1; } else { stuck[best_band][best_ch] = true; } } }
fn writeSamples( self: *const Encoder, writer: *BitWriter, sub: *const [2][subbands][time_slots]f32, code: *const [subbands][2]u8, scale_index: *const [subbands][2][granules]u8, ) void { const channels: usize = self.channels; const per_granule = time_slots / granules; for (0..granules) |g| { var slot: usize = 0; while (slot < per_granule) : (slot += 3) { for (0..self.total_bands) |band| { for (0..channels) |ch| { const class = self.classOf(band, code[band][ch]); if (class == 0) continue; const levels = layer12.class_levels[class - 2]; const step = layer12.scale_steps[class - 2][scale_index[band][ch][g]]; const at = g * per_granule + slot;
if (class < 17) { const half: i32 = (@as(i32, 1) << @intCast(class - 1)) - 1; for (0..3) |k| { const raw = quantize(sub[ch][band][at + k], step, half, levels); writer.write(raw, @intCast(class)); } } else { // Three values in one code word, least significant // first, which is the order the decoder unpacks. const half: i32 = @intCast(levels / 2); const width: u6 = @intCast(levels + 2 - (levels >> 3)); var word: u32 = 0; var scale: u32 = 1; for (0..3) |k| { const raw = quantize(sub[ch][band][at + k], step, half, levels); word += raw * scale; scale *= levels; } writer.write(word, width); } } } } } }};
/// Turn one subband sample into the code the decoder will read.////// The decoder computes `(raw - half) * step`, so this is that inverted and/// rounded, held inside the range the class can express. Clamping rather/// than wrapping matters: a value one past the end is a sample slightly/// louder than its scalefactor promised, and clipping it is a small error/// where wrapping it is a loud click.fn quantize(value: f32, step: f32, half: i32, levels: u32) u32 { const scaled = @round(value / step) + @as(f32, @floatFromInt(half)); const limit: f32 = @floatFromInt(levels - 1); return @intFromFloat(std.math.clamp(scaled, 0, limit));}
/// The scalefactor index whose range just covers a peak of `peak`.////// Table B.1's entry `i` is 2^(1 - i/3), and a subband quantized against it/// can represent values up to about that, so the wanted index is the largest/// whose scalefactor is still at least the peak.fn scalefactorFor(peak: f32) u8 { if (!(peak > 0)) return 62; const index = @floor(3.0 * (1.0 - std.math.log2(peak))); return @intFromFloat(std.math.clamp(index, 0, 62));}
const Chosen = struct { selector: u8, count: u8 };
/// Pick the scalefactor selector that sends the fewest of the three.////// The three granules very often want the same scalefactor, or two the same,/// and the format has a two-bit field for saying so: it is six bits saved per/// subband per repeat, which at 32 subbands and two channels is worth having.fn chooseSelector(s: *const [granules]u8) Chosen { if (s[0] == s[1] and s[1] == s[2]) return .{ .selector = 2, .count = 1 }; if (s[0] == s[1]) return .{ .selector = 1, .count = 2 }; if (s[1] == s[2]) return .{ .selector = 3, .count = 2 }; return .{ .selector = 0, .count = 3 };}
/// Make the scalefactors the selector does not send match the ones the/// decoder will use in their place, so that what was quantized against is/// what will be dequantized against.fn applySelector(s: *[granules]u8, selector: u8) void { switch (selector) { 1 => s[1] = s[0], 2 => { s[1] = s[0]; s[2] = s[0]; }, 3 => s[2] = s[1], else => {}, }}
const testing = std.testing;
test "the header the encoder writes is the one it meant" { var encoder = try Encoder.init(.{ .sample_rate = 44100, .channels = 2, .bitrate = 192_000 }); var input: [frame_samples * 2]f32 = @splat(0); var out: [max_frame_bytes]u8 = undefined; const size = try encoder.encodeFrame(&input, &out);
const header = Header.parse(out[0..4].*) orelse return error.NotAHeader; try testing.expectEqual(Header.Layer.layer2, header.layer); try testing.expectEqual(Header.Version.mpeg1, header.version); try testing.expectEqual(@as(u32, 44100), header.sampleRate()); try testing.expectEqual(@as(u32, 192_000), header.bitRate().?); try testing.expectEqual(@as(u8, 2), header.channels()); try testing.expectEqual(size, header.frameSize().?);}
test "unsupported settings are refused rather than approximated" { try testing.expectError( Error.UnsupportedSampleRate, Encoder.init(.{ .sample_rate = 44101, .bitrate = 192_000 }), ); try testing.expectError( Error.UnsupportedBitRate, Encoder.init(.{ .sample_rate = 44100, .bitrate = 193_000 }), ); try testing.expectError( Error.UnsupportedChannelCount, Encoder.init(.{ .sample_rate = 44100, .channels = 3, .bitrate = 192_000 }), );}
test "the bit rates offered are the ones that work" { var buffer: [14]u32 = undefined; const rates = try bitRates(44100, &buffer); try testing.expect(rates.len == 14); for (rates) |rate| { var encoder = try Encoder.init(.{ .sample_rate = 44100, .bitrate = rate }); try testing.expect(encoder.nextFrameBytes() > 0); }}
test "padding holds the stated bit rate over many frames" { // 144 * 128000 / 44100 is not a whole number, so some frames have to // carry an extra byte; over a second of audio the average has to come out // at the rate that was asked for. var encoder = try Encoder.init(.{ .sample_rate = 44100, .channels = 2, .bitrate = 128_000 }); var input: [frame_samples * 2]f32 = @splat(0); var out: [max_frame_bytes]u8 = undefined; var total: usize = 0; const count = 100; for (0..count) |_| total += try encoder.encodeFrame(&input, &out);
const seconds = @as(f64, count * frame_samples) / 44100.0; const measured = @as(f64, @floatFromInt(total * 8)) / seconds; try testing.expectApproxEqRel(@as(f64, 128_000), measured, 0.005);}
test "what is encoded decodes back to something like the input" { const mp3 = @import("root.zig"); const rate = 44100; const frames = 24; const count = frames * frame_samples;
var input: [count * 2]f32 = undefined; for (0..count) |n| { const t = @as(f64, @floatFromInt(n)) / rate; const left = 0.45 * @sin(2.0 * std.math.pi * 1000.0 * t) + 0.25 * @sin(2.0 * std.math.pi * 3200.0 * t); const right = 0.40 * @sin(2.0 * std.math.pi * 700.0 * t) + 0.20 * @sin(2.0 * std.math.pi * 5100.0 * t); input[n * 2] = @floatCast(left); input[n * 2 + 1] = @floatCast(right); }
var encoder = try Encoder.init(.{ .sample_rate = rate, .channels = 2, .bitrate = 256_000 }); var stream: std.ArrayList(u8) = .empty; defer stream.deinit(testing.allocator); var frame: [max_frame_bytes]u8 = undefined; for (0..frames) |f| { const size = try encoder.encodeFrame( input[f * frame_samples * 2 ..][0 .. frame_samples * 2], &frame, ); try stream.appendSlice(testing.allocator, frame[0..size]); }
var audio = try mp3.decodeAll(testing.allocator, stream.items); defer audio.deinit(); try testing.expectEqual(@as(u32, rate), audio.sample_rate); try testing.expectEqual(@as(u8, 2), audio.channels);
// The filterbank pair delays the signal by 481 samples, and the first and // last frames are the ones missing their neighbours' overlap, so the // comparison starts after one frame and stops before the last. const delay = analysis.delay; var signal: f64 = 0; var noise: f64 = 0; var n: usize = frame_samples; while (n < count - frame_samples) : (n += 1) { for (0..2) |ch| { const want = input[(n - delay) * 2 + ch]; const got = audio.samples[n * 2 + ch]; signal += @as(f64, want) * want; noise += @as(f64, got - want) * (got - want); } } const snr = 10 * std.math.log10(signal / @max(noise, 1e-30)); // Better than 70 dB: at this bit rate the few subbands that carry // anything get most of the frame between them, so they are quantized // very finely indeed. testing.expect(snr > 70) catch |err| { std.debug.print("round trip SNR {d:.2} dB at 256 kbit/s\n", .{snr}); return err; };}