The Initial Problem: Random pluginval Crashes Randomly
I ran into this while testing a plug-in I am developing in pluginval where I kept getting random, inexplicable crashes in pluginval itself with fairly unhelpful stack traces. These crashes would only happen some of the time, making them seem likely related to a threading issue.
Although I was able to pair things down a bit, my plugin was still too complex to easily rule out various issues so I decided to see if I could reproduce the behavior with a new simple plugin project, but at first I could not!
As I did my best to debug with the unhelpful stack traces from pluginval, I noticed that they all seemed to be related to traversal of AudioProcessorGraph connections. Typically right before the exception was thrown, something like this:
Starting test: pluginval / Editor DPI Awareness...
*** FAILED: VALIDATION CRASHED
0: pluginval: juce::SystemStats::getStackBacktrace + 0x71
1: pluginval: `anonymous namespace'::getCrashLogContents + 0x51
2: pluginval: `anonymous namespace'::handleCrash + 0x13
3: pluginval: juce::handleCrash + 0x14
4: UnhandledExceptionFilter + 0x1ea
5: memset + 0x1b32
6: _C_specific_handler + 0x96
7: _chkstk + 0x11f
8: RtlRaiseException + 0x399
9: KiUserExceptionDispatcher + 0x2e
10: PluginvalTest: juce::HeapBlock<juce::AudioProcessorGraph::Node::Connection,0>::operator juce::AudioProcessorGraph::Node::Connection * + 0xa
Could It Be the AudioProcessorGraph?
My plugin that was crashing pluginval uses an AudioProcessorGraph internally as a member and has a small graph of 8 processors/nodes, 9 including the root AudioProcessorGraph.
So on a haunch I ended up altering the simple plugin test project to minimally replicate the internal use of an AudioProcessorGraph. And as soon as I did that the crash was back!
What I Discovered
If I used the following test setup I was able to reproduce the crash every time:
- an AudioProcessor as a top-level plugin, fairly minimal
- an AudioProcessorGraph that is a member of the top-level plugin, nothing special
- two graph nodes:
- a node for an AudioProcessorGraph::AudioGraphIOProcessor for audio output
- a node for a TestProcessor that is also a dummby AudioProcessor for use as a test graph node
- a single connection between the TestProcessor node and the AudioGraphIOProcessor node
If I instead only use the AudioProcessorGraph and added an AudioGraphIOProcessor node, no connections, no secondary TestProcessor node, I will still get the crash, but only sometimes (threading related?)
Steps to Reproduce
- Create a new plugin project in Projucer.
- Under the project settings I set plugin type to VST3, “Plugin is a Synth”, and use C++17.
- Add new files TestProcessor.cpp and TestProcessor.h
- Copy the source included in this forum post to the respective files: PluginProcessor.h/PluginProcessor.cpp, and TestProcessor.h/TestProcessor.cpp
- Build the plugin project.
- Run pluginval and add the test plugin
- Run tests at strictness 3 or higher
Note: I am building under Windows 10 64 bit, with Visual Studio 16.9.0; I built pluginval from source, and even upgraded its Juce modules to 6.0.7 to match the version I am using to build the plugins that are being tested just in case. I am more or less copying the techniques from: JUCE: Tutorial: Cascading plug-in effects.
Here is the source I am using:
PluginProcessor.h
#pragma once
#include <JuceHeader.h>
#include "TestProcessor.h"
using Node = juce::AudioProcessorGraph::Node;
using AudioGraphIOProcessor = juce::AudioProcessorGraph::AudioGraphIOProcessor;
//==============================================================================
/**
*/
class PluginvalTestAudioProcessor : public juce::AudioProcessor
{
public:
//==============================================================================
PluginvalTestAudioProcessor();
~PluginvalTestAudioProcessor() override;
//==============================================================================
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
#ifndef JucePlugin_PreferredChannelConfigurations
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
#endif
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
//==============================================================================
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override;
//==============================================================================
const juce::String getName() const override;
bool acceptsMidi() const override;
bool producesMidi() const override;
bool isMidiEffect() const override;
double getTailLengthSeconds() const override;
//==============================================================================
int getNumPrograms() override;
int getCurrentProgram() override;
void setCurrentProgram (int index) override;
const juce::String getProgramName (int index) override;
void changeProgramName (int index, const juce::String& newName) override;
//==============================================================================
void getStateInformation (juce::MemoryBlock& destData) override;
void setStateInformation (const void* data, int sizeInBytes) override;
//==============================================================================
void setTestParam(float newValue);
private:
std::unique_ptr<juce::AudioProcessorGraph> graphProcessor;
Node::Ptr audioOutputNode;
Node::Ptr testNode;
void initGraphMinimalTest();
void initGraphBasicTest();
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginvalTestAudioProcessor)
};
PluginProcessor.cpp
#include "PluginProcessor.h"
//==============================================================================
PluginvalTestAudioProcessor::PluginvalTestAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
: AudioProcessor (BusesProperties()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
#endif
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)
#endif
)
#endif
{
}
PluginvalTestAudioProcessor::~PluginvalTestAudioProcessor()
{
}
//==============================================================================
void PluginvalTestAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
auto numInputs = getMainBusNumInputChannels();
auto numOutputs = getMainBusNumOutputChannels();
if (graphProcessor == nullptr) graphProcessor = std::unique_ptr<juce::AudioProcessorGraph>(new juce::AudioProcessorGraph());
graphProcessor->setPlayConfigDetails(numInputs, numOutputs, sampleRate, samplesPerBlock);
graphProcessor->prepareToPlay(sampleRate, samplesPerBlock);
// Pick ONE of the two test methods by uncommenting/commenting,
// The 'basic' test causes the crash everytime, the 'minimal' test has an intermitent crash
//-----------------------------------------------------------------------------------------
initGraphBasicTest(); // crashes every time
//initGraphMinimalTest(); // only crashes some times; requires multiple test runs to get crash
}
void PluginvalTestAudioProcessor::releaseResources()
{
graphProcessor->releaseResources();
}
void PluginvalTestAudioProcessor::initGraphBasicTest()
{
graphProcessor->clear();
audioOutputNode = graphProcessor->addNode(std::make_unique<AudioGraphIOProcessor>(AudioGraphIOProcessor::audioOutputNode));
testNode = graphProcessor->addNode(std::unique_ptr<AudioProcessor>(new TestProcessor()));
for (int channel = 0; channel < 2; ++channel)
{
graphProcessor->addConnection({ { testNode->nodeID, channel }, { audioOutputNode->nodeID, channel } });
}
}
void PluginvalTestAudioProcessor::initGraphMinimalTest()
{
audioOutputNode = graphProcessor->addNode(std::make_unique<AudioGraphIOProcessor>(AudioGraphIOProcessor::audioOutputNode));
}
//==============================================================================
const juce::String PluginvalTestAudioProcessor::getName() const { return JucePlugin_Name; }
bool PluginvalTestAudioProcessor::acceptsMidi() const
{
#if JucePlugin_WantsMidiInput
return true;
#else
return false;
#endif
}
bool PluginvalTestAudioProcessor::producesMidi() const
{
#if JucePlugin_ProducesMidiOutput
return true;
#else
return false;
#endif
}
bool PluginvalTestAudioProcessor::isMidiEffect() const
{
#if JucePlugin_IsMidiEffect
return true;
#else
return false;
#endif
}
double PluginvalTestAudioProcessor::getTailLengthSeconds() const { return 0.0; }
int PluginvalTestAudioProcessor::getNumPrograms() { return 1; }
int PluginvalTestAudioProcessor::getCurrentProgram() { return 0; }
void PluginvalTestAudioProcessor::setCurrentProgram(int index) {}
const juce::String PluginvalTestAudioProcessor::getProgramName (int index) { return {}; }
void PluginvalTestAudioProcessor::changeProgramName(int index, const juce::String& newName) {}
#ifndef JucePlugin_PreferredChannelConfigurations
bool PluginvalTestAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
#if JucePlugin_IsMidiEffect
juce::ignoreUnused (layouts);
return true;
#else
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
#if ! JucePlugin_IsSynth
if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
return false;
#endif
return true;
#endif
}
#endif
void PluginvalTestAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear (i, 0, buffer.getNumSamples());
}
//==============================================================================
bool PluginvalTestAudioProcessor::hasEditor() const { return false; }
juce::AudioProcessorEditor* PluginvalTestAudioProcessor::createEditor() { return nullptr; }
//==============================================================================
void PluginvalTestAudioProcessor::getStateInformation (juce::MemoryBlock& destData) {}
void PluginvalTestAudioProcessor::setStateInformation(const void* data, int sizeInBytes) {}
//==============================================================================
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new PluginvalTestAudioProcessor(); }
TestProcessor.h
#pragma once
#include <JuceHeader.h>
class TestProcessor : public juce::AudioProcessor
{
public:
//==============================================================================
TestProcessor();
~TestProcessor() override;
//==============================================================================
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
bool isBusesLayoutSupported(const BusesLayout& layouts) const override;
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
//==============================================================================
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override;
//==============================================================================
const juce::String getName() const override;
bool acceptsMidi() const override;
bool producesMidi() const override;
bool isMidiEffect() const override;
double getTailLengthSeconds() const override;
//==============================================================================
int getNumPrograms() override;
int getCurrentProgram() override;
void setCurrentProgram(int index) override;
const juce::String getProgramName(int index) override;
void changeProgramName(int index, const juce::String& newName) override;
//==============================================================================
void getStateInformation(juce::MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override;
private:
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TestProcessor)
};
TestProcessor.cpp
#include "TestProcessor.h"
//==============================================================================
TestProcessor::TestProcessor()
: juce::AudioProcessor(BusesProperties()
.withInput("Input", juce::AudioChannelSet::stereo(), true)
.withOutput("Output", juce::AudioChannelSet::stereo(), true)
)
{
}
TestProcessor::~TestProcessor() {}
//==============================================================================
const juce::String TestProcessor::getName() const { return "TestProcessor"; }
bool TestProcessor::acceptsMidi() const { return true; }
bool TestProcessor::producesMidi() const { return true; }
bool TestProcessor::isMidiEffect() const { return false; }
double TestProcessor::getTailLengthSeconds() const { return 0.0; }
int TestProcessor::getNumPrograms() { return 1; }
int TestProcessor::getCurrentProgram() { return 0; }
void TestProcessor::setCurrentProgram(int index) {}
const juce::String TestProcessor::getProgramName(int index) { return {}; }
void TestProcessor::changeProgramName(int index, const juce::String& newName) {}
//==============================================================================
void TestProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) {}
void TestProcessor::releaseResources() {}
bool TestProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const
{
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
return true;
}
void TestProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples());
}
//==============================================================================
bool TestProcessor::hasEditor() const { return false; }
juce::AudioProcessorEditor* TestProcessor::createEditor() { return nullptr; }
//==============================================================================
void TestProcessor::getStateInformation(juce::MemoryBlock& destData) {}
void TestProcessor::setStateInformation(const void* data, int sizeInBytes) {}