Here’s a junk drawer of a “tool”, mentioned here, that’s grown as I’ve been learning Lua. I’m sure you’ve all had a similar catch-all. Probably still do
It includes:
- Pattern Editor commands for automation envelope editing
- Pattern Matrix rectangular selection keybindings
- Copy/Paste whole device automation (pattern and song-wide)
- Random other stuff
I think there’s some potentially interesting stuff here.
Particularly the automation editing. Since it’s all done from the Pattern Editor, you’ve got the full range of navigation commands to move through the selected envelope (plus new “Jump To Prev/Next Automation Point” commands). So you rarely have to leave the Pattern Editor or fuss with the mouse when editing automation.
I plan to implement actual Tension Curve and Hold segments as an abstraction layer over the sub-line grid. I’m pretty sure it’s not only possible, but potentially awesome.
There’s a dialog in the Tools menu (and a keybinding for it in Global:View) for setting three custom automation point offsets (small (0.001), medium (0.01), and large (0.1) by default), for quickly honing in by ear, from course adjustments to fine, on the exact value you want.
There are keybindings and menu entries for copying/pasting a whole device’s automation, pattern and song-wide.
And keybindings for rectangular Pattern Matrix selections, delimited by the current position and a roving boundary corner.
I’ll eventually split it all up into separate tools and submit them. But not until I actually know what I’m doing, hah.
The usual disclaimer: This is brand new code from a new Lua coder and API user. It’s undoubtedly full of bugs, untested edge cases, and assorted stupidities.
Comments and style/idiomaticity pointers welcome. And of course bug reports.
Here’s the code as it exists right now, if you just want to look at it:
--------------------------------------------------------------------------------
-- com.thunk.Misc.xrnx
-- main.lua
--------------------------------------------------------------------------------
_AUTO_RELOAD_DEBUG = true
local s = renoise.song()
local t = renoise.tool()
local a = renoise.app()
local tr = s.transport
local doc = renoise.Document
--------------------------------------------------------------------------------
-- Utils
--------------------------------------------------------------------------------
local function confine (val, lo, hi)
return math.min(hi, math.max(lo, val))
end
local function rescale (val, old_min, old_max, new_min, new_max)
if old_min == old_max then
error("Can't rescale in zero-length range")
else
return (((val - old_min) / (old_max - old_min)) * (new_max - new_min)) + new_min
end
end
local function debug_msg(status, ...)
local msg = ""
for i, thing in ipairs(arg) do
msg = msg .. tostring(thing) .. " "
end
if status then
a:show_status(msg)
else
a:show_message(msg)
end
end
--------------------------------------------------------------------------------
-- Pattern Matrix Selection
--------------------------------------------------------------------------------
-------- Local Vars
local y_origin = false
local x_origin = false
local y_bound = false
local x_bound = false
-------- Local Functions
local function matrix_op(sel, x1, x2, y1, y2)
for i = math.min(x1, x2), math.max(x1, x2), 1 do
for j = math.min(y1, y2), math.max(y1, y2), 1 do
s.sequencer:set_track_sequence_slot_is_selected(i, j, sel)
end
end
end
local function clear_matrix_selection()
matrix_op(false, 1, #s.tracks, 1, tr.song_length.sequence)
end
local function set_matrix_selection(x_origin, y_origin, x_bound, y_bound)
matrix_op(true, x_origin, x_bound, y_origin, y_bound)
end
-------- Bindable Functions
function matrix_select(dir)
local sti = s.selected_track_index
local sqi = s.selected_sequence_index
if sqi ~= y_origin or sti ~= x_origin then
y_origin = sqi
x_origin = sti
y_bound = sqi
x_bound = sti
end
if dir == "left" and x_bound > 1 then
x_bound = x_bound - 1
elseif dir == "right" and x_bound < #s.tracks then
x_bound = x_bound + 1
elseif dir == "up" and y_bound > 1 then
y_bound = y_bound - 1
elseif dir == "down" and y_bound < tr.song_length.sequence then
y_bound = y_bound + 1
end
clear_matrix_selection()
set_matrix_selection(x_origin, y_origin, x_bound, y_bound)
end
-------- Key Bindings
t:add_keybinding{
name = "Pattern Matrix:Selection:Move Boundary Column Right",
invoke = function() matrix_select("right") end
}
t:add_keybinding{
name = "Pattern Matrix:Selection:Move Boundary Column Left",
invoke = function() matrix_select("left") end
}
t:add_keybinding{
name = "Pattern Matrix:Selection:Move Boundary Row Down",
invoke = function() matrix_select("down") end
}
t:add_keybinding{
name = "Pattern Matrix:Selection:Move Boundary Row Up",
invoke = function() matrix_select("up") end
}
--------------------------------------------------------------------------------
-- Pattern Editor Automation Editing
--------------------------------------------------------------------------------
-------- Prefs
prefs = doc.create("pattern_editor_automation_editing_preferences")
{
automation_shift_0 = 0.001,
automation_shift_1 = 0.01,
automation_shift_2 = 0.1
}
t.preferences = prefs
-------- Local Functions
local function get_auto(param, pattern_track)
if not param then
param = s.selected_parameter
end
if not pattern_track then
pattern_track = s.selected_pattern_track
end
if param and param.is_automatable then
return pattern_track:find_automation(param), param, pattern_track
elseif not param then
a:show_status("No parameter selected")
else
a:show_status("Parameter is not automatable")
end
return nil, nil, pattern_track
end
local function defaults_for_nil_time_auto(time, auto)
if not time then
time = s.selected_line_index
end
if not auto then
auto = get_auto()
end
return time, auto
end
local function interpolate_auto(time, auto)
time, auto = defaults_for_nil_time_auto(time, auto)
local prev, next
for i, point in ipairs(auto.points) do
if point.time < time then
if not prev or point.time > prev.time then
prev = point
end
elseif point.time > time then
if not next or point.time < next.time then
next = point
end
else
return point.value
end
end
if prev and next then
return rescale(time, prev.time, next.time, prev.value, next.value)
elseif prev then
return prev.value
elseif next then
return next.value
else
return false
end
end
local function add_interpolated_point(time, auto)
time, auto = defaults_for_nil_time_auto(time, auto)
if not auto then
return false
end
local value = interpolate_auto(time, auto)
if value then
auto:add_point_at(time, value)
return true
else
return false
end
end
local function delete_point(time, auto)
time, auto = defaults_for_nil_time_auto(time, auto)
if not auto then
return false
elseif auto:has_point_at(time) then
auto:remove_point_at(time)
return true
else
return false
end
end
local function delete_points_in_range(auto, start_line, end_line)
for i, point in ipairs(auto.points) do
if point.time >= start_line and point.time <= end_line then
auto:remove_point_at(point.time)
end
end
end
local function get_automation_point(line, auto)
for i, point in ipairs(auto.points) do
if point.time == line then
return point
end
end
return false
end
local function jump_to_automation_point(comparison_fn)
local line = s.selected_line_index
local auto = get_auto()
local next = nil
if auto then
for i, point in ipairs(auto.points) do
if comparison_fn(point, line, next) then
next = point.time
end
end
if next then
local next_pos = tr.edit_pos
next_pos.line = next
tr.edit_pos = next_pos
end
end
end
-------- Bindable Functions
function jump_to_previous_automation_point()
jump_to_automation_point(
function (point, line, next)
return math.ceil(point.time) < line and (not next or point.time > next)
end
)
end
function jump_to_next_automation_point()
jump_to_automation_point(
function (point, line, next)
return math.floor(point.time) > line and (not next or point.time < next)
end
)
end
function offset_point(offset)
local line = s.selected_line_index
local auto, param, pattern_track = get_auto()
if not param then
return false
elseif not auto then
auto = pattern_track:create_automation(param)
end
local point = get_automation_point(line, auto)
if point then
auto:add_point_at(line, confine(point.value + offset, 0, 1))
elseif not add_interpolated_point(line, auto) then
auto:add_point_at(line, param.value)
end
return true
end
function insert_or_delete_point(time, auto)
if delete_point(time, auto) then
return true
else
return add_interpolated_point(time, auto)
end
end
function delete_points_in_selection ()
local sel = s.selection_in_pattern
local auto = get_auto()
if auto and sel then
delete_points_in_range(auto, sel.start_line, sel.end_line)
return true
else
return false
end
end
function delete_envelope(param, pt)
local auto, param, pt = get_auto(param, pt)
if auto then
pt:delete_automation(param)
end
end
-- Probably useless
function insert_or_delete_point_or_selection()
if not delete_points_in_selection() then
insert_or_delete_point()
end
end
-------- Interface
local misc_dialog = nil
function toggle_misc_dialog()
if not (misc_dialog and misc_dialog.visible) then
local vb = renoise.ViewBuilder()
misc_dialog = a:show_custom_dialog(
"Automation Shift Values",
vb:row {
vb:column {
vb:text {
text = "Shift 0: "
},
vb:text {
text = "Shift 1: "
},
vb:text {
text = "Shift 2: "
},
},
vb:column {
vb:slider {
min = 0,
max = .2,
bind = t.preferences.automation_shift_0,
},
vb:slider {
min = 0,
max = .2,
bind = t.preferences.automation_shift_1,
},
vb:slider {
min = 0,
max = .2,
bind = t.preferences.automation_shift_2,
},
},
vb:column {
vb:valuefield {
min = 0,
max = 1,
bind = t.preferences.automation_shift_0,
},
vb:valuefield {
min = 0,
max = 1,
bind = t.preferences.automation_shift_1,
},
vb:valuefield {
min = 0,
max = 1,
bind = t.preferences.automation_shift_2,
},
},
},
-- vb:row {
-- vb:value {
function(dialog, key) return key end
)
else
misc_dialog:close()
end
end
-------- Keybindings and Menu Entries
t:add_keybinding{
name = "Global:View:Toggle Pattern Editor Automation Editing Dialog",
invoke = toggle_misc_dialog
}
t:add_menu_entry{
name = "Main Menu:Tools:Pattern Editor Automation Editing Dialog",
invoke = toggle_misc_dialog
}
----
t:add_keybinding{
name = "Pattern Editor:Navigation:Jump To Previous Automation Point",
invoke = jump_to_previous_automation_point
}
t:add_keybinding{
name = "Pattern Editor:Navigation:Jump To Next Automation Point",
invoke = jump_to_next_automation_point
}
----
do
local install_cmd = function(name, offset)
t:add_keybinding{
name = "Pattern Editor:Pattern Operations:" .. name,
invoke = function() offset_point(offset) end
}
end
install_cmd("Increase Automation By Shift 0", prefs.automation_shift_0)
install_cmd("Decrease Automation By Shift 0", prefs.automation_shift_0 * -1)
install_cmd("Increase Automation By Shift 1", prefs.automation_shift_1)
install_cmd("Decrease Automation By Shift 1", prefs.automation_shift_1 * -1)
install_cmd("Increase Automation By Shift 2", prefs.automation_shift_2)
install_cmd("Decrease Automation By Shift 2", prefs.automation_shift_2 * -1)
end
t:add_keybinding{
name = "Pattern Editor:Pattern Operations:Insert/Delete Automation Point",
invoke = insert_or_delete_point
}
t:add_keybinding{
name = "Pattern Editor:Pattern Operations:Delete Automation Points in Selection",
invoke = function() delete_points_in_selection() end
}
t:add_keybinding{
name = "Pattern Editor:Pattern Operations:Insert or Delete Automation Point or Selection",
invoke = function() insert_or_delete_point_or_selection() end
}
t:add_keybinding{
name = "Pattern Editor:Pattern Operations:Delete Envelope",
invoke = function() delete_envelope() end
}
--------------------------------------------------------------------------------
-- Device Automation Copying
--------------------------------------------------------------------------------
-------- Local Vars
local td_cb_ti = nil
local td_cb_dev = nil
local ptd_cb_pt = nil
local ptd_cb_dev = nil
-------- Local Functions
local function clear_pt_device_automation(to_pt, to_device)
for i, param in ipairs(to_device.parameters) do
if to_pt:find_automation(param) then
to_pt:delete_automation(param)
end
end
end
local function copy_ptd_auto(from_pt, from_device, to_pt, to_device)
clear_pt_device_automation(to_pt, to_device)
for i = 1, #from_device.parameters, 1 do
local from_pta = from_pt:find_automation(from_device:parameter(i))
if from_pta then
to_pt:create_automation(to_device:parameter(i)):copy_from(from_pta)
end
end
end
local function copy_td_audo(from_ti, from_device, to_ti, to_device)
for i, pattern in ipairs(s.patterns) do
copy_ptd_auto(pattern.tracks[from_ti], from_device,
pattern.tracks[to_ti], to_device)
end
end
-------- Bindable Functions
function copy_track_device_automation()
td_cb_dev = s.selected_track_device
td_cb_ti = s.selected_track_index
end
function copy_pattern_track_device_automation()
ptd_cb_pt = s.selected_pattern_track
ptd_cb_dev = s.selected_track_device
end
function paste_track_device_automation()
if not (td_cb_ti or td_cb_dev) then
a:show_status("Clipboard empty")
else
copy_td_audo(td_cb_ti, td_cb_dev, s.selected_track_index,
s.selected_device)
end
end
function paste_pattern_track_device_automation()
if not (ptd_cb_pt or ptd_cb_dev) then
a:show_status("Clipboard empty")
else
copy_ptd_auto(ptd_cb_pt, ptd_cb_dev,
s.selected_pattern_track, s.selected_device)
end
end
-------- Keybindings and Menu Entries
do
local install_cmd = function(entry, fn)
t:add_keybinding{ name = "DSP Chain:Edit:" .. entry, invoke = fn }
t:add_keybinding{ name = "Mixer:Edit:" .. entry, invoke = fn }
t:add_menu_entry{ name = "DSP Device:" .. entry, invoke = fn }
t:add_menu_entry{ name = "Mixer:" .. entry, invoke = fn }
end
install_cmd("Copy All Automation", copy_track_device_automation)
install_cmd("Paste All Automation", paste_track_device_automation)
install_cmd("Copy Pattern Automation", copy_pattern_track_device_automation)
install_cmd("Paste Pattern Automation", paste_pattern_track_device_automation)
end
--------------------------------------------------------------------------------
-- Misc
--------------------------------------------------------------------------------
-------- Bindable Functions
function select_previous_next_track(prev)
if prev then
s:select_previous_track()
else
s:select_next_track()
end
end
function play_from_cursor()
tr:start_at(tr.edit_pos)
end
function play_stop_from_cursor()
if tr.playing then
tr:stop()
else
tr:start_at(tr.edit_pos)
end
end
function deselect()
s.selection_in_pattern = {}
end
-- FIXME: Won't select final sub_columns under certain circumstances.
-- API bug, I think.
function select_line_in_track()
local track = s.selected_track
local ti = s.selected_track_index
local line = tr.edit_pos.line
s.selection_in_pattern = {
start_line = line,
start_track = ti,
-- start_column = 1,
end_line = line,
end_track = ti,
-- end_column = track.visible_note_columns + track.visible_effect_columns
}
end
-------- Keybindings
t:add_keybinding{
name = "Global:Transport:Select Previous Track",
invoke = function() select_previous_next_track(true) end
}
t:add_keybinding{
name = "Global:Transport:Select Next Track",
invoke = function() select_previous_next_track() end
}
t:add_keybinding{
name = "Pattern Editor:Play:Play (From Cursor)",
invoke = function() play_from_cursor() end
}
t:add_keybinding{
name = "Pattern Editor:Play:Play/Stop (From Cursor)",
invoke = function() play_stop_from_cursor() end
}
t:add_keybinding{
name = "Pattern Editor:Selection:Deselect",
invoke = function() deselect() end
}
t:add_keybinding{
name = "Pattern Editor:Selection:Select Line In Track",
invoke = function() select_line_in_track() end
}
--------------------------------------------------------------------------------
-- Junk
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- end main.lua
--------------------------------------------------------------------------------