Pattern Editor Envelope Editing, Device Automation Copy/Paste, More

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 :slight_smile:

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
--------------------------------------------------------------------------------

The vertical spacing in the above code block is HUGE. It’s denser in my editor. I’m not THAT whitespacey.

Haha, ok, so I realized the Matrix selection stuff is built-in, and immutably bound to Shift + Arrow – “globally used for selections”.

I remapped navigation, so I didn’t know it was there, and ended up recreating it.

It’s not a complete loss, though, as the Matrix selection commands in this tool are bindable Shift + Arrow replacements.