diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index bcf36d3a68..711d1d9c16 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(common SHARED + audio/audio_formats.cpp cross_os_debug/xdbg.cpp cross_sockets/xsocket.cpp goos/Interpreter.cpp diff --git a/common/audio/audio_formats.cpp b/common/audio/audio_formats.cpp new file mode 100644 index 0000000000..231c69079d --- /dev/null +++ b/common/audio/audio_formats.cpp @@ -0,0 +1,300 @@ +#include "audio_formats.h" +#include "common/util/BinaryWriter.h" +#include "third-party/fmt/core.h" + +/*! + * Write a wave file from a vector of samples. + */ +void write_wave_file_mono(const std::vector& samples, + s32 sample_rate, + const std::string& name) { + WaveFileHeader header; + memcpy(header.chunk_id, "RIFF", 4); + header.chunk_size = 36 + 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 + 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); + + BinaryWriter writer; + writer.add(header); + + for (auto& samp : samples) { + writer.add(samp); + } + + writer.write_to_file(name); +} + +std::vector decode_adpcm(BinaryReader& reader) { + std::vector decoded_samples; + s32 sample_prev[2] = {0, 0}; + constexpr s32 f1[5] = {0, 60, 115, 98, 122}; + constexpr s32 f2[5] = {0, 0, -52, -55, -60}; + + int block_idx = 0; + while (true) { + if (!reader.bytes_left()) { + break; + } + u8 shift_filter = reader.read(); + u8 flags = reader.read(); + u8 shift = shift_filter & 0b1111; + u8 filter = shift_filter >> 4; + + if (shift > 12) { + assert(false); + } + + if (filter > 4) { + assert(false); + } + + if (flags == 7) { + break; + } + + u8 input_buffer[14]; + + for (int i = 0; i < 14; i++) { + input_buffer[i] = reader.read(); + } + + for (int i = 0; i < 28; i++) { + int16_t nibble = input_buffer[i / 2]; + if (i % 2 == 0) { + nibble = (nibble & 0x0f); + } else { + nibble = (nibble & 0xf0) >> 4; + } + + 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 (sample < -0x8000) { + sample = -0x8000; + } + + sample_prev[1] = sample_prev[0]; + sample_prev[0] = sample; + + decoded_samples.push_back(sample); + } + block_idx++; + } + + return decoded_samples; +} + +// I attempted to write an encoder below, which works, but has some limitations. +// - In some cases we can't recover the original data exactly because the decode saturates the +// the output to fit in a signed 16-bit integer. +// - There are some cases when there are multiple ways to encode the same data. +// The break_filter_ties function attempts to handle this, but doesn't work 100% of the time. + +template +T saturate(T in, T minimum, T maximum) { + if (in < minimum) { + return minimum; + } + if (in > maximum) { + return maximum; + } + return in; +} + +constexpr int SAMPLES_PER_BLOCK = 28; + +void encode_block_with_filter(int filter_idx, + const s16* samples_in, + s32* out, + const s32* prev_samples_in) { + constexpr s32 f1[5] = {0, 60, 115, 98, 122}; + constexpr s32 f2[5] = {0, 0, -52, -55, -60}; + s32 prev_samples[2] = {prev_samples_in[0], prev_samples_in[1]}; + + for (int sample_idx = 0; sample_idx < SAMPLES_PER_BLOCK; sample_idx++) { + s32 sample = samples_in[sample_idx]; + s32 delta = + sample - (prev_samples[0] * f1[filter_idx] + prev_samples[1] * f2[filter_idx] + 32) / 64; + out[sample_idx] = delta; + prev_samples[1] = prev_samples[0]; + prev_samples[0] = sample; + } +} + +int get_shift_error(int shift, const s32* samples, bool /*debug*/) { + int result = 0; + + for (int sample_idx = 0; sample_idx < SAMPLES_PER_BLOCK; sample_idx++) { + int left_shift = 32 - (12 + 4 - shift); + assert(left_shift >= 0); + s32 sample_left = samples[sample_idx] << left_shift; + s32 sample_right = sample_left >> (32 - 4); + s32 sample_compressed = sample_right << (12 - shift); + + s32 err = std::abs(sample_compressed - samples[sample_idx]); + + result += err; + } + return result; +} + +int get_max_bits(s32 value) { + int result = 0; + if (value >= 0) { + int last = 1; + while (value) { + result++; + last = value & 1; + value >>= 1; + } + if (last) { + result++; + } + } else { + int last = 0; + while (value != -1) { + result++; + last = value & 1; + value >>= 1; + } + if (!last) { + result++; + } + } + return result; +} + +int break_filter_ties(s32* errors, s32* filter_shifts) { + s32 best_error = INT32_MAX; + + for (int filter_idx = 0; filter_idx < 5; filter_idx++) { + if (errors[filter_idx] < best_error) { + best_error = errors[filter_idx]; + } + } + + s32 best_shift = INT32_MAX; + int best_filter = -1; + for (int filter_idx = 5; filter_idx-- > 0;) { + if (errors[filter_idx] == best_error) { + if (filter_shifts[filter_idx] <= best_shift) { + best_shift = filter_shifts[filter_idx]; + best_filter = filter_idx; + } + } + } + + return best_filter; +} + +void test_encode_adpcm(const std::vector& samples, + const std::vector& filter_debug, + const std::vector& shift_debug) { + // the data is made of blocks. + // Each block decodes to 28 samples. + // each block has a shift and FIR filter. + // the window is continuous across blocks. + + // we could try all combinations of filters / shifts and pick the best, but that's slow and + // we don't know how to break ties if multiple are the same. + // we will try all 5 filters, then be smart about picking the best shift from there. + + // filter coefficients. + // there are 5x FIR filters that you can pick between. + + // last two samples from chosen encoding of the previous block + // init to 0, like the decoder + s32 prev_block_samples[2] = {0, 0}; + + // TODO - this will drop some samples at the end, if we don't use a multiple of 28. + // probably best to go back and pad with zeros or something. + int block_count = samples.size() / SAMPLES_PER_BLOCK; + + for (int block_idx = 0; block_idx < block_count; block_idx++) { + // try each filter + s32 pre_shift_samples_per_filter[5][SAMPLES_PER_BLOCK]; + for (int filter_idx = 0; filter_idx < 5; filter_idx++) { + encode_block_with_filter(filter_idx, samples.data() + SAMPLES_PER_BLOCK * block_idx, + pre_shift_samples_per_filter[filter_idx], prev_block_samples); + } + + // this is somewhat arbitrary, but we will require that the largest delta in the previous encode + // can be represented. + + s32 filter_errors[5] = {0, 0, 0, 0, 0}; + s32 filter_shifts[5] = {-1, -1, -1, -1}; + for (int filter_idx = 0; filter_idx < 5; filter_idx++) { + // find the largest value + s32 max_sample = INT32_MIN; + s32 min_sample = INT32_MAX; + + bool debug = block_idx == 10966 && filter_idx == 4; + + for (int sample_idx = 0; sample_idx < SAMPLES_PER_BLOCK; sample_idx++) { + s32 s = pre_shift_samples_per_filter[filter_idx][sample_idx]; + max_sample = std::max(s, max_sample); + min_sample = std::min(s, min_sample); + } + + if (debug) { + fmt::print("Range: {}\n", max_sample - min_sample); + } + + // see how many bits we need and pick shift. + auto bits_for_max = std::max(4, std::max(get_max_bits(min_sample), get_max_bits(max_sample))); + + filter_shifts[filter_idx] = 4 + 12 - bits_for_max; + + filter_errors[filter_idx] = get_shift_error(filter_shifts[filter_idx], + pre_shift_samples_per_filter[filter_idx], debug); + + if (filter_errors[filter_idx] == 0) { + while (filter_shifts[filter_idx] >= 0) { + int next_error = get_shift_error(filter_shifts[filter_idx] - 1, + pre_shift_samples_per_filter[filter_idx], false); + if (next_error == 0) { + filter_shifts[filter_idx]--; + } else { + break; + } + } + } + } + + int best_filter = break_filter_ties(filter_errors, filter_shifts); + s32 best_shift = filter_shifts[best_filter]; + + if (filter_errors[best_filter] || best_filter != filter_debug[block_idx] || + best_shift != shift_debug[block_idx]) { + fmt::print("Block {} me {}, {} : answer {} {}: ERR {}\n", block_idx, best_filter, best_shift, + filter_debug[block_idx], shift_debug[block_idx], filter_errors[best_filter]); + fmt::print("filter errors:\n"); + for (int i = 0; i < 5; i++) { + fmt::print(" [{}] {} {}\n", i, filter_errors[i], filter_shifts[i]); + } + fmt::print("prev: {} {}\n", prev_block_samples[0], prev_block_samples[1]); + assert(false); + } + + prev_block_samples[0] = samples.at(block_idx * 28 + 27); + prev_block_samples[1] = samples.at(block_idx * 28 + 26); + + } // end loop over blocks +} diff --git a/common/audio/audio_formats.h b/common/audio/audio_formats.h new file mode 100644 index 0000000000..ea72c50f5e --- /dev/null +++ b/common/audio/audio_formats.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include "common/util/BinaryReader.h" +#include "common/common_types.h" + +// The header data for a simple wave file +struct WaveFileHeader { + // wave file header + char chunk_id[4]; + s32 chunk_size; + char format[4]; + + // format chunk + char subchunk1_id[4]; + s32 subchunk1_size; + s16 aud_format; + s16 num_channels; + s32 sample_rate; + s32 byte_rate; + s16 block_align; + s16 bits_per_sample; + + // data chunk + char subchunk2_id[4]; + s32 subchunk2_size; +}; + +void write_wave_file_mono(const std::vector& samples, + s32 sample_rate, + const std::string& name); + +std::vector decode_adpcm(BinaryReader& reader); + +std::vector encode_adpcm(const std::vector& samples); \ No newline at end of file diff --git a/common/util/BinaryReader.h b/common/util/BinaryReader.h index b548c3366f..d6d43b0aae 100644 --- a/common/util/BinaryReader.h +++ b/common/util/BinaryReader.h @@ -6,31 +6,35 @@ */ #include +#include #include "common/util/assert.h" +#include "common/common_types.h" #include class BinaryReader { public: - explicit BinaryReader(const std::vector& _buffer) : buffer(_buffer) {} + explicit BinaryReader(const std::vector& _buffer) : m_buffer(_buffer) {} template T read() { - assert(seek + sizeof(T) <= buffer.size()); - T& obj = *(T*)(buffer.data() + seek); - seek += sizeof(T); + assert(m_seek + sizeof(T) <= m_buffer.size()); + T obj; + memcpy(&obj, m_buffer.data() + m_seek, sizeof(T)); + m_seek += sizeof(T); return obj; } void ffwd(int amount) { - seek += amount; - assert(seek <= buffer.size()); + m_seek += amount; + assert(m_seek <= m_buffer.size()); } - uint32_t bytes_left() const { return buffer.size() - seek; } - uint8_t* here() { return buffer.data() + seek; } - uint32_t get_seek() const { return seek; } + uint32_t bytes_left() const { return m_buffer.size() - m_seek; } + uint8_t* here() { return m_buffer.data() + m_seek; } + uint32_t get_seek() const { return m_seek; } + void set_seek(u32 seek) { m_seek = seek; } private: - std::vector buffer; - uint32_t seek = 0; + std::vector m_buffer; + uint32_t m_seek = 0; }; diff --git a/decompiler/CMakeLists.txt b/decompiler/CMakeLists.txt index 4a9ed5f51e..abf5e3986b 100644 --- a/decompiler/CMakeLists.txt +++ b/decompiler/CMakeLists.txt @@ -18,6 +18,7 @@ add_library( data/dir_tpages.cpp data/game_count.cpp data/game_text.cpp + data/streamed_audio.cpp data/StrFileReader.cpp data/tpage.cpp diff --git a/decompiler/ObjectFile/ObjectFileDB.cpp b/decompiler/ObjectFile/ObjectFileDB.cpp index e318eeca6a..b36b13b085 100644 --- a/decompiler/ObjectFile/ObjectFileDB.cpp +++ b/decompiler/ObjectFile/ObjectFileDB.cpp @@ -157,7 +157,7 @@ ObjectFileDB::ObjectFileDB(const std::vector& _dgos, lg::info("ObjectFileDB Initialized\n"); if (obj_files_by_name.empty()) { - lg::die( + lg::error( "No object files have been added. Check that there are input files and the allowed_objects " "list."); } diff --git a/decompiler/config.cpp b/decompiler/config.cpp index dccb5f247a..45f9d2c64f 100644 --- a/decompiler/config.cpp +++ b/decompiler/config.cpp @@ -33,6 +33,9 @@ Config read_config_file(const std::string& path_to_config_file) { config.dgo_names = inputs_json.at("dgo_names").get>(); config.object_file_names = inputs_json.at("object_file_names").get>(); config.str_file_names = inputs_json.at("str_file_names").get>(); + config.audio_dir_file_name = inputs_json.at("audio_dir_file_name").get(); + config.streamed_audio_file_names = + inputs_json.at("streamed_audio_file_names").get>(); if (cfg.contains("obj_file_name_map_file")) { config.obj_file_name_map_file = cfg.at("obj_file_name_map_file").get(); diff --git a/decompiler/config.h b/decompiler/config.h index c7af0b1be0..7b5161896b 100644 --- a/decompiler/config.h +++ b/decompiler/config.h @@ -68,6 +68,9 @@ struct Config { std::vector object_file_names; std::vector str_file_names; + std::string audio_dir_file_name; + std::vector streamed_audio_file_names; + std::string obj_file_name_map_file; bool disassemble_code = false; diff --git a/decompiler/config/jak1_ntsc_black_label/inputs.jsonc b/decompiler/config/jak1_ntsc_black_label/inputs.jsonc index 6a816ad2ba..40138334a2 100644 --- a/decompiler/config/jak1_ntsc_black_label/inputs.jsonc +++ b/decompiler/config/jak1_ntsc_black_label/inputs.jsonc @@ -256,5 +256,18 @@ "TEXT/4COMMON.TXT", "TEXT/5COMMON.TXT", "TEXT/6COMMON.TXT" + ], + + // uncomment the next line to extract audio to wave files. + //"audio_dir_file_name": "VAG/VAGDIR.AYB", + "audio_dir_file_name" : "", + + "streamed_audio_file_names": [ + "VAG/VAGWAD.ENG", + "VAG/VAGWAD.FRE", + "VAG/VAGWAD.GER", + "VAG/VAGWAD.ITA", + "VAG/VAGWAD.JAP", + "VAG/VAGWAD.SPA" ] } diff --git a/decompiler/data/streamed_audio.cpp b/decompiler/data/streamed_audio.cpp new file mode 100644 index 0000000000..8636b6e3de --- /dev/null +++ b/decompiler/data/streamed_audio.cpp @@ -0,0 +1,222 @@ + +#include "common/log/log.h" +#include "common/util/FileUtil.h" +#include "common/util/BinaryReader.h" +#include "common/audio/audio_formats.h" +#include "third-party/fmt/core.h" +#include "third-party/json.hpp" + +#include "streamed_audio.h" + +namespace decompiler { + +// number of bytes per "audio page" in the VAG directory file. +constexpr int AUDIO_PAGE_SIZE = 2048; + +// Swap endian of 32-bit value. +uint32_t swap32(uint32_t in) { + return ((in << 24) | ((in & 0xff00) << 8) | ((in & 0xff0000) >> 8) | (in >> 24)); +} + +/*! + * A processed version of the VAGDIR file containing a map from 8-char name to location in the + * WAD files. + */ +struct AudioDir { + struct Entry { + std::string name; + 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) { + fmt::print("\"{}\" 0x{:07x} - 0x{:07x}\n", 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 version; + u32 zero; + u32 channel_size; + u32 sample_rate; + u32 z[3]; + char name[16]; + + VagFileHeader swapped_endian() const { + VagFileHeader result(*this); + result.version = swap32(result.version); + result.channel_size = swap32(result.channel_size); + 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'; + fmt::print("{}{}{}{} v {} zero {} chan {} samp {} z {} {} {} name {}\n", magic[0], magic[1], + magic[2], magic[3], version, zero, channel_size, sample_rate, z[0], z[1], z[2], + temp_name); + } +}; + +/*! + * Read the DIR file into an AudioDir + */ +AudioDir read_audio_dir(const std::string& path) { + // matches the format in file. + struct DirEntry { + char name[8]; + u32 value; + }; + auto data = file_util::read_binary_file(path); + lg::info("Got {} bytes of audio dir.\n", 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; + + 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); + } + + return result; +} + +std::string remove_trailing_spaces(const std::string& in) { + auto short_name = in; + while (!short_name.empty() && short_name.back() == ' ') { + short_name.pop_back(); + } + return short_name; +} + +struct AudioFileInfo { + std::string filename; + double length_seconds; +}; + +AudioFileInfo process_audio_file(const std::vector& data, + const std::string& name, + const std::string& suffix) { + BinaryReader reader(data); + + auto header = reader.read(); + if (header.magic[0] == 'V') { + header = header.swapped_endian(); + } else { + assert(false); + } + header.debug_print(); + + for (int i = 0; i < 16; i++) { + assert(reader.read() == 0); + } + + std::vector decoded_samples = decode_adpcm(reader); + + while (reader.bytes_left()) { + assert(reader.read() == 0); + } + + auto file_name = fmt::format("{}_{}.wav", remove_trailing_spaces(name), suffix); + write_wave_file_mono(decoded_samples, header.sample_rate, + file_util::get_file_path({"assets", "streaming_audio", file_name})); + + std::string vag_filename; + for (int i = 0; i < 16; i++) { + if (header.name[i]) { + vag_filename.push_back(header.name[i]); + } + } + return {vag_filename, (double)decoded_samples.size() / header.sample_rate}; +} + +void process_streamed_audio(const std::string& dir, const std::vector& audio_files) { + lg::info("Streaming audio: {}\n", dir); + file_util::create_dir_if_needed(file_util::get_file_path({"assets", "streaming_audio"})); + auto dir_data = read_audio_dir(file_util::get_file_path({"iso_data", dir})); + double audio_len = 0.f; + + std::vector langs; + std::vector> filename_data; + for (auto& e : dir_data.entries) { + std::vector placeholders = {remove_trailing_spaces(e.name)}; + for (size_t i = 0; i < audio_files.size(); i++) { + placeholders.push_back("????"); + } + filename_data.push_back(placeholders); + } + + for (size_t lang_id = 0; lang_id < audio_files.size(); lang_id++) { + auto& file = audio_files[lang_id]; + auto wad_data = file_util::read_binary_file(file_util::get_file_path({"iso_data", file})); + auto suffix = std::filesystem::path(file).extension().u8string().substr(1); + langs.push_back(suffix); + dir_data.set_file_size(wad_data.size()); + for (int i = 3; 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(audio_data, dir_data.entries.at(i).name, suffix); + audio_len += info.length_seconds; + filename_data[i][lang_id + 1] = info.filename; + } + } + + nlohmann::json file_list; + file_list["names"] = filename_data; + file_list["languages"] = langs; + + file_util::write_text_file( + file_util::get_file_path({"assets", "streaming_audio", "file_list.txt"}), file_list.dump(2)); +} + +} // namespace decompiler diff --git a/decompiler/data/streamed_audio.h b/decompiler/data/streamed_audio.h new file mode 100644 index 0000000000..b98d47c4e7 --- /dev/null +++ b/decompiler/data/streamed_audio.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +namespace decompiler { +void process_streamed_audio(const std::string& dir, const std::vector& audio_files); +} \ No newline at end of file diff --git a/decompiler/main.cpp b/decompiler/main.cpp index 45a4990c13..206251cffd 100644 --- a/decompiler/main.cpp +++ b/decompiler/main.cpp @@ -6,6 +6,7 @@ #include "config.h" #include "common/util/FileUtil.h" #include "common/versions.h" +#include "decompiler/data/streamed_audio.h" int main(int argc, char** argv) { using namespace decompiler; @@ -119,6 +120,10 @@ int main(int argc, char** argv) { } } + if (!config.audio_dir_file_name.empty()) { + process_streamed_audio(config.audio_dir_file_name, config.streamed_audio_file_names); + } + lg::info("Disassembly has completed successfully."); return 0; }