Macros & Mods System
Architecture Overview
Macros and mods (LFOs, envelopes, random) are modulation sources that can be linked to device parameters. The system has three layers:
- Data layer —
MacroInfo/ModInfoincore/MacroInfo.hppandcore/ModInfo.hpp - UI layer —
MacroPanelComponent/ModsPanelComponentinDeviceSlotComponent - Audio layer —
PluginManager::syncDeviceMacros()/syncDeviceModifiers()wires TE'sMacroParameter/LFOModifierto pluginAutomatableParameters
Key Types
MacroTarget (core/MacroInfo.hpp)
struct MacroTarget {
DeviceId deviceId;
int paramIndex; // Index into plugin's getAutomatableParameters()
};
MacroInfo
struct MacroInfo {
juce::String name;
float value = 0.5f; // Knob position 0..1
std::vector<MacroLink> links; // Each link has a MacroTarget + amount
};
ModTarget (core/ModInfo.hpp)
Same structure as MacroTarget — {deviceId, paramIndex}.
How Macro Linking Works
1. Parameter Discovery
DeviceProcessor::populateParameters()fillsDeviceInfo::parametersTrackManager::updateDeviceParameters()stores themDeviceSlotComponent::getDeviceParamNames()readsdevice_.parametersfor the UI dropdown
2. Link Creation (UI)
- User opens macro panel → selects a target param from dropdown
DeviceSlotComponent::onMacroTargetChangedInternal()→TrackManager::setDeviceMacroTarget()- Stores
MacroTarget{deviceId, paramIndex}inMacroInfo::links
3. Audio-Thread Application (PluginManager::syncDeviceMacros())
MacroInfo.value → te::MacroParameter → param->addModifier(macroParam, link.amount)
- Creates a TE
MacroParameterfor each macro - For each link, finds the target plugin via
syncedDevices_[deviceId].plugin - Gets
plugin->getAutomatableParameters()[paramIndex] - Calls
param->addModifier(*macroParam, link.amount) - TE handles the audio-rate modulation internally
Making a Plugin Macro-Linkable
Requirements
A plugin must have AutomatableParameters registered with TE for macros to link. CachedValues alone won't work — getAutomatableParameters() would return empty.
Pattern: CachedValue + AutomatableParameter (e.g. ArpeggiatorPlugin)
- Register AutomatableParameters in the constructor using
addParam():
gateParam = addParam("gate", "Gate", {0.01f, 1.0f});
gateParam->setParameter(gate.get(), juce::dontSendNotification);
- Sync CachedValue → AutomatableParam when UI changes values:
// Use a ValueTree::Listener to push CachedValue changes to AutomatableParam
state.addListener(¶mSyncListener_);
void syncParamFromProperty(const juce::Identifier& property) {
if (property == ArpIDs::gate && gateParam)
gateParam->setParameter(gate.get(), juce::dontSendNotification);
}
- Read from AutomatableParam in applyToBuffer (includes modulation):
float gateVal = gateParam ? gateParam->getCurrentValue() : gate.get();
- Create a DeviceProcessor in
DeviceProcessor.hpp/.cpp:
class MyProcessor : public DeviceProcessor {
int getParameterCount() const override;
ParameterInfo getParameterInfo(int index) const override;
void populateParameters(DeviceInfo& info) const override;
void setParameterByIndex(int paramIndex, float value);
float getParameterByIndex(int paramIndex) const;
};
- Register in PluginManager::loadDeviceAsPlugin():
processor = std::make_unique<MyProcessor>(device.id, plugin);
Pattern: Native TE Params (e.g. CompressorPlugin, EqualiserPlugin)
These already have AutomatableParameters. Just create a DeviceProcessor subclass that wraps getAutomatableParameters().
Mod (LFO/Envelope) System
Same target structure (ModTarget = {deviceId, paramIndex}), but modulation source is an LFO or envelope modifier instead of a knob.
Key differences from macros:
- Mods are TE
LFOModifierobjects attached toplugin->getModifierList()orrackType->getModifierList() syncDeviceModifiers()handles wiring- LFO retrigger: track-level mods get MIDI via TE's
createModifierNodeForList(), rack-level need explicitaddConnection(rackIOId, 0, modifier->itemID, 0)
UI Visibility
In DeviceSlotComponent:
modButton_/macroButton_visibility controlled inresizedContent(),resizedHeaderExtra(),resizedCollapsed(), andcreateCustomUI()- MIDI devices (
DeviceType::MIDI) typically hide mods; macros can be shown selectively - DrumGrid hides both
Debugging
- Check
device_.parametersis populated (log ingetDeviceParamNames()) - Verify
getAutomatableParameters().size()matches expected param count - Check
syncDeviceMacros()runs after link changes (breakpoint or DBG) - Ensure
paramIndexinMacroTargetmatches the index ingetAutomatableParameters()