How to make values in your tool update when you change them in Renoise

Hi, i made my 1st simple tool with the help of Starter Pack and the “Create new tool” tool.

-- Placeholder for the dialog  
local dialog = nil  
  
-- Placeholder to expose the ViewBuilder outside the show_dialog() function  
local vb = nil  
  
-- Reload the script whenever this file is saved.  
-- Additionally, execute the attached function.  
_AUTO_RELOAD_DEBUG = function()  
  
end  
  
-- Read from the manifest.xml file.  
class "RenoiseScriptingTool" (renoise.Document.DocumentNode)  
function RenoiseScriptingTool:__init()  
 renoise.Document.DocumentNode.__init(self)  
 self:add_property("Name", "Untitled Tool")  
 self:add_property("Id", "Unknown Id")  
end  
  
local manifest = RenoiseScriptingTool()  
local ok,err = manifest:load_from("manifest.xml")  
local tool_name = manifest:property("Name").value  
local tool_id = manifest:property("Id").value  
  
local function show_dialog()  
  
 -- This block makes sure a non-modal dialog is shown once.  
 -- If the dialog is already opened, it will be focused.  
 if dialog and dialog.visible then  
 dialog:show()  
 return  
 end  
  
 -- The ViewBuilder is the basis  
 local vb = renoise.ViewBuilder()  
 local song = renoise.song()  
  
 local content = vb:column {  
  
 vb:row {  
  
 vb:text {  
 width = 20,  
 text = "BPM"  
 },  
  
 vb:valuebox {  
 min = 32,  
 max = 255,  
 value = song.transport.bpm,  
 notifier = function(value)  
 song.transport.bpm = value  
 end  
 }  
 },  
  
 vb:button {  
 text = "BPM command",  
 tooltip = "Place BPM command at pattern's 1st line",  
 notifier = function()  
 if song.transport.bpm > 255 then song.transport.bpm = 255 end  
 song.selected_pattern.tracks[1].lines[1].effect_columns[1].number_string = 'ZT'  
 song.selected_pattern.tracks[1].lines[1].effect_columns[1].amount_string = ("%X"):format(song.transport.bpm)  
 end  
 },  
 }  
  
 dialog = renoise.app():show_custom_dialog(tool_name, content)  
  
end  
  
renoise.tool():add_menu_entry {  
 name = "Pattern Editor:"..tool_name.."...",  
 invoke = show_dialog  
}  

It can change bpm and place bpm command at the beginnig of the pattern when you press button.
So how to make bpm value update in my tool’s dialog window when I change it in Renoise?

This is a so called “observable”. You can add code to your program whenever the bpm is changed in Renoise.
By chance this is exactly described in the documentation:

Thanks, I already figured out that I need this “observable” and “add_notifier” things. But can`t figure out yet how to implement them in my code

You can also use the ‘bind’ option for viewbuilder elements, from the API docs:

  
-- Valid in the construction table only: Bind the views value to a  
-- renoise.Document.ObservableNumber object. Will change the Observable  
-- value as soon as the views value changes, and change the view's value as  
-- soon as the Observable's value changes - automatically keeps both values  
-- in sync.  
-- Notifiers can be added to either the view or the Observable object.  
value.bind  
 ->[ObservableNumber Object]  
  

If you go with explicitly using notifiers then you may find the following functions useful:

  
-- Notifier handler functions  
local notifier = {}  
 function notifier.add(observable, n_function)  
 if not observable:has_notifier(n_function) then  
 observable:add_notifier(n_function)  
 end  
 end  
  
 function notifier.remove(observable, n_function)  
 if observable:has_notifier(n_function) then  
 observable:remove_notifier(n_function)  
 end  
 end  
  

I use this everywhere I need notifiers, it just makes it easier. The ‘observable’ parameter is the the control you want to attach a notifier to, so for bpm this would be

  
renoise.song().transport.bpm_observable  
  

‘n_function’ would be the function you want triggered every time a notifier is called

Thanks, I already figured out that I need these “observable” and “add_notifier” things. But can`t figure out yet how to implement them in my code.

If you could help me with the few lines of code it would be much appreciated. I never used Lua before, and english is not my native language. Can you show me what exactly I must add to my code?

Sure, here is your code with additions that will do what you want

  
  
-- Placeholder for the dialog  
local dialog = nil  
  
-- Placeholder to expose the ViewBuilder outside the show_dialog() function  
local vb = nil  
  
-- Reload the script whenever this file is saved.  
-- Additionally, execute the attached function.  
_AUTO_RELOAD_DEBUG = function()  
  
end  
  
-- Read from the manifest.xml file.  
class "RenoiseScriptingTool" (renoise.Document.DocumentNode)  
function RenoiseScriptingTool:__init()  
 renoise.Document.DocumentNode.__init(self)  
 self:add_property("Name", "Untitled Tool")  
 self:add_property("Id", "Unknown Id")  
end  
  
local manifest = RenoiseScriptingTool()  
local ok,err = manifest:load_from("manifest.xml")  
local tool_name = manifest:property("Name").value  
local tool_id = manifest:property("Id").value  
  
  
-- Notifier handler functions  
local notifier = {}  
 function notifier.add(observable, n_function)  
 if not observable:has_notifier(n_function) then  
 observable:add_notifier(n_function)  
 end  
 end  
  
 function notifier.remove(observable, n_function)  
 if observable:has_notifier(n_function) then  
 observable:remove_notifier(n_function)  
 end  
 end  
  
local function show_dialog()  
  
 -- This block makes sure a non-modal dialog is shown once.  
 -- If the dialog is already opened, it will be focused.  
 if dialog and dialog.visible then  
 dialog:show()  
 return  
 end  
  
 -- The ViewBuilder is the basis  
 local vb = renoise.ViewBuilder()  
 local song = renoise.song()  
  
 -- Setup BPM notifier  
 local function update_bpm()  
 vb.views["bpm"].value = renoise.song().transport.bpm  
 end  
 notifier.add(renoise.song().transport.bpm_observable, update_bpm)  
  
  
 local content = vb:column {  
  
 vb:row {  
  
 vb:text {  
 width = 20,  
 text = "BPM"  
 },  
  
 vb:valuebox {  
 id = "bpm", -- Added so that you can identify which GUI control to update  
 min = 32,  
 max = 255,  
 value = song.transport.bpm,  
 notifier = function(value)  
 song.transport.bpm = value  
 end  
 }  
 },  
  
 vb:button {  
 text = "BPM command",  
 tooltip = "Place BPM command at pattern's 1st line",  
 notifier = function()  
 if song.transport.bpm > 255 then song.transport.bpm = 255 end  
 song.selected_pattern.tracks[1].lines[1].effect_columns[1].number_string = 'ZT'  
 song.selected_pattern.tracks[1].lines[1].effect_columns[1].amount_string = ("%X"):format(song.transport.bpm)  
 end  
 },  
 }  
  
 dialog = renoise.app():show_custom_dialog(tool_name, content)  
  
end  
  
renoise.tool():add_menu_entry {  
 name = "Pattern Editor:"..tool_name.."...",  
 invoke = show_dialog  
}  
  

Thank you very much afta8. I’ll try this tomorrow, cause it’s late night here. I guess this example should be enough for me to start making more complicated tools. I`m working on tool that fills selected area with any number of repetitions of the note at start, linear, exponential, and with optional randomness.

I doubt that will work either…
Forgetting to add the n_function and declaring vb.views local on two spots where on one of both, the vb would not need to be declared local.

A few adjustments (4 lines of code involved in total:3 added, 1 changed)

  
  
-- Placeholder for the dialog  
local dialog = nil  
  
-- Placeholder to expose the ViewBuilder outside the show_dialog() function  
local vb = nil  
  
-- Reload the script whenever this file is saved.  
-- Additionally, execute the attached function.  
_AUTO_RELOAD_DEBUG = function()  
  
end  
  
-- Read from the manifest.xml file.  
class "RenoiseScriptingTool" (renoise.Document.DocumentNode)  
function RenoiseScriptingTool:__init()  
 renoise.Document.DocumentNode.__init(self)  
 self:add_property("Name", "Untitled Tool")  
 self:add_property("Id", "Unknown Id")  
end  
  
local manifest = RenoiseScriptingTool()  
local ok,err = manifest:load_from("manifest.xml")  
local tool_name = manifest:property("Name").value  
local tool_id = manifest:property("Id").value  
  
function n_function()  
  
 if vb ~= nil then  
 vb.views["bpm"].value = renoise.song().transport.bpm  
 end  
  
end  
  
-- Notifier handler functions  
local notifier = {}  
 function notifier.add(observable, n_function)  
 if not observable:has_notifier(n_function) then  
 observable:add_notifier(n_function)  
 end  
 end  
  
 function notifier.remove(observable, n_function)  
 if observable:has_notifier(n_function) then  
 observable:remove_notifier(n_function)  
 end  
 end  
  
local function show_dialog()  
  
 -- This block makes sure a non-modal dialog is shown once.  
 -- If the dialog is already opened, it will be focused.  
 if dialog and dialog.visible then  
 dialog:show()  
 return  
 end  
  
 -- The ViewBuilder is the basis  
 vb = renoise.ViewBuilder()  
 local song = renoise.song()  
  
 -- Setup BPM notifier  
 local function update_bpm()  
 vb.views["bpm"].value = renoise.song().transport.bpm  
 end  
 notifier.add(renoise.song().transport.bpm_observable, update_bpm)  
  
  
 local content = vb:column {  
  
 vb:row {  
  
 vb:text {  
 width = 20,  
 text = "BPM"  
 },  
  
 vb:valuebox {  
 id = "bpm", -- Added so that you can identify which GUI control to update  
 min = 32,  
 max = 255,  
 value = song.transport.bpm,  
 notifier = function(value)  
 song.transport.bpm = value  
 end  
 }  
 },  
  
 vb:button {  
 text = "BPM command",  
 tooltip = "Place BPM command at pattern's 1st line",  
 notifier = function()  
 if song.transport.bpm > 255 then song.transport.bpm = 255 end  
 song.selected_pattern.tracks[1].lines[1].effect_columns[1].number_string = 'ZT'  
 song.selected_pattern.tracks[1].lines[1].effect_columns[1].amount_string = ("%X"):format(song.transport.bpm)  
 end  
 },  
 }  
  
 dialog = renoise.app():show_custom_dialog(tool_name, content)  
  
end  
  
renoise.tool():add_menu_entry {  
 name = "Pattern Editor:"..tool_name.."...",  
 invoke = show_dialog  
}  
  

I tested before posting… it worked

n_function is just the parameter passed to the function so it doesn’t need to be declared first. Yes vb doesn’t need to be local, but doesn’t seem to make a difference if it is, as long as the notifier function is created in the same context it should be ok right?

My previous replay was a late night reply, as usual i’m more with my head in dreamland than in the real world, i didn’t noticed this line:
notifier.add(renoise.song().transport.bpm_observable, update_bpm)

So i frankly was wondering what you were heading to with that notifier array.
I used an array construction with functions and notifiers once (in the pitch automator) but using a different declaration method.

No worries, it happens to the best of us… :)
Using the array for the notifier functions was just something I picked up from another thread here. I think its how an object based approach would work but I’m not too hot on that way of doing things. Thats the thing about lua it lets you do the same thing in many different ways!

That worked with one value, but didn`t work when I tried something like this:

-- Notifier handler functions  
local notifier = {}  
 function notifier.add(observable, n_function)  
 if not observable:has_notifier(n_function) then  
 observable:add_notifier(n_function)  
 end  
 end  
  
 function notifier.remove(observable, n_function)  
 if observable:has_notifier(n_function) then  
 observable:remove_notifier(n_function)  
 end  
 end  
  
 -- Setup selection notifier  
 local function update_selection()  
 vb.views["lines"].value = renoise.song().selection_in_pattern.end_line - renoise.song().selection_in_pattern.start_line + 1  
 end  
  
 notifier.add(renoise.song().selection_in_pattern.start_line, update_selection)  
 notifier.add(renoise.song().selection_in_pattern.end_line, update_selection)  
``` and then  
  

vb:row {

vb:text {text = “lines selected”},

vb:valuebox {
id = “lines”,
min = 0,
max = 200,
value = renoise.song().selection_in_pattern.end_line - renoise.song().selection_in_pattern.start_line + 1,
notifier = function(value)
renoise.song().selection_in_pattern.end_line = renoise.song().selection_in_pattern.start_line - 1 + value
end
}

Neither start_line or end_line is an observable. Observables always end with “_observable”, that is how you tell them apart from other variables.

Generally speaking, there are observables available for most, but not every value. But it’s still possible to check for changes manually by using the idle loop.

Add something like this to your script, and it will be executed ~ 10 times per second (API scripts run in the UI thread, so the exact value will depend on the actual song/CPU usage):

  
local cached_start_line = nil  
local cached_end_line = nil  
  
renoise.tool().app_idle_observable:add_notifier(function()  
  
 local rns = renoise.song()  
 if (rns.selection_in_pattern.start_line ~= cached_start_line) or  
 (rns.selection_in_pattern.end_line ~= cached_end_line)  
 then  
  
 -- respond when selection has changed  
 cached_start_line = rns.selection_in_pattern.start_line  
 cached_end_line = rns.selection_in_pattern.end_line  
  
 print("Pattern selection now goes from",cached_start_line,"to,"cached_end_line)  
  
 end  
  
end)  
  
  

Thanks, I knew that but didn’t pay attention to these particular variables.

Your code is shorter and easier to understand) So I can` see why use the afta8 approach.

But your code returns error when there is no selection.

Oh… but it is for print in terminal only? It seems that it can’t be used to cahnge something in tool`s dialog

It was only meant as a small example.
Generally speaking, you can reference any part of your dialog from anywhere in the script.

If you want access to the ‘lines’ valuebox, just use the same the approach as afta8 pointed out,
using the ‘vb’ (reference to viewbuilder) to change the ‘lines’ controls properties:

vb.views["lines"].value = cached_end_line - cached_start_line + 1  

You will of course need a valid selection to work on in the first place. Figuring out how to check if such a thing exist could be an interesting little coding exercise in itself?

Thanks again, now the code doesn’t look scary.

Ok, i can deal with it by adding one more “if”. I just didn`t realize at first that “0” and “nil” are different things.

But this part of code still doesn`t work

 notifier = function(value)  
 renoise.song().selection_in_pattern.end_line = renoise.song().selection_in_pattern.start_line - 1 + value  
 end  
  
``` Is "selection_in_pattern.end_line" read only?

selection_in or is_selected etc are the marker positions themselves and do not provide direct access to the cell contents themselves.

You would need something in this kind of fashion:

  
if not renoise.song().selection_in_pattern.start_track == nil then  
 renoise.song().patterns[renoise.song().selected_pattern_index].tracks[renoise.song().selection_in_pattern.start_track].lines[renoise.song().selection_in_pattern.end_line]:copy_from(renoise.song().patterns[renoise.song().selected_pattern_index].tracks[renoise.song().selection_in_pattern.start_track].lines[renoise.song().selection_in_pattern.start_line])  
end  
  

If you want to access specific note columns, you even have to go one step further. the above code copies the “whole” line in that specific track.

Yes, actually I already implemented basic features of my tool i.e. copy note in various ways. Now I just wanted a control to adjust selection, expand or shrink. It`s not necessary, but I thought it would be useful.

That is possible, but you should stay within the perimeters of what start_line-1+value is. and it would probably be more effective to use either this:
renoise.song().selection_in_pattern = { end_line = renoise.song().selection_in_pattern.start_line-1 + value }

or this:
local selection = renoise.song().selection_in_pattern
selection.end_line = renoise.song().selection_in_pattern.start_line-1 + value
renoise.song().selection_in_pattern = selection

When I tried this, selection expanded from 1st to last track, and selection start jumped to 1st line.

This worked fine. So, it`s just local variable that did the trick… Thanks.