NKSP Language
This document intends to give you a compact introduction and overview to
the NKSP real-time instrument script language, so you can start writing
your own instrument scripts in short time. It concentrates on describing
the script language. If you rather want to learn how to modify and
attach scripts to your sounds, then please refer to the gigedit manual for
how to manage instrument scripts with gigedit
for Gigasampler/GigaStudio format sounds, or refer to the SFZ opcode
script
for attaching NKSP scripts with
SFZ format sounds.
At a Glance
NKSP stands for "is Not KSP", which denotes its distinction to an existing proprietary language called KSP. NKSP is a script language specifically designed to write real-time capable software extensions to LinuxSampler's sampler engines that can be bundled individually with sounds by sound designers themselves. Instead of defining a completely new script language, NKSP is leaned on that mentioned properiatary script language. The biggest advantage is that sound designers and musicians can leverage the huge amount of existing KSP scripts which are already available for various purposes on the Internet, instead of being forced to write all scripts from scratch in a completely different language.
That also means however that there are some differences between those two languages. Some extensions have been added to the NKSP core language to make it a bit more convenient and less error prone to write scripts, and various new functions had to be added due to the large difference of the sampler engines and their underlying sampler format. Efforts have been made though to make NKSP as much compatible to KSP as possible. The NKSP documentation will emphasize individual differences in the two languages and function implementations wherever they may occur, to give you immediate hints where you need to take care of regarding compatibility issues when writing scripts that should be spawned on both platforms.
Please note that the current focus of NKSP is the sound controlling aspect of sounds. At this point there is no support for the graphical user interface function set of KSP in NKSP.
Event Handlers
NKSP is an event-driven language. That means you are writing so called event handlers which define what the sampler shall do on individual events that occur, while using the sound the script was bundled with. An event handler in general looks like this:
- on event-name
- statements
- end on
There are currently six events available:
Event Type | Description |
---|---|
on note | This event handler is executed when a new note was triggered, i.e. when hitting a key on a MIDI keyboard. |
on release | This event handler is executed when a note was released, i.e. when releasing a key on a MIDI keyboard. |
on controller | This event handler is executed when a MIDI control change event occurred. For instance when turning the modulation wheel at a MIDI keyboard. |
on rpn | This event handler is executed when a MIDI RPN event occurred. |
on nrpn | This event handler is executed when a MIDI NRPN event occurred. |
on init | Executed only once, as very first event handler, right after the script had been loaded. This code block is usually used to initialize variables in your script with some initial, useful data. |
You are free to decide for which ones of those event types you are going to write an event handler for. You can write an event handler for only one event type or write event handlers for all of those event types. Also dependent on the respective event type, there are certain things you can do and things which you can't do. But more on that later.
Note Events
As a first example, the following tiny script will print a message to your terminal whenever you trigger a new note with your MIDI keyboard.
- on note
- message("A new note was triggered!")
- end on
Probably you are also interested to see which note you triggered exactly.
The sampler provides you a so called
built-in variable
called $EVENT_NOTE
which reflects the note number
(as value between 0 and 127) of the note that has just been triggered. Additionally
the built-in variable $EVENT_VELOCITY
provides you the
velocity value (also between 0 and 127) of the note event.
- on note
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- end on
The &
character concatenates text strings with each other.
In this case it is also automatically converting the note number into a
text string.
Release Events
As counter part to the note
event handler, there is also the
release
event handler, which is executed when a note was
released. This event handler can be used similarly:
- on release
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- end on
Please note that you can hardly find MIDI keyboards which support release velocity. So with most keyboards this value will be 127.
Controller Events
Now let's extend the first script to not only show note-on and note-off events, but also to show a message whenever you use a MIDI controller (i.e. modulation wheel, sustain pedal, etc.).
- on note
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- end on
- on release
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- end on
- on controller
- message("MIDI Controller " & $CC_NUM " changed its value to " & %CC[$CC_NUM])
- end on
It looks very similar to the note event handlers. $CC_NUM
reflects the MIDI controller number of the MIDI controller that had been
changed and %CC
is a so called array variable, which not only
contains a single number value, but instead it contains several values at
the same time. The built-in %CC
array variable contains the current
controller values of all 127 MIDI controllers. So %CC[1]
for
example would give you the current controller value of the modulation
wheel, and therefore %CC[$CC_NUM]
reflects the new controller
value of the controller that just had been changed.
There is some special aspect you need to be aware about: in contrast to the MIDI standard,
monophonic aftertouch (a.k.a. channel pressure) and pitch bend wheel are
handled by NKSP as if they were regular MIDI controllers. So a value change
of one of those two triggers a regular controller
event handler
to be executed. To obtain the current aftertouch value you can use
%CC[$VCC_MONO_AT]
, and to get the current pitch bend wheel
value use %CC[$VCC_PITCH_BEND]
.
RPN / NRPN Events
There are also dedicated event handlers for MIDI RPN and NRPN events:
- on rpn
- message("RPN address msb=" & msb($RPN_ADDRESS) & ",lsb=" & lsb($RPN_ADDRESS) &
- "-> value msb=" & msb($RPN_VALUE) & ",lsb=" & lsb($RPN_VALUE))
- if ($RPN_ADDRESS = 2)
- message("Standard Coarse Tuning RPN received")
- end if
- end on
- on nrpn
- message("NRPN address msb=" & msb($RPN_ADDRESS) & ",lsb=" & lsb($RPN_ADDRESS) &
- "-> value msb=" & msb($RPN_VALUE) & ",lsb=" & lsb($RPN_VALUE))
- end on
Since MIDI RPN and NRPN events are actually MIDI controller events,
you might as well handle these with the previous
controller
event handler. But since RPN and NRPN messages
are not just one MIDI message, but rather always handled by a set of
individual MIDI messages, and since the
precise set and sequence of actual MIDI commands sent varies between
vendors and even among individual of their products, it highly makes sense to
use these two specialized event handlers for these instead, because the
sampler will already relief you from that burden to deal with all those
low-level MIDI event processing issues and all their wrinkles involved
when handling RPNs and NRPNs.
So by reading $RPN_ADDRESS
you get the RPN / NRPN parameter
number that had been changed, and $RPN_VALUE
represents the
new value of that RPN / NRPN parameter. Note that these two built-in
variables are a 14-bit representation of the parameter number and new
value. So their possible value range is 0 .. 16383
. If you
rather want to use their (in MIDI world) more common separated two 7 bit
values instead, then you can easily do that by wrapping them into either
msb()
or lsb()
calls like also demonstrated above.
Script Load Event
As the last one of the six event types available with NKSP, the following
is an example of an init
event handler.
- on init
- message("This script has been loaded and is ready now!")
- end on
You might think, that this is probably a very exotic event. Because in fact, this "event" is only executed once for your script: exactly when the script was loaded by the sampler. This is not an unimportant event handler though. Because it is used to prepare your script for various purposes. We will get more about that later.
Comments
Let's face it: software code is sometimes hard to read, especially when you are not a professional software developer who deals with such kinds of things every day. To make it more easy for you to understand, what you had in mind when you wrote a certain script three years ago, and also if some other developer might need to continue working on your scripts one day, you should place as many comments into your scripts as possible. A comment in NKSP is everything that is nested into an opening and closing pair of curly braces.
{ This is a comment. }
You cannot only use this to leave some human readable explanations here and there, you might also use such curly braces to quickly disable parts of your scripts for a moment, i.e. when debugging certain things.
- on init
- { The following will be prompted to the terminal when the sampler loaded this script. }
- message("My script loaded.")
- { This code block is commented out, so these two messages will not be displayed }
- { message("Another text") message("And another one") }
- end on
Variables
In order to be able to write more complex and more useful scripts, you also need to remember some data somewhere for being able to use that data at a later point. This can be done by using variables . We already came across some built-in variables, which are already defined by the sampler for you. To store your own data you need to declare your own user variables, which has the following form:
declare $variable-name := initial-value
The left hand side's variable-name
is an arbitrary name
you can chose for your variable. That name might consist of English
letters A to Z (lower and upper case), digits (0
to 9
),
and the underscore character "_
".
Variable names must be unique. So you can neither declare several variables
with the same name, nor can you use a name for your variable that is
already been reserved by built-in variables.
The right hand side's initial-value
is simply the first
value the variable should store right after it was created. You can also
omit that.
declare $variable-name
In that case the sampler will automatically assign 0
for you
as the variable's initial value. This way we could for example count the
total amount of notes triggered.
- on init
- declare $numberOfNotes := 0
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- message("This is the " & $numberOfNotes & "th note triggered so far.")
- end on
In the init
event handler we create our own variable
$numberOfNotes
and assign 0
to it as its
initial value. Like mentioned before, that initial assignment is optional.
In the note
event handler we then increase the
$numberOfNotes
variable by one, each time a new note was
triggered and then print a message to the terminal with the current total
amount of notes that have been triggered so far.
init
event handlers.
Variable Types
There are currently five different variable types, which you can easily recognize upon their first character.
Variable Form | Data Type | Description |
---|---|---|
$variable-name | Integer Scalar | Stores one single integer number value. |
%variable-name | Integer Array | Stores a certain amount of integer number values. |
~variable-name | Real Number Scalar | Stores one single real (floating point) number value. |
?variable-name | Real Number Array | Stores a certain amount of real (floating point) number values. |
@variable-name | String | Stores one text string. |
So the first character just before the actual variable name, always denotes the data type of the variable. Also note that all variable types share the same variable name space. That means you cannot declare a variable with a name that has already been used to declare a variable of another variable type.
Array Variables
We already used the first two variable types. However we have not seen yet how to declare such array variables. This is the common declaration form for creating your own array variables.
- on init
- declare %variable-name[array-size] := ( list-of-values )
- end on
So let's say you wanted to create an array variable with the first 12 prime numbers, then it might look like this.
- on init
- declare %primes[12] := ( 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 )
- end on
Like with integer variables, assigning some initial values with
list-of-values
is optional. The array
declaration form without initial value assignment looks like this.
- on init
- declare %variable-name[array-size]
- end on
When you omit that initial assignment, then all numbers of that array will
automatically be initialized with 0
each. With array
variables however, it is always mandatory to provide
array-size
with an array
variable declaration, so the sampler can create that array with the
requested amount of values when the script is loaded. In contrast to many
other programming languages, changing that amount of values of an array
variable is not possible after the variable had been declared. That's due
to the fact that this language is dedicated to real-time applications, and
changing the size of an array variable at runtime would harm real-time
stability of the sampler and thus could lead to audio dropouts. So NKSP
does not allow you to do that.
String Variables
You might also store text with variables. These are called text string variables, or short: string variables. Let's skip the common declaration form of string variables and let us modify a prior example to just use such kind of variable.
- on init
- declare $numberOfNotes
- declare @firstText := "This is the "
- declare @secondText
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- @secondText := "th note triggered so far."
- message(@firstText & $numberOfNotes & @secondText)
- end on
It behaves exactly like the prior example and shall just give you a first idea how to declare and use string variables.
Variable Scope
By default, all variables you declare with NKSP are global variables . That means every event handler can access the data of such a global variable. Furthermore, each instance of an event handler accesses the same data when it is referencing that variable. And the latter fact can be a problem sometimes, which we will outline next.
Let's assume you wanted to write an instrument script that shall resemble a simple delay effect. You could do that by writing a note event handler that automatically triggers several new notes for each note being triggered on a MIDI keyboard. The following example demonstrates how that could be achieved.
- on init
- { The amount of notes to play }
- declare const $delayNotes := 4
- { Tempo with which the new notes will follow the orignal note }
- declare const $bpm := 90
- { Convert BPM to microseconds (duration between the notes) }
- declare const $delayMicroSeconds := 60 * 1000000 / $bpm
- { Just a working variable for being used with the while loop below }
- declare $i
- { For each successive note we trigger, we will reduce the velocity a bit}
- declare $velocity
- end on
- on note
- { First initialize the variable $i with 4 each time we enter this event handler, because each time we executed this handler, the variable will be 0 }
- $i := $delayNotes
- { Loop which will be executed 4 times in a row }
- while ($i)
- { Calculate the velocity for the next note being triggered }
- $velocity := 127 * $i / ($delayNotes + 1)
- { Suspend this script for a short moment ... }
- wait($delayMicroSeconds)
- { ... and after that short break, trigger a new note. }
- play_note($EVENT_NOTE, $velocity)
- { Decrement loop counter $i by one }
- $i := $i - 1
- end while
- end on
In this example we used a new keyword const
. This additional
variable qualifier defines that we don't intend to change this variable
after declaration. So if you know beforehand, that a certain variable should
remain with a certain value, then you might use the const
qualifier to avoid that you i.e. change the value accidently when you
modify the script somewhere in future.
Now when you trigger one single note on your keyboard with that script, you will hear the additional notes being triggered. And also when you hit another note after a while, everything seems to be fine. However if you start playing quick successive notes, you will notice something goes wrong. The amount of notes being triggered by the script is now incorrect and also the volume of the individual notes triggered by the script is wrong. What's going on?
To understand the problem in the last example, let's consider what is
happening when executing that script exactly: Each time you play a note
on your keyboard, a new instance of the note
event handler
will be spawned and executed by the sampler. In all our examples so far
our scripts were so simple, that in practice only one event handler instance
was executed at a time. This is different in this case though. Because
by calling the wait()
function, the respective handler
execution instance is paused for a while and in total each handler
instance will be executed for more than 2 seconds in this particular
example. As a consequence, when
you play multiple, successive notes on your keyboard in short time, you
will have several instances of the note
event handler running
simultaniously. And that's where the problem starts. Because by default,
as said, all variables are global variables. So the event handler instances
which are now running in parallel, are all reading and modifying the same
data. Thus the individual event handler instances will modify the
$i
and $velocity
variables of each other, causing
an undesired misbehavior.
play_note()
allows you to pass
between one and four function arguments. For the function arguments you
don't provide to a play_note()
call, NKSP will automatically
use default values. If you want your script to be compatible with KSP,
then you should always pass four arguments to that function though.
Polyphonic Variables
As a logical consequence of the previously described data concurrency
problem, it would be desirable to have each event handler instance use
its own variable instance, so that the individual handler instances stop
interfering with each other. For this purpose the so called
polyphonic variable
qualifier exists with NKSP. Declaring such a variable is identical to
declaring a regular variable, just that you add the keyword polyphonic
.
declare polyphonic $variable-name
So to fix the bug in our previous example, we simply make the variables
$i
and $velocity
polyphonic variables.
- on init
- { The amount of notes to play }
- declare const $delayNotes := 4
- { Tempo with which the new notes will follow the orignal note }
- declare const $bpm := 90
- { Convert BPM to microseconds (duration between the notes) }
- declare const $delayMicroSeconds := 60 * 1000000 / $bpm
- { Just a working variable for being used with the while loop below }
- declare polyphonic $i { < --- NOW POLYPHONIC !!! }
- { For each successive note we trigger, we will reduce the velocity a bit}
- declare polyphonic $velocity { < --- NOW POLYPHONIC !!! }
- end on
- on note
- { First initialize the variable $i with 4 each time we enter this event handler, because each time we executed this handler, the variable will be 0 }
- $i := $delayNotes
- { Loop which will be executed 4 times in a row }
- while ($i)
- { Calculate the velocity for the next note being triggered }
- $velocity := 127 * $i / ($delayNotes + 1)
- { Suspend this script for a short moment ... }
- wait($delayMicroSeconds)
- { ... and after that short break, trigger a new note. }
- play_note($EVENT_NOTE, $velocity)
- { Decrement loop counter $i by one }
- $i := $i - 1
- end while
- end on
And that's it! The script works now as intended. Now you might wonder, why are variables not polyphonic by default? Isn't that more common and wouldn't that be more safer than using global variables by default? The reason is that a polyphonic variable consumes a lot more memory than a regular (global) variable. That's because for each polyphonic variable, the sampler has to allocate in advance (when the script is loaded) as many instances of that polyphonic variable as there are maximum events allowed with the sampler. So that's a lot! Considering that today's computers have plenty of RAM this might be a theoretical aspect, but in the end: this default scope of variables was already like this with KSP so we are also doing it like this with NKSP for compatibility reasons.
Please note that the polyphonic qualifier only exists for integer variables and real number variables (scalars). So you cannot declare polyphonic string variables, nor can you declare polyphonic array variables. Like in the previous explanation, this is due to the fact that it would consume a huge amount of memory for such variables. And with string variables and array variables, the required amount of memory would be much higher than with simple integer or real number variables.
As summary, the following are guideline rules describing when you should use the polyphonic qualifier for a certain variable. You should declare a particular variable polyphonic if one (or even both) of the following two conditions apply to that variable.
-
If you call the
wait()
function within your event handlers and the respective variable is modified and read before and after at least one of the individualwait()
calls. -
If you have loops that might run for a very long time, while accessing
the respective variable in between. That's because if your script is
running consecutively for too long, the sampler will automatically suspend your
script for a while to avoid your script becoming a real-time stability
hazard for the sampler. Your script will then automatically be resumed
after a short moment by the sampler, so effectively this is similar to
something like an "automated"
wait()
function call by the sampler.
In all other cases you should rather use regular (global) variables instead.
But keep in mind that you might need to re-assign a certain value for
some global variables when you enter the respective event handler, just
like we did with $i := $delayNotes
right from the start
during discussion of the previous example script.
There is another special aspect regarding the variable scope of polyphonic
variables: note
handlers and release
handlers of
the same script share the same polyphonic variable scope, that means you
may pass data from a particular note's note
handler to its
release
handler by using the same polyphonic variable name.
Control Structures
A computer is more than a calculator that adds numbers and stores them somewhere. One of the biggest strength of a computer, which makes it such powerful, is the ability to do different things depending on various conditions. For example your computer might clean up your hard drive while you are not sitting in front of it, and it might immediately stop doing so when you need all its resources to cut your latest video which you just shot.
In order to do that for you, a computer program allows you to define conditions and a list of instructions the computer shall perform for you under those individual conditions. These kinds of software mechanisms are called Control Structures.
if Branches
The most fundamental control structure are if branches, which has the following general form.
- if (condition)
- statements
- end if
The specified condition
is evaluated each time script
execution reaches this control block. The condition can for example be
the value of a variable, some arithmetic expression, a function call or
a combination of them. In all cases the sampler expects the
condition
expression to evaluate to some numeric
(or boolean) value. If the evaluated number is exactly 0
then
the condition is interpreted to be false and thus the list of
statements
is not executed. If the evaluated value is any
other value than 0
then the condition is interpreted to be
true and accordingly the list of statements
will be
executed.
Alternatively you might also specify a list of instructions which shall be executed when the condition is false.
- if (condition)
- statements-when-true
- else
- statements-when-false
- end if
In this case the first list of statements is executed when the
condition
evaluated to true, otherwise the second
list of statements is executed instead.
Once again, let's get back to the example of counting triggered notes. You might have noticed that it did not output correct English for the first three notes. Let's correct this now.
- on init
- declare $numberOfNotes
- declare @postfix
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- if ($numberOfNotes == 1)
- @postfix := "st"
- else
- if ($numberOfNotes == 2)
- @postfix := "nd"
- else
- if ($numberOfNotes == 3)
- @postfix := "rd"
- else
- @postfix := "th"
- end if
- end if
- end if
- message("This is the " & $numberOfNotes & @postfix & " note triggered so far.")
- end on
We are now checking the value of $numberOfNotes
before we
print out a message. If $numberOfNotes
equals one, then we
assign the string "st"
to the variable @postfix
,
if $numberOfNotes
equals 2 instead we assign the string
"nd"
instead, if it equals 3 instead we assign
"rd"
, in all other cases we assign the string
"th"
. And finally we assemble the text message to be
printed out to the terminal on line 23.
Select Case Branches
The previous example now outputs the numbers in correct English. But the script code looks a bit bloated, right? That's why there is a short hand form.
- select expression
- case integer-1
- statements-1
- case integer-2
- statements-2
- .
- .
- .
- end select
The provided expression
is first evaluated to an integer
value. Then this value is compared to the integer values of the nested
case
lines. So it first compares the evaluated value of
expression
with integer-1
, then it
compares it with integer-2
, and so on. The first integer
number that matches with the evaluated value of expression
,
will be interpreted as being the current valid condition. So if
expression
equals integer-1
,
then statements-1
will be executed, otherwise if
expression
equals integer-2
,
then statements-2
will be executed, and so on.
Using a select-case construct, our previous example would look like follows.
- on init
- declare $numberOfNotes
- declare @postfix
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- @postfix := "th"
- select $numberOfNotes
- case 1
- @postfix := "st"
- case 2
- @postfix := "nd"
- case 3
- @postfix := "rd"
- end select
- message("This is the " & $numberOfNotes & @postfix & " note triggered so far.")
- end on
select (expression)
. Some developers familiar with
other programming languages might prefer this style. However if you want
to keep compatibility with KSP, you should not use parentheses for
select expressions.
The amount of case conditions you add to such select-case blocks is completely up to you. Just remember that the case conditions will be compared one by one, from top to down. The latter can be important when you define a case line that defines a value range. So for instance the following example will not do what was probably intended.
- on init
- declare $numberOfNotes
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- select $numberOfNotes
- case 1 to 99
- message("Less than 100 notes triggered so far")
- exit
- case 1
- message("First note was triggered!") { Will never be printed ! }
- exit
- case 2
- message("Second note was triggered!") { Will never be printed ! }
- exit
- case 3
- message("Third note was triggered!") { Will never be printed ! }
- exit
- end select
- message("Wow, already the " & $numberOfNotes & "th note triggered.")
- end on
You probably get the idea what this script "should" do. For the 1st note
it should print "First note was triggered!"
, for the 2nd
note it should print "Second note was triggered!"
, for the 3rd
note it should print "Third note was triggered!"
, for the 4th
up to 99th note it should print "Less than 100 notes triggered so far"
,
and starting from the 100th note and all following ones, it should print
the precise note number according to line 23. However, it doesn't!
To correct this problem, you need to move the first case block to the end, like follows.
- on init
- declare $numberOfNotes
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- select $numberOfNotes
- case 1
- message("First note was triggered!")
- exit
- case 2
- message("Second note was triggered!")
- exit
- case 3
- message("Third note was triggered!")
- exit
- case 1 to 99
- message("Less than 100 notes triggered so far")
- exit
- end select
- message("Wow, already the " & $numberOfNotes & "th note triggered.")
- end on
Or you could of course fix the questioned case range from case 1 to 99
to case 4 to 99
. Both solutions will do.
We also used the built-in function exit()
in the
previous example. You can use it to stop execution at that point of your
script. In the previous example it prevents multiple messages to be
printed to the terminal.
exit()
function only stops execution of the current
event handler instance! It does not stop execution of other
instances of the same event handler, nor does it stop execution of other
handlers of other event types, and especially it does not stop or
prevent further or future execution of your entire script! In other words,
you should rather see this function as a return statement, in case you are
familiar with other programming languages already.
while Loops
Another fundamental control construct of program flow are loops. You can use so called while loops with NKSP.
- while (condition)
- statements
- end while
A while loop is entered if the provided condition
expression evaluates to true and will then continue to execute
the given list of statements
down to the end of the statements
list. The condition
is re-evaluated each time execution
reached the end of the statements
list and according to
that latest evaluated condition
value at that point, it
will or will not repeat executing the statements again. If the condition
turned false instead, it will leave the loop and continue executing
statements that follow after the while loop block.
The next example will print the same message three times in a row to the terminal, right after the script had been loaded by the sampler.
- on init
- declare $i := 3
- while ($i)
- message("Print this three times.")
- $i := $i - 1
- end while
- end on
When the while loop is reached for the first time in this example, the
condition value is 3
. And as we learned before, all integer
values that are not 0
are interpreted as being a true condition.
Accordingly the while loop is entered, the message is printed to the
terminal and the variable $i
is reduced by one. We reached
the end of the loop's statements list, so it is now re-evaluating the
condition, which is now the value 2
and thus the loop
instructions are executed again. That is repeated until the loop was
executed for the third time. The variable $i
is now
0
, so the loop condition turned finally to false and the
loop is thus left at that point and the text message was printed
three times in total.
User Functions
We already came across various built-in functions, which you may call by your scripts to perform certain tasks or behavior which is already provided for you by the sampler. NKSP also allows you to write your own functions, which you then may call from various places of your script.
When working on larger scripts, you may notice that you easily get to the point where you may have to duplicate portions of your script code, since there are certain things that you may have to do again and again in different parts of your script. Software developers usually try to avoid such code duplications to keep the overall amount of code as small as possible, since the overall amount of code would bloat quickly and would make the software very hard to maintain. One way for you to avoid such script code duplications with NKSP is to write so called User Functions.
Let's assume you wanted to create a simple stuttering effect. You may do so like in the following example.
- on note
- while (1)
- wait(200000)
- if (not (event_status($EVENT_ID) .and. $EVENT_STATUS_NOTE_QUEUE))
- exit()
- end if
- change_vol($EVENT_ID, -20000) { Reduce volume by 20 dB. }
- wait(200000)
- if (not (event_status($EVENT_ID) .and. $EVENT_STATUS_NOTE_QUEUE))
- exit()
- end if
- change_vol($EVENT_ID, 0) { Increase volume to 0 dB. }
- end while
- end on
This script will run an endless loop for each note being triggered.
Every 200ms
it will turn the volume alternatingly down and
up to create the audible stuttering effect. After each wait()
call it calls event_status($EVENT_ID)
to check whether
this note is still alive, and as soon as the note died, it will stop
execution of the script instance by calling exit()
. The latter
is important in this example, because otherwise the script execution instances would
continue to run in this endless loop forever, even after the respectives
notes are gone. Which would let your CPU usage increase with every new note
and would never decrease again.
This behavior of the sampler is not a bug, it is intended, since there may
also be cases where you want to do certain things by script even after the
respective notes are dead and gone. However as you can see, that script is
using the same portions of script code twice. To avoid that, you could also
write the same script with a user function like this:
- function pauseMyScript
- wait(200000)
- if (not (event_status($EVENT_ID) .and. $EVENT_STATUS_NOTE_QUEUE))
- exit()
- end if
- end function
- on note
- while (1)
- call pauseMyScript
- change_vol($EVENT_ID, -20000) { Reduce volume by 20 dB. }
- call pauseMyScript
- change_vol($EVENT_ID, 0) { Increase volume back to 0 dB. }
- end while
- end on
The script became in this simple example only slightly smaller, but it also
became easier to read and behaves identically to the previous solution.
And in practice, with a more complex script, you can
reduce the overall amount of script code a lot this way. You can choose any
name for your own user functions, as long as the name is not already
reserved by a built-in function. Note that for calling a user function,
you must always precede the actual user function name with the
call
keyword. Likewise you may however not use the
call
keyword for calling any built-in function. So that
substantially differs calling built-in functions from calling user functions.
Synchronized Blocks
When we introduced the polyphonic keyword previously, we learned that a script may automatically be suspended by the sampler at any time and then your script is thus sleeping for an arbitrary while. The sampler must do such auto suspensions under certain situations in cases where an instrument script may become a hazard for the sampler's overall real-time stability. If the sampler would not do so, then instrument scripts might easily cause audio dropouts, or at worst, buggy instrument scripts might even lock up the entire sampler in an endless loop. So auto suspension is an essential feature of the sampler's real-time instrument script engine.
Now the problem as a script author is that you don't really know beforehand why and when your script might get auto suspended by the sampler. And when you are working on more complex, sophisticated scripts, you will notice that this might indeed be a big problem in certain sections of your scripts. Because in practice, a sophisticated script often has at least one certain consecutive portion of statements which must be executed in strict consecutive order by the sampler, which might otherwise cause concurrency issues and thus misbehavior of your script if that sensible code section was auto suspended in between. A typical example of such concurrency sensible code sections are statements which are reading and conditionally modifying global variables. If your script gets auto suspended in such a code section, another script handler instance might then interfere and change those global variables in between.
To avoid that, you can place such a sensible code section at the very beginning of your event handler. For example consider you might be writing a custom glissando script starting like this:
- on init
- declare $keysDown
- declare $firstNoteID
- declare $firstNoteNr
- declare $firstVelocity
- end on
- on note
- { The concurrency sensible code section for the "first active" note. }
- inc($keysDown)
- if ($keysDown = 1 or event_status($firstNoteID) = $EVENT_STATUS_INACTIVE)
- $firstNoteID = $EVENT_ID
- $firstNoteNr = $EVENT_NOTE
- $firstVelocity = $EVENT_VELOCITY
- exit { return from event handler here }
- end if
- { The non-sensible code for all other subsequent notes would go here. }
- end on
- on release
- dec($keysDown)
- end on
Because the earlier statements are executed in an event handler, the higher the chance that they will never get auto suspended. And with those couple of lines in the latter example you might even be lucky that it won't ever get suspended in that sensible code section at least. However when it comes to live concerts you don't really want to depend on luck, and in practice such a sensible code section might be bigger than this one.
That's why we introduced synchronized
code blocks for the
NKSP language, which have the following form:
- synchronized
- statements
- end synchronized
All statements
which you put into such a synchronized
code block are guaranteed that they will never get auto suspended by
the sampler.
synchronized
blocks are a language extension which
is only available with NKSP. KSP does not support synchronized
blocks.
So to make our previous example concurrency safe, we would change it like this:
- on init
- declare $keysDown
- declare $firstNoteID
- declare $firstNoteNr
- declare $firstVelocity
- end on
- on note
- { The concurrency sensible code section for the "first active" note. }
- synchronized
- inc($keysDown)
- if ($keysDown = 1 or event_status($firstNoteID) = $EVENT_STATUS_INACTIVE)
- $firstNoteID = $EVENT_ID
- $firstNoteNr = $EVENT_NOTE
- $firstVelocity = $EVENT_VELOCITY
- exit { return from event handler here }
- end if
- end synchronized
- { The non-sensible code for all other subsequent notes would go here. }
- end on
- on release
- dec($keysDown)
- end on
If you are already familiar with some programming languages, then you might already have seen such synchronized code block concepts in languages like e.g. Java. This technique really provides an easy way to protect certain sections of your script against concurrency issues.
synchronized
code blocks only with great
care! If the amount of statements being executed in your synchronized block
is too large, then you will get audio dropouts. If you even use loops in
synchronized code blocks, then the entire sampler might even become
unresponsive in case your script is buggy!
Operators
A programming language provides so called operators to perform certain kinds of transformations of data placed next to the operators. These are the operators available with NKSP.
Arithmetic Operators
These are the most basic mathematical operators, which allow to add, subtract, multiply and divide integer values with each other.
- on init
- message("4 + 3 is " & 4 + 3) { Add }
- message("4 - 3 is " & 4 - 3) { Subtract }
- message("4 * 3 is " & 4 * 3) { Multiply }
- message("35 / 5 is " & 35 / 5) { Divide }
- message("35 mod 5 is " & 35 mod 5) { Remainder of Division ("modulo") }
- end on
You may either use direct integer literal numbers like used in the upper example, or you can use integer number variables or integer array variables.
Boolean Operators
To perform logical transformations of boolean data, you may use the following logical operators:
- on init
- message("1 and 1 is " & 1 and 1) { logical "and" }
- message("1 and 0 is " & 1 and 0) { logical "and" }
- message("1 or 1 is " & 1 or 1) { logical "or" }
- message("1 or 0 is " & 1 or 0) { logical "or" }
- message("not 1 is " & not 1) { logical "not" }
- message("not 0 is " & not 0) { logical "not" }
- end on
Keep in mind that with logical operators shown above,
all integer values other than 0
are interpreted as boolean true while an integer value of
precisely 0
is interpreted as being boolean false.
So the logical operators shown above always look at numbers at a whole. Sometimes however you might rather need to process numbers bit by bit. For that purpose the following bitwise operators exist.
- on init
- message("1 .and. 1 is " & 1 .and. 1) { bitwise "and" }
- message("1 .and. 0 is " & 1 .and. 0) { bitwise "and" }
- message("1 .or. 1 is " & 1 .or. 1) { bitwise "or" }
- message("1 .or. 0 is " & 1 .or. 0) { bitwise "or" }
- message(".not. 1 is " & .not. 1) { bitwise "not" }
- message(".not. 0 is " & .not. 0) { bitwise "not" }
- end on
Bitwise operators work essentially like logical operators, with the
difference that bitwise operators compare each bit independently.
So a bitwise .and.
operator for instance takes the 1st bit
of the left hand's side value, the 1st bit of the right hand's side value,
compares the two bits logically and then stores that result as 1st bit of
the final result value, then it takes the 2nd bit of the left hand's side value
and the 2nd bit of the right hand's side value, compares those two bits logically
and then stores that result as 2nd bit of the final result value, and so on.
Comparison Operators
For branches in your program flow, it is often required to compare data with each other. This is done by using comparison operators, enumerated below.
- on init
- message("Relation 3 < 4 -> " & 3 < 4) { "smaller than" comparison }
- message("Relation 3 > 4 -> " & 3 > 4) { "greater than" comparison }
- message("Relation 3 <= 4 -> " & 3 <= 4) { "smaller or equal than" comparison}
- message("Relation 3 >= 4 -> " & 3 >= 4) { "greater or equal than" comparison}
- message("Relation 3 # 4 -> " & 3 # 4) { "not equal to" comparison}
- message("Relation 3 = 4 -> " & 3 = 4) { "is equal to" comparison}
- end on
All these operations yield in a boolean result which could then
be used e.g. with if
or while
loop statements.
String Operators
Last but not least, there is exactly one operator for text string data;
the string concatenation operator &
, which
combines two text strings with each other.
- on init
- declare @s := "foo" & " bar"
- message(@s)
- end on
We have used it now frequently in various examples before.
Preprocessor Statements
Similar to low-level programming languages like C, C++, Objective C and the like, NKSP supports a set of so called preprocessor statements. These are essentially "instructions" which are "executed" or rather processed, before (and only before) the script is executed by the sampler, and even before the script is parsed by the actual NKSP language parser. You can think of a preprocessor as a very primitive parser, which is the first one getting in touch with your script, it modifies the script code if requested by your preprocessor statements in the script, and then passes the (probably) modified script to the actual NKSP language parser.
When we discussed comments in NKSP scripts before,
it was suggested that you might comment out certain code parts to disable
them for a while during development of scripts. It was also suggested
during this language tour that you should not use string variables or use
the message()
function with your final production sounds.
However those are very handy things during development of your instrument
scripts. You might even have a bunch of additional code in your scripts
which only satisfies the purpose to make debugging of your scripts more easy,
which however wastes on the other hand precious CPU time. So what do you
do? Like suggested, you could comment out the respective code sections as
soon as development of your script is completed. But then one day you
might continue to improve your scripts, and the debugging code would be
handy, so you would uncomment all the relevant code sections to get them
back. When you think about this, that might be quite some work each time.
Fortunately there is an alternative by using preprocessor statements.
Set a Condition
First you need to set a preprocessor condition in your script. You can do that like this:
SET_CONDITION(condition-name)
This preprocessor "condition" is just like some kind of
boolean variable
which is only available to the preprocessor and by using
SET_CONDITION(condition-name)
, this is like setting this
preprocessor condition to true. Like with regular script
variables, a preprocessor condition name can be chosen quite arbitrarily
by you. But again, there are some pre-defined preprocessor conditions
defined by the sampler for you. So you can only set a condition name here
which is not already reserved by a built-in preprocessor condition. Also
you shall not set a condition in your script again if you have already set it
before somewhere in your script. The NKSP preprocessor will ignore setting
a condition a 2nd time and will just print a warning when the script is
loaded, but you should take care of it, because it might be a cause for
some bug.
Reset a Condition
To clear a condition in your script, you might reset the condition like so:
RESET_CONDITION(condition-name)
This is like setting that preprocessor condition back to false again.
You should only reset a preprocessor condition that way if you did set it
with SET_CONDITION(condition-name)
before. Trying to
reset a condition that has not been set before, or trying to reset a
condition that has already been reset, will both be ignored by the sampler,
but again you will get a warning, and you should take care about it.
Conditionally Using Code
Now what do you actually do with such preprocessor conditions? You can use them for the NKSP language parser to either
- use certain parts of your code
- and / or to ignore certain parts of your code
You can achieve that by wrapping NKSP code parts into a pair of either
- USE_CODE_IF(condition-name)
- some-NKSP-code-goes-here
- END_USE_CODE
preprocessor statements, or between
- USE_CODE_IF_NOT(condition-name)
- some-NKSP-code-goes-here
- END_USE_CODE
statements. In the first case, the NKSP code portion is used by the NKSP
language parser if the given preprocessor condition-name
is set
(that is if condition is true).
If the condition is not set, the NKSP code portion in between is
completely ignored by the NKSP language parser.
In the second case, the NKSP code portion is used by the NKSP
language parser if the given preprocessor condition-name
is not set
(or was reset)
(that is if condition is false).
If the condition is set, the NKSP code portion in between is
completely ignored by the NKSP language parser.
Let's look at an example how to use that to define conditional debugging code.
- SET_CONDITION(DEBUG_MODE)
- on init
- declare const %primes[12] := ( 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 )
- declare $i
- USE_CODE_IF(DEBUG_MODE)
- message("This script has just been loaded.")
- $i := 0
- while ($i < num_elements(%primes))
- message("Prime " & $i & " is " & %primes[$i])
- $i := $i + 1
- end while
- END_USE_CODE
- end on
- on note
- USE_CODE_IF(DEBUG_MODE)
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- END_USE_CODE
- end on
- on release
- USE_CODE_IF(DEBUG_MODE)
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- END_USE_CODE
- end on
- on controller
- USE_CODE_IF(DEBUG_MODE)
- message("MIDI Controller " & $CC_NUM " changed its value to " & %CC[$CC_NUM])
- END_USE_CODE
- end on
The built-in function num_elements()
used above, can
be called to obtain the size of an array variable at runtime.
As this script looks now, the debug messages will be printed out. However
it requires you to just remove the first line, or to comment out the first
line, in order to disable all debug code portions in just a second:
- { Setting the condition is commented out, so our DEBUG_MODE is disabled now. }
- { SET_CONDITION(DEBUG_MODE) }
- on init
- declare const %primes[12] := ( 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 )
- declare $i
- USE_CODE_IF(DEBUG_MODE) { Condition is not set, so this entire block will be ignored now. }
- message("This script has just been loaded.")
- $i := 0
- while ($i < num_elements(%primes))
- message("Prime " & $i & " is " & %primes[$i])
- $i := $i + 1
- end while
- END_USE_CODE
- end on
- on note
- USE_CODE_IF(DEBUG_MODE) { Condition is not set, no message will be printed. }
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- END_USE_CODE
- end on
- on release
- USE_CODE_IF(DEBUG_MODE) { Condition is not set, no message will be printed. }
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- END_USE_CODE
- end on
- on controller
- USE_CODE_IF(DEBUG_MODE) { Condition is not set, no message will be printed. }
- message("MIDI Controller " & $CC_NUM " changed its value to " & %CC[$CC_NUM])
- END_USE_CODE
- end on
Now you might say, you could also achieve that by declaring and using a regular NKSP variable. That's correct, but there are two major advantages by using preprocessor statements.
- Saving Resources - The preprocessor conditions are only processed before the script is loaded into the NKSP parser. So in contrast to using NKSP variables, the preprocessor solution does not waste any CPU time or memory resources while executing the script. That also means that variable declarations can be disabled with the preprocessor this way and thus will also safe resources.
- Cross Platform Support - Since the code portions filtered out by the preprocessor never make it into the NKSP language parser, those filtered code portions might also contain code which would have lead to parser errors. For example you could use a built-in preprocessor condition to check whether your script was loaded into LinuxSampler or rather into another sampler. That way you could maintain one script for both platforms: NKSP and KSP. Accordingly you could also check a built-in variable to obtain the version of the sampler in order to enable or disable code portions of your script that might use some newer script features of the sampler which don't exist in older version of the sampler.
As a rule of thumb: if there are things that you could move from your NKSP executed programming code out to the preprocessor, then you should use the preprocessor instead for such things. And like stated above, there are certain things which you can only achieve with the preprocessor.
Disable Messages
Since it is quite common to switch a script between a development version
and a production version, you actually don't need to wrap all your
message()
calls into preprocessor statements like in the
previous example just to disable messages. There is actually a built-in
preprocessor condition dedicated to perform that task much more conveniently for you.
To disable all messages in your script, simply add SET_CONDITION(NKSP_NO_MESSAGE)
e.g. at the very beginning of your script.
So the previous example can be simplified to this:
- { Enable debug mode, so show all debug messages. }
- SET_CONDITION(DEBUG_MODE)
- { If our user declared condition "DEBUG_MODE" is not set ... }
- USE_CODE_IF_NOT(DEBUG_MODE)
- { ... then enable this built-in condition to disable all message() calls. }
- SET_CONDITION(NKSP_NO_MESSAGE)
- END_USE_CODE
- on init
- declare const %primes[12] := ( 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 )
- declare $i
- message("This script has just been loaded.")
- USE_CODE_IF(DEBUG_MODE)
- $i := 0
- while ($i < num_elements(%primes))
- message("Prime " & $i & " is " & %primes[$i])
- $i := $i + 1
- end while
- END_USE_CODE
- end on
- on note
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- end on
- on release
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- end on
- on controller
- message("MIDI Controller " & $CC_NUM " changed its value to " & %CC[$CC_NUM])
- end on
You can then actually also add RESET_CONDITION(NKSP_NO_MESSAGE)
at another section of your script, which will cause all subsequent
message()
calls to be processed again. So that way you can
easily enable and disable message()
calls of entire individual
sections of your script, without having to wrap all message()
calls into preprocessor statements.
What Next?
You have completed the introduction of the NKSP real-time instrument script language at this point. You can now dive into the details of the NKSP language by moving on to the NKSP reference documentation. Which provides you an overview and quick access to the details of all built-in functions, built-in variables and more.
You might also be interested to look at new NKSP core language features being added to the latest development version of the sampler: Real Numbers, Units and Finalness ...