Bypassing a hosted plugin

OK so the more I think about this the more I think you’re onto something here. I was missing the fact that the bypass feature is needed for cases of an AudioProcessor that are neither a plugin or a host.

IME there are two ways in which most hosts commonly implement a bypass. I normally like to refer to these as bypass and deactivate.

When bypassing, resources are not released, delay compensation continues, etc. In the case of a deactivate (what Logic calls a bypass :roll_eyes:) resources can be released and often delay compensation is dropped. Some hosts only implement one of these, others both. In JUCE it makes sense we make it easy to do either/or right?

So how about we have it that setBypassed/isBypassed/bypassChanged in the AudioProcessor is primarily designed for bypassing and processBlockBypassed for deactivation?

For a plugin the deactivation isn’t something that you really need to consider, the whole point of deactivation is that nothing in your plugin will be called for processing in order to reduce resources, so we can continue to have the wrappers switch between processBlock and processBlockBypassed for backwards compatibility.

For hosts, they can call setBypassed which will indicate to the plugin that the host wants the plugin to do the bypassing (some logic is required in each of the formats to deal with the edge case where the plugin doesn’t implement this feature, and so the processBlock call is forwarded onto processBlockBypassed).

However the host could also directly call processBlockBypassed, in the knowledge that plugin resources can be safely released, essentially acting as a deactive mechanism. The deactive mechanism is what exists now so I don’t think it would be breaking anything. The only change a host might experience is that processBlock could be forwarded onto processBlockBypassed if the plugin tells the host to bypass but for some reason the plugin doesn’t implement the bypass itself!?

For anything that is not a host or plugin, a similar thing could be applied. To apply a “bypass” an AudioProcessor would have to check it’s isBypassed() state inside of processBlock and act accordingly, but anyone calling into the AudioProcessor could skip that and directly call processBlockBypassed which conveniently has a default implementation that is essentially a deactivate.

So in summary…

  1. Add setBypassed() / isBypassed() to the AudioProcessor class.

    • Plugin hosts use this to tell the plugin to do the bypassing but continue calling processBlock
    • Each fomat deals with the edge case in which the plugin doesn’t implement a bypass and therefore the processBlock call is forwarded to processBlockBypassed
    • Plugin hosts can continue to call processBlockBypassed to achieve a deactivate as they have done in the past
    • Plugins use this to bypass themselves and also to indicate to the host that the plugin has changed the bypass state where possible (see point 2)
    • The wrappers conveniently uses isBypassed() to forward the processing call to the correct processBlock() / processBlockBypassed()
    • AudioProcessor's (except plugins) should use isBypassed() to implement a bypass feature in the future, not doing so would make calls to setBypassed() ineffective
    • Any host of an AudioProcessor (not a plugin host) can continue to call processBlockBypassed() as presumably it has done in the past to achieve a deactive.
  2. Add a bypassChanged callback to AudioProcessorListener

    • The plugin and wrapper can use this to keep in sync with each other
    • The format and host can use this to keep in sync with each other

Thoughts?

I’m sorry I was going to do this work last night and make a PR but I left my laptop charger at work and the laptop died.

One point to add to this is that if we go with the above suggestion I still think it will be useful to have the ability to mark a parameter as the bypass parameter for a plugin, particularly because then it can prevent the default bypass parameter being added in the VST3 and AAX wrappers. Synchronising the bypass and the setBypassed / isBypassed calls should be easy enough with the listener methods.

Let me try to “draw” some schemes to see if I have understood your proposal:

For the hosting side:

+------+
| Host |
+------+
    |
    | 
    V
+------------------------+
| "host" AudioProcessor, |
| which does the hosting |
+------------------------+
    |
    | 
    V
+--------------------------------+
| Hosting format wrapper,        |
| e.g. juce_VST3PluginFormat.cpp |
+--------------------------------+
    |
    | 
    V
+--------------------------------+
| Plug-in (potentially non-JUCE) |
+--------------------------------+

Then,

IF the format supports some notion of bypass AND the hosted plug-in implements it:

  • The Hosting AudioProcessor “forwards” setBypassed() / isBypassed() to the native bypass mechanism advertised by the hosted plug-in.
    For processing, only processBlock() is ever called, whatever the bypass state is, letting the hosted plug-in handle the bypass itself because it declared to be conscious about it.

ELSE (either the format does not support bypass natively, or the plug-in does not implement it)

  • The hosting AudioProcessor handles setBypassed() / isBypassed() internally (probably with a simple bool member?) and, depending upon it, it calls either processBlock() or processBlockBypassed(). In this case, the processing function of the plug-in ends up being called only when the plug-in is not in a bypassed state, because processBlock() is only called then.

For the client side:

+----------------------------+
| Plug-in format wrapper,    |
| e.g. juce_VST3_Wrapper.cpp |
+----------------------------+
    |
    |
    V
+-----------------------------+
| client AudioProcessor,      |
| that implements the plug-in |
+-----------------------------+

In this case, the native bypass of the wrapper should be wired with the AudioProcessor's setBypassed() / isBypassed(), but the wrapper will only ever directly call processBlock() in the AudioProcessor. Then, it should be a responsibility of the AudioProcessor’s own implementation to decide what to do there when isBypassed() returns true.
Most developers will do something like:

MyAudioProcessor::processBlock(...)
{
    if (isBypassed ())
    {
         processBlockBypassed(...);
         return;
    }
}

But that should be left as an implementation for one own’s MyAudioProcessor rather than put in the base AudioProcessor class.

Am I correct?

Almost, for backwards compatibility the wrappers (or anything in the JUCE framework that is currently hosting an AudioProcessor) will have to continue calling processBlockBypassed() when bypassed, or too much old code will be broken as a consequence.

I see, but then, how does an AudioProcessor signal that it is now compatible with the “new” API that uses setBypassed() / isBypassed()?

If we resume the idea of using a parameter returned by a getBypassParameter() virtual, then returning nullptr from it will keep the old behavior, while returning an actual parameter may trigger the new behavior that you have described, what do you think about it?

And, for the corner case of the hosting AudioProcessor that would then end up having one more AudioProcessorParameter that wasn’t there before, what if we tolerate the fact that the parameter returned by getBypassParameter() is not added with addParameter() to the AudioProcessor?

That way, that AudioProcessorParameter will effectively become an interface for setting/getting the bypass state, being notified when it changes, but without actually “cluttering” the list of parameters of the hosting AudioProcessor.

I think the best way to solve these issues is that the getBypassParameter does not need to return a parameter which it exports as part of it’s getParameters list. It can just create one and keep a reference to it and return that parameter without ever adding that parameter to it’s official parameter list. However, some plug-ins may choose to do this (like the VST3 wrapper plugin). Then it’s up to the plug-in.

2 Likes

Would you still have setBypassed/isBypassed methods?

No, I think that’d only be confusing and not necessary

Exactly, it should be up to the developer that derives its own MyAudioProcessor to decide whether to return a parameter from getBypassParameter(), and whether that parameter is part of those listed by getParameters() (added with addParameter()) or not.

That way, JUCE/ROLI is free to implement it as desired in its AudioPluginInstances (which are derived from AudioProcessor), as much as plug-in developers can for their own plug-ins.

Then I think the issue becomes that a host has to do this to bypass an AudioProcessor…

if (processor.getBypassParameter() != nullptr)
{
    processor.getBypassParameter()->setValue (1.f);
    // start calling processBlock
} else
{
    // start calling processBlockBypassed
}

Maybe AudioPluginInstance could have the setBypassed / isBypassed methods, hopefully anything else that hosts an AudioProcessor will have the advantage of knowing it’s AudioProcessors implement a getBypassParameter()? although that in itself is overhead, because the default will have to return a nullptr for backwards compatibility. And then to bypass they have to call…

processor.getBypassParameter()->setValue (1.f);

which seems a lot less intuitive than…

processor.setBypassed (true);

Thinking about it more I’m coming around to the idea. I’m wrong about what the host would need to do. As long as each format always returned a valid bypass parameter it could have something like this at the top of the processBlock() method…

if (bypassParameter.getValue() >= 0.5f && ! canPluginBypass)
{
    processBlockBypassed (buffer, midiMessages);
    return;
}

canPluginBypass would have to be an internal bool in each of the formats that determines if the plugin has implemented a bypass mechanism.

This should allow the host to bypass the plugin instance and continue calling processBlock() without it having to do any checks for if a bypass parameter exists.

So then the only slightly annoying thing is calling processor.getBypassParameter()->setValue (1.f) which could be abstracted away for hosts by adding a setBypassed / isBypassed to the AudioPluginInstance class.

My only hesitation is that I would like to hear from someone who is using AudioProcessor’s outside of a plugin or AudioPluginInstance, as to how they would feel creating a bypass parameter, overriding getBypassParameter() for each AudioProcessor, and using the bypass parameter to bypass the AudioProcessor’s?
@Rail_Jon_Rogut is this something you have a lot of experience with?

One advantage of this is that multiple processors could use the same bypass parameter to bypass a whole chain of AudioProcessors, at least on the surface this seems useful, but it’s not something I’ve really done before so it’s hard for me to say.

1 Like

Another quick thought might it be sensible to have a class such as AudioProcessorWithBypass that does some of the bulk work that might be required otherwise?

NOTE: I haven’t completely thought that through yet, just a passing thought.

I think I will just post what I have once it’s done and has passed code review and then we can discuss any issues like that. I think it’s quite difficult to discuss without seeing the actual code changes.

2 Likes

Consider an experimental/bypass branch for that, like you did for multibus, so that tweaks and evolutions on this don’t affect people that use devel for released products

5 Likes

I realise your working on this at the moment @fabian, but I was just talking to a colleague of mine and he had a suggestion I thought was worth adding here.

How about, assuming you have gone with the virtual getBypassParameter() solution, the default implementation returns any parameter in the list of parameters that is marked with a bypassParameter category. Just makes adding a bypass parameter in a plugin that little bit clearer, it will also work for the VST3AudioPluginInstance class too, and any future formats that have a way to mark a parameter as a bypass parameter.

@Anthony_Nicholls Well, currently I’m using getBypassParameter to see if the AudioProcessor expects processBlockBypassed calls or not. If getBypassParameter returns nullptr it will call processBlockBypassed when bypassed.

I know that this bypass feature request might not seem like a lot of work, but adding support for all the use-cases is really quite tricky.

  1. getBypassParameter returns nullptr
  2. getBypassParameter returns a parameter which is not in the plug-in’s parameter list
  3. getBypassParameter returns a parameter which is in the plug-in’s parameter list

I’ve finished the hosting side for all plug-in formats and finished the client code for AAX and VST3. But still need to do AU/AUv3/VST and then loads of testing.

I need to put this on the back burner for now, as some other priorities have come up. So please don’t expect this to be released this or next week. But I have some time to finish this work the week after. Sorry!

Could you put this on a branch, I’ll be happy to jump in and make pull requests into your branch, which might speed things up and reduce work load for you when you do get to it?

What I was thinking was that there could be a private member AudioProcessorParameter* bypassParameter = nullptr when addParameter is called it checks if the parameter is marked as a bypass parameter and sets bypassParameter to point to this parameter, then it returns bypassParameter in getBypassParameter()

Anyway it’s a small point.

Thanks for looking into this @fabian (and @t0m ), I’m very aware it’s a tricky thing to implement this feature taking into consideration everything. I did look at implementing the bypass parameter idea and quickly stopped, realising how late it was already and that I didn’t want to be coding into the early hours!

I think I’d want this to pass internal code review before we put this on a branch.

1 Like

OK this is now on develop with commit 0db9415.

Every AudioProcessor now has a new method called AudioProcessor::getBypassParamater. If the processor supports bypass control then this will return a non-nullptr to an AudioProcessorParameter. Note that all plug-in backends (except RTAS) support this method.

AudioProcessors can choose if the returned parameter will be part of the “normal” parameter list (returned by AudioProcessor::getParameters) or not. When hosting a plug-in, AudioProcessor::getBypassParamater will return a parameter which is part of the plug-in’s normal parameter list for VST3 and AAX (as this is mandatory for these plug-in types anyway) and for VST2/AU/AUv3 it will not be part of the normal parameter list.

5 Likes

Thanks Fabian. I haven’t had a chance to use this yet myself. However it’s been looked over by one of my colleagues as it will be useful for us to incorporate as we were doing some of the work this commit does in our own version of JUCE, and all looks good as far as we can see.

I’ll try and use this in the command line host I was building tonight if I get a chance.

2 Likes