Removing elements from OwnedArray


#1

This is a basic problem. I want to play some audio files simultaneously using a sequencer I built. When the sequencer hits a note-on it adds a file to a que of currently playing files and when the file has reached the end it gets removed from the que.

Consider the following class:

class fileToPlay
{
public:
AudioBuffer<float>* fileBuffer;
int startSample;
int endSample;	

fileToPlay() { fileBuffer = NULL; }
~fileToPlay() { fileBuffer = NULL; }

 void setFileToPlay(AudioBuffer<float>* _fileBuffer)
{
    fileBuffer = _fileBuffer;
    startSample = 0;
    endSample = fileBuffer->getNumSamples() - 1;
}
private:	
};

In another class I declared OwnedArray<fileToPlay> ftp

Then I read each fileToPlay in ftp and when needed I call ftp.remove()

When playing short files (like kick samples) the program runs fine. But when playing longer files (such as reverbed snares) one file might get removed while another is still playing, So the still-playing file gets moved while it is running which of course is a bad thing.

My thoughts are to use something like std::list instead of OwnedArray since list can erase elements without moving existing elements. I prefer using only JUCE objects in my code so is there another solution here?


#2

What is keep track of the current position in each fileToPlay? Is the issue that when the index of the fileToPlay changes it’s audio is get accessed with the wrong current position?

Is all access to ftp and the fileToPlay object happening from the same thread?


#3

Yes. They happen from the same thread. Another object increments startSample value while the file is playing. when startSample > endSample the sound is over and it sends a message to ftp to remove this fileToPlay from itself.

If there is another fileToPlay in ftp it will get moved around and then I get a null pointer for it as soon as the remove() function is called on ftp.


#4

Can you post the code that does the remove? And the code that is crashing?


#5

The ownedArray is a array of pointers, the owned objects are on the heap. So when you remove an element from the OwnedArray, the pointers are moved, but not the objects.

The OwnedArray has advantages over std::list, e.g. when you need to access the objects by index.


#6

Sure. I modifed the code a bit so now I have this class

class AudioOutEngine 
{		
 ...
double sliderValue;
int itemSelectedInComboBox = 0;	
OwnedArray<fileToPlay, CriticalSection> fileQue;	
...
 void sendToOutput(AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    //if there are audio files waiting in que, send them to the output  buffer
	if (fileQue.size()>0)
    {	
		for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
		{
			auto* channelData = buffer.getWritePointer(channel);

            //iterate through the que and send each pending file to the output buffer
			for (int q = 0; q < fileQue.size(); ++q)
			{
			    auto* processedData = fileQue[q]->fileBuffer->getReadPointer(channel);
				for (int sampleIndex = 0; sampleIndex < buffer.getNumSamples(); ++sampleIndex)
				{
					//Audio file finished playing. Do nothing fow now.
					if (fileQue[q]->startSample + sampleIndex > fileQue[q]->endSample)
					{
									
					}
					//feed the output buffer
					else
					{		
                     channelData[sampleIndex] = channelData[sampleIndex] + processedData[sampleIndex + fileQue[q]->startSample]*sliderValue;
					}					 
				}				 
			}
    
		// iterate through the que again. This time remove any file that has reached its endSample
		for (int i =0; i<fileQue.size();++i)
		{
			fileToPlay* temp = fileQue[i];
			temp->startSample = temp->startSample + buffer.getNumSamples();
			if (temp->startSample > temp->endSample)
			{
				fileQue.remove(i);
			}					 
		}	
     }
}

void addAudioToQue()
{
  //get the currently selected audio file from a ComboBox and add it to the que
  if (itemSelectedInComboBox)
  {
	fileQue.add(new fileToPlay());
	fileQue.getLast()->setFileToPlay(fileBuffers[itemSelectedInComboBox - 1]);
   }
 }
};

now, in PluginProcessor.h I declare
AudioOutEngine AOE;
and inside PluginProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
I have this line
AOE.sendToOutput(buffer, midiMessages);

To put it in words: processBlock calls AOE.SendToOutput which checks if there are any audio files in que. If there are it will pass them to buffer so they will be played (the function will accumulate all active files into one buffer). If not, nothing will happen.
on a different thread a sequencer runs. Whenever it hits a note-on step it will call AOE.addAudioToQue() which doesn’t do much besides checking which file the user selected from a ComboBox and adds it to our OwnedArray so now AOE.sendToOutput will have somthing to work with.

This setup works preety good. The files are playing in order with low latency but if I let the sequencer run for some time and I start swithching notes on and off eventually I manage to hit an assertion in this line
auto* processedData = fileQue[q]->fileBuffer->getReadPointer(channel);
and it lookes like this:

and reading from the immediate window:
channel = 0
fileQue.size() = 2
q = 1
…so I don’t get where this nullptr came from and what does it want.:roll_eyes:


#7

Your second thread can change fileQue.size() and the indices at any time.

One thing would be, to minimise the chance of changing of indices while in the loop by having only one look-up:

for (auto* file : fileQue)
{
    auto* processedData = file->fileBuffer->getReadPointer(channel);
    // ...

But that’s not a perfect solution, since the fileQue still can be altered or an item can be deleted while being used.
There are two solutions to that:

  • create a lock around the part that’s accessing the array and where the array is modified (which will ruin your effort of a second thread)
  • extend the life time of the entries in fileQue, until the sendToOutput is done. This is often done with a ReferenceCountedObject.

Once the sendToOutput got a reference to the item in fileQue, it won’t be deleted. It will be deleted, once the last reference (the Ptr object) goes out of scope.
And these can go into a ReferenceCountedArray, not an OwnedArray (since we don’t want to tie the ownership here).

Have a read into, there will be a caveat, if the audio thread is the last one to have a reference, the deallocation will happen on the audio thread. For that problem there is a pattern of a release pool…


#8

Since you have two threads, you should have critical sections around your sendToOutput function, not just on the array. Since between the time you call fileQue.size() and fileQue[q] the other thread could have changed the array.

	// iterate through the que again. This time remove any file that has reached its endSample
	for (int i = fileQue.size(); --i >= 0;)
	{
		fileToPlay* temp = fileQue[i];
		temp->startSample = temp->startSample + buffer.getNumSamples();
		if (temp->startSample > temp->endSample)
		{
			fileQue.remove(i);
		}
    }

You should always loop backwards when removing items otherwise you will skip items


#9

Ahhhhhhh :bulb:, I always wondered what the point of looping from end to front was!! thanks for clarifying it for me!! I feel enlightened now :smiley: