Skip to content

fix(engine): read extensible PCM and float WAV headers - #4267

Merged
miguel-heygen merged 1 commit into
mainfrom
hfoss39/fix-wav-extensible
Sep 22, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
hfoss39/fix-wav-extensible

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

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:

  • Captured complete four-channel PCM and float WAV files from FFmpeg 8.1.1. Always-running regression tests decode their samples through the FX reader and apply gain through the envelope reader, preserving the header.
  • Added rejection tests for unknown GUIDs, chunk-boundary truncation, physical truncation, and invalid extension lengths.
  • The existing installed-FFmpeg test now asserts decoded IEEE-float audio rather than requiring the outer header tag to be 3.
  • Formatting and whitespace checks pass. Local lint/test execution is deferred to CI. CI results pending.

This fixes the WAV failures exposed by #4260 and should land before it. #4260 will rebase after this merges.

@miguel-heygen
miguel-heygen marked this pull request as ready for review September 22, 2026 03:45

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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.
  2. Nothing that already worked moved. Rows 1-2 are byte-identical before and after. The change is strictly widening: wavFormatTag can only return null where the old inline read returned 0xfffe (already rejected downstream) or where the chunk is under 16 bytes (old: RangeError; new: a typed error, same outcome).
  3. "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 (readWav decodes it as float); the load-bearing part — walking to data and 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. main advanced 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

  1. readWavChunks throws "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.
  2. writeWav always emits a canonical 16-byte fmt , so an FX-processed 4-channel extensible input comes back out non-extensible with the channel mask dropped — ffprobe reports channel_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.
  3. wavChunks.test.ts has 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

@miguel-heygen
miguel-heygen merged commit c27bdda into main Sep 22, 2026
59 checks passed
@miguel-heygen
miguel-heygen deleted the hfoss39/fix-wav-extensible branch September 22, 2026 04:15
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