Skip to content

Instantly share code, notes, and snippets.

@TronicLabs
Forked from t-mat/minimal_vst2x_host.cpp
Created November 15, 2018 16:06
Show Gist options
  • Select an option

  • Save TronicLabs/06a5ce4be3de1a09f92154bf430d3090 to your computer and use it in GitHub Desktop.

Select an option

Save TronicLabs/06a5ce4be3de1a09f92154bf430d3090 to your computer and use it in GitHub Desktop.
WIN32 : Minimal VST 2.x host in C++11.
// WIN32 : Minimal VST 2.x host.
//
// This is a simplified version of the following project by hotwatermorning :
// A sample VST Host Application for C++ Advent Calendar 2013 5th day.
// https://github.com/hotwatermorning/VstHostDemo
//
#include <windows.h>
#include <process.h>
#include <mmdeviceapi.h>
#include <audioclient.h>
#include <stdio.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
// "aeffectx.h" is part of VST SDK. You can download VST SDK from
// Steinberg SDK Download Portal :
// http://www.steinberg.net/nc/en/company/developers/sdk_download_portal.html
#pragma warning(push)
#pragma warning(disable : 4996)
#include "VST3 SDK/pluginterfaces/vst2.x/aeffectx.h"
#pragma warning(pop)
class VstPlugin {
public:
VstPlugin(
const wchar_t* vstModulePath, size_t sampleRate, size_t blockSize
) {
init(vstModulePath, sampleRate, blockSize);
}
~VstPlugin() {
cleanup();
}
AEffect* getEffect() { return aEffect; }
AEffect* getEffect() const { return aEffect; }
intptr_t dispatcher(
int32_t opcode, int32_t index, intptr_t value, void *ptr, float opt
) const {
auto* e = getEffect();
return e->dispatcher(e, opcode, index, value, ptr, opt);
}
void processReplacing(
float** inputs, float** outputs, int32_t sampleFrames
) const {
auto* e = getEffect();
e->processReplacing(e, inputs, outputs, sampleFrames);
}
int32_t getFlags() const { return getEffect()->flags; }
bool getFlags(int32_t m) const { return (getFlags() & m) == m; }
bool flagsHasEditor() const { return getFlags(effFlagsHasEditor); }
bool flagsIsSynth() const { return getFlags(effFlagsIsSynth); }
size_t getSampleRate() const { return sampleRate; }
size_t getBlockSize() const { return blockSize; }
void vstOpen() const { dispatcher(effOpen, 0, 0, 0, 0); }
void vstClose() const { dispatcher(effClose, 0, 0, 0, 0); }
void vstSetProgram(int pn) const { dispatcher(effSetProgram, 0, pn, 0, 0); }
int vstGetProgram() const { return static_cast<int>(dispatcher(effGetProgram, 0, 0, 0, 0)); }
void vstSetSampleRate(float f) const { dispatcher(effSetSampleRate, 0, 0, 0, f); }
void vstSetBlockSize(int32_t bs) const { dispatcher(effSetBlockSize, 0, bs, 0, 0); }
void vstMainsChanged(bool sw) const { dispatcher(effMainsChanged, 0, sw ? 1 : 0, 0, 0); }
void vstEditOpen(void* ptr) const { dispatcher(effEditOpen, 0, 0, ptr, 0); }
void vstEditClose() const { dispatcher(effEditClose, 0, 0, 0, 0); }
void vstEditIdle() const { dispatcher(effEditIdle, 0, 0, 0, 0); }
void vstProcessEvents(VstEvents* e) const { dispatcher(effProcessEvents, 0, 0, e, 0); }
void vstStartProcess() const { dispatcher(effStartProcess, 0, 0, 0, 0); }
void vstStopProcess() const { dispatcher(effStopProcess, 0, 0, 0, 0); }
bool vstCanBeAutomated(int i) const { return 0 != dispatcher(effCanBeAutomated, i, 0, 0, 0); }
RECT vstEditGetRect() const {
RECT rc {};
ERect* erc = nullptr;
dispatcher(effEditGetRect, 0, 0, &erc, 0);
rc.left = erc->left;
rc.top = erc->top;
rc.right = erc->right;
rc.bottom = erc->bottom;
return rc;
}
void vstSetProcessPrecision(bool bDouble) const {
const auto c = bDouble ? kVstProcessPrecision64 : kVstProcessPrecision32;
dispatcher(effSetProcessPrecision, 0, c, 0, 0);
}
void openEditor(HWND hWnd) {
if(! isEditorOpened()) {
editorHwnd = hWnd;
vstEditOpen(hWnd);
auto rc = vstEditGetRect();
resizeEditor(rc);
ShowWindow(hWnd, SW_SHOW);
}
}
void closeEditor() {
if(isEditorOpened()) {
vstEditClose();
editorHwnd = nullptr;
}
}
bool isEditorOpened() const {
return editorHwnd != nullptr;
}
void resizeEditor(const RECT& clientRc) const {
if(isEditorOpened()) {
auto rc = clientRc;
const auto style = GetWindowLongPtr(editorHwnd, GWL_STYLE);
const auto exStyle = GetWindowLongPtr(editorHwnd, GWL_EXSTYLE);
const BOOL fMenu = GetMenu(editorHwnd) != nullptr;
AdjustWindowRectEx(&rc, style, fMenu, exStyle);
MoveWindow(
editorHwnd, 0, 0, rc.right-rc.left, rc.bottom-rc.top, TRUE
);
}
}
void setWindowSize(size_t width, size_t height) {
RECT rc {};
GetWindowRect(editorHwnd, &rc);
rc.right = rc.left + width;
rc.bottom = rc.top + height;
resizeEditor(rc);
}
const char* getDirectory() const {
return directoryMultiByte.c_str();
}
void note(int midiChannel, int noteNumber, bool onOff, int velocity) {
const int base = onOff ? 0x90 : 0x80;
VstMidiEvent event {};
event.type = kVstMidiType;
event.byteSize = sizeof(VstMidiEvent);
event.flags = kVstMidiEventIsRealtime;
event.midiData[0] = static_cast<char>(base + midiChannel);
event.midiData[1] = static_cast<char>(noteNumber);
event.midiData[2] = static_cast<char>(velocity);
{
auto lock = lockVstMidiEvents();
vstMidiEvents.push_back(event);
}
}
void processEvents() {
procVstMidiEvents.clear();
{
auto lock = lockVstMidiEvents();
std::swap(procVstMidiEvents, vstMidiEvents);
}
if(procVstMidiEvents.empty()) {
return;
}
const auto n = procVstMidiEvents.size();
const auto bytes = sizeof(VstEvents) + sizeof(VstEvent*) * std::max<size_t>(n, 2) - 2;
vstEventBuffer.resize(bytes);
auto* ve = reinterpret_cast<VstEvents*>(vstEventBuffer.data());
ve->numEvents = n;
ve->reserved = 0;
for(size_t i = 0; i < n; ++i) {
ve->events[i] = reinterpret_cast<VstEvent*>(&procVstMidiEvents[i]);
}
vstProcessEvents(ve);
}
float** processAudio(size_t frameCount, size_t& outputFrameCount) {
const auto maxFrameCount = outputBuffer.size() / nChannel;
if(frameCount > maxFrameCount) {
frameCount = maxFrameCount;
}
outputFrameCount = frameCount;
processReplacing(inputBufferHeads.data(), outputBufferHeads.data(), frameCount);
return outputBufferHeads.data();
}
private:
void init(const wchar_t* vstModulePath, size_t sampleRate, size_t blockSize) {
{
wchar_t buf[MAX_PATH+1] {};
wchar_t* namePtr = nullptr;
const auto r = GetFullPathName(vstModulePath, _countof(buf), buf, &namePtr);
if(r && namePtr) {
*namePtr = 0;
directoryUtf16 = buf;
char mbBuf[_countof(buf) * 4] {};
const auto s = WideCharToMultiByte(
CP_OEMCP, 0, directoryUtf16.c_str(), -1, mbBuf, sizeof(mbBuf), 0, 0);
if(s) {
directoryMultiByte = mbBuf;
}
}
}
this->sampleRate = sampleRate;
this->blockSize = blockSize;
hModule = LoadLibrary(vstModulePath);
typedef AEffect* (VstPluginEntryProc)(audioMasterCallback);
auto* vstEntryProc = reinterpret_cast<VstPluginEntryProc*>(GetProcAddress(hModule, "VSTPluginMain"));
if(!vstEntryProc) {
vstEntryProc = reinterpret_cast<VstPluginEntryProc*>(GetProcAddress(hModule, "main"));
}
if(! vstEntryProc) {
throw std::runtime_error("entry point not found");
}
aEffect = vstEntryProc(hostCallback_static);
if(!aEffect || aEffect->magic != kEffectMagic) {
throw std::runtime_error("not a vst plugin");
}
aEffect->user = this;
{
const int ni = aEffect->numInputs;
inputBuffer.resize(ni * blockSize);
for(int i = 0; i < ni; ++i) {
inputBufferHeads.push_back(inputBuffer.data() + i * blockSize);
}
}
{
const int no = aEffect->numOutputs;
outputBuffer.resize(no * blockSize);
for(int i = 0; i < no; ++i) {
outputBufferHeads.push_back(outputBuffer.data() + i * blockSize);
}
}
vstOpen();
vstSetSampleRate(static_cast<float>(sampleRate));
vstSetBlockSize(blockSize);
vstSetProcessPrecision(false);
vstMainsChanged(true);
vstStartProcess();
}
void cleanup() {
closeEditor();
vstStopProcess();
vstMainsChanged(false);
vstClose();
}
std::unique_lock<std::mutex> lockVstMidiEvents() const {
return std::unique_lock<std::mutex>(vstMidiEventsMutex);
}
static VstIntPtr hostCallback_static(
AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void *ptr, float opt
) {
VstIntPtr result = 0;
if(effect == nullptr || effect->user == nullptr) {
if(audioMasterVersion == opcode) {
result = kVstVersion;
}
} else {
auto* that = static_cast<VstPlugin*>(effect->user);
result = that->hostCallback(opcode, index, value, ptr, opt);
}
return result;
}
VstIntPtr hostCallback(
VstInt32 opcode, VstInt32 index, VstIntPtr value, void *ptr, float opt
) {
opt;
switch(opcode) {
default: break;
case audioMasterVersion: return kVstVersion;
case audioMasterCurrentId: return getEffect()->uniqueID;
case audioMasterGetSampleRate: return getSampleRate();
case audioMasterGetBlockSize: return getBlockSize();
case audioMasterGetCurrentProcessLevel: return kVstProcessLevelUnknown;
case audioMasterGetAutomationState: return kVstAutomationOff;
case audioMasterGetLanguage: return kVstLangEnglish;
case audioMasterGetVendorVersion: return 1;
case audioMasterGetDirectory: return reinterpret_cast<VstIntPtr>(getDirectory());
case audioMasterIdle:
if(isEditorOpened()) {
vstEditIdle();
}
return 0;
case audioMasterGetTime:
{
VstTimeInfo ti {};
ti.sampleRate = getSampleRate();
ti.nanoSeconds = GetTickCount() * 1000.0 * 1000.0;
ti.tempo = 120.0;
ti.timeSigNumerator = 4;
ti.timeSigDenominator = 4;
ti.smpteFrameRate = kVstSmpte24fps;
ti.flags = (kVstNanosValid | kVstPpqPosValid
| kVstTempoValid | kVstTimeSigValid);
timeinfo = ti;
}
return reinterpret_cast<VstIntPtr>(&timeinfo);
case audioMasterSizeWindow:
setWindowSize(static_cast<size_t>(index), static_cast<size_t>(value));
case audioMasterGetVendorString:
strcpy_s(static_cast<char*>(ptr), kVstMaxVendorStrLen, "TEST VENDOR");
return 1;
case audioMasterGetProductString:
strcpy_s(static_cast<char*>(ptr), kVstMaxVendorStrLen, "TEST PRODUCT");
return 1;
case audioMasterCanDo:
{
static const char* hostCapabilities[] = {
"sendVstEvents",
"sendVstMidiEvents",
"sizeWindow",
"startStopProcess",
"sendVstMidiEventFlagIsRealtime"
};
for(auto e : hostCapabilities) {
if(strcmp(e, static_cast<const char*>(ptr)) == 0) {
return 1;
}
}
}
return 0;
}
return 0;
}
protected:
HMODULE hModule { nullptr };
AEffect* aEffect { nullptr };
size_t sampleRate { 44100 };
size_t blockSize { 1024 };
HWND editorHwnd { nullptr };
VstTimeInfo timeinfo {};
std::wstring directoryUtf16;
std::string directoryMultiByte;
size_t nChannel { 2 };
std::vector<float> outputBuffer;
std::vector<float*> outputBufferHeads;
std::vector<float> inputBuffer;
std::vector<float*> inputBufferHeads;
std::mutex mutable vstMidiEventsMutex;
std::vector<VstMidiEvent> vstMidiEvents;
std::vector<VstMidiEvent> procVstMidiEvents;
std::vector<char> vstEventBuffer;
};
class Wasapi {
public:
using RefillCallbackFunc = std::function<bool(float*, uint32_t, const WAVEFORMATEX*)>;
Wasapi() {}
~Wasapi() { close(); }
void open(RefillCallbackFunc refillCallbackFunc, int hnsBufferDuration = 30 * 10000) {
HRESULT hr;
close();
this->refillCallbackFunc = refillCallbackFunc;
hClose = CreateEventEx(nullptr, nullptr, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
hRefillEvent = CreateEventEx(nullptr, nullptr, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
hr = CoCreateInstance(
__uuidof(MMDeviceEnumerator)
, nullptr
, CLSCTX_INPROC_SERVER
, IID_PPV_ARGS(&mmDeviceEnumerator)
);
assert(SUCCEEDED(hr));
hr = mmDeviceEnumerator->GetDefaultAudioEndpoint(
eRender
, eMultimedia
, &mmDevice
);
assert(SUCCEEDED(hr));
hr = mmDevice->Activate(
__uuidof(IAudioClient)
, CLSCTX_INPROC_SERVER
, nullptr
, reinterpret_cast<void**>(&audioClient)
);
assert(SUCCEEDED(hr));
audioClient->GetMixFormat(&mixFormat);
if(mixFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
mixFormatEx = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(mixFormat);
}
hr = audioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED
, AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST
, hnsBufferDuration
, 0
, mixFormat
, nullptr
);
assert(SUCCEEDED(hr));
hr = audioClient->GetService(
__uuidof(IAudioRenderClient)
, reinterpret_cast<void**>(&audioRenderClient)
);
assert(SUCCEEDED(hr));
hr = audioClient->GetBufferSize(&bufferFrameCount);
assert(SUCCEEDED(hr));
hr = audioClient->SetEventHandle(hRefillEvent);
assert(SUCCEEDED(hr));
// initial zero fill
{
BYTE* data = nullptr;
hr = audioRenderClient->GetBuffer(bufferFrameCount, &data);
assert(SUCCEEDED(hr));
hr = audioRenderClient->ReleaseBuffer(bufferFrameCount, AUDCLNT_BUFFERFLAGS_SILENT);
assert(SUCCEEDED(hr));
}
unsigned threadId = 0;
hThread = reinterpret_cast<HANDLE>(_beginthreadex(
nullptr, 0, threadFunc_static, reinterpret_cast<void*>(this), 0, &threadId
));
hr = audioClient->Start();
assert(SUCCEEDED(hr));
}
void close() {
if(hClose) {
SetEvent(hClose);
if(hThread) {
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
hThread = nullptr;
}
CloseHandle(hClose);
hClose = nullptr;
}
if(hRefillEvent) {
CloseHandle(hRefillEvent);
hRefillEvent = nullptr;
}
if(mixFormat) { CoTaskMemFree(mixFormat); mixFormat = nullptr; }
mixFormatEx = nullptr;
if(audioRenderClient) { audioRenderClient->Release(); audioRenderClient = nullptr; }
if(audioClient) { audioClient->Release(); audioClient = nullptr; }
if(mmDevice) { mmDevice->Release(); mmDevice = nullptr; }
if(mmDeviceEnumerator) { mmDeviceEnumerator->Release(); mmDeviceEnumerator = nullptr; }
}
static unsigned WINAPI threadFunc_static(void* arg) {
auto* that = reinterpret_cast<Wasapi*>(arg);
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
that->threadFunc();
CoUninitialize();
return 0;
}
void threadFunc() {
HANDLE events[2] = { hClose, hRefillEvent };
for(bool run = true; run; ) {
const auto r = WaitForMultipleObjects(_countof(events), events, FALSE, INFINITE);
switch(r) {
default:
break;
case WAIT_OBJECT_0: // hClose
run = false;
break;
case WAIT_OBJECT_0 + 1: // hRefillEvent
onRefill();
break;
}
}
}
void onRefill() {
UINT32 paddingFrameCount = 0;
audioClient->GetCurrentPadding(&paddingFrameCount);
const auto availableFrameCount = bufferFrameCount - paddingFrameCount;
BYTE* data = nullptr;
audioRenderClient->GetBuffer(availableFrameCount, &data);
const auto b = refillCallbackFunc(
reinterpret_cast<float*>(data)
, availableFrameCount
, mixFormat
);
DWORD dwReleaseFlags = 0;
if(! b) {
dwReleaseFlags = AUDCLNT_BUFFERFLAGS_SILENT;
}
audioRenderClient->ReleaseBuffer(availableFrameCount, dwReleaseFlags);
}
private:
HANDLE hThread { nullptr };
IMMDeviceEnumerator* mmDeviceEnumerator { nullptr };
IMMDevice* mmDevice { nullptr };
IAudioClient* audioClient { nullptr };
IAudioRenderClient* audioRenderClient { nullptr };
WAVEFORMATEX* mixFormat { nullptr };
WAVEFORMATEXTENSIBLE* mixFormatEx { nullptr };
HANDLE hRefillEvent { nullptr };
HANDLE hClose { nullptr };
UINT32 bufferFrameCount { 0 };
RefillCallbackFunc refillCallbackFunc {};
};
void mainLoop(const std::wstring& dllFilename) {
enum {
SAMPLE_RATE = 44100,
BLOCK_SIZE = 1024,
};
HWND const hwnd = GetConsoleWindow();
auto vstPlugin = std::unique_ptr<VstPlugin>(new VstPlugin {
dllFilename.c_str(), SAMPLE_RATE, BLOCK_SIZE
});
if(!vstPlugin->flagsIsSynth()) {
MessageBox(nullptr, L"Specified DLL is not VST Synth", dllFilename.c_str(), MB_ICONSTOP);
return;
}
if(vstPlugin->flagsHasEditor()) {
WNDCLASSEX wcex = { 0 };
wcex.cbSize = sizeof(wcex);
wcex.lpfnWndProc = DefWindowProc;
wcex.hInstance = GetModuleHandle(0);
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszClassName = L"TEST APP VST WINDOW";
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
RegisterClassEx(&wcex);
const auto style = WS_BORDER | WS_CAPTION;
HWND dummyHwnd = CreateWindow(
wcex.lpszClassName, dllFilename.c_str(), style, 0, 0, 320, 240, hwnd, 0, 0, 0
);
vstPlugin->openEditor(dummyHwnd);
}
const auto refillCallback = [&](
float* const data, uint32_t availableFrameCount, const WAVEFORMATEX* const mixFormat
) {
const auto nChannels = mixFormat->nChannels;
vstPlugin->processEvents();
const auto vstSamplesPerBlock = vstPlugin->getBlockSize();
int ofs = 0;
while(availableFrameCount > 0) {
size_t outputFrameCount = 0;
float** const vstOutput = vstPlugin->processAudio(availableFrameCount, outputFrameCount);
// VST vstOutput[][] format :
// vstOutput[0][0..1023] : Left [0..1023]
// vstOutput[1][0..1023] : Right [0..1023]
// vstOutput[2][0..1023] : Left [1024..2047]
// vstOutput[3][0..1023] : Right [1024..2047]
// wasapi data[] format :
// data[0..1023] : Left[0], Right[0], Left[1], Right[1] .. Left[511], Right[511]
// data[1024..2047] : Left[512], Right[512], Left[513], Right[513] .. Left[1023], Right[1023]
const auto nFrame = outputFrameCount;
for(size_t iFrame = 0; iFrame < nFrame; ++iFrame) {
for(size_t iChannel = 0; iChannel < nChannels; ++iChannel) {
const int vstOutputPage = (iFrame / vstSamplesPerBlock) * nChannels + iChannel;
const int vstOutputIndex = (iFrame % vstSamplesPerBlock);
const int wasapiWriteIndex = iFrame * nChannels + iChannel;
*(data + ofs + wasapiWriteIndex) = vstOutput[vstOutputPage][vstOutputIndex];
}
}
availableFrameCount -= nFrame;
ofs += nFrame * nChannels;
}
return true;
};
const int latencyInHns = 100 * 10000; // 100 ms
Wasapi wasapi;
wasapi.open(refillCallback, latencyInHns);
struct Key {
Key(int vk, int midiNote) : vk{vk}, midiNote{midiNote} {}
int vk { 0 };
int midiNote { 0 };
bool status { false };
};
std::vector<Key> keys;
keys.emplace_back('Z', 48);
keys.emplace_back('X', 50);
keys.emplace_back('C', 52);
keys.emplace_back('V', 53);
keys.emplace_back('B', 55);
keys.emplace_back('N', 57);
keys.emplace_back('M', 59);
keys.emplace_back('K', 60);
for(const auto& k : keys) {
printf("[%c] = %d\n", k.vk, k.midiNote);
}
for(bool run = true; run; Sleep(10)) {
MSG msg {};
while(BOOL b = PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) {
if(b == -1) {
run = false;
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
for(auto& k : keys) {
const auto on = (GetAsyncKeyState(k.vk) & 0x8000) != 0;
if(k.status != on) {
k.status = on;
const int midiChannel = 0;
const int noteNumber = k.midiNote;
const int velocity = 100;
vstPlugin->note(midiChannel, noteNumber, on, velocity);
}
}
}
}
int main() {
CoInitializeEx(NULL, COINIT_MULTITHREADED);
std::wstring dllFilename;
{
wchar_t fn[MAX_PATH+1] {};
OPENFILENAME ofn { sizeof(ofn) };
ofn.lStructSize = sizeof(ofn);
ofn.lpstrFile = fn;
ofn.lpstrFilter = L"VSTi DLL(*.dll)\0*.dll\0All Files(*.*)\0*.*\0\0";
ofn.nMaxFile = _countof(fn);
ofn.lpstrTitle = L"Select VST DLL";
ofn.Flags = OFN_FILEMUSTEXIST | OFN_ENABLESIZING;
GetOpenFileName(&ofn);
dllFilename = fn;
}
if(! dllFilename.empty()) {
mainLoop(dllFilename);
}
CoUninitialize();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment