Saving MIDI CC assignments in user presets?
-
I'm still hitting some bugs with this.
@Christoph-Hart I think you should take a look into this because it is a very good point.
So far I haven't really ran into this situation because most of my projects only have one or two presets. But I'm developing a project at the moment that might have 100 or more presets and this will definitely be an issue.
If a user has a set of CCs they like to work with then they likely want to set it up once for the entire instrument (all presets past, present, and future) and forget about it. They don't want to have to redo it each time they add an instance of the plugin.
With this instrument wide setup I don't see a reason to distinguish between daw load or user preset because the source of truth will be a separate file that can be read in both situations.
I'm currently trying to implement a working solution through scripting but I'm running into annoying issues with
setUpdateCallbackbecause it fires at init - it fires multiple times when using full instrument expansions. I'm gating it currently with flag variables but still haven't got it working reliably yet, and the code is a tangle.-- Should also include macros and MPE assignments too.
-
@David-Healey I have it set up like this. Works for me so far, but it's one of those things with a lot of use cases.
The only scripting I have is to load and save the MidiMappings.json file. The preprocessor handles the rest.
I had Claude write this (hence the em-dashes
), to catch all the states:The three persistence layers
- Plugin state (the DAW session) — per HISE default, every instance's current CC assignments are serialized into the plugin state chunk the host saves with the session. This is the day-to-day layer and it's completely automatic.
- User presets — new preprocessor
HISE_MIDI_AUTOMATION_IN_USER_PRESETS=0in project_info.xml ExtraDefinitions means presets neither store nor restore mappings. Switching or saving presets never touches CC assignments. - Default map file —
AppData/MidiMappings.json, handled byScripts/MidiMappings.jsviaEngine.createMidiAutomationHandler(). Only ever touched by the two buttons in the MIDI Control panel ("Save As Default Map" / "Load Default Map"). Nothing reads or writes it automatically — there is no auto-load inonInitor anywhere else.
Use cases
First launch after installation — No plugin state exists and
MidiMappings.jsondoesn't exist yet. The instance starts with zero CC mappings. If the user clicks "Load Default Map" at this point,loadDefault()returnsfalse(no file) and the button flashes "No map found"; nothing changes.New instance in a fresh DAW session (plugin already in use on this machine) — Same as above from the plugin's perspective: a fresh instance has no state, so it starts with no mappings, even if a default map file exists. The default map is opt-in per instance — the user clicks "Load Default Map" to apply it. This is the intentional trade-off of the design: no surprise mappings, at the cost of one click per new instance.
Reopening a DAW session where mappings were set — The host hands back the saved plugin state, and the exact assignments from when the session was saved are restored per instance. The default map file is not consulted, so it can't overwrite or drift from what the session had.
Reopening a DAW session where no mappings were set — The restored state contains an empty assignment set and the instance stays empty. There's no fallback to the default map file; saved state is authoritative.
Loading / switching user presets (any time) — No effect on mappings in either direction, thanks to the
HISE_MIDI_AUTOMATION_IN_USER_PRESETS=0flag. A preset saved while mappings existed doesn't carry them, and loading one doesn't clear them.Clicking "Save As Default Map" — Writes the current instance's entire assignment set to
AppData/MidiMappings.json(temp-file + atomic rename, so a failed write can't corrupt an existing map). Flashes "Saved!" or "Error!". Attempting to save with zero assignments shows "No mappings to save" and doesn't save an empty default map.Clicking "Load Default Map" — Replaces all current assignments in that instance with the file's set. If the file is missing or doesn't parse (hand-edited/corrupt JSON), current assignments are left untouched and the button flashes "No valid map found". A successful load gives no flash; the MIDI learn table updating is the feedback.
Multiple instances in one session — Each instance's mappings are fully independent (per-instance state). The only cross-instance channel is the default map file, and only via explicit save in one instance and explicit load in another. Changes in one instance never propagate on their own.
One subtlety worth remembering: since the default map is per-machine AppData, it's shared across all DAWs and sessions on that machine, but versioned by nothing — the last "Save As Default Map" click from any instance wins. There is no "Save over existing map?" kind of confirmation.
Also note that the
HISE_MIDI_AUTOMATION_IN_USER_PRESETSpreprocessor I added is unlikely to get merged, per Christoph's responses earlier in this post. -
@dannytaurus said in Saving MIDI CC assignments in user presets?:
Also note that the HISE_MIDI_AUTOMATION_IN_USER_PRESETS preprocessor I added is unlikely to get merged, per Christoph's responses earlier in this post.
Yes I'm hoping Christoph will find a solution that is mergable, although I don't see why yours shouldn't be.
-
@David-Healey I think maybe the
HISE_MIDI_AUTOMATION_IN_USER_PRESETSpreprocessor is more mergeable than theHISE_MIDI_AUTOMATION_IN_PLUGIN_STATE?The system might be built around a certain state that has repercussions when it changes.
For the
USER_PRESETone, on preset save theMidiAutomationstate manager is skipped, so the preset file gets noMidiAutomationnode at all and on preset load theMidiAutomationrestore step is skipped entirely, so nothing is overwritten in the UI.The mappings are still persisted in the DAW session chunk and embedded plugin data.
So maybe that change to the preset format - the missing
MidiAutomationnode - could cause some issues? -
In hindsight, I think adding the
HISE_MIDI_AUTOMATION_IN_PLUGIN_STATEpreprocessor in the same PR asHISE_MIDI_AUTOMATION_IN_USER_PRESETSwas a mistake.
I don't think I'll ever use it, since
USER_PRESETSand some simple save/load MidiMappings.json in script gets me exactly what I need.The
PLUGIN_STATEis more complex and messes with the state of the plugin instance, which I think is Christoph's main objection. -
@dannytaurus I'm getting a bit overloaded here following what each part does.
I want everything to be in an external file (midi/macros/mpe). If the file doesn't exist then create it based on the values stored in the preset.
When the plugin loads, the daw session loads, the user changes preset, I want the same values from the external file to be used.
Which combination of your preprocessor definitions should I be using to achieve this?
-
@David-Healey Sounds like you might want the truly global behaviour that I ultimately decided against.
If you enable both preprocessors (by setting them to
0since1is default and current behaviour) :HISE_MIDI_AUTOMATION_IN_USER_PRESETS=0 HISE_MIDI_AUTOMATION_IN_PLUGIN_STATE=0then NO mappings will be saved per-preset, NO mappings will be loaded from a saved DAW session state, and NO mappings will be loaded when the user changes presets.
Every preset saved by the user will have zero mappings.
Every instance of the plugin freshly loaded in a new DAW will have zero mappings.
Every instance of the plugin loaded from a saved DAW session will have zero mappings.
Every additional instance of the plugin loaded into a DAW will have zero mappings.Then all you have to do is wire up your global mappings file to the plugin-load and preset-load callbacks.
I decided against this approach because:
- This will overwrite a user's saved custom mappings with global when a saved DAW session is reloaded
- When loading a fresh instance of the plugin, the global mappings will load, which might not be what the user wants
- When changing presets, the global mappings will be loaded, which might not be what the user wants.
My take on those situations is:
- Load whatever mappings are in the saved DAW session because that's how the user saved them
- If the user wants to load mappings in a new instance of the plugin, it's just one "Load Default Mappings" button press
- When changing presets, I want whatever mappings are already there to stay there and not switch to the global ones.
I hope all that makes sense. I was genuinely trying to answer as briefly as possible, but there's a lot going on!

-
@David-Healey By the way - you might have to extend my PR to include macros and MPE in the saved data. I'm only accounting for MIDI CC mappings because I haven't down any thing with macros or MPE in my plugins yet.
-
@dannytaurus Aha I don't think that will help me then. The saving/loading I can do already, and I like having the fallback in the preset. The issue I have is that the updateCallbacks trigger multiple times and overwrite the data I've saved. I'll keep trying.
-
@David-Healey Yeah, that's why I wanted a solution that didn't involve any plugin/preset callbacks at all.
Just wanted zero mappings saved in the presets, and a nice clean
MidiMappings.jssave/load script. -
@dannytaurus I decided to see what Claude would do with the problem. It's a massive commit so I'm not going to rely on it, but I'll leave it here as it might be a starting point for Christoph - or who knows, maybe it's perfect!
https://github.com/davidhealey/HISE/commit/adc0826307163d1e8781d8b024a2090233dfe0c9