Skip to content

Accept PCM8/32, float64, A-law, mu-law and WAVEFORMATEXTENSIBLE in the WAV reader - #319

Merged
0xShug0 merged 3 commits into
0xShug0:mainfrom
5uck1ess:wav-reader-formats
Aug 27, 2026
Merged

Accept PCM8/32, float64, A-law, mu-law and WAVEFORMATEXTENSIBLE in the WAV reader#319
0xShug0 merged 3 commits into
0xShug0:mainfrom
5uck1ess:wav-reader-formats

Conversation

@5uck1ess

Copy link
Copy Markdown
Contributor

Follow-up to #180, split out at your request.

The reader handled PCM16, PCM24 and float32. Everything else — including ordinary PCM16 that happens to carry the 0xFFFE tag — came back as unsupported WAV encoding (need PCM16, PCM24, or float32), which is a confusing thing to read when PCM16 is exactly what is inside the file. Encoders emit WAVEFORMATEXTENSIBLE routinely for more than two channels or whenever a channel mask is set, and the real format tag then lives in the SubFormat GUID rather than in wFormatTag.

What changes

  • WAVEFORMATEXTENSIBLE (0xFFFE) unwraps to whatever its GUID names
  • PCM8 (unsigned, biased by 128 — the sign convention differs from every other PCM width), PCM32, float64
  • G.711 A-law and mu-law
  • Files that still cannot be decoded now say what they are: a FLAC, Ogg, MP3, MP4/M4A, AIFF, RF64 or CAF handed to the reader is named as such instead of failing with invalid WAV RIFF header

No behaviour change for PCM16/24 or float32.

Origin

The decoders are @dignome's, from the tree that arrived at fa8206c in #180 — they existed so the CLI and server path would accept the same files the web UI already did. That was out of scope for Echo, so it was reverted out and is landing here on its own merits instead.

Tests

The original had none; this adds 213 lines covering each added format.

The G.711 cases are pinned to the published decode values — mu-law 0x00 → -32124 and 0x80 → +32124, both 0x7F and 0xFF → 0, A-law 0x55 → +8, 0xD5 → -8, 0x2A → +32256, including A-law's absence of an exact zero. Re-deriving the same bit manipulation in the test and comparing it to itself would prove nothing.

Two negative cases check that widening the accepted set did not turn into accepting everything: ADPCM is still rejected with the unsupported-encoding message, and a FLAC is still identified as a FLAC.

Verification

Run by execution, not by inspection:

  • The new cases fail against the current reader with the old message, and pass against this one — so they are not vacuous
  • wav_reader_chunk_bounds_test still passes; the allocation bound from fix: bound allocations sized by attacker-controlled file and header fields #143 is untouched
  • Full ctest green (59/59) on Linux/CUDA
  • End to end: a hand-built 0xFFFE-tagged PCM16 file, which the current reader rejects, works as an --voice-ref through audiocpp_cli

…ENSIBLE

The reader handled PCM16, PCM24 and float32. Everything else -- including
ordinary PCM16 that happens to be tagged 0xFFFE -- came back as "unsupported WAV
encoding (need PCM16, PCM24, or float32)", which is confusing when PCM16 is
exactly what is inside. Encoders emit WAVEFORMATEXTENSIBLE routinely for more
than two channels or whenever a channel mask is set, and the real format tag
then lives in the SubFormat GUID rather than in wFormatTag.

Adds PCM8 (unsigned, biased by 128), PCM32, float64, G.711 A-law and mu-law, and
unwraps WAVEFORMATEXTENSIBLE to whatever its GUID names. Files that still cannot
be decoded now say what they are: a FLAC, Ogg, MP3, MP4, AIFF, RF64 or CAF given
to the reader is named as such instead of failing with "invalid WAV RIFF header".

Tests cover each added format. The G.711 cases are pinned to the published
decode values (mu-law 0x00 -> -32124, A-law 0x2A -> +32256, and A-law's absence
of an exact zero) rather than to a re-derivation of the same bit manipulation,
which would prove nothing. Two negative cases check that widening the accepted
set did not turn into accepting everything: ADPCM is still rejected, and a FLAC
is still identified as a FLAC.

Verified by execution: the new cases fail against the previous reader with the
old message and pass against this one; wav_reader_chunk_bounds_test still
passes.

This lands separately from 0xShug0#180 at @0xShug0's request. The decoders originate
from @dignome's contributed tree, where they existed so the CLI and server path
would accept the same files the web UI already did.
Four defects found by review before merge.

**A-law polarity was inverted on all 256 codes.** G.711 sets the sign bit for
*positive* samples in A-law and for *negative* ones in mu-law; this treated both
the same way. Decoded audio came out at the right amplitude, phase-inverted --
which is inaudible on its own and survives every spot check.

The tests did not catch it because they were circular: four "published" anchors
whose expected values had been worked out from the same shift-and-bias
arithmetic under test, so they confirmed the bug rather than finding it. They are
replaced with the full 256-entry decode tables for both codings, taken from
outside this codebase -- ffmpeg 9.0.1 decoding a 256-byte file, cross-checked
against the values implied by the ITU-T G.711 segment definitions. Reinstating
the old sign now fails on `A-law code 0`.

**The extensible SubFormat GUID was trusted on its first two bytes.** Only those
carry the format tag; the remaining fourteen are a fixed suffix shared by every
KSDATAFORMAT_SUBTYPE_*. Without checking them, an unrelated codec whose GUID
merely starts 0x0001 decoded as PCM16. Now compared, and cbSize is required to
be at least 22 as the structure demands. A sub-40-byte extensible fmt chunk gets
a clear error instead of falling through with a stale format tag.

**PCM32 and float64 silently dropped a trailing partial sample.** Integer
division trimmed it, so a truncated download decoded as valid audio. Both now
reject it, matching what PCM24 already did. PCM16 and float32 keep their existing
behaviour -- tightening those is not this PR's business.

Verified: full ctest green, an A-law file transcoded by ffmpeg works as a CLI
--voice-ref end to end, and each new negative case fails against the code as it
stood before this commit.
…d files

Two defects a second reviewer found in the parse loop, both introduced by the
first commit in this PR, and both invisible to tests that only ever read from a
file path.

**The 12-byte header sniff rewound absolutely.** After reading the RIFF header
for container identification it did `seekg(header_read, beg)`, which is
redundant for a stream that started at offset 0, wrong for one that did not, and
impossible for one that cannot seek. `read_wav_f32(std::istream &)` is public;
handed a pipe it set failbit and reported `incomplete WAV file` for a perfectly
good WAV. The bytes are already consumed, so the rewind is simply removed.

**The RIFF pad byte was required at EOF.** After an odd-sized chunk the reader
always seeks one byte. Seeking past the end is legal on an ifstream but not on
the in-memory buffer behind the string_view overload, so the same bytes parsed
from disk and threw from an upload. Plenty of writers omit the final pad, and
this PR is what makes it matter: PCM8, A-law and mu-law are one byte per sample,
so odd data chunks go from rare to routine. A pad byte carries no data, so a
missing one at EOF now ends the chunk loop instead of failing.

Also rejects a `fmt ` chunk shorter than 16 bytes, which previously read on into
whatever followed.

The PCM8, PCM32 and float64 expectations were still derived from the
implementation, the same construction that hid the A-law inversion. They are now
frozen from `ffmpeg -f f32le` output and extended to the endpoints that
distinguish a correct conversion from a plausible one: INT32_MAX, which float32
rounding maps to exactly 1.0, and a float64 value not representable in float32.

Verified: reinstating any of the three defects fails the suite with the matching
message; full ctest green; and our decode of a real ffmpeg-transcoded A-law file
matches ffmpeg on all 153280 samples with zero mismatches.
@5uck1ess

Copy link
Copy Markdown
Contributor Author

Pushed two rounds of fixes after review. Six defects, all in code this PR added — worth writing up since one of them was invisible by construction.

A-law was sign-inverted on all 256 codes. G.711 sets the sign bit for positive samples in A-law and negative in mu-law; I treated both the same way. Decoded audio came out at the correct amplitude, phase-inverted — inaudible on its own.

It survived review because my tests were circular. I'd written that the expected values were "pinned to the published G.711 values" and that re-deriving the same bit manipulation "would prove nothing" — then derived the four anchors from exactly that arithmetic. They confirmed the bug instead of catching it.

Both codings are now checked against full 256-entry tables produced by ffmpeg 9.0.1 and cross-checked against the ITU-T G.711 segment definitions. Reinstating the old sign fails on A-law code 0.

The extensible SubFormat GUID was trusted on its first two bytes. Only those carry the format tag; the other fourteen are a fixed suffix shared by every KSDATAFORMAT_SUBTYPE_*. Any codec whose GUID merely started 0x0001 decoded as PCM16. Now compared, with cbSize >= 22 enforced.

PCM32 and float64 silently dropped a trailing partial sample, so a truncated download decoded as valid audio. Both reject it now, matching PCM24. PCM16 and float32 keep their existing behaviour — tightening those isn't this PR's business.

The 12-byte header sniff rewound absolutely. read_wav_f32(std::istream &) is public, and given a pipe or a stream not starting at offset 0 it failed with incomplete WAV file on a valid WAV. The bytes are already consumed, so the rewind is gone.

The RIFF pad byte was required at EOF. Seeking past the end is legal on an ifstream but not on the buffer behind the string_view overload — so the same bytes parsed from disk and threw from an upload. This PR is what makes it bite: PCM8, A-law and mu-law are one byte per sample, so odd data chunks go from rare to routine.

A fmt chunk under 16 bytes read on into whatever followed. Now rejected.

The PCM8/PCM32/float64 expectations were still derived from the implementation — same construction that hid the A-law bug — so they're frozen from ffmpeg -f f32le and extended to the endpoints that separate a correct conversion from a plausible one: INT32_MAX, which float32 rounding maps to exactly 1.0, and a float64 value not representable in float32.

Verification. Every fix has a test that fails against the code as it stood before it. Full ctest green (59/59). Our decode of a real ffmpeg-transcoded A-law file matches ffmpeg on all 153280 samples, zero mismatches.

Reviewed with two models from different vendors; the second found the two parse-loop bugs the first missed, so a third pair of eyes on the chunk iteration wouldn't be wasted.

@0xShug0
0xShug0 merged commit af8cb75 into 0xShug0:main Aug 27, 2026
6 checks passed
@0xShug0

0xShug0 commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Thanks @5uck1ess! PR merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants