What's new

HOW TO: Approaching Modulation in KSP

EvilDragon

KSP Wizard
I see people are still struggling with this, after all this time. So I thought I could explain this to everyone once and for all, since it's quite an important thing when doing a custom scripted instrument.

First, let's break things down. Kontakt has two types of modulators - internal and external. Internal are: LFO, envelope, glide, step modulator, envelope follower. External are: pitch bend, aftertouch, MIDI CC, constant, random unipolar/bipolar, etc.

Next, internal modulators have two parts: the modulator itself (represented by its module in the Modulation section of Kontakt's instrument edit view), and the modulator target strip (found at the destination - source pitch, filter cutoff, amplifier volume, etc.). External modulators only have the modulator target strip.

Now, how do we KSP this all? This is where KSP offers two commands: find_mod() and find_target() (in Kontakt 7 these are replaced with get_mod_idx() and get_target_idx() and should be used instead, because they return $NI_NOT_FOUND if an object is not found, which is way better than returning 0 which old commands did, which can destroy a modulator's setup, most often amp envelope mod amount goes to 0 which breaks instrument playback!). First one targets either internal modulators or external modulator target strips. Second one targets only (and nothing else but) internal modulator target strips. These commands work by searching for a named reference, the name of a modulator/target strip. Kontakt assigns default names to these, but I find them completely useless, so I heartily recommend everyone to do their own naming.

Here's my suggestion for a clear-cut modulation naming scheme:

* use all caps
* name internal mods either by their purpose (PITCH LFO, FILTER ENV), or number them (LFO 1, LFO 2, ENV 1, ENV 2). I tend to go with the latter.
* name internal mod target strips in such a way that you include the modulator name and an arrow pointing to the target (LFO 1 -> PITCH, ENV 2 -> CUTOFF 1 (more on why I used 1 here later))
* name external mods like internal mod target strips (PB -> PITCH, AT -> VOLUME, RANDOM -> STARTPOINT...)

That should be clear enough for everyone, I think. If you have more than one internal modulator of the same type (say, multi LFO), Kontakt does NOT auto-number them incrementally unfortunately, and this can be a source of a lot of confusion. So make sure you rename them immediately upon instantiating them, and it should all be clearer.

But, how to rename modulators, or see their names in the first place? For this, you have to go to Script Editor, and press the Edit button there to open the text input area of the Script Editor. Now you can see the names of modulators and target strips when you right-click them. You don't have to have the Script Editor open at all times - but you have to have the text input area open. So after opening the text input area, close the Script Editor to give yourself a better overview of the instrument. I also recommend closing the Wave and Mapping Editors, as well as Group Editor, too (use Monitor->Groups tab in Kontakt's left-side browser instead of Group Editor, it's more spacious anyways).

IMPORTANT: In Kontakt 4, you could only rename modulators within one group - the renaming operation didn't pass through other editable groups (for example, when Edit All Groups is enabled), so you had to rename every modulator in every group manually (or do one group, then duplicate it - which is what I tend to do anyways, since batch-renaming things can sometimes lead to unexpected results if your modulators were added at different times and in different order, etc.). In Kontakt 5, when you have more than one group selected for editing, the renaming operation WILL be passed through all those selected groups. There are some situations that are very hard to explain in layman's termas when this doesn't happen, but it's mostly related to the abovementioned unexpected results. When you notice this situation, I recommend duplicating one of the existing groups with fully-named modulators, then pasting samples back into it.

Also, I read somewhere that people notice missing modulators after they've added them, etc. This happens when you select a modulator (it gets a yellow frame around it), then type in the Script Editor, and at some point you press the Delete key on your keyboard. Kontakt wrongly assigns keyboard focus to BOTH the Script Editor AND the instrument edit view - so you get both your character from the script deleted, AND the modulator! So be careful!!!

Let's notice one more thing regarding Kontakt 5 here. When you right-click the modulator or its target strip, you will see some numbers there (group, slot, idx). This is essentially what find_mod() and find_target() will return when used, these are the numbers that you put in set_engine_par() or get_engine_par(). So if you feel like you don't want to be bothering with this all, you can use the numbers, too. But I find that this really affects script readability in a bad way, so I always use the naming scheme. Then everything makes a lot more sense.

Before continuing onward, let me explain my naming scheme above for the case where I used "CUTOFF 1" in the modulator target name. In Kontakt, you can have up to 16 internal modulators per group. Each of those modulators can target up to 16 destinations. So let's say you have several filters loaded in Group FX, and you want one single LFO to modulate the filter cutoff in all of them. For find_mod() and find_target() to work we need to have UNIQUE names for EVERYTHING within a single group! This is why I added a number there. In the above situation, Group FX slot 1 filter cutoff would be called CUTOFF 1, slot 2 would be called CUTOFF 2, and so on. So we would have modulation target strips named like so:

LFO 1 -> CUTOFF 1
LFO 1 -> CUTOFF 2
LFO 1 -> CUTOFF 3
LFO 1 -> CUTOFF 4
etc.

This ensures unique naming and no find_mod() errors, provided you didn't do a typo of a modulator in the script, or something :) Note that we need this naming ONLY when there's a multiple of the SAME modulation links across multiple Group FX slots. So, if we have 4 filters or 4 EQs and we want to modulate the same parameter in all of them with just one modulator, this is the case when we use this incremental naming scheme. If we have an EQ in one slot, a filter in another, a Skreamer in yet another, and we have ONE modulator targetting a DIFFERENT parameter in all of them, we don't need to do this (since you would name the modulator targets differently, for example: LFO 1 -> CUTOFF, LFO 1 -> EQ GAIN2, LFO 1 -> SKR TONE...)
 
Last edited:
Let's put all this to good use now. Some examples:

1. CHANGING THE LFO FREQUENCY

Let's say our LFO is named "LFO 1" and we want to change its frequency. Let's say it's found in group 2, and that our ui_knob/slider is called $LFOFreq. Here's what we do:

Code:
set_engine_par($ENGINE_PAR_INTMOD_FREQUENCY, $LFOFreq, 1, get_mod_idx(1, "LFO 1"), -1)

That's all there's to it! Same code is used for other LFO parameters, and all envelope parameters in much the same way. $ENGINE_PAR_INTMOD_FREQUENCY applies to LFOs and step modulators.

2. CHANGING THE ENVELOPE MODULATION AMOUNT

Let's say we have an envelope modulating pitch in group 4. Our envelope is called "ENV 2" and our mod target strip is called, naturally, "ENV 2 -> PITCH" (because you're following my naming guidelines, are you? :)). Our ui_knob/slider is called $PitchEnv. We can deal with this in several ways, one of which is needlessly more complicated than others. Let's see...

* UNIPOLAR MODULATION AMOUNT

Code:
set_engine_par($ENGINE_PAR_MOD_TARGET_INTENSITY, $PitchEnv, 3, get_mod_idx(3, "ENV 2"), get_target_idx(3, get_mod_idx(3, "ENV 2"),"ENV 2 -> PITCH"))

This will target ONLY the modulation amount slider, which goes from 0 to 100%. Which means unipolar (positive) modulation happens. Notice how the code is structured: first we're telling which engine parameter we want to change, then we tell by what amount, then we point to the group we want to modify, then we tell which modulator we're pointing, and finally which modulator target strip we want to change (we repeat get_mod_idx() here because it's important for KSP to know the modulator to which the target strip is attached to). The range of our ui_knob/slider is 0 to 1000000.

* BIPOLAR MODULATION AMOUNT

This is the needlessly more complicated way: using the unipolar mod amount code and then targetting the Invert button with a separate engine parameter call. The range of our ui_knob/slider in this case is -1000000 to 1000000.

Code:
set_engine_par($ENGINE_PAR_MOD_TARGET_INTENSITY, abs($PitchEnv), 3, get_mod_idx(34, "ENV 2"), get_target_idx(3, get_mod_idx(3, "ENV 2"), "ENV 2 -> PITCH"))

if ($PitchEnv < 0)
    set_engine_par($MOD_TARGET_INVERT_SOURCE, 1, 3, get_mod_idx(3, "ENV 2"), get_target_idx(3, get_mod_idx(3, "ENV 2"), "ENV 2 -> PITCH"))
else
    set_engine_par($MOD_TARGET_INVERT_SOURCE, 0, 3, get_mod_idx(3, "ENV 2"), get_target_idx(3, get_mod_idx(3, "ENV 2"), "ENV 2 -> PITCH"))
end if

See? This is needlessly complicated. Thankfully there's another engine parameter that handles this all for itself. For whatever reason it's not documented, but I think everyone should know about it, since it offers one cool feature many people don't know about. This engine parameter is called $ENGINE_PAR_MOD_TARGET_MP_INTENSITY. Its range is 0-1000000, and this covers the ENTIRE bipolar range of the modulation amount slider, which means 0% is at 500000, 100% is at 1000000 and -100% is at 0. So, our code for bipolar modulation amount slider becomes this:

Code:
set_engine_par(ENGINE_PAR_MOD_TARGET_MP_INTENSITY, $PitchEnv, 3, get_mod_idx(3, "ENV 2"), get_target_idx(3, get_mod_idx(3, "ENV 2"), "ENV 2 -> PITCH"))

Our $PitchEnv range for this example is then, of course, 0 to 1000000.

But... that's not all! It seems that NI didn't build in any range checking for this engine parameter. Which means, we can go beyond the range of -100%~100%! This is especially important for pitch modulation, because sometimes we want to have a stronger modulation that goes beyond 12 semitones range that Kontakt allows at 100% modulation amount. So, just continue with the values, expanding the range. It follows a sort of exponential curve, so engine parameter of 2000000 is not 24 semitones, it's actually 324 semitones! So you don't want to go that high. Here's a table with some useful values:

-12 to 12 semitones -> 0 to 1000000
-24 to 24 semitones -> -130000 to 1130000
-36 to 36 semitones -> -221000 to 1221000
-48 to 48 semitones -> -293500 to 1293500

I doubt you'll need more than this. Also note that extreme pitch modulation can sometimes crash Kontakt, since this is not only an undocumented feature, but probably actually an unintentional bug on NI side. So it MIGHT be fixed at some point... but somehow I think this "exploit" is here for a reason. So there you go - this is how to extend modulation range for pitch modulations! This might or might not work for other engine parameters. Explore!

3. EXTERNAL MODULATORS

This is as simple as it gets. We can use either unipolar or bipolar modulation amount examples from above, except we do not need to use get_target_idx() at all! Just write -1 instead. Let's say we have aftertouch mapped to modulate LFO 1 frequency in group 10.

Code:
set_engine_par($ENGINE_PAR_INTMOD_INTENSITY, $ATVolume, 9, get_mod_idx(9, "AT -> LFO 1 FREQ"), -1)

That's all there's to it!


I think there's one more thing I'd like to say here, and one thing all KSPers should know. When find_mod() doesn't find a modulator with designated name, it actually returns a value of 0 instead of -1! This is actually a bug on NI side, that I don't know why isn't fixed yet. How does this manifest? Well, it manifests in such a way that it will CHANGE the value of the modulator whose slot ID is 0 REGARDLESS! This is usually amplifier envelope which is assigned by default by Kontakt. Usually it sets the modulation amount of it to 0%, which makes the instrument behave in a very weird way (envelopes don't work and notes are cut short). So, if your instrument starts behaving weirdly after a wrongly targetted find_mod(), you know what to look for!

This issue is fixed in Kontakt 7.1 onwards, with new commands get_mod_idx() and get_target_idx(). You can now query if a modulator or target exists by comparing against $NI_NOT_FOUND, or you can just use these commands outright without any fear about destroying any modulator settings and so on!

________________


I hope this has been helpful. I've probably missed some things I wanted to mention, but that's why this is a forum and this is a thread where you can leave your questions and I'll answer from time to time.


Cheers!
 
Last edited:
This is great, Mario! I agree with Greg that it should be a sticky. I can do that. (Or maybe Mario can, too?) However, maybe it's just me, but I tend to ignore stickies because they're not usually "active" topics. My eye just automatically skips them. So I'm thinking we should wait a few days, since it will be at the top anyway, then stickyize it after it starts to drop.
 
Another quirk I found out is you can't use $ALL_GROUPS as a group index in set_engine_par(). You need to parse all the groups one by one.
 
Hello dear top scripters !

I'm working on a SH-101-ish thing and I wonder how to achieve a PWM function.
I see two ways to get it : Precalculated PWM (table of values) or 2xLFOs fake PWM.
Wich one should be the lightest CPUwise / the most efficient soundwise ?
Any experience with PWM in Kontakt ?
 
Hi guys.

I need a bit of help to understand why/where I'm wrong.
Here's the 'partial' script I'm working on :


Code:
on init

     {SET: vraiables}
     declare $label_id
     declare $label_id2
     declare $label_id3    
     declare $buttonMCONTROL_id
     declare $CTime
     declare $asyncID := -1


     {SET: constants}
    declare const $LayerA_FIRST := 0
    declare const $LayerA_LAST := 19
    declare const $LayerB_FIRST := 20
    declare const $LayerB_LAST := 39
    declare const $DISP_TIME := 900


    {SET: group relative}

    declare $i
    declare ui_menu $LayerA
    declare ui_menu $LayerB
    declare %LayerAgroups[20] := (0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19)
    declare %LayerBgroups[20] := (20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39)


{---- START OF GLOBAL FUNCTIONS INIT ----}  

   $i := $LayerA_FIRST
   while ($i<=$LayerA_LAST)
     add_menu_item($LayerA,group_name($i),$i)
     inc($i)
   end while
        set_control_par(get_ui_id($LayerA),$CONTROL_PAR_FONT_TYPE,1)


   $i:= $LayerB_FIRST
   while ($i<=$LayerB_LAST)
     add_menu_item($LayerB,group_name($i),$i)
     inc($i)
   end while
        set_control_par(get_ui_id($LayerB),$CONTROL_PAR_FONT_TYPE,1)


{---- START OF LAYERS INIT ----}  

    {***** LAYER A *****}
            {LAYER A Volume : $AVOLUME}

                        declare ui_slider $AVOLUME(0,631000)
                            declare $AVOLUME_Id
                            $AVOLUME_Id := get_ui_id($AVOLUME)
                            set_control_par(get_ui_id($AVOLUME),$CONTROL_PAR_WIDTH,200)
                            set_control_par(get_ui_id($AVOLUME),$CONTROL_PAR_HEIGHT,30)
                            set_control_par_str(get_ui_id($AVOLUME),  $CONTROL_PAR_PICTURE, "HSLIDER")
                            set_control_par(get_ui_id($AVOLUME),$CONTROL_PAR_MOUSE_BEHAVIOUR,0)
                            move_control_px($AVOLUME,20,130)
                            make_persistent($AVOLUME)
                            set_control_par_str(get_ui_id($AVOLUME),$CONTROL_PAR_AUTOMATION_NAME,"LAYER A Volume")
                            {VALUE LABEL}
                            declare ui_label $A (1,1)
                            set_text ($A,"A VOLUME")
                            move_control_px($A, 20, 120)
                            set_control_par(get_ui_id($A),$CONTROL_PAR_HIDE,$HIDE_PART_BG)
                            set_control_par(get_ui_id($A),$CONTROL_PAR_FONT_TYPE,1)


            {LAYER A Volume Envelope : $AVOLENV}

                        declare ui_slider $AVOLENV(0, 1000000)
                            set_control_par(get_ui_id($AVOLENV),$CONTROL_PAR_WIDTH,150)
                            set_control_par(get_ui_id($AVOLENV),$CONTROL_PAR_HEIGHT,150)
                            set_control_par_str(get_ui_id($AVOLENV),  $CONTROL_PAR_PICTURE, "LiveKnob150")
                            set_control_par(get_ui_id($AVOLENV),$CONTROL_PAR_MOUSE_BEHAVIOUR,-500)
                            move_control_px($AVOLENV,295,129)
                            make_persistent($AVOLENV)  
                            set_control_par_str(get_ui_id($AVOLENV),$CONTROL_PAR_AUTOMATION_NAME,"LAYER A VOL ENVL")
                            {VALUE LABEL}
                            declare ui_label $A7 (1,1)
                            set_text ($A7,"A VOL ENVL")
                            move_control_px($A7, 325, 107)
                            set_control_par(get_ui_id($A7),$CONTROL_PAR_HIDE,$HIDE_PART_BG)
                            set_control_par(get_ui_id($A7),$CONTROL_PAR_FONT_TYPE,1)


{*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* CALLBACK *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*}



on ui_control ($LayerA)
   $i := $LayerA_FIRST
   while ($i<=$LayerA_LAST)
     purge_group($i,0)
     inc($i)
   end while
   purge_group($LayerA,1)
end on


    {/////LAYER RELATIVE\\\\\}
   
{---- START OF LAYERS CALLBACK ----}  

    {***** LAYER A *****}

            {LAYER A Volume : $AVOLUME}

    on ui_control ($AVOLUME)
    $i := 0
    while ($i < 19)   { loops through all array elements }
        set_engine_par($ENGINE_PAR_VOLUME,$AVOLUME,%LayerAgroups[$i],-1,-1)
        inc($i)

end while
end on


            {LAYER A Volume Envelope : $AVOLENV}

    on ui_control ($AVOLENV)
    $i := 0
    while ($i < 19)   { loops through all array elements }
        set_engine_par($ENGINE_PAR_MOD_TARGET_INTENSITY,$AVOLENV,%LayerAgroups[$i],find_mod(0,"LayerA_ENV_to_VOL"),find_target(0,"LayerA_ENV_to_VOL"),"LayerA_ENV_to_VOL"))
        inc($i)

end while
end on

The problem is in the bottom zone of the callback. Kontakt tells me "Expression expected" but I too blind to see where the hell is the damn expression lacking.
Need your help guys.

Thanks a lot.
 
Here's a table with some useful values:

-12 to 12 semitones -> 0 to 1000000
-24 to 24 semitones -> -130000 to 1130000
-36 to 36 semitones -> -221000 to 1221000
-48 to 48 semitones -> -293500 to 1293500
Has anyone expanded on these values to include the intermediate semitones? I want to make a pitchbend control where you can select the semitone. Is there some math formula that I can get exact pitch values? Thanks!
 
Top Bottom