Multibus API

No. In fact the default constructor is now exactly the same as it was before we released the multibus stuff (pre 4.1). Before multibus, every AudioProcessor had one main in and one main out bus, but the default number of channels was zero on both buses. Either the DAW would change the number of channels (if your AudioProcessor was wrapped in a plug-in) - or - if you are using the AudioProcessor yourself - you would need to call setPlayConfigDetails. This hasn’t changed with the new multibus API if you are using the default constructor. This way old JUCE projects will still work.

For easy migrating look at the AudioProcessor::containsLayout convenience function. With this you can use the old channel configuration maps. Just make sure to remove it from the channel configuration field in the Projucer. I’ve put the most relevant functions - the constructor and isBusesLayoutSupported at the top.

#define LegacyChannelConfiguration {{2, 2}, {1, 1}, {1, 2}}

class LegacyPlugin : public AudioProcessor
{
public:
    LegacyPlugin()
        : AudioProcessor (LegacyChannelConfiguration),
          state (*this, nullptr)
    {
        state.createAndAddParameter ("gain", "Volume", "Volume",
                                     NormalisableRange<float> (0.0f, 1.0f),
                                     0.5f,
                                     [] (float value) { return String (value); },
                                     [] (const String& s) { return s.getFloatValue(); });
    }
    
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        return containsLayout (layouts, LegacyChannelConfiguration);
    }
    
    //==============================================================================
    const String getName() const override                         { return "LegacyPlugin"; }
    bool acceptsMidi() const override                             { return false; }
    bool producesMidi() const override                            { return false; }
    double getTailLengthSeconds() const override                  { return 0.0; }
    int getNumPrograms() override                                 { return 1; }
    int getCurrentProgram() override                              { return 0; }
    void setCurrentProgram (int index) override                   {}
    const String getProgramName (int index) override              { return String(); }
    void changeProgramName (int, const String&) override          {}
    bool supportsDoublePrecisionProcessing() const override       { return true; }
    
    //==============================================================================
    void prepareToPlay (double /*sampleRate*/, int /*samplesPerBlock*/) override {}
    void releaseResources() override {}
    
    
    template <typename FloatType>
    void process (AudioBuffer<FloatType>& buffer)
    {
        if (const float* gainValue = state.getRawParameterValue ("gain"))
        {
            FloatType gain = static_cast<FloatType> (*gainValue);
            
            AudioBuffer<FloatType> inputBuffer  = getBusBuffer (buffer, true, 0);
            AudioBuffer<FloatType> outputBuffer = getBusBuffer (buffer, false, 0);
 
            const int n = buffer.getNumSamples();
            const int outChannels = outputBuffer.getNumChannels();
            const int inChannels  = inputBuffer .getNumChannels();
            
            const FloatType* inLeft  = inputBuffer.getReadPointer(0);
            const FloatType* inRight = inputBuffer.getReadPointer(jmin (inChannels - 1, 1));
            
            FloatType* outLeft  = outputBuffer.getWritePointer(0);
            FloatType* outRight = outputBuffer.getWritePointer(jmin (outChannels - 1, 1));
            
            for (int i = 0; i < n; ++i)
            {
                FloatType left  = *inLeft++;
                FloatType right = inChannels > 1 ? *inRight++ : left;
                
                if (outChannels > 1)
                {
                    *outLeft++  = left * gain;
                    *outRight++ = right * gain;
                }
                else
                {
                    *outLeft++ = static_cast<FloatType> (0.5) * gain * (left + right);
                }
            }
        }
        else
            jassertfalse;
    }
    
    void processBlock (AudioBuffer<float>&  buffer, MidiBuffer&) override   { process (buffer); }
    void processBlock (AudioBuffer<double>& buffer, MidiBuffer&) override   { process (buffer); }
    
    //==============================================================================
    bool hasEditor() const override                   { return false; }
    AudioProcessorEditor* createEditor() override     { return nullptr; }
    
    //==============================================================================
    void getStateInformation (juce::MemoryBlock& destData) override
    {
        MemoryOutputStream stream (destData, true);
        state.state.writeToStream (stream);
    }
    
    void setStateInformation (const void* data, int sizeInBytes) override
    {
        state.state.readFromData (data, sizeInBytes);
    }
private:
    AudioProcessorValueTreeState state;
};

If you want to do it the idiomatic and recommended way, you would use the following code (see my inline comments):

class ShinyNewPlugin : public AudioProcessor
{
public:
    ShinyNewPlugin()
          // Here we add the buses. We don't want any sidechains or aux buses so we are just adding
          // a single input and a single output bus. The default layout of both is stereo. Remember
          // this is just the default layout. The DAW may want to change this. Restrict this with
          // the isBusesLayoutSupported callback.
        : AudioProcessor (BusesProperties().withInput  ("Input", AudioChannelSet::stereo(), true)
                                           .withOutput ("Output", AudioChannelSet::stereo(), true)),
          state (*this, nullptr)
    {
        state.createAndAddParameter ("gain", "Volume", "Volume",
                                     NormalisableRange<float> (0.0f, 1.0f),
                                     0.5f,
                                     [] (float value) { return String (value); },
                                     [] (const String& s) { return s.getFloatValue(); });
    }
    
    // Here we restrict the layouts of the busses. Use getMainInputChannels and
    // getMainOutputChannels to check if the layout is supported or not
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        // we only support stereo and mono
        if (layouts.getMainInputChannels() == 0 || layouts.getMainInputChannels() > 2)
            return false;
        
        if (layouts.getMainOutputChannels() == 0 || layouts.getMainOutputChannels() > 2)
            return false;
        
        // we don't allow the narrowing the number of channels
        if (layouts.getMainInputChannels() > layouts.getMainOutputChannels())
            return false;
        
        return true;
    }
    
    //==============================================================================
    const String getName() const override                         { return "ShinyNewPlugin"; }
    bool acceptsMidi() const override                             { return false; }
    bool producesMidi() const override                            { return false; }
    double getTailLengthSeconds() const override                  { return 0.0; }
    int getNumPrograms() override                                 { return 1; }
    int getCurrentProgram() override                              { return 0; }
    void setCurrentProgram (int index) override                   {}
    const String getProgramName (int index) override              { return String(); }
    void changeProgramName (int, const String&) override          {}
    bool supportsDoublePrecisionProcessing() const override       { return true; }
    
    //==============================================================================
    void prepareToPlay (double /*sampleRate*/, int /*samplesPerBlock*/) override {}
    void releaseResources() override {}
    
    
    template <typename FloatType>
    void process (AudioBuffer<FloatType>& buffer)
    {
        if (const float* gainValue = state.getRawParameterValue ("gain"))
        {
            FloatType gain = static_cast<FloatType> (*gainValue);
            
            AudioBuffer<FloatType> inputBuffer  = getBusBuffer (buffer, true, 0);
            AudioBuffer<FloatType> outputBuffer = getBusBuffer (buffer, false, 0);
 
            const int n = buffer.getNumSamples();
            const int outChannels = outputBuffer.getNumChannels();
            const int inChannels  = inputBuffer .getNumChannels();
            
            const FloatType* inLeft  = inputBuffer.getReadPointer(0);
            const FloatType* inRight = inputBuffer.getReadPointer(jmin (inChannels - 1, 1));
            
            FloatType* outLeft  = outputBuffer.getWritePointer(0);
            FloatType* outRight = outputBuffer.getWritePointer(jmin (outChannels - 1, 1));
            
            for (int i = 0; i < n; ++i)
            {
                FloatType left  = *inLeft++;
                FloatType right = inChannels > 1 ? *inRight++ : left;
                
                if (outChannels > 1)
                {
                    *outLeft++  = left * gain;
                    *outRight++ = right * gain;
                }
                else
                {
                    *outLeft++ = static_cast<FloatType> (0.5) * gain * (left + right);
                }
            }
        }
        else
            jassertfalse;
    }
    
    void processBlock (AudioBuffer<float>&  buffer, MidiBuffer&) override   { process (buffer); }
    void processBlock (AudioBuffer<double>& buffer, MidiBuffer&) override   { process (buffer); }
    
    //==============================================================================
    bool hasEditor() const override                   { return false; }
    AudioProcessorEditor* createEditor() override     { return nullptr; }
    
    //==============================================================================
    void getStateInformation (juce::MemoryBlock& destData) override
    {
        MemoryOutputStream stream (destData, true);
        state.state.writeToStream (stream);
    }
    
    void setStateInformation (const void* data, int sizeInBytes) override
    {
        state.state.readFromData (data, sizeInBytes);
    }
private:
    AudioProcessorValueTreeState state;
};

A named the later code “ShinyNewPlugin” but both examples will work.

1 Like

BTW: spotted another bug on develop so please pull before attempting the above examples.

Thanks for the information. I want to use the idiomatic way. I copied the code from your example and it validates fine, but I’m getting weird crashes in JuceAU::pullInputAudio in AULab.

This line makes thing terminate:
const unsigned int numInputBuses = GetScope (kAudioUnitScope_Input).GetNumberOfElements();

It doesn’t happen all the tim - feel like it depends on the order of some initialization that appears to be off sometimes in my case. Simple examples appear to work though… I’ll spend some more time trying to figure it out.

See the bug that chkn reported above. This seems to be related. I’ll have a look…

Update: I can reproduce this now. I’ll push a fix for this as soon as possible.

1 Like

I’ll let you know once I can replicate it in one of the example plugins. And yep seems to be the same thing… my crash log is just less informative for some reason.

Ok now I’m getting it every time with just your code above (idiomatic example). I’m using the latest AULab as the test host, am building a 64bit debug version. I just created a new plugin in ProJucer and copy/pasted your code. I added a AudioProcessor* JUCE_CALLTYPE createPluginFilter() method and I had to set the base OSX SDK to 10.10 because that’s what I’m using.

The terminate happens as soon as the plugin is instanciated in AULab. Auval validates successfully.

The output is “libc++abi.dylib: terminating with uncaught exception of type int” and I wish there was a way to copy/paste a backtrace in Xcode…

com.apple.audio.IOThread.client (12)#0	0x00007fff8db18286 in __pthread_kill ()
#1	0x00007fff886329f9 in pthread_kill ()
#2	0x00007fff808429a3 in abort ()
#3	0x00007fff89014a21 in abort_message ()
#4	0x00007fff8903c9d1 in default_terminate_handler() ()
#5	0x00007fff8030a7eb in _objc_terminate() ()
#6	0x00007fff8903a0a1 in std::__terminate(void (*)()) ()
#7	0x00007fff8903a113 in std::terminate() ()
#8	0x000000010d1af406 in __clang_call_terminate ()
#9	0x000000010d1c1582 in JuceAU::pullInputAudio(unsigned int&, AudioTimeStamp const&, unsigned int) at /Users/adrianpf/Desktop/BLhePLug1145/Builds/MacOSX/../../../../../../JUCE/modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm:1519
#10	0x000000010d1b23fb in JuceAU::Render(unsigned int&, AudioTimeStamp const&, unsigned int) at /Users/adrianpf/Desktop/BLhePLug1145/Builds/MacOSX/../../../../../../JUCE/modules/juce_audio_plugin_client/AU/juce_AU_Wrapper.mm:1072
#11	0x000000010d1b236d in AUBase::RenderBus(unsigned int&, AudioTimeStamp const&, unsigned int, unsigned int) at /Users/adrianpf/Desktop/BLhePLug1145/Builds/MacOSX/../../../../../../JUCE/modules/juce_audio_plugin_client/AU/CoreAudioUtilityClasses/AUBase.h:332
#12	0x000000010d1dc1aa in AUBase::DoRenderBus(unsigned int&, AudioTimeStamp const&, unsigned int, AUOutputElement*, unsigned int, AudioBufferList&) at /Users/adrianpf/Desktop/BLhePLug1145/Builds/MacOSX/../../../../../../JUCE/modules/juce_audio_plugin_client/AU/CoreAudioUtilityClasses/AUBase.h:816
#13	0x000000010d1cbc58 in AUBase::DoRender(unsigned int&, AudioTimeStamp const&, unsigned int, unsigned int, AudioBufferList&) at /Users/adrianpf/Desktop/BLhePLug1145/Builds/MacOSX/../../../../../../JUCE/modules/juce_audio_plugin_client/AU/CoreAudioUtilityClasses/AUBase.cpp:1503
#14	0x000000010d1c7317 in CMgr_AudioUnitBaseRender(AUBase*, unsigned int*, AudioTimeStamp const*, unsigned int, unsigned int, AudioBufferList*) at /Users/adrianpf/Desktop/BLhePLug1145/Builds/MacOSX/../../../../../../JUCE/modules/juce_audio_plugin_client/AU/CoreAudioUtilityClasses/AUDispatch.cpp:433
#15	0x000000010c00a720 in AUInputElement::PullInput(unsigned int&, AudioTimeStamp const&, unsigned int, unsigned int) ()
#16	0x000000010c056c2e in AUMatrixMixer::Render(unsigned int&, AudioTimeStamp const&, unsigned int) ()

Yes. I can also reproduce this. Hang on…

1 Like

OK I’ve pushed a fix for this on develop. It was related to the fix I did for chkn above.

@pflugshaupt I can confirm that this fixed your bug
@chkn As I can’t seem to reproduce your issue, this commit may break the fix for your white noise bug. Please test!

1 Like

Thanks for the immediate action. The last change fixes the issue I saw. I’ll be sure to test more the next days.

Need advise. Planing to release a plug-in in two weeks and I’m a bit confused about the multibus subject. Temporarily my plug-in is on juce 4.2.3 and the multibus is implemented according the “deprecated multibus guide”. Is it ok to release “as is” or should I better update to 4.2.4 with the new multibus classes?

I’m in the same situation, the problem is, if you need quick fixes now, need the latest juce, the new api will break. Then you fix the changes, and have maybe new issues.
This is not a good situation at all.
I decided to wait and use the new new api, but its your choice.

Hopefully newer juce releases will not break the api anymore.

1 Like

I guess you should either use the older api or wait a few more weeks until more bugs have been fixed. Personally I wouldn’t release a plugin with the new stuff right now… not even a non-multibus plugin. I am seeing some weirdness with the new api, but I’m not sure what’s going on. I’m hoping many developers are adapting to the newest multibus api so we can get something that’s really stable.

Right now I am seeing some new problems with AudioProcessGraph and AudioProcessors using many discrete channels. I’ll post here again once I figure out where the issue might be.

Turns out it was my own bug (unsurprisingly). I am also having some unrelated problems using an AudioProcessorGraph inside a plugin I really could use some help with: AudioProcessorGraph crashes after removing node

@chkn @chip: I can understand your frustration having to adapt your plug-in to a new API two times. Obviously, I’d love people to use the new API as it is - IMHO - much more stable than the old one.

Having mostly written the new API, I obviously think that the transition from the old API to the new one is really not that hard. But I’m biased of course.

In any case, to make it easier for anyone using the old API to migrate to the new one, I’d like to offer free 1 on 1 help via Skype/Hangouts. Just drop me a PM and we schedule some time.

5 Likes

Fabian, I think rewriting the API was a good choice and I very much like what you came up with. It’s much easier to understand that the one before and so far it’s working nicely for me.

8 Likes

Any estimate date for the promotion of this new API from develop to master?

Projucer tip needing an update:

1 Like

I’m currently trying to manage the update to the latest release of Juce (namely, 4.3.0). Many things are not clear, the documentation is not clear about what is required to make it work, or what to do if we want to achieve some (a bit more than simplest) goals. Here is an issue I’m encountering:
I’ve got a plug-in with 10 inputs and 4 outputs. i’ve declared it using the following constructor:

AudioProcessor(BusesProperties().withInput("In", AudioChannelSet::discreteChannels(10), true)
                                .withOutput("Out", AudioChannelSet::ambisonic(), true))

The first issue I encounter with REAPER is the additional channels visible in the plug-in I/O pane: I’ve got 8in/8out whereas I previously had 10in/4out:

The second issue I encounter is the following code from processBlock() erasing some of my first output channels:

    for (int i = 4 ; i < std::max(getTotalNumInputChannels(), getTotalNumOutputChannels()); ++i)
    {
        buffer.clear (i, 0, buffer.getNumSamples());
    }

Indeed, some channel buffers >=4 repeat some channel buffers <4

Also, some of my 10 input channels pointers repeat the first channels’ pointers.

  1. Did I miss a virtual function’s override? If so, how am I supposed to implement these? (some code is appreciated as I’ve found no (clear) documentation or example anywhere)

  2. In case of issue for a buffer (not connected, etc.) it would be appreciated that the channel buffers be initialized with a nullptr (so that behaviour of the code could be adapted in a simple manner) or an external buffer which does not interfere with the valid channels (like for now, it duplicates inputs or overwrites output) -> “fail gracefully”

With the new API, where are we supposed to respond to the channel config changing? I used to allocate the new buffers etc in prepareToPlay however that seems to not always get called when setting a new config now. Is there a better place?

Edit: This is specifically in the VST3 Wrapper

Perhaps one of these AudioProcessor callbacks is what you are looking for?

/** This method is called when the total number of input or output channels is changed. */
virtual void numChannelsChanged();

/** This method is called when the number of buses is changed. */
virtual void numBusesChanged();

/** This method is called when the layout of the audio processor changes. */
virtual void processorLayoutsChanged();
3 Likes