From 06bb712a46419b61e6a2c822586bf633c53cdcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl?= Date: Thu, 22 Oct 2026 13:44:47 +0200 Subject: [PATCH] adding audio_host project for hooking noita FMOD --- tools/audio_host/CMakeLists.txt | 13 ++ tools/audio_host/audio_host.cpp | 210 ++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 tools/audio_host/CMakeLists.txt create mode 100644 tools/audio_host/audio_host.cpp diff --git a/tools/audio_host/CMakeLists.txt b/tools/audio_host/CMakeLists.txt new file mode 100644 index 0000000..10c3c1e --- /dev/null +++ b/tools/audio_host/CMakeLists.txt @@ -0,0 +1,13 @@ +# The 32-bit audio host (the game's FMOD DLLs are x86). Built on its own: +# cmake -S tools/audio_host -B build_audio32 -A Win32 && cmake --build build_audio32 --config Release +# then audio_host.exe goes next to noita_gpu.exe (and into demo/). +cmake_minimum_required(VERSION 3.20) +project(audio_host CXX) +set(CMAKE_CXX_STANDARD 17) +add_executable(audio_host audio_host.cpp) +target_include_directories(audio_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../src) +if(MSVC) + target_compile_options(audio_host PRIVATE /W4) + target_compile_definitions(audio_host PRIVATE _CRT_SECURE_NO_WARNINGS) + set_property(TARGET audio_host PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() diff --git a/tools/audio_host/audio_host.cpp b/tools/audio_host/audio_host.cpp new file mode 100644 index 0000000..18dba60 --- /dev/null +++ b/tools/audio_host/audio_host.cpp @@ -0,0 +1,210 @@ +// 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 +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include +#include +#include +#include + +#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 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 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 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 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(kHandles); + std::vector 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; +}