Special characters in plugin names when porting to VST3

In experimenting with porting plugins from VST2 to VST3 I hit an issue I thought was worth sharing. It seems if a plugins name contains a special character in the first 8 characters of the name, I found it with a hyphen (I’m assuming other characters have issues too), then the code supplied in the documentation does not create a FUID that correctly matches with the VST2 and so the plugin isn’t replaced. I experimented with a few changes to the code but I had no luck in generating a matching FUID when the name contained a hyphen.

What should we do if a VST2 plugin contains special characters in it’s name and we’re porting to VST3? how do we determine the correct FUID to report?

Do you mean the ASCII character with the hex value of 0x2D?
And how did you test that the UID is not the correct one?

Hi Arne, thanks for getting back to me.

Yes.

So in my case I’m using JUCE but ultimately I …

  • Created a VST2 plugin containing a name with a hyphen for example “name-with-hyphen”
  • Loaded the plugin in Cubase 13
  • Saved the session
  • Deleted the VST2 plugin
  • Built the same plugin as a VST3 (in this case I have JUCE_VST3_CAN_REPLACE_VST2=1)
  • Loaded the session
  • Cubase does not correctly identify the VST 3 plugin as being able to replace the VST2 plugin

If I follow the same steps but choose a name without any hyphen, such as “namewithouthyphen”, everything works as expected.

It’s hard for me to prove beyond any doubt that the UID generated is wrong so that is somewhat a presumption. I have tried this a few times with several names and each time a hyphen early in the name leads me into a situation where the plugin is not correctly identified as being a replacement for the VST3 which is what leads me to assume an incorrect UID is the issue.

Just to add I’m running all these tests on an M2 Max MBP running macOS Ventura 13.5.2, obviously Cubase is running in Rosetta at all times so it can load VST2 plugins (I also tested with Reaper with the same results).

I haven’t yet tried excluding JUCE and reproducing the same issue with the VST3 SDK only, consider it on my todo list. However, it seems the important chunk of code is in the documentation and the bulk of the JUCE code seems largely copied from the example given in the documentation. At a glance I couldn’t see anything obviously different that would explain the behaviour.

static void getUUIDForVST2ID (bool forControllerUID, uint8 uuid[16])
    {
        #if JUCE_WINDOWS && ! JUCE_MINGW
         const auto juce_sprintf = [] (auto&& head, auto&&... tail) { sprintf_s (head, (size_t) numElementsInArray (head), tail...); };
         const auto juce_strcpy  = [] (auto&& head, auto&&... tail) { strcpy_s  (head, (size_t) numElementsInArray (head), tail...); };
         const auto juce_strcat  = [] (auto&& head, auto&&... tail) { strcat_s  (head, (size_t) numElementsInArray (head), tail...); };
         const auto juce_sscanf  = [] (auto&&... args)              { sscanf_s  (args...); };
        #else
         const auto juce_sprintf = [] (auto&& head, auto&&... tail) { snprintf  (head, (size_t) numElementsInArray (head), tail...); };
         const auto juce_strcpy  = [] (auto&&... args)              { strcpy    (args...); };
         const auto juce_strcat  = [] (auto&&... args)              { strcat    (args...); };
         const auto juce_sscanf  = [] (auto&&... args)              { sscanf    (args...); };
        #endif

        char uidString[33];

        const int vstfxid = (('V' << 16) | ('S' << 8) | (forControllerUID ? 'E' : 'T'));
        char vstfxidStr[7] = { 0 };
        juce_sprintf (vstfxidStr, "%06X", vstfxid);

        juce_strcpy (uidString, vstfxidStr);

        char uidStr[9] = { 0 };
        juce_sprintf (uidStr, "%08X", JucePlugin_VSTUniqueID);
        juce_strcat (uidString, uidStr);

        char nameidStr[3] = { 0 };
        const size_t len = strlen (JucePlugin_Name);

        for (size_t i = 0; i <= 8; ++i)
        {
            juce::uint8 c = i < len ? static_cast<juce::uint8> (JucePlugin_Name[i]) : 0;

            if (c >= 'A' && c <= 'Z')
                c += 'a' - 'A';

            juce_sprintf (nameidStr, "%02X", c);
            juce_strcat (uidString, nameidStr);
        }

        unsigned long p0;
        unsigned int p1, p2;
        unsigned int p3[8];

        juce_sscanf (uidString, "%08lX%04X%04X%02X%02X%02X%02X%02X%02X%02X%02X",
                     &p0, &p1, &p2, &p3[0], &p3[1], &p3[2], &p3[3], &p3[4], &p3[5], &p3[6], &p3[7]);

        union q0_u {
            uint32 word;
            uint8 bytes[4];
        } q0;

        union q1_u {
            uint16 half;
            uint8 bytes[2];
        } q1, q2;

        q0.word = static_cast<uint32> (p0);
        q1.half = static_cast<uint16> (p1);
        q2.half = static_cast<uint16> (p2);

        // VST3 doesn't use COM compatible UUIDs on non windows platforms
       #if ! JUCE_WINDOWS
        q0.word = ByteOrder::swap (q0.word);
        q1.half = ByteOrder::swap (q1.half);
        q2.half = ByteOrder::swap (q2.half);
       #endif

        for (int i = 0; i < 4; ++i)
            uuid[i+0] = q0.bytes[i];

        for (int i = 0; i < 2; ++i)
            uuid[i+4] = q1.bytes[i];

        for (int i = 0; i < 2; ++i)
            uuid[i+6] = q2.bytes[i];

        for (int i = 0; i < 8; ++i)
            uuid[i+8] = static_cast<uint8> (p3[i]);
    }

Any thoughts welcome.

If I run the code from the SDK with the following input:

  • myVST2UID_4Chars = ‘abcd’
  • pluginName = “name-with-hyphen”
  • forControllerUID = false

The uidString is: 565354616263646E616D652D77697468

What do you get?

Yep, I get 565345616263646E616D652D77697468 too.

To be clear that’s what I get running the code from JUCE.

And do you use this in a moduleinfo.json (the compatibility part) or directly as the UID of your processor?

Good question, IIUC I think it’s reported as the main Controller Class UID

class JuceVST3EditController final : 
...
   #if JUCE_VST3_CAN_REPLACE_VST2
    inline static const FUID iid = getFUIDForVST2ID (true);
   #else
    inline static const FUID iid { TUID INLINE_UID (0xABCDEF01, 0x1234ABCD, JucePlugin_ManufacturerCode, JucePlugin_PluginCode) };
   #endif
class JuceVST3Component final : public Vst::IComponent
 ...
    tresult PLUGIN_API getControllerClassId (TUID classID) override
    {
        memcpy (classID, JuceVST3EditController::iid, sizeof (TUID));
        return kResultTrue;
    }

It’s also referenced when reporting class entries in getPClassInfo().

However, we also generate a moduleinfo.json which in this case looks like so…

{
  "Name": "name-with-hyphen",
  "Version": "1.0.0",
  "Factory Info": {
    "Vendor": "yourcompany",
    "URL": "www.yourcompany.com",
    "E-Mail": "",
    "Flags": {
      "Unicode": true,
      "Classes Discardable": false,
      "Component Non Discardable": false,
    },
  },
  "Classes": [
    {
      "CID": "565354616263646E616D652D77697468",
      "Category": "Audio Module Class",
      "Name": "name-with-hyphen",
      "Vendor": "yourcompany",
      "Version": "1.0.0",
      "SDKVersion": "VST 3.7.8",
      "Sub Categories": [
        "Fx",
      ],
      "Class Flags": 0,
      "Cardinality": 2147483647,
      "Snapshots": [
      ],
    },
    {
      "CID": "565345616263646E616D652D77697468",
      "Category": "Component Controller Class",
      "Name": "name-with-hyphen",
      "Vendor": "yourcompany",
      "Version": "1.0.0",
      "SDKVersion": "VST 3.7.8",
      "Sub Categories": [
        "Fx",
      ],
      "Class Flags": 0,
      "Cardinality": 2147483647,
      "Snapshots": [
      ],
    },
    {
      "CID": "ABCDEF01C0DEF00D4D616E7561626364",
      "Category": "Plugin Compatibility Class",
      "Name": "name-with-hyphen",
      "Vendor": "yourcompany",
      "Version": "1.0.0",
      "SDKVersion": "VST 3.7.8",
      "Class Flags": 0,
      "Cardinality": 2147483647,
      "Snapshots": [
      ],
    },
  ],
}

Correction: It looks correct, indeed. So I don’t know why it should not work.

Can you make the plug-in available? I can have a look here.

Well that’s typical, before sending over the plugins I wanted to triple check the VST3 version wasn’t replacing the VST2 version and then send you both and a Cubase 13 project but now I can’t reproduce the issue :person_facepalming:. I’ll look at this again tomorrow and get back to you. Thanks again for your time.

Yeah, that’s a typical thing I run into sometimes too.