The Missing Piece of the HISE VI Puzzle: Continuous Per-Event Modulation
-
So, I learned my VI trade developing for Kontakt. And for all its limitations and (previously) slow development pace, Kontakt was and is a pretty good platform. A couple of years ago, Kontakt received an interesting update:
Your events could now carry data! Nothing you couldn't already do yourself by tweaking your data model, but it was a simpler setup and a sign of things to come.
Two updates later, it silently dropped this:
I wrote about redirect_output() a few days ago in the Per-Event Routing thread, and how it allows you to easily reroute a note to a different pair of channels, making it stupidly easy to create, manage and scale large instruments that need multichannel support for any reason.
But the key is the highlighted one: $EVENT_PAR_MOD_VALUE_ID .
When used in set_event_par_arr(), it allowed you to set a modulator from script... for that specific event.
Suddenly, a world of possibilities opened up. Any kind of polyphonic stuff was possible. You could have 1001 different modulator and set each one for each note.
Any crazy envelope or lfo setup? No problem, define your data, set up your modulators in the patch edit mode and do what you please. Per-note continuous EQ and filtering? No issues there. And yes, it's all fully polyphonic, fully continuous, callable up to 1000 times a second with a timer (there's no timer in Kontakt, you make a while loop and call wait() inside), as long as the event is still playing.
In HISE, this is only possible with the script voice start modulator, and that's if you create a custom global object where you'll store data for each event ID and keep it tidy - you can then pull from it using the Message class inside the voice start callback.
Is there any possibility to implement the continuous per-event modulation in HISE? The events having small storage they go with (or there being per-MIDI-processor storage that events can access by ID) which is also then read by a Scriptnode Node?
Synth.updateEventData(eventId, dataslot, double value)
, with the scriptnode node then needing to be assigned to a slot from which it will read? a cable out so it can plug into controls?In an already polyphonic network, every event can now be controlled individually this way.
-
Wouldn't it make more sense to also add a node that writes the data for each event too?
Then it might make sense to attach this to the GlobalRoutingManager class, the functionality of the global cable is almost the same (minus the polyphony but that could be implemented using a per event storage data layout).
-
@Christoph-Hart Ah right, then you could cross modulate between modulators, but like you said, you can already do this with global cables minus polyphony.
Nice.
Here's the MessageCustom "class" that you shared a few months ago after I asked about events carrying data. I tweaked this a bit but the concept is the same
namespace MessageCustom { const var NUM_SLOTS = 512; const var NUM_PER_SLOT = 16; if(!isDefined(MessageCustomObject)) { global MessageCustomObject = []; MessageCustomObject.reserve(512); for(i = 0; i < NUM_SLOTS; i++) { MessageCustomObject[i] = []; MessageCustomObject[i].reserve(NUM_PER_SLOT); } } inline function clear() { MessageCustomObject[Message.getEventId() % NUM_SLOTS].clear(); } inline function clearForId(id) { MessageCustomObject[id % NUM_SLOTS].clear(); } inline function setCustomValue(index, value) { MessageCustomObject[Message.getEventId() % NUM_SLOTS][index] = value; } inline function setCustomValueForId(id, index, value) { MessageCustomObject[id % NUM_SLOTS][index] = value; } inline function getCustomValue(index) { return MessageCustomObject[Message.getEventId() % NUM_SLOTS][index]; } inline function getCustomValueForId(id, index) { return MessageCustomObject[id % NUM_SLOTS][index]; } }
The call is then e.g. after calling Synth.play,
MessageCustom.setCustomValueForId(NoteID.legatoSlide[i], Event.customIndexNote, eventNote);
The trick is just to make sure all is clear for that id by calling clearForId(eventid) before setting anything.
And anywhere else where I can get a an event ID, downstream, modulators etc., all I have to do is include the class in the script and call MessageCustom.get.
This is the example of separate storage that is always accessible that does not put weight on the events like you argued back then. I get that.
-
@aaronventure yes so I would just replicate this system in C++ and add accessor nodes in scriptnode. It‘s not super trivial because of the cross DLL boundary when using compiled networks but I can see the benefit.
Having a floating tile that visualizes the values would be good to for debugging.
But why do you need to clear it? You can just set it to zero manually if you need so, no?
-
@Christoph-Hart Here's one example how I use this right now.
I have script voice start pitch modulators in multiple samplers, and each has its own little logic for setting the correct pitch. Now, I can't do this from the MIDI script itself because the pitch will differ for each event. So it reads from the MessageCustom data object to perform its logic.
All events eventually pass through that pitch modulator. Some might not to have their pitch set by that specific modulator. So I use some of the slots to either flag them or do a simple ifDefined check for the slot inside the pitch modulator itself.
Eventually, the data object will get filled because the event counter will overrun it and the modulo means it'll just overwrite the old array indexes.
But the data in there will stay unless I clear it. So if a new ID is not not the one I want for a specific pitch modulator to play, it either needs to read undefined or it needs to read the value 0, which needs to be set.
So for each event, I can either set all 16 slots, or clear all slots and set only the ones I'm using (so far 10 is max but usually a lot less).
-
But the data in there will stay unless I clear it. So if a new ID is not not the one I want for a specific pitch modulator to play, it either needs to read undefined or it needs to read the value 0,
Yes, but why don't you write the zero for the events that shouldn't apply any pitch? This would be your responsibility with the new system then too and taken off that burden feels like adding Garbage Collection.
-
@Christoph-Hart Since the data object is an array, I found it easier to call MessageCustom.clearForId(eventid) than setting 0 for all the slots I don't use.
Fewer lines of code, cleaner overall.
-
@aaronventure actually I could add another data member to the array element that holds the actual event ID (and not the truncated modulo index), then you can check for equality and return undefined (or zero) without the collision at the wrap around. The performance overhead is neglible.
-
@Christoph-Hart i'm failing to understand the technicality of what you're suggesting; are you saying that clearing wouldn't be necessary as a new ID couldn't read the data that was set for the old ID?
-
@aaronventure yes exactly.
-
@Christoph-Hart perfect
-
@aaronventure Alright, I've written a few tools with this concept:
- a
GlobalRoutingManager.setEventData(eventId, slotIndex, value)
API method where you can set up to 16 double values per event ID and - get them with
GlobalRoutingManager.getEventData(eventId, slotIndex)
(if the data hasn't been set yet it will return undefined.
This basically replicates our existing Javascript helper class but in C++ and globally accessible through the global routing manager. But then it gets interesting:
- a voice start modulator that will query the given data slot and resort to a default value if not set (so you don't need to use the script voice start modulator for just passing on that value to whatever target anymore).
- a
routing.event_data_reader
node that will polyphonically read the event data and send it as scriptnode modulation connection. There are two modes available, one only reads and sends the value when the node processes the incoming note on message (to replicate voice start modulation like behaviour) and the other one listens for changes from the last event ID and outputs a time-varying modulation signal. - a
routing.event_data_writer
node that will write an incoming value into the data slot. This is polyphonic and timevarying, so you can basically send the envelope output from one scriptnode envelope to another (or create the data however you like). The update rate is tied to the buffer size though (because that's how the modules are processed one after another).
I'll clean it up a bit tomorrow and write some docs, but it looks promising.
- a
-
@Christoph-Hart said in The Missing Piece of the HISE VI Puzzle: Continuous Per-Event Modulation:
The update rate is tied to the buffer size though (because that's how the modules are processed one after another).
What does this mean for wrapping stuff up in frame/block nodes? Will it just shout about rate mismatch like the midi node when you try to place it within a block node?
-
@aaronventure no, the update rate is simply the buffer size as this is the rate that the sound generators are able to talk with each other.
-
@Christoph-Hart so on higher buffer sizes, you get lower resolution streams?
Alright, it's then important to highlight it, as this warrants like 20ms smoothing at the lowest for consistency. Can the read nodes have smoothing built in? it'll be necessary anyway, this would just make it less of a mess.
-
You can use Engine.setMaximumBufferSize() to limit the buffer length to 64 or something then you get a guaranteed update rate of 1,5ms if that‘s what you need.
The nodes will not have any smoothing as you can simply add a smoothed parameter node, but the event data envelope (which is a polyphonic HISE envelope that automatically reads out the values from the given slot will have a smoothing parameter (like the LFO).
-
@Christoph-Hart Ah, good to know, I didn't know I can hard limit the buffer size for the plugin.
Yes, smoothing parameter/control is what I meant. Hiding smoothing inside the actual readout would be naughty.
-
@aaronventure Alright, the feature is pushed now (alongside with the documentation for each thing).
I've also added a bunch of snippets to the browser that demonstrate various use cases (Custom Data Event 101/102/103).
Let me know if you find some quirks.
-
Does that solve this request? https://forum.hise.audio/topic/9501/message-event-flags-or-meta-data?_=1716378593963
-
@d-healey yup. You can now write 16 number values to a slot using an event ID and pick it up later either using scripting, a new modulator or nodes in scriptnode.