Skip to content

Fix crash loading Audio Units with side-chain input buses - #521

Open
RustamKuramshin wants to merge 1 commit into
spotify:masterfrom
RustamKuramshin:fix/au-sidechain-process-buffer
Open

RustamKuramshin wants to merge 1 commit into
spotify:masterfrom
RustamKuramshin:fix/au-sidechain-process-buffer

Conversation

@RustamKuramshin

@RustamKuramshin RustamKuramshin commented Oct 17, 2026 •

Copy link
Copy Markdown

Problem

Loading any Audio Unit that exposes a side-chain (or any extra) input bus crashed with SIGSEGV inside the plugin constructor on macOS. Reproduced with FabFilter Pro-C 2, TDR Kotelnikov GE, Softube FET Compressor Mk II, UAD 1176LN and Pulsar P11 Abyss (.component), all with pedalboard 0.9.26 from PyPI; the same plugins load fine as VST3.

from pedalboard import load_plugin
load_plugin("/Library/Audio/Plug-Ins/Components/TDR Kotelnikov GE.component")
# Fatal Python error: Segmentation fault

Cause

ExternalPlugin::process() builds the AudioBuffer handed to processBlock() with getTotalNumOutputChannels() channels. Plugins with extra input buses have more total input channels than that, and JUCE's AudioUnitPluginInstance::renderGetInput() locates every input bus by its channel offset inside this very buffer (getBusBuffer(*currentBuffer, true, busIndex)). For a stereo effect with a stereo side-chain the AU asks for bus 1 at offset 2, the buffer has only two channels, AudioBuffer::allocateChannels reads null pointers past its end and the plugin's render callback does memmove to address 0.

Backtrace (debug build):

juce::AudioBuffer<float>::allocateChannels          juce_AudioSampleBuffer.h:1273  (jassert dataToReferTo[i] != nullptr)
juce::AudioProcessor::getBusBuffer                   juce_AudioProcessor.h:649
juce::AudioUnitPluginInstance::renderGetInput        juce_AudioUnitPluginFormat.mm:1821 (busIndex=1, channelOffset=2)
<plugin> -> AudioUnitRender
juce::AudioUnitPluginInstance::processAudio          juce_AudioUnitPluginFormat.mm:1090
Pedalboard::ExternalPlugin<AudioUnitPluginFormat>::process         ExternalPlugin.h:1213
Pedalboard::ExternalPlugin<AudioUnitPluginFormat>::detectReloadType  ExternalPlugin.h:1038
Pedalboard::ExternalPlugin<AudioUnitPluginFormat>::reinstantiatePlugin

The VST3 wrapper in JUCE checks bus bounds itself, which is why only AU crashed.

Fix

Allocate max(getTotalNumInputChannels(), getTotalNumOutputChannels()) channels. The extra channels are zero-filled scratch, exactly like the extra output channels already were. Behaviour for plugins without extra buses is unchanged.

Verification

Debug build on macOS 26.7 / arm64 / Python 3.13: the five AUs above and their VST3 counterparts all load and process audio, with identical reported latency and output level between the AU and VST3 build of each plugin. A guard inside JUCE's renderGetInput was tried as well and turned out to be unnecessary once the buffer has the right size, so the JUCE submodule is untouched.

ExternalPlugin::process() built the AudioBuffer handed to processBlock()
with getTotalNumOutputChannels() channels. Plugins with extra input buses
(e.g. a side-chain input) have more total input channels than that, and
JUCE's AudioUnitPluginInstance::renderGetInput() locates each input bus by
its channel offset inside this very buffer. For a stereo effect with a
stereo side-chain the AU asks for bus 1 at offset 2, the buffer has only
two channels, and the host reads past its end: null channel pointers lead
to a memmove on address 0 and a SIGSEGV inside the plugin constructor
(detectReloadType). Every AudioUnit with a side-chain bus crashed on load
(FabFilter Pro-C 2, TDR Kotelnikov GE, Softube FET Compressor, UAD 1176,
Pulsar P11 ...).

Allocate max(total inputs, total outputs) channels instead; the extra
channels are zero-filled scratch, exactly like the extra output channels
already were. VST3 hosting is unaffected (the VST3 wrapper checks bus
bounds itself).
@RustamKuramshin
RustamKuramshin force-pushed the fix/au-sidechain-process-buffer branch from a34d97f to dad135a Compare October 17, 2026 22:28

@Manishram-ai Manishram-ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches JUCE's docs for processBlock, which say the buffer needs at least as many channels as the larger of total inputs and total outputs, including side-chain buses. Before this, a plugin with a side-chain input got a buffer sized to its outputs only. The extra channels now get the zeroed dummy buffers from the loop below, so the side-chain just sees silence. I couldn't test it without one of those plugins, but the change looks right to me.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants