Projects/Build a WAV/PCM Toolkit

Build a WAV/PCM Toolkit

Every lesson is one concrete spec with exact bytes or exact integer samples: the RIFF header pinned byte for byte, a 16-bit little-endian sample 00 80 decoding to -32768, the even-size padding byte after an odd data chunk, gain that clips at +32767 instead of wrapping, a mix sum that clamps, byteRate equal to sampleRate times blockAlign, a sine sample at a known phase with the rounding stated, and mono duplicated to stereo then averaged back with rounding. No real audio hardware, no external files - just bytes and samples you can assert.

31 lessonsSmall~20 min / lessonRIFF/WAVPCM audioDigital signal processing
The project

What you'll build over the next 31 lessons

Over 31 lessons you build a working WAV/PCM toolkit from scratch: a library that reads and writes real WAV files and transforms raw PCM samples, with a small command-line tool on top. Everything is pinned to exact bytes and exact integer samples, so the toolkit you write behaves identically in any language and needs no audio hardware or sample files to test.

You begin at the container level, walking the RIFF/WAVE format one chunk at a time - a four-byte id, a little-endian size, a payload - parsing the fmt chunk's fields, skipping chunks you do not recognise, and honouring the even-size padding byte. Then you decode PCM samples for every common depth (8-bit unsigned with its 128 bias, 16-bit and 24-bit signed little-endian, and 32-bit float), de-interleave stereo into per-channel slices, and turn the whole thing around to write valid files, proven correct by round-trip equality. On that foundation you add the sample math - gain with clipping, mixing with clamping, normalization to a target peak, linear fades, reversing, and channel conversions - then synthesize sine, square, and sawtooth tones, shape them with an ADSR-lite envelope, and add a delay echo. The capstone synthesizes a two-tone stereo signal, applies gain, a fade, and a mix, writes a 16-bit stereo WAV, and reads it back to confirm the exact samples and header.

This is a teaching-grade toolkit built around uncompressed PCM in the classic RIFF/WAVE container. It decodes 8/16/24-bit integer and 32-bit float PCM and writes 16-bit PCM, but it deliberately stops short of the wider format: writing is 16-bit only, no compressed or ADPCM codecs, no WAVE_FORMAT_EXTENSIBLE channel masks, no cue/list/metadata chunk authoring, and no resampling or filtering beyond the effects you build. That honest core is exactly what production libraries like libsndfile and dr_wav extend with more codecs, formats, and streaming.

build-a-wav-pcm-toolkit / lesson-01.md
Lesson 01 · The RIFF/WAVE container

The four-byte chunk id

A WAV file is built out of chunks, and every chunk starts with a four-byte ASCII tag that names it. Today you read those four bytes back as text, the smallest possible first step into the format.

The goal

Read four bytes and return them as a four-character ASCII string.

Start here - the target
TO DO
Scenario: A four-byte id decodes to its ASCII name
Giventhe bytes 0x52 0x49 0x46 0x46
Whenthey are read as a four-byte id
Thenthe id is the string "RIFF"
Andthe bytes 0x66 0x6D 0x74 0x20 read as the id "fmt " (with a trailing space)
Background

The WAV format is a specific use of a general container called RIFF (Resource Interchange File Format). A RIFF file is a sequence of chunks, and every chunk begins with a four-character code - four raw ASCII bytes that name what the chunk is. RIFF, WAVE, fmt , and data are the ones you will meet first.

These tags are stored as plain bytes, big-endian in the sense that byte order is just reading order - the first byte is the first character. Note that fmt is four characters including a trailing space (0x20); the format is strict about the count, so the space is real and required. Today is deliberately tiny: turn four bytes into a string. Every chunk you parse from here on starts exactly this way.

Make it work
// a chunk id is just four ASCII bytes, read in order
func readID(b []byte) string {
return string(b[0:4])
}
CheckpointDONE
You can read a four-byte chunk id as text. Commit and stop here.
Scope & extensions

Where this project stops - and where to go next

The toolkit reads and writes real 16-bit PCM WAV files end to end (verified against macOS's afinfo) with a working synthesize, transform, and inspect CLI, but write is 16-bit only, 32-bit float read is decoded yet not wired in, and there is no metadata, cue, or compressed-format support.

Extend it next
  • Wire 32-bit float decode into the sample reader dispatch and add a matching float encoder for true float round-trips
  • Add 8-bit and 24-bit write encoders so files write back at their source bit depth instead of always converting to 16-bit
  • Reject a short fmt chunk and a zero channel count at the parser itself, not only at the hardened read entry point
  • Make gain, mix, delay, and normalize bit-depth-aware instead of always clamping to the 16-bit range
  • Read and write basic LIST/INFO metadata and cue points
  • Add WAVE_FORMAT_EXTENSIBLE support for more than two channels or non-standard channel masks
Recommended reading

Books & references that go deeper

  • WAVE PCM soundfile format · Stanford CCRMA

    The single clearest one-page description of the canonical 44-byte WAV header - every field, its offset, size, and endianness, with the exact byte layout this project pins lesson by lesson.

  • Microsoft's authoritative definition of the fmt chunk fields (wFormatTag, nChannels, nSamplesPerSec, nAvgBytesPerSec, nBlockAlign, wBitsPerSample) and the derived-value rules the writing chapter reproduces.

  • A free, exceptionally readable DSP textbook - quantization, sampling, gain, mixing, and the math behind the synthesis and effects chapters, without assuming a signals background.

  • A short primer on the two numbers that define PCM audio - sample rate and bit depth - and what quantization and clipping actually mean, useful orientation before the decoding chapter.

  • The original RIFF container specification: chunk structure, the four-byte id plus little-endian size plus payload, and the word-alignment padding rule that the container chapter implements.