From 76e4bcb8bc04590693a9870d6bd32d7485b221db Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 17 Sep 2026 01:29:11 +0200 Subject: [PATCH] [decompiler] Extracting Sound Effects and Music (#4401) This PR adds the ability to extract sound effects to .wav files for each of the four games, and music to .wav files for Jak1 and Jak2. Basically what I have done is create a "fake player" which works the same as the player from Player.cpp except we just take the raw audio samples that would be sent to cubeb and then write them to wave files. I shamelessly stole @Hat-Kid's code for flavas, for parsing the sound effect names and for finding where the sound banks start for Jak 1 lol. --- CMakeLists.txt | 6 +- decompiler/CMakeLists.txt | 9 +- decompiler/config.cpp | 3 + decompiler/config.h | 1 + decompiler/config/jak1/jak1_config.jsonc | 5 +- decompiler/config/jak2/jak2_config.jsonc | 5 +- decompiler/config/jak3/jak3_config.jsonc | 2 +- decompiler/config/jakx/jakx_config.jsonc | 2 +- decompiler/data/streamed_audio.cpp | 207 +++++++++++++++++++++- decompiler/data/streamed_audio.h | 5 +- decompiler/decompilation_process.cpp | 15 +- game/sound/989snd/fakeplayer.cpp | 210 +++++++++++++++++++++++ game/sound/989snd/fakeplayer.h | 75 ++++++++ game/sound/CMakeLists.txt | 2 + game/sound/common/flava.h | 120 +++++++++++++ game/sound/common/sound_types.h | 3 + 16 files changed, 655 insertions(+), 15 deletions(-) create mode 100644 game/sound/989snd/fakeplayer.cpp create mode 100644 game/sound/989snd/fakeplayer.h create mode 100644 game/sound/common/flava.h 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;