AudioProcessorGraph slow manipulations

Okay, there were basically no changes in CalcRenderingSequence so not much merging to do…

First of all, we need a DelayChannelOp:

[code]class DelayChannelOp : public AudioGraphRenderingOp
{
public:
DelayChannelOp(const int ChannelNum_, const int Delay_) : ChannelNum(ChannelNum_), Delay(Delay_)
{
int size = Delay+5000; //safety margin for audio buffer size
DelayBuffer = new float[size];
memset(DelayBuffer,0,size*sizeof(float));
current=begin=&DelayBuffer[0];
end=&DelayBuffer[size-1];
delay=&DelayBuffer[Delay];
}
DelayChannelOp() : ChannelNum(0), Delay(0) {DelayBuffer=0;};

~DelayChannelOp() 
{
	if (DelayBuffer) delete DelayBuffer;
}

void perform (AudioSampleBuffer& sharedBufferChans, const OwnedArray <MidiBuffer>&, const int numSamples)
{
	float *in=sharedBufferChans.getSampleData(ChannelNum,0);
	int numSamples_ = numSamples;
	while (--numSamples_ >= 0)
	{
		*delay++ = *in;
		*in++ = *current++;
		if (delay>end) delay=begin;
		if (current>end) current=begin;
	}
}

private:
const int ChannelNum,Delay;
float *DelayBuffer,*begin,*end,*current,*delay;
};[/code]

Then, in CalcRenderingSequence, for each input to each node, we check which one causes the greatest amount of latency. All other inputs are delayed appropriately. Latency is passed down the rendering chain… I’ve marked all changes I did with a //CS comment.

class RenderingOpSequenceCalculator
{
public:

	RenderingOpSequenceCalculator (AudioProcessorGraph& graph_,
								   const Array<void*>& orderedNodes_,
								   Array<void*>& renderingOps)
		: graph (graph_),
		  orderedNodes (orderedNodes_)
	{
		nodeIds.add ((uint32) zeroNodeID); // first buffer is read-only zeros
		channels.add (0);

		midiNodeIds.add ((uint32) zeroNodeID);
		output_delay=0; //CS
		for (int i = 0; i < orderedNodes.size(); ++i)
		{
			createRenderingOpsForNode ((AudioProcessorGraph::Node*) orderedNodes.getUnchecked(i),
									   renderingOps, i);

			markAnyUnusedBuffersAsFree (i);
		}
		graph.setLatencySamples(output_delay); //CS
	}

	int getNumBuffersNeeded() const	 { return nodeIds.size(); }
	int getNumMidiBuffersNeeded() const	 { return midiNodeIds.size(); }

private:

	AudioProcessorGraph& graph;
	const Array<void*>& orderedNodes;
	Array <int> channels;
	Array <uint32> nodeIds, midiNodeIds;
	NamedValueSet NodeDelays; //CS
	int node_delay; //CS
	int max_delay,output_delay; //CS

	enum { freeNodeID = 0xffffffff, zeroNodeID = 0xfffffffe };

	static bool isNodeBusy (uint32 nodeID) throw()  { return nodeID != freeNodeID && nodeID != zeroNodeID; }

	void createRenderingOpsForNode (AudioProcessorGraph::Node* const node,
									Array<void*>& renderingOps,
									const int ourRenderingIndex)
	{
		const int numIns = node->getProcessor()->getNumInputChannels();
		const int numOuts = node->getProcessor()->getNumOutputChannels();
		const int totalChans = jmax (numIns, numOuts);

		Array <int> audioChannelsToUse;
		int midiBufferToUse = -1;

		max_delay=0; //CS
		float sendAmount = node->properties.getWithDefault("sendAmount",1);
		// get a list of all the inputs to this node
		Array <int> sourceNodes, sourceOutputChans;

		max_delay=0; //CS: first go over all input nodes regardless of channel to get max_delay of all inputs
		//output_delay=0;
		for (int i = graph.getNumConnections(); --i >= 0;)
		{
			const AudioProcessorGraph::Connection* const c = graph.getConnection (i);
			if (c->destNodeId == node->nodeId ) sourceNodes.add (c->sourceNodeId);
		}
		for (int i=0;i<sourceNodes.size();i++)	
		{
			node_delay = NodeDelays[String(sourceNodes.getUnchecked(i))];
			if (node_delay>max_delay) 
					max_delay = node_delay;
		}

		sourceNodes.clear();
		//CS: end of initial max_delay calculation
		for (int inputChan = 0; inputChan < numIns; ++inputChan)
		{
			/*CS: now defined outside of channel loop
			// get a list of all the inputs to this node
			Array <int> sourceNodes, sourceOutputChans;
			*/

			for (int i = graph.getNumConnections(); --i >= 0;)
			{
				const AudioProcessorGraph::Connection* const c = graph.getConnection (i);

				if (c->destNodeId == node->nodeId && c->destChannelIndex == inputChan)
				{
					sourceNodes.add (c->sourceNodeId);
					sourceOutputChans.add (c->sourceChannelIndex);
				}
			}

			int bufIndex = -1;
			

			if (sourceNodes.size() == 0)
			{
				// unconnected input channel

				if (inputChan >= numOuts)
				{
					bufIndex = getReadOnlyEmptyBuffer();
					jassert (bufIndex >= 0);
				}
				else
				{
					bufIndex = getFreeBuffer (false);
					renderingOps.add (new ClearChannelOp (bufIndex));
				}
			}
			else if (sourceNodes.size() == 1)
			{
				// channel with a straightforward single input..
				const int srcNode = sourceNodes.getUnchecked(0);
				const int srcChan = sourceOutputChans.getUnchecked(0);

				bufIndex = getBufferContaining (srcNode, srcChan);

				if (bufIndex < 0)
				{
					// if not found, this is probably a feedback loop
					bufIndex = getReadOnlyEmptyBuffer();
					jassert (bufIndex >= 0);
				}

				if (inputChan < numOuts
					 && isBufferNeededLater (ourRenderingIndex,
											 inputChan,
											 srcNode, srcChan))
				{
					// can't mess up this channel because it's needed later by another node, so we
					// need to use a copy of it..
					const int newFreeBuffer = getFreeBuffer (false);

					renderingOps.add (new CopyChannelOp (bufIndex, newFreeBuffer));

					bufIndex = newFreeBuffer;
				}
				node_delay = NodeDelays[String(srcNode)]; //CS
				
				if (node_delay<max_delay) //CS: need to add extra delay to keep everything in sync
					renderingOps.add ( new DelayChannelOp(bufIndex,max_delay-node_delay));
				
			}
			else
			{
				// channel with a mix of several inputs..

				// try to find a re-usable channel from our inputs..
				int reusableInputIndex = -1;

				for (int i = 0; i < sourceNodes.size(); ++i)
				{
					const int sourceBufIndex = getBufferContaining (sourceNodes.getUnchecked(i),
																	sourceOutputChans.getUnchecked(i));

					if (sourceBufIndex >= 0
						&& ! isBufferNeededLater (ourRenderingIndex,
												  inputChan,
												  sourceNodes.getUnchecked(i),
												  sourceOutputChans.getUnchecked(i)))
					{
						// we've found one of our input chans that can be re-used..
						reusableInputIndex = i;
						bufIndex = sourceBufIndex;
						node_delay = NodeDelays[String(sourceNodes.getUnchecked(i))]; //CS
						if (node_delay<max_delay) //CS
						{
							renderingOps.add(new DelayChannelOp
												(sourceBufIndex,max_delay-node_delay));
						}
						break;
					}
				}

				if (reusableInputIndex < 0)
				{
					// can't re-use any of our input chans, so get a new one and copy everything into it..
					bufIndex = getFreeBuffer (false);
					jassert (bufIndex != 0);

					const int srcIndex = getBufferContaining (sourceNodes.getUnchecked (0),
															  sourceOutputChans.getUnchecked (0));
					if (srcIndex < 0)
					{
						// if not found, this is probably a feedback loop
						renderingOps.add (new ClearChannelOp (bufIndex));
					}
					else
					{
						renderingOps.add (new CopyChannelOp (srcIndex, bufIndex));
					}

					reusableInputIndex = 0;
					node_delay = NodeDelays[String(sourceNodes.getUnchecked(0))]; //CS
					if (node_delay<max_delay) //CS
					{
						renderingOps.add(new DelayChannelOp
										(bufIndex,max_delay-node_delay)); 
					}
				}

				for (int j = 0; j < sourceNodes.size(); ++j)
				{
					if (j != reusableInputIndex)
					{
						const int srcIndex = getBufferContaining (sourceNodes.getUnchecked(j),
																  sourceOutputChans.getUnchecked(j));
						if (srcIndex >= 0) //CS: changed whole block
						{
							node_delay = NodeDelays[String(sourceNodes.getUnchecked(j))];
							if (node_delay<max_delay) //CS
							{
								if (! isBufferNeededLater (ourRenderingIndex,
								inputChan,
								sourceNodes.getUnchecked(j),
								sourceOutputChans.getUnchecked(j))) //CS
								{	
									renderingOps.add(new DelayChannelOp(srcIndex,         
										max_delay-node_delay)); //CS
								}
								else //CS: buffer is reused elsewhere, can't be delayed
								{	
									int buffer_to_delay = getFreeBuffer(false);
									renderingOps.add (new CopyChannelOp (srcIndex, buffer_to_delay));
									renderingOps.add(new DelayChannelOp(buffer_to_delay,         
										max_delay-node_delay)); //CS
									renderingOps.add (new AddChannelOp (buffer_to_delay, bufIndex));
								}
							}
							else renderingOps.add (new AddChannelOp (srcIndex, bufIndex));
						}	
					}
				}
			}
			
			jassert (bufIndex >= 0);
			audioChannelsToUse.add (bufIndex);

			if (inputChan < numOuts)
				markBufferAsContaining (bufIndex, node->nodeId, inputChan);
			sourceNodes.clear(); //CS
			sourceOutputChans.clear(); //CS
		}

		for (int outputChan = numIns; outputChan < numOuts; ++outputChan)
		{
			const int bufIndex = getFreeBuffer (false);
			jassert (bufIndex != 0);
			audioChannelsToUse.add (bufIndex);

			markBufferAsContaining (bufIndex, node->nodeId, outputChan);
		}

		// Now the same thing for midi..
		Array <int> midiSourceNodes;

		for (int i = graph.getNumConnections(); --i >= 0;)
		{
			const AudioProcessorGraph::Connection* const c = graph.getConnection (i);

			if (c->destNodeId == node->nodeId && c->destChannelIndex == AudioProcessorGraph::midiChannelIndex)
				midiSourceNodes.add (c->sourceNodeId);
		}

		if (midiSourceNodes.size() == 0)
		{
			// No midi inputs..
			midiBufferToUse = getFreeBuffer (true); // need to pick a buffer even if the processor doesn't use midi

			if (node->getProcessor()->acceptsMidi() || node->getProcessor()->producesMidi())
				renderingOps.add (new ClearMidiBufferOp (midiBufferToUse));
		}
		else if (midiSourceNodes.size() == 1)
		{
			// One midi input..
			midiBufferToUse = getBufferContaining (midiSourceNodes.getUnchecked(0),
												   AudioProcessorGraph::midiChannelIndex);

			if (midiBufferToUse >= 0)
			{
				if (isBufferNeededLater (ourRenderingIndex,
										 AudioProcessorGraph::midiChannelIndex,
										 midiSourceNodes.getUnchecked(0),
										 AudioProcessorGraph::midiChannelIndex))
				{
					// can't mess up this channel because it's needed later by another node, so we
					// need to use a copy of it..
					const int newFreeBuffer = getFreeBuffer (true);
					renderingOps.add (new CopyMidiBufferOp (midiBufferToUse, newFreeBuffer));
					midiBufferToUse = newFreeBuffer;
				}
			}
			else
			{
				 // probably a feedback loop, so just use an empty one..
				 midiBufferToUse = getFreeBuffer (true); // need to pick a buffer even if the processor doesn't use midi
			}
		}
		else
		{
			// More than one midi input being mixed..
			int reusableInputIndex = -1;

			for (int i = 0; i < midiSourceNodes.size(); ++i)
			{
				const int sourceBufIndex = getBufferContaining (midiSourceNodes.getUnchecked(i),
																AudioProcessorGraph::midiChannelIndex);

				if (sourceBufIndex >= 0
					 && ! isBufferNeededLater (ourRenderingIndex,
											   AudioProcessorGraph::midiChannelIndex,
											   midiSourceNodes.getUnchecked(i),
											   AudioProcessorGraph::midiChannelIndex))
				{
					// we've found one of our input buffers that can be re-used..
					reusableInputIndex = i;
					midiBufferToUse = sourceBufIndex;
					break;
				}
			}

			if (reusableInputIndex < 0)
			{
				// can't re-use any of our input buffers, so get a new one and copy everything into it..
				midiBufferToUse = getFreeBuffer (true);
				jassert (midiBufferToUse >= 0);

				const int srcIndex = getBufferContaining (midiSourceNodes.getUnchecked(0),
														  AudioProcessorGraph::midiChannelIndex);
				if (srcIndex >= 0)
					renderingOps.add (new CopyMidiBufferOp (srcIndex, midiBufferToUse));
				else
					renderingOps.add (new ClearMidiBufferOp (midiBufferToUse));

				reusableInputIndex = 0;
			}

			for (int j = 0; j < midiSourceNodes.size(); ++j)
			{
				if (j != reusableInputIndex)
				{
					const int srcIndex = getBufferContaining (midiSourceNodes.getUnchecked(j),
															  AudioProcessorGraph::midiChannelIndex);
					if (srcIndex >= 0)
						renderingOps.add (new AddMidiBufferOp (srcIndex, midiBufferToUse));
				}
			}
		}

		if (node->getProcessor()->producesMidi())
			markBufferAsContaining (midiBufferToUse, node->nodeId,
									AudioProcessorGraph::midiChannelIndex);
		NodeDelays.set(String(node->id),max_delay + node->getProcessor()->getLatencySamples()); //CS
		
		if (numOuts == 0) output_delay = max_delay; //CS: assume this is the audio output
		renderingOps.add (new ProcessBufferOp (node, audioChannelsToUse,
											   totalChans, midiBufferToUse));
	}

I’ve done this in a plugin context so there’s a variable output_delay which contains the latency at the audio output. This variable doesn’t play a role for a host application; however, I should mention that in it’s calculation I’ve assumed that there is only one output node. Otherwise there’d be an extra loop over all output nodes, with added DelayChannelOps for those that have less than maximum latency. Also, I have omitted the MIDI part, as I don’t use that. Should be pretty much identical though…
And feedback loops don’t work, but I don’t think a plugin with a non-zero latency in a feedback loop can be handled properly. They don’t work even without PDC, but that’s a different story…