Title: Ableton Live crash in large projects after minimal JUCE VST3 program-change UI implementation
Hi everyone,
I made a very small change to a JUCE VST3 plugin to display host-driven program changes in the UI, and now Ableton Live crashes in large projects.
I am not parsing incoming MIDI Program Change events for this feature.
I only use the AudioProcessor program API path.
Environment
- macOS
- Ableton Live
- JUCE plugin, VST3 only
- Started from basic JUCE template
What I changed
- Implemented program support in processor
- Number of programs: 128
- currentProgram stored in atomic int
- setCurrentProgram clamps and stores
- getProgramName returns Program 1..128
- Added processor accessor for UI
- getLastHostProgramChange reads atomic currentProgram
- Added state save and restore
- Save currentProgram in getStateInformation
- Restore currentProgram in setStateInformation
- Updated editor
- Timer at 30 Hz
- Poll processor value
- repaint only if value changed
- Draw current program in UI
Expected behavior
- Stable plugin
- UI updates when host changes program
Actual behavior
- Works in small/simple sessions
- Ableton Live crashes in large projects after this change
Main snippets
Header changes:
class Test2AudioProcessor : public juce::AudioProcessor
{
public:
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;
int getLastHostProgramChange() const noexcept;
private:
static constexpr int kNumPrograms = 128;
std::atomic<int> currentProgram { 0 };
};
Processor program implementation:
int Test2AudioProcessor::getNumPrograms()
{
return kNumPrograms;
}
int Test2AudioProcessor::getCurrentProgram()
{
return currentProgram.load (std::memory_order_relaxed);
}
void Test2AudioProcessor::setCurrentProgram (int index)
{
const auto clamped = juce::jlimit (0, getNumPrograms() - 1, index);
currentProgram.store (clamped, std::memory_order_relaxed);
}
const juce::String Test2AudioProcessor::getProgramName (int index)
{
if (! juce::isPositiveAndBelow (index, getNumPrograms()))
return {};
return "Program " + juce::String (index + 1);
}
void Test2AudioProcessor::changeProgramName (int index, const juce::String& newName)
{
juce::ignoreUnused (index, newName);
}
int Test2AudioProcessor::getLastHostProgramChange() const noexcept
{
return currentProgram.load (std::memory_order_relaxed);
}
State persistence:
void Test2AudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
juce::ValueTree state ("Test2State");
state.setProperty ("currentProgram", getCurrentProgram(), nullptr);
if (auto xml = state.createXml())
copyXmlToBinary (*xml, destData);
}
void Test2AudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
if (auto xml = getXmlFromBinary (data, sizeInBytes))
{
const juce::ValueTree state = juce::ValueTree::fromXml (*xml);
if (state.isValid() && state.hasType ("Test2State"))
setCurrentProgram ((int) state.getProperty ("currentProgram", 0));
}
}
Editor timer and repaint logic:
class Test2AudioProcessorEditor : public juce::AudioProcessorEditor
, private juce::Timer
{
private:
void timerCallback() override;
int lastDisplayedProgram = -1;
};
Test2AudioProcessorEditor::Test2AudioProcessorEditor (Test2AudioProcessor& p)
: AudioProcessorEditor (&p), audioProcessor (p)
{
setSize (400, 300);
startTimerHz (30);
}
void Test2AudioProcessorEditor::timerCallback()
{
const auto newProgram = audioProcessor.getLastHostProgramChange();
if (newProgram != lastDisplayedProgram)
{
lastDisplayedProgram = newProgram;
repaint();
}
}
void Test2AudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (juce::Colours::black);
g.setColour (juce::Colours::darkgrey);
g.setFont (juce::FontOptions (14.0f));
g.drawFittedText ("Host Program Change (VST3)",
getLocalBounds().removeFromTop (44),
juce::Justification::centred,
1);
const auto currentProgram = audioProcessor.getLastHostProgramChange() + 1;
g.setColour (juce::Colours::white);
g.setFont (juce::FontOptions (48.0f));
g.drawFittedText ("Program " + juce::String (currentProgram),
getLocalBounds().reduced (16),
juce::Justification::centred,
1);
}
Questions
- Is this pattern for host program display known to be problematic with Ableton Live VST3?
- Is there a known safer JUCE approach for this in large sessions?
- Could this be related to program-count handling or host lifecycle around setCurrentProgram?
If needed, I can share full crash logs and a minimal repro project.
