diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e946ede46..57415f2d95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -278,9 +278,6 @@ build_third_party_lib(fmt fmt) # build common library add_subdirectory(common) -# build decompiler -add_subdirectory(decompiler) - # cubeb - audio build_third_party_lib(cubeb cubeb) @@ -313,6 +310,9 @@ add_subdirectory(goalc) # build standalone tools add_subdirectory(tools) +# build decompiler +add_subdirectory(decompiler) + # build the gtest libraries if(WIN32) set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) diff --git a/decompiler/CMakeLists.txt b/decompiler/CMakeLists.txt index e5aac4f6a3..9f73503645 100644 --- a/decompiler/CMakeLists.txt +++ b/decompiler/CMakeLists.txt @@ -101,6 +101,7 @@ target_link_libraries(decomp stb_image xdelta3 tiny_gltf + sound ) add_executable(decompiler @@ -112,7 +113,9 @@ target_link_libraries(decompiler lzokay fmt stb_image - tiny_gltf) + tiny_gltf + sound +) add_executable(extractor @@ -125,4 +128,6 @@ target_link_libraries(extractor fmt compiler stb_image - tiny_gltf) + tiny_gltf + sound +) \ No newline at end of file diff --git a/decompiler/config.cpp b/decompiler/config.cpp index 55322cd027..fbd4696df8 100644 --- a/decompiler/config.cpp +++ b/decompiler/config.cpp @@ -336,6 +336,9 @@ Config make_config_via_json(nlohmann::json& json) { if (json.contains("rip_streamed_audio")) { config.rip_streamed_audio = json.at("rip_streamed_audio").get(); } + if (json.contains("rip_music")) { + config.rip_music = json.at("rip_music").get(); + } if (inputs_json.contains("animated_textures")) { config.animated_textures = diff --git a/decompiler/config.h b/decompiler/config.h index 6282d798e7..e5e4a5cd66 100644 --- a/decompiler/config.h +++ b/decompiler/config.h @@ -177,6 +177,7 @@ struct Config { bool levels_extract; bool save_texture_pngs = false; bool rip_streamed_audio = false; + bool rip_music = false; DecompileHacks hacks; diff --git a/decompiler/config/jak1/jak1_config.jsonc b/decompiler/config/jak1/jak1_config.jsonc index 912f662778..5fc78422d8 100644 --- a/decompiler/config/jak1/jak1_config.jsonc +++ b/decompiler/config/jak1/jak1_config.jsonc @@ -127,9 +127,12 @@ // save game textures as .png files to decompiler_out//textures "save_texture_pngs": false, - // whether or not to dump out streamed audio files to decompiler_out//audio + // whether or not to dump out streamed audio files to decompiler_out//audio/sfx and decompiler_out//audio/voice_lines "rip_streamed_audio": false, + // whether or not to dump out music files to decompiler_out//audio/music + "rip_music": false, + //////////////////////////// // PATCHING OPTIONS //////////////////////////// diff --git a/decompiler/config/jak2/jak2_config.jsonc b/decompiler/config/jak2/jak2_config.jsonc index 30f4ca8722..bc0062918b 100644 --- a/decompiler/config/jak2/jak2_config.jsonc +++ b/decompiler/config/jak2/jak2_config.jsonc @@ -133,9 +133,12 @@ // save game textures as .png files to decompiler_out//textures "save_texture_pngs": false, - // whether or not to dump out streamed audio files to decompiler_out//audio + // whether or not to dump out streamed audio files to decompiler_out//audio/sfx and decompiler_out//audio/voice_lines "rip_streamed_audio": false, + // whether or not to dump out music files to decompiler_out//audio/music + "rip_music": false, + //////////////////////////// // PATCHING OPTIONS //////////////////////////// diff --git a/decompiler/config/jak3/jak3_config.jsonc b/decompiler/config/jak3/jak3_config.jsonc index 65aad1ab57..93044c32b2 100644 --- a/decompiler/config/jak3/jak3_config.jsonc +++ b/decompiler/config/jak3/jak3_config.jsonc @@ -132,7 +132,7 @@ // save game textures as .png files to decompiler_out//textures "save_texture_pngs": false, - // whether or not to dump out streamed audio files to decompiler_out//audio + // whether or not to dump out streamed audio files to decompiler_out//audio/sfx and decompiler_out//audio/voice_lines "rip_streamed_audio": false, //////////////////////////// diff --git a/decompiler/config/jakx/jakx_config.jsonc b/decompiler/config/jakx/jakx_config.jsonc index 6154f698b5..20a559e855 100644 --- a/decompiler/config/jakx/jakx_config.jsonc +++ b/decompiler/config/jakx/jakx_config.jsonc @@ -110,7 +110,7 @@ "rip_collision": false, // save game textures as .png files to decompiler_out//textures "save_texture_pngs": false, - // whether or not to dump out streamed audio files to decompiler_out//audio + // whether or not to dump out streamed audio files to decompiler_out//audio/sfx and decompiler_out//audio/voice_lines "rip_streamed_audio": false, //////////////////////////// // PATCHING OPTIONS diff --git a/decompiler/data/streamed_audio.cpp b/decompiler/data/streamed_audio.cpp index 054dcede25..164584c2e9 100644 --- a/decompiler/data/streamed_audio.cpp +++ b/decompiler/data/streamed_audio.cpp @@ -1,11 +1,17 @@ - #include "streamed_audio.h" +#include + #include "common/audio/audio_formats.h" #include "common/log/log.h" #include "common/util/BinaryReader.h" #include "common/util/FileUtil.h" -#include "common/util/string_util.h" + +#include "game/sound/989snd/fakeplayer.h" +#include "game/sound/989snd/musicbank.h" +#include "game/sound/989snd/sfxblock.h" +#include "game/sound/common/flava.h" +#include "game/sound/common/sound_types.h" #include "fmt/format.h" #include "third-party/json.hpp" @@ -253,6 +259,203 @@ AudioFileInfo process_audio_file(const fs::path& output_folder, ((double)left_samples.size() + (double)right_samples.size()) / header.sample_rate}; } +size_t find_bank_offset(const std::vector& d) { + auto u32_at = [&](size_t p) { + u32 v = 0; + std::memcpy(&v, d.data() + p, sizeof(v)); + return v; + }; + auto is_fourcc = [&](size_t p, const char* cc) { + return p + 4 <= d.size() && d[p] == static_cast(cc[0]) && + d[p + 1] == static_cast(cc[1]) && d[p + 2] == static_cast(cc[2]) && + d[p + 3] == static_cast(cc[3]); + }; + + for (size_t off = 0; off + 16 <= d.size(); off += 2048) { + const u32 type = u32_at(off); + if (type != 1 && type != 3) { + continue; + } + const u32 num_chunks = u32_at(off + 4); + if (num_chunks < 2 || num_chunks > 3 || + off + 8 + static_cast(num_chunks) * 8 > d.size()) { + continue; + } + bool chunks_ok = true; + for (u32 i = 0; i < num_chunks; i++) { + const u32 coff = u32_at(off + 8 + i * 8); + const u32 csz = u32_at(off + 8 + i * 8 + 4); + if (off + static_cast(coff) + csz > d.size()) { + chunks_ok = false; + break; + } + } + if (chunks_ok && + (is_fourcc(off + u32_at(off + 8), "SBlk") || is_fourcc(off + u32_at(off + 8), "SBv2"))) { + return off; + } + } + return SIZE_MAX; +} + +void parse_jak1_name_table(const std::vector& d, + size_t limit, + std::string& bank_name, + std::vector& names) { + bank_name.clear(); + names.clear(); + if (limit < 0x18 || limit > d.size()) { + return; + } + auto read_name16 = [&](size_t p) { + char buf[17]; + std::memcpy(buf, d.data() + p, 16); + buf[16] = 0; + return std::string(buf); + }; + u32 count = 0; + std::memcpy(&count, d.data() + 0x14, sizeof(count)); + + bank_name = read_name16(0); + size_t pos = 0x18; + for (u32 i = 0; i < count && pos + 20 <= limit; i++, pos += 20) { + names.push_back(read_name16(pos)); + } +} + +void process_music(const fs::path& output_path, const fs::path& input_dir) { + auto dir = input_dir / "MUS"; + std::vector musFiles = file_util::find_files_in_dir(dir, std::regex(".*\\.MUS")); + double audio_len = 0.f; + + const flava::FlavaSet dummyFlava = {0, {{"default", 0}}, false}; + + // Create a fake player that will generate the samples to play the music tracks exactly as they + // are in the games :) + snd::FakePlayer fakeplayer; + std::vector left_samples, right_samples; + // Generate at most five minutes worth of music for each variation of each track. + const s64 FIVE_MINUTES = snd::SAMPLE_RATE * 300; + left_samples.reserve(FIVE_MINUTES); + right_samples.reserve(FIVE_MINUTES); + for (auto& mus : musFiles) { + auto mus_name = remove_trailing_spaces(mus.filename().replace_extension("").string()); + auto data = file_util::read_binary_file(mus); + + // Skip TWEAKVAL which is not a real musicbank + if (mus_name == "TWEAKVAL") + continue; + + std::vector sfx_names; + const size_t bank_offset = find_bank_offset(data); // Find where the music bank starts + if (bank_offset == SIZE_MAX) { + lg::error("'{}' is not a valid .MUS bank.", mus_name); + return; + } + + auto output_folder = output_path / mus_name; + file_util::create_dir_if_needed(output_folder); + snd::MusicBank* bank = + (snd::MusicBank*)fakeplayer.LoadBank(std::span(data).subspan(bank_offset)); + + auto flava_set = flava::lookup(mus_name); + if (!flava_set) + flava_set = &dummyFlava; + for (auto& flavaVariant : flava_set->variants) { + const auto variantName = std::string(flavaVariant.name); + if (variantName == "none") + continue; + // Turn off repetition for music. 0 means sound will repeat, 1 means it won't. + bank->Sounds[0].Repeats = 1; + fakeplayer.PlaySound(bank, 0, snd::MAX_VOLUME, 0, 0, 0); + if (flavaVariant.value > 0) { + // Play for a tenth of a second before setting the register, then clear left/right samples + // so they don't get added to track. This seems to help ensure that the correct flava + // actually plays. + fakeplayer.Tick(left_samples, right_samples, snd::SAMPLE_RATE / 10); + fakeplayer.SetSoundReg(flava_set->reg, flavaVariant.value); + left_samples.clear(); + right_samples.clear(); + } + fakeplayer.Tick(left_samples, right_samples, FIVE_MINUTES); + + auto file_name = variantName == "default" ? mus_name : mus_name + '_' + variantName; + file_name = fmt::format("{}.wav", file_name); + write_wave_file(left_samples, right_samples, snd::SAMPLE_RATE, output_folder / file_name); + audio_len += left_samples.size() / (float)snd::SAMPLE_RATE; + + left_samples.clear(); + right_samples.clear(); + fakeplayer.StopSound(); + } + + fakeplayer.UnloadBank(bank); + lg::info("File {}, total {:.2f} minutes", mus.filename().string(), audio_len / 60.0); + } +} + +void process_sfx(const fs::path& output_path, const fs::path& input_dir) { + auto dir = input_dir / "SBK"; + std::vector sbkFiles = file_util::find_files_in_dir(dir, std::regex(".*\\.SBK")); + double audio_len = 0.f; + + // Create a fake player that will generate the samples to play the sounds exactly as they are in + // the games :) + snd::FakePlayer fakeplayer; + std::vector left_samples, right_samples; + const s64 TEN_SECONDS = snd::SAMPLE_RATE * 10; + left_samples.reserve(TEN_SECONDS); + right_samples.reserve(TEN_SECONDS); + for (auto& sbk : sbkFiles) { + auto sbk_name = sbk.filename().replace_extension(""); + auto data = file_util::read_binary_file(sbk); + + const size_t bank_offset = find_bank_offset(data); // Find where the sfx bank starts + std::vector sfx_names; + if (bank_offset == SIZE_MAX) { + lg::error("'{}' is not a valid .SBK bank.", sbk_name.string()); + return; + } + if (bank_offset > 0) { + std::string bank_name; + parse_jak1_name_table(data, bank_offset, bank_name, sfx_names); + } + + auto output_folder = output_path / sbk_name; + file_util::create_dir_if_needed(output_folder); + snd::SFXBlock* block = + (snd::SFXBlock*)fakeplayer.LoadBank(std::span(data).subspan(bank_offset)); + + std::map names_by_index; + for (const auto& [name, index] : block->Names) { + names_by_index.insert({index, name}); + } + + for (u32 sound_index = 0; sound_index < block->Sounds.size(); ++sound_index) { + fakeplayer.PlaySound(block, sound_index, snd::MAX_VOLUME, 0, 0, 0); + fakeplayer.Tick(left_samples, right_samples, TEN_SECONDS); + + std::string name; + if (auto it = names_by_index.find(sound_index); it != names_by_index.end()) + name = it->second; + else if (sound_index < sfx_names.size()) + name = sfx_names[sound_index]; + else + name = "sound" + std::to_string(sound_index); + + auto file_name = fmt::format("{}.wav", remove_trailing_spaces(name)); + write_wave_file(left_samples, right_samples, snd::SAMPLE_RATE, output_folder / file_name); + audio_len += left_samples.size() / (float)snd::SAMPLE_RATE; + + left_samples.clear(); + right_samples.clear(); + } + + fakeplayer.UnloadBank(block); + lg::info("File {}, total {:.2f} minutes", sbk.filename().string(), audio_len / 60.0); + } +} + void process_streamed_audio(const decompiler::Config& config, const fs::path& output_path, const fs::path& input_dir, diff --git a/decompiler/data/streamed_audio.h b/decompiler/data/streamed_audio.h index f7a0ae1d9d..944487391a 100644 --- a/decompiler/data/streamed_audio.h +++ b/decompiler/data/streamed_audio.h @@ -12,4 +12,7 @@ void process_streamed_audio(const Config& config, const fs::path& output_path, const fs::path& input_dir, const std::vector& audio_files); -} + +void process_sfx(const fs::path& output_path, const fs::path& input_dir); +void process_music(const fs::path& output_path, const fs::path& input_dir); +} // namespace decompiler diff --git a/decompiler/decompilation_process.cpp b/decompiler/decompilation_process.cpp index 29b3c4536b..033b879774 100644 --- a/decompiler/decompilation_process.cpp +++ b/decompiler/decompilation_process.cpp @@ -308,11 +308,20 @@ int run_decompilation_process(decompiler::Config config, lg::info("[Mem] After extraction: {} MB", get_peak_rss() / (1024 * 1024)); - if (config.rip_streamed_audio) { + if (config.rip_streamed_audio || config.rip_music) { auto streaming_audio_out = out_folder / "audio"; file_util::create_dir_if_needed(streaming_audio_out); - process_streamed_audio(config, streaming_audio_out, in_folder, - config.streamed_audio_file_names); + + if (config.rip_streamed_audio) { + process_streamed_audio(config, streaming_audio_out / "voice_lines", in_folder, + config.streamed_audio_file_names); + process_sfx(streaming_audio_out / "sfx", in_folder); + } + // Rip music for jak 1 or jak 2 + if (config.rip_music && + (config.game_version == GameVersion::Jak1 || config.game_version == GameVersion::Jak2)) { + process_music(streaming_audio_out / "music", in_folder); + } } lg::info("Decompiler has finished successfully in {:.2f} seconds.", decomp_timer.getSeconds()); diff --git a/game/sound/989snd/fakeplayer.cpp b/game/sound/989snd/fakeplayer.cpp new file mode 100644 index 0000000000..f5a1d71ed4 --- /dev/null +++ b/game/sound/989snd/fakeplayer.cpp @@ -0,0 +1,210 @@ +#include "fakeplayer.h" + +#include "sfxblock.h" + +#include "game/sound/common/sound_types.h" + +#include "fmt/format.h" + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif +#include "common/log/log.h" + +namespace snd { + +FakePlayer::FakePlayer() : mVmanager(mSynth) {} + +FakePlayer::~FakePlayer() {} + +bool FakePlayer::Tick(std::vector& leftStream, std::vector& rightStream, long samples) { + static int htick = 200; + static int stick = snd::SAMPLE_RATE; + + for (long i = 0; i < samples; i++) { + // The handlers expect to tick at 240hz + // 48000/240 = 200 + if (htick == 200) { + mTick++; + + // m_handler->Tick returns true if the handler is done. + if (m_handler && m_handler->Tick()) { + m_handler = NULL; // deletes the handler. + return true; + } + + htick = 0; + } + + if (stick == snd::SAMPLE_RATE) { + // fmt::print("{} handlers active\n", m_handlers.size()); + stick = 0; + } + + stick++; + htick++; + auto s16Output = mSynth.Tick(); + leftStream.push_back(s16Output.left); + rightStream.push_back(s16Output.right); + } + return false; +} + +u32 FakePlayer::PlaySound(BankHandle bank_id, u32 sound_id, s32 vol, s32 pan, s32 pm, s32 pb) { + auto bank = mLoader.GetBankByHandle(bank_id); + if (bank == nullptr) { + lg::error("play_sound: Bank {} does not exist", static_cast(bank_id)); + return 0; + } + + auto handler = bank->MakeHandler(mVmanager, sound_id, vol, pan, pm, pb, GetTick(), 0); + if (!handler.has_value()) { + return 0; + } + + handler.value()->m_sound_handle = 0; + m_handler = std::move(handler.value()); + // fmt::print("play_sound {}:{} - {}\n", bank_id, sound_id, handle); + + return 0; +} + +u32 FakePlayer::PlaySoundByName(BankHandle bank_id, + char* bank_name, + char* sound_name, + s32 vol, + s32 pan, + s32 pm, + s32 pb) { + SoundBank* bank = nullptr; + if (bank_id == 0 && bank_name != nullptr) { + bank = mLoader.GetBankByName(bank_name); + } else if (bank_id != 0) { + bank = mLoader.GetBankByHandle(bank_id); + } else { + bank = mLoader.GetBankWithSound(sound_name); + } + + if (bank == nullptr) { + return 0; + } + + auto sound = bank->GetSoundByName(sound_name); + if (sound.has_value()) { + return PlaySound(bank, sound.value(), vol, pan, pm, pb); + } + + return 0; +} + +void FakePlayer::StopSound() { + if (m_handler) + m_handler->Stop(); + m_handler = nullptr; +} + +u32 FakePlayer::GetSoundID(u32 sound_handle) { + return m_handler ? m_handler->SoundID() : -1; +} + +void FakePlayer::SetSoundReg(u8 reg, u8 value) { + if (m_handler) + m_handler->SetRegister(reg, value); +} + +u8 FakePlayer::GetSoundGroup(u32 sound_id) { + return m_handler ? m_handler->Group() : -1; +} + +void FakePlayer::SetMasterVolume(u32 group, s32 volume) { + if (volume > snd::MAX_VOLUME) + volume = snd::MAX_VOLUME; + + if (volume < 0) + volume = 0; + + if (group == 15) + return; + + mVmanager.SetMasterVol(group, volume); + + // Master volume + if (group == 16) { + mSynth.SetMasterVol(0x3ffff * volume / snd::MAX_VOLUME); + } +} + +BankHandle FakePlayer::LoadBank(std::span bank) { + return mLoader.BankLoad(bank); +} + +void FakePlayer::UnloadBank(BankHandle bank_handle) { + auto* bank = mLoader.GetBankByHandle(bank_handle); + if (bank == nullptr) + return; + + mLoader.UnloadBank(bank_handle); +} + +void FakePlayer::SetPanTable(VolPair* pantable) { + mVmanager.SetPanTable(pantable); +} + +void FakePlayer::SetPlaybackMode(s32 mode) { + mVmanager.SetPlaybackMode(mode); +} + +void FakePlayer::SetSoundVolPan(s32 sound_id, s32 vol, s32 pan) { + if (m_handler) + m_handler->SetVolPan(vol, pan); +} + +void FakePlayer::SetSoundPmod(s32 sound_handle, s32 mod) { + if (m_handler) + m_handler->SetPMod(mod); +} + +s32 FakePlayer::GetSoundUserData(BankHandle block_handle, + char* block_name, + s32 sound_id, + char* sound_name, + SFXUserData* dst) { + SoundBank* bank = nullptr; + if (block_handle == nullptr && block_name != nullptr) { + bank = mLoader.GetBankByName(block_name); + } else if (block_handle != nullptr) { + bank = mLoader.GetBankByHandle(block_handle); + } else { + bank = mLoader.GetBankWithSound(sound_name); + } + + if (bank == nullptr) { + return 0; + } + + if (sound_id == -1) { + auto sound = bank->GetSoundByName(sound_name); + if (sound.has_value()) { + sound_id = sound.value(); + } else { + return 0; + } + } + + auto ud = bank->GetSoundUserData(sound_id); + if (ud.has_value()) { + dst->data[0] = ud.value()->data[0]; + dst->data[1] = ud.value()->data[1]; + dst->data[2] = ud.value()->data[2]; + dst->data[3] = ud.value()->data[3]; + return 1; + } else { + return 0; + } + + return 0; +} + +} // namespace snd diff --git a/game/sound/989snd/fakeplayer.h b/game/sound/989snd/fakeplayer.h new file mode 100644 index 0000000000..67cae3ddda --- /dev/null +++ b/game/sound/989snd/fakeplayer.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +#include "ame_handler.h" +#include "loader.h" +#include "sound_handler.h" + +#include "common/common_types.h" + +#include "../common/synth.h" +#include "game/sound/989snd/vagvoice.h" + +// This class is adapted from Player.cpp +// Instead of being used to playback audio, it is used to generate the audio samples +// that would ordinarily be generated during playback. These samples +// (made by calling the Tick function) are then written to .wav files. + +namespace snd { + +class FakePlayer { + public: + FakePlayer(); + ~FakePlayer(); + FakePlayer(const FakePlayer&) = delete; + FakePlayer operator=(const FakePlayer&) = delete; + + BankHandle LoadBank(std::span bank); + + u32 PlaySound(BankHandle bank, u32 sound, s32 vol, s32 pan, s32 pm, s32 pb); + void StopSound(); + u32 PlaySoundByName(BankHandle bank, + char* bank_name, + char* sound_name, + s32 vol, + s32 pan, + s32 pm, + s32 pb); + void SetSoundReg(u8 reg, u8 value); + u8 GetSoundGroup(u32 sound_id); + void SetGlobalExcite(u8 value) { GlobalExcite = value; }; + void SetMasterVolume(u32 group, s32 volume); + void UnloadBank(BankHandle bank_handle); + u32 GetSoundID(u32 sound_handle); + void SetPanTable(VolPair* pantable); + void SetPlaybackMode(s32 mode); + void SetSoundVolPan(s32 sound_handle, s32 vol, s32 pan); + void SubmitVoice(std::shared_ptr& voice) { mSynth.AddVoice(voice); }; + void SetSoundPmod(s32 sound_handle, s32 mod); + s32 GetTick() { return mTick; }; + s32 GetSoundUserData(BankHandle block_handle, + char* block_name, + s32 sound_id, + char* sound_name, + SFXUserData* dst); + + bool Tick(std::vector& leftStream, std::vector& rightStream, long samples); + + private: + std::unique_ptr m_handler; + + Loader mLoader; + Synth mSynth; + VoiceManager mVmanager; + s32 mTick{0}; + + // static long sound_callback(cubeb_stream* stream, + // void* user, + // const void* input, + // void* output_buffer, + // long len); + // static void state_callback(cubeb_stream* stream, void* user, cubeb_state state); +}; +} // namespace snd diff --git a/game/sound/CMakeLists.txt b/game/sound/CMakeLists.txt index 0e159ad2a8..8aecd005e7 100644 --- a/game/sound/CMakeLists.txt +++ b/game/sound/CMakeLists.txt @@ -1,4 +1,5 @@ set(SOUND_SOURCES + 989snd/fakeplayer.cpp 989snd/player.cpp 989snd/midi_handler.cpp 989snd/ame_handler.cpp @@ -19,6 +20,7 @@ set(SOUND_SOURCES ) add_library(sound STATIC ${SOUND_SOURCES}) +set_target_properties(sound PROPERTIES POSITION_INDEPENDENT_CODE ON) target_link_libraries(sound PRIVATE fmt cubeb common) add_executable(sndplay 989snd/sndplay.cpp) diff --git a/game/sound/common/flava.h b/game/sound/common/flava.h new file mode 100644 index 0000000000..524a20b608 --- /dev/null +++ b/game/sound/common/flava.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include + +namespace flava { + +struct Variant { + const char* name; + int value; +}; + +struct FlavaSet { + int reg; + std::vector variants; + bool battle_mode = false; +}; + +inline const FlavaSet* lookup(std::string music_name) { + for (char& ch : music_name) { + ch = static_cast(std::tolower(static_cast(ch))); + } + + static const std::map table = { + {"village1", + {0, + {{"default", 0}, + {"sage", 2}, + {"sage-hut", 3}, + {"birdlady", 4}, + {"farmer", 5}, + {"assistant", 6}, + {"mayor", 7}, + {"sculptor", 8}, + {"explorer", 9}, + {"dock", 10}}}}, + {"jungle", + {0, + {{"default", 0}, + {"jungle-temple-exit", 1}, + {"jungle-lurkerm", 2}, + {"jungle-temple-top", 3}}}}, + {"jungleb", {0, {{"default", 0}, {"jungleb-eggtop", 1}}}}, + {"beach", + {0, + {{"default", 0}, + {"beach-sentinel", 1}, + {"beach-cannon", 2}, + {"beach-grotto", 3}, + {"birdlady", 4}}}}, + {"misty", {0, {{"default", 0}, {"misty-battle", 1}, {"misty-boat", 2}, {"racer", 3}}}}, + {"firecany", {0, {{"default", 0}, {"racer", 1}, {"unused", 2}}}}, + {"village2", + {0, + {{"default", 0}, + {"sage", 1}, + {"assistant", 2}, + {"warrior", 3}, + {"geologist", 4}, + {"gambler", 5}, + {"levitator", 6}}}}, + {"swamp", {0, {{"default", 0}, {"swamp-launcher", 2}, {"swamp-battle", 3}, {"flutflut", 4}}}}, + {"rolling", {0, {{"default", 0}, {"rolling-gorge", 1}}}}, + {"ogre", {0, {{"default", 0}, {"ogre-middle", 1}, {"ogre-end", 2}}}}, + {"village3", + {0, + {{"default", 0}, + {"miners", 1}, + {"sage", 2}, + {"assistant", 3}, + {"to-maincave", 4}, + {"to-snow", 5}}}}, + {"maincave", + {0, + {{"default", 0}, {"robocave", 1}, {"robocave-top", 2}, {"maincave", 3}, {"darkcave", 4}}}}, + {"snow", + {0, + {{"default", 0}, + {"snow-battle", 1}, + {"flutflut", 2}, + {"snow-cave", 3}, + {"snow-fort", 4}, + {"snow-balls", 5}}}}, + {"lavatube", {0, {{"none", 0}, {"default", 1}, {"lavatube-middle", 2}, {"lavatube-end", 3}}}}, + {"citadel", + {0, + {{"default", 0}, + {"sage", 1}, + {"assistant", 2}, + {"sage-yellow", 3}, + {"sage-red", 4}, + {"sage-blue", 5}, + {"citadel-center", 6}}}}, + {"finalbos", {0, {{"default", 0}, {"finalboss-middle", 1}, {"finalboss-end", 2}}}}, + {"credits", {0, {{"none", 0}, {"default", 2}}}}, + + // jak2 + {"city1", {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"pilot", 5}}, true}}, + {"ruins", {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"mech", 3}, {"darkjak", 4}}}}, + {"atoll", {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"darkjak", 4}}, true}}, + {"sewer", + {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"mech", 3}, {"darkjak", 4}}, true}}, + {"fortress", {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"mech", 3}, {"darkjak", 4}}}}, + {"strip", {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"mech", 3}, {"darkjak", 4}}}}, + {"dig", {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"mech", 3}, {"darkjak", 4}}}}, + {"mountain", + {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"mech", 3}, {"darkjak", 4}}, true}}, + {"palcab", {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"darkjak", 4}}, true}}, + {"tomb", {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"darkjak", 4}}, true}}, + {"forest", + {14, {{"default", 0}, {"gun", 1}, {"board", 2}, {"mech", 3}, {"darkjak", 4}}, true}}, + }; + + auto it = table.find(music_name); + return it == table.end() ? nullptr : &it->second; +} + +} // namespace flava diff --git a/game/sound/common/sound_types.h b/game/sound/common/sound_types.h index f9878b345f..0511d9660e 100644 --- a/game/sound/common/sound_types.h +++ b/game/sound/common/sound_types.h @@ -5,6 +5,9 @@ namespace snd { +const s32 MAX_VOLUME = 0x400; +const u32 SAMPLE_RATE = 48000; // 48000 samples of 16bits per channel per second +//= 96000 bytes per second in mono, 192000 bytes per second in stereo struct VolPair { s16 left; s16 right;