Most WAV bugs I have seen come from this line:
samples = data[44:] 44 is the length of the smallest legal WAV: 12 bytes of RIFF header, a 24-byte fmt chunk, then the 8-byte header of the data chunk. So it is right for that one file. It is not the format.
WAV is RIFF, and RIFF is a list of chunks. Each chunk is a 4-byte id, a 4-byte little-endian size, then that many bytes, padded to an even length. The data chunk can sit anywhere in the list, and anything the writer added goes in front of it.
Five files, four offsets
These are from loremfile.dev, a CC0 fixture set I maintain. I parsed each one by walking its chunks.
| File | fmt body | Before data | Samples start |
|---|---|---|---|
| 16-bit stereo | 16 bytes | nothing | 44 |
| 8-bit stereo | 16 bytes | nothing | 44 |
| 32-bit float | 18 bytes | fact, 12 bytes | 58 |
| 24-bit, 48 kHz | 40 bytes | nothing | 68 |
| 16-bit with tags | 16 bytes | LIST, 50 bytes | 94 |
Same format, same 440 Hz sine wave, four different answers. Three of the five break data[44:]. None of them is unusual: a float export, a 24-bit export, and a file with a title and artist in it.
The header length is not fixed
fmt is 16 bytes for plain PCM, 18 when the writer adds the cbSize field, and 40 for WAVE_FORMAT_EXTENSIBLE. That moves the samples on its own.
Other chunks move them further. Non-PCM files should carry a fact chunk with the sample count, which puts the float file at 58. Metadata goes in LIST/INFO, which puts the tagged file at 94. Read the sizes and skip what you do not recognise.
Bit depth does not tell you the sample type
The format tag does:
-
1is integer PCM -
3is IEEE float -
0xFFFEis extensible, and the real format is a subformat 24 bytes into the chunk body
The float file reports 32 bits with tag 3. Read those bytes as 32-bit integers and every sample comes out huge, because you are reading an exponent as a magnitude.
The 24-bit file is the other half of this. Its tag is 0xFFFE, so code that switches on the top-level tag gives up on it, even though the subformat says plain PCM.
8-bit is unsigned
Everything above 8-bit is signed. Same sine wave, same instant, three of the files above:
8-bit: 128, 128, 128, 128, 129, 129, 130, 130 16-bit: 0, 0, 181, 181, 361, 361, 541, 541 float: 0.0, 0.0, 0.0055, 0.0055, 0.011, 0.011 Silence is 128 in the first and 0 in the other two. Read the 8-bit file as signed and silence becomes a full-scale offset.
Walking the chunks
import struct def wav_chunks(data): """Yield (chunk_id, body_offset, body_size) for every top-level RIFF chunk.""" if data[0:4] != b"RIFF" or data[8:12] != b"WAVE": raise ValueError("not a RIFF/WAVE file") pos = 12 while pos + 8 <= len(data): chunk_id = data[pos:pos + 4] size = struct.unpack_from("<I", data, pos + 4)[0] yield chunk_id, pos + 8, size pos += 8 + size + (size % 2) # chunks are padded to an even length def wav_samples(data): """The format tag, bit depth and sample bytes, wherever they turned out to be.""" tag = bits = None for chunk_id, offset, size in wav_chunks(data): if chunk_id == b"fmt ": tag, _, _, _, _, bits = struct.unpack_from("<HHIIHH", data, offset) if tag == 0xFFFE and size >= 40: # the real tag is the subformat tag = struct.unpack_from("<H", data, offset + 24)[0] elif chunk_id == b"data": return tag, bits, data[offset:offset + size] raise ValueError("no data chunk") Two things to keep. The size % 2 padding is required by RIFF and easy to miss, because it only matters on odd-sized chunks. Resolving 0xFFFE to its subformat means extensible files stop being a special case.
The files
The links in the table are direct, with no sign-up and no redirect, so curl and CI can fetch them. The full WAV set is on one page with a SHA-256 for each file. Everything is CC0, so you can copy them into your own fixtures instead of depending on my domain.
If you have a WAV reader, point it at the 58-byte and 94-byte files and see what comes out.

