wav-codec 0.1.0

WAV (RIFF/WAVE) reader and writer over std I/O. PCM and IEEE-float WAV only. Compressed WAV (ADPCM, mu-law, A-law) not decoded.
Documentation
  • Coverage
  • 100%
    25 out of 25 items documented1 out of 5 items with examples
  • Size
  • Source code size: 159.3 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.1 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 3s Average build duration of successful builds.
  • all releases: 3s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • hey-jj/wav-codec
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • hey-jj

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 data chunk and decodes whole blocks. On the 5.3 MB perf fixture read_samples runs at 0.8x and samples() at 1.9x the time of a raw fs::read plus chunks_exact decode (see BENCHES.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 data size, yields exactly one Err and then the iterator is fused. A 48-byte file declaring 4 GiB of data costs two samples and one error, and size_hint reports (0, Some(declared)).
  • Word alignment per RIFF: odd-sized chunks carry a pad byte on both the read and write sides, fact chunks of any length are skipped by their declared size, and the writer finalizes an even riff_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 Error with 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 wav_codec::WavReader;

let mut reader = WavReader::open("input.wav")?;
let spec = reader.spec();
println!("{} Hz, {} channels, {} bits", spec.sample_rate, spec.channels, spec.bits_per_sample);
let samples: Vec<i16> = reader.samples::<i16>().collect::<Result<_, _>>()?;

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 wav_codec::{SampleFormat, WavSpec, WavWriter};

let spec = WavSpec {
    channels: 2,
    sample_rate: 48_000,
    bits_per_sample: 24,
    sample_format: SampleFormat::Int,
};
let mut writer = WavWriter::create("out.wav", spec)?;
writer.write_samples(&[0i32, 1, -1, 8_388_607])?;
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: fmt size 16.
  • IEEE float, up to 2 channels: fmt size 18 plus a fact chunk with the frame count.
  • a container wider than the valid bits, or more than 2 channels: fmt size 40 (WAVE_FORMAT_EXTENSIBLE), plus fact for 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.