WAV: Complete Guide to Waveform Audio File Format
WAV (Waveform Audio File Format) is a Microsoft/IBM audio container standardised in 1991 as part of the Resource Interchange File Format (RIFF) specification. WAV files store uncompressed Pulse Code Modulation (PCM) audio — the raw digital representation of a sound waveform with no lossy compression applied. WAV is the universal exchange format between professional audio tools, Digital Audio Workstations (DAWs), hardware recorders, and broadcast systems.
RIFF Container Structure
WAV is built on the RIFF (Resource Interchange File Format) chunk-based container. Every RIFF file consists of a 12-byte RIFF header followed by nested chunks:
RIFF header (12 bytes):
ChunkID: "RIFF" (4 bytes, ASCII)
ChunkSize: total file size − 8 (4 bytes, little-endian uint32)
Format: "WAVE" (4 bytes, ASCII)
Mandatory chunks:
"fmt " chunk — audio format parameters
"data" chunk — raw sample data
Optional chunks (may appear between fmt and data):
"JUNK" / "PAD" — alignment padding
"fact" — number of sample frames (non-PCM formats)
"bext" — Broadcast Wave Format (BWF) metadata
"smpl" — sampler loop points
"LIST" — INFO sub-chunks (artist, title, etc.)
"cue " — cue point markers
"plst" — playlist
The fmt Chunk (16–40 bytes)
AudioFormat: 2 bytes — 1=PCM, 3=IEEE float, 6=A-law, 7=μ-law, 65534=Extensible
NumChannels: 2 bytes — 1=mono, 2=stereo, up to 65535
SampleRate: 4 bytes — e.g., 44100, 48000, 96000, 192000
ByteRate: 4 bytes — SampleRate × NumChannels × BitsPerSample/8
BlockAlign: 2 bytes — NumChannels × BitsPerSample/8
BitsPerSample: 2 bytes — 8, 16, 24, 32 (PCM); 32, 64 (IEEE float)
// For WAVE_FORMAT_EXTENSIBLE (AudioFormat=65534):
ExtensionSize: 2 bytes — 22
ValidBitsPerSample: 2 bytes
ChannelMask: 4 bytes — speaker position bitmask
SubFormat: 16 bytes — GUID identifying the actual format
WAVE_FORMAT_EXTENSIBLE (AudioFormat=65534) extends the basic fmt chunk to support more than 2 channels with speaker position mapping, bit depths where ValidBitsPerSample < BitsPerSample (e.g., 24-bit in a 32-bit container), and arbitrary codec subformats via GUID.
PCM Sample Encoding
For standard integer PCM:
- 8-bit: unsigned integers, 0–255, silence at 128
- 16-bit: signed integers, −32768 to +32767 (0x8000 to 0x7FFF), little-endian, silence at 0
- 24-bit: signed integers, 3 bytes per sample, little-endian — most common in studio recording
- 32-bit integer: signed, 4 bytes per sample
- 32-bit float (IEEE 754): values in −1.0 to +1.0 range; AudioFormat=3
- 64-bit float: double precision; AudioFormat=3 with BitsPerSample=64
Samples are interleaved by channel for multi-channel files: L₁ R₁ L₂ R₂ L₃ R₃ … (for stereo) or L₁ R₁ C₁ LFE₁ SL₁ SR₁ L₂ R₂ … (for 5.1).
Common Sample Rates and Their Use Cases
| Sample rate | Nyquist limit | Use case |
|---|---|---|
| 8,000 Hz | 4,000 Hz | Telephony, speech (G.711) |
| 16,000 Hz | 8,000 Hz | Wideband speech (WebRTC) |
| 22,050 Hz | 11,025 Hz | Half-CD, legacy multimedia |
| 44,100 Hz | 22,050 Hz | CD audio, consumer music |
| 48,000 Hz | 24,000 Hz | Professional video production |
| 88,200 Hz | 44,100 Hz | Double-speed CD production |
| 96,000 Hz | 48,000 Hz | High-resolution audio |
| 176,400 Hz | 88,200 Hz | Ultra-high-res production |
| 192,000 Hz | 96,000 Hz | High-end studio / mastering |
Python WAV Processing
Standard Library wave Module
import wave
import struct
# ---- Read a WAV file ----
with wave.open('input.wav', 'rb') as wf:
n_channels = wf.getnchannels() # 1=mono, 2=stereo
sample_width = wf.getsampwidth() # bytes per sample (1=8-bit, 2=16-bit, 3=24-bit)
frame_rate = wf.getframerate() # sample rate in Hz
n_frames = wf.getnframes() # total PCM frames
duration_s = n_frames / frame_rate
print(f"Channels : {n_channels}")
print(f"Sample width: {sample_width} bytes ({sample_width*8}-bit)")
print(f"Sample rate : {frame_rate} Hz")
print(f"Duration : {duration_s:.3f} s")
print(f"File size : {n_frames * n_channels * sample_width / 1024:.1f} KB (audio data)")
# Read all PCM frames
raw_data = wf.readframes(n_frames)
# Unpack 16-bit stereo samples
n_samples = n_frames * n_channels
samples = struct.unpack(f'<{n_samples}h', raw_data)
# Stereo: samples[0]=L, samples[1]=R, samples[2]=L, ...
NumPy and SciPy for Signal Processing
import numpy as np
import scipy.io.wavfile as wav
import scipy.signal as signal
# Read WAV (returns int16, int32, or float32 array)
rate, data = wav.read('input.wav')
print(f"Shape: {data.shape}, dtype: {data.dtype}, rate: {rate} Hz")
# For stereo: data.shape = (n_frames, 2)
# Normalise to float −1.0 to +1.0
if data.dtype == np.int16:
data_float = data.astype(np.float32) / 32768.0
elif data.dtype == np.int32:
data_float = data.astype(np.float64) / 2147483648.0
# Apply a simple low-pass filter at 8 kHz
sos = signal.butter(10, 8000, fs=rate, btype='low', output='sos')
filtered = signal.sosfilt(sos, data_float, axis=0)
# Write the filtered result
wav.write('filtered.wav', rate, (filtered * 32767).astype(np.int16))
# Compute spectrogram
f, t, Sxx = signal.spectrogram(data_float[:, 0] if data_float.ndim > 1 else data_float,
fs=rate, nperseg=1024)
soundfile for Advanced Format Support
import soundfile as sf
# Read 24-bit or 32-bit float WAV
data, samplerate = sf.read('studio_24bit.wav')
print(f"Shape: {data.shape}, rate: {samplerate}, dtype: {data.dtype}")
# soundfile returns float64 by default, preserving full precision
# Write 24-bit WAV
sf.write('output_24bit.wav', data, samplerate, subtype='PCM_24')
# Write 32-bit float WAV
sf.write('output_32f.wav', data, samplerate, subtype='FLOAT')
# Available subtypes: PCM_8, PCM_16, PCM_24, PCM_32, FLOAT, DOUBLE
print(sf.available_subtypes('WAV'))
# Write 5.1 surround (6-channel) WAV
import numpy as np
multichannel = np.zeros((samplerate * 5, 6)) # 5 seconds, 6 channels
sf.write('surround_51.wav', multichannel, samplerate, subtype='PCM_24')
Parsing Raw RIFF Chunks
import struct
def list_wav_chunks(path: str) -> list:
"""List all RIFF chunks in a WAV file."""
chunks = []
with open(path, 'rb') as f:
riff_id = f.read(4)
riff_size = struct.unpack('<I', f.read(4))[0]
wave_id = f.read(4)
assert riff_id == b'RIFF' and wave_id == b'WAVE', "Not a WAV file"
while True:
chunk_id_bytes = f.read(4)
if len(chunk_id_bytes) < 4:
break
chunk_id = chunk_id_bytes.decode('ascii', errors='replace').strip()
chunk_size = struct.unpack('<I', f.read(4))[0]
offset = f.tell()
chunks.append({'id': chunk_id, 'size': chunk_size, 'offset': offset})
f.seek(chunk_size + (chunk_size % 2), 1) # skip data + alignment byte
return chunks
for chunk in list_wav_chunks('input.wav'):
print(f" {chunk['id']!r:8s} {chunk['size']:>10,} bytes @ {chunk['offset']}")
WAV vs Other Audio Formats
| Format | Lossless | Compressed | Max bit depth | Max channels | Professional use |
|---|---|---|---|---|---|
| WAV | Yes | No | 32-bit float | 65535 | Universal |
| AIFF | Yes | No | 32-bit float | Unlimited | Apple/Pro |
| FLAC | Yes | Yes (~55% WAV) | 32-bit int | 8 | Archive/delivery |
| BWF | Yes | No | 32-bit float | 65535 | Broadcast |
| RF64 | Yes | No | 32-bit float | 65535 | >4 GB files |
| MP3 | No | Yes | 16-bit | 2 | Consumer |
WAV's 4 GB size limit: The standard RIFF chunk size field is a 32-bit unsigned integer, capping WAV files at ~4 GB (approximately 6.4 hours at 44.1 kHz 16-bit stereo). For longer recordings, use RF64 (EBU R94) which extends the size fields to 64 bits — FFmpeg writes RF64 automatically when the content would exceed 4 GB.
Common Pitfalls
- Sample rate mismatch causes chipmunk/slow-motion playback. If a DAW reads a 96 kHz file as 48 kHz, it plays back at half speed with half the pitch. Always preserve and communicate sample rate in project handoffs.
- 8-bit WAV is unsigned; 16-bit+ is signed. This asymmetry trips up custom parsers — 8-bit silence is 0x80 (128); 16-bit silence is 0x0000 (0).
- 24-bit in a 32-bit container. Some recorders write 32-bit WAV where only 24 bits carry audio (the lowest byte is always zero).
soundfilereads the actual bit depth from the file header correctly;scipy.io.wavfilereturns int32 samples that you'd need to right-shift by 8 bits. - BWF metadata (bext chunk) for professional workflows. Film and broadcast require BWF (Broadcast Wave Format) with SMPTE timecode origin, description, and UID fields in the
bextchunk. Use thebextoption in FFmpeg (-write_bext 1 -timestamp_from_frames 1) or dedicated tools like BWFMetaEdit.
Related conversions
Audio format pairs that come up most often: