Bypassing a hosted plugin

@yfede There is still a problem with the processBlock, processBlockBypassed. Consider my job of implementing what the JUCE’s VST/VST3/AU AudioProcessor (which wraps a VST/VST3/AU) should do. Old JUCE hosts will call either processBlock or processBlockBypassed - as they don’t know anything about getBypassParameter. How should I implement these methods? processBlockBypassed still needs to bypass the effect - so the VST/VST3/AU wrapper implementation needs to bypass the VST/VST3/AU when this method is called. But what happens when processBlock is then called? Should the wrapper un-bypass the VST/VST3/AU now?

My current idea is that processBlockBypassed will always keep the VST/VST3/AU in a bypassed state (new hosts shouldn’t be calling this anyway). That means even if the user tries to disable the bypass, as the host keeps calling processBlockBypassed, JUCE just immediately enables it again. If you switch from a processBlockBypassed call to a processBlock (i.e. the last processBlock call was a processBlockBypassed) then the VST/VST3/AU will be un-bypassed but only once. After that, the plug-in will always follow whatever the parameter bypass dictates. This way, any new host, which is only ever calling processBlock will work as expected as the processBlock callback is completely controlled by the bypass parameter. Older hosts will still be able to bypass and unbypass the VST/VST3/AU and the VST/VST3/AU’s editor’s bypass button will work if the old host only ever calls processBlock.

1 Like

I’m sorry, I cannot really follow you in hosting territory because I’m only writing plug-ins.
I known very little about how the hosting code is interfaced with AudioProcessors. :sweat:

The only thing that I can recommend, if you intend to implement that logic, is to do it outside of the AudioProcessor class because it is only specific to the hosting side of things, and should therefore go among other hosting-only code. Perhaps in the AudioPluginInstance?

No I am implementing an AudioProcessor (just like you when you write a plug-in). An AudioProcessor which wraps a VST/VST3/AU and may be called by old JUCE code which does not know about getBypassParameter.

If we are going to consider implementing this via a parameter that is marked as a bypass parameter then I have suggested this before (twice)…

However keep in mind that not all wrappers implement the bypassing mechanism as a parameter (VST2 and AU for example).

I think reporting a bypass parameter to the host is a good solution to deal with it from the plugin side. However on the host side there will be no way to know if the parameter is a bypass parameter or not for VST2 and AU (I think that includes AUv3) - so in other words it would only work for VST3 forcing any host that wants to host VST2 and AU to fall back to calling processBlockBypassed().

You could add a special parameter for VST2 and AU that deals with this on the hosting side but then you would be reporting an extra parameter that isn’t really there, imagine showing a list of available parameters to a user, which bypass parameter should they automate?

So I think that from the host side there needs to be a set/isBypassed and a bypassChanged callback, however to reduce confusion IMO it should be in the AudioPluginInstance class.

Keeping processBlockBypassed in the host could continue to work as it always did (no need to bypass or un-bypass) this way existing hosts work as they always did, no change in functionality. Future hosts should probably rely on setBypassed() however if they wanted to override that behaviour they could. For example Cubase has both a bypass and a deactivate, so a host could release a plugins resources and use the processBlockBypassed to pass audio through without it actually having to call into the plugin, but for the normal bypass procedure it would call setBypassed() and continue calling processBlock.

Hopefully that makes some sense.

EDIT: Also thanks for taking the time to look into this with so much consideration.

1 Like

Just to add the other thing to consider as I’ve mentioned before (but might have been missed), plugins are not obliged to report a bypass parameter or to implement the setBypass callbacks in the wrappers (even though the JUCE wrappers do) therefore there has to be a backup, the host code should determine if the plugin implements the setBypass and if it doesn’t AND setBypassed (true) has been called it should forward a call to processBlock onto processBlockBypassed.

1 Like

Sorry, it might be my ignorance of the hosting code of JUCE, but I was under the impression that the AudioProcessor that’s created for hosting a (possibly non-JUCE) plug-in, is only an “implementation detail” of hosting, and thus it is always compiled together with the hosting code that ends up using it.

If that is the case, then the hosting code and the AudioProcessor that it uses for wrapping plug-ins to be hosted should and could be updated together, not having a case in which one interacts with a different version of the other.

That means that, once the getBypassParameter() is introduced, the AudioProcessor used for wrapping hosted plug-ins should also be updated to always override that callback and returning an internal parameter that only that specific "hosting AudioProcessor" should use for keeping track of whether the hosted plug-in is in bypass state or not.

Then, when the host wants to change the bypass state for that plug-in, it acts on that parameter, and the change of its value should call whatever format-specific code is necessary to put in bypass mode the wrapped plug-in.

Conversely, when the wrapped plug-in signals that it wants to change its bypass state, the hosting AudioProcessor should reflect that by keeping that information in its bypass AudioProcessorParameter.

Am I completely off-track?

So I just finished @yfede’s suggestion and added the special parameter to VST2 and AU as you suggested. And then I read:

Arggghh… this is turning out to be a nightmare. I think we do need to go back to a setBypass/isBypassed/setBypassNotifyingHost similar scheme. So in essence, very similar to a real parameter, just that it’s not a real parameter.

That’s a very good point.

@yfede

No you are not. But you need to consider old JUCE hosts written by JUCE customers (not by us). In this code, they will have not updated their code yet to consider the special bypass parameter. If they then update to the newest code, it would be a breaking change, because they might have relied on processBlock and processBlockBypassed to do all the bypass work. As you say yourself, the host would need to “act on that parameter” and this would mean that all JUCE hosts out there would need change their code when updating to the latest JUCE.

Mark the added bypass parameter as non-automatable if the wrapped plug-in does not expose one explicitly

What do you think @anthony-nicholls? That could work?

I want to give that some thought before I say yes, I’ll get back to you tonight. My initial thought is that it’s a little hacky, i.e. a host might say how many parameters there are (which would actually be 1 more than there really is), or what about a generic editor implemented by a host. These often do, and I think they should, include non-automatable parameters. Not being automatabe doesn’t mean hidden (VST2 even has a separate flag for hiding parameters it’s just most hosts don’t implement it). Imagine a parameter in a plugin that sets some FFT size, you might want to prevent automation of it but you don’t want to prevent a user setting it in a generic editor, or more importantly from an external control surface.

Out of interest if we allow a plugin to mark a parameter as a bypass parameter and have the wrappers listen to that parameter to implement everything correctly from the plugin side (forget the hosting side for a second), is there still any good reason to have a set/isBypassed in the AudioProcessor class?

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);