From 3dc27e37e5b5cc4ddbbf8373c71d4cbaedfc9d37 Mon Sep 17 00:00:00 2001 From: Ziemas Date: Tue, 23 Jan 2024 02:47:06 +0100 Subject: [PATCH] decompiler: Extract Jak3 VAGs (#3328) --- common/audio/audio_formats.cpp | 23 ++- decompiler/config/jak3/ntsc_v1/inputs.jsonc | 10 +- decompiler/data/streamed_audio.cpp | 147 +++++++++++--------- game/overlord/jak1/iso.cpp | 4 +- 4 files changed, 115 insertions(+), 69 deletions(-) diff --git a/common/audio/audio_formats.cpp b/common/audio/audio_formats.cpp index 25cdbf61de..42d57c5471 100644 --- a/common/audio/audio_formats.cpp +++ b/common/audio/audio_formats.cpp @@ -56,11 +56,14 @@ void write_wave_file(const std::vector& left_samples, writer.write_to_file(name); } -std::pair, std::vector> decode_adpcm(BinaryReader& reader, const bool mono) { +std::pair, std::vector> decode_adpcm(BinaryReader& reader, + const bool stereo) { std::vector left_samples; std::vector right_samples; s32 left_sample_prev[2] = {0, 0}; s32 right_sample_prev[2] = {0, 0}; + bool first_left = true; + bool first_right = true; constexpr s32 f1[5] = {0, 60, 115, 98, 122}; constexpr s32 f2[5] = {0, 0, -52, -55, -60}; @@ -71,15 +74,29 @@ std::pair, std::vector> decode_adpcm(BinaryReader& reader, // instead they are partitioned into contiguous 8kb (thats 8192 bytes) chunks // alternating left/right bool processing_left_chunk = true; + // We need to skip the vag header for each channel + if (first_left) { + reader.ffwd(48); + bytes_read += 48; + first_left = false; + } while (true) { if (!reader.bytes_left()) { break; } - if (bytes_read == 0x2000) { + if (stereo && bytes_read == 0x2000) { // switch streams processing_left_chunk = !processing_left_chunk; bytes_read = 0; + + // We need to skip the vag header for each channel + if (first_right) { + // skip the header + reader.ffwd(48); + bytes_read += 48; + first_right = false; + } } u8 shift_filter = reader.read(); @@ -107,7 +124,7 @@ std::pair, std::vector> decode_adpcm(BinaryReader& reader, s32 sample = (s32)(s16)(nibble << 12); sample >>= shift; - if (mono || processing_left_chunk) { + if (!stereo || processing_left_chunk) { sample += (left_sample_prev[0] * f1[filter] + left_sample_prev[1] * f2[filter] + 32) / 64; if (sample > 0x7fff) { diff --git a/decompiler/config/jak3/ntsc_v1/inputs.jsonc b/decompiler/config/jak3/ntsc_v1/inputs.jsonc index 472258c465..9b72b5cc82 100644 --- a/decompiler/config/jak3/ntsc_v1/inputs.jsonc +++ b/decompiler/config/jak3/ntsc_v1/inputs.jsonc @@ -310,7 +310,15 @@ // "audio_dir_file_name": "jak3/VAG", "audio_dir_file_name": "", - "streamed_audio_file_names": [], + "streamed_audio_file_names": [ + "VAGWAD.ENG", + "VAGWAD.FRE", + "VAGWAD.GER", + "VAGWAD.SPA", + "VAGWAD.ITA", + "VAGWAD.COM", + "VAGWAD.INT" + ], "levels_to_extract": [ "LJKDMPK.DGO", diff --git a/decompiler/data/streamed_audio.cpp b/decompiler/data/streamed_audio.cpp index fd50b92156..515dbe41a3 100644 --- a/decompiler/data/streamed_audio.cpp +++ b/decompiler/data/streamed_audio.cpp @@ -11,6 +11,7 @@ #include "third-party/json.hpp" namespace decompiler { +using std::string; // number of bytes per "audio page" in the VAG directory file. constexpr int AUDIO_PAGE_SIZE = 2048; @@ -27,75 +28,67 @@ uint32_t swap32(uint32_t in) { struct AudioDir { struct Entry { std::string name; + bool stereo = false; + bool international = false; s64 start_byte = -1; - s64 end_byte = -1; }; std::vector entries; - void set_file_size(u64 size) { - if (!entries.empty()) { - entries.back().end_byte = size; - } - } - int entry_count() const { return entries.size(); } void debug_print() const { for (auto& e : entries) { - lg::debug("\"{}\" 0x{:07x} - 0x{:07x}", e.name, e.start_byte, e.end_byte); + // lg::debug("\"{}\" 0x{:07x} - 0x{:07x}", e.name, e.start_byte, e.end_byte); } } }; -/*! - * Read an entry from a WAD and return the binary data. - */ -std::vector read_entry(const AudioDir& dir, const std::vector& data, int entry_idx) { - const auto& entry = dir.entries.at(entry_idx); - ASSERT(entry.end_byte > 0); - return std::vector(data.begin() + entry.start_byte, data.begin() + entry.end_byte); -} - /*! * Matches the format in file. */ struct VagFileHeader { - char magic[4]; + u32 magic; u32 version; u32 zero; - u32 channel_size; + u32 size; u32 sample_rate; u32 z[3]; char name[16]; - VagFileHeader swapped_endian_jak1() const { + VagFileHeader swap_endian() const { VagFileHeader result(*this); result.version = swap32(result.version); - result.channel_size = swap32(result.channel_size); + result.size = swap32(result.size); result.sample_rate = swap32(result.sample_rate); return result; } - VagFileHeader swapped_endian_jak2() const { - VagFileHeader result(*this); - result.version = swap32(result.version); - result.channel_size = swap32(result.channel_size); - // for some reason, the sample rate is big endian for jak2 - // result.sample_rate = swap32(result.sample_rate); - return result; - } - void debug_print() { char temp_name[17]; memcpy(temp_name, name, 16); temp_name[16] = '\0'; - lg::debug("{}{}{}{} v {} zero {} chan {} samp {} z {} {} {} name {}", magic[0], magic[1], - magic[2], magic[3], version, zero, channel_size, sample_rate, z[0], z[1], z[2], - temp_name); + lg::debug("{:x} v {} zero {} chan {} samp {} z {} {} {} name {}", magic, version, zero, size, + sample_rate, z[0], z[1], z[2], temp_name); } }; +static std::string unpack_vag_name_jak3(u64 compressed) { + const char* char_map = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"; + u32 chars = compressed & 0x1fffff; + std::array buf; + buf.fill(0); + for (int i = 0; i < 8; i++) { + if (i == 4) { + chars = (compressed >> 21) & 0x1fffff; + } + buf[7 - i] = char_map[chars % 38]; + chars /= 38; + } + + return {buf.data()}; +} + /*! * Read the DIR file into an AudioDir */ @@ -103,9 +96,9 @@ AudioDir read_audio_dir(const decompiler::Config& config, const fs::path& path) auto data = file_util::read_binary_file(path); lg::info("Got {} bytes of audio dir.", data.size()); auto reader = BinaryReader(data); - u32 count = reader.read(); AudioDir result; if (config.game_version == GameVersion::Jak1) { + u32 count = reader.read(); // matches the format in file. struct DirEntryJak1 { char name[8]; @@ -129,20 +122,15 @@ AudioDir read_audio_dir(const decompiler::Config& config, const fs::path& path) e.name.push_back(c); } e.start_byte = AUDIO_PAGE_SIZE * entries[i].value; - if (i + 1 < (entries.size())) { - e.end_byte = AUDIO_PAGE_SIZE * entries[i + 1].value; - } else { - e.end_byte = -1; - } result.entries.push_back(e); } } else if (config.game_version == GameVersion::Jak2) { + u32 count = reader.read(); // matches the format in file. struct DirEntryJak2 { char name[8]; u32 value; - // TODO - no idea what this is - u32 boolean; + u32 stereo; }; u32 data_end = sizeof(u32) + sizeof(DirEntryJak2) * count; ASSERT(data_end <= data.size()); @@ -161,12 +149,45 @@ AudioDir read_audio_dir(const decompiler::Config& config, const fs::path& path) // padded with spaces, no null terminator. e.name.push_back(c); } + e.stereo = entries[i].stereo; e.start_byte = AUDIO_PAGE_SIZE * entries[i].value; - if (i + 1 < (entries.size())) { - e.end_byte = AUDIO_PAGE_SIZE * entries[i + 1].value; - } else { - e.end_byte = -1; - } + result.entries.push_back(e); + } + } else if (config.game_version == GameVersion::Jak3) { + struct VagDirJak3 { + u32 id[2]; + u32 version; + u32 count; + } dir; + struct DirEntryJak3 { + union { + u64 data; + struct { + u64 name : 42; + bool stereo : 1; + bool international : 1; + u8 param : 4; + u64 offset : 16; + }; + }; + }; + dir = reader.read(); + ASSERT(dir.id[0] == 0x41574756); + ASSERT(dir.id[1] == 0x52494444); + lg::warn("version {} count {}", dir.version, dir.count); + + std::vector entries; + + for (size_t i = 0; i < dir.count; i++) { + entries.push_back(reader.read()); + } + + for (size_t i = 0; i < entries.size(); i++) { + AudioDir::Entry e; + e.name = unpack_vag_name_jak3(entries[i].name); + e.stereo = entries[i].stereo; + e.international = entries[i].international; + e.start_byte = 0x8000 * entries[i].offset; result.entries.push_back(e); } } else { @@ -189,27 +210,22 @@ struct AudioFileInfo { }; AudioFileInfo process_audio_file(const fs::path& output_folder, - const std::vector& data, + nonstd::span data, const std::string& name, - const std::string& suffix) { + const std::string& suffix, + bool stereo) { BinaryReader reader(data); auto header = reader.read(); - if (header.magic[0] == 'V') { - header = header.swapped_endian_jak1(); - } else if (header.magic[0] == 'p') { - header = header.swapped_endian_jak2(); - } else { + if (header.magic == 0x70474156 /* big endian (VAGp)*/) { + header = header.swap_endian(); + } else if (header.magic != 0x56414770 /* little endian (pGAV) */) { ASSERT(false); } header.debug_print(); - for (int i = 0; i < 16; i++) { - ASSERT(reader.read() == 0); - } - - const auto [left_samples, right_samples] = - decode_adpcm(reader, !str_util::starts_with(std::string(header.name), "Stereo")); + reader = BinaryReader(data.subspan(0, header.size)); + const auto [left_samples, right_samples] = decode_adpcm(reader, stereo); while (reader.bytes_left()) { ASSERT(reader.read() == 0); @@ -251,12 +267,17 @@ void process_streamed_audio(const decompiler::Config& config, auto& file = audio_files[lang_id]; auto wad_data = file_util::read_binary_file(input_dir / "VAG" / file); auto suffix = fs::path(file).extension().u8string().substr(1); + bool int_bank_p = suffix.compare("INT") == 0; langs.push_back(suffix); - dir_data.set_file_size(wad_data.size()); for (int i = 0; i < dir_data.entry_count(); i++) { - auto audio_data = read_entry(dir_data, wad_data, i); - lg::info("File {}, total {:.2f} minutes", dir_data.entries.at(i).name, audio_len / 60.0); - auto info = process_audio_file(output_path, audio_data, dir_data.entries.at(i).name, suffix); + auto entry = dir_data.entries.at(i); + if (entry.international != int_bank_p) { + continue; + } + + lg::info("File {}, total {:.2f} minutes", entry.name, audio_len / 60.0); + auto data = nonstd::span(wad_data).subspan(entry.start_byte); + auto info = process_audio_file(output_path, data, entry.name, suffix, entry.stereo); audio_len += info.length_seconds; filename_data[i][lang_id + 1] = info.filename; } diff --git a/game/overlord/jak1/iso.cpp b/game/overlord/jak1/iso.cpp index 370bce7a02..38a7ed82cd 100644 --- a/game/overlord/jak1/iso.cpp +++ b/game/overlord/jak1/iso.cpp @@ -1195,7 +1195,7 @@ static u32 ProcessVAGData(IsoMessage* _cmd, IsoBufferHeader* buffer_header) { if (vag->buffer_number == 0) { // first buffer, set stuff up u32* data = (u32*)buffer_header->data; - if (data[0] != 0x70474156 /* 'pGAV' */ && data[0] != 0x56414770 /* 'VAGp' */) { + if (data[0] != 0x70474156 /* 'VAGp' */ && data[0] != 0x56414770 /* 'pGAV' */) { vag->stop = true; buffer_header->data_size = 0; return CMD_STATUS_IN_PROGRESS; @@ -1203,7 +1203,7 @@ static u32 ProcessVAGData(IsoMessage* _cmd, IsoBufferHeader* buffer_header) { vag->sample_rate = data[4]; vag->data_left = data[3]; - if (data[0] == 0x70474156 /* 'pGAV' */) { + if (data[0] == 0x70474156 /* 'VAGp' */) { vag->sample_rate = bswap(vag->sample_rate); vag->data_left = bswap(vag->data_left); }