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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Enough of RIFF/WAVE to get uncompressed samples in and out.//!//! This is not part of the FLAC format and nothing else in the library depends//! on it. It is here because a caller encoding audio almost always has it in a//! WAVE file, and writing the same two hundred lines again is nobody's idea of//! a good time.//!//! What is understood: `WAVE_FORMAT_PCM` and `WAVE_FORMAT_EXTENSIBLE` holding//! PCM, at 8, 16, 24 or 32 bits per sample. Eight-bit WAVE samples are//! unsigned by definition and are shifted into the signed range on the way in//! and back on the way out; every other width is signed little-endian already.//! Chunks that are not `fmt ` or `data` are skipped.//!//! What is not: floating-point samples, A-law and mu-law, RF64 for files past//! four gigabytes, and an extensible header whose valid bit count differs from//! its container's -- the container's width is what is used.
const std = @import("std");
pub const ReadError = std.Io.Reader.Error || error{ NotWave, UnsupportedWaveFormat, MalformedWave,};
pub const WriteError = std.Io.Writer.Error || error{WaveTooLarge};
const format_pcm: u16 = 0x0001;const format_extensible: u16 = 0xfffe;
/// What a WAVE file says its samples are.pub const Format = struct { sample_rate: u32, channels: u8, /// 8, 16, 24 or 32. bits_per_sample: u8, /// Interchannel samples, worked out from the size of the data chunk. total_samples: u64 = 0,
pub fn bytesPerSample(f: Format) u8 { return f.bits_per_sample / 8; }
pub fn blockAlign(f: Format) u16 { return @as(u16, f.channels) * f.bytesPerSample(); }};
/// The size of the header `writeHeader` produces: `RIFF`, a 16-byte `fmt `/// chunk, and the `data` chunk header.pub const header_length = 44;
/// Where the `RIFF` chunk's size field is, so that a caller who could not know/// it in advance can seek back and put it right.pub const riff_size_offset = 4;/// Where the `data` chunk's size field is, likewise.pub const data_size_offset = 40;
pub const Reader = struct { input: *std.Io.Reader, format: Format, /// Interchannel samples not yet read. Meaningless when `unknown_length`. remaining: u64, /// Set where the data chunk does not state a usable size, which is what a /// WAVE file written to a pipe looks like: the size fields cannot be /// filled in after the fact, so they are left at zero or at all ones. /// Reading then runs to the end of the stream instead. unknown_length: bool,
pub fn init(input: *std.Io.Reader) ReadError!Reader { const riff = input.takeArray(4) catch return error.NotWave; if (!std.mem.eql(u8, riff, "RIFF")) return error.NotWave; _ = input.takeInt(u32, .little) catch return error.NotWave; const wave = input.takeArray(4) catch return error.NotWave; if (!std.mem.eql(u8, wave, "WAVE")) return error.NotWave;
var format: ?Format = null; while (true) { const id = input.takeArray(4) catch |err| switch (err) { error.EndOfStream => return error.MalformedWave, error.ReadFailed => return error.ReadFailed, }; const size = try input.takeInt(u32, .little);
if (std.mem.eql(u8, id, "fmt ")) { format = try readFormat(input, size); } else if (std.mem.eql(u8, id, "data")) { const f = format orelse return error.MalformedWave; const align_bytes = f.blockAlign(); if (align_bytes == 0) return error.MalformedWave; const unknown = size == 0 or size == std.math.maxInt(u32); var complete = f; complete.total_samples = if (unknown) 0 else size / align_bytes; return .{ .input = input, .format = complete, .remaining = complete.total_samples, .unknown_length = unknown, }; } else { // Chunks are padded to an even length, and the pad byte is not // counted in the size. try input.discardAll(size + (size & 1)); } } }
fn readFormat(input: *std.Io.Reader, size: u32) ReadError!Format { if (size < 16) return error.MalformedWave; var tag = try input.takeInt(u16, .little); const channels = try input.takeInt(u16, .little); const sample_rate = try input.takeInt(u32, .little); _ = try input.takeInt(u32, .little); // bytes per second, which is redundant _ = try input.takeInt(u16, .little); // block align, likewise const bits_per_sample = try input.takeInt(u16, .little); var left = size - 16;
if (tag == format_extensible) { // Two bytes of extension size and twenty-two of extension. if (left < 24) return error.MalformedWave; _ = try input.takeInt(u16, .little); // extension size _ = try input.takeInt(u16, .little); // valid bits per sample _ = try input.takeInt(u32, .little); // channel mask // The first two bytes of the sub-format GUID hold the tag the // header would have carried if it were not extensible. tag = try input.takeInt(u16, .little); try input.discardAll(14); left -= 24; } try input.discardAll(left + (size & 1));
if (tag != format_pcm) return error.UnsupportedWaveFormat; if (channels == 0 or channels > 255) return error.UnsupportedWaveFormat; switch (bits_per_sample) { 8, 16, 24, 32 => {}, else => return error.UnsupportedWaveFormat, } return .{ .sample_rate = sample_rate, .channels = @intCast(channels), .bits_per_sample = @intCast(bits_per_sample), }; }
/// Read interleaved samples into `out`, which must have room for a whole /// number of interchannel samples. Returns how many values were written, /// which is short only at the end of the data. /// /// A partial interchannel sample at the end of the stream is not a sample /// and is dropped; a file that ends inside one is truncated, and where the /// data chunk stated its size that is an error rather than an ending. pub fn read(r: *Reader, out: []i32) ReadError!usize { const channels = r.format.channels; const width = r.format.bytesPerSample(); const wanted: u64 = if (r.unknown_length) out.len / channels else @min(out.len / channels, r.remaining);
var produced: usize = 0; var complete: u64 = 0; while (complete < wanted) : (complete += 1) { const started = produced; for (0..channels) |_| { const raw = r.input.take(width) catch |err| switch (err) { error.EndOfStream => { if (!r.unknown_length) return error.MalformedWave; return started; }, error.ReadFailed => return error.ReadFailed, }; out[produced] = decodeSample(raw, width); produced += 1; } } if (!r.unknown_length) r.remaining -= complete; return produced; }};
fn decodeSample(raw: []const u8, width: u8) i32 { return switch (width) { // Eight-bit WAVE samples are unsigned, centred on 128. 1 => @as(i32, raw[0]) - 128, 2 => std.mem.readInt(i16, raw[0..2], .little), 3 => blk: { const value: u32 = @as(u32, raw[0]) | (@as(u32, raw[1]) << 8) | (@as(u32, raw[2]) << 16); break :blk @as(i32, @bitCast(value << 8)) >> 8; }, 4 => std.mem.readInt(i32, raw[0..4], .little), else => unreachable, };}
/// Write a 44-byte header. `format.total_samples` may be zero if it is not yet/// known, in which case both size fields say zero and a caller that can seek/// puts them right with `patchSizes`.pub fn writeHeader(w: *std.Io.Writer, format: Format) WriteError!void { const data_bytes = format.total_samples * format.blockAlign(); if (data_bytes > std.math.maxInt(u32) - header_length) return error.WaveTooLarge; const size: u32 = @intCast(data_bytes);
try w.writeAll("RIFF"); try w.writeInt(u32, if (size == 0) 0 else size + header_length - 8, .little); try w.writeAll("WAVE");
try w.writeAll("fmt "); try w.writeInt(u32, 16, .little); try w.writeInt(u16, format_pcm, .little); try w.writeInt(u16, format.channels, .little); try w.writeInt(u32, format.sample_rate, .little); try w.writeInt(u32, format.sample_rate * format.blockAlign(), .little); try w.writeInt(u16, format.blockAlign(), .little); try w.writeInt(u16, format.bits_per_sample, .little);
try w.writeAll("data"); try w.writeInt(u32, size, .little);}
/// The two size fields, for a caller writing them back over a header whose/// sizes were not known when it was written.pub const Sizes = struct { riff: u32, data: u32,};
pub fn sizes(format: Format) WriteError!Sizes { const data_bytes = format.total_samples * format.blockAlign(); if (data_bytes > std.math.maxInt(u32) - header_length) return error.WaveTooLarge; return .{ .riff = @intCast(data_bytes + header_length - 8), .data = @intCast(data_bytes) };}
/// Write interleaved samples in the file's own width.pub fn writeSamples(w: *std.Io.Writer, format: Format, samples: []const i32) WriteError!void { switch (format.bytesPerSample()) { 1 => for (samples) |sample| try w.writeByte(@intCast(sample + 128)), 2 => for (samples) |sample| try w.writeInt(i16, @intCast(sample), .little), 3 => for (samples) |sample| { const value: u32 = @bitCast(sample); try w.writeAll(&.{ @truncate(value), @truncate(value >> 8), @truncate(value >> 16), }); }, 4 => for (samples) |sample| try w.writeInt(i32, sample, .little), else => unreachable, }}
const testing = std.testing;
test "a header this module wrote is one it can read" { const format: Format = .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 24, .total_samples = 3, }; const samples = [_]i32{ 0, -1, 8388607, -8388608, 1234567, -7654321 };
var buffer: [header_length + 18]u8 = undefined; var w: std.Io.Writer = .fixed(&buffer); try writeHeader(&w, format); try testing.expectEqual(@as(usize, header_length), w.end); try writeSamples(&w, format, &samples);
var input: std.Io.Reader = .fixed(&buffer); var r: Reader = try .init(&input); try testing.expectEqual(format, r.format);
var out: [6]i32 = undefined; try testing.expectEqual(@as(usize, 6), try r.read(&out)); try testing.expectEqualSlices(i32, &samples, &out); try testing.expectEqual(@as(u64, 0), r.remaining);}
test "every width round-trips at its extremes" { for ([_]u8{ 8, 16, 24, 32 }) |bits_per_sample| { const high: i32 = @intCast((@as(i64, 1) << @intCast(bits_per_sample - 1)) - 1); const low: i32 = @intCast(-(@as(i64, 1) << @intCast(bits_per_sample - 1))); const samples = [_]i32{ 0, 1, -1, high, low, high - 1, low + 1 }; const format: Format = .{ .sample_rate = 8000, .channels = 1, .bits_per_sample = bits_per_sample, .total_samples = samples.len, };
var buffer: [header_length + 32]u8 = undefined; var w: std.Io.Writer = .fixed(&buffer); try writeHeader(&w, format); try writeSamples(&w, format, &samples);
var input: std.Io.Reader = .fixed(buffer[0..w.end]); var r: Reader = try .init(&input); var out: [samples.len]i32 = undefined; _ = try r.read(&out); try testing.expectEqualSlices(i32, &samples, &out); }}
test "an extensible header is understood, and chunks in between are skipped" { // A `LIST` chunk of odd length before `fmt `, an extensible format chunk, // and then the data: all three things a real file does that the simple // path does not cover. var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); const w = &out.writer;
try w.writeAll("RIFF"); try w.writeInt(u32, 0, .little); try w.writeAll("WAVE");
try w.writeAll("LIST"); try w.writeInt(u32, 5, .little); try w.writeAll("INFO\x00"); try w.writeByte(0); // the pad byte an odd chunk needs
try w.writeAll("fmt "); try w.writeInt(u32, 40, .little); try w.writeInt(u16, format_extensible, .little); try w.writeInt(u16, 2, .little); try w.writeInt(u32, 48000, .little); try w.writeInt(u32, 48000 * 4, .little); try w.writeInt(u16, 4, .little); try w.writeInt(u16, 16, .little); try w.writeInt(u16, 22, .little); try w.writeInt(u16, 16, .little); try w.writeInt(u32, 3, .little); try w.writeInt(u16, format_pcm, .little); try w.splatByteAll(0, 14);
try w.writeAll("data"); try w.writeInt(u32, 8, .little); for ([_]i16{ 1, -1, 32767, -32768 }) |sample| try w.writeInt(i16, sample, .little);
var input: std.Io.Reader = .fixed(out.written()); var r: Reader = try .init(&input); try testing.expectEqual(@as(u32, 48000), r.format.sample_rate); try testing.expectEqual(@as(u8, 2), r.format.channels); try testing.expectEqual(@as(u8, 16), r.format.bits_per_sample); try testing.expectEqual(@as(u64, 2), r.format.total_samples);
var samples: [4]i32 = undefined; try testing.expectEqual(@as(usize, 4), try r.read(&samples)); try testing.expectEqualSlices(i32, &.{ 1, -1, 32767, -32768 }, &samples);}
test "a data chunk with no usable size reads to the end of the stream" { // What a WAVE file written to a pipe looks like: the size fields could not // be filled in afterwards, so they say all ones. var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); const w = &out.writer; try writeHeader(w, .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 16 }); // Overwrite the data size with the placeholder a streaming writer uses. std.mem.writeInt(u32, out.written()[data_size_offset..][0..4], std.math.maxInt(u32), .little); for ([_]i16{ 1, -1, 2, -2, 3, -3 }) |sample| try w.writeInt(i16, sample, .little); // And half an interchannel sample, which is not one. try w.writeInt(i16, 99, .little);
var input: std.Io.Reader = .fixed(out.written()); var r: Reader = try .init(&input); try testing.expect(r.unknown_length); try testing.expectEqual(@as(u64, 0), r.format.total_samples);
var samples: [16]i32 = undefined; try testing.expectEqual(@as(usize, 6), try r.read(&samples)); try testing.expectEqualSlices(i32, &.{ 1, -1, 2, -2, 3, -3 }, samples[0..6]); try testing.expectEqual(@as(usize, 0), try r.read(&samples));}
test "a file that ends inside its stated data is truncated" { var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); try writeHeader(&out.writer, .{ .sample_rate = 44100, .channels = 2, .bits_per_sample = 16, .total_samples = 4, }); try out.writer.writeInt(i16, 1, .little);
var input: std.Io.Reader = .fixed(out.written()); var r: Reader = try .init(&input); var samples: [8]i32 = undefined; try testing.expectError(error.MalformedWave, r.read(&samples));}
test "files that are not WAVE files are refused" { var not_riff: std.Io.Reader = .fixed("fLaC\x00\x00\x00\x22"); try testing.expectError(error.NotWave, Reader.init(¬_riff));
var not_wave: std.Io.Reader = .fixed("RIFF\x08\x00\x00\x00AVI "); try testing.expectError(error.NotWave, Reader.init(¬_wave));
// A format this module does not decode: IEEE float. var floating: std.Io.Writer.Allocating = .init(testing.allocator); defer floating.deinit(); const w = &floating.writer; try w.writeAll("RIFF"); try w.writeInt(u32, 36, .little); try w.writeAll("WAVE"); try w.writeAll("fmt "); try w.writeInt(u32, 16, .little); try w.writeInt(u16, 3, .little); try w.writeInt(u16, 1, .little); try w.writeInt(u32, 44100, .little); try w.writeInt(u32, 44100 * 4, .little); try w.writeInt(u16, 4, .little); try w.writeInt(u16, 32, .little); var input: std.Io.Reader = .fixed(floating.written()); try testing.expectError(error.UnsupportedWaveFormat, Reader.init(&input));}