fix(engine): read extensible PCM and float WAV headers - #4267
Conversation
jrusso1020
left a comment
There was a problem hiding this comment.
Approved.
I rebuilt the failure rather than taking the captured fixtures on trust: generated my own WAVs with the installed FFmpeg (n8.0.1), transcribed both the old and the new format-tag resolution, and ran every file through both readers.
The trigger is real, and broader than "four channels"
FFmpeg emits WAVE_FORMAT_EXTENSIBLE whenever channels > 2 or the sample width is not 8/16 — a mono 24-bit file comes out extensible:
| input | fmt tag | fmt size | bits |
|---|---|---|---|
stereo pcm_s16le |
0x0001 |
16 | 16 |
stereo pcm_f32le |
0x0003 |
18 | 32 |
4ch pcm_s16le |
0xfffe |
40 | 16 |
4ch pcm_f32le |
0xfffe |
40 | 32 |
mono pcm_s24le |
0xfffe |
40 | 24 |
Old vs. new over those same files (FX = readWav, ENV = applyVolumeEnvelopeToWav):
file FX old FX new ENV old ENV new
st_s16.wav ok 2ch float=false ok 2ch float=false applies applies
st_f32.wav ok 2ch float=true ok 2ch float=true applies applies
q_s16.wav THROW Unsupported… ok 4ch float=false SKIPPED applies
q_f32.wav THROW Unsupported… ok 4ch float=true SKIPPED applies
mono_s24.wav THROW Unsupported… THROW Unsupported… SKIPPED SKIPPED
st_s24.wav THROW Unsupported… THROW Unsupported… SKIPPED SKIPPED
Three things I wanted out of that table:
- The fix discriminates. Rows 3-4 throw / silently skip the envelope on the code this replaces, so the new regression tests go red on the implementation they guard — they aren't inert.
- Nothing that already worked moved. Rows 1-2 are byte-identical before and after. The change is strictly widening:
wavFormatTagcan only returnnullwhere the old inline read returned0xfffe(already rejected downstream) or where the chunk is under 16 bytes (old:RangeError; new: a typed error, same outcome). - "Sample-width limits are unchanged" holds where it was most likely to have broken. 24-bit extensible now resolves its GUID to PCM(1) and is then rejected on
bits !== 16— a different rejection path from before, identical outcome. That is the case I'd have expected a GUID-resolving patch to let through, and it doesn't.
Both GUIDs are byte-exact against the Microsoft definitions (…-0000-0010-8000-00AA00389B71, sub-type 1 and 3) with the little-endian Data1/Data2/Data3 layout correct — worth saying explicitly, since a byte-swapped GUID would still pass every test in this PR that only round-trips the same constant.
Gain and header preservation
Applied a 0.5 envelope to all four extensible files: every channel scaled exactly, data at offset 102 (s16) / 114 (f32), and the header bytes and total file length byte-identical afterwards. Your captured fixtures reproduce exactly what the test asserts — 5 frames of [0.5, -0.5, 0.25, -0.25] → [0.25, -0.25, 0.125, -0.125].
The negative tests are real ones, not shape-checks: the unknown-GUID case mutates the last byte, so it proves the full 16 bytes are compared rather than a prefix; and the three cbSize cases cover both sides of the < 22 and 18 + extraSize > length bounds.
Checks that came back clean
- Every site of the contract change. Exactly two WAV readers exist repo-wide (
audioFxRender,audioVolumeEnvelope); both now route through the one helper. The remaining RIFF hits are a writer, a producer fixture generator, and a font-signature check — no third reader left on the old inline read. - PR-body claims. "30 added, 18 removed across three source files" is exact (+4/-2, +4/-4, +22/-12). The new fixture test is genuinely always-running — no
skipIf— and no dependency was added. - The modified FFmpeg test is not a weakening. It swaps an encoder implementation detail (raw tag at byte 20 == 3) for a behavioural assertion (
readWavdecodes it as float); the load-bearing part — walking todataand checking the fade landed — is untouched. On this build stereo f32 is still plain tag 3, so the old assertion would have passed; the new one survives a build that chooses extensible. - Stale-after-merge.
mainadvanced one commit since your base (#4264) and touched none of the three source files, so nothing here goes stale on merge.
Non-blocking — land as-is or follow up
readWavChunksthrows"Invalid or unsupported WAV format header"with no file path, while every sibling error in that reader names it (Not a WAV file: ${path},WAV has no data chunk: ${path},Unsupported WAV format ${format}/${bits}-bit: ${path}). In a render processing many tracks, that's the one failure you can't attribute to a file.writeWavalways emits a canonical 16-bytefmt, so an FX-processed 4-channel extensible input comes back out non-extensible with the channel mask dropped —ffprobereportschannel_layout=unknown. I checked the consequence rather than assuming it: FFmpeg still decodes it and re-encodes to AAC without complaint, so this is a note, not a defect. It's only worth flagging because multichannel input was rejected outright before this PR, so the path is newly reachable.wavChunks.test.tshas a positive case for the float GUID only. The PCM GUID's positive path is covered end-to-end in the envelope fixture test, so it isn't a hole — just asymmetric at the unit level.
— Rames
FFmpeg can write PCM and IEEE-float audio inside a WAVE_FORMAT_EXTENSIBLE (
0xFFFE) header. The FX reader rejected these files and the volume-envelope reader declined to apply gain. Both now resolve the complete sub-format GUID through one shared helper to the existing PCM/float codecs. Truncated extensions and unknown GUIDs remain rejected; sample-width limits are unchanged.Reader change: 30 added lines, 18 removed across three source files. No encoder pinning, baseline changes, or added skips.
Validation:
This fixes the WAV failures exposed by #4260 and should land before it. #4260 will rebase after this merges.