From e95805bce43d7212449a9805fe5a9d73b573fc28 Mon Sep 17 00:00:00 2001 From: mo Date: Sun, 18 Oct 2026 19:58:11 +0100 Subject: [PATCH] fix: correct WAV RIFF length and padding (#1032) * fix: correct WAV RIFF length and padding Include the PCM payload in the RIFF chunk size and pad odd-length data chunks. Add regression coverage for empty, odd, even and larger payloads. * chore: add comment explaning the wav padding byte --------- Co-authored-by: Jan Laupetin --- src/ObjWriting/Sound/WavWriter.cpp | 9 ++++- test/ObjWritingTests/Sound/WavWriterTest.cpp | 40 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 test/ObjWritingTests/Sound/WavWriterTest.cpp diff --git a/src/ObjWriting/Sound/WavWriter.cpp b/src/ObjWriting/Sound/WavWriter.cpp index 2d8bfe84..f788ad49 100644 --- a/src/ObjWriting/Sound/WavWriter.cpp +++ b/src/ObjWriting/Sound/WavWriter.cpp @@ -7,8 +7,8 @@ WavWriter::WavWriter(std::ostream& stream) void WavWriter::WritePcmHeader(const WavMetaData& metaData, const size_t dataLen) const { - constexpr auto riffMasterChunkSize = static_cast(sizeof(WAV_CHUNK_ID_RIFF) + sizeof(uint32_t) + sizeof(WAV_WAVE_ID) + sizeof(WavChunkHeader) - + sizeof(WavFormatChunkPcm) + sizeof(WavChunkHeader) + sizeof(uint32_t)); + const auto riffMasterChunkSize = + static_cast(sizeof(WAV_WAVE_ID) + sizeof(WavChunkHeader) + sizeof(WavFormatChunkPcm) + sizeof(WavChunkHeader) + dataLen + (dataLen & 1u)); m_stream.write(reinterpret_cast(&WAV_CHUNK_ID_RIFF), sizeof(WAV_CHUNK_ID_RIFF)); m_stream.write(reinterpret_cast(&riffMasterChunkSize), sizeof(riffMasterChunkSize)); @@ -34,4 +34,9 @@ void WavWriter::WritePcmHeader(const WavMetaData& metaData, const size_t dataLen void WavWriter::WritePcmData(const void* data, const size_t dataLen) const { m_stream.write(static_cast(data), static_cast(dataLen)); + + // > The data is always padded to the nearest WORD boundary. + // https://learn.microsoft.com/en-us/windows/win32/xaudio2/resource-interchange-file-format--riff-#chunks + if (dataLen & 1u) + m_stream.put('\0'); } diff --git a/test/ObjWritingTests/Sound/WavWriterTest.cpp b/test/ObjWritingTests/Sound/WavWriterTest.cpp new file mode 100644 index 00000000..5b1ed62e --- /dev/null +++ b/test/ObjWritingTests/Sound/WavWriterTest.cpp @@ -0,0 +1,40 @@ +#include "Sound/WavWriter.h" + +#include +#include +#include +#include +#include + +TEST_CASE("WavWriter: RIFF contains the complete PCM payload and alignment padding", "[sound][wav]") +{ + const auto size = GENERATE(0uz, 1uz, 2uz, 31uz, 2048uz); + const std::string samples(size, static_cast(0x81)); + std::ostringstream output(std::ios::binary); + const WavWriter writer(output); + writer.WritePcmHeader({1, 22050, 8}, samples.size()); + writer.WritePcmData(samples.data(), samples.size()); + const auto bytes = output.str(); + REQUIRE(bytes.size() >= sizeof(WavHeader)); + WavHeader header{}; + std::memcpy(&header, bytes.data(), sizeof(header)); + CHECK(header.chunkIdRiff == WAV_CHUNK_ID_RIFF); + CHECK(header.chunkIdSize == bytes.size() - 8u); + CHECK(header.format == WAV_WAVE_ID); + CHECK(header.chunkHeader.chunkID == WAV_CHUNK_ID_FMT); + CHECK(header.chunkHeader.chunkSize == sizeof(WavFormatChunkPcm)); + CHECK(header.formatChunk.wFormatTag == WavFormat::PCM); + CHECK(header.formatChunk.nChannels == 1u); + CHECK(header.formatChunk.nSamplesPerSec == 22050u); + CHECK(header.formatChunk.nAvgBytesPerSec == 22050u); + CHECK(header.formatChunk.nBlockAlign == 1u); + CHECK(header.formatChunk.wBitsPerSample == 8u); + CHECK(header.subChunkHeader.chunkID == WAV_CHUNK_ID_DATA); + REQUIRE(header.subChunkHeader.chunkSize == samples.size()); + CHECK(bytes.substr(sizeof(header), samples.size()) == samples); + CHECK(bytes.size() % 2u == 0u); + if (samples.size() & 1u) + CHECK(bytes.back() == '\0'); + else + CHECK(bytes.size() == sizeof(header) + samples.size()); +}