Vous êtes sur la page 1sur 9

16/05/13

Nils Liberg's Kontakt Script Editor

Contents
Overview Extended script syntax Variable prefixes are optional Parenthesis are optional for if, while and select For-loops Else if Variable families User-defined functions Native functions Inlined functions Task functions Properties Macros Hexademical numbers Import Pragma

Overview
KScript Editor is a text editor specifically written to make it easier to write and work with Kontakt 2[1] scripts. It also features an integrated script compiler, so it's really an IDE. The editor is based on Scintilla - an open source text editor component. Note: this documentation may not be complete. I will add bits and pieces when I find time to do so.

Extended script syntax


Although it's possible to use just the editing facilities of the editor and copy and paste the code to Kontakt 2, it's also possible to compile your scripts in the editor by pressing F5. By doing so you can check for errors (activate the Extra syntax checks option for even more elaborate error checking) and you can use an extended script language syntax which makes it easier to write and maintain scripts. The following sections will explain the various extensions to the native KSP syntax that the KScript Editor allows you to use. To translate a script from the extended syntax which is easier to read and work with to the original syntax that Kontakt 2 understands you press the F5 key in KScript Editor. This compiles the script, ie. translates the extended syntax to ordinary KSP syntax. If the compilation was successful the compiled code is automatically placed on the clipboard so you can just go ahead and paste it into Kontakt 2. In the examples below the yellow code samples represents code written with the extended syntax and the gray boxes what the corresponding compiled code looks like.

Variable prefixes are optional


In normal KSP it's mandatory to prefix variables with one of the characters $, %, @ and !. With the extended syntax this is not necessary. The only case where a prefix is necessary is on the declaration line of a string variable or string array variable. Prefixes are not necessary once the variables have been declared (see the last line in the example below). Example:
d e c l a r ex: =a+b+c
nilsliberg.se/ksp/scripts/tutorial/editor.html

d e c l a r e$ x: =$ a+$ b+$ c
1/9

16/05/13

Nils Liberg's Kontakt Script Editor

d e c l a r el i s t [ 4 ] d e c l a r e@ n a m e n a m e: =' s u s t a i n s '

d e c l a r e% l i s t [ 4 ] d e c l a r e@ n a m e @ n a m e: =' s u s t a i n s '

Parenthesis are optional for if, while and select


In an effort to make the source code slightly more readable parenthesis are optional for if statements, while-loops and select statements.
w h i l ex< =1 0 i fx=1 s e l e c tx w h i l e( x< =1 0 ) i f( x=1 ) s e l e c t( x )

Note: if the condition starts with a left parenthesis but does not end with a right parenthesis this will at the moment confuse the compiler so for the time being you need to wrap the whole expression in parenthesis in that specific case.

For-loops
KSP only supports while-loops but with the extended syntax you can also use for-loops. Example:
f o ri: =0t o9 l i s t [ i ]: =1 e n df o r $ i: =0 w h i l e( $ i< =9 ) % l i s t [ $ i ]: =1 i n c ( $ i ) e n dw h i l e

It's also possible to loop downwards and/or optionally use a certain step size:
f o ri: =9d o w n t o0s t e p2 l i s t [ i ]: =1 e n df o r $ i: =9 w h i l e( $ i> =0 ) % l i s t [ $ i ]: =1 $ i: =$ i-2 e n dw h i l e

Else If
The extended syntax provides an "else if" construct since this is lacking in KSP.
i fx=1 { . . . } e l s ei fy=1 { . . . } e l s ei fz=1 { . . . } e n di f i f( $ x=1 ) { . . . } e l s e i f( $ y=1 ) { . . . } e l s e i f( $ z=1 ) { . . . } e n di f e n di f e n di f

Variable families
With KSP there is no good way to organize variables so there tends to be a huge list of variable declarations in the init callback which makes it hard to know what is used where. With the extended syntax you can declare variables which belong to the same category in a family and then refer to them as family.variable (the family name followed by a period followed by the variable name). This can make variable names slightly longer but makes it easier to quickly grasp what a variable is
nilsliberg.se/ksp/scripts/tutorial/editor.html 2/9

16/05/13

Nils Liberg's Kontakt Script Editor

used for. In the compiled script the dots are replaced by two underscores. Note that after the declaration of a variable inside a family you always have to use the fully qualified name to refer to it. For example, in the declaration of keys below one has to use keyswitch.N instead of just N. It is also possible to nest families.
o ni n i t f a m i l yk e y s w i t c h d e c l a r ec u r r e n t d e c l a r ec o n s tN: =1 0 d e c l a r ek e y s [ k e y s w i t c h . N ] e n df a m i l y e n do n o nn o t e k e y s w i t c h . c u r r e n t: =s e a r c h ( k e y s w i t c h . k e y s , E V E N T _ N O T E ) e n do n o ni n i t { f a m i l yk e y s w i t c h } d e c l a r e$ k e y s w i t c h _ _ c u r r e n t d e c l a r ec o n s t$ k e y s w i t c h _ _ N: =1 0 d e c l a r e% k e y s w i t c h _ _ k e y s [ $ k e y s w i t c h _ _ N ] e n do n o nn o t e $ k e y s w i t c h _ _ c u r r e n t: =s e a r c h ( % k e y s w i t c h _ _ k e y s , $ E V E N T _ N O T E ) e n do n

User-defined functions
In addition to the natively supported user-defined functions that KSP support and that you invoke using the "call" keyword KScript Editor adds support for two additional types of user-defined functions: inlined functions and task functions (abbreviated taskfunc). Native functions Native user-defined functions do not support parameters nor return value and inside them one cannot use certain builtin functions like allow_group. They are invoked using "call" (see the KSP Reference for further information). Kontakt has some restrictions on the order in which you define this type of function, but KScript Editor will automatically reorder the function definitions in the compiled code for you.
f u n c t i o nr a i s e _ a l l _ n o t e s _ o n e _ o c t a v e c h a n g e _ t u n e ( A L L _ E V E N T S ,1 0 0 0 0 0 * 1 2 ,0 ) e n df u n c t i o n o nn o t e c a l lr a i s e _ a l l _ n o t e s _ o n e _ o c t a v e e n do n

Inlined functions Inlined functions are declared similarily to native functions. However you can optionally add parameters and a return value. An inlined function is invoked using essentially the same syntax as a native one, but without the "call" keyword. If the function has a return value you can invoke the function in any expression with one limitation: if the body of the function definition consists of more than one line then the function can only be used as the single thing on the right hand side of an assignment, eg. y : =m y f u n c ( 4 ,x ) . A function with return value but no parameters needs to be invoked like this: y: =m y f u n c ( ) . The empty parenthesis are needed in this case to distinguish it from an ordinary variable reference. Please note that one and the same function definition can be inlined in one place and 'call'ed in another place. By deciding whether to use "call" or not you decide whether or not the function should be inlined or just called. The sample script below is a simple humanization script which adds a random number between -10 and 10 to incoming velocities. The limit_range function is used to clip the final velocity value to the range 1 to 127.
o ni n i t d e c l a r ev e l o c i t y e n do n o nn o t e v e l o c i t y: =E V E N T _ V E L O C I T Y+r a n d o m ( 1 0 ,1 0 ) l i m i t _ r a n g e ( v e l o c i t y ,1 ,1 2 7 ) c h a n g e _ v e l o ( E V E N T _ I D ,v e l o c i t y ) e n do n o ni n i t d e c l a r e$ v e l o c i t y e n do n o nn o t e $ v e l o c i t y: =$ E V E N T _ V E L O C I T Y+r a n d o m ( 1 0 ,1 0 ) { b e g i nl i m i t _ r a n g e ( $ v e l o c i t y , 1 , 1 2 7 ) } i f( $ v e l o c i t y<1 ) $ v e l o c i t y: =1 e n di f
3/9

nilsliberg.se/ksp/scripts/tutorial/editor.html

16/05/13

Nils Liberg's Kontakt Script Editor

{f o r c e sv a l u et ob eb e t w e e nm i na n dm a x} f u n c t i o nl i m i t _ r a n g e ( v a l u e ,m i n ,m a x ) i fv a l u e<m i n v a l u e: =m i n e n di f i fv a l u e>m a x v a l u e: =m a x e n di f e n df u n c t i o n

i f( $ v e l o c i t y>1 2 7 ) $ v e l o c i t y: =1 2 7 e n di f { e n dl i m i t _ r a n g e ( $ v e l o c i t y , 1 , 1 2 7 ) } c h a n g e _ v e l o ( $ E V E N T _ I D ,$ v e l o c i t y ) e n do n

Please compare the uncompiled and compiled code. All invokations of inlined functions are replaced by the body of the function upon compilation. Note how the parameters are inserted into the compiled code: any occurance of value, min and max in the function body is replaced by the parameters velocity, 1 and 127 respectively. From within the body of one function it is possible to call another function, but since the body of every function has to be inlined at some point it is not allowed for a function to directly or indirectly call itself. For functions which have no parameters it's preferred to leave out the parenthesis alltogether when you declare or call them. It is also possible, but not recommended, to use a pair of empty parenthesis like in languages like Java and C++. Example:
f u n c t i o nd o _ s o m e t h i n g { r e c o m m e n d e ds y n t a x } f u n c t i o nd o _ s o m e t h i n g ( ){ a l s oa l l o w e d }

If you declare a variable inside a function it is by default considered local to that function. This means that the function has its own copy of the variable so even if a variable with the same name was declared in 'on init' or some other function they won't interfer with each other. Local variables are prefixed with an underscore upon compilation (see $_tmp below). If you want a variable declared inside a function to be accessible from callbacks and other functions you can either declare it like "declare global $x" or make sure the function name starts with "on_init" and all variables inside that function will implicitly be considered global.
o ni n i t d e c l a r ex: =1 d e c l a r ey: =5 s w a p ( x ,y ) e n do n f u n c t i o ns w a p ( a ,b ) d e c l a r et m p t m p: =a a: =b b: =t m p e n df u n c t i o n o ni n i t d e c l a r e$ x: =1 d e c l a r e$ y: =5 d e c l a r e$ _ t m p { b e g i ns w a p ( $ x , $ y ) } $ _ t m p: =$ x $ x: =$ y $ y: =$ _ t m p { e n ds w a p ( $ x , $ y ) } e n do n

Return value A return value behaves just as if you had passed an extra parameter and assigned a value to it inside the function. The name "result" below carries no special meaning. You can use any name you choose. The function max consists of multiple lines and can only be used on the right hand side of an assignment: z := max(x, y), whereas the square function which is a single-line one can be used anywhere, for example in an arithmetical expression passed as a parameter to a builtin function.
o ni n i t d e c l a r ex: =1 d e c l a r ey: =5 d e c l a r ez z: =m a x ( x ,y ) m e s s a g e ( 1+s q u a r e ( y ) ) e n do n f u n c t i o nm a x ( a ,b )>r e s u l t i fa>b r e s u l t: =a e l s e r e s u l t: =b e n di f e n df u n c t i o n f u n c t i o ns q u a r e ( a )>r e s u l t
nilsliberg.se/ksp/scripts/tutorial/editor.html 4/9

o ni n i t d e c l a r e$ x: =1 d e c l a r e$ y: =5 d e c l a r e$ z i f( $ x > $ y ) $ z: =$ x e l s e $ z: =$ y e n di f m e s s a g e ( 1+$ y * $ y ) e n do n

16/05/13

Nils Liberg's Kontakt Script Editor

r e s u l t: =a * a e n df u n c t i o n

Declaration order (advanced) It's useful to know that local variables of a function which ends up not being used in a particular script are stripped from the compiled code. This makes it possible to build function libraries where unused functions don't clutter users' scripts with unnecessary variable declarations. In case several functions declare global variables and they are used in specific places in the callbacks or other functions it's good to be aware of the order in which these variables end up in the 'on init' callback (to make sure variables are declared before they are used for the first time): For functions which are invoked directly or indirectly from the init callback the local/global variables end up at the point the function was invoked from the first time (any later invokations have no effect as far as variable declarations are concerned) in the order in which the functions were invoked. For functions which are only invoked from other callbacks than the init callback declarations of global variables are placed at the top of the init callback and local variables at the bottom of the init callback in the order in which the corresponding functions were defined.

Pitfalls If a function contains an expression like "5*x" and it is invoked with parameter x set to C+5 then the compiled code with be 5*C+5 and not 5*(C+5) as one might expect. As you see the compiler does not automatically insert parenthesis around C+5 so the user needs to either write 5*(x) in the function or pass the expression (C+5) as parameter. Please note that if "use old compiler" is unchecked in the Settings menu you no longer need to worry about this. Task functions The task function feature is based on a system by Robert Villwock (a.k.a. Big Bob) called Task Control Module (abbreviated TCM) which has been integrated into KScript Editor. This extended syntax under the surface relies on functions being invoked using the "call" keyword. However, parameters you pass to a task function and local variables declared inside them (taskfunc) are specific to the current callback. Please see the official TCM Guide (pdf). If your script invokes w a i t ( . . . )inside a function then you run the risk of having the same function be entered in the context of another callback instance (i.e. the first callback is paused and another is executed and happens to enter the same function). This can cause problems with the latter invokation incorrectly overriding variable values set and relied upon in the first callback (after the wait call some variables would unexpectedly have assumed different values). Please note that local variables declared inside inlined functions "under the hood" are global variables with a name unique to the function in which it is declared. The Task Control Module solves this problem by allocating a full sized array with 32768 entries and divides this into a number of chunks where each chunk can be assigned to a callback instance. This memory area is then used as a stack where parameters to task functions, function return values and local variables are stored. Since each executed callback gets its own memory storage this solves the problem with values getting overwritten when a function is re-entered. Moreover, TCM makes it possible to pass parameters to and return a result from functions without having to rely on inlined functions. Although inlined functions are suitable in many cases it's a problem that many invokations of them can greatly increase the length of the compiled code (if a 100-line function is invoked 10 times it will result in 1000 lines of compiled code). So in short TCM has these advantages: Invoking complex functions multiple times does not increase the size of the compiled script much (since the "call" keyword is used under the hood) Task functions are re-entrant (if a function is interrupted by wait a second execution of it will not override values that are needed after the return from the wait call) Even if the wait function is not used the script developer benefits from being able to use parameters and return values without the significant increase to the size of the compiled code that inlined functions can result in. In order to use TCM you add t c m . i n i t ( s t a c k _ d e p t h )to your init callback, where stack_depth is the size of the percallback stacks. You also replace w a i t ( . . . )by t c m . w a i t ( . . . ) . The syntax for invoking a task function does not use "call", but please note that the "call" keyword will be added by the compiler. Here is an example of a task function:
o ni n i t {e a c hc a l l b a c ki sa b l et os t o r eu pt o 1 0 0v a l u e sa tat i m e( s p a c eu s e df o r p a r a m e t e r sa n dl o c a lv a r i a b l e s )} t c m . i n i t ( 1 0 0 ) d e c l a r ex e n do n
nilsliberg.se/ksp/scripts/tutorial/editor.html

o ni n i t . . . e n do n f u n c t i o n_ t w a i t . . . e n df u n c t i o n
5/9

16/05/13

Nils Liberg's Kontakt Script Editor

t a s k f u n cg e t _ r a n d o m _ v a l u e ( m i n ,m a x )>r e s u l t d e c l a r er r: =r a n d o m ( m i n ,m a x ) t c m . w a i t ( 5 0 0 0 0 0 ) {w i l lu s er i g h trv a l u ee v e ni f t h ef u n c t i o nw a sr e e n t e r e d} r e s u l t: =r e n dt a s k f u n c o nn o t e {i fa n o t h e rn o t ei sp l a y e d g e t _ r a n d o m _ v a l u em a yb er e e n t e r e d} x: =g e t _ r a n d o m _ v a l u e ( 1 0 ,4 0 ) m e s s a g e ( ' x='&x ) e n do n

f u n c t i o ng e t _ r a n d o m _ v a l u e % p [ $ s p 5 ]: =$ f p $ f p: =$ s p 5 $ s p: =$ f p % p [ $ f p + 1 ]: =r a n d o m ( % p [ $ f p + 2 ] , % p [ $ f p + 3 ] ) % p [ $ s p 1 ]: =5 0 0 0 0 0 c a l l_ t w a i t % p [ $ f p + 4 ]: =% p [ $ f p + 1 ] $ s p: =$ f p $ f p: =% p [ $ f p ] $ s p: =$ s p + 5 e n df u n c t i o n o nn o t e % p [ $ s p 3 ]: =1 0 % p [ $ s p 2 ]: =4 0 c a l lg e t _ r a n d o m _ v a l u e $ x: =% p [ $ s p 1 ] m e s s a g e ( " x="&$ x ) e n do n

Please note how the reference to the local variable r is changed into the stack reference %p[$fp+1]. This highlights that a local variable of a task function is stored in a place unique to each callback. A local variable of an inlined function on the other hand just gets a unique variable name but uses a global storage (hence the re-entrancy problems in that case). By default parameters are just passed into a function and any changes to their values don't make it out. If you want a parameter to be passed both in and out you can prefix it by the keyword var. If you want it to be passed only out (as a kind of result variable where the input doesn't matter) you can prefix it by the keyword out. This syntax was copied from the Pascal programming language which KSP generally seems to be inspired by. An example:
t a s k f u n cs w a p _ g e t _ m a x ( v a ra ,v a rb ,o u tm a x ) d e c l a r et m p t m p: =a a: =b b: =t m p i fa>b m a x: =a e l s e m a x: =b e n di f e n dt a s k f u n c

An invocation of this function is compiled like this:


s w a p _ g e t _ m a x ( x ,y ,z ) % p [ $ s p 3 ]: =$ x % p [ $ s p 2 ]: =$ y c a l ls w a p _ g e t _ m a x $ x: =% p [ $ s p 3 ] $ y: =% p [ $ s p 2 ] $ z: =% p [ $ s p 1 ]

Please note how $x and $y are both passed in and out of the stack system (%p) whereas $z is only passed out. Normally you would use a return value declared using the -> result syntax instead of the out keyword like this. However, out could be useful in case you want to return multiple values.

Properties
A property is a kind of pseudo variable. When you use the property name inside an expression the property reference is replaced by an invokation of the get function of the property (which is inlined). When you use the property name on the left hand side of an assignment, the set function is automatically invoked and the right hand side expression is passed as a parameter. Here is an example:
o ni n i t p r o p e r t yv o l u m e f u n c t i o ng e t ( )>r e s u l t r e s u l t: =g e t _ e n g i n e _ p a r ( . . . E N G I N E _ P A R _ V O L U M E ,1 ,1 ,1 ) e n df u n c t i o n
nilsliberg.se/ksp/scripts/tutorial/editor.html

o ni n i t m e s s a g e ( " V o l u m e :"&g e t _ e n g i n e _ p a r ( $ E N G I N E _ P A R _ V O L U M E s e t _ e n g i n e _ p a r ( $ E N G I N E _ P A R _ V O L U M E , 5 0 0 0 0 0 , 1 , 1 , 1 ) e n do n

6/9

16/05/13

Nils Liberg's Kontakt Script Editor

f u n c t i o ns e t ( v a l u e ) s e t _ e n g i n e _ p a r ( E N G I N E _ P A R _ V O L U M E ,. . . v a l u e ,1 ,1 ,1 ) e n df u n c t i o n e n dp r o p e r t y {i n v o k e sg e tf u n c t i o n :} m e s s a g e ( ' V o l u m e :'&v o l u m e ) {i n v o k e ss e tf u n c t i o n :} v o l u m e: =5 0 0 0 0 0 e n do n

You can also make properties that behave like array variables - even with more than one index. For example you can make a property that behaves as a two-dimensional array in the following way:
d e c l a r ed a t a [ 1 0 0 ] {1 0r o w s ,1 0c o l u m n s} p r o p e r t ym a t r i x f u n c t i o ng e t ( x ,y )>r e s u l t r e s u l t: =d a t a [ x*1 0+y ] e n df u n c t i o n f u n c t i o ns e t ( x ,y ,v a l u e ) d a t a [ x*1 0+y ]: =v a l u e e n df u n c t i o n e n dp r o p e r t y m a t r i x [ 4 ,5 ]: =1 0

If there are multiple indices they are separated by a comma as in the example above. The indices are automatically paired up with the get/set parameters from left to right. The last parameter of the set function is always the value to be set. Please note that it would be possible to pass matrix[0] (the first column) as an actual parameter to a function and then within the function add a second reference to the row. In some cases it can enhance readability to be able to specify the indices at separate places in a name. For example, if the property in the example above instead had been named col.row, then instead of writing matrix[4, 5] one could write col[4].row[5]. The indices are moved to the end by the compiler so it would be equivalent to col.row[4, 5], only more legible in some circumstances.

Macros
The extended syntax allows you to use macros . These are in many ways similar to functions. However, whereas functions interpret the code inside the function body, eg. to support declaration of local variables, macros are used to just perform a very simple text substitution. Macros are inlined as the first compilation step. The differences between functions and macros are: A macro may not invoke other macros. Macro parameters can be used more freely, eg. in declare statements and as part of variable names (not inside strings however).
m a c r od e c l a r e _ b u t t o n ( # v a r # ,# t e x t # ) d e c l a r eu i _ b u t t o n# v a r # _ b u t t o n s e t _ t e x t ( # v a r # _ b u t t o n ,# t e x t # ) e n dm a c r o o ni n i t d e c l a r e _ b u t t o n ( a c t i v e ," A c t i v e " ) e n do n o ni n i t d e c l a r eu i _ b u t t o n$ a c t i v e _ b u t t o n s e t _ t e x t ( $ a c t i v e _ b u t t o n ," A c t i v e " ) e n do n

A macro definition may contain top-level constructs like callbacks and function definitions, in which case the macro may and must be invoked at the top-level (outside of callbacks/functions).

m a c r oo n _ u i _ c o n t r o l _ d o ( # c o n t r o l # ,# c o m m a n d # ) o nu i _ c o n t r o l ( # c o n t r o l # ) # c o m m a n d # e n do n e n dm a c r o
nilsliberg.se/ksp/scripts/tutorial/editor.html

o ni n i t d e c l a r eu i _ b u t t o n$ a c t i v e e n do n o nu i _ c o n t r o l ( $ a c t i v e )
7/9

16/05/13

Nils Liberg's Kontakt Script Editor

o ni n i t d e c l a r eu i _ b u t t o na c t i v e e n do n o n _ u i _ c o n t r o l _ d o ( a c t i v e ,m e s s a g e ( a c t i v e ) )

m e s s a g e ( $ a c t i v e ) e n do n

Hexademical numbers
The extended syntax allows you to use hexadecimal numbers if you prefix them by "0x".
x: =0 x F F x: =2 5 5

Import
It can be useful to be able to split up a script into separate files. The extended syntax allows you to bring in the functions and callbacks from such a script module using the import keyword. The following sample script imports all functions from the file "MyFunctions.txt" which is assumed to be placed in the same folder as the script importing it. This is equivalent to replacing the import line with the contents of the given file.
i m p o r t" M y F u n c t i o n s . t x t "

It's also possible to import a module into its own namespace like this example shows. All variables then need to be prefixed with the given name followed by a dot (compare families). Importing modules this way ensures that there will be no variable name clashes with variables in the current script.
i m p o r t" M y F u n c t i o n s . t x t "a sf u n c s o ni n i t f u n c s . o n _ i n i t e n do n o nn o t e f u n c s . h u m a n i z a t i o n _ f a c t o r: =4 0 f u n c s . r a n d o m i z e _ n o t e _ v e l o c i t y e n do n

Pragma
It is possible to control how the compiler operates by using a pragma directive. On the surface it looks like a comment, but it is recognized by the compiler. At the moment there is only one use (but it may be extended in the future) - to instruct the compiler to save the compiled code in a file upon successful compilation. This is useful since it makes it easier to update the script source in Kontakt 4 which has a feature that lets you link the source code to a certain text file. Here is an example of how to have the compiler output the compiled code to a file:
o ni n i t { # p r a g m as a v e _ c o m p i l e d _ s o u r c eD : \ P r o g r a mF i l e s \ N a t i v eI n s t r u m e n t s \ K o n t a k t4 \ t e s t . t x t } e n do n

For this pragma directive to have any effect the path needs to be absolute, contain both "Native Instruments" and "Kontakt 4", and end with ".txt". These conditions are safety precautions since any earlier text file with the name given will be overwritten upon compilation. You can also make it so that some variable names are exempted from the variable name compaction/obfuscation (for example if you want to use them with the save_array() function and want the file names to be intelligible):
o ni n i t
nilsliberg.se/ksp/scripts/tutorial/editor.html

o ni n i t
8/9

16/05/13

Nils Liberg's Kontakt Script Editor

{ # p r a g m ap r e s e r v e _ n a m e sxy } d e c l a r ex d e c l a r ey d e c l a r ez e n do n

d e c l a r e$ x d e c l a r e$ y d e c l a r e$ j s 1 a t e n do n

[1] KONTAKT is a registered trademark of NATIVE INSTRUMENTS Software Synthesis GmbH. I am in no way affiliated with Native Instruments.

nilsliberg.se/ksp/scripts/tutorial/editor.html

9/9

Vous aimerez peut-être aussi