Bug: Wrong plugin loaded from multi-component VST3 bundles

Hi,

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

I’ve implemented a fix: GitHub - Conceptual-Machines/tracktion_engine at fix/vst3-multi-component-matching

Changes:

  1. Store/restore pluginFormatName in ValueTree state
  2. Add name verification after getTypeForIdentifierString() matches
  3. Add uniqueId + name matching in findMatchingPluginDescription() before falling back to uniqueId only
  4. Add “VST3” to format prefix fallbacks (only “VST” and “AudioUnit” were checked)

Happy to discuss or provide more details!

But doesn’t the first line here:

std::unique_ptr<juce::PluginDescription> ExternalPlugin::findMatchingPlugin() const
{
    CRASH_TRACER
    auto& pm = engine.getPluginManager();

    if (auto p = pm.knownPluginList.getTypeForIdentifierString (createIdentifierString (desc)))
        return p;

use the deprecatedUid:

static juce::String getDeprecatedPluginDescSuffix (const juce::PluginDescription& d)
{
    return "-" + juce::String::toHexString (d.fileOrIdentifier.hashCode())
         + "-" + juce::String::toHexString (d.deprecatedUid);
}

juce::String createIdentifierString (const juce::PluginDescription& d)
{
    return d.pluginFormatName + "-" + d.name + getDeprecatedPluginDescSuffix (d);
}

I thought this UID is different for different plugins within the same container?

Are you saying that a juce::PluginDescription doesn’t uniquely identify a plugin, even within a container?

The reason I ask is because I’m pretty sure Waves plugins have worked for years..

Thanks for responding!

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:

  1. pluginFormatName is stored/restored so primary matching works
  2. Name verification after UID matching as a safety check
  3. 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.

Sorry for the late follow up.

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.

Cheers

If that works for you let me know and I’ll add it to develop

1 Like

That’d be awesome :slight_smile:

Fixed:

1 Like