From 0a7caf1d102e89a445d898cf692661498ae78a91 Mon Sep 17 00:00:00 2001 From: Tyler Wilding Date: Sat, 9 Sep 2023 08:33:41 -0600 Subject: [PATCH] decomp: handle dumping jak 2 VAG file assets (#2969) There are potentially still some minor issues with the resulting files. Some of them appear to have minor artifacts that playing through the actual game do not -- but this is a much better starting point for someone to iterate from if they are interested in improving things. --- common/audio/audio_formats.cpp | 109 +++++++++++------ common/audio/audio_formats.h | 7 +- decompiler/config/jak2/ntsc_v1/inputs.jsonc | 10 +- decompiler/data/streamed_audio.cpp | 125 ++++++++++++++------ decompiler/data/streamed_audio.h | 5 +- decompiler/main.cpp | 3 +- 6 files changed, 182 insertions(+), 77 deletions(-) diff --git a/common/audio/audio_formats.cpp b/common/audio/audio_formats.cpp index e3d88bc322..83201b65f3 100644 --- a/common/audio/audio_formats.cpp +++ b/common/audio/audio_formats.cpp @@ -8,62 +8,86 @@ /*! * Write a wave file from a vector of samples. */ -void write_wave_file_mono(const std::vector& samples, s32 sample_rate, const fs::path& name) { +void write_wave_file(const std::vector& left_samples, + const std::vector& right_samples, + s32 sample_rate, + const fs::path& name) { WaveFileHeader header; memcpy(header.chunk_id, "RIFF", 4); - header.chunk_size = 36 + samples.size() * sizeof(s16); + header.chunk_size = 36 + ((left_samples.size() + right_samples.size()) * sizeof(s16)); memcpy(header.format, "WAVE", 4); // now the format memcpy(header.subchunk1_id, "fmt ", 4); header.subchunk1_size = 16; header.aud_format = 1; - header.num_channels = 1; // mono + if (right_samples.empty()) { + header.num_channels = 1; // mono + } else { + header.num_channels = 2; // stereo + } + header.sample_rate = sample_rate; header.byte_rate = sample_rate * header.num_channels * sizeof(s16); header.block_align = header.num_channels * sizeof(s16); header.bits_per_sample = 16; memcpy(header.subchunk2_id, "data", 4); - header.subchunk2_size = samples.size() * sizeof(s16); + header.subchunk2_size = (left_samples.size() + right_samples.size()) * sizeof(s16); BinaryWriter writer; writer.add(header); - for (auto& samp : samples) { - writer.add(samp); + if (right_samples.empty()) { + for (const auto& sample : left_samples) { + writer.add(sample); + } + } else { + for (int i = 0; i < left_samples.size(); i++) { + writer.add(left_samples.at(i)); + if (i < right_samples.size()) { + writer.add(right_samples.at(i)); + } else { + writer.add(0); + } + } } writer.write_to_file(name); } -std::vector decode_adpcm(BinaryReader& reader) { - std::vector decoded_samples; - s32 sample_prev[2] = {0, 0}; +std::pair, std::vector> decode_adpcm(BinaryReader& reader, const bool mono) { + std::vector left_samples; + std::vector right_samples; + s32 left_sample_prev[2] = {0, 0}; + s32 right_sample_prev[2] = {0, 0}; constexpr s32 f1[5] = {0, 60, 115, 98, 122}; constexpr s32 f2[5] = {0, 0, -52, -55, -60}; [[maybe_unused]] int block_idx = 0; + // 16 byte blocks + int bytes_read = reader.get_seek(); // we've already read n bytes into the file + // Jak VAG's don't interleave the samples because of course they don't + // instead they are partitioned into contiguous 8kb (thats 8192 bytes) chunks + // alternating left/right + bool processing_left_chunk = true; while (true) { if (!reader.bytes_left()) { break; } + + if (bytes_read == 0x2000) { + // switch streams + processing_left_chunk = !processing_left_chunk; + bytes_read = 0; + } + u8 shift_filter = reader.read(); - u8 flags = reader.read(); u8 shift = shift_filter & 0b1111; u8 filter = shift_filter >> 4; + u8 flags = reader.read(); - if (shift > 12) { - ASSERT(false); - } - - if (filter > 4) { - ASSERT(false); - } - - if (flags == 7) { - break; - } + // removed assertions here u8 input_buffer[14]; @@ -81,25 +105,42 @@ std::vector decode_adpcm(BinaryReader& reader) { s32 sample = (s32)(s16)(nibble << 12); sample >>= shift; - sample += (sample_prev[0] * f1[filter] + sample_prev[1] * f2[filter] + 32) / 64; - if (sample > 0x7fff) { - sample = 0x7fff; + if (mono || processing_left_chunk) { + sample += (left_sample_prev[0] * f1[filter] + left_sample_prev[1] * f2[filter] + 32) / 64; + + if (sample > 0x7fff) { + sample = 0x7fff; + } + + if (sample < -0x8000) { + sample = -0x8000; + } + + left_sample_prev[1] = left_sample_prev[0]; + left_sample_prev[0] = sample; + left_samples.push_back(sample); + } else { + sample += (right_sample_prev[0] * f1[filter] + right_sample_prev[1] * f2[filter] + 32) / 64; + + if (sample > 0x7fff) { + sample = 0x7fff; + } + + if (sample < -0x8000) { + sample = -0x8000; + } + + right_sample_prev[1] = right_sample_prev[0]; + right_sample_prev[0] = sample; + right_samples.push_back(sample); } - - if (sample < -0x8000) { - sample = -0x8000; - } - - sample_prev[1] = sample_prev[0]; - sample_prev[0] = sample; - - decoded_samples.push_back(sample); } + bytes_read += 16; block_idx++; } - return decoded_samples; + return {left_samples, right_samples}; } // I attempted to write an encoder below, which works, but has some limitations. diff --git a/common/audio/audio_formats.h b/common/audio/audio_formats.h index 6ec0e4ed28..12d710041f 100644 --- a/common/audio/audio_formats.h +++ b/common/audio/audio_formats.h @@ -29,8 +29,11 @@ struct WaveFileHeader { s32 subchunk2_size; }; -void write_wave_file_mono(const std::vector& samples, s32 sample_rate, const fs::path& name); +void write_wave_file(const std::vector& left_samples, + const std::vector& right_samples, + s32 sample_rate, + const fs::path& name); -std::vector decode_adpcm(BinaryReader& reader); +std::pair, std::vector> decode_adpcm(BinaryReader& reader, const bool mono); std::vector encode_adpcm(const std::vector& samples); diff --git a/decompiler/config/jak2/ntsc_v1/inputs.jsonc b/decompiler/config/jak2/ntsc_v1/inputs.jsonc index 71737ee82b..8d9da73f72 100644 --- a/decompiler/config/jak2/ntsc_v1/inputs.jsonc +++ b/decompiler/config/jak2/ntsc_v1/inputs.jsonc @@ -445,7 +445,15 @@ // "audio_dir_file_name": "jak2/VAG", "audio_dir_file_name": "", - "streamed_audio_file_names": [], + "streamed_audio_file_names": [ + "VAGWAD.ENG", + "VAGWAD.FRE", + "VAGWAD.GER", + "VAGWAD.ITA", + "VAGWAD.JAP", + "VAGWAD.KOR", + "VAGWAD.SPA" + ], // tpages that should always be possible to access. "common_tpages": [ diff --git a/decompiler/data/streamed_audio.cpp b/decompiler/data/streamed_audio.cpp index 306dd47a8c..fd50b92156 100644 --- a/decompiler/data/streamed_audio.cpp +++ b/decompiler/data/streamed_audio.cpp @@ -5,6 +5,7 @@ #include "common/log/log.h" #include "common/util/BinaryReader.h" #include "common/util/FileUtil.h" +#include "common/util/string_util.h" #include "third-party/fmt/core.h" #include "third-party/json.hpp" @@ -68,7 +69,7 @@ struct VagFileHeader { u32 z[3]; char name[16]; - VagFileHeader swapped_endian() const { + VagFileHeader swapped_endian_jak1() const { VagFileHeader result(*this); result.version = swap32(result.version); result.channel_size = swap32(result.channel_size); @@ -76,6 +77,15 @@ struct VagFileHeader { 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); @@ -89,46 +99,79 @@ struct VagFileHeader { /*! * Read the DIR file into an AudioDir */ -AudioDir read_audio_dir(const fs::path& path) { - // matches the format in file. - struct DirEntry { - char name[8]; - u32 value; - }; +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(); - u32 data_end = sizeof(u32) + sizeof(DirEntry) * count; - ASSERT(data_end <= data.size()); - std::vector entries; - for (u32 i = 0; i < count; i++) { - entries.push_back(reader.read()); - } - - while (reader.bytes_left()) { - ASSERT(reader.read() == 0); - } - AudioDir result; + if (config.game_version == GameVersion::Jak1) { + // matches the format in file. + struct DirEntryJak1 { + char name[8]; + u32 value; + }; + u32 data_end = sizeof(u32) + sizeof(DirEntryJak1) * count; + ASSERT(data_end <= data.size()); + std::vector entries; + for (u32 i = 0; i < count; i++) { + entries.push_back(reader.read()); + } - ASSERT(!entries.empty()); - for (size_t i = 0; i < entries.size(); i++) { - AudioDir::Entry e; - for (auto c : entries[i].name) { - // padded with spaces, no null terminator. - e.name.push_back(c); + while (reader.bytes_left()) { + ASSERT(reader.read() == 0); } - 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; + ASSERT(!entries.empty()); + for (size_t i = 0; i < entries.size(); i++) { + AudioDir::Entry e; + for (auto c : entries[i].name) { + // padded with spaces, no null terminator. + 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); } - result.entries.push_back(e); + } else if (config.game_version == GameVersion::Jak2) { + // matches the format in file. + struct DirEntryJak2 { + char name[8]; + u32 value; + // TODO - no idea what this is + u32 boolean; + }; + u32 data_end = sizeof(u32) + sizeof(DirEntryJak2) * count; + ASSERT(data_end <= data.size()); + std::vector entries; + for (u32 i = 0; i < count; i++) { + entries.push_back(reader.read()); + } + + while (reader.bytes_left()) { + ASSERT(reader.read() == 0); + } + ASSERT(!entries.empty()); + for (size_t i = 0; i < entries.size(); i++) { + AudioDir::Entry e; + for (auto c : entries[i].name) { + // padded with spaces, no null terminator. + 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 { + ASSERT_MSG(false, "Unsupported game version for extracting streaming audio"); } - return result; } @@ -153,7 +196,9 @@ AudioFileInfo process_audio_file(const fs::path& output_folder, auto header = reader.read(); if (header.magic[0] == 'V') { - header = header.swapped_endian(); + header = header.swapped_endian_jak1(); + } else if (header.magic[0] == 'p') { + header = header.swapped_endian_jak2(); } else { ASSERT(false); } @@ -163,7 +208,8 @@ AudioFileInfo process_audio_file(const fs::path& output_folder, ASSERT(reader.read() == 0); } - std::vector decoded_samples = decode_adpcm(reader); + const auto [left_samples, right_samples] = + decode_adpcm(reader, !str_util::starts_with(std::string(header.name), "Stereo")); while (reader.bytes_left()) { ASSERT(reader.read() == 0); @@ -171,7 +217,8 @@ AudioFileInfo process_audio_file(const fs::path& output_folder, file_util::create_dir_if_needed(output_folder / suffix); auto file_name = fmt::format("{}.wav", remove_trailing_spaces(name)); - write_wave_file_mono(decoded_samples, header.sample_rate, output_folder / suffix / file_name); + write_wave_file(left_samples, right_samples, header.sample_rate, + output_folder / suffix / file_name); std::string vag_filename; for (int i = 0; i < 16; i++) { @@ -179,13 +226,15 @@ AudioFileInfo process_audio_file(const fs::path& output_folder, vag_filename.push_back(header.name[i]); } } - return {vag_filename, (double)decoded_samples.size() / header.sample_rate}; + return {vag_filename, + ((double)left_samples.size() + (double)right_samples.size()) / header.sample_rate}; } -void process_streamed_audio(const fs::path& output_path, +void process_streamed_audio(const decompiler::Config& config, + const fs::path& output_path, const fs::path& input_dir, const std::vector& audio_files) { - auto dir_data = read_audio_dir(input_dir / "VAG" / "VAGDIR.AYB"); + auto dir_data = read_audio_dir(config, input_dir / "VAG" / "VAGDIR.AYB"); double audio_len = 0.f; std::vector langs; diff --git a/decompiler/data/streamed_audio.h b/decompiler/data/streamed_audio.h index b1f5ecbb46..f7a0ae1d9d 100644 --- a/decompiler/data/streamed_audio.h +++ b/decompiler/data/streamed_audio.h @@ -5,8 +5,11 @@ #include "common/util/FileUtil.h" +#include "decompiler/config.h" + namespace decompiler { -void process_streamed_audio(const fs::path& output_path, +void process_streamed_audio(const Config& config, + const fs::path& output_path, const fs::path& input_dir, const std::vector& audio_files); } diff --git a/decompiler/main.cpp b/decompiler/main.cpp index 1462cb2a4d..91d8022e0f 100644 --- a/decompiler/main.cpp +++ b/decompiler/main.cpp @@ -312,7 +312,8 @@ int main(int argc, char** argv) { auto streaming_audio_in = in_folder / "VAG"; auto streaming_audio_out = out_folder / "assets" / "streaming_audio"; file_util::create_dir_if_needed(streaming_audio_out); - process_streamed_audio(streaming_audio_out, in_folder, config.streamed_audio_file_names); + process_streamed_audio(config, streaming_audio_out, in_folder, + config.streamed_audio_file_names); } lg::info("Decompiler has finished successfully in {:.2f} seconds.", decomp_timer.getSeconds());