// vst-host.cpp — HOT-Step VST3 Host Tool // // Modes: // --scan Scan for VST3 plugins, output JSON // --gui --plugin Open plugin GUI in a native window // --process --plugin

--input --output Offline processing // --process-chain --chain --input --output Chain mode // // Part of the HOT-Step 9000 CPP engine. #include #include #include #include #include #include #include #include #include // Windows headers MUST come before VST3 SDK to avoid IConnectionPoint collision // (ocidl.h defines COM IConnectionPoint, VST3 has Steinberg::Vst::IConnectionPoint) #ifdef _WIN32 #include #include #include #include #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 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 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 pending; std::atomic refCount { 1 }; }; struct PluginInstance { VST3::Hosting::Module::Ptr module; IPtr 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 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 left_out(T, 0.0f); std::vector 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 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 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(f)), std::istreambuf_iterator()); 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 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 plugins; std::vector 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 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 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 = ¶mChanges; 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(cf)), std::istreambuf_iterator()); 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 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 views; std::vector 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 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 [--state ]\n", prog); fprintf(stderr, " %s --process --plugin --input --output [--state ]\n", prog); fprintf(stderr, " %s --process-chain --chain --input --output \n", prog); fprintf(stderr, " %s --monitor --chain --input [--control ] [--status ]\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; }