#include "framework.h" #include "PluginConfigApi.h" #include #pragma comment(lib, "detours.lib") #include #include #include #include #include #include void InjectCode(void* address, const std::vector data) { const size_t byteCount = data.size() * sizeof(uint8_t); DWORD oldProtect; VirtualProtect(address, byteCount, PAGE_EXECUTE_READWRITE, &oldProtect); memcpy(address, data.data(), byteCount); VirtualProtect(address, byteCount, oldProtect, nullptr); } void NopBytes(void* address, unsigned int num) { std::vector newbytes = {}; for (unsigned int i = 0; i < num; ++i) newbytes.push_back(0x90); InjectCode(address, newbytes); } void resizeInternalBuffers(int frames) { float* oldMixbuffer = divaAudioMixCls->mixbuffer; divaBufSizeInFrames = frames; divaAudioMixCls->mixbuffer = (float*)malloc(divaBufSizeInFrames * 4 * 4); divaAudioMixCls->mixbuffer_size = divaBufSizeInFrames * 4 * 4; free(oldMixbuffer); for (int i = 0; i < divaAudioMixCls->streamingChannels_len; i++) { float* oldChannelBuffer = divaAudioMixCls->streamingChannels[i].buffer; if (oldChannelBuffer != nullptr) { if (divaAudioMixCls->streamingChannels[i].playing == 0) { divaAudioMixCls->streamingChannels[i].buffer = (float*)malloc(divaBufSizeInFrames * 4 * 4); divaAudioMixCls->streamingChannels[i].buffer_size = divaBufSizeInFrames * 4 * 4; free(oldChannelBuffer); } else if (divaAudioMixCls->streamingChannels[i].mutex != nullptr && mtx_lock_0(&divaAudioMixCls->streamingChannels[i].mutex) == 0) { divaAudioMixCls->streamingChannels[i].buffer = (float*)malloc(divaBufSizeInFrames * 4 * 4); divaAudioMixCls->streamingChannels[i].buffer_size = divaBufSizeInFrames * 4 * 4; free(oldChannelBuffer); mtx_unlock(&divaAudioMixCls->streamingChannels[i].mutex); //printf("[DivaSound] %d\n", i); } } } divaAudCls->buffer_size = divaBufSizeInFrames; printf("[DivaSound] Resized internal buffers to %d frames\n", frames); } void resizeTestLoop() { bool dir = true; while (true) { Sleep(3000); if (dir) resizeInternalBuffers(divaBufSizeInFrames + 10000); else resizeInternalBuffers(divaBufSizeInFrames - 10000); dir = !dir; } } void stopPlayback() { if (useAsio) { if (BASS_ASIO_IsStarted()) BASS_ASIO_Stop(); } else { if (ma_device_is_started(&device)) ma_device_stop(&device); } } void closeDevice() { stopPlayback(); if (useAsio) { BASS_ASIO_Free(); } else { ma_device_uninit(&device); ma_context_uninit(&context); } } void audioCallback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { (void)pDevice; (void)pOutput; (void)pInput; (void)frameCount; if (divaAudCls->breakDword != 0 || divaAudioMixCls->mixbuffer == nullptr) { stopPlayback(); return; } if (frameCount > divaBufSizeInFrames) { printf("[DivaSound] Warning: PDAFT buffer is too small\n"); // allocate a larger buffer if necessary. // add 128 frames of padding because this isn't normal and we want to avoid it happening again if possible. resizeInternalBuffers(frameCount + 128); } divaAudioFillbuffer(divaAudioMixCls, (int16_t*)pOutput, frameCount, 0, 0); if (bitDepth > 16) // we should generate the output buffer ourselves { float volumes[4]; for (int i = 0; i < 4; i++) { volumes[i] = divaAudioMixCls->volume_master * divaAudioMixCls->volume_channels[i]; } int startChannel = 4 - nChannels; for (unsigned int i = 0; i < frameCount; i++) { for (int currentChannel = startChannel; currentChannel < 4; currentChannel++) { // note: currentChannel is for the game's buffer, outputChannel is for our output buffer. // outputChannel should also be used for volumes. int outputChannel = currentChannel - startChannel; if (bitDepth == 24) // 24 bit int output { int32_t out_val = (int32_t)(divaAudioMixCls->mixbuffer[i*4 + currentChannel] * volumes[outputChannel] * 8388607.0f); if (out_val > 8388607) out_val = 8388607; else if (out_val < -8388608) out_val = -8388608; uint64_t out_pos = (i*nChannels + outputChannel) * 3; ((byte*)pOutput)[out_pos] = out_val; ((byte*)pOutput)[out_pos + 1] = out_val >> 8; ((byte*)pOutput)[out_pos + 2] = out_val >> 16; } else if (bitDepth == 32) // floating point output { ((float*)pOutput)[i*nChannels + outputChannel] = divaAudioMixCls->mixbuffer[i*4 + currentChannel] * volumes[outputChannel]; } } } } } DWORD asioCallback(bool input, DWORD channel, void* buffer, DWORD length, void* user) { ma_uint32 frames = length * 8 / bitDepth / nChannels; audioCallback(&device, buffer, NULL, frames); return length; } void asioNotifyProc(DWORD notify, void* user); bool asioInit(bool enablePanel) { if (BASS_ASIO_Init == NULL) { printf("[DivaSound] BASS ASIO not loaded...\n"); return false; } if (HIWORD(BASS_ASIO_GetVersion()) != BASSASIOVERSION) { printf("[DivaSound] Incorrect BASS ASIO version. Use 1.4.0.0.\n"); return false; } if (!BASS_ASIO_Init(asioDevice, BASS_ASIO_THREAD)) { printf("[DivaSound] Failed to initialize device\n"); return false; } if (enablePanel && showAsioPanel) BASS_ASIO_ControlPanel(); if (!BASS_ASIO_ChannelEnable(false, 0, (ASIOPROC*)asioCallback, NULL)) { printf("[DivaSound] Failed to enable channels\n"); BASS_ASIO_Free(); return false; } for (int i = 1; i < nChannels; i++) { if (!BASS_ASIO_ChannelJoin(false, i, 0)) { printf("[DivaSound] Failed to enable channels\n"); BASS_ASIO_Free(); return false; } } if (BASS_ASIO_CheckRate(44100)) BASS_ASIO_SetRate(44100); BASS_ASIO_ChannelSetRate(false, 0, 44100); if (bitDepth == 32) BASS_ASIO_ChannelSetFormat(false, 0, BASS_ASIO_FORMAT_FLOAT); else if (bitDepth == 24) BASS_ASIO_ChannelSetFormat(false, 0, BASS_ASIO_FORMAT_24BIT); else BASS_ASIO_ChannelSetFormat(false, 0, BASS_ASIO_FORMAT_16BIT); BASS_ASIO_SetNotify((ASIONOTIFYPROC*)asioNotifyProc, NULL); return true; } void asioReinit() { printf("[DivaSound] Reinitialising ASIO device...\n"); closeDevice(); if (!asioInit(false)) return; BASS_ASIO_INFO asioInfo; BASS_ASIO_GetInfo(&asioInfo); double actualRate = BASS_ASIO_GetRate(); printf("[DivaSound] Output buffer size: %d (%dms at %dHz)\n", asioInfo.bufpref, (int)(asioInfo.bufpref * 1000 / actualRate), (int)actualRate); divaBufSizeInFrames = (int)(asioInfo.bufpref * 44100 / actualRate); divaBufSizeInMilliseconds = divaBufSizeInFrames * 1000 / 44100; printf("[DivaSound] PDAFT buffer size: %d (%dms at %dHz)\n", divaBufSizeInFrames, divaBufSizeInMilliseconds, 44100); resizeInternalBuffers(divaBufSizeInFrames); if (!BASS_ASIO_Start(asioInfo.bufpref, 1)) { printf("[DivaSound] Failed to start playback device\n"); BASS_ASIO_Free(); return; } printf("[DivaSound] Started playback\n"); } void asioNotifyProc(DWORD notify, void* user) { if (notify == BASS_ASIO_NOTIFY_RESET) { std::thread asioReinitThread(asioReinit); asioReinitThread.join(); } } bool maInit() { ma_backend backends[] = { maBackend }; contextConfig = ma_context_config_init(); contextConfig.threadPriority = ma_thread_priority_highest; if (ma_context_init(backends, sizeof(backends) / sizeof(backends[0]), &contextConfig, &context) != MA_SUCCESS) { printf("[DivaSound] Failed to initialize context\n"); return false; } deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.channels = nChannels; deviceConfig.sampleRate = 44100; deviceConfig.bufferSizeInMilliseconds = requestBuffer; // actual result may be larger deviceConfig.periods = nPeriods; deviceConfig.dataCallback = audioCallback; deviceConfig.pUserData = NULL; deviceConfig.playback.shareMode = maSharemode; if (bitDepth == 32) deviceConfig.playback.format = ma_format_f32; else if (bitDepth == 24) deviceConfig.playback.format = ma_format_s24; else deviceConfig.playback.format = ma_format_s16; if (nChannels == 4) { deviceConfig.playback.channelMap[0] = MA_CHANNEL_FRONT_LEFT; deviceConfig.playback.channelMap[1] = MA_CHANNEL_FRONT_RIGHT; deviceConfig.playback.channelMap[2] = MA_CHANNEL_BACK_LEFT; deviceConfig.playback.channelMap[3] = MA_CHANNEL_BACK_RIGHT; } if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) { printf("[DivaSound] Failed to open playback device\n"); ma_context_uninit(&context); return false; } //printf("[DivaSound] Opened playback device\n"); return true; } void loadConfig() { nChannels = GetPrivateProfileIntW(L"general", L"channels", 2, CONFIG_FILE); if (nChannels != 4) nChannels = 2; bitDepth = GetPrivateProfileIntW(L"general", L"bit_depth", 16, CONFIG_FILE); if (bitDepth != 32 && bitDepth != 24) bitDepth = 16; requestBuffer = GetPrivateProfileIntW(L"buffer", L"buffer_size", 10, CONFIG_FILE); if (requestBuffer < 0) requestBuffer = 0; nPeriods = GetPrivateProfileIntW(L"buffer", L"periods", 2, CONFIG_FILE); if (nPeriods < 1) nPeriods = 1; useOldInit = GetPrivateProfileIntW(L"general", L"alternate_init", 1, CONFIG_FILE) > 0 ? false : true; GetPrivateProfileStringW(L"general", L"backend", L"wasapi", backendName, 32, CONFIG_FILE); for (wchar_t& chr : backendName) chr = towlower(chr); useAsio = false; maSharemode = ma_share_mode_shared; if (lstrcmpW(backendName, L"asio") == 0) { StringCchCopyW(backendName, 32, L"ASIO"); useAsio = true; } else if (lstrcmpW(backendName, L"directsound") == 0) { StringCchCopyW(backendName, 32, L"DirectSound"); maBackend = ma_backend_dsound; } else if (lstrcmpW(backendName, L"wasapi_exclusive") == 0) { StringCchCopyW(backendName, 32, L"WASAPI (Exclusive mode)"); maBackend = ma_backend_wasapi; maSharemode = ma_share_mode_exclusive; } else { StringCchCopyW(backendName, 32, L"WASAPI"); maBackend = ma_backend_wasapi; } if (GetPrivateProfileIntW(L"asio", L"auto_device", TRUE, CONFIG_FILE)) { asioDevice = -1; } else { asioDevice = GetPrivateProfileIntW(L"asio", L"device", -1, CONFIG_FILE); } showAsioPanel = GetPrivateProfileIntW(L"asio", L"show_config", 0, CONFIG_FILE) > 0 ? true : false; } void hookedAudioInit(initClass *cls, uint64_t unk, uint64_t unk2) { //printf("[DivaSound] Loaded\n"); printf("[DivaSound] Output config: %S %dch %dbit\n", backendName, nChannels, bitDepth); divaAudCls = cls; divaAudCls->breakDword = 0; if (useOldInit) { printf("[DivaSound] Using old initialisation method\n"); // let the game set some stuff up // might be necessary sometimes divaAudioInit(divaAudCls, unk, unk2); //printf("[DivaSound] Game audio initialised\n"); } else { // setup some variables instead of using the original init function divaAudCls->mixer = new audioMixer(); divaAudCls->mixer->volume_master = 1.0f; divaAudCls->mixer->volume_channels[0] = 1.0f; divaAudCls->mixer->volume_channels[1] = 1.0f; divaAudCls->mixer->volume_channels[2] = 1.0f; divaAudCls->mixer->volume_channels[3] = 1.0f; divaAudCls->mixer->audioClass = divaAudCls; mtx_init(&divaAudCls->mixer->volume_mutex, 2); } divaAudioMixCls = divaAudCls->mixer; divaAudCls->channels = nChannels; // this could replace stereo patch divaAudCls->rate = 44100; // really does nothing divaAudCls->depth = bitDepth; // setting this to something other than 16 just removes output if (useAsio) { if (!asioInit(true)) return; BASS_ASIO_INFO asioInfo; BASS_ASIO_GetInfo(&asioInfo); double actualRate = BASS_ASIO_GetRate(); printf("[DivaSound] Output buffer size: %d (%dms at %dHz)\n", asioInfo.bufpref, (int)(asioInfo.bufpref * 1000 / actualRate), (int)actualRate); divaBufSizeInFrames = (int)(asioInfo.bufpref * 44100 / actualRate); divaBufSizeInMilliseconds = divaBufSizeInFrames * 1000 / 44100; printf("[DivaSound] PDAFT buffer size: %d (%dms at %dHz)\n", divaBufSizeInFrames, divaBufSizeInMilliseconds, 44100); divaAudioAllocMixer(divaAudioMixCls, unk, unk2, divaBufSizeInFrames); printf("[DivaSound] Created internal audio mixer\n"); if (!BASS_ASIO_Start(asioInfo.bufpref, 1)) { printf("[DivaSound] Failed to start playback device\n"); BASS_ASIO_Free(); return; } printf("[DivaSound] Started playback\n"); } else { if (!maInit()) return; if (device.playback.internalSampleRate) { maInternalBufferSizeInMilliseconds = device.playback.internalBufferSizeInFrames * 1000 / device.playback.internalSampleRate; // because miniaudio doesn't seem to have this printf("[DivaSound] Output buffer size: %d (%dms at %dHz)\n", device.playback.internalBufferSizeInFrames, maInternalBufferSizeInMilliseconds, device.playback.internalSampleRate); printf("[DivaSound] Buffer periods: %d\n", device.playback.internalPeriods); divaBufSizeInFrames = device.playback.internalBufferSizeInFrames * device.sampleRate / device.playback.internalSampleRate; // +128; // 128 is just a bit extra in case resampling needs it or something. idk divaBufSizeInMilliseconds = divaBufSizeInFrames * 1000 / device.sampleRate; printf("[DivaSound] PDAFT buffer size: %d (%dms at %dHz)\n", divaBufSizeInFrames, divaBufSizeInMilliseconds, device.sampleRate); } else { printf("[DivaSound] Unable to determine output sample rate. Assuming 44100Hz.\n"); maInternalBufferSizeInMilliseconds = device.playback.internalBufferSizeInFrames * 1000 / 44100; // because miniaudio doesn't seem to have this printf("[DivaSound] Output buffer size: %d (%dms at %dHz)\n", device.playback.internalBufferSizeInFrames, maInternalBufferSizeInMilliseconds, 44100); printf("[DivaSound] Buffer periods: %d\n", device.playback.internalPeriods); divaBufSizeInFrames = device.playback.internalBufferSizeInFrames * device.sampleRate / 44100; divaBufSizeInMilliseconds = divaBufSizeInFrames * 1000 / device.sampleRate; printf("[DivaSound] PDAFT buffer size: %d (%dms at %dHz)\n", divaBufSizeInFrames, divaBufSizeInMilliseconds, device.sampleRate); } divaAudioAllocMixer(divaAudioMixCls, unk, unk2, divaBufSizeInFrames); printf("[DivaSound] Created internal audio mixer\n"); if (ma_device_start(&device) != MA_SUCCESS) { printf("[DivaSound] Failed to start playback device\n"); ma_device_uninit(&device); ma_context_uninit(&context); return; } printf("[DivaSound] Started playback\n"); } divaAudCls->hCallback = new HANDLE(); *divaAudCls->hCallback = (HANDLE)1; // this *should* be an actual thread handle, but I bypassed the destruction stuff anyway lol divaAudCls->buffer_size = divaBufSizeInFrames; //loopThread = std::thread(resizeTestLoop); } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { loadConfig(); if (useOldInit) { // these patches are only needed if calling the game's own init function // they shouldn't be necessary anymore // force stereo mode InjectCode((void*)0x0000000140A860C0, { 0x02 }); // remove a call to get device info and skip the check for it NopBytes((void*)0x140626b56, 7); NopBytes((void*)0x140626b8a, 8); InjectCode((void*)0x140626b8a, { 0x48, 0x83, 0xEF, 0x18 }); // fix value of RDI because I changed the flow here NopBytes((void*)0x140626ba9, 11); // return from the original init early InjectCode((void*)0x0000000140626C29, { 0x48, 0xE9 }); } // skip some thread deinitialisation stuff I don't care about InjectCode((void*)0x0000000140625F42, { 0xEB, 0x29 }); DisableThreadLibraryCalls(hModule); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)divaAudioInit, (PVOID)hookedAudioInit); DetourTransactionCommit(); } else if (ul_reason_for_call == DLL_PROCESS_DETACH) { closeDevice(); } return TRUE; } using namespace PluginConfig; void OpenWiki() { ShellExecuteW(NULL, L"open", L"https://github.com/somewhatlurker/DivaSound/wiki", NULL, NULL, SW_SHOW); } PluginConfigOption config[] = { { CONFIG_DROPDOWN_TEXT, new PluginConfigDropdownTextData{L"backend", L"general", CONFIG_FILE, L"Backend:", L"Sets the audio output protocol.", L"WASAPI", std::vector({ L"WASAPI", L"WASAPI_Exclusive" }), true, false } }, { CONFIG_DROPDOWN_NUMBER, new PluginConfigDropdownNumberData{ L"channels", L"general", CONFIG_FILE, L"Channels:", L"Sets the number of channels.", 2, std::vector({ 2, 4 }), false } }, { CONFIG_DROPDOWN_NUMBER, new PluginConfigDropdownNumberData{ L"bit_depth", L"general", CONFIG_FILE, L"Bit Depth:", L"Sets the audio sample format.\n(32 uses floating point samples)", 16, std::vector({ 16, 24, 32 }), false } }, { CONFIG_BOOLEAN, new PluginConfigBooleanData{ L"alternate_init", L"general", CONFIG_FILE, L"Use new init", L"Use the full initialisation replacement.\nTry unchecking this if DivaSound seems to cause crashes.", true, false } }, { CONFIG_SPACER, new PluginConfigSpacerData{ 8 } }, { CONFIG_GROUP_START, new PluginConfigGroupData{ L"WASAPI Buffer Settings", 75 } }, { CONFIG_NUMERIC, new PluginConfigNumericData{ L"buffer_size", L"buffer", CONFIG_FILE, L"Target Buffer Size:", L"Sets the target buffer size in ms.\nWASAPI will often ignore this and adapt to your hardware config automatically.", 10, 1, 100 } }, { CONFIG_NUMERIC, new PluginConfigNumericData{ L"periods", L"buffer", CONFIG_FILE, L"Buffer Periods:", L"Sets how often the buffer should be filled.\nFewer periods usually allows for lower latency, but lowering this may cause issues.", 2, 1, 8 } }, { CONFIG_GROUP_END, NULL }, { CONFIG_SPACER, new PluginConfigSpacerData{ 8 } }, { CONFIG_SPACER, new PluginConfigSpacerData{ 0 } }, // 0px spacers are placeholders for ASIO config { CONFIG_SPACER, new PluginConfigSpacerData{ 0 } }, { CONFIG_SPACER, new PluginConfigSpacerData{ 0 } }, { CONFIG_SPACER, new PluginConfigSpacerData{ 0 } }, { CONFIG_SPACER, new PluginConfigSpacerData{ 0 } }, { CONFIG_SPACER, new PluginConfigSpacerData{ 0 } }, { CONFIG_BUTTON, new PluginConfigButtonData{ L"Help", L"Get help on the DivaSound wiki.", OpenWiki } }, { CONFIG_SPACER, new PluginConfigSpacerData{ 8 } }, }; extern "C" __declspec(dllexport) LPCWSTR GetPluginName(void) { return L"DivaSound"; } extern "C" __declspec(dllexport) LPCWSTR GetPluginDescription(void) { return L"DivaSound Plugin by somewhatlurker\n\nDivaSound replaces the game's original audio output,\nimproving device support and adding configurable options."; } extern "C" __declspec(dllexport) PluginConfigArray GetPluginOptions(void) { if (BASS_ASIO_GetDeviceInfo != NULL && BASS_ASIO_SetUnicode != NULL) { if (BASS_ASIO_SetUnicode(true)) // ensure strings are UTF-16 { std::vector devices; BASS_ASIO_DEVICEINFO info; for (int i = 0; BASS_ASIO_GetDeviceInfo(i, &info); i++) { devices.push_back(_wcsdup((WCHAR*)info.name)); } // make sure user-set device isn't out of bounds int asioDevice = GetPrivateProfileIntW(L"asio", L"device", -1, CONFIG_FILE); while (asioDevice + 1 > devices.size()) { devices.push_back(L"---"); } if (devices.size() > 0) { ((PluginConfigDropdownTextData*)config[0].data)->valueStrings.push_back(L"ASIO"); const int cfgPos = 10; delete config[cfgPos].data; delete config[cfgPos + 1].data; delete config[cfgPos + 2].data; delete config[cfgPos + 3].data; delete config[cfgPos + 4].data; delete config[cfgPos + 5].data; config[cfgPos] = { CONFIG_GROUP_START, new PluginConfigGroupData{ L"ASIO Settings", 90 } }; config[cfgPos + 1] = { CONFIG_BOOLEAN, new PluginConfigBooleanData{ L"show_config", L"asio", CONFIG_FILE, L"Show ASIO config", L"Show the ASIO driver config window when the game opens.\nUse this to adjust output settings, then disable it.\n(ASIO devices are unaffected by the normal buffer settings)", false, false } }; config[cfgPos + 2] = { CONFIG_BOOLEAN, new PluginConfigBooleanData{ L"auto_device", L"asio", CONFIG_FILE, L"Automatic ASIO device", L"Automatically choose the first available device for ASIO backend.\nDisable this to use a manually choose a device.", true, false } }; config[cfgPos + 3] = { CONFIG_DROPDOWN_INDEX, new PluginConfigDropdownIndexData{ L"device", L"asio", CONFIG_FILE, L"ASIO Device:", L"Sets the ASIO device.\nMake sure automatic device selection is turned off to enable this.", 0, devices } }; config[cfgPos + 4] = { CONFIG_GROUP_END, NULL }; config[cfgPos + 5] = { CONFIG_SPACER, new PluginConfigSpacerData{ 8 } }; } } } return PluginConfigArray{ _countof(config), config }; }