Add_timer() remove_timer(). How to pass a variable through the timer?

I have a long-standing recurring problem when programming with add_timer() / remove_timer() using these API functions.
The problem is that I can’t find a way to pass a variable through the timer add_timer().

For example, if I want to execute a function without delay with a variable in the function I can do this:

--the function 1 without timer
local function my_func(variable)
    print(variable)
    if  renoise.tool():has_timer(my_func) then
         renoise.tool():remove_timer(my_func)
    end
end
--bang!
my_func(20)
--the result is that it will print 20 immediately.

On the other hand, if I use add:timer(), how do I pass said variable “20”?

--the function 2 with timer
local function my_timer(variable)
    if not renoise.tool():has_timer(my_func) then
         renoise.tool():add_timer(my_func,1000)  -- variable???
    end
end
--bang!
my_timer(20)

--The result will not print the variable "20" after 1 second. It will probably print "nil".

Does anyone know how to pass this variable?
It is no use defining a local outside the function, because that local could change. During that second it should not be possible for the variable to change in any way!

The related documentation from API (Renoise.ScriptingTool.API.lua):

--[[

Register a timer function or table with a function and context (a method)
that periodically gets called by the app_idle_observable for your tool.

Modal dialogs will avoid that timers are called. To create a one-shot timer,
simply call remove_timer at the end of your timer function. Timer_interval_in_ms
must be > 0. The exact interval your function is called will vary
a bit, depending on workload; e.g. when enough CPU time is available the
rounding error will be around +/- 5 ms.

]]

-- Returns true when the given function or method was registered as a timer.
renoise.tool():has_timer(function or {object, function} or {function, object})
  -> [boolean]

-- Add a new timer as described above.
renoise.tool():add_timer(function or {object, function} or {function, object},
  timer_interval_in_ms)

-- Remove a previously registered timer.
renoise.tool():remove_timer(timer_func)

The objective is to be able to call the same function my_func(variable) several times in quick succession but only the last call is executed.

For example, if I call the function my_func(variable) 10 times in a row, through the other function with a timer, the timer should avoid executing the first 9 calls, executing the 10th call, obviously passing the variable through the timer.

You can solve it by using a local intermediate function. Here some pseudo code to fix it:

local yourVariable = something
local callback = func
  my_func(yourVariable)
end

renoise.tool():add_timer(callback,1000)

In Javascript, you would simply put in a lamda function, which is the same. Maybe this shorthand is possible in LUA, too? Similar like:

renoise.tool():add_timer(() => my_func(variable),1000)

So then your your removal and checking for timer would be more difficult, since now the intermediate function is the exact pointer…

Yes, I already tried it with an intermediate local function. But the problem is that I couldn’t stop the timer afterwards.

The goal is to be able to call the timer 10 times but for the timer to avoid executing the first 9 times. It will only execute the last time, passing in a variable (or set of variables) and stopping whatever timer is currently running.

I can already do all that, but I can’t send a variable through the timer. I was looking for something like this:

add_timer(func, time, "variables")

But the third argument cannot exist!

I think it’s all possible, but LUA syntax is so limited that there might be only “the one way”. Again some pseudocode for ideas:

local yourVariable = something
local numCalls = 0
local callback = func
  numCalls = numCalls + 1
  if (numCalls > 9) then
    my_func(yourVariable)
    renoise.tool():remove_timer(callback)
  end
end

renoise.tool():add_timer(callback,1000)

Also why do you need so often attach and detach timers, looks to me like a design flaw…

I have a function “A” that can be called from multiple functions “B1”, “B2”, “B3”… These last functions can be called through notifiers (with a slight delay) or directly (without delay). What I want is that, if function “A” is called several times in a row, it should only execute the last time. It is a way to optimize to avoid CPU overload and avoid unnecessary waits in the GUI.

This is a quick way to avoid overloading when calling a function A multiple times, since the first few times are useless.

I’m trying to find a general method to approach it. The 10 executions was an example. The solution should cover any case.

From my point of view add_timer(func, time) needs a location where you can put variables. These variables cannot be modified while the timer lasts. The problem is that if you use a local variable outside or inside the function, this variable can be modified by calling the function from a different function while the timer lasts, and that prevents using add_timer for other “more advanced” purposes.

In short, as the API currently stands, with add_timer, remove_timer it is possible to “brake” the execution of the first calls. The problem is that I can’t find a way to pass a variable through the timer. That is, you cannot write:

local function bang_func(variable)
   ...add_timer(my_func(variable),2000)  --my_func(variable) not work!!!
end

Only accept

local function bang_func(variable)
   ...add_timer(my_func,2000)  --and the "variable"???
end

So “variable” cannot be passed through add_timer to work inside my_func() when executed after 2 seconds.

The problem is this part of the API, which seems to not take into account that add_timer() does not allow passing variables.

Also, what does {object, function} mean in the documentation in add_timer()? As far as I know, you can only put the name of the function (without the parentheses) → function name() end … add_timer(name,time)

Actually, I have been able to pass a variable using an intermediate function. But then I can’t stop the timer! :sweat_smile:

In fact, Renoise has this problem in multiple parts, where the same function is called many times when that should not happen and will be optimized for it… How do you avoid executing the same function called from multiple parts of your code, which allows passing non-modifiable variables for the duration of the timer which prevents unnecessary calls and also allows stopping the timer?

A summary of the parts:

  1. One function A, multiple calls from others functions…
  2. prevent all calls except the last one with add_timer.
  3. **send one or more variables from add_timer that serve after the time has passed.
  4. Finally stop the timer from the function A.

Avoiding multiple calls is easy, you simply check if the timer that calls that function is latent or not with has_timer(). The has_timer() is precisely used for this. If the timer is latent, you simply destroy it with remove_timer() and call it again with add_timer(). By destroying it, avoid the first calls.

The difficulty is passing variables through the timer…

Speaking like walking at a fair: The last call has a prize!

I can still explain this further.

The my_func(variable) function takes 20 ms to execute.

For whatever reason, a tool may call mu_func(variable_1) several times in a row. Imagine pressing several buttons that need to execute my_func.

If you press these buttons at the same time, a queue of 10 executions is generated. These are the 10 calls:

  1. 20ms
  2. 20ms
  3. 20ms
  4. 20ms
  5. 20ms
  6. 20ms
  7. 20ms
  8. 20ms
  9. 20ms
  10. 20ms
    → total 200ms!

A small 20ms problem turns into a noticeable 200ms (a 10-call queue).

In this scenario, it is really only necessary to execute the last call. Therefore, it is possible to use has_timer, add_timer, remove_timer to only execute the last call contained in another function my_func_timer(variable_1). However, all 10 calls to my_func_timer(variable_1) should be able to change variable_1 of the my_func(variable_1) function, but it is not possible because from the my_func_timer(variable_1) function it is not possible to pass variable_1 through the timer add_timer(), because it does not allow a third argument let it be the variable_1 itself.

Here’s some code I’ve written so I can experiment…

-- Returns true when the given function or method was registered as a timer.
-- renoise.tool():has_timer(function or {object, function} or {function, object})
-- -> [boolean]

-- Add a new timer as described above.
-- renoise.tool():add_timer(function or {object, function} or {function, object},
-- timer_interval_in_ms)

-- Remove a previously registered timer.
-- renoise.tool():remove_timer(timer_func)


--defina a table with functions
local tbl_func={}

--create first func main
tbl_func.main=function(var_1)
  print(var_1,os.clock(),"___A")
  --remove the timer passed at the end
  if renoise.tool():has_timer(tbl_func.main) then
    renoise.tool():remove_timer(tbl_func.main)
  end
end

--bang the firt function with variable var_1=true
  --tbl_func.main(true)  --> the print return: true (and the time of os:clock() )



--use a function with timer to delay the first function 1.5 seconds
tbl_func.tmr=function(var_1)
  print(os.clock(),"___B")
  --destroy the previous timer
  if renoise.tool():has_timer(tbl_func.main) then
    renoise.tool():remove_timer(tbl_func.main)
  end
  --rebuild the last timer
  if not renoise.tool():has_timer(tbl_func.main) then
    renoise.tool():add_timer(tbl_func.main,1500)  --and the var_1 ???
  end
end


--multiple bang the timer function
tbl_func.tmr(true)
tbl_func.tmr(false)
tbl_func.tmr(true)
tbl_func.tmr(false)
tbl_func.tmr("hello!")

As it is now, the variable var_1 will not be passed, because it does not exist. Then tbl_func.main will print “nil” and the time. The goal is to be able to pass var_1, and end up printing only “Hello!”

You probably want to use app_idle_observable instead, so you can check the timing there and other things.

All notifiers and callbacks either accept a raw function or a function with a context (-> a class method), so you can pass the context as self like this:

function my_idle_function(context)
  rprint(context)
  context.counter = context.counter + 1
end
local idle_context = { counter = 1 }
renoise.tool().app_idle_observable:add_notifier({my_idle_function, idle_context})
1 Like

Here’s the same solution, but using a class instead:

class "MyIdleHandler"

function MyIdleHandler:__init()
  self.counter = 0
end

function MyIdleHandler:start()
  rprint("Start")
  renoise.tool().app_idle_observable:add_notifier({MyIdleHandler.on_idle, self})
end

function MyIdleHandler:stop()
  rprint("Stop")
  renoise.tool().app_idle_observable:remove_notifier(self)
end

function MyIdleHandler:on_idle()
  self.counter = self.counter + 1
  rprint("Idle: "..self.counter)
  if self.counter == 100 then
    self:stop()
  end
end

local my_idle_handler = MyIdleHandler()
my_idle_handler:start()
3 Likes

Thank you for this! I’ll try to use it to see if I can adjust it to my code. I think I’ll be able to use it.

However, if you ever want to update the API, consider being able to pass variables through add_timer(). This way we can have control of time and the passage of variables, both at the same time.

1 Like