Jump to content


Photo

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


  • Please log in to reply
2 replies to this topic

#1 thunk

thunk

    Advanced Member

  • Normal Members
  • PipPipPip
  • 76 posts
  • Gender:Male
  • Location:grotto, unknow
  • Interests:Long walks on the abyssal plain

Posted 06 May 2016 - 01:48

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

 

Attached Files


  • Neurogami likes this

#2 thunk

thunk

    Advanced Member

  • Normal Members
  • PipPipPip
  • 76 posts
  • Gender:Male
  • Location:grotto, unknow
  • Interests:Long walks on the abyssal plain

Posted 06 May 2016 - 02:01

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


  • Neurogami likes this

#3 thunk

thunk

    Advanced Member

  • Normal Members
  • PipPipPip
  • 76 posts
  • Gender:Male
  • Location:grotto, unknow
  • Interests:Long walks on the abyssal plain

Posted 06 May 2016 - 06:40

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.

Edited by thunk, 06 May 2016 - 06:42.