I’ve found a bug in ExternalPlugin where the wrong plugin component is loaded from VST3 bundles containing multiple plugins (e.g., Serum 2 which has both
“Serum 2” instrument and “Serum 2 FX” effect in the same .vst3 file).
Example:
// Get the instrument plugin description from KnownPluginList
auto& knownPlugins = engine.getPluginManager().knownPluginList;
juce::PluginDescription serumInstrument;
for (auto& desc : knownPlugins.getTypes()) {
if (desc.name == “Serum 2” && desc.isInstrument) {
serumInstrument = desc;
break;
}
}
// Create the plugin on a track
auto pluginTree = ExternalPlugin::create(engine, serumInstrument);
track->pluginList.insertPlugin(pluginTree, -1, nullptr);
// Save the Edit…
edit.save();
// Reload the Edit…
// BUG: The plugin that loads is “Serum 2 FX” (effect), not “Serum 2” (instrument)
Root cause in tracktion_ExternalPlugin.cpp:
pluginFormatName isn’t stored in the ValueTree state, so it’s empty on reload
findMatchingPlugin() uses getTypeForIdentifierString() which can return the wrong component via fuzzy matching
findMatchingPluginDescription() matches by uniqueId only, not considering the plugin name - so the first plugin matching the file path wins
Good point! Yes, you’re right that deprecatedUid should be different for different plugins within the same container.
The issue is that pluginFormatName isn’t persisted in the ValueTree state, so on reload it’s empty. This causes the primary matching via
getTypeForIdentifierString() to fail (since the identifier string doesn’t match without the format name), and then the code falls back to
findMaIdentifierString() which does fuzzy matching without considering the deprecatedUid.
Here’s the problematic flow:
// In findMatchingPlugin():
if (auto p = pm.knownPluginList.getTypeForIdentifierString(createIdentifierString(…)))
return p; // FAILS: pluginFormatName is empty, so identifier doesn’t match
// Falls back to findMaIdentifierString() which only fuzzy-matches by:
// - File path (same for all plugins in the bundle)
// - Name substring matching (can match the wrong component)
// - Does NOT check deprecatedUid
For Serum 2 specifically, both “Serum 2” (instrument) and “Serum 2 FX” (effect) are in the same .vst3 bundle with similar names, so the fuzzy matcher
picks whichever one comes first in the scan.
Regarding Waves: I can’t test those plugins myself so I’m not 100% sure why they work despite being bundled together. It’s possible their naming is
distinct enough that fuzzy matching doesn’t collide, or maybe there’s a different code path I’m missing. Either way, the fix should make the matching more
robust and explicit.
The fix ensures:
pluginFormatName is stored/restored so primary matching works
Name verification after UID matching as a safety check
uniqueId+name matching before falling back to uniqueId-only
Well the whole point of the UID is so that the name can be changed by the plugin and loading plugins doesn’t break if the name changes.
If the problem is that the format isn’t saved to the edit file, then wouldn’t it be best to just get the format from the fileOrIdentifier which we do save? There’s an implementation just below the line you quoted:
auto getPreferredFormat = [] (juce::PluginDescription d)
{
auto file = d.fileOrIdentifier.toLowerCase();
if (file.endsWith (".vst3")) return "VST3";
if (file.endsWith (".vst") || file.endsWith (".dll")) return "VST";
if (file.startsWith ("audiounit:")) return "AudioUnit";
return "";
};
So if the first:
if (auto p = pm.knownPluginList.getTypeForIdentifierString (createIdentifierString (desc)))
return p;
fails, we just do something like this immidiately below?
auto getPreferredFormat = [] (juce::PluginDescription d)
{
auto file = d.fileOrIdentifier.toLowerCase();
if (file.endsWith (".vst3")) return "VST3";
if (file.endsWith (".vst") || file.endsWith (".dll")) return "VST";
if (file.startsWith ("audiounit:")) return "AudioUnit";
return "";
};
auto descWithFormat = desc;
descWithFormat.pluginFormatName = getPreferredFormat (desc);
if (auto p = pm.knownPluginList.getTypeForIdentifierString (createIdentifierString (descWithFormat)))
return p;
Does that fix your issue?
I agree we should add:
if (auto p = pm.knownPluginList.getTypeForIdentifierString ("VST3" + createIdentifierString (desc)))
return p;
as well.
But I don’t think we should prioritise name matching over UID.
That makes sense. Inferring the format from fileOrIdentifier and retrying the UID-based lookup is cleaner than persisting the format name. I’ll update
our branch to use that approach. Agreed on not prioritizing name over UID.