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.
This commit is contained in:
Tyler Wilding
2023-09-09 08:33:41 -06:00
committed by GitHub
parent d048f420a0
commit 0a7caf1d10
6 changed files with 182 additions and 77 deletions
+75 -34
View File
@@ -8,62 +8,86 @@
/*!
* Write a wave file from a vector of samples.
*/
void write_wave_file_mono(const std::vector<s16>& samples, s32 sample_rate, const fs::path& name) {
void write_wave_file(const std::vector<s16>& left_samples,
const std::vector<s16>& 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<s16> decode_adpcm(BinaryReader& reader) {
std::vector<s16> decoded_samples;
s32 sample_prev[2] = {0, 0};
std::pair<std::vector<s16>, std::vector<s16>> decode_adpcm(BinaryReader& reader, const bool mono) {
std::vector<s16> left_samples;
std::vector<s16> 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>();
u8 flags = reader.read<u8>();
u8 shift = shift_filter & 0b1111;
u8 filter = shift_filter >> 4;
u8 flags = reader.read<u8>();
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<s16> 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.
+5 -2
View File
@@ -29,8 +29,11 @@ struct WaveFileHeader {
s32 subchunk2_size;
};
void write_wave_file_mono(const std::vector<s16>& samples, s32 sample_rate, const fs::path& name);
void write_wave_file(const std::vector<s16>& left_samples,
const std::vector<s16>& right_samples,
s32 sample_rate,
const fs::path& name);
std::vector<s16> decode_adpcm(BinaryReader& reader);
std::pair<std::vector<s16>, std::vector<s16>> decode_adpcm(BinaryReader& reader, const bool mono);
std::vector<u8> encode_adpcm(const std::vector<s16>& samples);
+9 -1
View File
@@ -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": [
+87 -38
View File
@@ -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>();
u32 data_end = sizeof(u32) + sizeof(DirEntry) * count;
ASSERT(data_end <= data.size());
std::vector<DirEntry> entries;
for (u32 i = 0; i < count; i++) {
entries.push_back(reader.read<DirEntry>());
}
while (reader.bytes_left()) {
ASSERT(reader.read<u8>() == 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<DirEntryJak1> entries;
for (u32 i = 0; i < count; i++) {
entries.push_back(reader.read<DirEntryJak1>());
}
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<u8>() == 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<DirEntryJak2> entries;
for (u32 i = 0; i < count; i++) {
entries.push_back(reader.read<DirEntryJak2>());
}
while (reader.bytes_left()) {
ASSERT(reader.read<u8>() == 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<VagFileHeader>();
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<u8>() == 0);
}
std::vector<s16> 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<u8>() == 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<std::string>& 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<std::string> langs;
+4 -1
View File
@@ -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<std::string>& audio_files);
}
+2 -1
View File
@@ -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());