v15b: fix CC7 to VSTi (legacy tag kHostMidiCC+cc + volume-param fallback), CC lane grid + 0/+- labels

This commit is contained in:
2026-08-26 16:45:42 +07:00
parent e54dec4010
commit 24109f8bc4
6 changed files with 132 additions and 14 deletions
+56 -1
View File
@@ -76,6 +76,8 @@ using Steinberg::Vst::ParamID;
using Steinberg::Vst::ParamValue;
using Steinberg::Vst::IMidiMapping;
using Steinberg::Vst::IParamValueQueue;
using Steinberg::Vst::ParameterInfo;
using Steinberg::Vst::TChar;
using Steinberg::Vst::kNoParamId;
using Steinberg::Vst::BusInfo;
using Steinberg::Vst::kAudio;
@@ -154,6 +156,11 @@ struct Vst3HostState {
// every overlapping voice gets a unique id and note-off releases them all.
int32 nextNoteId = 1;
std::vector<int32> noteIdsByPitch[128];
// CC → plugin param tag cache for the no-IMidiMapping volume fallback
// (scanVolumeParam). 0 = unresolved; kNoParamId = no matching param.
bool ccParamResolved[128] = {};
bool ccLogged[128] = {};
ParamID ccParamTag[128] = {};
};
// Resolve the plugin's ParamID for a MIDI CC / pitch-bend, preferring the
@@ -173,6 +180,39 @@ ParamID midiControllerTag(IPtr<IEditController>& controller, int16 channel,
return legacyTag;
}
// Case-insensitive substring match of an ASCII token on a UTF-16 SDK string.
bool titleContains(const TChar* s, const char* token) {
for (int i = 0; i < 127 && s[i] != 0; ++i) {
int j = 0;
for (; token[j]; ++j) {
if (i + j >= 127 || s[i + j] == 0) break;
char c = (char)(s[i + j] & 0x7F);
if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
if (c != token[j]) break;
}
if (token[j] == 0) return true;
}
return false;
}
// No-IMidiMapping fallback: scan the controller's parameter list for the
// first volume/gain/output/level/master param (priority order) and use its
// tag for CC 7/11. Plugins without mapping ignore the legacy kHostMidiCC
// tags, so CC volume curves die there without this scan.
ParamID scanVolumeParam(IPtr<IEditController>& controller) {
if (!controller) return kNoParamId;
static const char* tokens[] = {"volume", "gain", "output", "level", "master"};
int32 count = controller->getParameterCount();
for (int32 i = 0; i < count; ++i) {
ParameterInfo info = {};
if (controller->getParameterInfo(i, info) != kResultOk) continue;
for (const char* t : tokens) {
if (titleContains(info.title, t)) return info.id;
}
}
return kNoParamId;
}
} // namespace
#endif
@@ -474,7 +514,22 @@ void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value
auto* s = static_cast<Vst3HostState*>(state_);
if (!s || !s->controller) return;
ParamID tag = midiControllerTag(s->controller, 0, (int16)cc,
kHostMidiCC);
kHostMidiCC + cc);
// IMidiMapping absent (common): plugin cannot resolve kHostMidiCC+cc, so
// fall back to a scanned volume param (resolved once per CC).
if (cc < 128 && tag == kHostMidiCC + cc && !s->ccParamResolved[cc]) {
s->ccParamResolved[cc] = true;
s->ccParamTag[cc] = scanVolumeParam(s->controller);
std::cerr << "[midi] cc=" << cc << " no IMidiMapping → volume-param tag=0x"
<< std::hex << s->ccParamTag[cc] << std::dec << std::endl;
}
if (cc < 128 && tag == kHostMidiCC + cc) tag = s->ccParamTag[cc];
if (cc < 128 && !s->ccLogged[cc]) {
s->ccLogged[cc] = true;
std::cerr << "[midi] cc=" << cc << " tag=0x" << std::hex << tag
<< std::dec << " v=" << value << std::endl;
}
if (tag == kNoParamId) return;
ParamValue v = value / 127.0;
// setParamNormalized covers single-component plugins; the param queue
// reaches split plugins that read inputParameterChanges inside process().