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