What's new

Syncing Cos(x) Period to Tempo

Hi everyone,

I'm trying to use cos(x) or sin(x) to make an LFO and i'm having trouble syncing it. I'm pretty sure that i'm just having a brainfart on the trig side. At this time I just want the period to match some even divider of the tempo (1/4 note, 1/8th note). Anyone have any idea what i'm doing wrong?

First I declare some storage values to store the BPM and the ms length of each beat, this seems to work fine. Then I set the listener to trigger what I believe is every 1/2 beat.

Code:
declare $Tempo
declare $Tempo_ms
declare ~Tempo_ms_real
$Tempo := ms_to_ticks(60000000)/960
{$Tempo_ms := ticks_to_ms($Tempo)}
~Tempo_ms_real := 60000.0 / int_to_real($Tempo)
$Tempo_ms := real_to_int(~Tempo_ms_real)

set_listener($NI_SIGNAL_TIMER_BEAT,2 ) {Listen every 1/2 beat?}

Further down....

In this case I want my wave centered around 0.5 and to oscillate between 1 and 0 (so 0.5 +cos()/2). Then inside the cos() function i'm dividing by 2pi to get the period to be 1*x. Then every iteration of the listener call I have a counter ~x which is incremented by the length of 1 beat in seconds, this is what gets put into the cos() function as x. Then i'm just using that value to adjust pan.

Code:
on listener

?myXY[0] := 0.5 + cos(~x / 2.0 / 3.14159) /2.0  {1st cursor, X axis}
{?myXY[1] := (?myXY[1] + cos(~x*2.0) / 10.0)  {1st cursor, X axis}
~x := ~x+~Tempo_ms_real/1000.0 {Increment seconds every 1/2 beat}
{~x := ~x + 0.1}
    set_engine_par($ENGINE_PAR_PAN, real_to_int(?myXY[0] * 1000000.0), $menu,-1,-1)
    set_engine_par($ENGINE_PAR_VOLUME, real_to_int(?myXY[1] * 1000000.0), $menu,-1,-1)
message (?myXY[0])
end on

It oscillates, and I can control the speed. But its definitely not synced correctly. I also tried this for the cos() calculation:

?myXY[0] := 0.5 + cos(~x / 2.0 / 3.14159 * ~Tempo_ms_real/1000.0) /2.0

Which I think would then set the period to the amount of time for one beat, but that still doesn't seem to be working.

Anyone have any ideas what i'm doing wrong?

Thanks,
--
Andrew
 
The way you set it up is that the listener runs only twice in a single quarter note. So you only get two increments for the LFO.

You need to run the listener very fast if you want your LFO to work smoothly. Preferably in MS instead of BEAT mode, and refreshing it something like every 5 milliseconds or something.


Also, we do have a PI constant you can use, it's ~NI_MATH_PI.
 
The way you set it up is that the listener runs only twice in a single quarter note. So you only get two increments for the LFO.

You need to run the listener very fast if you want your LFO to work smoothly. Preferably in MS instead of BEAT mode, and refreshing it something like every 5 milliseconds or something.


Also, we do have a PI constant you can use, it's ~NI_MATH_PI.

oh okay that makes sense. Thanks.

Any best practices on how to handle the cos or sin function to get it synced? I'm surprised there aren't more examples online doing this, I dug around for an hour haha o_O
 
Try this:

Code:
on init
   make_perfview
   set_ui_height(5)

   declare ~TAU := 2.0 * NI_MATH_PI

   declare ~val

   declare ui_xy ?pad[2]
   pad -> width := 200
   pad -> height := 200
   move_control_px(pad, 250, 2)

   declare ui_knob Freq (0, 1000, 100)
   Freq := 100

   set_listener($NI_SIGNAL_TIMER_MS, 1000)
end on

{ time: integer time argument in milliseconds, usually ENGINE_UPTIME
  freq: frequency in Hz
  wave: 0 for sine, 1 for cosine }
function do_LFO(time, freq, wave) -> result
   declare local ~t
   declare local ~f := freq
   declare local ~phase

   t := int_to_real(time) / 1000.0

   select wave
       case 0
           phase := sin(TAU * f * t)
       case 1
           phase := cos(TAU * f * t)
   end select

   result := phase
end function

on listener
   val := do_LFO(ENGINE_UPTIME, int_to_real(Freq) / 100.0, 0)
   pad[0] := val / 2.0 + 0.5

   val := do_LFO(ENGINE_UPTIME, int_to_real(Freq) / 100.0, 1)
   pad[1] := val / 2.0 + 0.5
end on


This is working with Hz, but it's easy to recalculate the Hz value when you know the BPM and the note duration you want to have for one period of the LFO.
 
Try this:

Code:
on init
   make_perfview
   set_ui_height(5)

   declare ~TAU := 2.0 * NI_MATH_PI

   declare ~val

   declare ui_xy ?pad[2]
   pad -> width := 200
   pad -> height := 200
   move_control_px(pad, 250, 2)

   declare ui_knob Freq (0, 1000, 100)
   Freq := 100

   set_listener($NI_SIGNAL_TIMER_MS, 1000)
end on

{ time: integer time argument in milliseconds, usually ENGINE_UPTIME
  freq: frequency in Hz
  wave: 0 for sine, 1 for cosine }
function do_LFO(time, freq, wave) -> result
   declare local ~t
   declare local ~f := freq
   declare local ~phase

   t := int_to_real(time) / 1000.0

   select wave
       case 0
           phase := sin(TAU * f * t)
       case 1
           phase := cos(TAU * f * t)
   end select

   result := phase
end function

on listener
   val := do_LFO(ENGINE_UPTIME, int_to_real(Freq) / 100.0, 0)
   pad[0] := val / 2.0 + 0.5

   val := do_LFO(ENGINE_UPTIME, int_to_real(Freq) / 100.0, 1)
   pad[1] := val / 2.0 + 0.5
end on


This is working with Hz, but it's easy to recalculate the Hz value when you know the BPM and the note duration you want to have for one period of the LFO.

Excellent, this works great! Got it to work in my vanilla KSP script, is this that Sublime KSP lingo?

I'm going to make this so it starts the LFO when notes are played, but I think I see a clear path forward doing that just by setting some flags when on note is run and doing some artful subtraction on the time values. Thanks for the help, this opens up a world of possibilities.
 
Yes this is SublimeKSP lingo :grin:

Retriggering the LFO would be simply changing the "time_arg" part and adding another argument to the function.

Code:
function do_LFO(time, freq, wave, retrigger) -> result
   declare local ~t
   declare local ~f := freq
   declare local ~phase

   if retrigger = 1
      t := int_to_real(time - key_pressed_time) / 1000.0
   else
      t := int_to_real(time) / 1000.0
   end if
...

key_pressed_time is a variable you would set in note callback, also using ENGINE_UPTIME.
 
Nice, eventually I plan to switch over to that. Figured i'd try to do things the hard way first before trying to learn a syntax that differs from the documentation.

Ah I see, so when you hit a note you capture the Engine Time, and then just subtract if off inside a conditional that is triggered by new notes being pressed. By my calculation it sounds like Engine Time should go up to about 25 days of time in ms (2^31 / 1000ms / 60s / 60m / 24hr), starting from the time that either Kontakt was opened in the current session.

Does it naturally reset or overflow at that point? Just thinking of the case where someone leaves their DAW session open for weeks on end, and then their LFO goes out of wack at a bad time (very, very, very unlikely though). If it resets thats probably fine due to the unlikely nature of it being an issue. I leave DAW sessions open for days at a time, but I don't think ever for 25 days.
 
IIRC it overflows. If you want to be absolutely safe, you'd probably want to modulo it with some value. But I think the case for having a DAW session open for weeks on end is nearly infinitesimally small. :)
 
Last edited:
Mario, why am I getting crazy shit when I adjust the Frequency of this LFO? Be much better if it just sped up or slowed down as you might expect. ??? cheers, Dan
 
It's probably because the phase is in a different place when you change the frequency, related to the time argument of the function...
 
You need to set it up differently in order to handle frequency change smoothly. Instead of feeding the sin/cos functions straight with the frequency, you want to use a phasor and increment it upon every round of LCB. Basically, you need a Numerically Controlled Oscillator (NCO).
The concept is: describe the phase of the wave with a linear function going from 0 to 2π. This may be done by simply using 'mod' (even better, math.fmod if you are using Koala, so you spare some boring math rescaling) and some real math to basically obtain a value that increments itself at a given rate (which is the rate of your LFO) and is reset when reaches 2π. Then you simply feed this phase value straight to the sin/cos function.

https://zipcpu.com/dsp/2017/12/09/nco.html
 
Thanks Davide! I also see you have another post about this. Will give it a try today.


(not much) Later: Perfect! Much easier than I thought. Thanks again!
 
Last edited:
Top Bottom