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:
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
}
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)
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:
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?
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