From 009567eb22a2ed984340424b0e5e8bc06dae3c80 Mon Sep 17 00:00:00 2001 From: erysdren Date: Mon, 27 Jul 2026 20:49:01 -0500 Subject: [PATCH] sndpp: VDAT chunk support --- include/sndpp/WAV.h | 116 ++++++++++++++++++++++++++++++++++++++++++++ test/sndpp.cpp | 44 +++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/include/sndpp/WAV.h b/include/sndpp/WAV.h index bd58775d9..1df58c8e7 100644 --- a/include/sndpp/WAV.h +++ b/include/sndpp/WAV.h @@ -2,6 +2,7 @@ #include #include +#include #include @@ -26,6 +27,7 @@ class WAV : public RIFF { CHUNK_PAD = sourcepp::parser::binary::makeFourCC("PAD "), // Useless CHUNK_JUNK = sourcepp::parser::binary::makeFourCC("JUNK"), // Useless CHUNK_FLLR = sourcepp::parser::binary::makeFourCC("FLLR"), // Useless + CHUNK_VDAT = sourcepp::parser::binary::makeFourCC("VDAT"), // Valve lipsync data //CHUNK_LIST = sourcepp::parser::binary::makeFourCC("LIST"), // Metadata //CHUNK_LIST_WAVL = sourcepp::parser::binary::makeFourCC("WAVL"), // Wave list @@ -144,6 +146,34 @@ class WAV : public RIFF { std::array md5; }; + struct ChunkVDAT { + struct Emphasis { + float time; + float value; + }; + + struct Phoneme { + float startTime; + float endTime; + float volume; + uint32_t code; + std::string name; + }; + + struct Word { + float startTime; + float endTime; + std::string word; + std::vector phonemes; + }; + + float version; + std::string plaintext; + std::vector words; + std::vector emphasis; + std::unordered_map options; + }; + //struct ChunkLISTINFO { // struct META { // ChunkType infoType; @@ -264,6 +294,92 @@ class WAV : public RIFF { stream >> chunk->md5; } return chunk; + } else if constexpr (Type == CHUNK_VDAT) { + std::optional chunk = std::nullopt; + if (stream.size() > 0) { + chunk = ChunkVDAT{}; + auto getToken = [&stream]() { + std::string token; + if (stream.tell() >= stream.size()) { + return token; + } + bool gotToken = false; + char c = stream.read(); + while (true) { + if (!gotToken && std::isspace(c)) { + // skip leading whitespace + } else if (!gotToken && !std::isspace(c)) { + gotToken = true; + token += c; + } else if (gotToken && !std::isspace(c)) { + token += c; + } else if (gotToken && std::isspace(c)) { + break; + } + c = stream.read(); + } + return token; + }; + std::string token = getToken(); + while (token.length() > 0) { + if (token == "VERSION") { + token = getToken(); + chunk->version = std::stof(token); + } else if (token == "PLAINTEXT") { + token = getToken(); // opening brace + token = getToken(); // first token + while (true) { + chunk->plaintext += token; + token = getToken(); + if (!token.length() || token == "}") + break; + chunk->plaintext += ' '; + } + } else if (token == "WORDS") { + token = getToken(); // opening brace + token = getToken(); // first token + while (token == "WORD") { + ChunkVDAT::Word word; + word.word = getToken(); + word.startTime = std::stof(getToken()); + word.endTime = std::stof(getToken()); + token = getToken(); // opening brace + token = getToken(); // first token + while (token != "}") { + ChunkVDAT::Phoneme phoneme; + phoneme.code = std::stoi(token); + phoneme.name = getToken(); + phoneme.startTime = std::stof(getToken()); + phoneme.endTime = std::stof(getToken()); + phoneme.volume = std::stof(getToken()); + word.phonemes.push_back(phoneme); + token = getToken(); + } + chunk->words.push_back(word); + token = getToken(); + } + } else if (token == "EMPHASIS") { + token = getToken(); // opening brace + token = getToken(); // first token + while (token != "}") { + ChunkVDAT::Emphasis emphasis; + emphasis.time = std::stof(token); + emphasis.value = std::stof(getToken()); + chunk->emphasis.push_back(emphasis); + token = getToken(); + } + } else if (token == "OPTIONS") { + token = getToken(); // opening brace + token = getToken(); // first token + while (token != "}") { + chunk->options[token] = getToken(); + token = getToken(); + } + } + token = getToken(); + } + } + return chunk; } else { return this->hasNthChunk(Type, n) ? std::optional{std::move(chunkData)} : std::nullopt; } diff --git a/test/sndpp.cpp b/test/sndpp.cpp index 8b8ff6470..26e098e31 100644 --- a/test/sndpp.cpp +++ b/test/sndpp.cpp @@ -25,6 +25,50 @@ TEST(sndpp, read_wav) { EXPECT_EQ(data->size(), 34061); } +TEST(sndpp, read_wav_vdat) { + WAV wav{ASSET_ROOT "sndpp/gman_riseshine.wav"}; + ASSERT_TRUE(wav); + EXPECT_EQ(wav.getSignature(), RIFF_SIGNATURE); + EXPECT_EQ(wav.getChunks().size(), 5); + + const auto fmt = wav.getFirstWAVChunk(); + ASSERT_TRUE(fmt); + EXPECT_EQ(fmt->format, 2); + EXPECT_EQ(fmt->channels, 1); + EXPECT_EQ(fmt->samplesPerSecond, 22050); + EXPECT_EQ(fmt->averageBytesPerSecond, 11155); + EXPECT_EQ(fmt->blockAlign, 512); + EXPECT_EQ(fmt->bitsPerSample, 4); + EXPECT_EQ(fmt->extraCompressionInfo.size(), 34); + + const auto data = wav.getFirstWAVChunk(); + ASSERT_TRUE(data); + EXPECT_EQ(data->size(), 74240); + + const auto vdat = wav.getFirstWAVChunk(); + ASSERT_TRUE(vdat); + EXPECT_EQ(vdat->version, 1.0); + EXPECT_STREQ(vdat->plaintext.c_str(), "Rise and shine, Mr. Freeman. Rise and shine."); + EXPECT_STREQ(vdat->words[0].word.c_str(), "Rise"); + EXPECT_EQ(vdat->words[0].startTime, 0.0); + EXPECT_EQ(vdat->words[0].phonemes[0].code, 633); + EXPECT_STREQ(vdat->words[0].phonemes[0].name.c_str(), "r"); + EXPECT_EQ(vdat->words[0].phonemes[0].volume, 1.0); + EXPECT_STREQ(vdat->words[1].word.c_str(), "and"); + EXPECT_STREQ(vdat->words[2].word.c_str(), "shine,"); + EXPECT_STREQ(vdat->words[3].word.c_str(), "Mr."); + EXPECT_STREQ(vdat->words[4].word.c_str(), "Freeman."); + EXPECT_STREQ(vdat->words[5].word.c_str(), "shine"); + EXPECT_STREQ(vdat->words[6].word.c_str(), "Mr"); + EXPECT_STREQ(vdat->words[7].word.c_str(), "Freeman"); + EXPECT_STREQ(vdat->words[8].word.c_str(), "Rise"); + EXPECT_STREQ(vdat->words[9].word.c_str(), "and"); + EXPECT_STREQ(vdat->words[10].word.c_str(), "shine."); + EXPECT_STREQ(vdat->words[11].word.c_str(), "shine"); + EXPECT_EQ(vdat->emphasis.size(), 0); + EXPECT_STREQ(vdat->options.at("voice_duck").c_str(), "0"); +} + TEST(sndpp, read_xwv_v0) { XWV xwv{ASSET_ROOT "sndpp/biohazard_detected.xbox.wav"}; ASSERT_TRUE(xwv);