Scripting Best Practices


  • administrators

    This is a collection of useful tricks which will be extended over time. I divided them into two categories: Interface Design and MIDI Processing.

    Interface design

    When designing interfaces, these tricks might help you speed up your workflow or your scripts.

    Use a script as main interface

    In order to use a script as main interface for the instrument, you'll need to call this methods:

    Content.makeFrontInterface(600, 200);
    

    This script will now be the main interface, which will appear as plugin UI in the finished product.

    Separate MIDI processing logic from Interface scripts

    Although both things can be achieved with the same module (the Script Processor), it can be considered as good practice to keep these two things separated. The reason is (apart from the usual encapsulation benefits) a huge performance increase if you defer the interface script.

    Deferring a script means running all callbacks that are triggered by MIDI messages or the timer on the message thread instead of the audio thread. It comes with some trade-offs (you can't change the midi message with a deferred callback or rely on the accuracy of the timer), but since it is not running on the audio thread, you can do (almost) whatever you want in any of the callbacks without having to risk a drop out (in the worst case, the interface will get laggy).

    Deferring a script is simple: just call

    Synth.deferCallbacks(true);
    

    and from then, the callbacks will stay away from the audio thread.

    These are some things that heavily rely on the deferred callback feature:

    • setting text values of labels according to MIDI input
    • using the timer for animation purposes
    • heavy calculations from MIDI input

    But what if you want to control the behaviour of a MIDI processing script with your interface? Theoretically it would be simpler to combine interface and MIDI processing in this case. But I would suggest using two Script Processors and communicate between them using the standard setAttribute way:

    // ====================================================================== Interface Script
    const var interfaceSlider = Content.addKnob("interfaceSlider", 0, 0);
    
    const var MIDIProcessor = Synth.getMidiProcessor("MIDI Processor");
    
    Synth.deferCallbacks(true);
    
    function onControl(number, value)
    {
    	MIDIProcessor.setAttribute(0, value);
    }
    
    // ====================================================================== MIDI Processor Script
    
    const var processorSlider = Content.addKnob("processorSlider", 11, 5);
    
    function onNoteOn()
    {
    	Message.setVelocity(processorSlider.getValue() * 127);
    }
    

    dragging the knob in the interface script will change the knob in the MIDI Processor script (setAttribute uses the definition order to find the right interface widget from the supplied parameter index)

    Write helper functions for your widgets

    Almost every plugin has a limited set of widgets which all share the same appearance. By using a helper function that creates a widget and set the properties, you can use a one liner to create your actual elements:

    inline function createButton(id, x,y, text)
    {
        // Create a button and stores it to the temporary variable 'b'
        local b = Content.addButton(id, x, y);
        
        // Use the arguments to set properties differently
        b.set("text", text);
        
        // Some random constant properties for every widget
        b.set("saveInPreset", false);
        b.set("visible", false);
        
        return b;
    }
    
    button1 = createButton("button1", 0, 0, "Test 1");
    button2 = createButton("button2", 0, 0, "Test 2");	
    

    Use external files for interface definitions

    Interface definitions can be a pretty boring script with lots of redundancy. While the HISE scripting editor features some nice tools for developing, there are many text editors out there with far more advanced editing features. Using a text editor (eg. Sublime Text 3) for these files can speed up the repetitive work.

    Use a hidden SliderPack for data you want to store / restore

    Since the user preset system only saves and restores widgets, every data you want to store must be saved in a interface element (even if you actually don't want to display that information). Whenever a user preset should contain more than simple numbers (for example an array), the most convenient way to achieve this is to use a SliderPack and hide it by default (you can even add a debugging option that shows the slider pack if you want to check what's going on).

    If you need more than a simple array of numbers, you can also store whole objects into a widget: Panel.setValue({'x': 2, 'y': "String});It will be stored as JSON within the XML preset file.

    Writing MIDI processing scripts

    These best practices mainly aim at writing the fastest possible code.

    Don't allocate anything in the audio callbacks

    There are two types of callbacks:

    Perfomance uncritical callbacks: onInit, onControl

    The onInit callback is executed when the script is compiled (which happens only once when you load the plugin. You don't need to bother about performance here at all (as long as you don't do anything incredible slow). The onControl callback is executed when you move a widget and is happening in parallel to the audio rendering, so even if you have a lot to do there the chances are great it won't affect the audio performance.

    There is one exception to this rule: if you have parameter automation, the onControl callback is happening in the audio rendering callback (or another time critical callback in ProTools)

    Performance critical callbacks: onNoteOn, onNoteOff, onController, onTimer as long as the script is not deferred. These callbacks run directly in the audio thread and spending too much time there causes drop outs. The first and most important rule is: Avoid allocating memory because it requires a OS call with a unpredictable run time performance. These things in Javascript require allocation and must be avoided in the mentioned callbacks at all cost:

    • String operations. If you use string literals for the UIWidget.get() / set() methods you are fine, but any type of dynamic string operations allocate memory.
    • Variable Definitions
    • Array / Object definitions or operations which increase the length
    • API calls that create a reference to a module (just use it in the onInit callback)
    • Function calls (!). This is not obvious, but a standard function call creates a new scope which requires allocation. Read further how to solve this problem...

    Use inline functions

    This is a non standard extension of the Javascript language, but a key concept for writing fast and real time safe scripts.

    There are some limitations for the usage of inline functions:

    • no constructor (if your function definition is a prototype)
    • no recursion
    • no more than 5 parameters

    If this is the case (which should actually be with 99% of all functions you write in scripts, prepend the function definition with inline and it will be faster (and more predictable):

    reg a = 0;
    reg b = 1;
    
    function slowFunction(param1, param2)
    {
    	var sum = param1 + param2;
    	return sum;
    }
    
    inline function fastFunction(param1, param2)
    {
    	local sum = param1 + param2;
    	return sum;
    }
    
    while(a < 200000) a = slowFunction(a, b); // 140 ms
    
    a = 0;
    
    while(a < 200000) a = fastFunction(a, b); // 45 ms
    

    Inline functions can't be members of Objects, so it might clutter your global scope if you overdo it...

    Use the local keyword (stolen from Lua...) for variable declarations within the function and it will have a function only-scope (good for intermediate variables)

    Use const variables

    Writing const before a variable declaration tells the interpreter that this variable will not be changed (it differs from the const keyword in C++ in the way that you can still call methods on it that change something). Especially in combination with API calls it yields a huge performance boost (because it can resolve the function call on compile time):

    const var Knob = Content.addKnob("Knob", 0, 0);
    var slowKnob = Knob;
    var i = 0;
    
    Console.start();
    while(++i < 200000)
    {
    	Knob.getValue(); // 46 ms
    	slowKnob.getValue(); // 68 ms
    }
    Console.stop();
    

    There is absolutely no reason to not declare UI widgets, references to modules (via Synth.getModulator() etc.) not as const

    Unfortunately it is not possible to declare a array as const and then expect the same performance benefits so if your preset design relies on arrays of modules / widgets, you are out of luck here...

    Use reg keyword

    Another addition to Javascript: Use reg instead of var when declaring temporary variables which are accessed in the MIDI (or audio) callbacks. It tells the interpreter to store this in a fixed size container with faster access times:

    var f1 = 120000;
    var i1 = 0;
    
    reg f2 = 120000;
    reg i2 = 0;
    
    while(i1 < f1) i1 = i1 + 1; // ~23 - 40 ms
    
    while(i2 < f2) i2 = i2 + 1; // ~15 ms
    

    If you have a script with lots of variables, the interpreter must search the entire array for every variable access (so the 23 - 40 ms are depending on how many other variables are defined in the script while the access time to reg slots stay the same).

    Caveat: You only have 32 reg storage slots, but it should be enough

    Use MidiList objects as often as possible

    I added a custom data type, the MidiList which is a fixed array with length 128 that contains integer values and some handy methods for the most common tasks (searching for a value, setting all values, get the min and max value and so on) without having to manually iterate over the array.

    const var list = Engine.createMidiList();
    list.fill(1);
    

    Whenever you want to store polyphonic data (for example the last velocity for each note number), using a MidiList brings both speed increase as well as clarity (because most tasks can be done with a simple one liner).



  • Does makeFrontInterface() replace addToFront()?


  • administrators

    It's just a shortcut - I really got tired typing this all the time ::

    Content.setHeight(600);
    Content.setWidth(300);
    Synth.addToFront(true);
    
    // this line does the same thing:
    Content.makeFrontInterface(600, 300);
    


  • inline functions must be declared before they are called in the script - took me a while to work out why my inline function call wasn't working :)


  • administrators

    Actually I fixed this drive-by-style while I was rewriting the parser to support namespaces, so this code will work now:

    works(); // "yep"
    
    inline function works()
    {
        Console.print("yep");
    }
    

    If you're interested in the gory details, I added a "preparsing" level, which analyses the script and stores the IDs of const vars, reg and inline functions so that the actual parser knows if a variable name is one of the mentioned types.


Log in to reply

Looks like your connection to Scripting Best Practices was lost, please wait while we try to reconnect.