Issues with sidechain channel configuration in VST3 / Cubase

Cubase seems to always use the default channel count for sidechains no matter what the input/output configuration is.

For example, in a plugin that supports {1->1, 2->2}, we want the sidechain to match the input channel count (i.e. mono for 1->1, stereo for 2->2). However, Cubase never gives us the opportunity to do this and instead checks a very sparse set of options in isBusesLayoutSupported.

If we specify {1, 1, 1} (in, out, sidechain) as the default during construction, it will try the following when loaded on a stereo track:

channel config: {2, 2, 1} - rejected because we want stereo sidechain
channel config: {1, 1, 1} - not ideal, track is stereo!

If we specify {2, 2, 2} (in, out, sidechain) as the default, it will try the following when loaded on a mono track:

channel config: {1, 1, 2} - rejected because we want mono sidechain
channel config: {2, 2, 2} - not ideal, track is mono!

These are both wrong in different ways. Unlike other hosts (e.g. Pro Tools) Cubase can support both stereo and mono sidechains, so we suspect this is a JUCE wrapper issue. Other plugins (like FabFilter) are able to support {1, 1, 1} and {2, 2, 2} in Cubase.

Using latest JUCE master, Cubase 10.5, VST 3.6.14 SDK

As far as I can tell, Cubase will always use only the speaker arrangement reported in getBusArrangement() for sidechains. I can’t see anything obviously amiss in the JUCE VST3 wrapper that could be causing this and it could be that the plug-ins which appear to support dynamic sidechain channels are using workarounds to achieve this. Would a viable workaround be to set the default sidechain channel layout to the maximum you need (in your case stereo) and then ignore the second sidechain channel in your processing code if the plug-in is on a mono track?

Thanks for taking a look.

Reporting the maximum is not really a great workaround for us because then the sidechain must also use that maximum number of channels. For something like a 5.1 surround plugin, I’m not sure which channels we’d want to ignore to get the correct behavior when the plugin is on a mono track.

Does JUCE support dynamic channel layouts in a way that could be leveraged into a fix?

I think something must be missing. These are sends from a mono track to a JUCE effect on a mono (1) and a stereo (3) track, and a non-JUCE effect on a mono (2) and a stereo (4) track. In mono-to-mono, Cubase shows a send panner for the JUCE effect, and none for the other. So defaulting to 6 channels, I think it would use a 5.1 panner, which is very inconvenient.
sends

Yeah, I think it would result in a 5.1 panner in the surround case as well — definitely not ideal.

From the VST SDK docs:

The Plug-in returns kResultFalse if wanted arrangements are not supported. If the Plug-in accepts these arrangements, it should modify its buses to match the new arrangements (asked by the host with IComponent::getInfo () or IAudioProcessor::getBusArrangement ()) and then return kResultTrue. If the Plug-in does not accept these arrangements, but can adapt its current arrangements (according to the wanted ones), it should modify its buses arrangements and return kResultFalse.

I think the last sentence implies that after rejection, the host will call getBusArrangement to find out which layouts do work, and it’s expected that any adjustment should have been done in between. So a fix could be to make is/checkBusesLayoutSupported take a non-const BusesLayout, and make setBusesLayoutWithoutEnabling send them a copy of request. If the check fails but the layouts have been changed, we set them before returning false. This is a workaround:

bool MyAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const
{
    auto in{ layouts.getChannelSet(true, 0) }, out{ layouts.getChannelSet(false, 0) };
    if (in == out && (in == AudioChannelSet::mono() || in == AudioChannelSet::stereo()))
    {
        auto sc{ layouts.getChannelSet(true, 1) };
        if (sc == in || sc == AudioChannelSet::disabled()) return true;
        auto newLayouts{ layouts };
        newLayouts.getChannelSet(true, 1) = in;
        const_cast<MyAudioProcessor*>(this)->setBusesLayoutWithoutEnabling(newLayouts);
    }
    return false;
}

This was different in older versions of Cubase -mono effects were checked with mono sidechains and so on. Now it assumes the default setting is deliberate (like, a stereo sidechain for any main layout).

btw, this is closely related to VST2 SpeakerArrangement / VST3 BusArrangements host calling sequence compliancy considerations

Interesting!

Does your fix above seem like a viable workaround or would it make more sense to address in the JUCE wrapper itself?

Well, as a workaround it works for now, but the const_cast is ugly and it breaks the semantics of the method. I don’t know how I’d solve it in the wrapper (or in AudioProcessor) though -I don’t like the fix I suggested either. It’s a breaking change, it’s still semantically obscure, and the comparison of layouts is unnecessarily expensive. Also you’d have to consider the other wrappers. It may be better to write another method and leave isBusesLayoutSupported as it is. I guess Fabian will have to take a look.

Just tested your fix in our plugins (while also checking for the wrapperType) and it seems to work just fine!

Your suggestion of having isBusesLayoutSupported take a non-const BusesLayout seems pretty workable to me, though maybe a little odd since it’d only be utilized in VST3.

I also posted on the Steinberg forum about this here: https://sdk.steinberg.net/viewtopic.php?f=4&t=775&p=2478#p2478

The response from YVan seems to agree with @kamedin’s suggestion that the plugin needs to make adjustments itself after rejection.

1 Like

That, and being a breaking change, and that doing something like if (layouts == newLayouts) seems unnecessary.

I think something like this may work. First adding to AudioProcessor:

enum class BusesLayoutSupport : int
{
    notSupported,
    supportedWithoutChanges,
    supportedWithChanges
};

virtual BusesLayoutSupport isBusesLayoutSupportedWithOrWithoutChanges (BusesLayout& layouts)
{
    return (BusesLayoutSupport) isBusesLayoutSupported (layouts);
}

bool checkBusesLayoutSizes (const BusesLayout& layouts) const
{
    return layouts.inputBuses.size() == inputBuses.size()
        && layouts.outputBuses.size() == outputBuses.size();
}

Then changing checkBusesLayoutSupported to:

bool AudioProcessor::checkBusesLayoutSupported (const BusesLayout& layouts) const
{
    if (checkBusesLayoutSizes (layouts))
        return isBusesLayoutSupported (layouts);

    return false;
}

and setBusesLayoutWithoutEnabling to:

bool AudioProcessor::setBusesLayoutWithoutEnabling (const BusesLayout& arr)
{
    // because checkBusesLayoutSupported already checked sizes, and the loops below
    // don't change them, maybe this could be here in place of the assert
    if (! checkBusesLayoutSizes (arr))
        return false;

    auto numIns  = getBusCount (true);
    auto numOuts = getBusCount (false);
    auto request = arr;
    auto current = getBusesLayout();

    for (int i = 0; i < numIns; ++i)
        if (request.getNumChannels (true, i) == 0)
            request.getChannelSet (true, i) = current.getChannelSet (true, i);

    for (int i = 0; i < numOuts; ++i)
        if (request.getNumChannels (false, i) == 0)
            request.getChannelSet (false, i) = current.getChannelSet (false, i);

    auto support = isBusesLayoutSupportedWithOrWithoutChanges (request);

    if (support == BusesLayoutSupport::notSupported)
        return false;

    for (int dir = 0; dir < 2; ++dir)
    {
        const bool isInput = (dir != 0);

        for (int i = 0; i < (isInput ? numIns : numOuts); ++i)
        {
            auto& bus = *getBus (isInput, i);
            auto& set = request.getChannelSet (isInput, i);

            if (! bus.isEnabled())
            {
                if (! set.isDisabled())
                    bus.lastLayout = set;

                set = AudioChannelSet::disabled();
            }
        }
    }

    return setBusesLayout (request)
        && support == BusesLayoutSupport::supportedWithoutChanges;
}

So isBusesLayoutSupported stays as it is, and there’s another callback to allow the supported-with-changes case. There are still the other calls to checkBusesLayoutSupported, in getNextBestLayout and Bus::isLayoutSupported -I don’t know if those should consider this case too.

if the Plug-in does not accept these arrangements, but can adapt its current arrangements (according to the wanted ones), it should modify its buses arrangements and return kResultFalse.

I find this a very vague requirement of the VST3 SDK. Certainly no host ever relied on this back when the VST3 plug-in wrapper was written. I find it vague as the Plug-In could adapt it’s current arrangement to a number of different supported layouts. Which one should the Plug-In choose?

For example, in the sidechain example above. Let’s say we have {1,1}->{1,1} or {2,2}->{2,2}, i.e. the sidechain number of channels should always match the number of channels on the main bus. If the Plug-In is currently in the {1,1}->{1,1} configuration and Cubase tries to change it to {2,1}->{2,1} (which I believe is the case above), then the Plug-In could either return {2,2}->{2,2} or {1,1}->{1,1}. The “Euclidean distance” :slight_smile: between the requested and adopted , if you will, is the same for both. So which one should the plug-in adapt? However, with the latter, Cubase will never “discover” that stereo is supported on the sidechain. So to satisfy Cubase, we should be returning the former. But this is not specified in the VST3 specification at all. The above example may be more straightforward, but I’m wondering if it’s ever possible for a more complex plug-in with multiple buses to adopt it’s layout to whatever the host seems to be probing.

So one way to make the quoted VST3 SDK spec to be less vague (and it would be great to have @ygrabit blessing here) would be the following:

On a setBusArrangements call the Plug-In shall:

  1. Try to adopt the arrangement as requested by the host. If it can do this it should return kResultTrue.
  2. If this is not possible, then the plug-in should note the buses which currently have a different channel layout to the one requested by the host. It should try to find an arrangement where the channel layouts of these buses are matched by what the host is requesting. The remaining buses, the plug-in may choose any possible layout even if they differ to the requested layout. Overall, the plug-in should try to find an arrangement which is as similar as possible to the requested layout. The plug-in should return kResultFalse in this case.
  3. If the above is not possible (i.e. the layouts of the buses, for which the current and requested layout differ, cannot be adopted to the requested) then the plug-ins should try to return a supported arrangement which is as close as possible to the requested one. he plug-in should return kResultFalse in this case.

For example, in the above case, the plug-in currently has the {1,1}->{1,1} arrangement when the host requests {2,1}->{2,1}. In this case, rule 2 comes into effect: comparing the current and requested arrangement, only the main buses are changing their layout so the plug-in should try to honour this change. It can change the sidechains though, which is what the plug-in will do.

I think JUCE’s VST3 wrapper could be modified to adhere to the above rules (without changing the AudioProcessor interface). We already do crazy things like calculating “distances” between arrangements in the AudioUnit wrapper (see AudioProcessor::getNextBestLayout here).

1 Like

Hi

Here some more explanation, I will update the doc of the VST3SDK.

/** Try to set (host => Plug-in) a wanted arrangement for inputs and outputs.

The host should always deliver the same number of input and output buses than the Plug-in
needs (see \ref IComponent::getBusCount). The Plug-in has 3 possibilities to react on this
setBusArrangements call:

1.The Plug-in accepts these arrangements, then it should modify, if needed, its buses to match these new arrangements (later on asked
by the host with IComponent::getBusInfo () or IAudioProcessor::getBusArrangement ()) and then should return kResultTrue.

2.The Plug-in does not accept or support these requested arrangements for all
inputs/outputs or just for some or only one bus, but the Plug-in can try to adapt its current
arrangements according to the requested ones (requested arrangements for kMain buses should be
handled with more priority than the ones for kAux buses), then it should modify its buses arrangements
and should return kResultFalse.

3.Same than the point 2 above the Plug-in does not support these requested arrangements

but the Plug-in cannot find corresponding arrangements, the Plug-in could keep its current arrangement

or fall back to a default arrangement by modifying its buses arrangements and should return kResultFalse.

Cheers

Yvan

1 Like

I think at least in the case of Cubase, only the main buses are “forced”, as audio tracks have a fixed number of channels through all their path. Sidechains are not adapted to what is sent to them. So it would suffice to find the closest main layout, then the closest aux layout for those mains.

1 Like

Thank you @ygrabit and @kamedin. That makes total sense. I’ll think of a way to patch this in the VST3 wrapper and report back here.

2 Likes

Thank you so much @fabian! Just wanted to check if you’ve had any time to look into this yet.

Hello @fr810 , has the sidechain channel config issue been fixed please ?

1 Like

I believe this issue still has not been addressed.

Just to clarify the workaround:

bool MyAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
    // check layouts
    if (wrapperType == wrapperType_VST3 && /* mains are ok but sidechain is not */)
    {
        auto newLayouts{ layouts };
        // modify newLayouts with the desired sidechain
        const_cast<MyAudioProcessor*> (this)->setBusesLayoutWithoutEnabling (newLayouts);
        return false;
    }
}