Can someone help me set up some sidechain basics pls?

i always avoided making big sidechain-projects until now because there is a strange issue there that i hope to be able to describe in this post. i hope someone can help me figuring this out.

my ultimate goal is to have a programming environment in which i can debug a sidechainable plugin in standalone, because when debugging a plugin you don’t always care about wether or not it makes sense that there is no 2nd input right now.

ok so let’s say you have some processor base class with a constructor that says “hey, there can be sidechain”:

ProcessorBackEnd::ProcessorBackEnd() :
        juce::AudioProcessor(BusesProperties()
            .withInput("Input", ChannelSet::stereo(), true)
            .withOutput("Output", ChannelSet::stereo(), true)
#if PPDHasSidechain
            .withInput("Sidechain", ChannelSet::stereo(), false)
#endif
        ) ...

when building it as a VST3 the DAW correctly shows you have a sidechain bus:
image
when building standalone however you get this:


I assume this is not a bug, but just JUCE telling you like “hey, standalone apps can’t have sidechain” which kinda makes sense ofc, but it’s annoying for debugging. so I was told to try this:

  1. get rid of the sidechain input in the constructor of the processor.
  2. instead implement the canAddBus method of audioProcessor to return isInput so show the DAW that it can dynamically add a bus if it wants to.
  3. implement the isBusesLayoutSupported() method in a way that checks if the sc bus exists.

I did that:

ProcessorBackEnd::ProcessorBackEnd() :
        juce::AudioProcessor(BusesProperties()
            .withInput("Input", ChannelSet::stereo(), true)
            .withOutput("Output", ChannelSet::stereo(), true)
//#if PPDHasSidechain
//            .withInput("Sidechain", ChannelSet::stereo(), false)
//#endif
        )

and

bool ProcessorBackEnd::isBusesLayoutSupported(const BusesLayout& layouts) const
    {
        const auto mono = ChannelSet::mono();
        const auto stereo = ChannelSet::stereo();

        const auto mainSetIn = layouts.getMainInputChannelSet();
        const auto mainSetOut = layouts.getMainOutputChannelSet();

#if PPDHasSidechain
        const auto scSetIn = layouts.getChannelSet(true, 1);
        if(!scSetIn.isDisabled())
            if (scSetIn != mono && scSetIn != stereo)
                return false;
#endif
        if (mainSetOut != mono && mainSetOut != stereo)
            return false;

        return mainSetIn == mainSetOut;
    }

and

bool ProcessorBackEnd::canAddBus(bool isInput) const
{
    return PPDHasSidechain ? isInput : false;
}

so with these changes I can run the plugin in standalone successfully and even as a plugin, no problems, except that…
image
now the plugin has no sidechain input anymore according to the DAW.

now i tried these changes to the constructor:

#if PPDHasSidechain && !JucePlugin_Build_Standalone
            .withInput("Sidechain", ChannelSet::stereo(), true)
#endif

but JucePlugin_Build_Standalone is also true if you select the VST3 build. would be cool if juce had a preprocessordefine for the fact wether or not i’m actually building a vst3 file and not wether or not standalone builds might exist at all. is that a thing? it would not be the best solution ever because it would mean i have to work with that PPD all the way through my whole codebase and I already have lots of them, but it would be a possible solution at all.

I was told to check the state of the arguments of that jassert, when trying to set the sidechain input in the constructor. 0 inputs, 2 outputs. So I added this to the busesLayout verification code:

if (mainSetIn.isDisabled())
            return true;

which actually made it work in standalone and in the DAW, but now when I add a sidechain input I get an assert in the JUCE VST3 Wrapper:


I find it rather unlikely to believe that bitwig doesn’t know how to set sidechain inputs correctly, so there needs to be another reason for this to appear.

(I was just told I should make a custom standalone wrapper for this. I have not had the time to try that yet and it also seems a little overkill for a solution to a problem that should be simple, but if I get to do that I’ll update this post accordingly. Until then maybe some alternative ideas would be nice.)


so this is the current state of things. I’d be happy if someone could just tell me what exactly I’m missing here to make this work. I tried this multiple times through-out the last year and always gave up at some point because i ran out of ideas and stuff. this really shouldn’t be so complicated. but considering it apparently is still complicated i’d like to know what i have to do here or what i could try to do.

The preprocessor defines don’t work, because the code is in a shared static library, so you cannot rely on that.
But you can check at runtime for juce::JUCEApplicationBase::isStandaloneApp() (static method).
So how bout this:

juce::AudioProcessor::BusesProperties createBusesProperties()
{
    auto properties = juce::AudioProcessor::BusesProperties()
            .withInput("Input", ChannelSet::stereo(), true)
            .withOutput("Output", ChannelSet::stereo(), true);

    if (! juce::JUCEApplicationBase::isStandaloneApp())
        properties = properties.withInput("Sidechain", ChannelSet::stereo(), false);

    return properties;
}

ProcessorBackEnd::ProcessorBackEnd() :
        juce::AudioProcessor (createBusesProperties())
{
    // ...
}

That should at least help for the constructor.

1 Like

If you want to (I mean really really want) to make this a compile time selection, you can also add a definition for the above mentioned createBusesProperties function in a header file and have two separate cpp files that implement the method. You can add the one cpp (without sidechain) to your standalone app target and the other cpp to your AU and VST target. Not sure though how that reacts with Projucer.

yeah i’ll definitely consider this runtime solution if nothing else works, but i gotta say it looks a bit dirty, because the information wether it’s standalone or plugin exists from compile time and if i use a runtime solution for that now i’m pretty sure that will just yield in me having a standalone-branch in processBlock later. everytime a block is being processed it will keep on asking if it’s still a standalone app, as if that could ever change. but well… i’m glad there are solutions to this at all. this is terrible

It doesn’t, actually. The compiler knows whether a standalone target is being built at all, but the plugin’s shared code is used by any format. Only the standalone wrapper code knows it’s a standalone at compile time.

i see. that’s probably the reason why people adviced me to write my own standalone wrapper. and yeah… i currently try to do that. right now i’m sitting here in front of this >1000lines of cryptic lowlevel looking code not only trying to find out what exactly there causes this whole problem, but also asking myself what exactly i am supposed to do with that to integrate it into the project, as i have never written code outside of the “bounds” of the plugin and i have a strong feeling that i’m not supposed to fix a plugin wrapper only to get sidechain rolling. also wondering why i have to do that. juce is so old already. as if never ever anyone before made a sidechain plugin with it and wanted to debug standalone. can’t someone just give me instructions? maybe it’s because it’s sunday. a lot of people consider that a timeout. i just wanted to try my new plugin idea, idk…

I don’t think it is necessary to write your own standalone wrapper.

This createBusesProperties() is called exactly one time per lifetime. I wouldn’t say that’s an overhead.

In processBlock you will just use the information here:

auto sideChain = getBusBuffer (getBusCount (true) > 1 ? 1 : 0);

now you can use the AudioBuffer<float> sideChain without any further branching.

ok first of all thanks for helping to you and everyone else here!

Now I changed my constructor to this:

juce::AudioProcessor::BusesProperties ProcessorBackEnd::makeBusesProperties()
    {
        BusesProperties bp;
        bp.addBus(true, "Input", ChannelSet::stereo(), true);
        bp.addBus(false, "Output", ChannelSet::stereo(), true);
#if PPDHasSidechain
        if(!juce::JUCEApplicationBase::isStandaloneApp())
            bp.addBus(true, "Sidechain", ChannelSet::stereo(), true);
#endif
        return bp;
    }

    ProcessorBackEnd::ProcessorBackEnd() :
        juce::AudioProcessor(makeBusesProperties())

as you suggested i’m now checking in runtime if this is a standalone app. i still think it’s kinda silly considering that this information already exists before even building the application, but hey, still better than dealing with the plugin wrapper. that file is scary. so now my channel-layout is configurated like that:

bool ProcessorBackEnd::isBusesLayoutSupported(const BusesLayout& layouts) const
    {
        const auto mono = ChannelSet::mono();
        const auto stereo = ChannelSet::stereo();
        
        const auto mainIn = layouts.getMainInputChannelSet();
        const auto mainOut = layouts.getMainOutputChannelSet();

#if PPDHasSidechain
        const auto scIn = layouts.getChannelSet(true, 1);
        if(!scIn.isDisabled())
            if (scIn != mono && scIn != stereo)
                return false;
#endif
        if (mainOut != mono && mainOut != stereo)
            return false;

        auto mainSame = mainIn == mainOut;

        return mainSame;
    }

not quite sure yet if everything is alright here, but looks good i think. there has to be some mistake left somewhere, though, because now when i run the plugin in standalone and try to select an audio input in the plugin wrapper’s options menu i get this:


That’s weird, but there’s more. when i build this as a vst3 and use it on a stereo-track, i can only hear the left channel now. once i try to add a sidechain input the plugin crashes.

i can only say: this is one of the most terrible aspects about JUCE at this point and with such a sidechain-workflow we should all not be surprised that there are not more sidechain plugins out there. help me figuring it out and I can at least make a video tutorial about it so everyone else doesn’t have to struggle through this!

The check you’re doing might work, but the recommended way to check the wrapper type is with the wrapperType member of the AudioProcessor:

if (wrapperType == wrapperType_Standalone)
{
    ...
}

hm… but i’m trying to construct the audioProcessor with that information, as you can see

You can make this a compile-time check if you rebuild the code for each plugin format. If you want to use the recommended build system of building the plugin code only once, and then reusing that in each wrapper, then this information does not exist at compile time.

The juce::JUCEApplicationBase::isStandaloneApp() will definitely work.

2 Likes

well that’s good to know. it also seems to work. i checked if that branch in my constructor happens and it doesn’t, so that’s expected.

but still i don’t get why it doesn’t accept my inputs anymore. i mean now that i have this code that clearly skips the sidechain stuff if i’m building standalone that should make it act normally, right? i mean. obviously not. there seems to be more to it. but what could it be

Maybe it got lost in translation, but your screenshot (might have been in discord) showed the name for the side chain channelset as “discrete”, which is different from mono or stereo.

Can you add that just for sanity?

atm i’m just debugging the “easier” case where i just try to build standalone. as you know in that case i’m skipping making a sidechain bus and i’m also skipping the sidechain stuff in the buses validation method.


i have also not written sidechain-related code into processBlock yet, to keep things simple.

so the current goal is just to get the normal mic/line input back to work when sidechaining is not a thing in my sidechain project

Ok so there’s something weird I found.

The method isBusesLayoutSupported is being called twice at the beginning in a standalone build and I just debugged what mainIn and mainOut are there then.

first call:
Discrete #0 :: Stereo

2nd call:
Stereo :: Stereo

so as you can see whatever this discrete#0 thing is it makes the method return false first cause mainIn != mainOut then, but the 2nd call returns true, because i want both in and out to have the same channel layout, which is stereo in this case

  1. what does it mean when a juce::AudioChannelSet is #discrete?
    “Non-typed individual channels are indexed upwards from this value.” according to the API, but that doesn’t explain why my audio input is found to be that.

  2. why is the isBusesLayoutSupported() method being called twice at the start of execution and why does it yield 2 different results then? how can this information be interpreted or debugged? it’s kinda hard to tell. especially considering that i don’t really know what exactly happens before and after that method. i mean i could look at the callstack but that goes super lowlevel quickly. i just can’t believe one is supposed to go there just to get started with sidechaining.

  3. are there any plans by the JUCE team to extend the standalone wrapper to have a 2nd pseudo input for situations like this? that would be the cleanest fix here imo, cause it’s not really the AudioProcessor’s job to deal with differences between standalone- and plugin build, but its job is to process audio.

  4. Absorb/Processor.cpp at master · Mrugalla/Absorb · GitHub
    this is the processor.cpp of the project that fails to work. i usually don’t like to tell other people to look over my entire file with me as i am not a fan of wasting everyone’s time, but i have looked at this the whole day yesterday already and i just can’t see how this is supposed to not work. at least the standalone build should act like a normal non-sidechain plugin right now with all these if-statements about the wrapper. so if anyone sees anything sus in there pls tell me

The idea behind the bus types is, that the type information for each channel is checked when you connect them. If you plug a stereo signal into an M/S input, the developer should know about it and prepare sensible measures, i.e. silently convert or present the user a switch.
Other example would be surround, where sometimes the order of individual channels are different, or the intended placement of a channel differs.
To allow channelsets where the types are not known to the host developers, there is the possibility to use discrete channels, where you don’t know the semantics of the channel, only the number in the pack.

You are looking at it the wrong way round. The host is not telling you a layout, rather the opposite:
the host negotiates a usable layout with your plugin. If you read the name it becomes obvious:
Host: “I want you to process stereo into 5.1: is [this] buses layout supported? Can you do that?”
Your plugin: “Nope, I am not designed for that: return false”
Host: “Ok, how about processing a stereo signal into a stereo signal?”
Your plugin: “Yep, that’s fine: return true”
Host: “Oh hey, how about adding a side chain? I’ll give you another layout”
Your plugin: “Yep, that’s fine too: return true”
… and so on …

You see, this method could be called a number of times, depending what possible uses the host wants to probe. But the layout is not what is ultimately chosen. That is done in AudioProcessor.setBusesLayout(). The result is then available in prepareToPlay and in processBlock with getBus() etc.

I need to leave 3. and 4. for somebody else who has more practical experience with side chains.

yeah that makes sense. thx!

alright everyone. i’m back and i’d like to discuss this topic again. i’d especially appreciate if someone from the JUCE team itself could shed some light on this. after all the goal should actually be reachable quite easily. it’s just about having a plugin project that builds successfully both in standalone and VST3 and it doesn’t need to have proper sidechain functionality in standalone, as standalone builds are mostly used to debug GUI stuff. i think we should solve this together because setting up this stuff is so complicated that i can barely believe i’m the only one who fails at doing so. if we manage to make it work together i can make a video where i explain the steps and we can finally call it a day.

what happened in the meantime?
i had that issue where my audio inputs didn’t work anymore. i still don’t know what caused it, but formatting my computer helped and now i have a clean system again where all occuring issues are actually valid issues.

here’s the link to the repository of the project so you can see the curent state:

the relevant bit here is ofc Source/Processor.h and Source/Processor.cpp.

atm when i run the vst3 build in a daw (bitwig 4.3) the plugin crashes shortly after (but not exactly when) i add a sidechain input as seen in this screenshot:

running a debugger while it happens exposes that the issue stems from this part of the VST3 Wrapper:

it says that the host is misbehaving, but i’m not quite buying that, because if that was true all JUCE plugins with sidechain would crash in bitwig. someone would have noticed that before me already. i actually think that my sidechain implementation still sucks. i’ll install some other host later to check for that ofc, but for now i’d like to assume that i made a mistake, alright? so the question is what was the mistake? in the following i’ll copy some relevant parts of the processor code into this comment to make it easier to observe this problem from here.

juce::AudioProcessor::BusesProperties ProcessorBackEnd::makeBusesProperties()
    {
        BusesProperties bp;
        bp.addBus(true, "Input", ChannelSet::stereo(), true);
        bp.addBus(false, "Output", ChannelSet::stereo(), true);
#if PPDHasSidechain
        if (!juce::JUCEApplicationBase::isStandaloneApp())
        {
            bp.addBus(true, "Sidechain", ChannelSet::stereo(), true);
        }
#endif
        return bp;
    }

    ProcessorBackEnd::ProcessorBackEnd() :
        juce::AudioProcessor(makeBusesProperties()),
        ...

so we got the constructor of the processor, which only adds the feature of sidechaining to its busesProperties if it’s a standalone build plus my pre-processor-definition for wether the plugin is supposed to have sidechaining at all, PPDHasSidechain. on first glance this seems to work. standalone builds works fine and sidechain ones do so too, unless you add the sidechain input.

bool ProcessorBackEnd::canAddBus(bool isInput) const
    {
        return PPDHasSidechain ? isInput : false;
    }

this was the method earlier suggested in this post, which always returns false by default, but if a plugin has sidechain then it is capable of returning true if the bus is input-only. that seems to make sense to me. i should add the wrapperType-check in here too, but if that was a problem it would be exposed in the standalone build and not in the VST3 one.

bool ProcessorBackEnd::isBusesLayoutSupported(const BusesLayout& layouts) const
    {
        const auto mono = ChannelSet::mono();
        const auto stereo = ChannelSet::stereo();
        
        const auto mainIn = layouts.getMainInputChannelSet();
        const auto mainOut = layouts.getMainOutputChannelSet();

        if (mainIn != mainOut)
            return false;

        if (mainOut != stereo && mainOut != mono)
            return false;

#if PPDHasSidechain
        if (wrapperType != wrapperType_Standalone)
        {
            const auto scIn = layouts.getChannelSet(true, 1);
            if (!scIn.isDisabled())
                if (scIn != stereo && scIn != mono)
                    return false;
        }
#endif
        return true;
    }

here i basically try to (and hopefully do) say: in- and out-channel count must be the same and it should be mono or stereo only. and if sidechaining exists then i also want that to be either mono or stereo.


so, this is the state of things. as you can see there’s not much left to try here. that’s why i’m out of ideas.