Any way to retrieve a bus from BusesLayout by its string reference?

Hi friends,

I’m working on a plugin that has the following requirements:

  • Needs audio input – bus can be any layout because the plugin needs one mono input and internally makes sure to read only one channel at a time

  • Always outputs a stereo signal

  • Also needs MIDI input – because of this, I need to implement a sidechain possibility for Logic users to be able to route MIDI & audio into the same VST

In my constructor, I’m initializing AudioProcessor with a makeBusProperties() function like so:

MyAudioProcessor::MyAudioProcessor(): AudioProcessor(makeBusProperties())

This function seems to work alright, but I’m trying to figure out how to write my isBusesLayoutSupported() function correctly.

Here’s my makeBusProperties() function:

AudioProcessor::BusesProperties MyAudioProcessor::makeBusProperties()
{
    if(host.isLogic() || host.isGarageBand())
        return BusesProperties().withInput ("Input",     AudioChannelSet::mono(),   true)
                                .withInput ("Sidechain", AudioChannelSet::mono(),   true)
                                .withOutput("Output",    AudioChannelSet::stereo(), true);
    
    return     BusesProperties().withInput ("Input",     AudioChannelSet::mono(),   true)
                                .withOutput("Output",    AudioChannelSet::stereo(), true);
};

and here’s the isBusesLayoutSupported() I have so far:

bool MyAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
    if   ( layouts.getMainInputChannelSet()  == juce::AudioChannelSet::disabled()
        || layouts.getMainOutputChannelSet() == juce::AudioChannelSet::disabled() )
        return false;
    
    if   (layouts.getMainOutputChannelSet()  != juce::AudioChannelSet::stereo())
        return false;
    
    if (host.isLogic() || host.isGarageBand() )
        // make sure that sidechain input is not disabled...   [1]
        // but how to retrieve the sidechain bus from layouts ?
    
    return true;
};

the line of code I’d love to be able to write at [1] is

if (layouts.getSidechainChannelSet() == juce::AudioChannelSet::disabled())
    return false;

but I’m a bit perplexed at how retrieving various buses other than the main ins/outs from a BusesLayout is supposed to work.

Per the documentation, you can call getChannelSet(isInput, busIndex) – but I don’t understand how you’re supposed to know what the integer busIndex is for the bus you want to examine, considering that buses are created/initialized using a string identifier and not an integer index…

Why can’t you just retrieve a bus using its string identifier…?

Is there something I’m missing here?

Good question. Currently, you can’t :sweat_smile:

If you see the implementations of getMainInputChannelSet and getMainOutputChannelSet, they return getChannelSet (true, 0) and getChannelSet (false, 0) respectively. The bus index is given by its order in BusesProperties construction, so the first input and output buses are the main ones. Your sidechain here would be getChannelSet (true, 1).

1 Like

Thank you! So the busIndex is determined by the order of construction, starting from 0? (this would be nice to include in the documentation…)

so would this work:

if (host.isLogic() || host.isGarageBand() )
    if (layouts.getChannelSet(true, 1) == juce::AudioChannelSet::disabled())
        return false;

Yup. You could also just

return layouts.getMainInputChannelSet() != juce::AudioChannelSet::disabled()
    && layouts.getMainOutputChannelSet() == juce::AudioChannelSet::stereo()
    && ( layouts.getChannelSet (true, 1) != juce::AudioChannelSet::disabled()
      || ! (host.isLogic() || host.isGarageBand()) );
1 Like

sweet, thanks!

one other tangential thing I’ve been thinking about related to channel buses – if my plugin takes only a mono input source, is the way I have my makeBusesLayout() set up the best way? Or should I consider making the main input a stereo channelSet?

I could give the user the ability to select input from channel 0 or 1 if the plugin is receiving a stereo input? Or just force the host to only send a mono signal…?

initially I made an “inputChannel” parameter in my GUI/APVTS, but I’m not sure that’s even necessary at all if I have the main input bus configured to stereo only

edit: MONO only whoops

I think your makeBusesLayout() doesn’t matter too much, because this is just a starting point. The host and your plugin will figure out the actual busesLayout by probing all variations on your isBusesLayoutSupported().

What might be interesting is the virtual bool canAddBus (bool isInput). Maybe you should return true here, so the host knows it can add a sidechain bus. But haven’t tried it myself. I know that how hosts handle sidechain is a bit inconsistent, including what channelLayouts() they allow (many have mono sidechains only etc.)

1 Like

Interesting… should canAddBus return true even if the sidechain bus was initialized like I had it? I guess the question is, is there a danger of the sidechain bus being removed after it was initialized?

I suppose I could have canAddBus return true only if the host is Logic or Garageband, because that’s the only situation in which my plugin actually needs a sidechain input…

and in my situation, does it seem like having an inputChannel parameter is useful, or pointless…? I want to provide flexibility for the user, within reason, but it also occurs to me that the input channels numbers may or may not correspond directly to the channel numbers in the host…?

Honestly all of this is very confusing to me :sweat_smile:

I don’t really know the situation with Logic and GarageBand, but the problem with just rejecting a disabled sidechain is that sidechains can be disabled manually. So I’d accept any sidechain, check its state in processBlock, and inform the user if they have to enable it.

You’d have to consider what’s more appropriate for your process. You could always take channel 0, or give a choice, or mix them and work on a mono sum, or whatever is logical. I wouldn’t force a mono input in any case -different channel sets for the main in and out can be a problem for some hosts.

1 Like

ok, this makes sense concerning the sidechain, thanks. If the host is logic/garageband, then in isBusesLayoutSupported maybe I should just check that a sidechain bus exists at all, whether or not it’s enabled?

this was sort of my reasoning behind having an inputChannel parameter for the user - I could accept a stereo input and give the user a choice between channel 0, channel 1, or a mix of both? ¯_(ツ)_/¯

Yup. If I were doing it, I think I’d mix to mono and inform the user about it in the manual or something. If they want to route a single channel they can insert a stock channel mixer before. I mean, just to unclutter the interface.

1 Like

cool :slightly_smiling_face:

and just to clarify, in the case of Logic/Garageband, if I have my main input bus as stereo & also a sidechain bus, would that mean that the sidechain is in channels 2 and 3 of the input buffer? (or just 2)

Yup. You can also use getBusBuffer.

The bus exists because you created it, you shouldn’t check it there. You check it in processBlock (or maybe prepareToPlay) for Logic/Garageband, and inform the user if it’s disabled (like, turn a red light on or something).

1 Like

great, I think I get it. Thank you for all your help!

So now, here’s what my makeBusProperties looks like:

AudioProcessor::BusesProperties MyAudioProcessor::makeBusProperties()
{
    if (host.isLogic() || host.isGarageBand())
        return BusesProperties().withInput ("Input",     AudioChannelSet::stereo(), true)
                                .withInput ("Sidechain", AudioChannelSet::mono(),   true)
                                .withOutput("Output",    AudioChannelSet::stereo(), true);
    
    return     BusesProperties().withInput ("Input",     AudioChannelSet::stereo(), true)
                                .withOutput("Output",    AudioChannelSet::stereo(), true);
};

and here’s my isbusesLayoutSupported:

bool MyAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
    if   ( layouts.getMainInputChannelSet()  == juce::AudioChannelSet::disabled()  // [1]
        || layouts.getMainOutputChannelSet() == juce::AudioChannelSet::disabled() )
        return false;
    
    if   ( layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()
        || layouts.getMainInputChannelSet()  != juce::AudioChannelSet::stereo() )
        return false;
    
    return true;
};

my last remaining concern is on line [1] – if the host is Logic/Garageband and I’m relying on sidechain for input, would the mainInputBus be disabled in that case? Or should it still always be enabled?

I wasn’t aware of the getBusBuffer method, I’ll look into it :slightly_smiling_face:

for the getBusBuffer, is this basically how it should be used:

processBlock(AudioBuffer<float>& buffer)
{
    int busIndex = (host.isLogic() || host.isGarageband()) ? 1 : 0;

    AudioBuffer<float> input = AudioProcessor::getBusBuffer (buffer, true, busIndex);
}

and after this call is made, is it correct that channels 0 & 1 of the input buffer would contain the desired input signal, regardless of whether the host was Logic/Garageband or not?

Yup. Or just

auto input = getBusBuffer (buffer, true, host.isLogic() || host.isGarageband());

I’ve read a bit about the Logic thing -it’s quite a workaround, inserting the effect as an instrument and sending audio through the sidechain. I think your isBusesLayoutSupported should be

return layouts.getMainOutputChannelSet() == juce::AudioChannelSet::stereo()
  && ( layouts.getMainInputChannelSet() != juce::AudioChannelSet::disabled()
     || ! (host.isLogic() || host.isGarageBand()) );

because in Logic, you don’t care if the main input is disabled. Then you check the sidechain in (I guess) prepareToPlay.

Anyway, I’d ask someone who has done it about the Logic workaround. I’m not sure about the need to use bus 1 for the sidechain in this case -instruments don’t have a main audio input anyway.

(edit) …or, you could just
return layouts.getMainOutputChannelSet() == juce::AudioChannelSet::stereo();
use bus 0 for all cases, and see if Logic takes it as a sidechain, just to check.

1 Like

OK, I think that makes sense, thank you :slightly_smiling_face:

@kamedin I’ve been working on implementing this, and I think I’m close!

I’ve created an enum in my plugin processor to determine the mode of input from the input bus:
enum ModulatorInputSource { left, right, mixToMono };

and here’s what my processBlock looks like:

void MyAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    if( (host.isLogic() || host.isGarageBand()) && (getBusesLayout().getChannelSet(true, 1) == AudioChannelSet::disabled()) )
    {
        // give user a warning that sidechain audio input must be enabled
        return;
    }
    
    AudioBuffer<float> inBus  = AudioProcessor::getBusBuffer(buffer, true, (host.isLogic() || host.isGarageBand()));
    AudioBuffer<float> output = AudioProcessor::getBusBuffer(buffer, false, 0);

    if (isSuspended())
    {
        output = inBus;
        return;
    }
    
    int inputChannelIndexInInputBuffer;
    
    switch(modulatorInput)
    {
        case ModulatorInputSource::left:
            inputChannelIndexInInputBuffer = 0;
            break;
        case ModulatorInputSource::right:
            inputChannelIndexInInputBuffer = 1;
            break;
        case ModulatorInputSource::mixToMono:
            const int channelToUse = 0;
            for(int chan = 0; chan < inBus.getNumChannels(); ++chan)
            {
                const float* orig = inBus.getReadPointer(channelToUse);
                const float* read = inBus.getReadPointer(chan);
                      float* dest = inBus.getWritePointer(channelToUse);
                
                for(int s = 0; s < inBus.getNumSamples(); ++s)
                    dest[s] = (orig[s] + read[s]) / 2.0f;
            }
            inputChannelIndexInInputBuffer = channelToUse;
            break;
    }  // TO DO: if host is Logic or Garageband, is this unnecessary? would the sidechain input always be channel 0 of inBus?
    
    AudioBuffer<float> input (inBus.getArrayOfWritePointers() + inputChannelIndexInInputBuffer, 1, inBus.getNumSamples());
    // input needs to be a MONO buffer containing the input modulator signal... so whether it needs to get channel 0 or 1 of the input bus, or sum the 2 channels to mono, either way this buffer needs to contain THAT data

// ... do stuff with the now mono buffer "input"

does this seem like a good way to go about this business of getting a certain channel from the input bus, vs summing the input bus to mono?

I’m trying this approach because my goal is to have a single buffer (input) to use, that will always be mono, no matter what “input mode” is being used…

thanks again for all your help :slightly_smiling_face:

and actually, now that I’m thinking about it, I guess I could further optimize the summing to mono process by using another proxy buffer…

case ModulatorInputSource::mixToMono:
        {
            const int channelToUse = 0;
            AudioBuffer<float> destProxy (inBus.getArrayOfWritePointers() + channelToUse, 1, inBus.getNumSamples());
            
            for(int chan = 0; chan < inBus.getNumChannels(); ++chan)
                destProxy.addFrom(0, 0, inBus, chan, 0, inBus.getNumSamples());
            
            destProxy.applyGain(1 / inBus.getNumChannels());
            inputChannelIndexInInputBuffer = channelToUse;
            break;
        }

¯_(ツ)_/¯