4.3.0 Some AU plugins have wrong I/O Count

Hi
Was not able to pin it to a commit, but since we updated to 4.3.0, there is now an issue where some plugins.
(Found the issue with Lexicon LXP reverb (and the MXP)) as VST it shows up as stereo, but as AU it shows up as mono.
Also tried the latest dev.tip, same issue
Any ideas about what causes this?

We don’t have LXP reverb here. Any Waves plug-in or demo version of a plug-in where I could re-produce this?

Hi
It seems this issue is due to me ignoring breaking changes in plugin hosting in 4.3.0 (The mulit bus stuff)
So I would need to rephrase the question.
Is there a step-by-step quide to get this running?
The “tutorial video” is more of a demo.
I can probably use some hours and make something work, but this is VERY fragile stuff with maaaany plugins freaking out in different ways if not handled correctly.

In simple terms the structure of our host is this:

ProcessorGraph (Contains all plugins (“PluginContainer” class)

  • PluginContainer class (A Processor that has an instance of an AudioPluginInstance
    I don’t think this setup is that crazy (?)
    But it involves some communication, as the container class need to report the bus structure of the plugin, or the plugin needs to be informed of the structure of the container.

What I need to get out product stable again is some way to tell a plugin to load with default number of IO, like it did by before, or maybe just load with max number of IO.

Then later we can add in options for the users to change bus structure etc, but that is a bigger task, now we just need to get this running again

Hi Nikolai,

You are right this has gotten a bit more complicated, especially if you are using an AudioProcessor to wrap another PluginInstance. The correct way to wrap an AudioProcessor is the following way:

class WrapperProcessor
     : public AudioProcessor
{
public:
    WrapperProcessor (AudioPluginInstance* processorToUse)
        : AudioProcessor (getBusesPropertiesFromProcessor (processorToUse)),
          plugin (processorToUse)
    {}
    
    //==============================================================================
    const String getName() const override                                          { return plugin->getName(); }
    bool canAddBus (bool inputBus) const override                                  { return plugin->canAddBus (inputBus); }
    bool canRemoveBus (bool inputBus) const override                               { return plugin->canRemoveBus (inputBus); }
    bool supportsDoublePrecisionProcessing() const override                        { return plugin->supportsDoublePrecisionProcessing(); }
    double getTailLengthSeconds() const override                                   { return plugin->getTailLengthSeconds(); }
    bool acceptsMidi() const override                                              { return plugin->acceptsMidi(); }
    bool producesMidi() const override                                             { return plugin->producesMidi(); }
    bool supportsMPE() const override                                              { return plugin->supportsMPE(); }
    bool isMidiEffect() const override                                             { return plugin->isMidiEffect(); }
    void reset() override                                                          { plugin->reset(); }
    AudioProcessorEditor* createEditor() override                                  { return plugin->createEditor(); }
    bool hasEditor() const override                                                { return plugin->hasEditor(); }
    int getNumParameters() override                                                { return plugin->getNumParameters(); }
    const String getParameterName (int parameterIndex) override                    { return plugin->getParameterName (parameterIndex); }
    String getParameterID (int index) override                                     { return plugin->getParameterID (index); }
    float getParameter (int parameterIndex) override                               { return plugin->getParameter (parameterIndex); }
    String getParameterName (int parameterIndex, int maximumStringLength) override { return plugin->getParameterName (parameterIndex, maximumStringLength); }
    const String getParameterText (int parameterIndex) override                    { return plugin->getParameterText (parameterIndex); }
    String getParameterText (int parameterIndex, int maximumStringLength) override { return plugin->getParameterText (parameterIndex, maximumStringLength); }
    int getParameterNumSteps (int parameterIndex) override                         { return plugin->getParameterNumSteps (parameterIndex); }
    float getParameterDefaultValue (int parameterIndex) override                   { return plugin->getParameterDefaultValue (parameterIndex); }
    String getParameterLabel (int index) const override                            { return plugin->getParameterLabel (index); }
    bool isParameterOrientationInverted (int index) const override                 { return plugin->isParameterOrientationInverted (index); }
    void setParameter (int parameterIndex, float newValue) override                { plugin->setParameter (parameterIndex, newValue); }
    bool isParameterAutomatable (int parameterIndex) const override                { return plugin->isParameterAutomatable (parameterIndex); }
    bool isMetaParameter (int parameterIndex) const override                       { return plugin->isMetaParameter (parameterIndex); }
    int getNumPrograms() override                                                  { return plugin->getNumPrograms(); }
    int getCurrentProgram() override                                               { return plugin->getCurrentProgram(); }
    void setCurrentProgram (int index) override                                    { plugin->setCurrentProgram (index); }
    const String getProgramName (int index) override                               { return plugin->getProgramName (index); }
    void changeProgramName (int index, const String& newName) override             { plugin->changeProgramName (index, newName); }
    void getStateInformation (juce::MemoryBlock& destData) override                { plugin->getStateInformation (destData); }
    void getCurrentProgramStateInformation (juce::MemoryBlock& destData) override  { plugin->getCurrentProgramStateInformation (destData); }
    void setStateInformation (const void* data, int sizeInBytes) override          { plugin->setStateInformation (data, sizeInBytes); }
    void setCurrentProgramStateInformation (const void* data, int bytes) override  { plugin->setCurrentProgramStateInformation (data, bytes); }
    
    //==============================================================================
    void prepareToPlay (double sampleRate, int maximumExpectedSamplesPerBlock) override
    {
        plugin->releaseResources();
        
        plugin->setRateAndBufferSizeDetails (sampleRate, maximumExpectedSamplesPerBlock);
        
        // sync number of buses
        for (int dir = 0; dir < 2; ++dir)
        {
            const bool isInput = (dir == 0);
            int expectedNumBuses =         getBusCount (isInput);
            int requiredNumBuses = plugin->getBusCount (isInput);
            
            for (; expectedNumBuses < requiredNumBuses; expectedNumBuses++)
                plugin->addBus (isInput);
                
            for (; requiredNumBuses < expectedNumBuses; requiredNumBuses++)
                plugin->removeBus (isInput);
        }
        
        plugin->setBusesLayout (getBusesLayout());
        plugin->prepareToPlay (sampleRate, maximumExpectedSamplesPerBlock);
    }
    
    void releaseResources() override         { return plugin->releaseResources(); }
    
    //==============================================================================
    void processBlock (AudioBuffer<float>& buffer,  MidiBuffer& midiMessages) override         { plugin->processBlock (buffer, midiMessages); }
    void processBlock (AudioBuffer<double>& buffer, MidiBuffer& midiMessages) override         { plugin->processBlock (buffer, midiMessages); }
    void processBlockBypassed (AudioBuffer<float>& buffer,  MidiBuffer& midiMessages) override { plugin->processBlockBypassed (buffer, midiMessages); }
    void processBlockBypassed (AudioBuffer<double>& buffer, MidiBuffer& midiMessages) override { plugin->processBlockBypassed (buffer, midiMessages); }
    
protected:
    
    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override        { return plugin->checkBusesLayoutSupported (layouts); }
    bool canApplyBusesLayout (const BusesLayout& layouts) const override           { return plugin->setBusesLayout (layouts); }
    bool canApplyBusCountChange (bool isInput, bool isAddingBuses,
                                 BusProperties& outNewBusProperties) override
    {
        if (isAddingBuses)
        {
            int busIdx = plugin->getBusCount (isInput);
            
            if (! plugin->addBus (isInput))
                return false;
            
            if (Bus* bus = plugin->getBus (isInput, busIdx))
            {
                outNewBusProperties.busName              = bus->getName();
                outNewBusProperties.defaultLayout        = bus->getDefaultLayout();
                outNewBusProperties.isActivatedByDefault = bus->isEnabledByDefault();
                
                return true;
            }
            else
            {
                jassertfalse;
                return false;
            }
        }
        else
            return plugin->removeBus (isInput);
    }
private:
    //==============================================================================
    static BusesProperties getBusesPropertiesFromProcessor (AudioProcessor* processor)
    {
        BusesProperties retval;
        
        for (int dir = 0; dir < 2; ++dir)
        {
            const bool isInput = (dir == 0);
            const int n = processor->getBusCount (isInput);
            
            for (int i = 0; i < n; ++i)
                if (AudioProcessor::Bus* bus = processor->getBus (isInput, i))
                    retval.addBus (isInput, bus->getName(), bus->getDefaultLayout(), bus->isEnabledByDefault());
        }
        
        return retval;
    }
    
    ScopedPointer<AudioProcessor> plugin;
};

The reason why the extra code is needed in prepareToPlay is that the AudioProcessor class has always been passive: even before JUCE 4, the owner of an AudioProcessor could call setPlayConfigDetails and change the number of channels without the plug-in getting any callbacks. This is the same for the multi-bus API: the owner of an AudioProcessor can muck about with the number of buses and formats without the AudioProcessor ever getting a callback. The first time the plug-in can actually “know” which format, buses, sampleRate, bufferSize it has been assigned is in the prepareToPlay method. Therefore, the above code forwards all those properties in the prepareToPlay method. Does this make sense?

Let me know how you get along with the above code.

2 Likes

THANK YOU! Your answer far exceeded by expectations. This was just what I needed. I will look through it now and compare with my code.
thank you again

[NEVER MIND KEEP SCROLLING TO NEXT POST]
Hi
So I implemented my wrapper in the same way (line by line) as the code above.
My wrapper is a bit more complex, but all of the “plugin-talking” code is the same, except that I
call:

plugin = processorToUse
AudioProcessor (getBusesPropertiesFromProcessor (processorToUse))

In a function after the constructor, rather then IN the constructor.
Anyway, now got it to compile and load some plugins, but many hit an assertion here:

bool AudioProcessor::setBusesLayout (const BusesLayout& arr)
{
jassert (arr.inputBuses. size() == getBusCount (true)
&& arr.outputBuses.size() == getBusCount (false));
…
…

This is what happens up to the assert:
AudioProcessorGraph::Node calls prepareToPlay on the wrapper processor
The wrapper “syncs number of busses” (in Fabians code above) and then calls:
plugin->setBusesLayout(getBusesLayout()); And there is an assert.
Once this assert is hit, all kinds of error start to happen a bit depending on the plugin, I guess that is just because of the first assert

[NEVER MIND KEEP SCROLLING TO NEXT POST]
Ok, I’m just trying to understand this. But are you sure the PrepareToPlay code is right?

If you run

  	 expectedNumBuses = getBusCount(isInput);
  	 requiredNumBuses = plugin->getBusCount(isInput);

after the “Sync-loop” nothing has changed

Ok, Sorry I figured out what is wrong on my end.
Short story is, Fabians wrapper code is fine, although it needs some safety checks.
My problems are related to specifics of my implementation.

My issue here is that I need to sync the busses two ways, and Fabians code assumes one way.

So in the constructor Fabian initializes the wrapper with the busproperties of the plug using getBusesPropertiesFromProcessor
This ensures that the wrapper gets the default busprops/layout from the plugin.

The sync in prepare to play, is if the wrapper wants to change the layout of the plugin.

Since I need to sync the wrapper to the plugin in a separate function from the constructor I’m kind of lost, as i can’t find a way to copy the busproperties and the layout from the plugin to the wrapper. I guess there is a good reason why i’m not allowed to call AudioProcessor::createBus.

So. I will re-think my approach and report back.

Yes my code above is just guidance. Definitely needs some safety checks.

All plug-in formats stipulate that the plug-in is mostly at the mercy of the host when it comes to bus layouts (except in reporting their default layout). So syncing in the other direction might be hard to achieve.