A14: MIDI-out từ plugin (VST2/VST3) → SHM → Rust emit → DAW track

- Vst2Instrument: audioMaster callback thu VstEvents, push SHM midiOutQueue
- Vst3Instrument: IEventList output bus (kAudioMasterWantMidi) — check
- SharedMemoryIPC.h: midiOutQueue[64] + midiOutCount (offset 27556/28324, sizeof 28328)
- shm.rs: MidiEventIPC Serialize, drain_midi_out() emit 'bridge-midi-out'
- lib.rs: pump emit midi-out events
- nativeBridgeService.js: onMidiOut API
- app.jsx: route MIDI-out về selected track qua SonicMidiRouter (feedback guard)
- TASKS.md: A14 ✅
This commit is contained in:
2026-08-19 21:12:07 +07:00
parent 6efef85e47
commit 6a3b430da0
10 changed files with 375 additions and 226 deletions
+30
View File
@@ -30,6 +30,10 @@ thread_local bool t_editingProbe = false;
// Resize hook registered by the bridge (main.cpp): plugin audioMaster
// (SizeWindow) -> move the host window on the UI worker thread.
static void (*g_vst2ResizeHook)(void*, int, int) = nullptr;
// A14: plugin-generated MIDI (VST2 audioMaster opcode=8 -> VstEvents).
// main.cpp registers a hook that writes into the SHM midiOutQueue.
static void (*g_vst2MidiOutHook)(uint8_t cmd, uint8_t ch, uint8_t d1,
uint8_t d2, uint32_t deltaFrames) = nullptr;
// base64 encode/decode (RFC 4648) — local copies; Vst3Instrument.cpp keeps
// its own so neither file depends on the other's statics.
@@ -118,6 +122,28 @@ intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
case kAudioMasterGetBlockSize:
return self ? (intptr_t)self->maxBlockSize() : 512;
case kAudioMasterWantMidi: return 1; // we send MIDI events
case kAudioMasterProcessEvents: {
// Plugin -> host MIDI output (Scaler2/Instacomposer/etc.
// generate chords). ptr = VstEvents*.
if (!ptr || !g_vst2MidiOutHook) return 0;
const VstEvents* evs = static_cast<const VstEvents*>(ptr);
if (evs->numEvents <= 0) return 1;
int n = evs->numEvents > 64 ? 64 : evs->numEvents;
for (int i = 0; i < n; ++i) {
const VstEvent* e = evs->events[i];
if (!e || e->type != kVstMidiType) continue;
// VstMidiEvent starts with the VstEvent prefix — layout-compatible,
// but the C++ types are unrelated so the cast must be reinterpret_cast.
const VstMidiEvent* me = reinterpret_cast<const VstMidiEvent*>(e);
uint8_t status = (uint8_t)me->midiData[0];
if (!status) continue; // running status not used by plugins
g_vst2MidiOutHook(status & 0xF0, status & 0x0F,
(uint8_t)me->midiData[1],
(uint8_t)me->midiData[2],
(uint32_t)me->deltaFrames);
}
return 1;
}
case kAudioMasterGetTime: {
static VstTimeInfo ti;
static double pos = 0.0;
@@ -615,6 +641,10 @@ void Vst2Instrument::setResizeHook(void (*fn)(void*, int, int)) {
g_vst2ResizeHook = fn;
}
void Vst2Instrument::setMidiOutHook(void (*fn)(uint8_t, uint8_t, uint8_t, uint8_t, uint32_t)) {
g_vst2MidiOutHook = fn;
}
void Vst2Instrument::captureState() {
auto* s = static_cast<Vst2HostState*>(state_);
if (!s || !s->effect) return;
+4
View File
@@ -51,6 +51,10 @@ public:
// Register the SizeWindow hook (main.cpp posts the resize to the UI
// worker). Probe builds never set it -> no-op.
static void setResizeHook(void (*fn)(void* hwnd, int w, int h));
// A14: register the plugin-MIDI-out sink (main.cpp writes SHM
// midiOutQueue). Probe builds never set it -> no-op.
static void setMidiOutHook(void (*fn)(uint8_t cmd, uint8_t ch, uint8_t d1,
uint8_t d2, uint32_t deltaFrames));
private:
void* state_; // Vst2HostState* (pimpl — aeffect.h stays out of the header)
+22
View File
@@ -120,6 +120,7 @@ static std::mutex g_guiMutex;
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
static ChannelWorker* g_uiWorker = nullptr;
static SharedAudioBufferIPC* g_shmIPC = nullptr; // A14: midi-out hook writes SHM
// VST2 (DUNE 3) thread contract: entry + init dispatcher + processReplacing
// must share ONE thread. processReplacing runs on the audio (main) thread,
// so VST2 loads (LoadLibrary + VSTPluginMain + init dispatcher) are deferred
@@ -852,6 +853,7 @@ int main(int argc, char* argv[]) {
}
auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
if (!shmIPC) { CloseHandle(hMapFile); return 1; }
g_shmIPC = shmIPC; // A14: midi-out hook (audio thread) writes SHM via this
// VSTi crackle fix: 8-slot audio ring (see SharedMemoryIPC.h). Must be set
// on Windows too — ringMask stays 0 otherwise and the ring collapses to
// 1 slot (slot = writeIndex & 0), dropping whole blocks on any jank.
@@ -903,6 +905,26 @@ int main(int argc, char* argv[]) {
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
});
});
// A14: plugin MIDI output sink. audioMaster(kAudioMasterProcessEvents)
// (VstEvents from Scaler2/Instacomposer/...) -> SHM midiOutQueue,
// drained by the Tauri pump to `bridge-midi-out`, JS routes notes to
// DAW tracks. Append-only, no mutex — same pattern as midiQueue (audio
// thread writes, pump reads+resets). Cap 64; drop when full.
Vst2Instrument::setMidiOutHook([](uint8_t cmd, uint8_t ch, uint8_t d1,
uint8_t d2, uint32_t deltaFrames) {
if (!g_shmIPC) return;
if (g_shmIPC->midiOutCount >= 64) return;
auto& ev = g_shmIPC->midiOutQueue[g_shmIPC->midiOutCount++];
ev.command = cmd;
ev.channel = ch;
ev.pitch = d1;
ev.velocity = d2;
ev.data2 = 0;
ev.data3 = 0;
ev.reserved[0] = 0;
ev.reserved[1] = 0;
ev.sampleOffset = deltaFrames;
});
// B9: native editor windows per channel — keep alive (HWND outlives the job).
// Registry la global (g_guiWindows) — WM_DESTROY cleanup can tu VstWindowProc.
// B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE).