Something went wrong. Try again.
A sans-IO RIFF/WAVE reader and writer for Zig: 8, 16, 24 and 32 bit PCM and 32 and 64 bit float, in and out.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Reads and writes RIFF/WAVE files.//!//! It is sans-IO: `Reader` takes a `std.Io.Reader` and `Writer` takes a//! `std.Io.Writer`, so a file, a socket, a pipe and a slice of memory are all//! the same thing to it. It allocates nothing unless asked to — `readAlloc`//! is the only function here that takes an allocator.//!//! The container is `zig-riff`'s and not this library's. RIFF is what a WAVE//! file *is* — twelve bytes of header, then chunks with their lengths and//! their pad bytes — and WAVE is one of the forms built on it, alongside AVI//! and WebP. So the chunk walk lives there, this lives here, and what is here//! is only what makes a WAVE a WAVE: the `fmt ` chunk, the samples in `data`,//! and the conversion between those samples and numbers.//!//! ```zig//! const wav = @import("wav");//!//! var input: std.Io.Reader = .fixed(bytes);//! var file: wav.Reader = try .init(&input);//! const samples = try file.readAlloc(gpa, f32, 48_000 * 60);//! defer gpa.free(samples);//!//! var out: wav.Writer = try .init(sink, .{//! .sample_rate = file.header.sample_rate,//! .channels = file.header.channels,//! .format = .s16,//! .frames = samples.len / file.header.channels,//! });//! try out.write(f32, samples);//! try out.finish();//! ```//!//! Samples come out and go in as `f32`, `f64` or `i32`, whatever the file//! holds. The float types are normalized so that full scale is ±1, which is//! what a signal processing path wants; `i32` is the sample exactly as the//! file holds it, which is what a lossless path wants. `isSampleType` says//! what each means in full.//!//! ## What is understood//!//! Integer PCM at 8, 16, 24 and 32 bits, and IEEE float at 32 and 64, in a//! plain `WAVE_FORMAT_PCM` or `WAVE_FORMAT_IEEE_FLOAT` header or in a//! `WAVE_FORMAT_EXTENSIBLE` one carrying either of those as its sub-format.//! Chunks that are neither `fmt ` nor `data` are stepped over. A `data` chunk//! whose length was never filled in — which is what a WAVE file written to a//! pipe looks like, and what ffmpeg writes byte for byte — is read to the end//! of the stream.//!//! ## What is not//!//! The compressed payloads: A-law, µ-law, ADPCM and the rest of the tag//! registry. The big-endian `RIFX` container, whose lengths are the other way//! round while a WAVE's own fields are little-endian by definition. RF64 and//! BW64, which is how a file longer than four gigabytes says so. Each of//! those is refused by name — `error.UnsupportedFormat`, `error.Rifx`,//! `error.Rf64` — rather than misread. What is written is always the canonical 44 byte header,//! so a file with more than two channels is written without the channel mask//! that would have said which speaker each one is for, and a file read with//! one loses it.
const format_mod = @import("format.zig");const header_mod = @import("header.zig");const writer_mod = @import("Writer.zig");
/// Reading a WAVE file.pub const Reader = @import("Reader.zig");/// Writing one.pub const Writer = writer_mod;
/// How samples are laid out in the `data` chunk.pub const Format = format_mod.Format;/// What a WAVE file says about the samples in it.pub const Header = header_mod.Header;/// The two length fields of a header, for a caller that is patching them.pub const Sizes = header_mod.Sizes;
/// Which sample types `encode`, `decode`, `Reader.read` and `Writer.write`/// speak, and what each of them means.pub const isSampleType = format_mod.isSampleType;/// Samples in a `data` chunk, as numbers.pub const decode = format_mod.decode;/// Numbers, as samples in a `data` chunk.pub const encode = format_mod.encode;
/// Render the 44 byte header into a buffer.pub const render = header_mod.render;/// Write the 44 byte header.pub const writeHeader = header_mod.writeHeader;/// Convert samples and write them, without a `Writer` in between.pub const writeSamples = writer_mod.writeSamples;/// What the length fields of a header should say.pub const sizes = header_mod.sizes;
/// The size of the header this library writes.pub const header_size = header_mod.header_size;/// Where the `RIFF` chunk's length field sits within it.pub const riff_size_offset = header_mod.riff_size_offset;/// Where the `data` chunk's length field sits within it.pub const data_size_offset = header_mod.data_size_offset;/// What both length fields carry when the length is not known.pub const unknown_length = header_mod.unknown_length;
/// `WAVE_FORMAT_PCM`.pub const format_pcm = format_mod.format_pcm;/// `WAVE_FORMAT_IEEE_FLOAT`.pub const format_ieee_float = format_mod.format_ieee_float;/// `WAVE_FORMAT_EXTENSIBLE`.pub const format_extensible = format_mod.format_extensible;
test { // Reaches the files above so that `zig build test` runs every test in the // module rather than only the ones written here. @import("std").testing.refAllDecls(@This()); _ = Reader; _ = Writer; _ = format_mod; _ = header_mod;}
test "a file, out and back" { const std = @import("std"); const testing = std.testing;
const frames = 480; var samples: [frames * 2]f32 = undefined; for (0..frames) |i| { const phase = @as(f32, @floatFromInt(i)) / 48.0; samples[i * 2] = @sin(phase); samples[i * 2 + 1] = @cos(phase); }
var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit();
var w: Writer = try .init(&out.writer, .{ .sample_rate = 48000, .channels = 2, .format = .s24, .frames = frames, }); try w.write(f32, &samples); try w.finish();
var input: std.Io.Reader = .fixed(out.written()); var r: Reader = try .init(&input); try testing.expectEqual(@as(u32, 48000), r.header.sample_rate); try testing.expectEqual(@as(u16, 2), r.header.channels); try testing.expectEqual(Format.s24, r.header.format); try testing.expectEqual(@as(?u64, frames), r.header.frames);
const back = try r.readAlloc(testing.allocator, f32, frames); defer testing.allocator.free(back); try testing.expectEqual(samples.len, back.len); // 24 bits is a step of about 1.2e-7, so a sample is back to within one. for (samples, back) |before, after| { try testing.expectApproxEqAbs(before, after, 1.0 / 8388608.0); }}