Files

211 lines
12 KiB
C++

// audio_host: a 32-bit helper that plays the game's sounds with the FMOD Studio of the Noita install (fmodstudio.dll / fmod.dll 2.1.5 are x86 and the
// game is 64-bit). It loads those DLLs at run time from the Noita folder (they are never copied or shipped), loads the banks of data/audio/Desktop and runs
// the commands the game writes into the shared memory of src/audio/audio_ipc.h. It ends with the game.
// "Made with FMOD Studio by Firelight Technologies Pty Ltd."
// usage: audio_host <noita dir> <shared memory name> <game pid>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstdio>
#include <cstring>
#include <map>
#include <string>
#include <vector>
#include "audio/audio_ipc.h"
using namespace ng::audio_ipc;
namespace {
struct FVec { float x, y, z; };
struct F3D { FVec position, velocity, forward, up; };
typedef int(__stdcall* PCreate)(void**, unsigned);
typedef int(__stdcall* PInit)(void*, int, unsigned, unsigned, void*);
typedef int(__stdcall* PSys)(void*);
typedef int(__stdcall* PLoadBank)(void*, const char*, unsigned, void**);
typedef int(__stdcall* PGetEvent)(void*, const char*, void**);
typedef int(__stdcall* PListener)(void*, int, const F3D*, const FVec*);
typedef int(__stdcall* PCreateInst)(void*, void**);
typedef int(__stdcall* PInst)(void*);
typedef int(__stdcall* PStop)(void*, int);
typedef int(__stdcall* PSet3D)(void*, const F3D*);
typedef int(__stdcall* PSetParam)(void*, const char*, float, int);
typedef int(__stdcall* PGetState)(void*, int*);
typedef int(__stdcall* PSetFloat)(void*, float);
typedef int(__stdcall* PBankCount)(void*, int*);
typedef int(__stdcall* PBankList)(void*, void**, int, int*);
typedef int(__stdcall* PDescPath)(void*, char*, int, int*);
struct FParamId { unsigned d1, d2; };
struct FParamDesc { const char* name; FParamId id; float minimum, maximum, defaultvalue; int type; unsigned flags; unsigned guid[4]; };
typedef int(__stdcall* PParamCount)(void*, int*);
typedef int(__stdcall* PParamByIndex)(void*, int, FParamDesc*);
typedef int(__stdcall* PParamLabel)(void*, int, int, char*, int, int*);
PCreate fCreate; PInit fInit; PSys fUpdate, fRelease; PLoadBank fLoadBank; PGetEvent fGetEvent; PListener fListener;
typedef int(__stdcall* PSetBool)(void*, int);
PCreateInst fCreateInst; PInst fStart, fRelInst; PStop fStop; PSet3D fSet3D; PSetParam fSetParam; PGetState fGetState; PSetFloat fSetVolume; PSetBool fSetPaused;
PBankCount fBankCount; PBankList fBankList; PDescPath fDescPath;
F3D at(float x, float y, float z, float vx, float vy) { // 0047b600: position in pixels, velocity = the movement since the last update, forward +z, up +y
F3D a{};
a.position = {x, y, z};
a.velocity = {vx, vy, 0.0f};
a.forward = {0.0f, 0.0f, 1.0f};
a.up = {0.0f, 1.0f, 0.0f};
return a;
}
} // namespace
int main(int argc, char** argv) {
if (argc < 4) return 2;
const std::string dir = argv[1];
HANDLE map = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, argv[2]);
if (!map) return 3;
auto* sh = (Shared*)MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(Shared));
if (!sh || sh->magic != kMagic) return 4;
HANDLE game = OpenProcess(SYNCHRONIZE, FALSE, (DWORD)std::strtoul(argv[3], nullptr, 10));
auto fail = [&](const char* m) { std::snprintf(sh->error, sizeof sh->error, "%s", m); sh->ready = -1; return 5; };
SetDllDirectoryA(dir.c_str()); // fmodstudio.dll needs fmod.dll next to it
HMODULE core = LoadLibraryA((dir + "\\fmod.dll").c_str());
HMODULE studio = LoadLibraryA((dir + "\\fmodstudio.dll").c_str());
if (!core || !studio) return fail("fmod.dll / fmodstudio.dll not found in the Noita folder");
#define GET(v, T, n) v = (T)GetProcAddress(studio, n); if (!v) return fail("missing " n);
GET(fCreate, PCreate, "FMOD_Studio_System_Create");
GET(fInit, PInit, "FMOD_Studio_System_Initialize");
GET(fUpdate, PSys, "FMOD_Studio_System_Update");
GET(fRelease, PSys, "FMOD_Studio_System_Release");
GET(fLoadBank, PLoadBank, "FMOD_Studio_System_LoadBankFile");
GET(fGetEvent, PGetEvent, "FMOD_Studio_System_GetEvent");
GET(fListener, PListener, "FMOD_Studio_System_SetListenerAttributes");
GET(fCreateInst, PCreateInst, "FMOD_Studio_EventDescription_CreateInstance");
GET(fStart, PInst, "FMOD_Studio_EventInstance_Start");
GET(fRelInst, PInst, "FMOD_Studio_EventInstance_Release");
GET(fStop, PStop, "FMOD_Studio_EventInstance_Stop");
GET(fSet3D, PSet3D, "FMOD_Studio_EventInstance_Set3DAttributes");
GET(fSetParam, PSetParam, "FMOD_Studio_EventInstance_SetParameterByName");
GET(fGetState, PGetState, "FMOD_Studio_EventInstance_GetPlaybackState");
GET(fSetVolume, PSetFloat, "FMOD_Studio_EventInstance_SetVolume");
GET(fSetPaused, PSetBool, "FMOD_Studio_EventInstance_SetPaused");
GET(fBankCount, PBankCount, "FMOD_Studio_Bank_GetEventCount");
GET(fBankList, PBankList, "FMOD_Studio_Bank_GetEventList");
GET(fDescPath, PDescPath, "FMOD_Studio_EventDescription_GetPath");
#undef GET
void* sys = nullptr;
if (fCreate(&sys, 0x00020105) != 0 || !sys) return fail("FMOD_Studio_System_Create failed");
if (fInit(sys, 512, 0, 0, nullptr) != 0) return fail("FMOD_Studio_System_Initialize failed");
std::vector<void*> banks;
{ // every bank of data/audio/Desktop (the strings bank gives the event paths)
WIN32_FIND_DATAA fd;
const std::string bdir = dir + "\\data\\audio\\Desktop\\";
HANDLE fh = FindFirstFileA((bdir + "*.bank").c_str(), &fd);
if (fh == INVALID_HANDLE_VALUE) return fail("no banks in data/audio/Desktop");
do { void* b = nullptr; if (fLoadBank(sys, (bdir + fd.cFileName).c_str(), 0, &b) == 0) banks.push_back(b); } while (FindNextFileA(fh, &fd));
FindClose(fh);
}
if (std::getenv("NG_AUDIO_LIST")) { // every event path, for the tables
FILE* f = std::fopen(std::getenv("NG_AUDIO_LIST"), "w");
for (void* b : banks) {
int n = 0; fBankCount(b, &n);
std::vector<void*> d(n > 0 ? n : 1); int got = 0; fBankList(b, d.data(), n, &got);
for (int i = 0; i < got; ++i) { char p[512]; int r = 0; if (fDescPath(d[i], p, sizeof p, &r) == 0 && f) std::fprintf(f, "%s\n", p); }
}
if (f) std::fclose(f);
}
if (const char* dn = std::getenv("NG_AUDIO_DESCRIBE")) { // the parameters of some events (path;path;...), for the tables: NG_AUDIO_DESCRIBE_OUT
auto pc = (PParamCount)GetProcAddress(studio, "FMOD_Studio_EventDescription_GetParameterDescriptionCount");
auto pb = (PParamByIndex)GetProcAddress(studio, "FMOD_Studio_EventDescription_GetParameterDescriptionByIndex");
auto pl = (PParamLabel)GetProcAddress(studio, "FMOD_Studio_EventDescription_GetParameterLabelByIndex");
FILE* f = std::fopen(std::getenv("NG_AUDIO_DESCRIBE_OUT") ? std::getenv("NG_AUDIO_DESCRIBE_OUT") : "describe.txt", "w");
std::string list = dn;
size_t p0 = 0;
while (f && p0 <= list.size()) {
size_t e = list.find(';', p0); if (e == std::string::npos) e = list.size();
const std::string path = list.substr(p0, e - p0); p0 = e + 1;
void* d = nullptr;
if (path.empty() || fGetEvent(sys, path.c_str(), &d) != 0) { std::fprintf(f, "%s: missing\n", path.c_str()); continue; }
int n = 0; if (pc) pc(d, &n);
std::fprintf(f, "%s: %d parameters\n", path.c_str(), n);
for (int i = 0; i < n && pb; ++i) {
FParamDesc pdsc{}; pb(d, i, &pdsc);
std::fprintf(f, " %s [%g .. %g] default %g type %d flags %u\n", pdsc.name ? pdsc.name : "?", pdsc.minimum, pdsc.maximum, pdsc.defaultvalue, pdsc.type, pdsc.flags);
if (pl && (pdsc.flags & 0x20)) for (int k = 0; k < 64; ++k) { char lb[128]; int r = 0; if (pl(d, i, k, lb, sizeof lb, &r) != 0) break; std::fprintf(f, " label %d: %s\n", k, lb); }
}
}
if (f) std::fclose(f);
}
{ // every event path, for the game (the music's track lists)
size_t pos = 0;
for (void* b : banks) {
int n = 0; fBankCount(b, &n);
std::vector<void*> d(n > 0 ? n : 1); int got = 0; fBankList(b, d.data(), n, &got);
for (int i = 0; i < got; ++i) {
char pth[512]; int r = 0;
if (fDescPath(d[i], pth, sizeof pth, &r) != 0) continue;
const size_t l = std::strlen(pth);
if (pos + l + 2 >= sizeof sh->events) break;
std::memcpy(sh->events + pos, pth, l); pos += l; sh->events[pos++] = 10;
}
}
sh->events[pos] = 0;
}
sh->ready = 1;
FILE* alog = std::getenv("NG_AUDIOLOG") ? std::fopen(std::getenv("NG_AUDIOLOG"), "w") : nullptr; // (tests: what was played, what was missing)
std::map<std::string, void*> descs;
auto describe = [&](const char* path) -> void* {
auto it = descs.find(path);
if (it != descs.end()) return it->second;
void* d = nullptr;
if (fGetEvent(sys, path, &d) != 0) d = nullptr;
descs[path] = d;
return d;
};
struct Kept { void* inst = nullptr; float x = 0, y = 0; };
std::vector<Kept> kept(kHandles);
std::vector<void*> oneshots;
for (;;) {
if (game && WaitForSingleObject(game, 0) == WAIT_OBJECT_0) break;
bool quit = false;
while (sh->tail != sh->head) {
const Cmd c = sh->ring[sh->tail % kRing];
MemoryBarrier();
sh->tail = sh->tail + 1;
const int h = ((c.handle % kHandles) + kHandles) % kHandles;
switch (c.op) {
case PlayOneShot: case Start: {
void* d = describe(c.name);
void* inst = nullptr;
if (alog) { std::fprintf(alog, "%s %s (%.0f, %.0f)\n", d ? (c.op == Start ? "start" : "play") : "MISSING", c.name, c.x, c.y); std::fflush(alog); }
if (!d || fCreateInst(d, &inst) != 0 || !inst) break;
const F3D a = at(c.x, c.y, c.z, 0, 0);
fSet3D(inst, &a);
if (c.value > 0.0f && c.value != 1.0f) fSetVolume(inst, c.value);
fStart(inst);
if (c.op == PlayOneShot) fRelInst(inst); // (released: it frees itself when it ends)
else { if (kept[h].inst) { fStop(kept[h].inst, 0); fRelInst(kept[h].inst); } kept[h] = {inst, c.x, c.y}; sh->playing[h] = 1; }
break;
}
case Move: if (kept[h].inst) { const F3D a = at(c.x, c.y, c.z, c.x - kept[h].x, c.y - kept[h].y); fSet3D(kept[h].inst, &a); kept[h].x = c.x; kept[h].y = c.y; } break;
case SetParam: if (kept[h].inst) fSetParam(kept[h].inst, c.name, c.value, 0); break;
case SetVolume: if (kept[h].inst) fSetVolume(kept[h].inst, c.value); break;
case Pause: for (int k = 0; k < kHandles; ++k) if (kept[k].inst) fSetPaused(kept[k].inst, c.value != 0.0f ? 1 : 0); break; // (the pause menu: the loops and the music hold)
case Stop: case StopNow: if (kept[h].inst) { fStop(kept[h].inst, c.op == StopNow ? 1 : 0); fRelInst(kept[h].inst); kept[h].inst = nullptr; sh->playing[h] = 0; } break;
case Listener: { const F3D a = at(c.x, c.y, c.z, 0, 0); const FVec p{c.x, c.y, c.z}; fListener(sys, 0, &a, &p); break; }
case Release: if (kept[h].inst) { fRelInst(kept[h].inst); kept[h].inst = nullptr; sh->playing[h] = 0; } break; // (it plays to its end on its own)
case Quit: quit = true; break;
default: break;
}
}
if (quit) break;
for (int h = 0; h < kHandles; ++h)
if (kept[h].inst) { int st = 2; fGetState(kept[h].inst, &st); if (st == 2) { fRelInst(kept[h].inst); kept[h].inst = nullptr; sh->playing[h] = 0; } }
fUpdate(sys);
Sleep(8);
}
fRelease(sys);
return 0;
}