# On Listener callback and realtime tempo changes



## magneto538 (Nov 15, 2017)

Hey folks,
I'm working on a step sequencer that has to be started when a MIDI key is pressed. This means no sync to the DAW transport, but obviously I need the tempo information coming from the DAW.
I need the sequencer to be able to handle realtime tempo changes. This means that when the BPM changes, the sequencer would just slow down/accelerate instead of jumping to another value. Basically I have to keep track of the "phase" of the sequencer's progress instead of having something like _ENGINE_UPTIME - engine_uptime_stored_upon_sequencer_start._
The solution I found so far is:

- The resolution of the Listener callback would be ticks_to_ms(1). By doing so, not only I am linking the Listener to the tempo, but potentially I am also saving some CPU cycles (the slower the BPM, the lower the resolution of the Listener callback). Obviously, inside the Listener callback I have a function that changes the Listener parameter whenever the BPM changes, so it's always in sync.

- Inside the Listener callback, I have a variable _time_ that is incremented by 1 at every call to the Listener. Since the resolution of the Listener callback is 1 tick, this means that - theoretically - _time_ would store the time elapsed since the start of the sequencer (in ticks). When I change the BPM of the DAW, the resolution of the Listener increases/decreases accordingly, the rate of the increment of _time_ changes and results effectively in slowing down/accelerating the sequencer.

By using this solution, the sequencer DOES slow down according to the BPM changes. The thing is - it runs a little faster than it should. Basically, if I check the sequencer's speed with a metronome (loaded in the same DAW, so no sync issues), I see that it always accelerates compared to the metronome.

This makes me question about the actual duration of the Listener callback (assuming that it changes slightly between each call to the callback by some microseconds). Instead of incrementing by 1, I also tried to keep track of the actual duration of each cycle of Listener and use it as increment, but didn't work. I also tried to change the Listener parameter (thus, the increment value), but after a while the sequencer would still run a bit faster than it's supposed to.

Ideas?


----------



## EvilDragon (Nov 15, 2017)

NI products like West Africa, India, etc., don't seem to have problems with DAW tempo changes (regardless of being synced to DAW transport or not!) so I would advise you to check the code of those if you have them (that is, if you have Komplete). They are using something like solution 2 you're talking about - they just count ticks, actually. So regardless of the tempo, a certain number of ticks will always be worth a certain step of the sequence. Then there's even no need to check for host BPM using $DURATION_QUARTER, unless you're doing half/double time tricks (which those libraries do, so you might as well check that out).


----------



## derstefmitf (Nov 15, 2017)

When I scripted a mulit-arpeggiator engine I didn't use the listener callback. Instead I use the old blip note trick to trigger parallel running function from the release callback and I simply use the $DURATION_X variables as the wait duration between the steps of each arp. Runs stable. I know that the listener callback is the way to go for time related stuff, but because I needed separated, but parallel running arps, I went the other way and it works just fine.


----------



## magneto538 (Nov 15, 2017)

derstefmitf said:


> When I scripted a mulit-arpeggiator engine I didn't use the listener callback. Instead I use the old blip note trick to trigger parallel running function from the release callback and I simply use the $DURATION_X variables as the wait duration between the steps of each arp. Runs stable. I know that the listener callback is the way to go for time related stuff, but because I needed separated, but parallel running arps, I went the other way and it works just fine.



I'm sure it works, but the listener callback is there to perform these very operations. Plus, it gives you a lot more flexibility and precision when it comes to shift events over time (both back and forth). I'm running 16 parallel step sequencers from on listener and they work just fine - apart from my little problem here, obviously.

I'm not a fan at all of using wait() commands for this purpose and the blip note to create a loop between on note and on release. Actually, the listener callback does exactly that, but you keep all the realtime-related script completely separate from note/release stuff. And you don't have all the voices generated from the blip notes, of course. I don't see any advantage in creating a loop between the two callbacks - we already have one dedicated to that. You could also use wait() in it, although I'd do it differently to be honest.



EvilDragon said:


> NI products like West Africa, India, etc., don't seem to have problems with DAW tempo changes (regardless of being synced to DAW transport or not!) so I would advise you to check the code of those if you have them (that is, if you have Komplete). They are using something like solution 2 you're talking about - they just count ticks, actually. So regardless of the tempo, a certain number of ticks will always be worth a certain step of the sequence. Then there's even no need to check for host BPM using $DURATION_QUARTER, unless you're doing half/double time tricks (which those libraries do, so you might as well check that out).



The ones I'm talking about are two different parts of the same solution actually  I changed the numbers with bullets so perhaps it's clearer. Aren't the scripts from West Africa etc. locked with password?


----------



## EvilDragon (Nov 15, 2017)

Nope, in majority of NI products, if you can get into edit mode of the instrument, scripts aren't locked at all.


----------



## EvilDragon (Nov 15, 2017)

derstefmitf said:


> but because I needed separated, but parallel running arps,



This is totally possible with listener callback, too. I do this in Wide Blue Sound Orbit/Eclipse. It's just a matter of counting a different number of ticks for each sequencer that's running (=different rate for each sequencer), and using a taskfunc in case wait() is needed.


----------



## magneto538 (Nov 16, 2017)

So I did some tests and the outcome is, frankly, a bit scary.

First of all, I managed to change my script in order to match exactly the way West Africa deals with each step. Instead of worrying about the time elapsed since the sequencer was triggered (as I used to do before), I am now counting the duration of each step in ticks and triggering the note of the sequencer only when the counter is reset, which should be more reliable.

Regardless, I am still encountering timing issues. I ran some tests on my library and what I saw is, the triggering of each next step happens a few ticks before it should. This parameter varies according to the BPM (and also changes a bit in an apparently random way) and becomes relevant as I enhance the resolution of the LCB (up 10-15 ticks per each step with the Listener par set at ticks_to_ms(1)). At first I thought about some bottleneck in my script, but then I tried this very simple piece of script:


```
on init
    set_listener($NI_SIGNAL_TIMER_BEAT, 2)    {1 = quarters, 2 = eighths, 4 = sixteenths etc.}

    declare $time_ms
    declare $time
    declare @t
end on

on listener
    $time := ($KSP_TIMER - $time_ms)
    $time_ms := $KSP_TIMER
    @t := @t & " " & ms_to_ticks($time)
    message(@t)
end on
```

Basically, this script posts to the message the time (in ticks) that passed from the previous call to the LCB. If the LCB was working fine, you should read "480" regardless (the LCB is called with a rate of 2 ticks/beat, so every 1/8 note). You will immediately notice that this is not the case. The numbers change according to the BPM and honestly seem to follow some pattern, although it's still unclear to me. The test was run on Kontakt 5.4.2, 5.5.2 and 5.6.5, both in VST and standalone versions, with the exact same outcome.

I also tested the same script with the LCB rate I am planning to use (and some adjustments to fit the new logic). Things get even worse here - I assume that's because of the higher rate itself.


```
on init
    message("")
    set_listener($NI_SIGNAL_TIMER_MS, ticks_to_ms(1))

    declare $time_ms
    declare $time
    declare @t
    declare $ticks := 0
end on

on listener
    change_listener_par($NI_SIGNAL_TIMER_MS, ticks_to_ms(1))

    if ($ticks = 0)
        $time := ($KSP_TIMER - $time_ms)
        $time_ms := $KSP_TIMER
        @t := @t & " " & ms_to_ticks($time)
        message(@t)
    end if

    $ticks := ($ticks+1) mod ms_to_ticks($DURATION_EIGHTH)
end on
```

Last but not least, I ran some tests on West Africa library. I loaded it inside a DAW, set the host sync options to OFF and played a MIDI sequence. After some time, you DO hear some time shift. It happens after a while because the rate of the LCB is lower than my library, but it happens. When you turn the DAW sync options on, anyways, the problem is solved.


----------



## EvilDragon (Nov 16, 2017)

I am not sure if using $KSP_TIMER is 100% reliable...


----------



## magneto538 (Nov 16, 2017)

EvilDragon said:


> I am not sure if using $KSP_TIMER is 100% reliable...



Isn't it supposed to be used to "test the efficiency of script and not to make musical calculations" (quote from the manual)? How come it is unreliable?

As unreliable as it may be, anyways, the data I read match what I am experiencing.


----------



## andreasOL (Nov 16, 2017)

KSP_TIMER is not reliable for this, as it shows real elapsed microseconds which allows one to measure the efficiency of a script in terms of how fast it processes the script.

But imagine you do an offline bounce of a track in a DAW. If the real length of such a track is e.g. 60 seconds and you have a fast system it can be that the offline bounce only takes 5 seconds so it runs in "compressed time" through the scripts. If you display KSP_TIMER values you won't get past 5 seconds, but ENGINE_UPTIME shows you exact musical time positions spanning 60 seconds if you display the value at the beginning of the bounce and at the end (and subtract both as ENGINE_UPTIME cannot be reset like KSP_TIMER)


----------



## EvilDragon (Nov 16, 2017)

Yes, KSP_TIMER is linked to CPU clock. Not at all reliable for use with LCB.


----------



## magneto538 (Nov 16, 2017)

Perhaps I haven't explained well what I'm trying to accomplish.



andreasOL said:


> KSP_TIMER is not reliable for this, as it shows real elapsed microseconds which allows one to measure the efficiency of a script in terms of how fast it processes the script.



...which is exactly why I'm using it in the scripts above, the purpose of which is to measure how much time is necessary to perform one full loop of LCB.
Since LCB is supposed to be executed at regular time intervals, it's odd to see that this is not happening when you run it at very high resolutions (such as 1 tick/cycle). You can check this by yourself, in case you don't trust KSP_TIMER, by adding to the scripts I posted a play_note and make it play an impulse sample, then record the output. The played impulses will start to be out of the grid after a while.

Since KSP_TIMER is in microseconds, it's perfect to measure the time in ticks required to execute the last call to LCB. It's pointless to use ENGINE_UPTIME for this purpose, as it is in milliseconds, so it doesn't have nearly as close as the resolution needed: 1 tick at 120 BPM lasts only 521 microseconds, so ENGINE_UPTIME is unable to display such a value. On the other hand, ENGINE_UPTIME has to be used (and I'm using it) in my library to keep track of the time etc.

EDIT: I made a simple instrument to test this. The impulses begin to play when the transport of the DAW is started. I added a little monitor to check the effective duration of the callback and a value edit that changes the listener par (in ticks). Feel free to check it out, perhaps I'm missing something about all this. Here you go: http://www.mediafire.com/file/hunqmuodvo1hdvm/LCB_test.zip


----------



## andreasOL (Nov 16, 2017)

```
on init
    message("")
    set_listener($NI_SIGNAL_TIMER_MS, 1000)

    declare @str
    declare $n := 0
    declare $engine0
    $engine0:=$ENGINE_UPTIME
    reset_ksp_timer
end on

on listener
    inc($n)

    if ($n<=10)
        @str:[email protected]&" "&($ENGINE_UPTIME-$engine0)&"/"&$KSP_TIMER
        message(@str)
    end if
end on
```

This outputs ENGINE_UPTIME and KSP_TIMER for the first 10 executions of the LCB (both normalized to start at 0). Buffer size on my computer is set to 11ms a.t.m.

I get an output like

1/10916 2/10920 3/10921 4/10922 5/10923 6/10924 7/10925 8/10926 9/10927 10/10929

so while musical time steps from 1 to 10 ms it is all rushed during a real time interval of 13 microseconds.

If I set the LCB interval to 5ms I see 5 pairs of two executions only a few microseconds apart and then about 10000 microseconds between those groups of two (won't type the numbers here).

If I set the LCB interval to 10ms which is about my audio buffer size I see both musical time (ENGINE_UPTIME) and real time (KSP_TIMER) doing steps of 10 ms


Looks to me as if LCB calls that fall within one audio buffer size time interval are executed without any relation to real-time, i.e. as quickly as possibly so that KSP_TIMER does not show you where you are during the "inner" calls.

Perhaps this isn't related completely to your problem but IMO it's an interesting aspect of how Kontakt distributes script callbacks within the time frame determined by the audio buffer size.


----------



## magneto538 (Nov 16, 2017)

andreasOL said:


> ```
> on init
> message("")
> set_listener($NI_SIGNAL_TIMER_MS, 1000)
> ...



It's interesting indeed. Also, it's a confirmation that there is an actual difference in the duration of each call to LCB.


----------



## olmerk (Feb 3, 2021)

So which method for the present is the most most reliable to build an arpeggiator? Still blip notes?


----------



## EvilDragon (Feb 3, 2021)

For me it's beat listener at maximum resolution.


----------

