HISE Logo Forum
    • Categories
    • Register
    • Login

    Scripting Best Practices

    Scheduled Pinned Locked Moved Blog Entries
    9 Posts 3 Posters 4.0k Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Christoph HartC
      Christoph Hart
      last edited by Christoph Hart

      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).

      1 Reply Last reply Reply Quote 1
      • d.healeyD
        d.healey
        last edited by d.healey

        Does makeFrontInterface() replace addToFront()?

        Libre Wave - Freedom respecting instruments and effects
        My Patreon - HISE tutorials
        YouTube Channel - Public HISE tutorials

        1 Reply Last reply Reply Quote 1
        • Christoph HartC
          Christoph Hart
          last edited by

          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);
          
          1 Reply Last reply Reply Quote 1
          • d.healeyD
            d.healey
            last edited by

            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 :)

            Libre Wave - Freedom respecting instruments and effects
            My Patreon - HISE tutorials
            YouTube Channel - Public HISE tutorials

            1 Reply Last reply Reply Quote 0
            • Christoph HartC
              Christoph Hart
              last edited by Christoph Hart

              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.

              1 Reply Last reply Reply Quote 3
              • B
                benosterhouse
                last edited by

                O man, glad I'm seeing this now!
                I was declaring variables all over the place in callbacks.
                Out of curiosity: is there a difference between defining variables as const vs const var ?

                d.healeyD 1 Reply Last reply Reply Quote 0
                • Christoph HartC
                  Christoph Hart
                  last edited by

                  Nope, it's just semantics.

                  1 Reply Last reply Reply Quote 0
                  • d.healeyD
                    d.healey @benosterhouse
                    last edited by

                    @benosterhouse You might enjoy this too - https://github.com/christophhart/hise_documentation/blob/master/scripting/scripting-in-hise/hise-script-coding-standards.md

                    Libre Wave - Freedom respecting instruments and effects
                    My Patreon - HISE tutorials
                    YouTube Channel - Public HISE tutorials

                    1 Reply Last reply Reply Quote 0
                    • B
                      benosterhouse
                      last edited by

                      Thanks! I see there's a bit more there, and I'd imagine more up to date.

                      1 Reply Last reply Reply Quote 0
                      • d.healeyD d.healey referenced this topic on
                      • d.healeyD d.healey referenced this topic on
                      • d.healeyD d.healey referenced this topic on
                      • First post
                        Last post

                      49

                      Online

                      1.7k

                      Users

                      11.7k

                      Topics

                      101.8k

                      Posts