Issue: If the Instrument Has More Than 2 Output Channels, Logic Pro Refuses to Scan It
Just spent a shameful amount of time tracking this down...
Logic Pro is quite picky in some scenarios, and multi-out is definitely one of them. It needs a defined bus layout.
If your Cmajor instrument outputs more than 2 channels and then you build the JUCE export on macOS, the AU will work in other hosts but not Logic Pro.
I created this patcher which fixes the issue for me by patching the Cmajor JUCE export file. However, (this may depend on the channel count), the patcher will only work with the "Release" build target. Building in "Debug" still fails to scan. No idea why? It passes auval in both cases. It did scan if I didn't hoist any parameters, because when I do, the load time of the Debug AU would become pretty long in my case, so perhaps Logic just time out whereas other hosts powered through?
In any case, here's the patcher, in case you think this needs fixing and is not just an anomaly of sorts, or if someone else runs into the same problem.
This is a .cmake file.
# CMake script to patch the exported JUCE code for Logic-compatible multi-output layouts
# This patch converts a single large output bus (e.g., 52 channels) into multiple
# stereo output buses (Main + Aux 2..N) and tightens isBusesLayoutSupported so that
# Logic Pro will load the AU instrument with multi-outputs.
# Target file
set(JUCE_PLUGIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/build/cmajor-juce-export/include/cmajor/helpers/cmaj_JUCEPlugin.h")
# Verify file exists
if(NOT EXISTS "${JUCE_PLUGIN_FILE}")
message(FATAL_ERROR "JUCE plugin file not found: ${JUCE_PLUGIN_FILE}")
endif()
# Read file
file(READ "${JUCE_PLUGIN_FILE}" JUCE_PLUGIN_CONTENT)
# If already patched, skip
string(FIND "${JUCE_PLUGIN_CONTENT}" "// PATCHED: Logic multi-output layout" PATCH_ALREADY_APPLIED)
if(NOT PATCH_ALREADY_APPLIED EQUAL -1)
message(STATUS "✅ Logic multi-output patch already applied, skipping...")
return()
endif()
# ──────────────────────────────────────────────────────────────────────────────
# Patch 1: Replace getBusesProperties to construct multiple stereo output buses
# ──────────────────────────────────────────────────────────────────────────────
set(ORIG_GET_BUSES_SIG "static BusesProperties getBusesProperties (const EndpointDetailsList& inputs,\n const EndpointDetailsList& outputs)")
set(NEW_GET_BUSES_IMPL [===[
static BusesProperties getBusesProperties (const EndpointDetailsList& inputs,
const EndpointDetailsList& outputs)
{
// PATCHED: Logic multi-output layout
BusesProperties layout;
uint32_t inputChannelCount = 0, outputChannelCount = 0;
for (auto& input : inputs)
inputChannelCount += input.getNumAudioChannels();
for (auto& output : outputs)
outputChannelCount += output.getNumAudioChannels();
// Inputs: keep simple — add one stereo input bus if we have at least 2 input channels
if (inputChannelCount >= 2)
layout.addBus (true, "Main Input", juce::AudioChannelSet::stereo(), true);
// Outputs:
if (outputChannelCount <= 2)
{
// Stereo (or mono) — single bus named Out
layout.addBus (false, "Out", juce::AudioChannelSet::canonicalChannelSet ((int) outputChannelCount), true);
}
else
{
// Multi-output case: create N stereo buses, first named Out, then Out 2..N
const int numOutputBuses = (int) (outputChannelCount / 2);
for (int i = 0; i < numOutputBuses; ++i)
{
const bool isMain = (i == 0);
const auto name = (i == 0 ? juce::String ("Out")
: juce::String ("Out ") + juce::String (i + 1));
layout.addBus (false, name, juce::AudioChannelSet::stereo(), isMain);
}
}
return layout;
}
]===])
# Replace the original function definition block
# We'll locate the original signature and replace the entire function body using a simple regex-free approach:
# Find start at signature, end at the first line that starts with '}' after the body.
string(FIND "${JUCE_PLUGIN_CONTENT}" "${ORIG_GET_BUSES_SIG}" GET_BUSES_START)
if(GET_BUSES_START EQUAL -1)
message(FATAL_ERROR "Could not find getBusesProperties function signature to patch")
endif()
# Extract before signature
string(SUBSTRING "${JUCE_PLUGIN_CONTENT}" 0 ${GET_BUSES_START} CONTENT_BEFORE)
# Extract from signature to end to locate closing brace
string(SUBSTRING "${JUCE_PLUGIN_CONTENT}" ${GET_BUSES_START} -1 CONTENT_FROM_SIG)
# Find the matching closing brace of the function by locating the first line that begins with '}' after a newline
# Since cmake string processing is limited, we'll heuristically find the first occurrence of '\n }\n' after the signature.
string(FIND "${CONTENT_FROM_SIG}" "\n }" CLOSE_BRACE_POS)
if(CLOSE_BRACE_POS EQUAL -1)
message(FATAL_ERROR "Could not locate end of getBusesProperties function body")
endif()
math(EXPR CLOSE_BRACE_END "${CLOSE_BRACE_POS} + 6") # include '\n }'
string(SUBSTRING "${CONTENT_FROM_SIG}" ${CLOSE_BRACE_END} -1 CONTENT_AFTER_FUNC)
# Rebuild content with new function implementation
set(JUCE_PLUGIN_CONTENT "${CONTENT_BEFORE}${NEW_GET_BUSES_IMPL}${CONTENT_AFTER_FUNC}")
# ──────────────────────────────────────────────────────────────────────────────
# Patch 2: Replace isBusesLayoutSupported with a strict, Logic-friendly version
# ──────────────────────────────────────────────────────────────────────────────
set(ORIG_IS_BUSES_SIG "bool isBusesLayoutSupported (const BusesLayout& layout) const override")
set(NEW_IS_BUSES_IMPL [===[
bool isBusesLayoutSupported (const BusesLayout& layout) const override
{
// PATCHED: Logic multi-output layout
if (! patch->isLoaded())
return true;
// Compute total channels from endpoints
uint32_t inputChannelCount = 0, outputChannelCount = 0;
for (auto& input : patch->getInputEndpoints())
inputChannelCount += input.getNumAudioChannels();
for (auto& output : patch->getOutputEndpoints())
outputChannelCount += output.getNumAudioChannels();
// Expected input buses: 0 or 1 (stereo or disabled)
const int expectedInputBuses = (inputChannelCount >= 2 ? 1 : 0);
if (layout.inputBuses.size() != expectedInputBuses)
return false;
for (auto& ib : layout.inputBuses)
if (ib != juce::AudioChannelSet::stereo() && ! ib.isDisabled())
return false;
// Expected output buses: 1 for <=2 ch, otherwise N = channels/2 (stereo buses)
const int expectedOutputBuses = (outputChannelCount <= 2 ? 1 : (int) (outputChannelCount / 2));
if (layout.outputBuses.size() != expectedOutputBuses)
return false;
for (auto& ob : layout.outputBuses)
if (ob != juce::AudioChannelSet::stereo() && ! ob.isDisabled())
return false;
return true;
}
]===])
# Replace original isBusesLayoutSupported body
string(FIND "${JUCE_PLUGIN_CONTENT}" "${ORIG_IS_BUSES_SIG}" IS_BUSES_START)
if(IS_BUSES_START EQUAL -1)
message(FATAL_ERROR "Could not find isBusesLayoutSupported signature to patch")
endif()
string(SUBSTRING "${JUCE_PLUGIN_CONTENT}" 0 ${IS_BUSES_START} CONTENT_BEFORE_IS)
string(SUBSTRING "${JUCE_PLUGIN_CONTENT}" ${IS_BUSES_START} -1 CONTENT_FROM_IS)
string(FIND "${CONTENT_FROM_IS}" "\n }" IS_CLOSE_BRACE_POS)
if(IS_CLOSE_BRACE_POS EQUAL -1)
message(FATAL_ERROR "Could not locate end of isBusesLayoutSupported body")
endif()
math(EXPR IS_CLOSE_BRACE_END "${IS_CLOSE_BRACE_POS} + 6")
string(SUBSTRING "${CONTENT_FROM_IS}" ${IS_CLOSE_BRACE_END} -1 CONTENT_AFTER_IS)
set(JUCE_PLUGIN_CONTENT "${CONTENT_BEFORE_IS}${NEW_IS_BUSES_IMPL}${CONTENT_AFTER_IS}")
# ──────────────────────────────────────────────────────────────────────────────
# Patch 3: Make getPlaybackParams use total channels across all buses
# ──────────────────────────────────────────────────────────────────────────────
set(ORIG_GET_PARAMS_BODY "return Patch::PlaybackParams (rate, requestedBlockSize,\n static_cast<choc::buffer::ChannelCount> (layout.getMainInputChannels()),\n static_cast<choc::buffer::ChannelCount> (layout.getMainOutputChannels()));")
set(NEW_GET_PARAMS_BODY [===[
{
auto totalIns = 0;
for (auto& ib : layout.inputBuses) totalIns += ib.size();
auto totalOuts = 0;
for (auto& ob : layout.outputBuses) totalOuts += ob.size();
return Patch::PlaybackParams (rate, requestedBlockSize,
static_cast<choc::buffer::ChannelCount> (totalIns),
static_cast<choc::buffer::ChannelCount> (totalOuts));
}
]===])
string(REPLACE "${ORIG_GET_PARAMS_BODY}" "${NEW_GET_PARAMS_BODY}" JUCE_PLUGIN_CONTENT "${JUCE_PLUGIN_CONTENT}")
# Write back patched file
file(WRITE "${JUCE_PLUGIN_FILE}" "${JUCE_PLUGIN_CONTENT}")
message(STATUS "✅ Patched Logic-compatible multi-output layout:")
That sounds grim. I'll take a look.