Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
533 lines
26 KiB
C++
533 lines
26 KiB
C++
#include "engine/framework/audio/wav_reader.h"
|
|
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
void require(bool condition, const std::string & message) {
|
|
if (!condition) {
|
|
throw std::runtime_error(message);
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
void write_le(std::ofstream & output, T value) {
|
|
output.write(reinterpret_cast<const char *>(&value), sizeof(T));
|
|
if (!output) {
|
|
throw std::runtime_error("failed to write test WAV");
|
|
}
|
|
}
|
|
|
|
void write_bytes(std::ofstream & output, const char * bytes, std::streamsize count) {
|
|
output.write(bytes, count);
|
|
if (!output) {
|
|
throw std::runtime_error("failed to write test WAV");
|
|
}
|
|
}
|
|
|
|
void write_pcm24_sample(std::ofstream & output, int32_t value) {
|
|
const uint32_t bits = static_cast<uint32_t>(value) & 0x00FFFFFFu;
|
|
const char bytes[3] = {
|
|
static_cast<char>(bits & 0xFFu),
|
|
static_cast<char>((bits >> 8) & 0xFFu),
|
|
static_cast<char>((bits >> 16) & 0xFFu),
|
|
};
|
|
write_bytes(output, bytes, 3);
|
|
}
|
|
|
|
void write_pcm24_wav(
|
|
const std::filesystem::path & path,
|
|
int sample_rate,
|
|
int channels,
|
|
const std::vector<int32_t> & samples) {
|
|
const uint32_t data_bytes = static_cast<uint32_t>(samples.size() * 3);
|
|
const uint16_t block_align = static_cast<uint16_t>(channels * 3);
|
|
const uint32_t byte_rate = static_cast<uint32_t>(sample_rate * block_align);
|
|
|
|
std::ofstream output(path, std::ios::binary);
|
|
if (!output) {
|
|
throw std::runtime_error("failed to open test WAV: " + path.string());
|
|
}
|
|
write_bytes(output, "RIFF", 4);
|
|
write_le<uint32_t>(output, 36u + data_bytes);
|
|
write_bytes(output, "WAVE", 4);
|
|
write_bytes(output, "fmt ", 4);
|
|
write_le<uint32_t>(output, 16u);
|
|
write_le<uint16_t>(output, 1u);
|
|
write_le<uint16_t>(output, static_cast<uint16_t>(channels));
|
|
write_le<uint32_t>(output, static_cast<uint32_t>(sample_rate));
|
|
write_le<uint32_t>(output, byte_rate);
|
|
write_le<uint16_t>(output, block_align);
|
|
write_le<uint16_t>(output, 24u);
|
|
write_bytes(output, "data", 4);
|
|
write_le<uint32_t>(output, data_bytes);
|
|
for (const int32_t sample : samples) {
|
|
write_pcm24_sample(output, sample);
|
|
}
|
|
}
|
|
|
|
// G.711 decode tables, all 256 codes each. Ground truth from outside this
|
|
// codebase: ffmpeg 9.0.1 decoding a 256-byte file of every code, matching the
|
|
// values implied by the ITU-T G.711 segment definitions.
|
|
//
|
|
// Every expected value in this file must come from an external decoder, never
|
|
// from the arithmetic in wav_reader.cpp. The two codings do not share a sign
|
|
// convention -- mu-law sets the top bit for negative samples, A-law for
|
|
// positive ones -- and a table derived from the implementation cannot see that.
|
|
constexpr int16_t kALawExpected[256] = {
|
|
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736,
|
|
-7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784,
|
|
-2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368,
|
|
-3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392,
|
|
-22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944,
|
|
-30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136,
|
|
-11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472,
|
|
-15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568,
|
|
-344, -328, -376, -360, -280, -264, -312, -296,
|
|
-472, -456, -504, -488, -408, -392, -440, -424,
|
|
-88, -72, -120, -104, -24, -8, -56, -40,
|
|
-216, -200, -248, -232, -152, -136, -184, -168,
|
|
-1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184,
|
|
-1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696,
|
|
-688, -656, -752, -720, -560, -528, -624, -592,
|
|
-944, -912, -1008, -976, -816, -784, -880, -848,
|
|
5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736,
|
|
7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784,
|
|
2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368,
|
|
3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392,
|
|
22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944,
|
|
30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136,
|
|
11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472,
|
|
15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568,
|
|
344, 328, 376, 360, 280, 264, 312, 296,
|
|
472, 456, 504, 488, 408, 392, 440, 424,
|
|
88, 72, 120, 104, 24, 8, 56, 40,
|
|
216, 200, 248, 232, 152, 136, 184, 168,
|
|
1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184,
|
|
1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696,
|
|
688, 656, 752, 720, 560, 528, 624, 592,
|
|
944, 912, 1008, 976, 816, 784, 880, 848,
|
|
};
|
|
|
|
constexpr int16_t kMuLawExpected[256] = {
|
|
-32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956,
|
|
-23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764,
|
|
-15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412,
|
|
-11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316,
|
|
-7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,
|
|
-5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,
|
|
-3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,
|
|
-2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,
|
|
-1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,
|
|
-1372, -1308, -1244, -1180, -1116, -1052, -988, -924,
|
|
-876, -844, -812, -780, -748, -716, -684, -652,
|
|
-620, -588, -556, -524, -492, -460, -428, -396,
|
|
-372, -356, -340, -324, -308, -292, -276, -260,
|
|
-244, -228, -212, -196, -180, -164, -148, -132,
|
|
-120, -112, -104, -96, -88, -80, -72, -64,
|
|
-56, -48, -40, -32, -24, -16, -8, 0,
|
|
32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,
|
|
23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,
|
|
15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,
|
|
11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316,
|
|
7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140,
|
|
5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092,
|
|
3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004,
|
|
2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980,
|
|
1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436,
|
|
1372, 1308, 1244, 1180, 1116, 1052, 988, 924,
|
|
876, 844, 812, 780, 748, 716, 684, 652,
|
|
620, 588, 556, 524, 492, 460, 428, 396,
|
|
372, 356, 340, 324, 308, 292, 276, 260,
|
|
244, 228, 212, 196, 180, 164, 148, 132,
|
|
120, 112, 104, 96, 88, 80, 72, 64,
|
|
56, 48, 40, 32, 24, 16, 8, 0,
|
|
};
|
|
|
|
void require_near(float actual, float expected, const std::string & label) {
|
|
if (std::fabs(actual - expected) > 1.0e-7F) {
|
|
throw std::runtime_error(label + " mismatch");
|
|
}
|
|
}
|
|
|
|
// Writes a fmt chunk of `format_tag`/`bits` plus a data chunk of raw bytes. When
|
|
// `extensible` is set the chunk is the 40-byte WAVEFORMATEXTENSIBLE layout and
|
|
// `format_tag` moves into the SubFormat GUID, exactly as encoders emit it for
|
|
// multichannel or channel-masked PCM.
|
|
void write_wav(
|
|
const std::filesystem::path & path,
|
|
uint16_t format_tag,
|
|
uint16_t bits,
|
|
int sample_rate,
|
|
int channels,
|
|
const std::vector<char> & payload,
|
|
bool extensible = false,
|
|
bool valid_guid_tail = true,
|
|
uint16_t cb_size = 22) {
|
|
const uint16_t block_align = static_cast<uint16_t>(channels * ((bits + 7) / 8));
|
|
const uint32_t byte_rate = static_cast<uint32_t>(sample_rate) * block_align;
|
|
const uint32_t data_bytes = static_cast<uint32_t>(payload.size());
|
|
const uint32_t fmt_bytes = extensible ? 40u : 16u;
|
|
|
|
std::ofstream output(path, std::ios::binary);
|
|
if (!output) {
|
|
throw std::runtime_error("failed to open test WAV: " + path.string());
|
|
}
|
|
write_bytes(output, "RIFF", 4);
|
|
write_le<uint32_t>(output, 20u + fmt_bytes + data_bytes);
|
|
write_bytes(output, "WAVE", 4);
|
|
write_bytes(output, "fmt ", 4);
|
|
write_le<uint32_t>(output, fmt_bytes);
|
|
write_le<uint16_t>(output, extensible ? uint16_t{0xFFFE} : format_tag);
|
|
write_le<uint16_t>(output, static_cast<uint16_t>(channels));
|
|
write_le<uint32_t>(output, static_cast<uint32_t>(sample_rate));
|
|
write_le<uint32_t>(output, byte_rate);
|
|
write_le<uint16_t>(output, block_align);
|
|
write_le<uint16_t>(output, bits);
|
|
if (extensible) {
|
|
write_le<uint16_t>(output, cb_size); // cbSize
|
|
write_le<uint16_t>(output, bits); // wValidBitsPerSample
|
|
write_le<uint32_t>(output, uint32_t{0x3}); // dwChannelMask
|
|
write_le<uint16_t>(output, format_tag); // SubFormat GUID, first field
|
|
// Remainder of KSDATAFORMAT_SUBTYPE_*: 0000-0010-8000-00aa00389b71
|
|
const char valid_tail[14] = {
|
|
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, static_cast<char>(0x80),
|
|
0x00, 0x00, static_cast<char>(0xAA), 0x00, 0x38, static_cast<char>(0x9B),
|
|
0x71,
|
|
};
|
|
const char foreign_tail[14] = {
|
|
static_cast<char>(0xBE), static_cast<char>(0xEF), static_cast<char>(0xDE),
|
|
static_cast<char>(0xAD), 0x42, 0x41, 0x44, 0x47, 0x55, 0x49, 0x44, 0x21,
|
|
0x00, 0x00,
|
|
};
|
|
write_bytes(output, valid_guid_tail ? valid_tail : foreign_tail, 14);
|
|
}
|
|
write_bytes(output, "data", 4);
|
|
write_le<uint32_t>(output, data_bytes);
|
|
if (data_bytes > 0) {
|
|
write_bytes(output, payload.data(), static_cast<std::streamsize>(data_bytes));
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
std::vector<char> to_bytes(const std::vector<T> & values) {
|
|
std::vector<char> out(values.size() * sizeof(T));
|
|
if (!values.empty()) {
|
|
std::memcpy(out.data(), values.data(), out.size());
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void require_near(float actual, float expected, float tolerance, const std::string & label) {
|
|
if (!std::isfinite(actual)) {
|
|
throw std::runtime_error(label + " is not finite");
|
|
}
|
|
if (std::fabs(actual - expected) > tolerance) {
|
|
throw std::runtime_error(label + " mismatch");
|
|
}
|
|
}
|
|
|
|
// Runs `body` and requires it to throw with `needle` in the message. A silent
|
|
// success here would mean the reader accepted something it cannot decode.
|
|
void require_throws_containing(
|
|
const std::function<void()> & body, const std::string & needle, const std::string & label) {
|
|
try {
|
|
body();
|
|
} catch (const std::exception & ex) {
|
|
if (std::string(ex.what()).find(needle) == std::string::npos) {
|
|
throw std::runtime_error(label + ": wrong message: " + ex.what());
|
|
}
|
|
return;
|
|
}
|
|
throw std::runtime_error(label + ": expected a throw, got none");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
try {
|
|
const auto root = std::filesystem::temp_directory_path() / "audio_cpp_wav_reader_test";
|
|
std::filesystem::remove_all(root);
|
|
std::filesystem::create_directories(root);
|
|
const auto path = root / "pcm24_stereo.wav";
|
|
write_pcm24_wav(
|
|
path,
|
|
48000,
|
|
2,
|
|
{
|
|
0,
|
|
0x007FFFFF,
|
|
-0x00800000,
|
|
-1,
|
|
});
|
|
|
|
const auto wav = engine::audio::read_wav_f32(path);
|
|
require(wav.sample_rate == 48000, "PCM24 sample rate mismatch");
|
|
require(wav.channels == 2, "PCM24 channel count mismatch");
|
|
require(wav.samples.size() == 4, "PCM24 sample count mismatch");
|
|
require_near(wav.samples[0], 0.0F, "PCM24 zero");
|
|
require_near(wav.samples[1], 8388607.0F / 8388608.0F, "PCM24 max positive");
|
|
require_near(wav.samples[2], -1.0F, "PCM24 min negative");
|
|
require_near(wav.samples[3], -1.0F / 8388608.0F, "PCM24 negative one");
|
|
|
|
// --- WAVEFORMATEXTENSIBLE wrapping ordinary PCM16 ---------------------
|
|
// The case that actually bites: the payload is plain PCM16, but the
|
|
// format tag says 0xFFFE and the real tag lives in the SubFormat GUID.
|
|
// A reader that stops at the tag rejects a file it can decode.
|
|
{
|
|
const auto ext = root / "extensible_pcm16.wav";
|
|
write_wav(ext, 0x0001, 16, 44100, 2,
|
|
to_bytes<int16_t>({0, 16384, -16384, -32768}), true);
|
|
const auto wav = engine::audio::read_wav_f32(ext);
|
|
require(wav.sample_rate == 44100, "EXTENSIBLE PCM16 sample rate mismatch");
|
|
require(wav.channels == 2, "EXTENSIBLE PCM16 channel count mismatch");
|
|
require(wav.samples.size() == 4, "EXTENSIBLE PCM16 sample count mismatch");
|
|
require_near(wav.samples[0], 0.0F, 1.0e-7F, "EXTENSIBLE PCM16 zero");
|
|
require_near(wav.samples[1], 0.5F, 1.0e-7F, "EXTENSIBLE PCM16 half");
|
|
require_near(wav.samples[2], -0.5F, 1.0e-7F, "EXTENSIBLE PCM16 negative half");
|
|
require_near(wav.samples[3], -1.0F, 1.0e-7F, "EXTENSIBLE PCM16 full negative");
|
|
}
|
|
|
|
// --- WAVEFORMATEXTENSIBLE wrapping float32 ---------------------------
|
|
// Proves the GUID is actually read rather than assumed to be PCM.
|
|
{
|
|
const auto ext = root / "extensible_f32.wav";
|
|
write_wav(ext, 0x0003, 32, 48000, 1,
|
|
to_bytes<float>({0.0F, 0.25F, -0.75F}), true);
|
|
const auto wav = engine::audio::read_wav_f32(ext);
|
|
require(wav.channels == 1, "EXTENSIBLE float32 channel count mismatch");
|
|
require(wav.samples.size() == 3, "EXTENSIBLE float32 sample count mismatch");
|
|
require_near(wav.samples[1], 0.25F, 1.0e-7F, "EXTENSIBLE float32 quarter");
|
|
require_near(wav.samples[2], -0.75F, 1.0e-7F, "EXTENSIBLE float32 negative");
|
|
}
|
|
|
|
// --- PCM8 is unsigned, biased by 128 ---------------------------------
|
|
// The sign convention differs from every other PCM width, so a decoder
|
|
// that treats it as signed silently inverts the waveform.
|
|
{
|
|
const auto path8 = root / "pcm8.wav";
|
|
write_wav(path8, 0x0001, 8, 8000, 1,
|
|
std::vector<char>{static_cast<char>(128), static_cast<char>(255),
|
|
static_cast<char>(0), static_cast<char>(64),
|
|
static_cast<char>(1)});
|
|
const auto wav = engine::audio::read_wav_f32(path8);
|
|
require(wav.samples.size() == 5, "PCM8 sample count mismatch");
|
|
// Frozen from `ffmpeg -i pcm8.wav -f f32le -`. Note the endpoints are
|
|
// asymmetric: 255 is 127/128, not 1.0. ffmpeg agrees.
|
|
const float expected8[5] = {0.0F, 0.9921875F, -1.0F, -0.5F, -0.9921875F};
|
|
for (int i = 0; i < 5; ++i) {
|
|
require_near(wav.samples[static_cast<size_t>(i)], expected8[i], 1.0e-7F,
|
|
"PCM8 sample " + std::to_string(i));
|
|
}
|
|
}
|
|
|
|
// --- PCM32 -----------------------------------------------------------
|
|
{
|
|
const auto path32 = root / "pcm32.wav";
|
|
write_wav(path32, 0x0001, 32, 96000, 1,
|
|
to_bytes<int32_t>({0, 1073741824, -2147483647 - 1, 2147483647, 1}));
|
|
const auto wav = engine::audio::read_wav_f32(path32);
|
|
require(wav.sample_rate == 96000, "PCM32 sample rate mismatch");
|
|
require(wav.samples.size() == 5, "PCM32 sample count mismatch");
|
|
// Frozen from ffmpeg. INT32_MAX lands on exactly 1.0 rather than
|
|
// 2147483647/2^31, because float32's ULP at that magnitude is 256 and
|
|
// the cast rounds up before the divide. ffmpeg does the same.
|
|
const float expected32[5] = {0.0F, 0.5F, -1.0F, 1.0F, 4.656612873077393e-10F};
|
|
for (int i = 0; i < 5; ++i) {
|
|
require_near(wav.samples[static_cast<size_t>(i)], expected32[i], 1.0e-12F,
|
|
"PCM32 sample " + std::to_string(i));
|
|
}
|
|
}
|
|
|
|
// --- float64 ---------------------------------------------------------
|
|
{
|
|
const auto path64 = root / "float64.wav";
|
|
write_wav(path64, 0x0003, 64, 44100, 2,
|
|
to_bytes<double>({0.0, 0.125, -0.875, 1.0, 1.0 + 0x1p-30}));
|
|
const auto wav = engine::audio::read_wav_f32(path64);
|
|
require(wav.channels == 2, "float64 channel count mismatch");
|
|
require(wav.samples.size() == 5, "float64 sample count mismatch");
|
|
// Frozen from ffmpeg. The last one is deliberately not representable
|
|
// in float32 and collapses to 1.0 on both sides.
|
|
const float expected64[5] = {0.0F, 0.125F, -0.875F, 1.0F, 1.0F};
|
|
for (int i = 0; i < 5; ++i) {
|
|
require_near(wav.samples[static_cast<size_t>(i)], expected64[i], 1.0e-7F,
|
|
"float64 sample " + std::to_string(i));
|
|
}
|
|
}
|
|
|
|
// --- G.711 mu-law and A-law, every code ---------------------------
|
|
// One byte per code, decoded in one pass and compared against the
|
|
// external tables above. A spot check cannot catch a whole-table sign
|
|
// inversion; this can.
|
|
{
|
|
std::vector<char> codes(256);
|
|
for (int i = 0; i < 256; ++i) {
|
|
codes[static_cast<size_t>(i)] = static_cast<char>(i);
|
|
}
|
|
|
|
const auto path_mu = root / "mulaw_all.wav";
|
|
write_wav(path_mu, 0x0007, 8, 8000, 1, codes);
|
|
const auto mu = engine::audio::read_wav_f32(path_mu);
|
|
require(mu.samples.size() == 256, "mu-law sample count mismatch");
|
|
for (int i = 0; i < 256; ++i) {
|
|
require_near(
|
|
mu.samples[static_cast<size_t>(i)],
|
|
static_cast<float>(kMuLawExpected[i]) / 32768.0F,
|
|
1.0e-7F,
|
|
"mu-law code " + std::to_string(i));
|
|
}
|
|
|
|
const auto path_a = root / "alaw_all.wav";
|
|
write_wav(path_a, 0x0006, 8, 8000, 1, codes);
|
|
const auto alaw = engine::audio::read_wav_f32(path_a);
|
|
require(alaw.samples.size() == 256, "A-law sample count mismatch");
|
|
for (int i = 0; i < 256; ++i) {
|
|
require_near(
|
|
alaw.samples[static_cast<size_t>(i)],
|
|
static_cast<float>(kALawExpected[i]) / 32768.0F,
|
|
1.0e-7F,
|
|
"A-law code " + std::to_string(i));
|
|
}
|
|
}
|
|
|
|
// --- Extensible headers are validated, not trusted -------------------
|
|
// Only the first two bytes of the SubFormat GUID are the format tag. The
|
|
// other fourteen are a fixed suffix; without checking them, any codec
|
|
// whose GUID happens to start with 0x0001 decodes as PCM.
|
|
{
|
|
const auto bad_guid = root / "extensible_foreign_guid.wav";
|
|
write_wav(bad_guid, 0x0001, 16, 44100, 1,
|
|
to_bytes<int16_t>({0, 16384}), true, /*valid_guid_tail=*/false);
|
|
require_throws_containing(
|
|
[&] { (void)engine::audio::read_wav_f32(bad_guid); },
|
|
"KSDATAFORMAT_SUBTYPE", "foreign SubFormat GUID rejection");
|
|
|
|
// WAVEFORMATEXTENSIBLE requires cbSize >= 22; anything less is an
|
|
// internally inconsistent header.
|
|
const auto bad_cb = root / "extensible_bad_cbsize.wav";
|
|
write_wav(bad_cb, 0x0001, 16, 44100, 1,
|
|
to_bytes<int16_t>({0, 16384}), true, true, /*cb_size=*/0);
|
|
require_throws_containing(
|
|
[&] { (void)engine::audio::read_wav_f32(bad_cb); },
|
|
"cbSize", "extensible cbSize rejection");
|
|
}
|
|
|
|
// --- Truncated sample data is an error, not a silent trim ------------
|
|
// PCM24 already rejected a partial trailing sample; PCM32 and float64
|
|
// divided and dropped it, which turns a truncated download into audio
|
|
// that looks fine.
|
|
{
|
|
auto pcm32 = to_bytes<int32_t>({1073741824});
|
|
pcm32.push_back(0x7F);
|
|
const auto path32 = root / "pcm32_partial.wav";
|
|
write_wav(path32, 0x0001, 32, 44100, 1, pcm32);
|
|
require_throws_containing(
|
|
[&] { (void)engine::audio::read_wav_f32(path32); },
|
|
"malformed PCM32", "PCM32 partial sample rejection");
|
|
|
|
auto f64 = to_bytes<double>({0.25});
|
|
f64.push_back(static_cast<char>(0xAA));
|
|
const auto path64 = root / "float64_partial.wav";
|
|
write_wav(path64, 0x0003, 64, 44100, 1, f64);
|
|
require_throws_containing(
|
|
[&] { (void)engine::audio::read_wav_f32(path64); },
|
|
"malformed float64", "float64 partial sample rejection");
|
|
}
|
|
|
|
// --- All three overloads agree, including on a missing final pad -----
|
|
// RIFF pads odd-sized chunks, but writers routinely omit the byte when
|
|
// the chunk ends the file, and one-byte-per-sample formats make odd data
|
|
// chunks common. The path overload tolerated this because seeking past
|
|
// EOF is legal on an ifstream; the in-memory overload did not, so the
|
|
// same bytes parsed from a file and rejected from an upload buffer.
|
|
{
|
|
const auto odd = root / "pcm8_odd_no_pad.wav";
|
|
write_wav(odd, 0x0001, 8, 8000, 1,
|
|
std::vector<char>{static_cast<char>(0), static_cast<char>(128),
|
|
static_cast<char>(254)});
|
|
std::ifstream in(odd, std::ios::binary);
|
|
const std::string blob((std::istreambuf_iterator<char>(in)),
|
|
std::istreambuf_iterator<char>());
|
|
require(blob.size() % 2 == 1, "test file should end without the pad byte");
|
|
|
|
const auto from_path = engine::audio::read_wav_f32(odd);
|
|
const auto from_memory = engine::audio::read_wav_f32(std::string_view(blob));
|
|
require(from_path.samples.size() == 3, "odd-size PCM8 sample count from path");
|
|
require(from_memory.samples.size() == 3, "odd-size PCM8 sample count from memory");
|
|
for (size_t i = 0; i < from_path.samples.size(); ++i) {
|
|
require_near(from_memory.samples[i], from_path.samples[i], 0.0F,
|
|
"path and memory overloads disagree at " + std::to_string(i));
|
|
}
|
|
|
|
// The istream overload must not assume the stream began at offset 0,
|
|
// which an absolute rewind after the header sniff would.
|
|
std::istringstream prefixed(std::string("PREFIX!!") + blob, std::ios::binary);
|
|
prefixed.seekg(8);
|
|
const auto from_stream = engine::audio::read_wav_f32(prefixed);
|
|
require(from_stream.samples.size() == 3, "offset istream sample count");
|
|
require_near(from_stream.samples[0], from_path.samples[0], 0.0F,
|
|
"offset istream disagrees with path");
|
|
}
|
|
|
|
// --- A short fmt chunk is an error, not a read into the next chunk ----
|
|
{
|
|
const auto short_fmt = root / "short_fmt.wav";
|
|
{
|
|
std::ofstream out(short_fmt, std::ios::binary);
|
|
const std::vector<char> payload = to_bytes<int16_t>({0, 16384});
|
|
write_bytes(out, "RIFF", 4);
|
|
write_le<uint32_t>(out, 20u + 14u + static_cast<uint32_t>(payload.size()));
|
|
write_bytes(out, "WAVE", 4);
|
|
write_bytes(out, "fmt ", 4);
|
|
write_le<uint32_t>(out, 14u); // one field short of the minimum
|
|
write_le<uint16_t>(out, uint16_t{1});
|
|
write_le<uint16_t>(out, uint16_t{1});
|
|
write_le<uint32_t>(out, uint32_t{44100});
|
|
write_le<uint32_t>(out, uint32_t{88200});
|
|
write_le<uint16_t>(out, uint16_t{2});
|
|
write_bytes(out, "data", 4);
|
|
write_le<uint32_t>(out, static_cast<uint32_t>(payload.size()));
|
|
write_bytes(out, payload.data(), static_cast<std::streamsize>(payload.size()));
|
|
}
|
|
require_throws_containing(
|
|
[&] { (void)engine::audio::read_wav_f32(short_fmt); },
|
|
"malformed WAV fmt chunk", "short fmt chunk rejection");
|
|
}
|
|
|
|
// --- Still rejects what it genuinely cannot decode -------------------
|
|
// Widening the accepted set must not turn into accepting everything.
|
|
{
|
|
const auto path_bad = root / "unsupported.wav";
|
|
write_wav(path_bad, 0x0011, 4, 8000, 1, std::vector<char>{0x01, 0x02});
|
|
require_throws_containing(
|
|
[&] { (void)engine::audio::read_wav_f32(path_bad); },
|
|
"unsupported WAV encoding", "ADPCM rejection");
|
|
|
|
const auto path_flac = root / "actually.flac";
|
|
{
|
|
std::ofstream output(path_flac, std::ios::binary);
|
|
write_bytes(output, "fLaC\0\0\0\x22\0\0\0\0", 12);
|
|
}
|
|
require_throws_containing(
|
|
[&] { (void)engine::audio::read_wav_f32(path_flac); },
|
|
"FLAC", "FLAC container identification");
|
|
}
|
|
|
|
std::cout << "wav_reader_test passed\n";
|
|
} catch (const std::exception & ex) {
|
|
std::cerr << "wav_reader_test failed: " << ex.what() << "\n";
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|