// Minimal WAV (RIFF/WAVE) reader + writer for the audio-validation tooling: dump a // captured stream to disk so it can be *listened to*, and read one back to analyze. // Supports the two formats the mirror carries -- 16-bit PCM (tag 1) and 32-bit float // (tag 3) -- interleaved, any channel count / sample rate. Header-only, no deps beyond // the C++ standard library, so the tool and a unit test share it. Not a general WAV // library: it reads/writes the canonical 44-byte-header layout these tools produce. #pragma once #include #include #include #include #include namespace coop { struct WavData { std::uint32_t sample_rate = 0; std::uint32_t channels = 0; std::uint32_t bits = 0; std::uint32_t format_tag = 0; // 1 = PCM, 3 = IEEE float std::vector pcm; // interleaved frames }; namespace detail { inline void wav_put_u32(std::vector& b, std::uint32_t v) { b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF); b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF); } inline void wav_put_u16(std::vector& b, std::uint16_t v) { b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF); } inline std::uint32_t wav_get_u32(const std::uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (static_cast(p[3]) << 24); } inline std::uint16_t wav_get_u16(const std::uint8_t* p) { return static_cast(p[0] | (p[1] << 8)); } } // namespace detail // Write interleaved PCM to a WAV file. Returns false on an I/O error. inline bool wav_write(const std::wstring& path, const void* pcm, std::size_t bytes, std::uint32_t sample_rate, std::uint32_t channels, std::uint32_t bits, std::uint32_t format_tag) { const std::uint32_t block_align = channels * (bits / 8); const std::uint32_t byte_rate = sample_rate * block_align; std::vector hdr; hdr.reserve(44); const char* riff = "RIFF"; hdr.insert(hdr.end(), riff, riff + 4); detail::wav_put_u32(hdr, 36 + static_cast(bytes)); // file size - 8 const char* wave = "WAVE"; hdr.insert(hdr.end(), wave, wave + 4); const char* fmt = "fmt "; hdr.insert(hdr.end(), fmt, fmt + 4); detail::wav_put_u32(hdr, 16); // PCM fmt chunk size detail::wav_put_u16(hdr, static_cast(format_tag)); detail::wav_put_u16(hdr, static_cast(channels)); detail::wav_put_u32(hdr, sample_rate); detail::wav_put_u32(hdr, byte_rate); detail::wav_put_u16(hdr, static_cast(block_align)); detail::wav_put_u16(hdr, static_cast(bits)); const char* data = "data"; hdr.insert(hdr.end(), data, data + 4); detail::wav_put_u32(hdr, static_cast(bytes)); FILE* f = nullptr; if (_wfopen_s(&f, path.c_str(), L"wb") != 0 || f == nullptr) { return false; } const bool ok = std::fwrite(hdr.data(), 1, hdr.size(), f) == hdr.size() && (bytes == 0 || std::fwrite(pcm, 1, bytes, f) == bytes); std::fclose(f); return ok; } // Read a WAV file (PCM/float, canonical layout). Returns false if it can't be parsed. inline bool wav_read(const std::wstring& path, WavData& out) { FILE* f = nullptr; if (_wfopen_s(&f, path.c_str(), L"rb") != 0 || f == nullptr) { return false; } std::fseek(f, 0, SEEK_END); const long size = std::ftell(f); std::fseek(f, 0, SEEK_SET); if (size < 44) { std::fclose(f); return false; } std::vector all(static_cast(size)); const bool read_ok = std::fread(all.data(), 1, all.size(), f) == all.size(); std::fclose(f); if (!read_ok || std::memcmp(all.data(), "RIFF", 4) != 0 || std::memcmp(all.data() + 8, "WAVE", 4) != 0) { return false; } // Walk chunks for "fmt " and "data". std::size_t pos = 12; bool have_fmt = false, have_data = false; while (pos + 8 <= all.size()) { const std::uint8_t* p = all.data() + pos; const std::uint32_t chunk_size = detail::wav_get_u32(p + 4); const std::size_t body = pos + 8; if (std::memcmp(p, "fmt ", 4) == 0 && body + 16 <= all.size()) { out.format_tag = detail::wav_get_u16(all.data() + body + 0); out.channels = detail::wav_get_u16(all.data() + body + 2); out.sample_rate = detail::wav_get_u32(all.data() + body + 4); out.bits = detail::wav_get_u16(all.data() + body + 14); have_fmt = true; } else if (std::memcmp(p, "data", 4) == 0) { const std::size_t avail = all.size() - body; const std::size_t n = std::min(chunk_size, avail); out.pcm.assign(all.begin() + body, all.begin() + body + n); have_data = true; } pos = body + chunk_size + (chunk_size & 1); // chunks are word-aligned } return have_fmt && have_data; } } // namespace coop