#include "sndbhook.h" #include "hooks/develhook.h" #include "util/logging.h" #include "util/memutils.h" #include "util/sigscan.h" #include "util/detour.h" #include "util/libutils.h" #include "util/utils.h" #include "avs/core.h" #include // This implementation outsources libavcodec from host ffmpeg (so that linking ffmpeg stack with spicetools isn't required) namespace hooks::soundbank::avcodec { enum pcm_resampler : int8_t { bmswac_resampler_none = -1, bmswac_resampler_u8, bmswac_resampler_s16, bmswac_resampler_s16_ad, bmswac_resampler_f32 }; typedef enum pcm_resampler pcm_resampler_t; typedef void (*BmswConfigInit_t)(const char *path); typedef uint64_t (*BmswEnumValidSoundbanksAddr_t)(); typedef int (*BmswSymlink_t)(const char *srcwpath, const char *dstwpath, int canonic); typedef int (*BmswTranscoderAsfToWav_t)(const uint8_t *srcasf, uint32_t srcsz, uint8_t **dstwav, uint32_t *dstsz, pcm_resampler_t resampler); static BmswConfigInit_t BmswConfigInit; BmswEnumValidSoundbanksAddr_t BmswEnumValidSoundbanksAddr; static BmswSymlink_t BmswSymlink; static BmswTranscoderAsfToWav_t BmswTranscoderAsfToWav; static HMODULE bmsw_ = nullptr; bool init() { log_info("hooks::soundbank::avcodec", "{}", __FUNCTION__); // Initialize bmsound-wine.dll once if (bmsw_) return true; if ((bmsw_ = libutils::try_library(MODULE_PATH / "bmsound-wine.dll"))) { BmswConfigInit = (BmswConfigInit_t) GetProcAddress(bmsw_, "BmswConfigInit"); BmswEnumValidSoundbanksAddr = (BmswEnumValidSoundbanksAddr_t) GetProcAddress(bmsw_, "BmswEnumValidSoundbanksAddr"); BmswSymlink = (BmswSymlink_t) GetProcAddress(bmsw_, "BmswSymlink"); BmswTranscoderAsfToWav = (BmswTranscoderAsfToWav_t) GetProcAddress(bmsw_, "BmswTranscoderAsfToWav"); // Load config BmswConfigInit("prop/linux.json"); return true; } log_warning("hooks::soundbank", "Library not found: '{}'", (MODULE_PATH / "bmsound-wine.dll").string()); return false; } void deinit() { log_info("hooks::soundbank::avcodec", "{}", __FUNCTION__); } } // Replicates wine MF wma=>wav playback bug 1:1, follows https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/multimedia/mediafoundation/AudioClip/main.cpp #include #include #include #include #include namespace hooks::soundbank::mf_broken { typedef HRESULT (*MFCreateMFByteStreamOnStream_t)(IStream *, IMFByteStream **); typedef HRESULT (*MFCreateSourceReaderFromByteStream_t)(IMFByteStream *, IMFAttributes *, IMFSourceReader **); typedef HRESULT (*MFStartup_t)(ULONG, DWORD); typedef HRESULT (*MFShutdown_t)(); MFCreateMFByteStreamOnStream_t MFCreateMFByteStreamOnStream; MFCreateSourceReaderFromByteStream_t MFCreateSourceReaderFromByteStream; MFStartup_t MFStartup; MFShutdown_t MFShutdown; struct wav_hdr { char sig_riff[4] = {'R', 'I', 'F', 'F'}; uint32_t filesz = -1; char sig_wave[4] = {'W', 'A', 'V', 'E'}; char sig_fmt[4] = {'f', 'm', 't', ' '}; uint32_t chunksize = 16; uint16_t format = 1; uint16_t channels = 2; uint32_t samplerate = 44100; uint32_t byterate = 44100 * 2 * 2; uint16_t stride = 2 * 2; uint16_t bitdepth = 16; char sig_data[4] = {'d', 'a', 't', 'a'}; uint32_t datasz = -1; }; HRESULT asf_unpack_pcm_s16le(BYTE *dst, IMFSourceReader *srcreader, uint32_t szmax, uint32_t *sz) { HRESULT hr = S_OK; size_t srcpos = 0; size_t srcchunk = 0; BYTE *audiobuf = nullptr; IMFSample *samples = nullptr; IMFMediaBuffer *mediabuf = nullptr; while (true) { DWORD flags = 0; // Get next samples chunk, in memcpy capable format hr = srcreader->ReadSample((DWORD) MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, nullptr, &flags, nullptr, &samples); if (FAILED(hr)) { log_warning("hooks::soundbank", "ReadSample() failure, skipping.."); break; } if (flags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) { log_warning("hooks::soundbank", "Unsupported WAVE format, skipping.."); samples->Release(); break; } if (flags & MF_SOURCE_READERF_ENDOFSTREAM) { break; } if (samples == nullptr) { log_warning("hooks::soundbank", "Missing samples"); continue; } hr = samples->ConvertToContiguousBuffer(&mediabuf); if (FAILED(hr)) { log_warning("hooks::soundbank", "ConvertToContiguousBuffer() failure, skipping.."); samples->Release(); break; } // Store to destination (szmax as overflow check) mediabuf->Lock(&audiobuf, nullptr, reinterpret_cast(&srcchunk)); if (szmax - srcpos < srcchunk) { srcchunk = (signed int) szmax - srcpos; } memcpy(dst + srcpos, audiobuf, srcchunk); srcpos += srcchunk; hr = mediabuf->Unlock(); // Necessary per samples chunk audiobuf = nullptr; samples->Release(); mediabuf->Release(); if (srcpos >= szmax) { break; } } *sz = srcpos; return hr; } HRESULT asf_to_wav(const BYTE *srcasf, uint32_t srcsz, BYTE **dstwav, uint32_t *dstsz) { HRESULT hr = S_OK; // Reuse raw asf container into a byte stream https://stackoverflow.com/questions/46493191/create-imfbytestream-from-byte-array IStream *shstream = SHCreateMemStream(srcasf, srcsz); IMFByteStream *bstream; MFCreateMFByteStreamOnStream(shstream, &bstream); // Create preconfigured from source buffer reader IMFSourceReader *srcreader; hr = MFCreateSourceReaderFromByteStream(bstream, nullptr, &srcreader); // Setup wav metadata/header (s16le@44.1kHz 2ch) UINT32 a, b, c; IMFMediaType *mediatype; PROPVARIANT duration; srcreader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &mediatype); srcreader->GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE, MF_PD_DURATION, &duration); #ifdef _WIN64 mediatype->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &a); mediatype->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &b); mediatype->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &c); #else a = 2; b = 44100; c = 16; #endif wav_hdr dsthdr = { .channels=static_cast(a), .samplerate=b, .bitdepth=static_cast(c) }; // ASF reader => PCM data if (!*dstwav) *dstwav = (BYTE *) malloc(size_t(((double) duration.uhVal.QuadPart / 10000000.0 * dsthdr.samplerate + 4096) * dsthdr.bitdepth * dsthdr.channels) + sizeof(struct wav_hdr)); asf_unpack_pcm_s16le(*dstwav + sizeof(struct wav_hdr), srcreader, 0xFFFFFFFF, dstsz); //_INFO: overflow handled elsewhere // Finalize and store header dsthdr.datasz = *dstsz; dsthdr.filesz = *dstsz + sizeof(struct wav_hdr) - 8; memcpy(*dstwav, &dsthdr, sizeof(struct wav_hdr)); *dstsz += sizeof(wav_hdr); // Cleanup without releasing srcasf PropVariantClear(&duration); mediatype->Release(); srcreader->Release(); bstream->Close(); shstream->Release(); return hr; } bool init() { HMODULE mf_ = libutils::try_library("mf.dll"); HMODULE mfreadwrite_ = libutils::try_library("mfreadwrite.dll"); HMODULE mfplat_ = libutils::try_library("mfplat.dll"); if (!mf_ || !mfreadwrite_ || !mfplat_) return false; // Required by linker MFCreateMFByteStreamOnStream = (MFCreateMFByteStreamOnStream_t) GetProcAddress(mfplat_, "MFCreateMFByteStreamOnStream"); MFStartup = (MFStartup_t) GetProcAddress(mfplat_, "MFStartup"); MFShutdown = (MFShutdown_t) GetProcAddress(mfplat_, "MFShutdown"); MFCreateSourceReaderFromByteStream = (MFCreateSourceReaderFromByteStream_t) GetProcAddress(mfreadwrite_, "MFCreateSourceReaderFromByteStream"); if (!MFCreateMFByteStreamOnStream || !MFCreateSourceReaderFromByteStream || !MFStartup || !MFShutdown || FAILED(MFStartup(MF_VERSION, MFSTARTUP_FULL))) return false; return true; } void deinit() { MFShutdown(); } } namespace hooks::soundbank { constexpr char SNDPATHFMT[] = "%05d/%05d"; constexpr size_t SNDPATHFMTMAX = sizeof(SNDPATHFMT) - 1; constexpr size_t MNTPATHMAX = sizeof("/sdAABBB/AABBB/AABBBC_pre.2dx"); constexpr size_t AVSPATHMAX = sizeof("/data/sound/AABBB/AABBBC_pre.2dx"); constexpr uint8_t CACHEMAX = 8; constexpr int operator "" _i(long double x) { return static_cast(x); } typedef struct snd_bank snd_bank; typedef struct snd_s3p snd_s3p; typedef struct snd_2dx snd_2dx; typedef void *(*BmsbEnumValidSoundbanks_t)(void *, int32_t, int32_t); struct snd_2dx { public: struct bank { struct voice_hdr { char sig[4]; uint32_t fbegin; uint32_t sz; int16_t unk0; int16_t type; int16_t unk1; int16_t volume; int32_t unk2; [[nodiscard]] constexpr BYTE *wave() noexcept { return ((BYTE *) this) + this->fbegin; } }; struct voice_ptr { uint32_t fbegin; [[nodiscard]] constexpr void sz(voice_hdr *val) noexcept { (this + 1)->fbegin = this->fbegin + val->fbegin + val->sz; // see constructor } [[nodiscard]] constexpr size_t sz() const noexcept { return (this + 1)->fbegin > 0 ? (this + 1)->fbegin - this->fbegin : 0; // see constructor } }; struct { char name[16]; uint32_t sz; uint32_t voice_cnt; char unk[48]; } header; voice_ptr table; [[nodiscard]] constexpr size_t sz() const noexcept { return (&table + header.voice_cnt - 1)->fbegin + (&table + header.voice_cnt - 1)->sz(); } }; [[nodiscard]] static constexpr size_t assertsz_asf(size_t sz, int8_t resampler = 1) noexcept { resampler = (resampler > 1 || resampler == -1) ? 2 : 1; // resampler ratio multiplier (see pcm_resampler enum, original data is float) return static_cast(static_cast(sz) * 14.8) * resampler + WAVHDRMAX; // 0.wma is consistent 180 therefore VBR Q90, sometimes 160 though, but samples are all over the place } [[nodiscard]] constexpr const decltype(bank::header) *header() const noexcept { return &this->bank_->header; } [[nodiscard]] constexpr bank::voice_ptr *voice_ptr(size_t i) const noexcept { return &bank_->table + i; } [[nodiscard]] constexpr bank::voice_hdr *voice(size_t i) const noexcept { return (bank::voice_hdr *) ((char *) this->bank_ + this->voice_ptr(i)->fbegin); } [[nodiscard]] constexpr size_t voice_emplace(size_t assertsz) noexcept //_INFO: assertsz guaranteed until serialize(true), overridden by voice_ptr::sz(v), compute value with assertsz_*() { // memory space assertion if (this->voice_ptr(this->bank_->header.voice_cnt)->fbegin + assertsz >= banksz_) { log_info("hooks::soundbank", "snd_2dx::voice_emplace({}=>{})", banksz_, banksz_ + assertsz + ALLOCSIZE); banksz_ += assertsz + ALLOCSIZE; bank_ = (bank *) realloc(bank_, banksz_); } // voice_ptr updating works through voice_ptr::sz() //_REV: table and header.sz resizing event if voice_cnt == 4077, as voice_ptr::sz() needs to read 1 ahead // voice_hdr setup bank::voice_hdr *voice_hdr = this->voice(this->bank_->header.voice_cnt); std::copy("2DX9", &"2DX9"[4], voice_hdr->sig); voice_hdr->fbegin = sizeof(bank::voice_hdr); voice_hdr->sz = 0; voice_hdr->unk0 = (bank_->header.name[1] - '0') % 2 == 1 ? 0x3231 : 0x3230; // 0x3230/0x3231 for even/odd style voice_hdr->type = this->bank_->header.voice_cnt == 0 ? 0 : -1; // special case for Voice[0] voice_hdr->unk1 = 0x40; voice_hdr->volume = 1; voice_hdr->unk2 = 0x0; return this->bank_->header.voice_cnt++; } static snd_2dx *unpack(char *refbank) = delete; static snd_2dx *pack(int id); bank *serialize(bool trim); ~snd_2dx(); private: static constexpr size_t WAVHDRMAX = 78; static constexpr size_t ALLOCSIZE = 4 * 1024 * 1024; struct bank *bank_; size_t banksz_; explicit snd_2dx(uint8_t *bindbank); }; snd_2dx::snd_2dx(uint8_t *bindbank) : bank_(reinterpret_cast(bindbank)), banksz_(0) { if (bank_) return; // Necessary bank header initialization banksz_ = ALLOCSIZE; bank_ = (bank *) malloc(ALLOCSIZE); memset(&bank_->header, 0, sizeof(bank_->header) + 4078 * sizeof(bank::voice_ptr)); // initializes bank::voice_ptr::sz and bank::header.unk bank_->header.name[0] = '\0'; bank_->header.voice_cnt = 0; bank_->header.sz = sizeof(bank_->header) + 4078 * sizeof(bank::voice_ptr); // 4078 keysounds limit (16KB header), refer to voice_emplace() // Necessary voice pointer table initialization (refer to voice_ptr::sz()) (&bank_->table)[0].fbegin = bank_->header.sz; } snd_2dx *snd_2dx::pack(int id) { snd_2dx *r = new snd_2dx(nullptr); snprintf(r->bank_->header.name, sizeof(r->bank_->header.name), "%05d.2dx", id); return r; } snd_2dx::bank *snd_2dx::serialize(bool trim) { if (trim) { //_REV: prune voice_ptr for unused voice slots + move data segment up adjusting voice_ptr->fbegin } return bank_; } snd_2dx::~snd_2dx() { free(bank_); } struct snd_s3p { public: struct bank { struct voice_hdr { char sig[4]; // S3V0 uint32_t fbegin; uint32_t sz; uint8_t unk[20]; [[nodiscard]] constexpr BYTE *asf() const noexcept { return ((BYTE *) this) + this->fbegin; } }; struct voice_ptr { uint32_t fbegin; uint32_t sz; }; struct { const char sig[4]; // S3P0 uint32_t voice_cnt; } header; voice_ptr table; }; [[nodiscard]] constexpr const decltype(bank::header) *header() const noexcept { return &this->bank_->header; } [[nodiscard]] constexpr bank::voice_ptr *voice_ptr(size_t i) const noexcept { return &bank_->table + i; } [[nodiscard]] constexpr bank::voice_hdr *voice(size_t i) const noexcept { return (bank::voice_hdr *) ((char *) this->bank_ + this->voice_ptr(i)->fbegin); } static snd_s3p *unpack(uint8_t *refbank); static snd_s3p *pack(int id) = delete; bank *serialize(bool trim) = delete; ~snd_s3p(); private: struct bank *bank_; size_t banksz_; //_TODO: keep track of allocated memory explicit snd_s3p(uint8_t *bindbank); }; snd_s3p::snd_s3p(uint8_t *bindbank) : bank_(reinterpret_cast(bindbank)), banksz_(0) { } snd_s3p::~snd_s3p() { free(bank_); } snd_s3p *snd_s3p::unpack(uint8_t *refbank) { return refbank ? new snd_s3p(refbank) : nullptr; } struct snd_bank { public: static snd_bank *new_instance(int id); static void flush_cache(); static void pop_cache(); static snd_bank *get_bank(int id, uint8_t suffix = 0); static uint8_t *read_bank_any(const char *avspath, const char *ext, size_t *sz); static bool has_format(const char *avspath, const char *ext); static avs::core::avs_file_t map_ifs(char *avspath); static void dump(const char *path, const uint8_t *src, size_t sz); int id; char avspath[AVSPATHMAX]; void cache_bank_any(const uint8_t *sndbuf, size_t sz); [[nodiscard]] constexpr bool is_cached() const noexcept { return uid_ != UID_INVALID; } [[nodiscard]] constexpr const char *sndpath() const noexcept { const char *it = avspath + AVSPATHMAX; size_t c = 0; while (it >= avspath && c < 2) { it--; if (*it == '/') c++; } it++; if (strlen(it) > SNDPATHFMTMAX) return SNDPATHFMT; return it; } inline static avcodec::pcm_resampler_t resampler() noexcept { return resampler_; } private: static constexpr avs::core::avs_file_t E_NOT_FOUND = 0x80070002; static constexpr uint8_t UID_INVALID = 0x00; static snd_bank *cache_; static size_t cachesz_; static avcodec::pcm_resampler_t resampler_; friend void hooks::soundbank::init(HINSTANCE hmodule, const char *model, const char *ext); uint8_t uid_; uint32_t mnt_; snd_bank *next_; snd_bank(); ~snd_bank(); }; void snd_bank::dump(const char *path, const uint8_t *src, size_t sz) { FILE *fh = fopen(path, "wb"); fwrite(src, sz, 1, fh); fclose(fh); } snd_bank *snd_bank::new_instance(int id) { snd_bank *prev, *instance; // Latest entry should always be head instance = new snd_bank(); instance->id = id; instance->next_ = cache_; cache_ = instance; cachesz_ += 1; // Clean cache (last entry indexing target excluded, cache limit reached) if (instance->next_ && instance->next_->uid_ == UID_INVALID) { log_info("hooks::soundbank", "Cache prune"); prev = instance->next_; instance->next_ = prev->next_; log_info("hooks::soundbank", "Removing {}::Bank[CACHE]::{}", prev->id, (uint32_t) prev->uid_); delete prev; cachesz_ -= 1; } if (cachesz_ > CACHEMAX) { log_info("hooks::soundbank", "Cache full"); for (uint8_t i = 0; i < CACHEMAX; i += 2) { prev = nullptr; instance = cache_; while (instance && instance->next_) { prev = instance; instance = instance->next_; } prev->next_ = nullptr; delete instance; cachesz_ -= 1; } } return cache_; } void snd_bank::cache_bank_any(const uint8_t *sndbuf, size_t sz) { // Update struct using next free cache uid if (uid_ == UID_INVALID) { snd_bank *it = cache_; while (it) { if (uid_ == UID_INVALID || (uid_ == it->uid_ && it != this)) { uid_--; it = cache_; continue; } it = it->next_; } } snprintf(this->avspath, sizeof(this->avspath), "data/sound/system/%02x", (unsigned int) uid_); // Store file char path[AVSPATHMAX]; snprintf(path, sizeof(path), "%s.2dx", this->avspath); log_info("hooks::soundbank", "Storing as intermediate soundbank at '{}'", (const char *) path); dump(path, sndbuf, sz); } snd_bank::snd_bank() : id(0), avspath(""), uid_(UID_INVALID), mnt_(E_NOT_FOUND), next_(nullptr) { } snd_bank::~snd_bank() { if (uid_ != UID_INVALID) { char path[AVSPATHMAX]; snprintf(path, sizeof(path), "data/sound/system/%02x.2dx", uid_); log_info("hooks::soundbank", "{}::Bank[CACHE]::Purge '{}'", this->id, (const char *) path); remove(path); } if (mnt_ != E_NOT_FOUND) { log_info("hooks::soundbank", "{}::Bank[CACHE]::Purge mount(0x{:08x})", this->id, mnt_); avs::core::avs_fs_umount(mnt_); } } snd_bank *snd_bank::get_bank(int id, uint8_t suffix) { // Find in cache (latest=>oldest) id = id + (suffix * 1e6_i); for (snd_bank *it = cache_; it; it = it->next_) if (it->id == id) return it; // Find in avs (has valid s3p/.2dx resource at data/sound/AABBB/AABBB.2dx) snd_bank *bank = new_instance(id); snprintf(bank->avspath, AVSPATHMAX, "data/sound/%05d", (id % 1e6_i)); if (has_format(bank->avspath, ".ifs")) bank->mnt_ = map_ifs(bank->avspath); snprintf(bank->avspath + strlen(bank->avspath), 7, "/%05d", (id % 1e6_i)); if (suffix) snprintf(bank->avspath + strlen(bank->avspath), 2, "%d", suffix); if (!(has_format(bank->avspath, ".s3p") || has_format(bank->avspath, ".2dx"))) { pop_cache(); bank = nullptr; } return bank; } void snd_bank::flush_cache() { snd_bank *prev; while (cache_) { prev = cache_; cache_ = cache_->next_; delete prev; } } void snd_bank::pop_cache() { snd_bank *prev = cache_; cache_ = cache_->next_; delete prev; } uint8_t *snd_bank::read_bank_any(const char *avspath, const char *ext, size_t *sz) { constexpr uint16_t O_RDONLY = 1; // rw:12 consistent across versions with s3p support uint8_t *r = nullptr; char path[AVSPATHMAX]; snprintf(path, sizeof(path), "%s%s", avspath, ext); avs::core::avs_file_t fd = avs::core::avs_fs_open(path, O_RDONLY, 420); if (fd != E_NOT_FOUND) { { avs::core::avs_stat st; // NOLINT(cppcoreguidelines-pro-type-member-init) avs::core::avs_fs_fstat(fd, &st); *sz = st.filesize; } r = (uint8_t *) malloc(*sz); if (r) avs::core::avs_fs_read(fd, r, *sz); avs::core::avs_fs_close(fd); } return r; } bool snd_bank::has_format(const char *avspath, const char *ext) { char path[AVSPATHMAX] = ""; snprintf(path, sizeof(path), "%s%s", avspath, ext); avs::core::avs_stat st; // NOLINT(cppcoreguidelines-pro-type-member-init) bool r = static_cast(avs::core::avs_fs_lstat(path, &st)); // lstat should be faster than open log_info("hooks::soundbank", "'{}' exists? => {}", path, r); return static_cast(r); } avs::core::avs_file_t snd_bank::map_ifs(char *avspath) { // Map */${name}.ifs => /sc${name} char ifspath[AVSPATHMAX]; char mntpath[MNTPATHMAX]; const char *it = avspath + strlen(avspath); while (it >= avspath && *it != '/') { it--; } it++; snprintf(ifspath, sizeof(ifspath), "%s.ifs", avspath); snprintf(mntpath, sizeof(mntpath), "sc%s", it); avs::core::avs_file_t r = avs::core::avs_fs_mount(mntpath, ifspath, "imagefs", nullptr); // Finalize avspath if (r != E_NOT_FOUND) { snprintf(mntpath + strlen(mntpath), strlen(it) + 2, "/%s", it); log_info("hooks::soundbank", "'{}' => '{}'", avspath, mntpath); strcpy(avspath, mntpath); } return r; } static struct offset_t { intptr_t RD_05d_s3p; // %05d/%05d.s3p intptr_t RD_05d_c_s3p; // %05d/%05d%c.s3p intptr_t RD_05d_2dx; // %05d/%05d.2dx intptr_t RD_05d_c_2dx; // %05d/%05d%c.2dx intptr_t TX_BmsbEnumValidSoundbanks; // enumerates above databank paths, simple exist check (lstat()/OPEN -> CLOSE_NOWRITE only) intptr_t DAT_song_titlel; intptr_t DAT_song_artistl; intptr_t DAT_song_id; bool is_lstr_wchar; memutils::VProtectGuard *guard; } offset_; snd_bank *snd_bank::cache_ = nullptr; size_t snd_bank::cachesz_ = 0; avcodec::pcm_resampler_t snd_bank::resampler_ = avcodec::bmswac_resampler_s16_ad; //_REM: Temporary (revert to bmswac_resampler_s16) static BmsbEnumValidSoundbanks_t BmsbEnumValidSoundbanks; void *on_enum_valid_soundbanks(void *song, int32_t difficulty, int32_t unk2) { // Keysounds populated into CtrlSound::Bank[2][X] = [0:s3p][1:2dx][2:_pre.2dx], always uses lowest i databank for resource confirmed valid by this function // Struct memory layout 1:1 with data/info/(0/1)/music_*.bin snd_bank *keysounds = nullptr; const char *titlel = (const char *) song + offset_.DAT_song_titlel; const char *artistl = (const char *) song + offset_.DAT_song_artistl; const int32_t id = *(int32_t *) ((const char *) song + offset_.DAT_song_id); const char suffix = *((const char *) song + offset_.DAT_song_id + 8 + difficulty); if (offset_.is_lstr_wchar) { std::wstring_convert> converter; log_info("hooks::soundbank", "Processing s3p->2dx transcoding routine for [{}]({} - {})", id, converter.to_bytes(std::wstring(reinterpret_cast(artistl), wcslen(reinterpret_cast(artistl)))), converter.to_bytes(std::wstring(reinterpret_cast(titlel), wcslen(reinterpret_cast(titlel)))) ); } else log_info("hooks::soundbank", "Processing s3p->2dx transcoding routine for [{}]({} - {})", id, artistl, titlel); if (!(keysounds = snd_bank::get_bank(id, suffix - '0'))) { log_warning("hooks::soundbank", "soundbank::get_bank() was 0x0, this shouldn't happen.."); memcpy(reinterpret_cast(offset_.RD_05d_2dx), "%05d/%05d", SNDPATHFMTMAX); return BmsbEnumValidSoundbanks(song, difficulty, unk2); } else if (!snd_bank::has_format(keysounds->avspath, ".2dx")) { // Get s3p and process into s3p soundbank for intermediate wma voice storage size_t szs3p; uint8_t *bufs3p = snd_bank::read_bank_any(keysounds->avspath, ".s3p", &szs3p); snd_s3p *snds3p = snd_s3p::unpack(bufs3p); // Process wma voices into wav voices stored directly into 2dx soundbank size_t i, j; snd_2dx *snd2dx = snd_2dx::pack(id); for (i = 0; i < snds3p->header()->voice_cnt; i++) { // WMA=>WAV transcode snd_s3p::bank::voice_hdr *voicesrc = snds3p->voice(i); j = snd2dx->voice_emplace(snd_2dx::assertsz_asf(voicesrc->sz)); snd_2dx::bank::voice_hdr *voicedst = snd2dx->voice(j); BYTE *voicedst_wave = voicedst->wave(); avcodec::BmswTranscoderAsfToWav(voicesrc->asf(), voicesrc->sz, &voicedst_wave, &voicedst->sz, snd_bank::resampler()); snd2dx->voice_ptr(j)->sz(voicedst); // updates voice_ptr table, do not skip } // Output to cache (reuse output file, while in valid cache) snd_2dx::bank *buf2dx = snd2dx->serialize(false); keysounds->cache_bank_any(reinterpret_cast(buf2dx), buf2dx->sz()); //_REV: symlink system/uid_.2dx -> /tmp/uid_.2dx } // Resume execution (force %05.2dx branch) const char *t = keysounds->sndpath(); memcpy(reinterpret_cast(offset_.RD_05d_2dx), t, SNDPATHFMTMAX); log_info("hooks::soundbank", "RD_05d_2dx set to '{}'", reinterpret_cast(offset_.RD_05d_2dx)); log_info("hooks::soundbank", "Processing completed"); *((char *) song + offset_.DAT_song_id + 8 + difficulty) = '0'; void *r = BmsbEnumValidSoundbanks(song, difficulty, unk2); *((char *) song + offset_.DAT_song_id + 8 + difficulty) = suffix; return r; } static class Tests : public hooks::devel::Routine { public: using Routine::Routine; void entry_main() override { size_t szs3p; BYTE *wav = nullptr; uint32_t wavsz = 0; char avspath[AVSPATHMAX]; avs::core::avs_file_t avsmnt = 0; char sig[5] = " "; int id = 25073; if (!mf_broken::init()) { log_warning("hooks::soundbank", "MF initialization failed, skipping.."); } // Filesystem tests, s3p unpacking snprintf(avspath, sizeof(avspath), "data/sound/%05d", id); if (snd_bank::has_format(avspath, ".ifs")) avsmnt = snd_bank::map_ifs(avspath); snprintf(avspath + strlen(avspath), 7, "/%05d", id); snd_bank::has_format(avspath, ".s3p"); snd_bank::has_format(avspath, ".2dx"); snd_bank::has_format(avspath, "_pre.2dx"); uint8_t *bufs3p = snd_bank::read_bank_any(avspath, ".s3p", &szs3p); snd_s3p *snds3p = snd_s3p::unpack(bufs3p); if (avsmnt) avs::core::avs_fs_umount(avsmnt); // S3P => WMA => WAV => 2DX pipeline if (snds3p) { // S3P/S3V metadata memcpy(sig, snds3p->header()->sig, 4); log_info("hooks::soundbank", "{}::Bank[S3P]::Signature {}", id, sig); log_info("hooks::soundbank", "{}::Bank[S3P]::Size {}", id, szs3p); log_info("hooks::soundbank", "{}::Bank[S3P]::VoiceCount {}", id, snds3p->header()->voice_cnt); log_info("hooks::soundbank", "{}::Bank[S3P]::Voice[0]::Size {}", id, snds3p->voice_ptr(0)->sz); memcpy(sig, snds3p->voice(0)->sig, 4); log_info("hooks::soundbank", "{}::Bank[S3P]::Voice[0]::Signature {}", id, sig); snd_bank::dump("raw_wma.wma", snds3p->voice(0)->asf(), snds3p->voice(0)->sz); // MF codec (tainted PCM data, bad) mf_broken::asf_to_wav(snds3p->voice(0)->asf(), snds3p->voice(0)->sz, &wav, &wavsz); snd_bank::dump("mf_wav.wav", wav, wavsz); free(wav); wav = nullptr; wavsz = 0; // AV codec (1:1 unmodified PCM data, other than resampling if enabled) wav = (BYTE *) malloc(snd_2dx::assertsz_asf(snds3p->voice(0)->sz, avcodec::bmswac_resampler_f32)); avcodec::BmswTranscoderAsfToWav(snds3p->voice(0)->asf(), snds3p->voice(0)->sz, &wav, &wavsz, avcodec::bmswac_resampler_f32); snd_bank::dump("ac_wav.wav", wav, wavsz); free(wav); wav = nullptr; wavsz = 0; // S3P=>2DX (max 4076 voices) size_t i, j; snd_2dx *snd2dx = snd_2dx::pack(id); log_info("hooks::soundbank", "{}::Bank[2DX]::Name {}", id, snd2dx->header()->name); for (i = 0; i < snds3p->header()->voice_cnt; i++) { // WMA=>WAV transcode snd_s3p::bank::voice_hdr *voicesrc = snds3p->voice(i); j = snd2dx->voice_emplace(snd_2dx::assertsz_asf(voicesrc->sz)); snd_2dx::bank::voice_hdr *voicedst = snd2dx->voice(j); BYTE *voicedst_wave = voicedst->wave(); //mf_broken::asf_to_wav(voicesrc->asf(), voicesrc->sz, &voicedst_wave, &voicedst->sz); avcodec::BmswTranscoderAsfToWav(voicesrc->asf(), voicesrc->sz, &voicedst_wave, &voicedst->sz, snd_bank::resampler()); snd2dx->voice_ptr(j)->sz(voicedst); // updates voice_ptr table, do not skip } // Valid 2DX (trim is not necessary) log_info("hooks::soundbank", "{}::Bank[2DX]::Dumping", id); snd_2dx::bank *buf2dx = snd2dx->serialize(false); snd_bank::dump("ac_soundbank.2dx", reinterpret_cast(buf2dx), buf2dx->sz()); } else { log_info("hooks::soundbank", "Tests failed"); } // Caching and intermediate 2dx storage detection BmsbEnumValidSoundbanks_t t = BmsbEnumValidSoundbanks; BmsbEnumValidSoundbanks = [](void *, int32_t, int32_t) -> void * { return nullptr; }; char *tmp = (char *) malloc(0x3b8 + strlen("00000000") + 1); int arr[6] = {25073, 24016, 26057, 25073, 26087, 29095}; strcpy(tmp, "SJISTITLE"); strcpy(tmp + 0xc0, "SJISARTIST"); strcpy(tmp + 0x3b8, "00000000"); for (int it: arr) { *(int32_t *) (tmp + 0x3b0) = it; on_enum_valid_soundbanks(tmp, 3, 0); } free(tmp); BmsbEnumValidSoundbanks = t; snd_bank::flush_cache(); mf_broken::deinit(); } } tests_("sndb_test"); void init(HINSTANCE hmodule, const char *model, const char *ext) { if (false && IsDebuggerPresent()) { log_warning("hooks::soundbank", "Debugger detected, skipping.."); return; } if (!avcodec::init()) { log_warning("hooks::soundbank", "AVCodec initialization failed, skipping.."); return; } // Override .text .data per datacode _REV: prefer runtime offset lookup based on universal asm pattern long extl = strtol(ext, nullptr, 10); if (extl < 2019100800) { offset_.DAT_song_titlel = 0x00; offset_.DAT_song_artistl = 0xc0; offset_.DAT_song_id = 0x1c8; offset_.is_lstr_wchar = false; snd_bank::resampler_ = avcodec::bmswac_resampler_s16_ad; } else if (extl < 2024082700) { offset_.DAT_song_titlel = 0x00; offset_.DAT_song_artistl = 0xc0; offset_.DAT_song_id = 0x3b0; offset_.is_lstr_wchar = false; } else { offset_.DAT_song_titlel = 0x00; offset_.DAT_song_artistl = 0x1c0; offset_.DAT_song_id = 0x67c; offset_.is_lstr_wchar = true; } switch (to_hash(ext)) { case to_hash("2018091900"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x1170b0; break; case to_hash("2019090200"): case to_hash("2019100700"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x37a540; break; case to_hash("2020092900"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x5ac460; break; case to_hash("2021083000"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x766bc0; break; case to_hash("2021091500"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x766c60; break; case to_hash("2022082400"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x46e160; break; case to_hash("2023090500"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0xafd780; break; case to_hash("2024082600"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x8eae40; break; case to_hash("2025082500"): offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x969fb0; break; default: offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + (int64_t) avcodec::BmswEnumValidSoundbanksAddr(); if (offset_.TX_BmsbEnumValidSoundbanks == (intptr_t) hmodule) { log_warning("hooks::soundbank", "Unsupported game version({}), skipping..", ext); return; } log_info("hooks::soundbank", "Using user supplied offsets(0x{:x}), support may be limited..", offset_.TX_BmsbEnumValidSoundbanks); } log_info("hooks::soundbank", "Supported game version({})", ext); // Override .rdata offset_.RD_05d_s3p = replace_pattern(hmodule, "253035642f253035642e73337000", "003035642f253035642e73337000", 0, 0); offset_.RD_05d_c_s3p = replace_pattern(hmodule, "253035642f2530356425632e73337000", "003035642f2530356425632e73337000", 0, 0); offset_.RD_05d_2dx = replace_pattern(hmodule, "253035642F253035642E32647800", "73797374656D2F30302E32647800", 0, 0); offset_.RD_05d_c_2dx = replace_pattern(hmodule, "253035642f2530356425632e32647800", "253035642f2530356425632e32647800", 0, 0); if (!(offset_.TX_BmsbEnumValidSoundbanks && offset_.RD_05d_s3p && offset_.RD_05d_c_s3p && offset_.RD_05d_2dx && offset_.RD_05d_c_2dx)) { log_warning("hooks::soundbank", "Could not find valid offsets, skipping.."); return; } // Setup on_enum_valid_ksbd and optional tests offset_.guard = new memutils::VProtectGuard((void *) offset_.RD_05d_2dx, SNDPATHFMTMAX); detour::trampoline(reinterpret_cast(offset_.TX_BmsbEnumValidSoundbanks), reinterpret_cast(on_enum_valid_soundbanks), reinterpret_cast(&BmsbEnumValidSoundbanks)); hooks::devel::bind_routine(&tests_); log_info("hooks::soundbank", "Soundbank preprocessor ready"); } void deinit(HINSTANCE hmodule) { log_info("hooks::soundbank", "Releasing soundbank preprocessor"); snd_bank::flush_cache(); delete offset_.guard; avcodec::deinit(); } }