Files
hot-step-cpp-ROCm/engine/tools/vst-host.cpp
T
2026-08-16 18:24:52 +07:00

1313 lines
49 KiB
C++

// vst-host.cpp — HOT-Step VST3 Host Tool
//
// Modes:
// --scan Scan for VST3 plugins, output JSON
// --gui --plugin <path> Open plugin GUI in a native window
// --process --plugin <p> --input <i> --output <o> Offline processing
// --process-chain --chain <json> --input <i> --output <o> Chain mode
//
// Part of the HOT-Step 9000 CPP engine.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <fstream>
#include <algorithm>
#include <mutex>
#include <atomic>
// Windows headers MUST come before VST3 SDK to avoid IConnectionPoint collision
// (ocidl.h defines COM IConnectionPoint, VST3 has Steinberg::Vst::IConnectionPoint)
#ifdef _WIN32
#include <windows.h>
#include <objbase.h>
#include <mmdeviceapi.h>
#include <audioclient.h>
#endif
// VST3 SDK hosting
#include "public.sdk/source/vst/hosting/module.h"
#include "public.sdk/source/vst/hosting/hostclasses.h"
#include "public.sdk/source/vst/hosting/plugprovider.h"
#include "public.sdk/source/vst/hosting/processdata.h"
#include "public.sdk/source/vst/hosting/parameterchanges.h"
#include "public.sdk/source/vst/hosting/eventlist.h"
#include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/ivstcomponent.h"
#include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/vsttypes.h"
#include "pluginterfaces/gui/iplugview.h"
#include "pluginterfaces/base/funknown.h"
#include "public.sdk/source/common/memorystream.h"
#include "pluginterfaces/base/ibstream.h"
// yyjson for JSON output
#include "vendor/yyjson/yyjson.h"
// WAV I/O (our existing header)
#include "audio-io.h"
using namespace Steinberg;
using namespace Steinberg::Vst;
// Global host context
static FUnknown* gHostContext = nullptr;
// ── Helpers ──────────────────────────────────────────────────────────────────
static void init_host_context() {
if (!gHostContext) {
gHostContext = new HostApplication();
// Set the global plugin context so PlugProvider passes it to IComponent::initialize()
PluginContextFactory::instance().setPluginContext(gHostContext);
#ifdef _WIN32
// Suppress crash/error dialogs during plugin loading
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);
// COM is needed by some plugins and by the module scanner (shell links)
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
#endif
}
}
// ── Scan Mode ────────────────────────────────────────────────────────────────
// Flush stderr before each module load attempt so crash diagnosis is possible
static int cmd_scan() {
init_host_context();
auto paths = VST3::Hosting::Module::getModulePaths();
fprintf(stderr, "[vst-host] Scan found %zu module path(s)\n", paths.size());
yyjson_mut_doc * doc = yyjson_mut_doc_new(nullptr);
yyjson_mut_val * root = yyjson_mut_arr(doc);
yyjson_mut_doc_set_root(doc, root);
for (const auto & path : paths) {
std::string error;
VST3::Hosting::Module::Ptr module;
try {
module = VST3::Hosting::Module::create(path, error);
} catch (...) {
continue;
}
if (!module) continue;
auto & factory = module->getFactory();
auto classInfos = factory.classInfos();
int audio_effects = 0;
for (auto & classInfo : classInfos) {
if (classInfo.category() != kVstAudioEffectClass) continue;
audio_effects++;
yyjson_mut_val * obj = yyjson_mut_obj(doc);
yyjson_mut_obj_add_strcpy(doc, obj, "name", classInfo.name().c_str());
yyjson_mut_obj_add_strcpy(doc, obj, "vendor", classInfo.vendor().c_str());
yyjson_mut_obj_add_strcpy(doc, obj, "version", classInfo.version().c_str());
yyjson_mut_obj_add_strcpy(doc, obj, "path", path.c_str());
yyjson_mut_obj_add_strcpy(doc, obj, "uid", classInfo.ID().toString().c_str());
yyjson_mut_obj_add_strcpy(doc, obj, "subcategories",
classInfo.subCategoriesString().c_str());
yyjson_mut_arr_append(root, obj);
}
fprintf(stderr, "OK (%d effects)\n", audio_effects);
}
size_t json_len = 0;
char * json = yyjson_mut_write(doc, YYJSON_WRITE_PRETTY, &json_len);
if (json && json_len > 0) {
#ifdef _WIN32
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD written = 0;
WriteFile(hStdout, json, (DWORD)json_len, &written, nullptr);
WriteFile(hStdout, "\n", 1, &written, nullptr);
FlushFileBuffers(hStdout);
#else
fputs(json, stdout);
fputs("\n", stdout);
fflush(stdout);
#endif
free(json);
}
yyjson_mut_doc_free(doc);
return 0;
}
// ── Process Mode ─────────────────────────────────────────────────────────────
// Minimal IComponentHandler — lets JUCE plugins push param changes from GUI
// to the audio processor in realtime via inputParameterChanges.
// Without this, GUI knob moves never reach processBlock.
class ParamChangeHandler final : public IComponentHandler {
public:
struct Change { ParamID id; ParamValue value; };
tresult PLUGIN_API beginEdit(ParamID) override { return kResultOk; }
tresult PLUGIN_API performEdit(ParamID id, ParamValue value) override {
std::lock_guard<std::mutex> lock(mtx);
// Overwrite existing pending change for this param (last value wins)
for (auto & c : pending) { if (c.id == id) { c.value = value; return kResultOk; } }
pending.push_back({ id, value });
return kResultOk;
}
tresult PLUGIN_API endEdit(ParamID) override { return kResultOk; }
tresult PLUGIN_API restartComponent(int32) override { return kResultOk; }
tresult PLUGIN_API queryInterface(const TUID _iid, void** obj) override {
if (FUnknownPrivate::iidEqual(_iid, IComponentHandler::iid) ||
FUnknownPrivate::iidEqual(_iid, FUnknown::iid)) {
*obj = this; addRef(); return kResultOk;
}
*obj = nullptr; return kNoInterface;
}
uint32 PLUGIN_API addRef() override { return ++refCount; }
uint32 PLUGIN_API release() override { uint32 r = --refCount; if (!r) delete this; return r; }
// Called from audio thread — drain pending changes into ProcessData
void drainInto(ParameterChanges & dest) {
std::lock_guard<std::mutex> lock(mtx);
for (auto & c : pending) {
int32 idx = 0;
auto * q = dest.addParameterData(c.id, idx);
if (q) q->addPoint(0, c.value, idx);
}
pending.clear();
}
private:
std::mutex mtx;
std::vector<Change> pending;
std::atomic<uint32> refCount { 1 };
};
struct PluginInstance {
VST3::Hosting::Module::Ptr module;
IPtr<PlugProvider> provider;
IComponent * component = nullptr;
IAudioProcessor * processor = nullptr;
bool active = false;
int32 numActiveInputBuses = 1;
int32 numActiveOutputBuses = 1;
ParamChangeHandler * paramHandler = nullptr; // realtime param bridge
~PluginInstance() {
if (active && processor) {
processor->setProcessing(false);
component->setActive(false);
}
}
};
static bool load_plugin(const std::string & path, PluginInstance & inst) {
std::string error;
inst.module = VST3::Hosting::Module::create(path, error);
if (!inst.module) {
fprintf(stderr, "[vst-host] Failed to load module: %s\n Error: %s\n",
path.c_str(), error.c_str());
return false;
}
auto & factory = inst.module->getFactory();
for (auto & classInfo : factory.classInfos()) {
if (classInfo.category() == kVstAudioEffectClass) {
inst.provider = owned(new PlugProvider(factory, classInfo, true));
break;
}
}
if (!inst.provider) {
fprintf(stderr, "[vst-host] No audio effect class found in: %s\n", path.c_str());
return false;
}
inst.component = inst.provider->getComponent();
if (!inst.component) {
fprintf(stderr, "[vst-host] Failed to get component\n");
return false;
}
if (inst.component->queryInterface(IAudioProcessor::iid,
(void **)&inst.processor) != kResultOk) {
fprintf(stderr, "[vst-host] Component does not support IAudioProcessor\n");
return false;
}
// Wire up param change handler so GUI edits reach the audio thread in realtime
auto controller = inst.provider->getController();
if (controller) {
inst.paramHandler = new ParamChangeHandler();
controller->setComponentHandler(inst.paramHandler);
}
return true;
}
static bool load_state(PluginInstance & inst, const std::string & state_path) {
std::ifstream f(state_path, std::ios::binary);
if (!f.is_open()) return false;
f.seekg(0, std::ios::end);
size_t size = (size_t)f.tellg();
f.seekg(0, std::ios::beg);
std::vector<char> data(size);
f.read(data.data(), (std::streamsize)size);
// Use Steinberg's stream to pass state to the component
auto * stream = new Steinberg::MemoryStream(data.data(), (int32)size);
tresult res = inst.component->setState(stream);
if (res != kResultOk) {
fprintf(stderr, "[vst-host] WARNING: setState returned %d\n", res);
}
// Also restore controller state if separate controller exists
auto controller = inst.provider->getController();
if (controller) {
stream->seek(0, IBStream::kIBSeekSet, nullptr);
controller->setComponentState(stream);
}
stream->release();
return true;
}
static bool save_state(PluginInstance & inst, const std::string & state_path) {
auto * stream = new Steinberg::MemoryStream();
tresult res = inst.component->getState(stream);
if (res != kResultOk) {
fprintf(stderr, "[vst-host] getState failed: %d\n", res);
stream->release();
return false;
}
std::ofstream f(state_path, std::ios::binary);
if (!f.is_open()) { stream->release(); return false; }
f.write((const char *)stream->getData(), (std::streamsize)stream->getSize());
stream->release();
return true;
}
static bool setup_processing(PluginInstance & inst, int sample_rate, int block_size,
int32 process_mode = kOffline) {
ProcessSetup setup;
setup.processMode = process_mode;
setup.symbolicSampleSize = kSample32;
setup.maxSamplesPerBlock = block_size;
setup.sampleRate = (double)sample_rate;
// VST3 spec + JUCE requirement: setupProcessing BEFORE activateBus/setActive.
// Steinberg's own plugins (Ozone etc) tolerate wrong order. JUCE enforces it.
if (inst.processor->setupProcessing(setup) != kResultOk) {
fprintf(stderr, "[vst-host] setupProcessing failed\n");
return false;
}
// Activate buses AFTER setupProcessing (correct VST3 spec order)
int32 numInputBuses = inst.component->getBusCount(kAudio, kInput);
int32 numOutputBuses = inst.component->getBusCount(kAudio, kOutput);
for (int32 i = 0; i < numOutputBuses; i++)
inst.component->activateBus(kAudio, kOutput, i, true);
for (int32 i = 0; i < numInputBuses; i++)
inst.component->activateBus(kAudio, kInput, i, true);
// setActive — JUCE calls isBusesLayoutSupported here (strict check)
if (inst.component->setActive(true) != kResultOk) {
// JUCE strict bus layout check failed — deactivate aux/sidechain buses only.
// Bus 0 = main stereo in, buses 1+ = sidechain/aux (e.g. DIAMOND compressor).
// Deactivating bus 0 would leave the plugin with no input — don't do that.
fprintf(stderr, "[vst-host] setActive failed, retrying with aux input buses deactivated\n");
for (int32 i = 1; i < numInputBuses; i++)
inst.component->activateBus(kAudio, kInput, i, false);
if (inst.component->setActive(true) != kResultOk) {
fprintf(stderr, "[vst-host] setActive failed even without input buses\n");
return false;
}
numInputBuses = 1; // only main bus active after retry
}
inst.processor->setProcessing(true);
inst.active = true;
inst.numActiveOutputBuses = numOutputBuses;
inst.numActiveInputBuses = numInputBuses; // all active unless retry below set some off
return true;
}
static int cmd_process(const char * plugin_path, const char * input_path,
const char * output_path, const char * state_path) {
init_host_context();
// Load audio
int T = 0, sr = 0;
float * audio = audio_io_read_wav(input_path, &T, &sr);
if (!audio || T <= 0) {
fprintf(stderr, "[vst-host] Failed to read input: %s\n", input_path);
return 1;
}
fprintf(stderr, "[vst-host] Input: %d samples @ %d Hz (%.1f sec)\n",
T, sr, (float)T / sr);
// Load plugin
PluginInstance inst;
if (!load_plugin(plugin_path, inst)) return 1;
// Restore state
if (state_path && state_path[0]) {
if (load_state(inst, state_path)) {
fprintf(stderr, "[vst-host] State loaded from: %s\n", state_path);
}
}
// Setup processing
const int block_size = 4096;
if (!setup_processing(inst, sr, block_size)) return 1;
// audio_io_read_wav returns planar [L:T][R:T]
float * left_in = audio;
float * right_in = audio + T;
// Allocate output buffers (planar)
std::vector<float> left_out(T, 0.0f);
std::vector<float> right_out(T, 0.0f);
// Process in blocks
// Provide minimal ProcessContext — JUCE plugins dereference this unconditionally.
// Null processContext causes immediate crash in JUCE's VST3 wrapper.
ProcessContext ctx = {};
ctx.state = ProcessContext::kPlaying
| ProcessContext::kTempoValid
| ProcessContext::kTimeSigValid;
ctx.sampleRate = (double)sr;
ctx.projectTimeSamples = 0;
ctx.tempo = 120.0;
ctx.timeSigNumerator = 4;
ctx.timeSigDenominator = 4;
auto process_block = [&](float * inL, float * inR, float * outL, float * outR,
int n, TSamples time_samples) {
float * in_bufs[2] = { inL, inR };
float * out_bufs[2] = { outL, outR };
AudioBusBuffers input_bus;
input_bus.numChannels = 2;
input_bus.silenceFlags = 0;
input_bus.channelBuffers32 = in_bufs;
AudioBusBuffers output_bus;
output_bus.numChannels = 2;
output_bus.silenceFlags = 0;
output_bus.channelBuffers32 = out_bufs;
// Sidechain bus — zeroed, for plugins with aux inputs (e.g. DIAMOND)
static float sc_zero[2][4096] = {};
float * sc_bufs[2] = { sc_zero[0], sc_zero[1] };
AudioBusBuffers sc_bus; sc_bus.numChannels = 2; sc_bus.silenceFlags = 3; sc_bus.channelBuffers32 = sc_bufs;
AudioBusBuffers in_buses[2] = { input_bus, sc_bus };
ProcessData data;
data.processMode = kOffline;
data.symbolicSampleSize = kSample32;
data.numSamples = n;
data.numInputs = inst.numActiveInputBuses;
data.numOutputs = inst.numActiveOutputBuses;
data.inputs = in_buses;
data.outputs = &output_bus;
data.inputParameterChanges = nullptr;
data.outputParameterChanges = nullptr;
data.inputEvents = nullptr;
data.outputEvents = nullptr;
ctx.projectTimeSamples = time_samples;
data.processContext = &ctx;
inst.processor->process(data);
};
// Warm-up pre-roll: plugins with slow envelope/RMS/auto-gain detectors
// (multi-second analysis windows, e.g. FidelityX) start cold and fade in
// over the first seconds of an offline render. DAWs don't show this — the
// instance is warm from playback, or the DAW renders pre-roll before the
// bounce point. Feed the track's opening seconds through the plugin and
// discard the output so detectors are converged at sample 0. Negative
// projectTimeSamples marks these blocks as pre-roll.
{
const int warmup = std::min(T, 10 * sr);
fprintf(stderr, "[vst-host] Warm-up pre-roll: %.1f sec\n", (float)warmup / sr);
std::vector<float> scratch_L(block_size), scratch_R(block_size);
int wpos = 0;
while (wpos < warmup) {
int n = std::min(block_size, warmup - wpos);
process_block(left_in + wpos, right_in + wpos,
scratch_L.data(), scratch_R.data(),
n, (TSamples)wpos - warmup);
wpos += n;
}
}
int pos = 0;
while (pos < T) {
int n = std::min(block_size, T - pos);
process_block(left_in + pos, right_in + pos,
left_out.data() + pos, right_out.data() + pos,
n, pos);
pos += n;
}
// Write output as WAV: reassemble planar [L:T][R:T] and encode
std::vector<float> output_planar(T * 2);
memcpy(output_planar.data(), left_out.data(), T * sizeof(float));
memcpy(output_planar.data() + T, right_out.data(), T * sizeof(float));
std::string wav_data = audio_encode_wav(output_planar.data(), T, sr, WAV_F32);
{
std::ofstream f(output_path, std::ios::binary);
if (!f.is_open()) {
fprintf(stderr, "[vst-host] Failed to write output: %s\n", output_path);
free(audio);
return 1;
}
f.write(wav_data.data(), (std::streamsize)wav_data.size());
}
fprintf(stderr, "[vst-host] Output written: %s\n", output_path);
free(audio);
return 0;
}
// ── GUI Mode (Windows) ──────────────────────────────────────────────────────
#ifdef _WIN32
// Minimal IPlugFrame — prevents null-deref when plugins call resizeView()
class SimplePlugFrame : public IPlugFrame {
HWND hwnd_;
public:
SimplePlugFrame(HWND h) : hwnd_(h) {}
tresult PLUGIN_API resizeView(IPlugView* view, ViewRect* newSize) override {
if (!newSize || !hwnd_) return kResultFalse;
RECT wr = { 0, 0, newSize->right - newSize->left, newSize->bottom - newSize->top };
DWORD style = (DWORD)GetWindowLongPtrA(hwnd_, GWL_STYLE);
AdjustWindowRect(&wr, style, FALSE);
SetWindowPos(hwnd_, nullptr, 0, 0, wr.right - wr.left, wr.bottom - wr.top,
SWP_NOMOVE | SWP_NOZORDER);
if (view) view->onSize(&*newSize);
return kResultOk;
}
tresult PLUGIN_API queryInterface(const TUID _iid, void** obj) override {
if (FUnknownPrivate::iidEqual(_iid, IPlugFrame::iid) ||
FUnknownPrivate::iidEqual(_iid, FUnknown::iid)) {
*obj = this;
addRef();
return kResultOk;
}
*obj = nullptr;
return kNoInterface;
}
uint32 PLUGIN_API addRef() override { return 1; }
uint32 PLUGIN_API release() override { return 1; }
};
static IPlugView * g_plugView = nullptr;
static PluginInstance * g_guiInst = nullptr;
static const char * g_guiStatePath = nullptr;
static bool g_guiRunning = true;
static LRESULT CALLBACK VstWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_CLOSE:
g_guiRunning = false;
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProcA(hwnd, msg, wp, lp);
}
}
static int cmd_gui(const char * plugin_path, const char * state_path) {
init_host_context();
PluginInstance inst;
if (!load_plugin(plugin_path, inst)) return 1;
// Load state if exists
if (state_path && state_path[0]) {
load_state(inst, state_path);
}
// Setup processing (needed for some plugins to show GUI correctly)
setup_processing(inst, 48000, 4096);
// Get edit controller and create view
auto controller = inst.provider->getController();
if (!controller) {
fprintf(stderr, "[vst-host] No edit controller available\n");
return 1;
}
IPlugView * view = controller->createView(ViewType::kEditor);
if (!view) {
fprintf(stderr, "[vst-host] Plugin has no editor view\n");
return 1;
}
// Get preferred size
ViewRect rect;
if (view->getSize(&rect) != kResultOk) {
rect.left = 0; rect.top = 0;
rect.right = 800; rect.bottom = 600;
}
int w = rect.right - rect.left;
int h = rect.bottom - rect.top;
// Register window class
WNDCLASSA wc = {};
wc.lpfnWndProc = VstWndProc;
wc.hInstance = GetModuleHandleA(nullptr);
wc.lpszClassName = "VstHostWindow";
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassA(&wc);
// Adjust window rect for non-client area
RECT wr = { 0, 0, w, h };
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX), FALSE);
HWND hwnd = CreateWindowA("VstHostWindow", "HOT-Step VST3 Plugin Editor",
WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX),
CW_USEDEFAULT, CW_USEDEFAULT,
wr.right - wr.left, wr.bottom - wr.top,
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
if (!hwnd) {
fprintf(stderr, "[vst-host] Failed to create window\n");
view->release();
return 1;
}
// Provide IPlugFrame so plugins can call resizeView() without crashing
static SimplePlugFrame * s_plugFrame = nullptr;
delete s_plugFrame;
s_plugFrame = new SimplePlugFrame(hwnd);
view->setFrame(s_plugFrame);
// Attach plugin view to window
if (view->attached(hwnd, kPlatformTypeHWND) != kResultOk) {
fprintf(stderr, "[vst-host] Failed to attach plugin view\n");
view->release();
DestroyWindow(hwnd);
return 1;
}
g_plugView = view;
g_guiInst = &inst;
g_guiStatePath = state_path;
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
fprintf(stderr, "[vst-host] Plugin GUI opened (%dx%d). Close window to save state.\n", w, h);
// Message loop
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Detach view
view->removed();
view->release();
// Save state on close
if (state_path && state_path[0]) {
if (save_state(inst, state_path)) {
fprintf(stderr, "[vst-host] State saved to: %s\n", state_path);
}
}
return 0;
}
#endif
// ── Chain Mode ───────────────────────────────────────────────────────────────
static int cmd_process_chain(const char * chain_json_path, const char * input_path,
const char * output_path) {
// Read chain JSON
std::ifstream f(chain_json_path);
if (!f.is_open()) {
fprintf(stderr, "[vst-host] Failed to read chain: %s\n", chain_json_path);
return 1;
}
std::string json_str((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
yyjson_doc * doc = yyjson_read(json_str.c_str(), json_str.size(), 0);
if (!doc) {
fprintf(stderr, "[vst-host] Failed to parse chain JSON\n");
return 1;
}
yyjson_val * root = yyjson_doc_get_root(doc);
yyjson_val * plugins_arr = yyjson_obj_get(root, "plugins");
if (!plugins_arr || !yyjson_is_arr(plugins_arr)) {
fprintf(stderr, "[vst-host] Chain JSON must have a 'plugins' array\n");
yyjson_doc_free(doc);
return 1;
}
// Build list of enabled plugins
struct ChainEntry {
std::string path;
std::string state;
};
std::vector<ChainEntry> entries;
size_t idx, max;
yyjson_val * val;
yyjson_arr_foreach(plugins_arr, idx, max, val) {
yyjson_val * enabled_val = yyjson_obj_get(val, "enabled");
if (enabled_val && !yyjson_get_bool(enabled_val)) continue;
yyjson_val * path_val = yyjson_obj_get(val, "path");
yyjson_val * state_val = yyjson_obj_get(val, "state");
if (!path_val) continue;
ChainEntry e;
e.path = yyjson_get_str(path_val);
e.state = state_val ? yyjson_get_str(state_val) : "";
entries.push_back(e);
}
yyjson_doc_free(doc);
if (entries.empty()) {
fprintf(stderr, "[vst-host] No enabled plugins in chain, copying input to output\n");
// Just copy input to output
std::ifstream src(input_path, std::ios::binary);
std::ofstream dst(output_path, std::ios::binary);
dst << src.rdbuf();
return 0;
}
fprintf(stderr, "[vst-host] Processing chain with %zu plugin(s)\n", entries.size());
// Process sequentially: input → plugin1 → plugin2 → ... → output
std::string current_input = input_path;
std::string temp_path;
for (size_t i = 0; i < entries.size(); i++) {
bool is_last = (i == entries.size() - 1);
std::string current_output;
if (is_last) {
current_output = output_path;
} else {
// Use temp file for intermediate results
temp_path = std::string(output_path) + ".vst_temp_" + std::to_string(i) + ".wav";
current_output = temp_path;
}
fprintf(stderr, "[vst-host] [%zu/%zu] %s\n",
i + 1, entries.size(), entries[i].path.c_str());
int ret = cmd_process(entries[i].path.c_str(),
current_input.c_str(),
current_output.c_str(),
entries[i].state.c_str());
if (ret != 0) {
fprintf(stderr, "[vst-host] Plugin %zu failed, aborting chain\n", i);
return ret;
}
current_input = current_output;
}
// Clean up temp files
for (size_t i = 0; i + 1 < entries.size(); i++) {
std::string tf = std::string(output_path) + ".vst_temp_" + std::to_string(i) + ".wav";
remove(tf.c_str());
}
return 0;
}
// ── Monitor Mode (Windows WASAPI) ────────────────────────────────────────────
#ifdef _WIN32
struct MonitorState {
// Audio data (planar [L:T][R:T])
float * audio = nullptr;
int T = 0;
int sr = 0;
int pos = 0;
bool loop = true;
// Chain
std::vector<PluginInstance> plugins;
std::vector<std::string> state_paths;
// Control
std::string control_file;
std::string status_file;
std::string current_track;
FILETIME control_mtime = {};
bool running = true;
bool paused = false;
// Processing buffers (reused per block)
std::vector<float> buf_a_L, buf_a_R, buf_b_L, buf_b_R;
};
static bool monitor_load_track(MonitorState & ms, const std::string & path) {
int T = 0, sr = 0;
float * audio = audio_io_read_wav(path.c_str(), &T, &sr);
if (!audio || T <= 0) {
fprintf(stderr, "[monitor] Failed to load: %s\n", path.c_str());
return false;
}
if (ms.audio) free(ms.audio);
ms.audio = audio;
ms.T = T;
ms.sr = sr;
ms.pos = 0;
ms.current_track = path;
fprintf(stderr, "[monitor] Loaded: %s (%d frames, %d Hz, %.1fs)\n",
path.c_str(), T, sr, (float)T / sr);
return true;
}
static void monitor_check_control(MonitorState & ms) {
if (ms.control_file.empty()) return;
HANDLE hFile = CreateFileA(ms.control_file.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile == INVALID_HANDLE_VALUE) return;
FILETIME ft;
GetFileTime(hFile, nullptr, nullptr, &ft);
bool changed = (ft.dwHighDateTime != ms.control_mtime.dwHighDateTime ||
ft.dwLowDateTime != ms.control_mtime.dwLowDateTime);
if (!changed) { CloseHandle(hFile); return; }
ms.control_mtime = ft;
DWORD size = GetFileSize(hFile, nullptr);
if (size == 0 || size == INVALID_FILE_SIZE) { CloseHandle(hFile); return; }
std::vector<char> buf(size + 1, 0);
DWORD bytesRead = 0;
ReadFile(hFile, buf.data(), size, &bytesRead, nullptr);
CloseHandle(hFile);
yyjson_doc * doc = yyjson_read(buf.data(), bytesRead, 0);
if (!doc) return;
yyjson_val * root = yyjson_doc_get_root(doc);
yyjson_val * action_val = yyjson_obj_get(root, "action");
if (action_val) {
const char * action = yyjson_get_str(action_val);
if (action && !strcmp(action, "stop")) {
ms.running = false;
yyjson_doc_free(doc);
return;
}
if (action && !strcmp(action, "pause")) {
ms.paused = true;
}
if (action && !strcmp(action, "play")) {
ms.paused = false;
}
}
yyjson_val * track_val = yyjson_obj_get(root, "track");
if (track_val) {
const char * track = yyjson_get_str(track_val);
if (track && ms.current_track != track) {
monitor_load_track(ms, track);
}
}
// Seek support: { "seek": 42.5 } = jump to 42.5 seconds
yyjson_val * seek_val = yyjson_obj_get(root, "seek");
if (seek_val && yyjson_is_num(seek_val)) {
double seek_sec = yyjson_get_real(seek_val);
int new_pos = (int)(seek_sec * ms.sr);
if (new_pos < 0) new_pos = 0;
if (new_pos >= ms.T) new_pos = ms.T - 1;
ms.pos = new_pos;
}
yyjson_doc_free(doc);
}
static void monitor_process_block(MonitorState & ms, float * out_L, float * out_R, int n) {
if (!ms.audio || ms.T <= 0) {
memset(out_L, 0, n * sizeof(float));
memset(out_R, 0, n * sizeof(float));
return;
}
// Paused: output silence without advancing position
if (ms.paused) {
memset(out_L, 0, n * sizeof(float));
memset(out_R, 0, n * sizeof(float));
return;
}
// Ensure buffers are large enough
if ((int)ms.buf_a_L.size() < n) {
ms.buf_a_L.resize(n); ms.buf_a_R.resize(n);
ms.buf_b_L.resize(n); ms.buf_b_R.resize(n);
}
// Read from WAV (with loop)
float * src_L = ms.buf_a_L.data();
float * src_R = ms.buf_a_R.data();
for (int i = 0; i < n; i++) {
int p = ms.pos + i;
if (p >= ms.T) {
if (ms.loop) { p = p % ms.T; }
else { src_L[i] = 0; src_R[i] = 0; continue; }
}
src_L[i] = ms.audio[p];
src_R[i] = ms.audio[ms.T + p];
}
ms.pos += n;
if (ms.loop && ms.pos >= ms.T) ms.pos = ms.pos % ms.T;
// Process through each plugin in chunks — JUCE allocates internal buffers
// at prepareToPlay for maxSamplesPerBlock. Never pass more than that.
// block_size = 4096 so this only matters if WASAPI gives an unusually large buffer.
const int MAX_CHUNK = 4096;
ProcessContext mctx = {};
mctx.state = ProcessContext::kPlaying
| ProcessContext::kTempoValid
| ProcessContext::kTimeSigValid;
mctx.sampleRate = (ms.sr > 0) ? (double)ms.sr : 48000.0;
mctx.tempo = 120.0;
mctx.timeSigNumerator = 4;
mctx.timeSigDenominator = 4;
float * cur_L = src_L, * cur_R = src_R;
float * dst_L = ms.buf_b_L.data(), * dst_R = ms.buf_b_R.data();
int processed = 0;
while (processed < n) {
int chunk = std::min(n - processed, MAX_CHUNK);
for (size_t pi = 0; pi < ms.plugins.size(); pi++) {
int32 nIn = ms.plugins[pi].numActiveInputBuses;
int32 nOut = ms.plugins[pi].numActiveOutputBuses;
float * in_bufs[2] = { cur_L + processed, cur_R + processed };
float * ob[2] = { dst_L + processed, dst_R + processed };
AudioBusBuffers ib; ib.numChannels = 2; ib.silenceFlags = 0; ib.channelBuffers32 = in_bufs;
AudioBusBuffers ob_bus; ob_bus.numChannels = 2; ob_bus.silenceFlags = 0; ob_bus.channelBuffers32 = ob;
static float sc_zero[2][4096] = {};
float * sc_bufs[2] = { sc_zero[0], sc_zero[1] };
AudioBusBuffers sc_bus; sc_bus.numChannels = 2; sc_bus.silenceFlags = 3; sc_bus.channelBuffers32 = sc_bufs;
AudioBusBuffers in_buses[2] = { ib, sc_bus };
AudioBusBuffers out_buses[1] = { ob_bus };
// Drain GUI param changes into this block's inputParameterChanges
ParameterChanges paramChanges;
if (ms.plugins[pi].paramHandler)
ms.plugins[pi].paramHandler->drainInto(paramChanges);
ProcessData pd;
pd.processMode = kRealtime; pd.symbolicSampleSize = kSample32;
pd.numSamples = chunk;
pd.numInputs = nIn;
pd.numOutputs = nOut;
pd.inputs = in_buses;
pd.outputs = out_buses;
pd.inputParameterChanges = &paramChanges;
pd.outputParameterChanges = nullptr;
mctx.projectTimeSamples = ms.pos - n + processed;
pd.inputEvents = nullptr; pd.outputEvents = nullptr; pd.processContext = &mctx;
ms.plugins[pi].processor->process(pd);
// after first plugin, subsequent plugins read from dst
if (pi == 0) { std::swap(cur_L, dst_L); std::swap(cur_R, dst_R); }
}
processed += chunk;
}
memcpy(out_L, cur_L, n * sizeof(float));
memcpy(out_R, cur_R, n * sizeof(float));
}
// Window proc for plugin GUIs in monitor mode
static MonitorState * g_monitorState = nullptr;
static int g_monitorWindowCount = 0;
static LRESULT CALLBACK MonitorWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
g_monitorWindowCount--;
if (g_monitorWindowCount <= 0 && g_monitorState) {
g_monitorState->running = false;
PostQuitMessage(0);
}
return 0;
default:
return DefWindowProcA(hwnd, msg, wp, lp);
}
}
static int cmd_monitor(const char * chain_json_path, const char * input_path,
const char * control_path, const char * status_path) {
init_host_context();
// Parse chain JSON
std::ifstream cf(chain_json_path);
if (!cf.is_open()) { fprintf(stderr, "[monitor] Failed to read chain: %s\n", chain_json_path); return 1; }
std::string json_str((std::istreambuf_iterator<char>(cf)), std::istreambuf_iterator<char>());
cf.close();
yyjson_doc * doc = yyjson_read(json_str.c_str(), json_str.size(), 0);
if (!doc) { fprintf(stderr, "[monitor] Failed to parse chain JSON\n"); return 1; }
yyjson_val * root = yyjson_doc_get_root(doc);
yyjson_val * plugins_arr = yyjson_obj_get(root, "plugins");
struct ChainDef { std::string path, state; };
std::vector<ChainDef> defs;
if (plugins_arr && yyjson_is_arr(plugins_arr)) {
size_t idx, max; yyjson_val * val;
yyjson_arr_foreach(plugins_arr, idx, max, val) {
yyjson_val * ev = yyjson_obj_get(val, "enabled");
if (ev && !yyjson_get_bool(ev)) continue;
yyjson_val * pv = yyjson_obj_get(val, "path");
yyjson_val * sv = yyjson_obj_get(val, "state");
if (!pv) continue;
defs.push_back({ yyjson_get_str(pv), sv ? yyjson_get_str(sv) : "" });
}
}
yyjson_doc_free(doc);
if (defs.empty()) { fprintf(stderr, "[monitor] No enabled plugins\n"); return 1; }
// Setup monitor state
MonitorState ms;
ms.control_file = control_path ? control_path : "";
ms.status_file = status_path ? status_path : "";
// Load initial track
if (!monitor_load_track(ms, input_path)) return 1;
// Load plugins
ms.plugins.resize(defs.size());
ms.state_paths.resize(defs.size());
// Must be >= WASAPI buffer size (typically 1024-2048 at 48kHz shared mode).
// JUCE allocates internal scratch buffers at prepareToPlay for exactly this size.
// Passing a larger block crashes with 0xC0000005. 4096 covers all WASAPI configs.
const int block_size = 4096;
for (size_t i = 0; i < defs.size(); i++) {
ms.state_paths[i] = defs[i].state;
if (!load_plugin(defs[i].path, ms.plugins[i])) {
fprintf(stderr, "[monitor] Failed to load plugin %zu: %s\n", i, defs[i].path.c_str());
return 1;
}
if (!defs[i].state.empty()) load_state(ms.plugins[i], defs[i].state);
if (!setup_processing(ms.plugins[i], ms.sr, block_size, kRealtime)) {
fprintf(stderr, "[monitor] Failed to setup plugin %zu\n", i);
return 1;
}
}
// Register window class for plugin GUIs
WNDCLASSA wc = {};
wc.lpfnWndProc = MonitorWndProc;
wc.hInstance = GetModuleHandleA(nullptr);
wc.lpszClassName = "VstMonitorWindow";
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassA(&wc);
g_monitorState = &ms;
g_monitorWindowCount = 0;
// Open GUI for each plugin
std::vector<IPlugView*> views;
std::vector<SimplePlugFrame*> plugFrames;
for (size_t i = 0; i < ms.plugins.size(); i++) {
auto ctrl = ms.plugins[i].provider->getController();
if (!ctrl) continue;
IPlugView * view = ctrl->createView(ViewType::kEditor);
if (!view) continue;
ViewRect rect;
if (view->getSize(&rect) != kResultOk) { rect = {0, 0, 800, 600}; }
int w = rect.right - rect.left, h = rect.bottom - rect.top;
RECT wr = { 0, 0, w, h };
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX), FALSE);
std::string title = "Monitor: " + defs[i].path.substr(defs[i].path.find_last_of("/\\") + 1);
HWND hwnd = CreateWindowA("VstMonitorWindow", title.c_str(),
WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX),
CW_USEDEFAULT, CW_USEDEFAULT,
wr.right - wr.left, wr.bottom - wr.top,
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
if (!hwnd) { view->release(); continue; }
// Provide IPlugFrame so plugins can call resizeView() without crashing
auto * frame = new SimplePlugFrame(hwnd);
view->setFrame(frame);
plugFrames.push_back(frame);
if (view->attached(hwnd, kPlatformTypeHWND) != kResultOk) {
view->release(); DestroyWindow(hwnd); continue;
}
ShowWindow(hwnd, SW_SHOW);
g_monitorWindowCount++;
views.push_back(view);
}
fprintf(stderr, "[monitor] %d plugin GUI(s) opened\n", g_monitorWindowCount);
// ── WASAPI init ──
IMMDeviceEnumerator * pEnum = nullptr;
IMMDevice * pDevice = nullptr;
IAudioClient * pAudioClient = nullptr;
IAudioRenderClient * pRenderClient = nullptr;
WAVEFORMATEX * pwfx = nullptr;
HANDLE hAudioEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
UINT32 bufferFrames = 0;
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pEnum);
if (FAILED(hr)) { fprintf(stderr, "[monitor] Failed to create device enumerator\n"); return 1; }
hr = pEnum->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice);
if (FAILED(hr)) { fprintf(stderr, "[monitor] No default audio device\n"); return 1; }
hr = pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void**)&pAudioClient);
if (FAILED(hr)) { fprintf(stderr, "[monitor] Failed to activate audio client\n"); return 1; }
hr = pAudioClient->GetMixFormat(&pwfx);
if (FAILED(hr)) { fprintf(stderr, "[monitor] Failed to get mix format\n"); return 1; }
fprintf(stderr, "[monitor] WASAPI: %d Hz, %d ch, %d bits\n",
(int)pwfx->nSamplesPerSec, (int)pwfx->nChannels, (int)pwfx->wBitsPerSample);
REFERENCE_TIME bufDuration = 200000; // 20ms buffer
hr = pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
bufDuration, 0, pwfx, nullptr);
if (FAILED(hr)) { fprintf(stderr, "[monitor] WASAPI Initialize failed: 0x%08X\n", (unsigned)hr); return 1; }
pAudioClient->SetEventHandle(hAudioEvent);
pAudioClient->GetBufferSize(&bufferFrames);
hr = pAudioClient->GetService(__uuidof(IAudioRenderClient), (void**)&pRenderClient);
if (FAILED(hr)) { fprintf(stderr, "[monitor] Failed to get render client\n"); return 1; }
fprintf(stderr, "[monitor] Buffer: %u frames (%.1f ms)\n",
bufferFrames, 1000.0f * bufferFrames / pwfx->nSamplesPerSec);
// Pre-fill buffer
{ BYTE * data; pRenderClient->GetBuffer(bufferFrames, &data);
memset(data, 0, bufferFrames * pwfx->nBlockAlign);
pRenderClient->ReleaseBuffer(bufferFrames, 0); }
pAudioClient->Start();
fprintf(stderr, "[monitor] Playing... Close all plugin windows to stop.\n");
// Temp buffers for processed audio
std::vector<float> proc_L(bufferFrames), proc_R(bufferFrames);
DWORD controlCheckTick = GetTickCount();
// ── Main loop: WASAPI + Win32 messages ──
while (ms.running) {
DWORD wait = MsgWaitForMultipleObjects(1, &hAudioEvent, FALSE, 100, QS_ALLINPUT);
if (wait == WAIT_OBJECT_0) {
// WASAPI wants data
UINT32 padding = 0;
pAudioClient->GetCurrentPadding(&padding);
UINT32 available = bufferFrames - padding;
if (available > 0) {
BYTE * data = nullptr;
if (SUCCEEDED(pRenderClient->GetBuffer(available, &data))) {
monitor_process_block(ms, proc_L.data(), proc_R.data(), (int)available);
// Interleave into WASAPI buffer (float32, potentially >2 channels)
float * fdata = (float *)data;
int ch = pwfx->nChannels;
for (UINT32 i = 0; i < available; i++) {
fdata[i * ch + 0] = proc_L[i];
fdata[i * ch + 1] = (ch >= 2) ? proc_R[i] : 0;
for (int c = 2; c < ch; c++) fdata[i * ch + c] = 0;
}
pRenderClient->ReleaseBuffer(available, 0);
}
}
}
// Pump Win32 messages on EVERY iteration — not just on QS_ALLINPUT.
// JUCE's 30Hz timer, repaint callbacks, and waveform display all fire
// via WM_TIMER / WM_PAINT. If we only pump on the message event, WASAPI
// events (22ms) dominate the loop and starve JUCE's UI thread entirely.
{
MSG msg;
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) { ms.running = false; break; }
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// Check control file periodically (~500ms)
DWORD now = GetTickCount();
if (now - controlCheckTick > 500) {
controlCheckTick = now;
monitor_check_control(ms);
}
// Write status file periodically (~200ms) for UI position display
static DWORD statusTick = 0;
if (!ms.status_file.empty() && now - statusTick > 200) {
statusTick = now;
double pos_sec = (ms.sr > 0) ? (double)ms.pos / ms.sr : 0;
double dur_sec = (ms.sr > 0) ? (double)ms.T / ms.sr : 0;
char sbuf[256];
snprintf(sbuf, sizeof(sbuf),
"{\"position\":%.2f,\"duration\":%.2f,\"loop\":%s,\"paused\":%s}",
pos_sec, dur_sec, ms.loop ? "true" : "false", ms.paused ? "true" : "false");
HANDLE hStatus = CreateFileA(ms.status_file.c_str(), GENERIC_WRITE,
FILE_SHARE_READ, nullptr,
CREATE_ALWAYS, 0, nullptr);
if (hStatus != INVALID_HANDLE_VALUE) {
DWORD written;
WriteFile(hStatus, sbuf, (DWORD)strlen(sbuf), &written, nullptr);
CloseHandle(hStatus);
}
}
}
// ── Cleanup ──
pAudioClient->Stop();
if (pRenderClient) pRenderClient->Release();
if (pAudioClient) pAudioClient->Release();
if (pDevice) pDevice->Release();
if (pEnum) pEnum->Release();
if (pwfx) CoTaskMemFree(pwfx);
CloseHandle(hAudioEvent);
// Detach and release views
for (auto * v : views) { v->removed(); v->release(); }
for (auto * f : plugFrames) delete f;
// Save all plugin states
for (size_t i = 0; i < ms.plugins.size(); i++) {
if (!ms.state_paths[i].empty()) {
if (save_state(ms.plugins[i], ms.state_paths[i])) {
fprintf(stderr, "[monitor] State saved: %s\n", ms.state_paths[i].c_str());
}
}
}
if (ms.audio) free(ms.audio);
fprintf(stderr, "[monitor] Done.\n");
return 0;
}
#endif // _WIN32
// ── Usage + Main ─────────────────────────────────────────────────────────────
static void usage(const char * prog) {
fprintf(stderr, "vst-host — HOT-Step VST3 Plugin Host\n\n");
fprintf(stderr, "Usage:\n");
fprintf(stderr, " %s --scan\n", prog);
fprintf(stderr, " %s --gui --plugin <path.vst3> [--state <file>]\n", prog);
fprintf(stderr, " %s --process --plugin <path.vst3> --input <in.wav> --output <out.wav> [--state <file>]\n", prog);
fprintf(stderr, " %s --process-chain --chain <chain.json> --input <in.wav> --output <out.wav>\n", prog);
fprintf(stderr, " %s --monitor --chain <chain.json> --input <in.wav> [--control <file>] [--status <file>]\n", prog);
}
int main(int argc, char * argv[]) {
if (argc < 2) { usage(argv[0]); return 1; }
const char * mode = nullptr;
const char * plugin = nullptr;
const char * input = nullptr;
const char * output = nullptr;
const char * state = nullptr;
const char * chain = nullptr;
const char * control = nullptr;
const char * status = nullptr;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--scan")) mode = "scan";
else if (!strcmp(argv[i], "--gui")) mode = "gui";
else if (!strcmp(argv[i], "--process")) mode = "process";
else if (!strcmp(argv[i], "--process-chain")) mode = "chain";
else if (!strcmp(argv[i], "--monitor")) mode = "monitor";
else if (!strcmp(argv[i], "--plugin") && i+1 < argc) plugin = argv[++i];
else if (!strcmp(argv[i], "--input") && i+1 < argc) input = argv[++i];
else if (!strcmp(argv[i], "--output") && i+1 < argc) output = argv[++i];
else if (!strcmp(argv[i], "--state") && i+1 < argc) state = argv[++i];
else if (!strcmp(argv[i], "--chain") && i+1 < argc) chain = argv[++i];
else if (!strcmp(argv[i], "--control") && i+1 < argc) control = argv[++i];
else if (!strcmp(argv[i], "--status") && i+1 < argc) status = argv[++i];
else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) {
usage(argv[0]); return 0;
}
}
if (!mode) { usage(argv[0]); return 1; }
if (!strcmp(mode, "scan")) {
return cmd_scan();
}
else if (!strcmp(mode, "gui")) {
#ifdef _WIN32
if (!plugin) { fprintf(stderr, "Error: --plugin required\n"); return 1; }
return cmd_gui(plugin, state);
#else
fprintf(stderr, "Error: GUI mode only supported on Windows\n");
return 1;
#endif
}
else if (!strcmp(mode, "process")) {
if (!plugin || !input || !output) {
fprintf(stderr, "Error: --plugin, --input, --output required\n");
return 1;
}
return cmd_process(plugin, input, output, state);
}
else if (!strcmp(mode, "chain")) {
if (!chain || !input || !output) {
fprintf(stderr, "Error: --chain, --input, --output required\n");
return 1;
}
return cmd_process_chain(chain, input, output);
}
else if (!strcmp(mode, "monitor")) {
#ifdef _WIN32
if (!chain || !input) {
fprintf(stderr, "Error: --chain, --input required for monitor\n");
return 1;
}
return cmd_monitor(chain, input, control, status);
#else
fprintf(stderr, "Error: Monitor mode only supported on Windows\n");
return 1;
#endif
}
usage(argv[0]);
return 1;
}