// host/audio_decode against synthetic .wav layouts and the real 40250 audio: every .wav/.mp3 in Client/pack // (through CEterPackManager, as the live client reads them) and every Client/BGM/*.mp3 must decode to // non-empty 44.1 kHz stereo float whose length matches the source duration. // // host_audio_decode_test <40250 Client dir> // // The pack/BGM half exits 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1. #include "EterPack/StdAfx.h" #include "EterPack/EterPackManager.h" #include "../../src/platform/UserInterface/UserInterface.h" #include "audio_decode.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using mt_host::decode_audio; static int g_failures = 0; #define CHECK(cond) \ do { \ if (!(cond)) { \ std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \ ++g_failures; \ } \ } while (0) static void put16(std::vector& v, unsigned x) { v.push_back(x & 0xff); v.push_back(x >> 8 & 0xff); } static void put32(std::vector& v, unsigned x) { put16(v, x & 0xffff); put16(v, x >> 16); } // A RIFF/WAVE file with a LIST chunk before "fmt " (odd-sized, so padded) and `frames` frames of a ramp. static std::vector make_wav(unsigned tag, unsigned channels, unsigned rate, unsigned bits, unsigned frames, bool extensible = false) { std::vector fmt; put16(fmt, extensible ? 0xFFFE : tag); put16(fmt, channels); put32(fmt, rate); put32(fmt, rate * channels * bits / 8); put16(fmt, channels * bits / 8); put16(fmt, bits); if (extensible) { put16(fmt, 22); put16(fmt, bits); put32(fmt, 0); put16(fmt, tag); const std::uint8_t guid_tail[14] = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71}; fmt.insert(fmt.end(), guid_tail, guid_tail + 14); } std::vector pcm; for (unsigned i = 0; i < frames * channels; ++i) { const double s = std::sin(i * 0.01) * 0.5; if (bits == 8) pcm.push_back(std::uint8_t(128 + s * 127)); else if (bits == 16) put16(pcm, unsigned(std::int16_t(s * 32767))); else if (bits == 24) { const unsigned x = unsigned(std::int32_t(s * 8388607)); put16(pcm, x); pcm.push_back(x >> 16 & 0xff); } else if (tag == 3) { float f = float(s); unsigned x; std::memcpy(&x, &f, 4); put32(pcm, x); } else put32(pcm, unsigned(std::int32_t(s * 2147483647.0))); } std::vector out = {'R', 'I', 'F', 'F', 0, 0, 0, 0, 'W', 'A', 'V', 'E', 'L', 'I', 'S', 'T', 3, 0, 0, 0, 'a', 'b', 'c', 0}; out.insert(out.end(), {'f', 'm', 't', ' '}); put32(out, unsigned(fmt.size())); out.insert(out.end(), fmt.begin(), fmt.end()); out.insert(out.end(), {'d', 'a', 't', 'a'}); put32(out, unsigned(pcm.size())); out.insert(out.end(), pcm.begin(), pcm.end()); const unsigned riff = unsigned(out.size() - 8); std::memcpy(&out[4], &riff, 4); return out; } // Output frame count within a few frames of the resampled source length. static bool frames_match(const std::vector& out, double src_frames, double src_rate) { const double expected = src_frames * mt_host::kMixRate / src_rate; return out.size() % 2 == 0 && std::fabs(double(out.size() / 2) - expected) <= 8 + expected * 0.001; } static bool peak_ok(const std::vector& out) { float peak = 0; for (float f : out) { if (!std::isfinite(f)) return false; peak = std::max(peak, std::fabs(f)); } return peak <= 1.5f; } static void synthetic() { struct Case { unsigned tag, channels, rate, bits; bool ext; }; const Case cases[] = { {1, 1, 22050, 16, false}, {1, 1, 32000, 16, false}, {1, 1, 44100, 16, false}, {1, 2, 44100, 16, false}, {1, 1, 22000, 16, false}, {1, 1, 11025, 8, false}, {1, 2, 48000, 24, false}, {1, 2, 44100, 32, false}, {3, 1, 44100, 32, false}, {1, 2, 22050, 16, true}, {3, 2, 48000, 32, true}, }; for (const Case& c : cases) { const unsigned frames = c.rate / 2; std::vector out; const std::vector wav = make_wav(c.tag, c.channels, c.rate, c.bits, frames, c.ext); const bool ok = decode_audio(wav.data(), wav.size(), out); if (!ok || !frames_match(out, frames, c.rate) || !peak_ok(out)) std::fprintf(stderr, "synthetic tag %u ch %u %u Hz %u bit ext %d: ok %d frames %zu\n", c.tag, c.channels, c.rate, c.bits, c.ext, ok, out.size() / 2); CHECK(ok && frames_match(out, frames, c.rate) && peak_ok(out)); } // Mono is duplicated to both channels. { std::vector out; const std::vector wav = make_wav(1, 1, 44100, 16, 1000); CHECK(decode_audio(wav.data(), wav.size(), out) && out.size() == 2000); bool same = out.size() == 2000; for (size_t i = 0; same && i < out.size(); i += 2) same = out[i] == out[i + 1]; CHECK(same); } // The length cap. { std::vector out; const std::vector wav = make_wav(1, 1, 44100, 16, 44100 * 3); CHECK(decode_audio(wav.data(), wav.size(), out, 1) && out.size() == 44100 * 2); } // Garbage, truncation, unsupported formats. { std::vector out; std::vector junk(4096, 0x5a); CHECK(!decode_audio(junk.data(), junk.size(), out) && out.empty()); CHECK(!decode_audio(nullptr, 0, out)); std::vector wav = make_wav(1, 1, 22050, 16, 1000); wav.resize(40); CHECK(!decode_audio(wav.data(), wav.size(), out)); const std::vector adpcm = make_wav(2, 1, 22050, 16, 1000); CHECK(!decode_audio(adpcm.data(), adpcm.size(), out)); } } static bool has_ext(const std::string& name, const char* ext) { const size_t n = std::strlen(ext); return name.size() > n && strcasecmp(name.c_str() + name.size() - n, ext) == 0; } int main(int argc, char** argv) { synthetic(); const char* strict = std::getenv("MT_ASSETS_STRICT"); const bool is_strict = strict && std::string(strict) == "1"; const std::string client = argc > 1 ? argv[1] : ""; if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0) { std::fprintf(stderr, "host_audio_decode_test: no 40250 Client/pack at '%s'\n", client.c_str()); if (g_failures) return 1; return is_strict ? 1 : 77; } PackSingletons(); CHECK(PackInitialize("pack")); struct Access : CEterPackManager { static const CEterFileDict::TDict& dict(CEterPackManager& m) { return static_cast(m).m_FileDict.GetDict(); } }; CEterPackManager& mgr = CEterPackManager::Instance(); std::vector names; for (const auto& kv : Access::dict(mgr)) { const std::string name = kv.second.pkInfo->filename; if ((has_ext(name, ".wav") || has_ext(name, ".mp3")) && (names.empty() || names.back() != name)) names.push_back(name); } std::sort(names.begin(), names.end()); names.erase(std::unique(names.begin(), names.end()), names.end()); size_t wav = 0, mp3 = 0, bgm = 0, failed = 0; double seconds = 0, decode_ms = 0; auto decode_one = [&](const std::string& label, const std::uint8_t* data, size_t size) { std::vector out; const auto t0 = std::chrono::steady_clock::now(); const bool ok = decode_audio(data, size, out) && peak_ok(out); decode_ms += std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); if (!ok) { if (failed < 10) std::fprintf(stderr, "cannot decode %s (%zu bytes)\n", label.c_str(), size); ++failed; } seconds += out.size() / 2.0 / mt_host::kMixRate; }; for (const std::string& name : names) { CMappedFile file; LPCVOID data = nullptr; if (!mgr.Get(file, name.c_str(), &data)) { std::fprintf(stderr, "cannot read %s\n", name.c_str()); ++failed; continue; } (has_ext(name, ".wav") ? wav : mp3) += 1; decode_one(name, static_cast(data), file.Size()); } if (DIR* d = opendir("BGM")) { while (dirent* e = readdir(d)) { if (!has_ext(e->d_name, ".mp3")) continue; std::ifstream in(std::string("BGM/") + e->d_name, std::ios::binary); const std::vector bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); ++bgm; decode_one(std::string("BGM/") + e->d_name, bytes.data(), bytes.size()); } closedir(d); } // 2026-09-29 survey (tools/epk_scan): 1601 PCM16 .wav entries (1578 distinct paths) and 9 .mp3 in the packs, 25 .mp3 in Client/BGM. CHECK(wav == 1578); CHECK(mp3 == 9); CHECK(bgm == 25); CHECK(failed == 0); std::printf("pack wav %zu, pack mp3 %zu, BGM mp3 %zu, failed %zu, %.0f s of audio decoded in %.0f ms\n", wav, mp3, bgm, failed, seconds, decode_ms); if (g_failures) std::fprintf(stderr, "%d failure(s)\n", g_failures); return g_failures ? 1 : 0; }