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.
Confirmed defect
decodeWavaccepts a RIFF/WAVE stream that contains nofmtchunk 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
fmtchunk happens to be found —src/core/audio/wav.ts:110-133:A missing
datachunk is rejected (wav.ts:134), but a missingfmtchunk is not. There is nosawFmtflag.Verified empirically. Taking a valid mono 16-bit/48 kHz file from
encodeWavand renaming only itsfmtchunk id toXXXX:No throw. The caller receives a
DecodedWavit cannot distinguish from a good one.Consequence.
decodeWavis on the untrusted-input path:readSampleChannelsinsampleEditService.ts:29decodes whatever.wavbytes are in OPFS, and the §9.6.mpcwebimport decodessamples/<id>.wavstraight out of a user-supplied zip. A file whose real format is, say, stereo 24-bit at 44.1 kHz but whosefmtchunk 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 wrongchannels/sample_rateare written into thesamplesrow (§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
fmtchunk was seen and throw the same way the missing-datacase 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.tshas 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 = 0hangs 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:fmtchunkbitsPerSample = 0RangeError: Invalid typed array length: InfinitynumChannels = 0datachunkSize ≫ buffer (0xFFFFFFF0)Math.min, decodes 4 framesdatachunkfmtchunk (body runs past the buffer)DataViewthrowsRangeErrorAnd in
encodeWav:[[0.5,0.5,0.5],[0.5]]?? 0(wav.ts:69), emitting 3 stereo frames with R =[0.5, 0, 0]channels[0]alone (wav.ts:45)The ragged-channel case is reachable:
sampleEditService.tsbuilds channel arrays withchannel.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-
fmtfix, plus malformed-header tests covering the rows above so the decoder is tested against files it did not itself write. WhetherbitsPerSample = 0and 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.