Possible bug in juce_VST3_Wrapper.cpp processParameterChanges

I am facing a problem when selecting a specific program in a VST3 plugin, from the host. It seems to randomly select the previous program.

While debugging, I came to this:

            if (vstParamID == JuceAudioProcessor::paramPreset)
            {
                auto numPrograms  = pluginInstance->getNumPrograms();
                auto programValue = roundToInt (value * (jmax (0, numPrograms - 1)));

                if (numPrograms > 1 && isPositiveAndBelow (programValue, numPrograms)
                     && programValue != pluginInstance->getCurrentProgram())
                    pluginInstance->setCurrentProgram (programValue);
            }

Could it be because you remove 1 to numPrograms in roundToInt (value * (jmax (0, numPrograms - 1)))?

Please check and let me know if I should look elsewhere.

Thanks.

Mariano

Yes, there seems to be a mismatch when normalising vs denormalising. Here in setCurrentProgram:

void setCurrentProgram (int program) override
{
    if (programNames.size() > 0 && editController != nullptr)
    {
        auto value = static_cast<Vst::ParamValue> (program) / static_cast<Vst::ParamValue> (programNames.size());

        editController->setParamNormalized (programParameterID, value);
        Steinberg::int32 index;
        inputParameterChanges->addParameterData (programParameterID, index)->addPoint (0, value, index);
    }
}

It goes even further than that. The syncProgramNames method fills the programNames StringArray with some duplicates and skips some program names.

Could you please look into this and let me know if you get the same behaviour at your end? It looks like the normalisation/denormalisation of program nr is flawed.

Ok, hopefully someone starts paying attention to this. I can’t do “fuzzy” voice selection;-)

In syncProgramNames, the issue seems to arise out of floating point truncation here:

void toString (Vst::ParamValue value, Vst::String128 result) const override
{
    toString128 (result, owner.getProgramName (static_cast<int> (value * info.stepCount)));
}

info.stepCount equals 93 in my case, and value is the program index divided by as much.

Index 12 and 13 are both evaluated as 12.

Are you using the JUCE plug-in host? I’ve tried reproducing this with the following code but I’m not seeing any issues:

/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

  name:             VST3Programs

  dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats, juce_audio_plugin_client, 
                    juce_audio_processors, juce_audio_utils, juce_core, juce_data_structures, juce_events, 
                    juce_graphics, juce_gui_basics, juce_gui_extra
  exporters:        xcode_mac

  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

  type:             AudioProcessor
  mainClass:        MyPlugin

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once


//==============================================================================
class MyPlugin  : public AudioProcessor
{
public:
    //==============================================================================
    MyPlugin()
        : AudioProcessor (BusesProperties().withInput  ("Input",  AudioChannelSet::stereo())
                                           .withOutput ("Output", AudioChannelSet::stereo()))
    {
    }

    ~MyPlugin()
    {
    }

    //==============================================================================
    void prepareToPlay (double, int) override
    {
        // Use this method as the place to do any pre-playback
        // initialisation that you need..
    }

    void releaseResources() override
    {
        // When playback stops, you can use this as an opportunity to free up any
        // spare memory, etc.
    }

    void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
    {
        ScopedNoDenormals noDenormals;
        auto totalNumInputChannels  = getTotalNumInputChannels();
        auto totalNumOutputChannels = getTotalNumOutputChannels();

        // In case we have more outputs than inputs, this code clears any output
        // channels that didn't contain input data, (because these aren't
        // guaranteed to be empty - they may contain garbage).
        // This is here to avoid people getting screaming feedback
        // when they first compile a plugin, but obviously you don't need to keep
        // this code if your algorithm always overwrites all the output channels.
        for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
            buffer.clear (i, 0, buffer.getNumSamples());

        // This is the place where you'd normally do the guts of your plugin's
        // audio processing...
        // Make sure to reset the state if your inner loop is processing
        // the samples and the outer loop is handling the channels.
        // Alternatively, you can process the samples with the channels
        // interleaved by keeping the same state.
        for (int channel = 0; channel < totalNumInputChannels; ++channel)
        {
            auto* channelData = buffer.getWritePointer (channel);

            // ..do something to the data...
        }
    }

    //==============================================================================
    AudioProcessorEditor* createEditor() override          { return nullptr; }
    bool hasEditor() const override                        { return false;   }

    //==============================================================================
    const String getName() const override                  { return "VST3Programs"; }
    bool acceptsMidi() const override                      { return false; }
    bool producesMidi() const override                     { return false; }
    double getTailLengthSeconds() const override           { return 0; }

    //==============================================================================
    int getNumPrograms() override                          { return programs.size(); }
    int getCurrentProgram() override                       { return selected; }
    void setCurrentProgram (int index) override            { selected = index; DBG ("PROGRAM SELECTED: " + programs[selected]); }
    const String getProgramName (int index) override       { return programs[index]; }
    void changeProgramName (int, const String&) override   {}

    //==============================================================================
    void getStateInformation (MemoryBlock& destData) override
    {
        // You should use this method to store your parameters in the memory block.
        // You could do that either as raw data, or use the XML or ValueTree classes
        // as intermediaries to make it easy to save and load complex data.
    }

    void setStateInformation (const void* data, int sizeInBytes) override
    {
        // You should use this method to restore your parameters from this memory block,
        // whose contents will have been created by the getStateInformation() call.
    }

    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        // This is the place where you check if the layout is supported.
        // In this template code we only support mono or stereo.
        if (layouts.getMainOutputChannelSet() != AudioChannelSet::mono()
            && layouts.getMainOutputChannelSet() != AudioChannelSet::stereo())
            return false;

        // This checks if the input layout matches the output layout
        if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
            return false;

        return true;
    }

private:
    int selected = 0;
    StringArray programs { "Program 1",  "Program 2",  "Program 3",  "Program 4",  "Program 5",
                           "Program 6",  "Program 7",  "Program 8",  "Program 9",  "Program 10",
                           "Program 11", "Program 12", "Program 13", "Program 14", "Program 15",
                           "Program 16", "Program 17", "Program 18", "Program 19", "Program 20" };
    
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyPlugin)
};

What do your plug-in’s get/setCurrentProgram() methods look like?

Thanks for looking into this. Could I ask you to try with 93 programs? It seems to be a truncation issue, so 20 items may not do the trick.

My get/setCurrentProgram are pretty the same: getting/setting an instrument from a table.

/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

  name:             VST3Programs

  dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats, juce_audio_plugin_client, 
                    juce_audio_processors, juce_audio_utils, juce_core, juce_data_structures, juce_events, 
                    juce_graphics, juce_gui_basics, juce_gui_extra
  exporters:        xcode_mac

  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

  type:             AudioProcessor
  mainClass:        MyPlugin

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once


//==============================================================================
class MyPlugin  : public AudioProcessor
{
public:
    //==============================================================================
    MyPlugin()
        : AudioProcessor (BusesProperties().withInput  ("Input",  AudioChannelSet::stereo())
                                           .withOutput ("Output", AudioChannelSet::stereo()))
    {
        for (int i = 0; i < 93; ++i)
            programs.add ("Program" + String (i));
    }

    ~MyPlugin()
    {
    }

    //==============================================================================
    void prepareToPlay (double, int) override
    {
        // Use this method as the place to do any pre-playback
        // initialisation that you need..
    }

    void releaseResources() override
    {
        // When playback stops, you can use this as an opportunity to free up any
        // spare memory, etc.
    }

    void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
    {
        ScopedNoDenormals noDenormals;
        auto totalNumInputChannels  = getTotalNumInputChannels();
        auto totalNumOutputChannels = getTotalNumOutputChannels();

        // In case we have more outputs than inputs, this code clears any output
        // channels that didn't contain input data, (because these aren't
        // guaranteed to be empty - they may contain garbage).
        // This is here to avoid people getting screaming feedback
        // when they first compile a plugin, but obviously you don't need to keep
        // this code if your algorithm always overwrites all the output channels.
        for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
            buffer.clear (i, 0, buffer.getNumSamples());

        // This is the place where you'd normally do the guts of your plugin's
        // audio processing...
        // Make sure to reset the state if your inner loop is processing
        // the samples and the outer loop is handling the channels.
        // Alternatively, you can process the samples with the channels
        // interleaved by keeping the same state.
        for (int channel = 0; channel < totalNumInputChannels; ++channel)
        {
            auto* channelData = buffer.getWritePointer (channel);

            // ..do something to the data...
        }
    }

    //==============================================================================
    AudioProcessorEditor* createEditor() override          { return nullptr; }
    bool hasEditor() const override                        { return false;   }

    //==============================================================================
    const String getName() const override                  { return "VST3Programs"; }
    bool acceptsMidi() const override                      { return false; }
    bool producesMidi() const override                     { return false; }
    double getTailLengthSeconds() const override           { return 0; }

    //==============================================================================
    int getNumPrograms() override                          { return programs.size(); }
    int getCurrentProgram() override                       { return selected; }
    void setCurrentProgram (int index) override            { selected = index; DBG ("PROGRAM SELECTED: " + programs[selected]); }
    const String getProgramName (int index) override       { return programs[index]; }
    void changeProgramName (int, const String&) override   {}

    //==============================================================================
    void getStateInformation (MemoryBlock& destData) override
    {
        // You should use this method to store your parameters in the memory block.
        // You could do that either as raw data, or use the XML or ValueTree classes
        // as intermediaries to make it easy to save and load complex data.
    }

    void setStateInformation (const void* data, int sizeInBytes) override
    {
        // You should use this method to restore your parameters from this memory block,
        // whose contents will have been created by the getStateInformation() call.
    }

    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        // This is the place where you check if the layout is supported.
        // In this template code we only support mono or stereo.
        if (layouts.getMainOutputChannelSet() != AudioChannelSet::mono()
            && layouts.getMainOutputChannelSet() != AudioChannelSet::stereo())
            return false;

        // This checks if the input layout matches the output layout
        if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
            return false;

        return true;
    }

private:
    int selected = 0;
    StringArray programs;
    
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyPlugin)
};

This is working fine for me.

My issue was in getProgramName, when called from VST3EditController::toString. Can you debug that part as well with your sample?

OK, I’ve pushed a fix here:

Does this solve the issue?

Seems to have fixed the issue for duplicate program names, thanks!

Though setCurrentProgram still often selects the program nr below. As you didn’t find any issue with that, I need to delve deeper in my code to see if the bug is at my end.

But in the meantime could you confirm that dividing by numPrograms (see above) and then multiplying by numPrograms - 1 is what was intended?

No, I don’t think that was intentional. I’ve pushed a fix here:

Thanks for reporting!

Awesome! Seems to work fine now.

Thanks for the fix!