diff --git a/scripts/verify-audio-asset-data.mjs b/scripts/verify-audio-asset-data.mjs index edc05cd..cb8e28e 100644 --- a/scripts/verify-audio-asset-data.mjs +++ b/scripts/verify-audio-asset-data.mjs @@ -195,6 +195,28 @@ function validateAudioFile(errors, context, path) { if (path.toLowerCase().endsWith('.wav')) { if (bytes.subarray(0, 4).toString('ascii') !== 'RIFF' || bytes.subarray(8, 12).toString('ascii') !== 'WAVE') { errors.push(`${context} at "${path}" does not have a WAV header`); + return; + } + + const metadata = readWavMetadata(bytes); + if (!metadata) { + errors.push(`${context} at "${path}" is missing readable WAV fmt/data chunks`); + return; + } + if (metadata.audioFormat !== 1) { + errors.push(`${context} at "${path}" should be PCM WAV audio, got format ${metadata.audioFormat}`); + } + if (metadata.channels < 1 || metadata.channels > 2) { + errors.push(`${context} at "${path}" should be mono or stereo WAV audio, got ${metadata.channels} channels`); + } + if (metadata.sampleRate < 22050) { + errors.push(`${context} at "${path}" should use at least 22050 Hz WAV audio, got ${metadata.sampleRate} Hz`); + } + if (![16, 24, 32].includes(metadata.bitsPerSample)) { + errors.push(`${context} at "${path}" should use 16/24/32-bit WAV audio, got ${metadata.bitsPerSample}-bit`); + } + if (metadata.dataSize <= 0) { + errors.push(`${context} at "${path}" does not contain WAV sample data`); } return; } @@ -211,6 +233,39 @@ function validateAudioFile(errors, context, path) { errors.push(`${context} at "${path}" uses an unsupported audio extension`); } +function readWavMetadata(bytes) { + let offset = 12; + let format; + let dataSize = 0; + + while (offset + 8 <= bytes.length) { + const chunkType = bytes.subarray(offset, offset + 4).toString('ascii'); + const chunkSize = bytes.readUInt32LE(offset + 4); + const dataOffset = offset + 8; + if (dataOffset + chunkSize > bytes.length) { + return undefined; + } + + if (chunkType === 'fmt ' && chunkSize >= 16) { + format = { + audioFormat: bytes.readUInt16LE(dataOffset), + channels: bytes.readUInt16LE(dataOffset + 2), + sampleRate: bytes.readUInt32LE(dataOffset + 4), + bitsPerSample: bytes.readUInt16LE(dataOffset + 14) + }; + } else if (chunkType === 'data') { + dataSize += chunkSize; + } + + offset = dataOffset + chunkSize + (chunkSize % 2); + } + + if (!format || dataSize <= 0) { + return undefined; + } + return { ...format, dataSize }; +} + function lineForIndex(source, index) { return source.slice(0, index).split(/\r?\n/).length; }