AudioProcessorGraph Node Bypass


#1

Hi,

in order to control CPU load in a large host, I thought it would be a good idea to temporarily suspend instrument and effects nodes (switch to bypass) that are not currently needed, for example, when the host knows there will be no MIDI data for a certain instrument to play. If out of 20+ instruments only an average of 5 are used at any one time, this will make a huge difference. The host could enable a node shortly before it will be needed and disable it again when there will be no more MIDI data for a while.

This idea of course is based on my (naive?) assumption that setting a node to bypass is more light-weight and efficient than removing/adding nodes from the graph (including a potentially large number of connections).

Is this assumption correct, of am I missing something?

Anyone else interested in implementing this?


#2

I am.


#3

Should not be to difficult to implement, but I’m not familiar enough with the graph code yet.

It is important to consider that “Bypass” is highly non-trivial, because there is no way to know what that means for a particular plugin with multiple inputs, side-chains and multiple outputs. A simple stereo filter may just pass through the buffers, but what for the other inputs and outputs? This is probably the reason why almost every plugin implements its own internal bypass switch.

On the other hand, “Suspend” is easy: It simply ignores all inputs and zeros all outputs of a node. :smiley:

I would love to have member functions like

bool AudioProcessorGraph::Node::suspendProcessing ( bool suspended );
bool AudioProcessorGraph::suspendProcessingForNode ( uint32 nodeId, bool suspended );

Node suspension should also work while a graph is running without disrupting playback. That would allow for dynamic performance optimization depending on the MIDI data managed by the host.


#4

I first tried suspending the plugin but that didn’t seem to reduce cpu load. Perhaps I made a mistake but I decided to go for a full vst power-down in order to learn how to call and use custom vst behaviors from an AudioProcessorGraph.

I noticed that VSTPluginInstance already had a setPower(bool) method, but it was private. So the first thing to do was to make that public. The setPower method dispatched the bool value to the plugin, and set the isPowerOn bool to reflect the current state. I then added a public getPower method which returned the state.

Since VSTPluginInstance is an AudioProcessor, I added virtual setPower/getPower methods to it, and all who inherited from it. Then I modified the processBlock method of VSTPluginInstance and had it completely bypass the processing loop when the isPowerOn bool was false, and that causes the cpu load to go down.

After doing that I modifed the plugin host demo such that I could tell a filter to power-down. The syntax being node->getProcessor()-setPower(bool). A problem however was that if you power-down the plugin before it received its note off midi messages (for soft synths) then upon power-up it immediately began making noise again. To deal with that I decided to add the ability to command a vst plugin to insert allNotesOff messages (for all 16 midi channels) into its midiBuffer whenever I set a bool indicating it needs to be done. To do that I added bool ‘allNotesOff’ and a method to set it on the VSTPluginInstance, modified the processBlock method to notice it, reset it, then insert the noteOff messages in the midiBuffer. I then had to add the allNotesOff() method to AudioProcessor and its ancestors.

From the audio plugin host demo, when I want to power-down a plugin I first call the allNotesOff() method, wait long enough for VSTPluginInstance::processBlock to be called from the audio callback, followed by setPower(false). I don’t have any experience yet with anything other than pc/vst systems and have no idea how other platforms think of these capabilities.

A nice enhancement to the power-down cycle, which I didn’t implement yet, would be for the processBlock method to wait until the output audio falls below a certain threshold and then does the bypass. This would allow any audio-tailoff’s to complete and make for nicer transitions on-stage, between songs. I’m in the process of writing on-stage software for myself, and am going for a system whereby upon command, I can transfer from one instrument to another seamlessly, without have to wait while plugins are loaded or unloaded.

I had to change the following files:
juce_amalgamated.h
juce_amalgamated.cpp
juce_audioprocessor.h
juce_audioprocessorgraph.cpp
juce_vstpluginformat.cpp

class JUCE_API AudioProcessor - added three new virtual methods (below acceptsMidi and producesMidi)

    /** Returns true if the processor wants midi messages. */
    virtual bool acceptsMidi() const = 0;

    /** Returns true if the processor produces midi messages. */
    virtual bool producesMidi() const = 0;

	/** Kurt Olsen, allows enable/disable so that unused processors don't consume cpu */
	virtual void setPower (const bool on) {}
	virtual bool getPower () const {return true;}
	virtual void allNotesOff() {};

class VSTPluginInstance - modifed the declaration as follows, I show it in context, look for my name tagged to the changes:

    //==============================================================================
    void getStateInformation (MemoryBlock& destData);
    void getCurrentProgramStateInformation (MemoryBlock& destData);
    void setStateInformation (const void* data, int sizeInBytes);
    void setCurrentProgramStateInformation (const void* data, int sizeInBytes);

    //==============================================================================
	
    void setPower (const bool on); // kurt olsen - made this public, it was private
	bool getPower () const { return isPowerOn; } // kurt olsen - added this method
	void allNotesOff() { bAllNotesOff = true;} // kurt olsen - added this method

    void timerCallback();
    void handleAsyncUpdate();
    VstIntPtr handleCallback (VstInt32 opcode, VstInt32 index, VstInt32 value, void *ptr, float opt);

private:
    //==============================================================================
    friend class VSTPluginWindow;
    friend class VSTPluginFormat;

    AEffect* effect;
    String name;
    CriticalSection lock;
    bool wantsMidiMessages, initialised, isPowerOn;
	bool bAllNotesOff; // kurt olsen - added this boolean
    mutable StringArray programNames;

class VSTPluginInstance - implementation, changed beginning of processBlock to check for isPowerOn:

void VSTPluginInstance::processBlock (AudioSampleBuffer& buffer,
                                      MidiBuffer& midiMessages)
{
    const int numSamples = buffer.getNumSamples();

    if (initialised && isPowerOn)

Then, a little further down in the processBlock method I detect, and transmit allNotesOff when told to

        if (wantsMidiMessages)
        {
            midiEventsToSend.clear();
            midiEventsToSend.ensureSize (1);

			// kurt olsen - added this 
			if( bAllNotesOff == true ) {
				bAllNotesOff = false;
				for(int im = 1; im <= 16; im++ ) {
					const MidiMessage pcm = MidiMessage::allNotesOff(im);
					midiMessages.addEvent(pcm,0);
				}
			}

			MidiBuffer::Iterator iter (midiMessages);

BOTH AudioProcessorGraph and AudioProcessorGraph::AudioGraphIOProcessor needed these:

	void setPower(const bool) {}; // kurt olsen - added this method
	bool getPower() const { return true; } // kurt olsen - added this method

To use this from the plugin host demo, some snippets…

Class FilterComponent, i changed the paint method to denote power on/off

    void paint (Graphics& g)
    {
        g.setColour (ColourFilterConnectionBackground);

        const int x = 4;
        const int y = pinSize;
        const int w = getWidth() - x * 2;
        const int h = getHeight() - pinSize * 2;

        g.fillRect (x, y, w, h);

        const AudioProcessorGraph::Node::Ptr f (graph.getNodeForId (filterID));
		if( f->getProcessor()->getPower() ) {
			g.setColour (Colours::black);
		} else {
			g.setColour (Colours::red);
		}

		g.setFont (font);
        g.drawFittedText (getName(),
                          x + 4, y + 2, w - 8, h - 4,
                          Justification::centred, 2);

        g.setColour (ColourFilterConnectionBorder);
        g.drawRect (x, y, w, h);
    }

I added a new popup menu item to handle power on/off and it’s handler goes like this:

			bool bEnabled = node->getProcessor()->getPower();

			if( bEnabled ) {
				node->getProcessor()->allNotesOff();
				Time::waitForMillisecondCounter( Time::getMillisecondCounter() + 100 );
				DBG("Sent AllNotesOff");

			}
			node->getProcessor()->setPower(!bEnabled);
			node->properties.set("enabled", node->getProcessor()->getPower());

Remember that you might have to modify these classes in both the amalgamated and non-amalgamated versions of the source files.
Good luck.


#5

Is your solution limited to VST? It would be very desirable to have one that covers AudioUnits too.

This is something you should rather handle in your host’s MIDI code. Depending on where your stream of MIDI originates from, just sending an all-notes-off in the middle of something is not sufficient. Without proper note on/off tracking per MIDI channel, a clean shutdown of all open notes will not work with all synths.

As I noted in my previous post, suspension is only a rudimentary measure for gaining some performance back from unused synths. It might be more desirable and flexible to just rebuild the graph without the disabled nodes? As far as I understand the graph code today, there should be no noticeable clicks or cracks.


#6

Could you elaborate on that please? I’m not quite sure what you mean.


#7

Sure. The source of your MIDI data is responsible for no longer sending messages to the graph when you want to mute/unmute individual nodes. Whether that is a sequencer or live keyboard input doesn’t matter. Somewhere early in your data stream, you need to keep track of which notes/keys are currently depressed and reset all open notes when you want all sound to stop (and prevent new notes from being played, of course).

If the MIDI stream continues to flow after an all-note-soff message, the latter is pretty useless.


#8

I see what you mean. In my case I’ll be performing live so that’s the source of the midi. I’ll just have to see how it goes and deal with it as the problems come up!


#9

I’ve implemented a solution that works better for ‘powerdown’ of vst plugins. In the previous incarnation I allowed setPower() to do what it always did, and added a getPower() method and then just skipped the processing loop when isPowerOn was set. The problem is that some (not all) of my soft synths would emit a screech, pop, click etc. When I called setPower(true). Also, some freebie plugins would show their nag screens again on power up.

So I decided to NOT call setPower(bool) - which also dispatches to the vst plugin itself. Instead I’ve added two new methods named setPowerEx(bool) and getPowerEx(). These simply set a new bool isPowerOnEx. Then, in the processing loop - instead of ignoring it when isPowerOn is set, I ignore it when isPowerOnEx is set. Doing this, stopped the annoying noises.

One other issue that cropped up is when the main processing loop is bypassed, the ‘else’ clause in the loop only cleared the output channels. The problem with this is that then the vst passes the input buffer to the output buffer. So…if I had 5 virtual guitar amps in the graph, all connected to the audio input, even when powered down, the input noise was sent to the output 5 times resulting significant ‘hiss’ even with the amps powered down. So to deal with that I’m clearing both the input and output buffers instead of just the output buffers. So far the AudioProcessorGraph seems to deal with it ok, and I can have a lot of plugins loaded and quickly enable them without enduring the lengthy dll loading/initialization or cpu load. So far so good. I think I’ll add another option for ‘bypass’ so that in some circumstances I will allow the input to pass to the output (by not clearing both the input and output buffers).

to be continued…