Skip to content

Fix the last of the WAV header guesswork: file headers, 8-bit sign, EXTENSIBLE - #14

Merged
thorwhalen merged 1 commit into
masterfrom
fix/wav-header-parsing-and-8bit-unsigned
Sep 22, 2026
Merged

thorwhalen merged 1 commit into
masterfrom
fix/wav-header-parsing-and-8bit-unsigned

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Four defects in recode.audio, all in how a WAV header is read:

  1. extract_wav_header_from_file still inferred header size as
    chunk_size + 8 - subchunk2_size — the arithmetic every other reader in the
    module had already moved away from. On real ffmpeg output it returned nearly
    the whole file as "header" and never raised on non-WAV input. Now walks the
    RIFF structure like the rest of the module and raises ValueError on a
    non-WAV file.
  2. 8-bit WAV PCM is unsigned (0..255, silence at 128); recode read/wrote it
    signed, so digital silence decoded as -128 and files it wrote were misread by
    every other tool. Fixed in the WAV path behind a new keyword-only
    eight_bit_unsigned=True on decode_wav_bytes/encode_wav_bytes; passing
    False reproduces today's bytes exactly. Closes 8-bit WAV is decoded as signed, but WAV 8-bit PCM is unsigned (silence reads as -128) #12.
  3. WAVE_FORMAT_EXTENSIBLE (fmt tag 0xFFFE, what ffmpeg writes for >2
    channels or >16 bits) made stdlib wave raise unknown format: 65534 on
    Python 3.10 (the CI version; stdlib only learned to read it in 3.12). A
    fmt chunk Wave_read refuses is now parsed directly, accepting the same
    PCM SubFormat GUID 3.12 accepts; every other wave.Error still surfaces
    unchanged. Closes WAVE_FORMAT_EXTENSIBLE decodes on Python 3.12 but raises on 3.10 (the CI version) #13.
  4. Restores header_size_of_wav_bytes's meta parameter and MIN_WAV_N_BYTES,
    both dropped in Fix #4: locate the WAV data chunk instead of inferring where it must be #11 while the docstring still promised them (meta as an
    ignored no-op).

No public export or existing default changed except eight_bit_unsigned
(new keyword-only, defaults to True — the only behavior-changing default in
this PR, and it's a bug fix: 8-bit WAV silence was previously mis-decoded).

Dependents check (fleet_dependents.json: recode -> [hum, know], both
present on this box):

  • hum only calls recode.decode_wav_bytes (never passes
    eight_bit_unsigned, so gets the new default). Installed this branch's
    recode over hum's env and ran its non-hardware-dependent test suite:
    22 passed, 1 skipped (a pyo-audio-engine test, skipped identically without
    this change — no audio backend on this box), 1 deselected. No 8-bit WAV
    fixtures in hum's suite, so this is an import/wiring check rather than a
    behavioral one for the 8-bit path specifically.
  • know calls recode.mk_codec (in recode/base.py, untouched by this
    diff — confirmed via git diff showing zero changes to base.py/__init__.py)
    and recode.decode_wav_bytes (in know/boxes/dols.py). Could not install
    know on this box — audiostream2py -> pyaudio needs portaudio.h, a
    system audio library not present here. know's own test suite
    (test_main_exports.py, test_slabsIter.py) doesn't exercise
    decode_wav_bytes at all (the latter reads WAV via soundfile, not
    recode), so even a successful install would not have tested the changed
    path. Recorded as a source-level read in DECISIONS.md: the risk is confined
    to 8-bit WAV input, which is uncommon for keyboard/stream audio capture, and
    is a documented, opt-outable bug fix rather than an arbitrary default change.

Branch sat pushed with green CI and no PR for two weeks (thorwhalen/fleet_stuff
cleanup). Verified: master has not moved since the branch was cut (no rebase
needed). recode has no pyproject.toml/wads config (setup.py/setup.cfg
only) -> gated with plain venv+pytest (py3.10, matching CI): 172/172 passed
(145 in test_recode.py + 27 doctests), matching the commit's own claimed
"151 -> 172 passing". Hosted CI on the branch was already green from the
original 2026-09-08 push (3.10, 3.11, 3.12, 3.13).

Closes #12
Closes #13

🤖 Generated with Claude Code

…XTENSIBLE

Four defects in `recode.audio`, all in how a WAV header is read.

`extract_wav_header_from_file` still inferred the header size as
`chunk_size + 8 - subchunk2_size` -- the very arithmetic #4 removed everywhere
else, and the last instance left. It reads bytes 40-44 as the audio size, which
is true only of a bare 44-byte header. On real ffmpeg output it returned nearly
the whole file as "header" (26499 of 26562 bytes on a 6-channel sample); it
disagreed with `header_size_of_wav_bytes` on the same bytes and never raised on
anything. It now walks the RIFF structure like everything else, reading only as
much of the file as the header occupies, and raising `ValueError` when the file
is not a WAV.

8-bit WAV PCM is unsigned (0..255, silence at 128); recode read and wrote it
signed, so digital silence decoded as -128 and the files it wrote were misread
by every other tool. Fixed in the WAV path only, behind a keyword-only
`eight_bit_unsigned=True` on `decode_wav_bytes`/`encode_wav_bytes`; passing
`False` reproduces today's bytes exactly. `soundfile` now agrees with recode in
both directions on 8-bit files. Closes #12.

`WAVE_FORMAT_EXTENSIBLE` (fmt tag 0xFFFE) is what ffmpeg writes for anything
above two channels or sixteen bits, and stdlib `wave` only learned to read it in
3.12 -- so those files raised `unknown format: 65534` on 3.10, the version CI
runs. `Wave_read` stays the primary reader; a `fmt ` chunk it refuses is parsed
directly, accepting the same PCM SubFormat GUID 3.12 insists on, so every other
`wave.Error` still surfaces unchanged. A 6-channel ffmpeg file now decodes
byte-identically on 3.10 and 3.12. Closes #13.

Finally, #11 dropped `header_size_of_wav_bytes`' `meta` parameter and the
`MIN_WAV_N_BYTES` constant while the new docstring promised `meta` was still
accepted. Both are restored, `meta` as an ignored no-op.

Also splits the chunk walk out of `_wav_data_chunk` into `_wav_chunk`, which is
what the three new readers share. No behaviour change there.

Suite: 151 -> 172 passing, green on 3.10, 3.11, 3.12 and 3.13.

Claude-Session: https://claude.ai/code/session_01L1aQPB34n7PU7jmbztSjBe
@thorwhalen
thorwhalen merged commit dcefcea into master Sep 22, 2026
4 checks passed
@thorwhalen
thorwhalen deleted the fix/wav-header-parsing-and-8bit-unsigned branch September 22, 2026 12:59
thorwhalen added a commit that referenced this pull request Sep 22, 2026
encode_wav_bytes(np.int8 array, width_bytes=1) worked before #14 and raised
OverflowError after it (numpy 2) because the +128 bias was applied to np.int8
scalars. Samples are now widened with operator.index before the shift, which
also keeps non-integer samples failing loudly instead of truncating.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant