Safely delete objects used in processBlock


#1

I'm a little lost, I want to remove an object used in the audio callback from other trhead, but if I delete it and the object is currently processed by the audio callback, then I get an EXC_BAD_ACCESS (because the it points to nullptr)

Oscilloscope is a Component, it has Channels that read samples and then generates a Path. Oscilloscope access to Channels to get the paths to paint().

void Oscilloscope::Channel::preProcess (AudioBuffer <float> *buffer) noexcept
{
    if (! owner->isProcessing())
        return;

    if (buffer->getNumChannels() > 0)
    {
        buffer->clear();
        float* channelData = buffer->getWritePointer (0);

        for (int i = 0; i < buffer->getNumSamples(); ++i)
        {
            waveGenerator->process (&channelData[i]);
            channelData[i] *= owner->inputGain;
        }
    }
}

WaveGenerator is an abstract class for SineWaveGenerator, SquareWaveGenerator, etc. To change the waveGenerator type (waveGenerator is a ScopedPointer):

void Oscilloscope::Channel::setWaveGenerator (double frequency, int numWaves, WaveGeneratorType waveGeneratorType)
{
    waveGenerator = nullptr;
    if (waveGeneratorType == WaveGeneratorType::Sine
        waveGenerator = new SineWaveGenerator();
    else if (waveGeneratorType == WaveGeneratorType::Square)
        waveGenerator = new SquareWaveGenerator();
    //...etc...
    
    waveGenerator->prepareToPlay (owner->sampleRate);
    int roundedNumSamples = std::round (owner->sampleRate / frequency);
    double frequencyHz = owner->sampleRate / roundedNumSamples;

    fifoBuffer.resize (roundedNumSamples * numWaves);

    waveGenerator->set (frequencyHz);

 

preProcess is called by an AudioAppComponent class (Oscilloscope : Analizer : Component)

void MainContentComponent::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)

{
    bufferToFill.clearActiveBufferRegion();
    AudioBuffer <float> buffer (2, bufferToFill.buffer->getNumSamples());
    buffer.clear();

    if (analizer != nullptr)
        analizer->preProcess (&buffer);

    for (int i=0; i < filterManagers.size(); i++)
        filterManagers[i]->processBlock (buffer);

    if (analizer != nullptr)
        analizer->process (&buffer);
}

 

Any help is appreciated


#2

If I understand you correctly you want to switch the waveGenerator.

First idea instead of deleting and then (after some time) create a new waveGenerator, you could save the pointer, create a new waveGenerator and then delete the old waveGenerator:


void Oscilloscope::Channel::setWaveGenerator (double frequency, int numWaves, WaveGeneratorType waveGeneratorType) 
{ 
    WaveGenerator* oldGenerator = waveGenerator;
    /* waveGenerator = nullptr; */
    if (waveGeneratorType == WaveGeneratorType::Sine)
        waveGenerator = new SineWaveGenerator(); 
    else if (waveGeneratorType == WaveGeneratorType::Square) 
        waveGenerator = new SquareWaveGenerator(); 
    //...etc... waveGenerator->prepareToPlay (owner->sampleRate);
    delete oldGenerator; 

    int roundedNumSamples = std::round (owner->sampleRate / frequency); 
    double frequencyHz = owner->sampleRate / roundedNumSamples; 
    fifoBuffer.resize (roundedNumSamples * numWaves); 
    waveGenerator->set (frequencyHz);

But that does not help, if the object is deleted while the pointer in getNextAudioBlock is used...

You can solve this by using a ReferenceCountedObject: http://www.juce.com/doc/classReferenceCountedObject

When the object is referenced in getNextAudioBlock, the old waveGenerator stays alive until the reference goes out of scope and so the reference count drops to zero.

 

 

 

I haven't used JUCE's ReferenceCountedObject yet, but as a design approach it should work.

 


#3

Thanks for your response, 

I used ReferenceCountedObject for other purposes in other project, I can not imagine a practical solution with it. Yes, I can check if counter is zero, then delete itself, but if in elsewhere I've created a new instance of WaveGenerator, it will not work, as it counts instances and references.

I imagine other ways, such as using flags: waveGenerator::remove() {removeFlag = true}, then in the processblock call to: waveGenerator::needsDeletion() {if (removeFlag==true) delete this;}, but I do not think it's a smart way to do it...

 


#4

You don't delete the object, it will be deleted automatically, if there is no reference left.
I add some pseudo / untested code:

class WaveGenerator : public ReferenceCountedObject
{
    void processBlock();
    typedef ReferenceCountedObjectPtr<WaveGenerator> Ptr;
};

class MainContentComponent {
    // [...]

    void setWaveGenerator (double frequency, int numWaves, WaveGeneratorType waveGeneratorType) {
        if (waveGeneratorType == WaveGeneratorType::Sine) 
            waveGenerator = new SineWaveGenerator(); 
        else if (waveGeneratorType == WaveGeneratorType::Square) 
            waveGenerator = new SquareWaveGenerator();
        // no need to delete the old object, it is deleted automatically if no other reference is held

        // [...]
    }

    WaveGenerator::Ptr getWaveGeneratorPtr() {
        return waveGenerator;
    }

    void MainContentComponent::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
    {
        // this reference keeps our generator from beeing deleted, until wave goes out of scope
        WaveGenerator::Ptr wave = getWaveGeneratorPtr();

        // to be perfectly save, if you have no preselected object
        if (wave) {
            wave->processBlock();
        }
    }


private:
    WaveGenerator::Ptr waveGenerator;
};

The reference count in the getNextAudioBlock scope is exact the flag you thought of...

Hope this helps

 


#5

I think I've confused about ReferenceCountedObject, I'll take a look again to this class.
I will try your example, Thanks!
 


#6

Won't that go out of scope at the end of getNextAudioBlock and cause a deallocation on the audio thread? Perhaps consider the ReleasePool Timur proposes in his [url=https://github.com/CppCon/CppCon2015/raw/master/Presentations/C%2B%2B%20In%20the%20Audio%20Industry/C%2B%2B%20In%20the%20Audio%20Industry%20-%20Timur%20Doumler%20-%20CppCon%202015.pdf]CppCon presentation[/url] (pp 67-70).

 


#7

Yes, the reference goes out of scope, and if there was no change in the object which holds the second reference, nothing happens.

But you are right, if deconstruction is problematic in terms of timing (release is much less problematic than allocating, right?), then you need something else, like a third reference (probably in the ReleasePool, I forgot the details of Timur's proposal) and asynchronous checks, if this is the only one holding a reference and dropping it in that case.

If WaveGenerator is not a huge objects with much of data in it, the cost of building and maintaining the release pool might eventually be saved.

But the ReleasePool is a good idea for that, didn't think of it...


#8

Yes,  +1 to Andrew's comment, the asynchronous ReleasePool is the solution!

Hopefully I'll have the time to add a JUCE-ified version of that ReleasePool to JUCE at some point.