| name | macros-mods |
| description | Guide for implementing and debugging macro and mod (LFO/envelope) linking on device parameters. Use when adding macro/mod support to a plugin, fixing macro link issues, or understanding the modulation system. |
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/ModInfo in core/MacroInfo.hpp and core/ModInfo.hpp
- UI layer —
MacroPanelComponent/ModsPanelComponent in DeviceSlotComponent
- Audio layer —
PluginManager::syncDeviceMacros()/syncDeviceModifiers() wires TE's MacroParameter/LFOModifier to plugin AutomatableParameters
Key Types
MacroTarget (core/MacroInfo.hpp)
struct MacroTarget {
DeviceId deviceId;
int paramIndex;
};
MacroInfo
struct MacroInfo {
juce::String name;
float value = 0.5f;
std::vector<MacroLink> links;
};
ModTarget (core/ModInfo.hpp)
Same structure as MacroTarget — {deviceId, paramIndex}.
How Macro Linking Works
1. Parameter Discovery
DeviceProcessor::populateParameters() fills DeviceInfo::parameters
TrackManager::updateDeviceParameters() stores them
DeviceSlotComponent::getDeviceParamNames() reads device_.parameters for 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} in MacroInfo::links
3. Audio-Thread Application (PluginManager::syncDeviceMacros())
MacroInfo.value → te::MacroParameter → param->addModifier(macroParam, link.amount)
- Creates a TE
MacroParameter for 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:
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
LFOModifier objects attached to plugin->getModifierList() or rackType->getModifierList()
syncDeviceModifiers() handles wiring
- LFO retrigger: track-level mods get MIDI via TE's
createModifierNodeForList(), rack-level need explicit addConnection(rackIOId, 0, modifier->itemID, 0)
UI Visibility
In DeviceSlotComponent:
modButton_ / macroButton_ visibility controlled in resizedContent(), resizedHeaderExtra(), resizedCollapsed(), and createCustomUI()
- MIDI devices (
DeviceType::MIDI) typically hide mods; macros can be shown selectively
- DrumGrid hides both
Debugging
- Check
device_.parameters is populated (log in getDeviceParamNames())
- Verify
getAutomatableParameters().size() matches expected param count
- Check
syncDeviceMacros() runs after link changes (breakpoint or DBG)
- Ensure
paramIndex in MacroTarget matches the index in getAutomatableParameters()