Multibus API - Advice on Stereo or Multichannel Synth

I’m sure this is probably simple but I can’t figure out the the correct use of canAddBus(), canRemoveBus(), isBusesLayoutSupported() etc etc.

Ideally, I want my synth to be either 1xStereo, or 8xStereo and that’s all. Of course all hosts will support my 1xStereo layout but not necessarily the 8xStereo. Logic seems to insist on 16xStereo as the next layout above 1xStereo. I’m fine with that as long as I have at least 8, I can just silence the unused ones (or otherwise only offer 1xStereo).

Thus far I’ve been playing with the MultiOutSynth example and so far I’ve got this:

...
    MultiOutSynth()
        : AudioProcessor (BusesProperties()
                          .withOutput ("Output #1",  AudioChannelSet::stereo(), true)
                          .withOutput ("Output #2",  AudioChannelSet::stereo(), false)
                          .withOutput ("Output #3",  AudioChannelSet::stereo(), false)
                          .withOutput ("Output #4",  AudioChannelSet::stereo(), false)
                          .withOutput ("Output #5",  AudioChannelSet::stereo(), false)
                          .withOutput ("Output #6",  AudioChannelSet::stereo(), false)
                          .withOutput ("Output #7",  AudioChannelSet::stereo(), false)
                          .withOutput ("Output #8",  AudioChannelSet::stereo(), false)
                          .withOutput ("Output #9",  AudioChannelSet::stereo(), false)
                          .withOutput ("Output #10", AudioChannelSet::stereo(), false)
                          .withOutput ("Output #11", AudioChannelSet::stereo(), false)
                          .withOutput ("Output #12", AudioChannelSet::stereo(), false)
                          .withOutput ("Output #13", AudioChannelSet::stereo(), false)
                          .withOutput ("Output #14", AudioChannelSet::stereo(), false)
                          .withOutput ("Output #15", AudioChannelSet::stereo(), false)
                          .withOutput ("Output #16", AudioChannelSet::stereo(), false))
    {
        // initialize other stuff (not related to buses)
        ...
    }

    ~MultiOutSynth() {}

    //==============================================================================
    bool canAddBus    (bool isInput) const override   { return (! isInput && getBusCount (false) < 16); }
    bool canRemoveBus (bool isInput) const override   { return (! isInput && getBusCount (false) > 1); }

    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        if (layouts.inputBuses.size() > 0)
            return false;

        int numStereoBuses = 0;
        
        for (auto channelSet : layouts.outputBuses)
        {
            if (channelSet != AudioChannelSet::stereo())
                return false;
            else
                ++numStereoBuses;
        }
        
        return (numStereoBuses == 1) || (numStereoBuses >= 8);
    }
...

I wonder if I’m going down the right path here?

Or for this specifc requirement do I need to override canApplyBusCountChange() ? Especially if I start with a deafult layout of < 16 (as I hit this case an inder-out-of-bounds assertion in Array::getReference() when the AU wrapper is calling syncAudioUnitWithChannelSet()).

I think there may be no way to achieve this as buses are always added and removed bus by bus. This means you can’t go from a single output bus to 16 output buses without also allowing 2 output buses, 3 output buses, etc.

What you could try to only allow the extra buses to be disabled in isBusesLayoutSupported (for example channelSet == AudioChannelSet::disabled()) if there are less than 8 buses. In any case you should probably always allow your extra buses to be disabled (even if there are 8 or more) as some DAWs don’t remove buses but rather disable them.

Ok, that should work. I could then do something like this:

  • Only one bus = Stereo out Bus 1
  • Less than 8 buses = Stereo bounce out of Bus 1, silence all others
  • 8 buses or more = Multi-out stereo buses 1-8, silence all others.

That almost seems to work as long as I initialise the audio processor BusesProperties() with 16x stereo as the default layout (then only use 1 or 8 of the buses depending on what is enabled). If I only initialise the BusesProperties() with 1x or 8s Stereo then I get the assertion when Logic uses the 16xSetero Multi layout. I would have thought canApplyBusCountChange() would deal with this increase in bus count but it seems the default layout has to be largest the plugin can accept.

That doesn’t seem right, so I must be missing something? This works in Logic but I worry that I’m misunderstanding something about the API that will cause it to fail if, for example, another DAW arbitrarily only allows 1xStereo and 32xStereo.

This is what I have now…

    MultiOutSynth()
        : AudioProcessor (BusesProperties()
                          .withOutput (getSynthBusName (0),   AudioChannelSet::stereo(), true)
                          .withOutput (getSynthBusName (1),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (2),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (3),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (4),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (5),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (6),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (7),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (8),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (9),   AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (10),  AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (11),  AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (12),  AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (13),  AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (14),  AudioChannelSet::stereo(), false)
                          .withOutput (getSynthBusName (15),  AudioChannelSet::stereo(), false))
    {
        // initialize other stuff (not related to buses)
        ...
    }

    ~MultiOutSynth() {}

    //==============================================================================
    
    static String getSynthBusName (int idx)
    {
        static const char* names[] = {
            "Output 1",
            "Output 2",
            "Output 3",
            "Output 4",
            "Output A",
            "Output B",
            "Output C",
            "Output D"
        };
        
        return isPositiveAndBelow (idx, numElementsInArray (names))
             ? String (names[idx])
             : String ("Unused #") + String (idx);
    }

    bool canAddBus    (bool isInput) const override   { return ! isInput; }
    bool canRemoveBus (bool isInput) const override   { return (! isInput && getBusCount (false) > 1); }

    bool canApplyBusCountChange (bool isInput, bool isAdding,
                                 AudioProcessor::BusProperties& outProperties) override
    {
        if (isInput)                              return false;
        if (getBusCount (false) == 0)             return false;
        if (  isAdding && ! canAddBus    (false)) return false;
        if (! isAdding && ! canRemoveBus (false)) return false;
        
        if (isAdding)
        {
            outProperties.busName = getSynthBusName (getBusCount (isInput) - 1);
            outProperties.defaultLayout = AudioChannelSet::stereo();
            outProperties.isActivatedByDefault = true;
        }
        
        return true;
    }
    
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        if (layouts.inputBuses.size() > 0)
            return false;
        
        for (int i = 0; i < layouts.outputBuses.size(); ++i)
        {
            auto channelSet = layouts.outputBuses.getUnchecked (i);
            
            // main bus must be stereo
            if (i == 0 && channelSet != AudioChannelSet::stereo())
                return false;
            else if (channelSet != AudioChannelSet::stereo()
                  && channelSet != AudioChannelSet::disabled())
                return false;
        }
        
        return true;
    }

The assertion I was getting seems to be caused by the bus count of the MusicDeviceBase object in the AU wrapper being updated in JuceAU::SetBusCount() after the AudioProcessor buses and the size of the layout arrays not being resized to the new bus count.

Moving this to before the main for() loop that adds or removes buses…
err = MusicDeviceBase::SetBusCount (scope, count);

And also adding this to resize the layout array, before the main for() loop…
(isInput ? currentInputLayout : currentOutputLayout).resize (requestedNumBus);

… seems to fix the issue.

Here’s patch:
au-addbuses.diff.txt (1.6 KB)

The only thing missing is to handle the error case after the comment “was there an error?” since we could be left with more or fewer buses than we should have to restore the bus state.

Thanks @martinrobinson-2! I’ve fixed this on the develop branch.

1 Like