Example: Slicing Up A Processing Function With Coroutines

Heres an updated version, example on how to use Lua coroutines to slice up a processing function.
How to create something like a background worker thread in Lua, using coroutines and Renoises idle notifier.

process_slicer.lua
[luabox]
–[[============================================================================
process_slicer.lua
============================================================================]]–

–[[

ProcessSlicer allows you to slice up a function which takes a lot of
processing time into multiple calls via Lua coroutines.

  • Example usage:

local slicer = ProcessSlicer(my_process_func, argument1, argumentX)

– This starts calling ‘my_process_func’ in idle, passing all arguments
– you’ve specified in the ProcessSlicer constructor.
slicer:start()

– To abort a running sliced process, you can call “stop” at any time
– from within your processing function of outside of it in the main thread.
– As soon as your process function returns, the slicer is automatically
– stopped.
slicer:stop()

– To give processing time back to Renoise, call ‘coroutine.yield()’
– anywhere in your process function to temporarily yield back to Renoise:
function my_process_func()
for j=1,100 do
– do something that needs a lot of time, and periodically call
– “coroutine.yield()” to give processing time back to Renoise. Renoise
– will switch back to this point of the function as soon as has done
– “its” job:
coroutine.yield()
end
end

  • Drawbacks:

By slicing your processing function, you will also slice any changes that are
done to the Renoise song into multiple undo actions (one action per slice/yield).

Modal dialogs will block the slicer, cause on_idle notifications are not fired then.
It will even block your own process GUI when trying to show it modal.

]]

class “ProcessSlicer”

function ProcessSlicer:__init(process_func, …)
assert(type(process_func) == “function”,
“expected a function as first argument”)

self.__process_func = process_func
self.__process_func_args = arg
self.__process_thread = nil
end


– returns true when the current process currently is running

function ProcessSlicer:running()
return (self.__process_thread ~= nil)
end


– start a process

function ProcessSlicer:start()
assert(not self:running(), “process already running”)

self.__process_thread = coroutine.create(self.__process_func)

renoise.tool().app_idle_observable:add_notifier(
ProcessSlicer.__on_idle, self)
end


– stop a running process

function ProcessSlicer:stop()
assert(self:running(), “process not running”)

renoise.tool().app_idle_observable:remove_notifier(
ProcessSlicer.__on_idle, self)

self.__process_thread = nil
end


– function that gets called from Renoise to do idle stuff. switches back
– into the processing function or detaches the thread

function ProcessSlicer:__on_idle()
assert(self.__process_thread ~= nil, "ProcessSlicer internal error: "…
“expected no idle call with no thread running”)

– continue or start the process while its still active
if (coroutine.status(self.__process_thread) == ‘suspended’) then
local succeeded, error_message = coroutine.resume(
self.__process_thread, unpack(self.__process_func_args))

if (not succeeded) then
– stop the process on errors
self:stop()
– and forward the error to the main thread
error(error_message)
end

– stop when the process function completed
elseif (coroutine.status(self.__process_thread) == ‘dead’) then
self:stop()
end
end
[/luabox]

And an example tool which creates a small GUI around this, allowing to start/stop a process and shows the current progress:

main.lua
[luabox]
–[[============================================================================
main.lua
============================================================================]]–

–[[

This tool shows how to slice up a function which takes a lot of processing
time into multiple calls via Lua coroutines.

This mainly is useful to:

  • show the current progress of your processing function
  • allow users to abort the progress at any time
  • avoids that Renoise asks to kill your process function cause it assumes
    its frozen when taking more than 10 seconds

Please have a look at “process_slicer.lua” for more info. This file basically
just shows how to create a GUI for the ProcessSlicer class.

]]

require “process_slicer”


– main: dummy example for sliced working function which needs a lot of time

function main(update_progress_func)
local i = 0

local num_iterations = 1000
for j=1,num_iterations do
i = i + 1

– waste time (your tool would do something useful here)
for k=1,1000000 do
local a = 12/12*3434
end

– show the progress in the GUI
update_progress_func(i / num_iterations)

– and periodically give time back to renoise
coroutine.yield()
end
end


– creates a dialog which starts stops and visualizes a sliced progress

local function create_gui()

local dialog, process
local vb = renoise.ViewBuilder()

– Note: we allow multiple dialogs and processes in this example. If you
– only want one dialog to be shown and only one process running, make
– ‘dialog’ and ‘process’ global, and check if if the dialog is visible
– here. If your dialog and viewbuilder is global, you also don’t have to
– pass an “update_progress_func” to the processing function, but can call
– it directly.

----- process GUI functions (callbacks):

local function update_progress(progress)
if (not dialog or not dialog.visible) then
– abort processing when the dialog was closed
process:stop()
return
end

– else update the progress text
if (progress == 1.0) then
vb.views.start_button.text = “Start”
vb.views.progress_text.text = “Done!”
else
vb.views.progress_text.text = string.format(
“Working hard (%d%% done)…”, progress * 100)
end
end

local function start_stop_process()
if (not process or not process:running()) then
– start running
vb.views.start_button.text = “Stop”
process = ProcessSlicer(main, update_progress)
process:start()

elseif (process and process:running()) then
– stop running
vb.views.start_button.text = “Start”
vb.views.progress_text.text = “Process Aborted!”
process:stop()
end
end

---- process GUI

local DEFAULT_DIALOG_MARGIN =
renoise.ViewBuilder.DEFAULT_DIALOG_MARGIN

local DEFAULT_CONTROL_SPACING =
renoise.ViewBuilder.DEFAULT_CONTROL_SPACING

local DEFAULT_DIALOG_BUTTON_HEIGHT =
renoise.ViewBuilder.DEFAULT_DIALOG_BUTTON_HEIGHT

local dialog_content = vb:column {
uniform = false,
margin = DEFAULT_DIALOG_MARGIN,
spacing = DEFAULT_CONTROL_SPACING,

– (add some content here)

vb:text {
id = “progress_text”,
text = “Hit ‘Start’ to begin a sliced process:”
},

vb:button {
id = “start_button”,
text = “Start”,
height = DEFAULT_DIALOG_BUTTON_HEIGHT,
width = 80,
notifier = start_stop_process
}
}

dialog = renoise.app():show_custom_dialog(
“Sliced Process Example”, dialog_content)
end


renoise.tool():add_menu_entry{
name = “Main Menu:Tools:Example Tool Sliced Process…”,
invoke = function()
create_gui()
end
}
[/luabox]

Complete tool is attached and can also be found in the Renoise Tools SVN reps

Interesting, but the tool doesn’t show up in menu (dlded v5 api from link)

Seems to work okay here.

Yes, thank you!