wav-codec
WAV (RIFF/WAVE) reader and writer over std::io. PCM and IEEE-float WAV
only. Compressed WAV (ADPCM, mu-law, A-law) not decoded. The reader
decodes PCM at 8, 16, 24, and 32 bits, packed or in wider
WAVE_FORMAT_EXTENSIBLE containers, plus IEEE float at 32 and 64 bits. The
writer emits the same encodings with the canonical header for each.
- Zero runtime dependencies,
#![forbid(unsafe_code)], MSRV 1.63.0. - Block-buffered decoding: the reader refills a 64 KiB buffer with bulk
reads clamped to the
datachunk and decodes whole blocks. On the 5.3 MB perf fixtureread_samplesruns at 0.8x andsamples()at 1.9x the time of a rawfs::readpluschunks_exactdecode (seeBENCHES.md). The test suite holds both paths within 3x. - Truncation is an error. A stream that ends inside a sample, or short of
the declared
datasize, yields exactly oneErrand then the iterator is fused. A 48-byte file declaring 4 GiB of data costs two samples and one error, andsize_hintreports(0, Some(declared)). - Word alignment per RIFF: odd-sized chunks carry a pad byte on both the
read and write sides,
factchunks of any length are skipped by their declared size, and the writer finalizes an evenriff_size. - Byte-exact round-trips: six canonical conformance fixtures across u8, i16, i24, i32, f32, and f64 re-encode to their original sha256. Two fixtures from ffmpeg 8.1.2 (f64 and EXTENSIBLE s24) match after nearest-integer scaling.
- Malformed input maps to a typed
Errorwith no panic path: 27 conformance vectors, every truncation length and single-byte mutation of each, and a cargo-fuzz target over open plus drain.
Reading
use WavReader;
let mut reader = open?;
let spec = reader.spec;
println!;
let samples: = reader..?;
samples borrows the reader and into_samples consumes it. Both yield
Result<S>. read_samples fills a slice in bulk and returns the count, with
a short count only at the end of the data chunk. seek positions the
reader at a frame and advances by the container width, so a 24-in-32 file
seeks in 4-byte steps.
| file encoding | readable as | writable from |
|---|---|---|
| 8-bit PCM (offset binary) | i8, i16, i32 |
i8, i16, i32 (in range) |
| 16-bit PCM | i16, i32 |
i8, i16, i32 (in range) |
| 24-bit PCM, 3 or 4 bytes | i32 |
i8, i16, i32 (in range) |
| 32-bit PCM | i32 |
i8, i16, i32 |
| 32-bit float | f32, f64 |
f32 |
| 64-bit float | f64 |
f32, f64 |
8-bit files are unsigned on disk and exposed as i8 with the value
stored_byte - 128. A type too narrow for the file, or of the wrong class,
yields one Error::TooWide.
Writing
use ;
let spec = WavSpec ;
let mut writer = create?;
writer.write_samples?;
writer.finalize?;
WavWriter::new picks the narrowest container for the bit depth.
new_with_spec_ex takes an explicit container width, for example 24 valid
bits in 4-byte containers. Header layout by spec:
- integer PCM, up to 2 channels, valid bits filling the container:
fmtsize 16. - IEEE float, up to 2 channels:
fmtsize 18 plus afactchunk with the frame count. - a container wider than the valid bits, or more than 2 channels:
fmtsize 40 (WAVE_FORMAT_EXTENSIBLE), plusfactfor float.
finalize writes the pad byte for an odd data size, patches the RIFF,
data, and fact sizes, and returns Error::UnfinishedFrame when the
sample count is off a frame boundary. The header is patched to whole
frames so the file stays readable. Dropping a writer runs the same steps
and discards the result, so Drop cannot report UnfinishedFrame. Call
finalize to observe that error. A write that would push the data size
past the 32-bit header fields returns Error::TooLarge before any
arithmetic can wrap.
Scope
Rejected with a clean error: compressed format tags, RF64 and BW64
(over 4 GiB), RIFX, W64, and any WAVE_FORMAT_EXTENSIBLE GUID other
than PCM and IEEE float. The crate is an adapter over std::io::Read and
std::io::Write, so no_std is unsupported.
Testing
cargo test replays the 27-vector conformance manifest in tests/, every
fixture pinned by sha256, and the perf gate regenerates its 5.3 MB fixture
from the manifest's LCG description. cargo bench --bench read_throughput
reports MiB/s for the three decode paths and fails in release on a ratio
above 3x. cargo fuzz run read_drain (nightly) seeds from the fixtures.
CI builds the library and doctests at 1.63.0 and builds and runs an
external consumer crate on that toolchain.
License
MIT. Copyright (c) 2026 Justin Jones.