> account for changes in IIDX32 onward affecting `hooks::soundbank` > `hooks::soundbank` should support any ambiguous IIDX32/33 datacodes again
979 lines
35 KiB
C++
979 lines
35 KiB
C++
#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 "avs/core.h"
|
|
#include <cstring>
|
|
|
|
|
|
// 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 <mfapi.h>
|
|
#include <mfidl.h>
|
|
#include <mfreadwrite.h>
|
|
#include <mfobjects.h>
|
|
#include <shlwapi.h>
|
|
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<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);
|
|
#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<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 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<int>(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<size_t>(static_cast<double>(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<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(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<struct bank *>(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 *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<bool>(avs::core::avs_fs_lstat(path, &st)); // lstat should be faster than open
|
|
log_info("hooks::soundbank", "'{}' exists? => {}", path, r);
|
|
return static_cast<bool>(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;
|
|
|
|
static constexpr unsigned int by_ext(const char *ext, int h = 0)
|
|
{
|
|
return !ext[h] ? 5381 : (by_ext(ext, h + 1) * 33) ^ ext[h];
|
|
}
|
|
} 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<std::codecvt_utf8<wchar_t>> converter;
|
|
log_info("hooks::soundbank", "Processing s3p->2dx transcoding routine for [{}]({} - {})",
|
|
id,
|
|
converter.to_bytes(std::wstring(reinterpret_cast<const wchar_t *>(artistl), wcslen(reinterpret_cast<const wchar_t *>(artistl)))),
|
|
converter.to_bytes(std::wstring(reinterpret_cast<const wchar_t *>(titlel), wcslen(reinterpret_cast<const wchar_t *>(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<void *>(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<const uint8_t *>(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<void *>(offset_.RD_05d_2dx), t, SNDPATHFMTMAX);
|
|
log_info("hooks::soundbank", "RD_05d_2dx set to '{}'", reinterpret_cast<const char *>(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;
|
|
}
|
|
void run_tests()
|
|
{
|
|
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<const uint8_t *>(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();
|
|
}
|
|
void init(HINSTANCE hmodule, 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 (offset_t::by_ext(ext))
|
|
{
|
|
case offset_t::by_ext("2018091900"):
|
|
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x1170b0;
|
|
break;
|
|
case offset_t::by_ext("2019090200"):
|
|
case offset_t::by_ext("2019100700"):
|
|
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x37a540;
|
|
break;
|
|
case offset_t::by_ext("2020092900"):
|
|
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x5ac460;
|
|
break;
|
|
case offset_t::by_ext("2021083000"):
|
|
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x766bc0;
|
|
break;
|
|
case offset_t::by_ext("2021091500"):
|
|
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x766c60;
|
|
break;
|
|
case offset_t::by_ext("2022082400"):
|
|
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x46e160;
|
|
break;
|
|
case offset_t::by_ext("2023090500"):
|
|
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0xafd780;
|
|
break;
|
|
case offset_t::by_ext("2024082600"):
|
|
offset_.TX_BmsbEnumValidSoundbanks = (intptr_t) hmodule + 0x8eae40;
|
|
break;
|
|
case offset_t::by_ext("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<void *>(offset_.TX_BmsbEnumValidSoundbanks), reinterpret_cast<void *>(on_enum_valid_soundbanks), reinterpret_cast<void **>(&BmsbEnumValidSoundbanks));
|
|
hooks::devel::bind_routine(hooks::devel::sndb_test, run_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();
|
|
}
|
|
} |