The bug
WAV 8-bit PCM is unsigned (0–255, silence at 128) per the spec. recode decodes it as signed, so every 8-bit sample comes back off by a 128 wrap:
raw payload bytes [ 0, 64, 128, 192, 255]
spec-correct amplitudes [-128, -64, 0, 64, 127] # unsigned minus 128
recode decode_wav_bytes [ 0, 64,-128, -64, -1] # wrong
Digital silence (128) decodes as -128 — full negative.
Why it is only worth filing now
Before #11 the 8-bit path was usually reading from the wrong offset anyway: the old size-subtraction counted the RIFF word-alignment pad byte as header, so a 3-sample 8-bit file returned [2, 3, 0] where the payload was [1, 2, 3]. #11 fixed the alignment, so the 8-bit path now lands on the right bytes for the first time — which makes the sign error the remaining defect and newly reachable.
Why it was kept out of #11
The signedness is not decided in the WAV code. It comes from mk_pcm_audio_codec / num_find_num_type_for picking the struct character, whose blast radius is every recode consumer, not just the WAV path. Non-WAV PCM callers may legitimately want today's signed b.
Suggested shape
For width_bytes == 1 on the WAV path specifically, decode with the unsigned struct char B and subtract 128; the encoder adds it back. Leave generic PCM callers on the current signed behaviour, or give them an explicit signed= choice.
Acceptance test — the probe values above round-trip:
assert decode_wav_bytes(eight_bit_wav)[0] == [-128, -64, 0, 64, 127]
assert encode_wav_bytes([-128, -64, 0, 64, 127], sr, width_bytes=1) == eight_bit_wav
Note _wav(..., width=1) in test_recode.py already builds valid 8-bit fixtures.
Found by adversarial review while fixing #4.
The bug
WAV 8-bit PCM is unsigned (0–255, silence at 128) per the spec.
recodedecodes it as signed, so every 8-bit sample comes back off by a 128 wrap:Digital silence (
128) decodes as-128— full negative.Why it is only worth filing now
Before #11 the 8-bit path was usually reading from the wrong offset anyway: the old size-subtraction counted the RIFF word-alignment pad byte as header, so a 3-sample 8-bit file returned
[2, 3, 0]where the payload was[1, 2, 3]. #11 fixed the alignment, so the 8-bit path now lands on the right bytes for the first time — which makes the sign error the remaining defect and newly reachable.Why it was kept out of #11
The signedness is not decided in the WAV code. It comes from
mk_pcm_audio_codec/num_find_num_type_forpicking the struct character, whose blast radius is everyrecodeconsumer, not just the WAV path. Non-WAV PCM callers may legitimately want today's signedb.Suggested shape
For
width_bytes == 1on the WAV path specifically, decode with the unsigned struct charBand subtract 128; the encoder adds it back. Leave generic PCM callers on the current signed behaviour, or give them an explicitsigned=choice.Acceptance test — the probe values above round-trip:
Note
_wav(..., width=1)intest_recode.pyalready builds valid 8-bit fixtures.Found by adversarial review while fixing #4.