Fix AI proxy origin in Tauri, SF2 program_select, VSTi GUI reopen + sample rate bridge
- aiGateway.js: use window.API_BASE_URL (engine port) instead of window.location.origin (tauri://localhost asset protocol returned index.html -> JSON parse error); check content-type before parsing response - NativeInstrumentEngine.cpp: fluid_synth_program_select for SF2 instrument + program change - Vst3Instrument.cpp: activate all audio/event buses, channel=0 forced, GUI resize/reopen, kEvent using fix - main.cpp: VST GUI reopen via WM_DESTROY, OleInitialize, steady_clock loop - lib.rs: BridgeSampleRate state, restart_bridge_with_sample_rate command, borrow fix - app.jsx/nativeBridgeService.js: isMidiTrack widening, bridge masterbus connect, held MIDI notes, sample-rate restart, openNativeGUI channel
This commit is contained in:
@@ -47,8 +47,7 @@ bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) {
|
||||
|
||||
void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
|
||||
if (!synth) return;
|
||||
fluid_synth_bank_select(FS_SYNTH, channel, bank);
|
||||
fluid_synth_program_change(FS_SYNTH, channel, program);
|
||||
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
|
||||
@@ -69,7 +68,7 @@ void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t
|
||||
|
||||
void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||
if (!synth) return;
|
||||
fluid_synth_program_change(FS_SYNTH, channel, program);
|
||||
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// native_bridge/src/Vst3Instrument.cpp
|
||||
// native_bridge/src/Vst3Instrument.cpp
|
||||
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
|
||||
//
|
||||
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
|
||||
@@ -10,6 +10,9 @@
|
||||
#include "Vst3Instrument.h"
|
||||
|
||||
#ifdef HAVE_VST3SDK
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include "public.sdk/source/vst/hosting/module.h"
|
||||
#include "public.sdk/source/vst/hosting/hostclasses.h"
|
||||
#include "public.sdk/source/vst/hosting/processdata.h"
|
||||
@@ -75,6 +78,7 @@ using Steinberg::Vst::IParamValueQueue;
|
||||
using Steinberg::Vst::kNoParamId;
|
||||
using Steinberg::Vst::BusInfo;
|
||||
using Steinberg::Vst::kAudio;
|
||||
using Steinberg::Vst::kEvent;
|
||||
using Steinberg::Vst::kInput;
|
||||
using Steinberg::Vst::kOutput;
|
||||
using Steinberg::Vst::kRealtime;
|
||||
@@ -287,6 +291,26 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
|
||||
ctrlCP->connect(compCP);
|
||||
}
|
||||
|
||||
// Activate all audio and event buses (MIDI & Audio) - required by VST3 specification.
|
||||
{
|
||||
int32 numAudioInputs = component->getBusCount(kAudio, kInput);
|
||||
for (int32 i = 0; i < numAudioInputs; ++i) {
|
||||
component->activateBus(kAudio, kInput, i, true);
|
||||
}
|
||||
int32 numAudioOutputs = component->getBusCount(kAudio, kOutput);
|
||||
for (int32 i = 0; i < numAudioOutputs; ++i) {
|
||||
component->activateBus(kAudio, kOutput, i, true);
|
||||
}
|
||||
int32 numEventInputs = component->getBusCount(kEvent, kInput);
|
||||
for (int32 i = 0; i < numEventInputs; ++i) {
|
||||
component->activateBus(kEvent, kInput, i, true);
|
||||
}
|
||||
int32 numEventOutputs = component->getBusCount(kEvent, kOutput);
|
||||
for (int32 i = 0; i < numEventOutputs; ++i) {
|
||||
component->activateBus(kEvent, kOutput, i, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Audio processor: setup → activate → start processing (audioclient.cpp).
|
||||
FUnknownPtr<IAudioProcessor> processor(component);
|
||||
if (!processor) {
|
||||
@@ -372,7 +396,7 @@ void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, ui
|
||||
e.ppqPosition = 0;
|
||||
e.flags = Event::kIsLive;
|
||||
e.type = Event::kNoteOnEvent;
|
||||
e.noteOn.channel = (int16)channel;
|
||||
e.noteOn.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility
|
||||
e.noteOn.pitch = (int16)pitch;
|
||||
e.noteOn.tuning = 0.f;
|
||||
e.noteOn.velocity = velocity;
|
||||
@@ -395,7 +419,7 @@ void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOf
|
||||
e.ppqPosition = 0;
|
||||
e.flags = Event::kIsLive;
|
||||
e.type = Event::kNoteOffEvent;
|
||||
e.noteOff.channel = (int16)channel;
|
||||
e.noteOff.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility
|
||||
e.noteOff.pitch = (int16)pitch;
|
||||
e.noteOff.velocity = 0.f;
|
||||
e.noteOff.noteId = -1;
|
||||
@@ -411,8 +435,8 @@ void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value
|
||||
#else
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (!s || !s->controller) return;
|
||||
ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)cc,
|
||||
kHostMidiCC | (ParamID)(cc & 0x7F));
|
||||
ParamID tag = midiControllerTag(s->controller, 0, (int16)cc,
|
||||
kHostMidiCC);
|
||||
ParamValue v = value / 127.0;
|
||||
// setParamNormalized covers single-component plugins; the param queue
|
||||
// reaches split plugins that read inputParameterChanges inside process().
|
||||
@@ -429,7 +453,7 @@ void Vst3Instrument::programChange(uint32_t channel, uint32_t program) {
|
||||
#else
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (!s || !s->controller) return;
|
||||
ParamID tag = kHostMidiProgramChange | (ParamID)(channel & 0xF);
|
||||
ParamID tag = kHostMidiProgramChange;
|
||||
// ponytail: normalized value should be program/(count-1) from the program
|
||||
// list param stepCount; 127ths is a reasonable approximation.
|
||||
ParamValue v = (program & 0x7F) / 127.0;
|
||||
@@ -446,8 +470,8 @@ void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
#else
|
||||
auto* s = static_cast<Vst3HostState*>(state_);
|
||||
if (!s || !s->controller) return;
|
||||
ParamID tag = midiControllerTag(s->controller, (int16)channel, (int16)kPitchBend,
|
||||
kHostMidiPitchBend | (ParamID)(channel & 0xF));
|
||||
ParamID tag = midiControllerTag(s->controller, 0, (int16)kPitchBend,
|
||||
kHostMidiPitchBend);
|
||||
ParamValue v = bend14 / 16383.0;
|
||||
s->controller->setParamNormalized(tag, v);
|
||||
int32 idx = 0;
|
||||
@@ -498,10 +522,21 @@ bool Vst3Instrument::openGUI(void* parentWindowHandle) {
|
||||
view->setFrame(&s->plugFrame);
|
||||
tresult ts = view->isPlatformTypeSupported(kPlatformTypeHWND);
|
||||
std::cerr << "[dbg] openGUI: isPlatformTypeSupported=" << (int)ts << std::endl;
|
||||
if (ts != kResultTrue && ts != kResultOk) return false;
|
||||
// Log only — proceed to attach anyway to support non-compliant plugins.
|
||||
tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND);
|
||||
std::cerr << "[dbg] openGUI: attached=" << (int)ta << std::endl;
|
||||
if (ta != kResultOk) return false;
|
||||
#ifdef _WIN32
|
||||
HWND hwnd = (HWND)parentWindowHandle;
|
||||
Steinberg::ViewRect rect;
|
||||
if (view->getSize(&rect) == kResultOk) {
|
||||
int w = rect.right - rect.left;
|
||||
int h = rect.bottom - rect.top;
|
||||
RECT r = { 0, 0, w, h };
|
||||
AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
SetWindowPos(hwnd, nullptr, 0, 0, r.right - r.left, r.bottom - r.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
#endif
|
||||
s->view = view;
|
||||
guiAttached_ = true;
|
||||
// ponytail: the bridge loop is a worker thread without a Windows message
|
||||
@@ -556,7 +591,7 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
||||
}
|
||||
tresult pr = processor->process(s->processData);
|
||||
float mx = 0.f;
|
||||
if (pr == kResultOk && s->processData.numOutputs > 0) {
|
||||
if (pr >= 0 && s->processData.numOutputs > 0) {
|
||||
const Steinberg::Vst::AudioBusBuffers& out = s->processData.outputs[0];
|
||||
const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr;
|
||||
const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr;
|
||||
|
||||
+60
-23
@@ -14,9 +14,9 @@
|
||||
#include <unistd.h>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#endif
|
||||
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
@@ -48,6 +48,19 @@ static void sleep_ms(uint32_t ms) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
||||
if (uMsg == WM_DESTROY) {
|
||||
void* ptr = (void*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
|
||||
if (ptr) {
|
||||
INativeInstrument* inst = static_cast<INativeInstrument*>(ptr);
|
||||
inst->closeGUI();
|
||||
}
|
||||
}
|
||||
return DefWindowProcA(hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
#endif
|
||||
|
||||
// B9: native Win32 window for the VST editor (replaces the WebView2 surface —
|
||||
// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the
|
||||
// ChannelWorker thread so the worker's idle message pump services its messages.
|
||||
@@ -57,7 +70,7 @@ static void* create_native_vst_window(const char* title) {
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
WNDCLASSA wc = {};
|
||||
wc.lpfnWndProc = DefWindowProcA;
|
||||
wc.lpfnWndProc = VstWindowProc;
|
||||
wc.hInstance = GetModuleHandleA(nullptr);
|
||||
wc.lpszClassName = kWndClass;
|
||||
RegisterClassA(&wc);
|
||||
@@ -86,7 +99,7 @@ public:
|
||||
ChannelWorker() {
|
||||
th_ = std::thread([this] {
|
||||
#ifdef _WIN32
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
OleInitialize(nullptr);
|
||||
#endif
|
||||
std::unique_lock<std::mutex> lk(mu_);
|
||||
for (;;) {
|
||||
@@ -112,7 +125,7 @@ public:
|
||||
lk.lock();
|
||||
}
|
||||
#ifdef _WIN32
|
||||
CoUninitialize();
|
||||
OleUninitialize();
|
||||
#endif
|
||||
});
|
||||
}
|
||||
@@ -229,6 +242,10 @@ int main(int argc, char* argv[]) {
|
||||
instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from);
|
||||
};
|
||||
|
||||
double blockDurationMs = (double)block / sampleRate * 1000.0;
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
uint64_t blockCount = 0;
|
||||
|
||||
// 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
|
||||
while (true) {
|
||||
#ifdef _WIN32
|
||||
@@ -289,8 +306,6 @@ int main(int argc, char* argv[]) {
|
||||
playheadSamples = c.arg1;
|
||||
}
|
||||
} else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id
|
||||
// ponytail: per-plugin channel mapping chua co — gan GUI cho
|
||||
// instrument dau tien duoc load (smoke test = 1 instrument).
|
||||
// Chay tren CUNG ChannelWorker da load instrument: thread rieng
|
||||
// cho openGUI lai tao COM apartment moi, con plugin thi song o
|
||||
// apartment cu da chet (load thread exit) -> Nexus attached()
|
||||
@@ -299,16 +314,14 @@ int main(int argc, char* argv[]) {
|
||||
// len worker (async, VST3 init co the mat giay) truoc khi type=4
|
||||
// duoc xu ly — khong doi, instruments.get() con rong -> "no
|
||||
// instrument loaded". Poll toi da 10s cho LOAD hoan tat.
|
||||
uint32_t guiCh = 16;
|
||||
for (int tries = 0; tries < 200 && guiCh == 16; ++tries) {
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
if (instruments.get(ch)) { guiCh = ch; break; }
|
||||
}
|
||||
if (guiCh == 16) sleep_ms(50);
|
||||
uint32_t guiCh = c.channel;
|
||||
if (guiCh >= 16) guiCh = 0;
|
||||
for (int tries = 0; tries < 200 && !instruments.get(guiCh); ++tries) {
|
||||
sleep_ms(50);
|
||||
}
|
||||
if (guiCh == 16) {
|
||||
if (!instruments.get(guiCh)) {
|
||||
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1
|
||||
<< " plugin=" << c.arg2 << " (no instrument loaded)" << std::endl;
|
||||
<< " plugin=" << c.arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
|
||||
} else {
|
||||
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
|
||||
workers[guiCh]->post([&instruments, &guiWindows, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() {
|
||||
@@ -317,14 +330,27 @@ int main(int argc, char* argv[]) {
|
||||
void* hwnd = (void*)(uintptr_t)arg1;
|
||||
#ifdef _WIN32
|
||||
if (arg1 == 0) {
|
||||
// B9: bridge tự tạo native window — editor VST3 đính
|
||||
// vào đây; message pump bởi ChannelWorker idle loop.
|
||||
hwnd = create_native_vst_window(arg2.c_str());
|
||||
if (!hwnd) {
|
||||
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
|
||||
return;
|
||||
HWND existingHwnd = nullptr;
|
||||
auto it = guiWindows.find(guiCh);
|
||||
if (it != guiWindows.end()) {
|
||||
existingHwnd = (HWND)it->second;
|
||||
}
|
||||
if (existingHwnd && IsWindow(existingHwnd)) {
|
||||
hwnd = existingHwnd;
|
||||
SetWindowTextA((HWND)hwnd, arg2.c_str());
|
||||
ShowWindow((HWND)hwnd, SW_SHOW);
|
||||
SetForegroundWindow((HWND)hwnd);
|
||||
} else {
|
||||
hwnd = create_native_vst_window(arg2.c_str());
|
||||
if (!hwnd) {
|
||||
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
|
||||
return;
|
||||
}
|
||||
guiWindows[guiCh] = hwnd; // keep window alive
|
||||
if (auto* inst = instruments.get(guiCh)) {
|
||||
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)inst);
|
||||
}
|
||||
}
|
||||
guiWindows[guiCh] = hwnd; // keep window alive
|
||||
}
|
||||
#else
|
||||
(void)guiWindows;
|
||||
@@ -376,8 +402,19 @@ int main(int argc, char* argv[]) {
|
||||
break;
|
||||
}
|
||||
|
||||
// E. Sleep briefly for the next frame tick (block@44100 ≈ 5.8ms)
|
||||
sleep_ms(1);
|
||||
// E. Synchronize with real-time audio playback
|
||||
blockCount++;
|
||||
auto targetTime = startTime + std::chrono::microseconds(static_cast<int64_t>(blockCount * blockDurationMs * 1000.0));
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (now < targetTime) {
|
||||
auto diff = std::chrono::duration_cast<std::chrono::microseconds>(targetTime - now).count();
|
||||
if (diff > 1000) {
|
||||
sleep_ms(diff / 1000);
|
||||
}
|
||||
while (std::chrono::steady_clock::now() < targetTime) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
Reference in New Issue
Block a user