MIDI Processor Inconsistent/Processing Speed Issue?
-
@Christoph-Hart This works really well. I thought about a global array but couldn't think of a good way to be getting around the memory issue. Are event IDs just incremental ints for each new note on? If so, modulo is the obvious solution there, thanks. 512 is plenty indeed.
Copying this thing into an external script and including it everywhere you want to use it is the simplest implementation.
This is so incredibly useful you should pin it somewhere.
I added two more functions:
inline function setCustomValueForId(id, index, value) { GLOBAL_STORAGE[id % NUM_SLOTS][index] = value; } inline function getCustomValueForId(id, index) { return GLOBAL_STORAGE[id % NUM_SLOTS][index]; }
-
Are event IDs just incremental ints for each new note on
To be fully precise, they are unsigned 16 bit integers, so they go from 0 to 65536 (they are part of the message object so every bit saved is a good bit).
Now what happens if you play more than 65536 notes? Either Y2K will happen 24 years too late, or it starts at zero again :)
This is so incredibly useful you should pin it somewhere.
Might be a good spot here:
https://docs.hise.audio/tutorials/recipes/event-processing/index.html
-
This solves the same timestamp issue but there was a still issue with two notes being played one next to another with a time interval big enough so as to avoid having two same timestamps, but small enough to be under the humanization window. This would cause ID overwrites and checking became problematic. I think I have accounted for all cases but will continue testing.
You may be thinking that this is highly specific to my project and, that's true. Any other complex project will have its own ultimate solution to prevent note hangings depending on how it's handling note ons and its usage of Synth.play().
Note hanging is in 99% of cases a timing issue where a noteOff doesn't execute on the proper event. For systems where you can pause code execution (Kontakt) this is trivial and a simple check can prevent this, once you're aware of how it all works (and many devs still aren't because there isn't a concrete example in the manual or a PSA on "How to avoid note hanging once and for all"). In HISE, where you have this queue-like thing (code executes and the note gets stored into the queue, even though conditions may change by the time it's supposed to play), it can get interesting.
So I'll propose a method for dealing with any complex situations where note hangings might occur without having to test for and eliminate all possible edge cases as well as having to create complex hierarchical setups where refactoring and keeping track of things becomes troublesome.
Synth.playNoteWithStartOffsetIf (channel, number, velocity, offset, condition). Whenever the time for execution comes due to possible usage of note delaying, the note will only play if the condition is true. Usually that condition would be to check whether a key is still being held, but could be anything else like checking if a variable (or array) still contains its ID etc.)
Let me know if you think this makes any sense.
-
@aaronventure would a
Synth.cancelNote(eventId)
function also work? You can call this in your note off callback and it will then look in all queued upcoming events and remove the ones with matching event ID.function onNoteOn() { Message.delayEvent(44100); } function onNoteOff() { // if you play a note < 1 second, this will cancel the future event. Synth.cancelNote(Message.getEventId()); }
This is easier to implement and I don't have to drag that condition function around (the
condition
parameter in your suggestion must be a function object so if we try storing this from a realtime thread, our friend the AudioThreadGuard says no. -
@Christoph-Hart said in MIDI Processor Inconsistent/Processing Speed Issue?:
(the condition parameter in your suggestion must be a function object so if we try storing this from a realtime thread, our friend the AudioThreadGuard says no.
Can't it just take true/false like a bunch of other methods? I mean e.g. Message.ignoreEvent(bool) takes a boolean, this should be the same (I have no clue how it works it the backend so I'm just throwing stuff at the wall.
The thing with
Synth.cancelNote(eventId)
is you again have to query various conditions. What's the physical note that caused this ID to be triggered? It's not necessarily the one in Message.getNoteNumber(). So here we go storing data in our custom array and handing it down the line again, while writing logic for the edge cases. It's barely a step less than adding another layer in hierarchy and culling the events at the lowermost script processor (which is what I'm doing now).The idea with
Synth.playNoteWithStartOffsetIf()
was that you write the condition for the note to start and then you just continue writing your algorithms. The conditions for the note to start after already scheduling it will in most cases only be about whether a certain key is pressed down (because the assumption is that you're already doing all the logic before calling Synth.play() in the first place).The idea of it is to have one last safety check before the note fires off at a point where you have no control over it (the code after calling the method gets executed anyway, but the playNote gets delayed with Message.delay() ). It would be the endgame anti-hang solution so that each processor doesn't need its own child processor with project-specific culling logic.
Basically a "go to that house, knock on the door and if no one's home, come back" kinda thing, give the order and forget about it.
I can understand if this is somehow hard to implement. I don't need this immediately today or tomorrow, I can deal with complex shit because I'm masochistic like that (I'm here after all, aren't I?). But long term it would be a good feature because it enables rock-solid note management for any project that has any kind of time-specific note management (I remember Mike on VI saying he gave up on HISE because it didn't have wait() like Kontakt -- my opinion is that you can achieve everything with Message.delay() but the queue introduces complexity to follow-up logic both because of event inheritance, script processor execution order and (the lack of, for noobs like me until today) custom event information, whereas in Kontakt you're just thinking in a linear timeline, for better or worse.
-
The condition is supposed to be evaluated just before the note is started and not when you call this method, hence it needs a callable object that performs a check. Just passing in a variable will evaluate it and then store the value from the time you call it which renders it completely useless (think an scripting equivalent of the useless machines you can find on Youtube).
I still think that the cancelNote function is a valuable tool for the stuck notes problem and is definitely a good addition, so I‘ll add it regardless of whether it‘s the solution for this particular problem.
In C++ there is a paradigm called RAII which comes down to „whoever created something is responsible for cleaning it up“ and this principle can be applied for most scenarios regarding stuck notes. So in your case where you start 5 artificial events with a short randomized delay on each noteOn all you need to do is to store the event id of the original note that triggered those and then call the cancelNote function in the noteoff callback. I don‘t think that the implementation is more complex than with the function you suggest, there you would have to query the „activeness“ of the note that started it which would have require similar bookkeeping.
But I agree about the attractiveness of having a wait function and it has been one of the first things I tried to hack into the JS engine back in 2013/2014 when I started the HISE development. The problem is that I can‘t suspend the function execution and then pick it up with the exact state for each variable but since my brain has grown since 2014 maybe I find a smart solution when I think about it now…
-
ok, hear me out: forget everything I wrote above. What if I simply solve that problem at the root so you don't have to do anything?
For each note off I just need to iterate the queue of pending note ons and remove the ones that match the event ID and have a bigger timestamp (the second condition is important because it allows super fast notes where a note-on and note-off occurs within the same buffer). This removes basically every scenario where a script creates a note-on that is scheduled after its note-off.
It comes with a tiny performance overhead because it has to do a nested loop (once for the incoming notes and once for each queued event), so while it has a theoretical complexity of
O(n^2)
, it most likely won't matter too much because the number of events can be expected to stay reasonably low. On the other hand I could just disable this functionality by default and add a API callSynth.setCancelFutureNotes(true)
that enables this for each sound generator that has a script fiddling with note delays (so all you need to do is to call this once in each onInit callback of a script that is about to mess with the event timestamps). -
@Christoph-Hart Alright, that sounds even better than what I proposed.
So it would check which noteOfById calls were made and remove these from the queue, if I got that right?
And the enabling call sounds good without adding overhead by default.
@Christoph-Hart said in MIDI Processor Inconsistent/Processing Speed Issue?:
But I agree about the attractiveness of having a wait function and it has been one of the first things I tried to hack into the JS engine back in 2013/2014 when I started the HISE development. The problem is that I can‘t suspend the function execution and then pick it up with the exact state for each variable but since my brain has grown since 2014 maybe I find a smart solution when I think about it now…
Yeah it does have some cool uses too, some of which are like a cult secret. You can dynamically create timers by having a while loop with wait() in each iteration which puts a callback on hold until conditions are met. Once you realize that playing an artificial note and immediately doing a noteOff for its ID in the very next line creates a release callback with that same ID, you effectively get the option of infinite dynamic async callbacks. You can then call stopWait() with callback/event IDs and cancel the waiting elsewhere, etc.
Other times it can get a bit messy but with great power comes great responsibility.
-
So it would check which noteOfById calls were made and remove these from the queue, if I got that right?
Yes. Whenever a note-off message is about to be processed, it looks in the queue if there is a pending note on with a bigger timestamp and then just removes that so it won't happen. Whether the note-off was created artificially or is an actual key event doesn't matter.
I've pushed the change (without the new API call so it's now enabled by default). Feel free to check if that solves your issues.
-
@Christoph-Hart Alright, some observations.
So this new implementation that you added requires a specific eventID storage paradigm if you're ignoring the physical events and creating your own.
Due to the possibility of two notes with the same timestamp resulting in two noteOns happening before noteOffs, this will not prevent note hanging if you're overwriting note IDs is any way other than for the same note number.
So you NEED per-note ID storage, even for monophonic instruments.
If doing MPE, it's gonna require another dimension for the channels in case of multiple cases of same note number on different channels.
-
@aaronventure well if you spawn off multiple events from a single note then you definitely have to keep track of them and call note-off in the respective note-off callback of the message. I don't think this is a design flaw, but just a reasonable bookkeeping task (again, think of it as RAII: the one who created it (key down) is responsible for the cleanup (key up).
I can offer to add a few more data containers that make this a bit more convenient, but there are a few of those already:
https://docs.hise.audio/scripting/scripting-api/midilist/index.html
https://docs.hise.audio/scripting/scripting-api/unorderedstack/index.htmlWhat this new method brings to the table is that you don't have to care about the order of on / off messages anymore due to possible timestamp manipulations, as long as the event ID matches you're fine now - I agree that this is messy and should not be the burden of the script developer.
However I realized that this didn't apply to artificial events (because they are not processed by the safe check as it was). I've extended the robustness of this method (it performs this check now at the API call that adds the note-off) and now I think it should be fine.
I've added the docs including an example that should demonstrate the "proper" way of handling artificial note creation.
- https://docs.hise.audio/scripting/scripting-api/synth/index.html#setfixnoteonafternoteoff
- https://docs.hise.audio/scripting/scripting-api/message/index.html#delayevent
- https://docs.hise.audio/tutorials/recipes/event-processing/index.html#play-chords-from-single-notes
Now if that is still not convenient for you, I think the next stop would be an API call that automatically handles this logic. I vaguely remember that you could pass in
-1
somewhere in KSP and it would "tie" the note to the note off for the current note-on message and the HiseScript equivalent would be:// adds a note on that will be automatically stopped at the note-off message with the given source event ID Synth.attachNote(int sourceEventId, int noteNumber, int velocity, int timestamp);
-
@Christoph-Hart said in MIDI Processor Inconsistent/Processing Speed Issue?:
Now if that is still not convenient for you, I think the next stop would be an API call that automatically handles this logic. I vaguely remember that you could pass in
-1
somewhere in KSP and it would "tie" the note to the note off for the current note-on message and the HiseScript equivalent would be:// adds a note on that will be automatically stopped at the note-off message with the given source event ID Synth.attachNote(int sourceEventId, int noteNumber, int velocity, int timestamp);
Wouldnt this be nicer (and more flexible) like this:
Synth.attachNote(int sourceEventId, int targetEventId);
-
@Lindon Ah you mean first you call
addNoteOff()
(or whatever), then "link" the returned event ID of the new note to the sourceId? Yes that's even better. -
@Christoph-Hart said in MIDI Processor Inconsistent/Processing Speed Issue?:
@aaronventure well if you spawn off multiple events from a single note then you definitely have to keep track of them and call note-off in the respective note-off callback of the message. I don't think this is a design flaw, but just a reasonable bookkeeping task (again, think of it as RAII: the one who created it (key down) is responsible for the cleanup (key up).
I think you misunderstood me, artificial events are no longer an issue for me after you showed me how to pass down event ID-related info down the chain and keep it tight.
The issue is the one from the very first post in this thread: two notes in a chord, not perfectly on grid, but same timestamp. MIDI Processors get processed in parallel instead of serially.
It invalidates any logic and scripting that wants to extend beyond not even a single scripting processor, but a single callback (e.g. setting flags for whatever down the line). Logic that isn't organized into per-note, 128-size arrays (and even more for MPE) gets overwritten by the second parallel event. If there's a note launch there and that's all you need, fine. But if there's anything down the line that you're hoping will successfully refer to the logic here, unless the data is stored into per-note ( x per channel for MPE) arrays, it gets overwritten.
This is why being able to store data along with MIDI events is crucial. If the per-note event ID array model is not a good fit for your project for any reason and you want to play artificial notes, you're fucked without being able to pass down event-related data.
So my observation was just about the importance of storing event IDs in a per-note (x per channel for MPE) array for any project dealing with artificial notes, unless you want to deal with writing a custom note filter(like I have). The queue removal is great and eliminates the need for a custom note filter, but it doesn't work if a simple chord causes parallel note-on execution that overwrites your artificial note ID unless you have proper storage (per-note array, x per-channel for MPE).
-
@Christoph-Hart But the note ID for the note off is already the same as the one for the corresponding physical note on. Correct me if I'm wrong, I think Lindon meant that in Note On, you would play your artificial note, storing its ID into a variable, then call
Synth.attachNote(artificialNoteId, Message.getEventId);
Then when you release the physical key, it notes off all the artificial notes you attached this way as well without you having to call the noteOff method and worry about passing in the correct IDs.
Together with your queue culling implementation, that's one hell of a cocktail.
-
I think Lindon meant that in Note On, you would play your artificial note, storing its ID into a variable, then call Synth.attachNote()
Yes I think I meant the same. I'll just go ahead and implement it, then we can discuss what I did wrong :)
If the per-note event ID array model is not a good fit for your project for any reason
That's the entire point of having an ID attached to an event, there should be no use case where the event ID can't be used for note identification - it's far more robust than note-number or MIDI channel (read MPE) identification, so there is absolutely no reason to switch to another data model (except you have a very narrow use case and can trim some overhead).
The issue you had at the beginning of the post was an outlier because of how the internals of the sampler module work(ed) and I'm sure there are other edge cases like this lurking in the shadows but in general you should always rely on the ID for, well, IDentifying any event.
it doesn't work if a simple chord causes parallel note-on execution that overwrites your artificial note ID unless you have proper storage (per-note array, x per-channel for MPE).
Have you checked out the snippet from the docs I posted (the chord script)? This will still work with messages at the same timestamp.