feat: Add iidx soundbank hook

> mockup using MF decoder (which by itself is broken, it seems)
> working .s3p unpacking and .2dx packing
> use avs filesystem (read only)
> soundbank patching disabled (missing some logic)
> unity games may be broken (linker)
> related to #1
This commit is contained in:
[ ]
2025-05-22 23:53:19 +09:00
parent 8a5f1b5017
commit 933dcf72a9
4 changed files with 706 additions and 1 deletions
+688
View File
@@ -0,0 +1,688 @@
#include "bmsbhook.h"
#include "util/logging.h"
#include "util/memutils.h"
#include "util/sigscan.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "avs/core.h"
#include <cstring>
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;
};
void dump(const char *path, const char *src, size_t sz)
{
FILE *fh = fopen(path, "wb");
fwrite(src, sz, 1, fh);
fclose(fh);
}
// This implementation 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
namespace hooks::bmsb::mf_broken
{
//_BUG: manual symbol lookup, should not link against: mf, mfplat, mfreadwrite
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mfobjects.h>
#include <shlwapi.h>
typedef HRESULT (*MFCreateMFByteStreamOnStream_t)(IStream *, IMFByteStream **);
MFCreateMFByteStreamOnStream_t MFCreateMFByteStreamOnStream;
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::bmsb", "ReadSample() failure, skipping..");
break;
}
if (flags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)
{
log_warning("hooks::bmsb", "Unsupported WAVE format, skipping..");
samples->Release();
break;
}
if (flags & MF_SOURCE_READERF_ENDOFSTREAM)
{
break;
}
if (samples == nullptr)
{
log_warning("hooks::bmsb", "Missing samples");
continue;
}
hr = samples->ConvertToContiguousBuffer(&mediabuf);
if (FAILED(hr))
{
log_warning("hooks::bmsb", "ConvertToContiguousBuffer() failure, skipping..");
samples->Release();
break;
}
// Store to destination (szmax as overflow check)
mediabuf->Lock(&audiobuf, nullptr, reinterpret_cast<DWORD *>(&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);
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);
wav_hdr dsthdr = {
.channels=static_cast<uint16_t>(a),
.samplerate=b,
.bitdepth=static_cast<uint16_t>(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 mfdll = libutils::try_library("Mfplat.dll");
if (!mfdll) return false;
// Missing in includes
MFCreateMFByteStreamOnStream = (MFCreateMFByteStreamOnStream_t) GetProcAddress(mfdll, "MFCreateMFByteStreamOnStream");
// MF init checks
if (!MFCreateMFByteStreamOnStream || FAILED(MFStartup(MF_VERSION)))
return false;
return true;
}
void deinit()
{
MFShutdown();
}
}
namespace hooks::bmsb
{
constexpr char SNDPATHFMT[] = "%05d/%05d";
constexpr size_t SNDPATHFMTMAX = sizeof(SNDPATHFMT) - 1;
constexpr size_t SNDPATHMAX = sizeof("AABBB/AABBBC_pre.2dx");
constexpr size_t CACHEMAX = 32;
typedef struct soundbank soundbank;
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) noexcept
{
return static_cast<size_t>(static_cast<double>(sz) * 8.81875) + sizeof(wav_hdr); // 0.wma is consistent 180 therefore VBR Q90, assuming worst case: x8.81875@~160kb/s
}
[[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);
}
//_INFO: assertsz guaranteed until serialize(true), overridden by voice_ptr::sz(v), compute value with assertsz_*()
[[nodiscard]] constexpr size_t voice_emplace(size_t assertsz) noexcept
{
// memory space assertion
if (this->voice_ptr(this->bank_->header.voice_cnt)->fbegin + assertsz >= banksz_)
{
log_info("hooks:bmsb", "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 ALLOCSIZE = 4 * 1024 * 1024;
struct bank *bank_;
size_t banksz_;
explicit snd_2dx(char *bindbank);
};
snd_2dx::snd_2dx(char *bindbank) : bank_(reinterpret_cast<struct bank *>(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(char *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(char *bindbank);
};
snd_s3p::snd_s3p(char *bindbank) : bank_(reinterpret_cast<struct bank *>(bindbank)), banksz_(0)
{
}
snd_s3p::~snd_s3p()
{
free(bank_);
}
snd_s3p *snd_s3p::unpack(char *refbank)
{
return refbank ? new snd_s3p(refbank) : nullptr;
}
struct soundbank
{
public:
static soundbank *new_instance(int id);
static void flush_cache();
static void pop_cache();
static soundbank *get_bank(int id);
static char *read_data(const char *sndpath, const char *ext, size_t *sz);
static bool has_format(const char *sndpath, const char *ext);
int id;
char sndpath[SNDPATHMAX];
private:
static soundbank *cache_;
static size_t cachesz_;
soundbank *next_;
soundbank();
~soundbank();
};
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)
memutils::VProtectGuard *guard;
} offset_;
soundbank *soundbank::cache_;
size_t soundbank::cachesz_;
static BmsbEnumValidSoundbanks_t BmsbEnumValidSoundbanks;
soundbank *soundbank::new_instance(int id)
{
soundbank *prev, *instance;
// Latest entry should always be head
instance = new soundbank();
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_->sndpath[0] != 's')
{
prev = instance->next_;
instance->next_ = prev->next_;
delete prev;
cachesz_ -= 1;
}
if (cachesz_ >= CACHEMAX)
{
for (prev = instance; instance; (instance = instance->next_) && (prev = instance));
prev->next_ = nullptr;
delete instance;
cachesz_ -= 1;
}
return cache_;
}
soundbank::soundbank() : id(0), sndpath(""), next_(nullptr)
{
}
soundbank::~soundbank()
{
}
soundbank *soundbank::get_bank(int id)
{
// Find in cache (latest=>oldest)
for (soundbank *it = soundbank::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)
soundbank *bank = new_instance(id);
snprintf(bank->sndpath, SNDPATHMAX, "%05d/%05d", id, id);
if (!(soundbank::has_format(bank->sndpath, ".s3p") || soundbank::has_format(bank->sndpath, ".2dx")))
{
soundbank::pop_cache();
bank = nullptr;
}
return bank;
}
void soundbank::flush_cache()
{
soundbank *prev;
while (cache_)
{
prev = cache_;
cache_ = cache_->next_;
delete prev;
}
}
void soundbank::pop_cache()
{
soundbank *prev = cache_;
cache_ = cache_->next_;
delete prev;
}
char *soundbank::read_data(const char *sndpath, const char *ext, size_t *sz)
{
constexpr uint16_t O_RDONLY = 1; // rw:12 consistent across versions with s3p support
constexpr avs::core::avs_file_t E_NOT_FOUND = 0x80070002;
char *r = nullptr;
char avspath[64] = "";
snprintf(avspath, sizeof(avspath), "/data/sound/%s%s", sndpath, ext);
avs::core::avs_file_t fd = avs::core::avs_fs_open(avspath, 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 = (char *) malloc(*sz);
if (r) avs::core::avs_fs_read(fd, reinterpret_cast<uint8_t *>(r), *sz);
avs::core::avs_fs_close(fd);
}
return r;
}
bool soundbank::has_format(const char *sndpath, const char *ext)
{
char avspath[64] = "";
snprintf(avspath, sizeof(avspath), "/data/sound/%s%s", sndpath, ext);
avs::core::avs_stat st; // NOLINT(cppcoreguidelines-pro-type-member-init)
return static_cast<bool>(avs::core::avs_fs_lstat(avspath, &st)); // lstat should be faster than open
}
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/music_*.bin _BUG: therefore differs a bit between some styles
soundbank *keysounds = nullptr;
const char *titlel = (const char *) song;
const char *artistl = (const char *) song + 0xc0;
const int32_t id = *(int32_t *) ((const char *) song + 0x3b0);
log_info("hooks::bmsb", "Processing s3p->2dx detour routine for [{}]({} - {})", id, artistl, titlel);
if (!(keysounds = soundbank::get_bank(id)))
{
log_info("hooks::bmsb", "soundbank::get_bank() was 0x0, trying to recover");
memcpy(reinterpret_cast<void *>(offset_.RD_05d_2dx), "%05d/%05d", SNDPATHFMTMAX);
BmsbEnumValidSoundbanks(song, difficulty, unk2);
return;
}
else if (!soundbank::has_format(keysounds->sndpath, ".2dx"))
{
// Get s3p
size_t szs3p;
char *bufs3p = soundbank::read_data(keysounds->sndpath, ".s3p", &szs3p);
// Process s3p into wma
snd_s3p *snds3p = snd_s3p::unpack(bufs3p);
// Process wma into wav
// Process wav into 2dx
// Output 2dx into /var/tmp (z:\var\tmp)
// symlink system/00.2dx -> /tmp/output
// Store to cache (skip transcoding on consecutive runs)
}
// Resume execution
memcpy(reinterpret_cast<void *>(offset_.RD_05d_2dx), strlen(keysounds->sndpath) == SNDPATHFMTMAX ? keysounds->sndpath : "%05d/%05d", SNDPATHFMTMAX);
log_info("hooks::bmsb", "RD_05d_2dx set as '{}'", reinterpret_cast<const char *>(offset_.RD_05d_2dx)); //_REM: tests
log_info("hooks::bmsb", "Processing completed");
BmsbEnumValidSoundbanks(song, difficulty, unk2);
}
void run_tests()
{
size_t szs3p;
char sig[5];
sig[4] = '\0';
char *bufs3p = soundbank::read_data("25073/25073", ".s3p", &szs3p);
snd_s3p *snds3p = snd_s3p::unpack(bufs3p);
if (snds3p)
{
// S3P/S3V
log_info("hooks:bmsb", "s3p(25073/25006)? => ({}/{})", soundbank::has_format("25073/25073", ".s3p"), soundbank::has_format("25006/25006", ".s3p"));
memcpy(sig, snds3p->header()->sig, 4);
log_info("hooks:bmsb", "25073::Bank[S3P]::Signature {}", sig);
log_info("hooks:bmsb", "25073::Bank[S3P]::Size {}", szs3p);
log_info("hooks:bmsb", "25073::Bank[S3P]::VoiceCount {}", snds3p->header()->voice_cnt);
log_info("hooks:bmsb", "25073::Bank[S3P]::Voice[0]::Size {}", snds3p->voice_ptr(0)->sz);
memcpy(sig, snds3p->voice(0)->sig, 4);
log_info("hooks:bmsb", "25073::Bank[S3P]::Voice[0]::Signature {}", sig);
// MF codec (tainted PCM data, bad)
BYTE *wav = nullptr;
uint32_t wavsz;
dump("mf_raw.wma", reinterpret_cast<const char *>(snds3p->voice(0)->asf()), snds3p->voice(0)->sz);
mf_broken::asf_to_wav(snds3p->voice(0)->asf(), snds3p->voice(0)->sz, &wav, &wavsz);
dump("mf_wav.wav", reinterpret_cast<const char *>(wav), wavsz);
// S3P=>2DX (max 4076 voices)
size_t i, j;
snd_2dx *snd2dx = snd_2dx::pack(25073);
log_info("hooks:bmsb", "25073::Bank[2DX]::Name {}", 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);
auto *voicedst_wave = voicedst->wave();
mf_broken::asf_to_wav(voicesrc->asf(), voicesrc->sz, &voicedst_wave, &voicedst->sz);
snd2dx->voice_ptr(j)->sz(voicedst); // updates voice_ptr table, do not skip
}
// Valid 2DX (trim is not necessary)
log_info("hooks:bmsb", "25073::Bank[2DX]::dump()");
snd_2dx::bank *buf2dx = snd2dx->serialize(false);
dump("mf_25073.2dx", reinterpret_cast<const char *>(buf2dx), buf2dx->sz());
}
else
{
log_info("hooks:bmsb", "Tests failed");
}
}
void init(HINSTANCE hmodule)
{
if (false && IsDebuggerPresent())
{
log_warning("hooks::bmsb", "Debugger detected, skipping..");
return;
}
if (!mf_broken::init())
{
log_warning("hooks::bmsb", "MF initialization failed, skipping..");
}
// Tests _REM: tests
run_tests();
return;
// Override .text
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0xafd780; //_TODO: Hardcoded for iidx30 for now, requires asm pattern lookup
// 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::bmsb", "Could not find valid offsets, skipping..");
return;
}
// Setup on_enum_valid_ksbd
offset_.guard = new memutils::VProtectGuard((void *) offset_.RD_05d_2dx, SNDPATHFMTMAX);
detour::trampoline(reinterpret_cast<void *>(offset_.TX_BmsbEnumValidSoundbanks), reinterpret_cast<void *>(on_enum_valid_soundbanks), reinterpret_cast<void **>(&BmsbEnumValidSoundbanks));
log_info("hooks::bmsb", "Soundbank preprocessor ready");
}
void deinit(HINSTANCE hmodule)
{
log_info("hooks::bmsb", "Releasing soundbank preprocessor");
soundbank::flush_cache();
delete offset_.guard;
mf_broken::deinit();
}
}