Skip to content

decodeWav accepts a WAV with no fmt chunk and decodes it against silent defaults #66

Description

@BootBlock

Confirmed defect

decodeWav accepts a RIFF/WAVE stream that contains no fmt chunk at all and decodes it against silent defaults, returning plausible-looking audio instead of rejecting the file.

The format fields are initialised to defaults and only overwritten if a fmt chunk happens to be found — src/core/audio/wav.ts:110-133:

let audioFormat = 1;
let numChannels = 1;
let sampleRate = 48_000;
let bitsPerSample = 16;
let dataOffset = -1;
...
if (chunkId === 'fmt ') { audioFormat = ...; numChannels = ...; sampleRate = ...; bitsPerSample = ...; }
else if (chunkId === 'data') { ... }

A missing data chunk is rejected (wav.ts:134), but a missing fmt chunk is not. There is no sawFmt flag.

Verified empirically. Taking a valid mono 16-bit/48 kHz file from encodeWav and renaming only its fmt chunk id to XXXX:

decodeWav -> RETURNED ch=1 sr=48000 bd=16 frames=4

No throw. The caller receives a DecodedWav it cannot distinguish from a good one.

Consequence. decodeWav is on the untrusted-input path: readSampleChannels in sampleEditService.ts:29 decodes whatever .wav bytes are in OPFS, and the §9.6 .mpcweb import decodes samples/<id>.wav straight out of a user-supplied zip. A file whose real format is, say, stereo 24-bit at 44.1 kHz but whose fmt chunk is absent or misspelled decodes as mono 16-bit at 48 kHz — the sample plays as noise at the wrong rate and length, and the wrong channels/sample_rate are written into the samples row (§9.3), so the corruption persists. Nothing surfaces an error, so this presents to the user as "my sample imported but sounds broken", which is far harder to diagnose than a rejection.

Suggested fix: track whether a fmt chunk was seen and throw the same way the missing-data case already does.

The coverage gap this sits in

§13.2 mandates TDD and §11.1 names the WAV encoder as mandatory-coverage logic, but wav.test.ts has 11 tests and exactly one malformed-input case'rejects bytes that are not a RIFF/WAVE stream' (line 105). Everything else is golden-bytes encoding or encode→decode round-trips, i.e. only ever feeding the decoder bytes the encoder just produced. A decoder is only round-tripped against its own encoder, so no test ever hands it a file it did not write.

That is exactly how #17 (numChannels = 0 hangs the tab) got in, and this issue is the same blind spot with a different trigger.

Untested cases in decodeWav, each verified by hand against the current code:

Input Actual behaviour Verdict
No fmt chunk silently decodes with defaults defect (above)
bitsPerSample = 0 RangeError: Invalid typed array length: Infinity untyped internal error, not a friendly rejection
numChannels = 0 infinite loop already #17
data chunkSize ≫ buffer (0xFFFFFFF0) clamped by Math.min, decodes 4 frames correct — no change needed
Zero-length data chunk 0 frames, no crash correct
Truncated fmt chunk (body runs past the buffer) DataView throws RangeError untyped, but safe

And in encodeWav:

Input Actual behaviour Verdict
Zero-length channel 44-byte header-only file, round-trips to 0 frames correct
Ragged channel lengths — [[0.5,0.5,0.5],[0.5]] silently zero-pads the short channel via ?? 0 (wav.ts:69), emitting 3 stereo frames with R = [0.5, 0, 0] fabricates samples rather than rejecting; frame count is taken from channels[0] alone (wav.ts:45)

The ragged-channel case is reachable: sampleEditService.ts builds channel arrays with channel.slice(startFrame, endFrame) per channel, so any future path where per-channel bounds diverge would silently pad rather than fail.

Asked for here: the missing-fmt fix, plus malformed-header tests covering the rows above so the decoder is tested against files it did not itself write. Whether bitsPerSample = 0 and ragged channels should throw typed errors or be tolerated is a judgement call worth settling in this issue — the rows marked correct need no change and are recorded so they are not re-investigated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsamplerSample import, editing, chopping, and the Looper (spec 8.5.4/8.5.8)testingUnit, offline-render, and real-browser verification (spec 11)

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions