What's new

LFO from different groups slightly out of phase [SOLVED via KSP]

Thanks. Now I've got enough matter to fuel more experiments...
I'll post my solution here later, in the spirit of sharing.
 
Ok, I'm now calculating the vibrato's detune factor globally, but applying it polyphonically. I haven't programmed the fade-in yet, but that should be easy.

Code:
on init
  declare ~lfo := 0.0
  declare polyphonic $buffer
  declare $detune
  declare ~vibrato_period := 200.0                    { expressed in listener's cycles }
  declare ui_slider $vibrato_rate (0,127)
  declare ui_slider $vibrato_cents (0,120)
  set_listener ($NI_SIGNAL_TIMER_MS, 10000)
end on

on ui_control ($vibrato_rate)
  ~vibrato_period := 5.0 + 295.0 * pow ((127.0-int_to_real($vibrato_rate)) / 127.0, 4.0)
  end on

on listener
  ~lfo := ~lfo + 2.0 * ~NI_MATH_PI / ~vibrato_period
  if (~lfo >= 2.0 * ~NI_MATH_PI)
    ~lfo := ~lfo - 2.0 * ~NI_MATH_PI
  end if

  $detune := real_to_int (sin (~lfo) * 1000.0) * $vibrato_cents
end on

on note
  while (event_status ($EVENT_ID) # $EVENT_STATUS_INACTIVE)
    change_tune ($EVENT_ID,  $detune - $buffer  , 1)
    $buffer := $detune
    wait (10000)
  end while
end on

It sounds like what I want, and the tune interval between notes even remains constant. Swell!

However, there's a major problem: it would seem the event status never becomes inactive. (According to another test, it remains equal to $EVENT_STATUS_NOTE_QUEUE indefinitely.) So after many notes, the system chokes up and can't generate audio in real time anymore. [UPDATE: as explained in the followings posts, there might be a bug in my particular version of Kontakt. Tests in other versions show that this script works well in fact.]

I noticed this problem because at first I erroneously wrote wait (10) instead of wait (10000), which made the choke occur after only a few dozen notes ;)

Replacing the while loop's condition with while ($NOTE_HELD = 1) avoids that problem, but then the vibrato doesn't continue during the volume envelope's release phase, if there's one. So it's not a very satisfying alternative.

I suppose I could fade the vibrato out at a chosen rate after note off, but I wonder: What could be the reason(s) why every note remains active forever (and I actually mean forever)?

I greatly appreciate the help, btw.

(And btw2, I realize this method will limit the overall polyphony quite much, as it requires much processing... But it does sound good :))
 
Last edited:
Hmmm doesn't make sense that an event would never be set as inactive... This simple test shows that it's working:

Code:
on init
   declare $ID
   set_listener($NI_SIGNAL_TIMER_MS, 1000)
end on

on note
   $ID := $EVENT_ID
end on

on listener
   message(event_status($ID))
end on


I suppose it might depend if there is some note processing happening in other script slots, hmmm... But still, weird. I'm trying it with play_note() in preceeding script slot, it's working fine, regardless of play_note() mode (-1, 0 or timed length)... Hmmmm!
 
I've just implemented your status debugger and... Even with all other scripts bypassed, the status remains active forever.

I suppose there's something in my groups that makes this happen, but I can't see what. I have hundred of groups, but they're all pretty basic.

Normally, the status should toggle to inactive when either all envelopes have been played out, or all samples have been played to their end - whatever happens first, right? (and my samples have no loop)

[UPDATE: I've just used your script in a newly created instrument with only one group and one single-shot sample, and the status still remains active forever, even though the voice count quickly gets back from one to zero. My Kontakt version is 5.7.0]

[UPDATE: I just tried it in an old Kontakt version 5.2 I still keep installed for debugging purposes, and there it works as it should. So the problem may be that I've got a buggy build of 5.7 :sad:]

[One more UPDATE: Now when I load the test nki I generated in 5.2 into 5.7, it works all right. Weird... Anyway the conclusion of the story is: the posted script does in fact work.)
 
Last edited:
I'm trying this in 5.8.1 and 6. Works fine in both.

Also tried in 5.7.3 and it works as well. In a minimal instrument, too.
 
Ok, let's leave it at that. I'll update, that's all there is to it.

Thanks for the help !!!
 
Alright. Sorry, i may have lost focus on the issues, but...

Code:
on note
  while (event_status ($EVENT_ID) # $EVENT_STATUS_INACTIVE)
    change_tune ($EVENT_ID,  $detune - $buffer  , 1)
    $buffer := $detune
    wait (10000)
  end while
end on

...has a bug?

And ($NOTE_HELD = 1) will break the release stage, which is expected.

So, maybe set a flag on the note callback and copy the $EVENT_ID to a new variable.

Then use the listener to set the tuning.(after the lfo calculations)
Code:
  if (event_status ($EVENT_ID) # $EVENT_STATUS_INACTIVE)
    change_tune ($ID,  $detune - $buffer  , 1)
    $buffer := $detune
end if
end on

EDIT: Sorry, missed the previous posts. Please ignore as there's probably nothing to gain from this.
Cheers
 
Alright. Sorry, i may have lost focus on the issues, but...

Code:
on note
  while (event_status ($EVENT_ID) # $EVENT_STATUS_INACTIVE)
    change_tune ($EVENT_ID,  $detune - $buffer  , 1)
    $buffer := $detune
    wait (10000)
  end while
end on

...has a bug?

And ($NOTE_HELD = 1) will break the release stage, which is expected.

So, maybe set a flag on the note callback and copy the $EVENT_ID to a new variable.

Then use the listener to set the tuning.(after the lfo calculations)
Code:
  if (event_status ($EVENT_ID) # $EVENT_STATUS_INACTIVE)
    change_tune ($ID,  $detune - $buffer  , 1)
    $buffer := $detune
end if
end on

EDIT: Sorry, missed the previous posts. Please ignore as there's probably nothing to gain from this.
Cheers

Yes, something like that should work.

However, I'll do a variant: in the note callback, I'll put a second loop after the while($NOTE_HELD=1) loop, which will fade out the LFO for a preset amount of time, before breaking into oblivion. That way, I'll have vibrato fade in and fade out :). It wasn't really in the plans, and it's probably unnecessary as a workaround since the bug is only anecdotic, but it will actually be an improvement, so... Cheers!
 
in the note callback, I'll put a second loop after the while($NOTE_HELD=1) loop, which will fade out the LFO for a preset amount of time, before breaking into oblivion.

I see. Any reason why you can't do this on RCB?
Having sequential loops on the NCB has sometimes resulted in unexpected results for me in the past.
 
I see. Any reason why you can't do this on RCB?
I suppose I could, but doing everything in the same callback guarantees continuity... And, because of the bug I encountered earlier, I choose to monitor the release from within the note callback anyway.

And here it is. Finally! - Tadaa:

Code:
on init
  declare ~lfo := 0.0
  declare $detune
  declare ~vibrato_period := 50.0                     { expressed in listener's cycles (centiseconds) }
  declare ui_slider $vibrato_rate (0,127)
  declare ui_slider $vibrato_cents (0,120)
  set_listener ($NI_SIGNAL_TIMER_MS, 10000)
  declare $fade_in := 300                             { expressed in listener's cycles }
  declare $fade_out := 150                            { expressed in listener's cycles }
  declare ~fade_factor
  declare polyphonic $fade_counter
  declare polyphonic $detune_buffer
  declare polyphonic $note_detune
end on

on ui_control ($vibrato_rate)
  ~vibrato_period := 5.0 + 295.0 * pow ((127.0-int_to_real($vibrato_rate)) / 127.0, 4.0)
end on

on listener
  ~lfo := ~lfo + 2.0 * ~NI_MATH_PI / ~vibrato_period
  if (~lfo >= 2.0 * ~NI_MATH_PI)
    ~lfo := ~lfo - 2.0 * ~NI_MATH_PI
  end if
  $detune := real_to_int (sin (~lfo) * 1000.0) * $vibrato_cents
end on

on note
  while ($NOTE_HELD = 1)
    $detune_buffer := $note_detune
    ~fade_factor := int_to_real ($fade_counter) / int_to_real ($fade_in)
    $note_detune := real_to_int (int_to_real ($detune) * ~fade_factor)
    change_tune ($EVENT_ID,  $note_detune - $detune_buffer , 1)
    wait (10000)
    if ($fade_counter < $fade_in)
      inc ($fade_counter)
    end if
  end while
  $fade_counter := $fade_out * $fade_counter / $fade_in
  while ($fade_counter >= 0)
    $detune_buffer := $note_detune
    ~fade_factor := int_to_real ($fade_counter) / int_to_real ($fade_out)
    $note_detune := real_to_int (int_to_real ($detune) * ~fade_factor)
    change_tune ($EVENT_ID,  $note_detune - $detune_buffer , 1)
    wait (10000)
    dec ($fade_counter)
  end while
end on
[EDIT: the variable ~fade_factor should really be polyphonic, but there are no such thing as polyphonic reals [[EDIT: See @Lindon 's solution below]]. It's ok though, because there's such a short time between write and read that the chances of it being overwritten by another note callback is negligible. But note that this variable is not even necessary: it was just meant to split a calculation over two lines of code, to make it more readable to Humans. You can just as well condense the calculation into one single line and get rid of ~fade_factor.]

Note that when I started this thread I had no intention of doing any of this in KSP :grin:. And, probably because I am a beginner, I am still amazed at what it can do !
 
Last edited:
While following this interesting subject, I will have added a label to view the LFO's rate but can n't. :sad:

Code:
declare ui_label $rate_vibrato_label (1,1)
    set_text ($rate_vibrato_label,"")
    set_control_par (get_ui_id($rate_vibrato_label), $CONTROL_PAR_TEXT_ALIGNMENT, 1)
    set_control_par (get_ui_id($rate_vibrato_label), $CONTROL_PAR_FONT_TYPE, 24)
    set_control_par (get_ui_id($rate_vibrato_label), $CONTROL_PAR_WIDTH, 75)
    set_control_par (get_ui_id($rate_vibrato_label), $CONTROL_PAR_TEXTPOS_Y,2)
    set_text ($rate_vibrato_label, "LFO RATE")

on ui_control ($vibrato_rate)
  $vibrato_period := 5 + real_to_int (995.0 * pow ((129.0-int_to_real($vibrato_rate)) / 129.0 ,1.75) )
  set_text ($rate_vibrato_label, get_engine_par_disp ($ENGINE_PAR_INTMOD_FREQUENCY ,-1, 0, -1))
  wait (2500000)
  set_text ($rate_vibrato_label, "LFO RATE")

end on
 
Use find_mod() to point to the correct modulator. Read my sticky thread about modulation in KSP. :)

Also not sure why you're using that vibrato_period math and then getting an engine parameter... do you even have an LFO modulator in the instrument, or?

If you want to use get_engine_par_disp() then you have to set the LFO frequency with set_engine_par(), obviously. :)
 
While following this interesting subject, I will have added a label to view the LFO's rate but can n't. :sad:

Code:
declare ui_label $rate_vibrato_label (1,1)
    set_text ($rate_vibrato_label,"")
    set_control_par (get_ui_id($rate_vibrato_label), $CONTROL_PAR_TEXT_ALIGNMENT, 1)
    set_control_par (get_ui_id($rate_vibrato_label), $CONTROL_PAR_FONT_TYPE, 24)
    set_control_par (get_ui_id($rate_vibrato_label), $CONTROL_PAR_WIDTH, 75)
    set_control_par (get_ui_id($rate_vibrato_label), $CONTROL_PAR_TEXTPOS_Y,2)
    set_text ($rate_vibrato_label, "LFO RATE")

on ui_control ($vibrato_rate)
  $vibrato_period := 5 + real_to_int (995.0 * pow ((129.0-int_to_real($vibrato_rate)) / 129.0 ,1.75) )
  set_text ($rate_vibrato_label, get_engine_par_disp ($ENGINE_PAR_INTMOD_FREQUENCY ,-1, 0, -1))
  wait (2500000)
  set_text ($rate_vibrato_label, "LFO RATE")

end on
Rate=1/Period
Of course here the period is expressed in listener's cycles, which are 0.01s appart. So to get the rate in seconds, do:
Rate=100/Period (I think)

Btw, you may want to use the last version of the script I posted: The period is now a real number, which makes it both more precise and more flexible (@EvilDragon : that's why I kept resetting the phase counter with -2PI rather than =0. Because now the period is not necessarily a multiple of the listener's interval anymore)

And I realise we could slightly simplify the math in the listener by declaring ~TAU := 2.0 * ~NI_MATH_PI once and for all, in the ICB.

Btw2, does anyone know a way to convert a real number to a string - or any other way to display a real number in a label?
Just set_text($anywhere,~whatever) seems to display gibberish. (Or is it a problem with my version again?)
 
Last edited:
Well you seem not to need this: " the variable ~fade_factor should really be polyphonic, but there are no such thing as polyphonic reals." from your explanation, but as you are in the note call back when you use ~fade_factor...

you could do this on those occasions where you do need it:

?fade_factor[$EVENT_NOTE]

to effectively get polyphonic versions of reals.
 
Well you seem not to need this: " the variable ~fade_factor should really be polyphonic, but there are no such thing as polyphonic reals." from your explanation, but as you are in the note call back when you use ~fade_factor...

you could do this on those occasions where you do need it:

?fade_factor[$EVENT_NOTE]

to effectively get polyphonic versions of reals.
Indeed! Thanks.
 
Use find_mod() to point to the correct modulator. Read my sticky thread about modulation in KSP. :)

Also not sure why you're using that vibrato_period math and then getting an engine parameter... do you even have an LFO modulator in the instrument, or?

If you want to use get_engine_par_disp() then you have to set the LFO frequency with set_engine_par(), obviously. :)

OK, if I understand fine, your pedagogic thread available for internal modulators already assigned: because currently it is a purely "KSP" modulation.
I tried adding but this is valid for a modulation already declared, open internally in the instrument.


Code:
declare $lfo_idx
$lfo_idx := find_mod(1,"LFO_SINE_VOLUME")

on ui_control ($vibrato_rate)
  $vibrato_period := 5 + real_to_int (995.0 * pow ((129.0-int_to_real($vibrato_rate)) / 129.0 ,1.75) )
  set_text ($rate_vibrato_label, get_engine_par_disp ($ENGINE_PAR_INTMOD_FREQUENCY ,0, $lfo_idx, -1))
  wait (2500000)
  set_text ($rate_vibrato_label, "LFO RATE")

I will re-read your answer because don't understand everything. :sad:
 
Lately, @thecld asked me for some clarification about the code I posted in this thread, in a private conversation. Since my answer was enxtensive and -dare i say- potentially useful to others, I'm posting both it and his leading question here, with his permission.

All the following is a "condensing" of my earlier posts in this thread, and the code it refers to is the one from this post: https://vi-control.net/community/th...ut-of-phase-solved-via-ksp.82645/post-4396351

Hi Fredeke,

I'm trying to learn / understand how to build a scripted LFO - I've come across your thread about LFOs being out of sync and I'm trying to understand how do you created that formula for phase increment (I believe that that's the part that I should be interested in if I want to avoid glitches when changing the LFO rate?):

Code:
on ui_control ($vibrato_rate)
  $vibrato_period := 5 + real_to_int (995.0 * pow ((129.0-int_to_real($vibrato_rate)) / 129.0 ,1.75) )
end on

That number: 1.75 -> from where does it come from? I can understand that 5+995 = original vibrato_period and 129.00 = vibrato_rate.
I've checked out that site that EvilDragon has linked so I know where you get that idea but I'm having hard time understanding it completely to build my own version of it... :)

Hope you don't the question, would really appreciate any help if you have the time, thanks in advance!
Roman

Hi

You understand the principle well: add the period to a counter, and derive pitch changes from a sine function of that counter (or a triangle function, or whatever you want your LFO's shape to be).
Also, set your counter back -2PI every time it gets greater than 2PI, just to avoid its value getting too large for KSP to handle over time.

But all that is in the listener callback, not here.

This is the ui_control callback. It's really a sophistication you don't need: It's not part of the LFO engine, it's just my overcomplicated way of controlling the LFO's speed.

Apparently my LFO engine needs a period rather than a rate - I can't remember why. So if you want to adapt it, I suggest you start with a simple "period" ui control and no ui_control callback.

So, all I'll be explaining from now on is dispensable, and can be implemented later if at all.
I'll explain it anyway, but only because I like the sound of my own voice.

Me, I wanted to provide users with a "rate" knob instead of a "period" knob (to emulate vintage synthesizers).

Remember: Period = 1 / Rate

So if you want to provide a "rate" control instead of a "period" control, you could just do that. Just make sure your "rate" knob can't reach 0, because you can't divide by 0.

Here's why, in my case, the formula became much more complicated:

In my instrument, all controls used 0-to-127 values to mimic MIDI. They didn't necessarily have to, but I had decided that early on in development, and it was too late to change it. So the whole formula you mention is just meant to translate a 0-to-127 rate into a time unit based period.

At this point, you can probably stop reading. But I'll explain the formula anyway:

Let's read that line from $vibrato_rate (the knob) outwards to $vibrato_period (the value needed in the actual LFO engine which is in the listener callback):

- The knob has values from 0 to 127, so (127-$vibrato_rate) would invert it, and I guess I wanted it inverted. I suppose that constant 127 became 128 because I wanted to avoid a zero result and then it became 129 just because I liked the feel of that response better.

- Then there's that division by 129 which is meant to get the knob's position in form of a real number between 0 and 1, instead of an integer from 0 to 127, just for my sanity's sake.

- Then I though the knob's response was too sensitive to my taste at one end of its course, and not precise enough at the other end, so I applied a squared (= power of 2) response to it. But then it felt overcorrected so I settled on a power of 1.75. (The neat thing is that when you elevate a number comprised between 0 and 1 to any power, the result is still comprised between 0 and 1)

- Then I needed a value in seconds, or milliseconds, or microseconds - I can't remember which - for the period. Anyway, it required multiplying that 0<x<1 number by 1000. I split 1000 into 995 and 5, because I needed a tiny offset (5), because really, a period of 0 ms would make no sense and have no use. Of course, a multiplier of 995 is not 1000 (and I suppose it could just as well have been 1000), but I'm guessing either it made no difference in practice, or maybe I even liked it better that way.

Again, I arrived at some of those numbers by trial and error.
I hope this helps.
 
Last edited:
Top Bottom