Macros & Mods System
SkillDev toolsGuide 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Macros & Mods System skill
What this skill tells your AI
The instructions your AI receives, as published by conceptual-machines/magda-core in .claude/skills/macros-mods/SKILL.md and read by ahel’s review.
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()
Signals
- GitHub stars
- 206
- Forks
- 23
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
macros-mods- Source
- github.com/conceptual-machines/magda-core