New Tool (2.7 - 3.1): Set Track Width To Active Columns

Thanks 4Tey,

I`ve updated the original post and tools page with a newer 1.32 as I had added a small modification that makes the button read “Busy” when the tool is working for next release.

https://forum.renoise.com/t/new-tool-2-7-3-1-set-track-width-to-active-columns/31078

Hi,

This tool is one of my favourite but for whatever reason it has disappeared from the Pattern context menu. It is accessible from the Tools menu however. It’s been like this for a while, maybe since the 3.0.1 update and it’s still not working in 3.1. I tried uninstalling/reinstalling, disabling all other tools, but it didn’t fix the problem.

Would you have an idea as to why it disappeared? Thanks in advance.

Hi clairobscur,

As of v1.32 of the tool, when I added the GUI, I removed the pattern menu shortcuts for neatness. There is a shortcut Set Track Widths To Active Columns available so that the GUI can be called up quickly if needed:

v1.32

-New GUI

-Now optionally hides empty fx columns; with option to leave at least one if you like the visual track bordering/buffering

-Less Shortcuts + menus for tidyness

Menu : Tools: Ledger`s Scripts: Set Track Widths To Active Columns

or

Shortcut: Set Track Widths To Active Columns

Hi clairobscur,

As of v1.32 of the tool, when I added the GUI, I removed the pattern menu shortcuts for neatness. There is a shortcut Set Track Widths To Active Columns available so that the GUI can be called up quickly if needed:

I apologize for missing that piece of information. Thank you for your reply!

No problem, the info is a bit hidden away in the first post in the spoiler button (like below)

Click to view contents

!! 3x Gold coins !!

Hey ledger,

would be nice if the tool was also removing empty columns before an active column within a track… :slight_smile:

You could also add that code then to your tool “split into separate tracks” (which is very slow btw., did you use a interval to do stuff, so its same slow on all machines, to prevent execution overload?)

EDIT: Oh, that tool was by fladd.

Hey ledger,

would be nice if the tool was also removing empty columns before an active column within a track… :slight_smile:

All suggestions noted, but tool updates are snails pace here at the moment unfortunately…

bug on linux at line 6 (on unix letter case matters):

change

require "Process_slicer"

to

require "process_slicer"

@Ledger,

I took the liberty of trying to severely optimize the scanning. Have a look to see if it’s working all right. I skipped the process slicer since it seems so fast anyway, and fixed a GUI bug happening when reopening the window.

EDIT: Added a toggle for optimizing vol/pan/dly column visibility.

7062 ledger.scripts.SetTrackToActiveColumns.xrnx

Thanks joule, will take a look

bug on linux at line 6 (on unix letter case matters):

change

require “Process_slicer”

to

require “process_slicer”

Thanks kopias, will take a look here too

@Ledger,

I took the liberty of trying to severely optimize the scanning. Have a look to see if it’s working all right. I skipped the process slicer since it seems so fast anyway, and fixed a GUI bug happening when reopening the window.

EDIT: Added a toggle for optimizing vol/pan/dly column visibility.

attachicon.gifledger.scripts.SetTrackToActiveColumns.xrnx

I have updated the tools page with the process slicer fix and also linked the OP to your post here joule. So far it seems to work great, but I`ll try and have a scan over the code soon just to familiarise myself with the changes, then update the tools page.

Did you test drive it yet? :slight_smile: I think you should update the official version with it, if there isn’t any problem. It’s so much faster.

Did you test drive it yet? :slight_smile: I think you should update the official version with it, if there isn’t any problem. It’s so much faster.

Sorry, forgot about this :). I did start looking at the code a while ago, but got a bit stuck decifering your string technique, then never got round to looking again. Anyhow, i`ve just updated tool page with 1.34 and your improvements.

Thanks for adding them.


http://www.renoise.com/tools/set-track-to-active-columns

v1.34 Joule`s speed and GUI improvements added

It’s just scanning the line string by the relevant offsets instead of iterating the standard note/effect columns. My feeling is that when you on average have to read a line more than, say maybe three times, it’s faster to access the line string and use string functions to get the values.

Hmm… It’s also possible to optimize this tool further by not scanning patterntrack aliases :stuck_out_tongue:

I see, just got a bit lost in the details if I remember.

Feel free to upload more optimisations :slight_smile:

though it does work pretty fast now how it is!

@Ledger,

Raul noticed a bug that I’ve fixed here… The sample fx column wasn’t taken into account.

I also want to add another checkbox - “include unused patterns”. This checkbox would only be displayed when patterns like these are found.

But I have another question… Currently, “orphan” delay values et c are disregarded when hiding note columns. If a column only has a panning value and no note values, it will currently be hidden. Should that be fixed as well? I imagine it better to ‘encourage’ the user to clean up such nonsensical data, by not hiding it.

Ah, yes sounds like I would have missed the sample fx col as I don`t really use it. Thanks for fixing.

The checkbox sounds like a welcome addition.

Yes, the hiding orphan values should probably be considered a bug too, as they can still effect previously playing samples. i.e.

C-4

— 32

— 44

if the 32 and 44 are volume or pan commands they will effect the C-4, if it is still sounding, so they are still legitimate.

A fix there is welcome too. Just upload when you are happy with it and I`ll put it up on tools site.

Cheers!

Thanks for improving this tool!

I am studying this particular code, how it “reads” each line to detect the data.

Maybe this tool is ideal to pose a question.Imagine that you do a little bit the beast with the 1000 patterns, full of data within the selected track to analyze.It is very likely that the tool takes more than 10 seconds to do the whole sweep.Then, the famous Renoise warning will appear to stop the process of the tool.What would be the best procedure to program, to avoid the limit of the 10 seconds of time? Imagine that you do 2 equal functions that cover each 500 patterns, and execute them one after the other. Would this work?I am commenting on an extreme case to understand it.

I mean, those 10 seconds are a limit for each function?

It is very likely that the tool takes more than 10 seconds to do the whole sweep.Then, the famous Renoise warning will appear to stop the process of the tool.What would be the best procedure to program, to avoid the limit of the 10 seconds of time?

I think you’ll need to look at yield and coroutines Raul in the file process_slicer.lua → https://forum.renoise.com/t/example-slicing-up-a-processing-function-with-coroutines/30333And before you ask, no (luckily) I’ve never looked at it. The very very basic synopsis is that it breaks up say a long executing ‘for loop/while loop’ and yields back every so often to Renoise HQ, just to let Renoise know that the script/tool hasn’t stuck in an infinite loop.

@Raul, the earlier 1.3 version of this tool uses the process slicer. It is still available to download on the tools page

http://www.renoise.com/tools/set-track-to-active-columns

You can see the ProcessSlicer() calls from line 196 onwards in the following copy of main(). There is a require “Process_slicer” on line 1, as the ProcessSlicer() class is in another .lua file in the tool

main() from 1.3

Click to view contents
require "Process_slicer"  
require "note_column_functions"
require "effects_column_functions"

------------------------------------------
--Set up Preferences file
------------------------------------------
--create preferences xml
options = renoise.Document.create {
  include_fx = true,
  leave_one = false
}
 --assign options-object to .preferences so renoise knows to load and update it with the tool
renoise.tool().preferences = options
------------------------------------------
--------------------------------------------
--dialog global
local my_dialog
--------------------------------------------------------------
--GUI
--------------------------------------------------------------
function invoke_gui()
  
  --1 dialog at a time
  if (my_dialog and my_dialog.visible) then -- only allows one dialog instance
    my_dialog:close()
    return
  end
  --viewbuilder object
  local vb = renoise.ViewBuilder()
  
  ---------------------------------------
  --executes when re-order button pressed
  ----------------------------------------
  local function re_order_button()
    if vb.views["popup"].value == 1 then
      process_single_track_in_pattern() 
    elseif vb.views["popup"].value == 2 then  
     -- process_selection_in_pattern()
     main_selection_in_pattern()
    elseif vb.views["popup"].value == 3 then
      process_whole_track()
    end 
  end
  
  --dialog content
  local dialog_content = vb:column {
   
      margin = 8,
      
      vb:vertical_aligner {
       mode = "center",
       spacing = 8,
      
      vb:column {
       style = "group",
       margin = 7,
       
      vb:text {
       text = "Range:" 
      },
      
      vb:popup {
       value = 1,
       id = "popup",
       width = 120,
       items = {
         "All Tracks in Song",
         "Track in Song",
         
        },           
       }, 
       
       vb:text {
       text = "Include Effects Columns:" 
      },
      
      vb:checkbox{
      active = true,
      value = options.include_fx.value, --`not` preserve columns as GUI label is left align
      notifier = function()
                   --set bool for including fx scan or not
                   options.include_fx.value = not options.include_fx.value
                   --set `Min of 1 Effects Column:` checkbox to be active or not
                   vb.views["min"].active = options.include_fx.value
                 end
      },
      
      
         vb:text {
       text = "Min of 1 Effects Column:" 
      },
      
      vb:checkbox{
      id = "min",
      active = options.include_fx.value,
      value = options.leave_one.value, --`not` preserve columns as GUI label is left align
      notifier = function()options.leave_one.value = not options.leave_one.value
                 end
      },
       
       
      },--column
      
      vb:horizontal_aligner {
       mode = "center",
       margin = 4,
      
      vb:button {
       text = "Apply",
       id = "button",
       height = 24,
       width = 110,
       notifier = function()
                    
                    --process Single Track note columns only
                    if vb.views["popup"].value == 2 then
                      process_single_track()
                      if options.include_fx.value then
                        --all tracks fx
                        process_single_track_fx()
                      end
                      return --job done
                    end 
                    --process All Tracks
                    if vb.views["popup"].value == 1 then
                      --all tracks notes
                      process()
                      --and all track fx
                      if options.include_fx.value then
                        --all tracks fx
                        process_fx()
                      end
                    end
                  end,
      },           
     }
    }
   }
  ------------------------------------------------------      
   --key Handler
  local function my_keyhandler_func(dialog, key)
  
   --scrolling the range popup with the up and down keys
 --[[ if key.name == "down" then
     if vb.views["popup"].value < 3 then
       vb.views["popup"].value = vb.views["popup"].value + 1
     else 
       return
     end
   elseif key.name == "up" then
     if vb.views["popup"].value > 1 then
       vb.views["popup"].value = vb.views["popup"].value - 1
     else
       return
     end 
     
   --executing re-order button with return key
   elseif key.name == "return" then
     re_order_button()--]]
     
   --closing the dialog with escape
   if not (key.modifiers == "" and key.name == "esc") then
      return key
   else
     dialog:close()
   end
  end
  
  my_dialog = renoise.app():show_custom_dialog("To Active Columns", dialog_content, my_keyhandler_func)
end

--------------------------------
--Keybindings
--------------------------------
renoise.tool():add_keybinding {
  name = "Global:Tools:Set Track Widths To Active Columns",
  invoke = function() invoke_gui()
  end  
}
renoise.tool():add_menu_entry {
  name = "Main Menu:Tools:Ledger`s Scripts:Set Track Widths To Active Columns",
  invoke = function() invoke_gui()
  end
}
----------------------------------------------------------------------------------------
--Process slicers Current track or all tracks --called in Apply button notifier function
----------------------------------------------------------------------------------------
--Current track
function process_single_track()
  --single_track = true
  local slicer = ProcessSlicer(function() set_track_to_active_columns(true)end)
  slicer:start()
end
-----------------------------------------------------------------------
--All tracks
function process()
 -- single_track = false
  local slicer = ProcessSlicer(function() set_track_to_active_columns(false)end)
  slicer:start()
end
-----------------------------------------------------------------------
--EFFECTS
-----------------------------------------------------------------------
--Current track
function process_single_track_fx()
 -- single_track = true
  local slicer = ProcessSlicer(function() set_track_to_active_fx_columns(true)end)
  slicer:start()
end
--All tracks
function process_fx()
 -- single_track = false
  local slicer = ProcessSlicer(function() set_track_to_active_fx_columns(false)end)
  slicer:start()
end

Process Slicer class

Click to view contents
--[[============================================================================
process_slicer.lua
============================================================================]]--
--[[
ProcessSlicer allows you to slice up a function which takes a lot of 
processing time into multiple calls via Lua coroutines. 
* Example usage:
local slicer = ProcessSlicer(my_process_func, argument1, argumentX)
-- This starts calling 'my_process_func' in idle, passing all arguments 
-- you've specified in the ProcessSlicer constructor.
slicer:start() 
-- To abort a running sliced process, you can call "stop" at any time
-- from within your processing function of outside of it in the main thread. 
-- As soon as your process function returns, the slicer is automatically 
-- stopped.
slicer:stop() 
-- To give processing time back to Renoise, call 'coroutine.yield()' 
-- anywhere in your process function to temporarily yield back to Renoise:
function my_process_func()
  for j=1,100 do
    -- do something that needs a lot of time, and periodically call 
    -- "coroutine.yield()" to give processing time back to Renoise. Renoise 
    -- will switch back to this point of the function as soon as has done 
    -- "its" job:
    coroutine.yield()
  end  
end

* Drawbacks:
By slicing your processing function, you will also slice any changes that are 
done to the Renoise song into multiple undo actions (one action per slice/yield).
Modal dialogs will block the slicer, cause on_idle notifications are not fired then. 
It will even block your own process GUI when trying to show it modal.
]]
class "ProcessSlicer"
function ProcessSlicer:__init(process_func, ...)
  assert(type(process_func) == "function", 
    "expected a function as first argument")
  self.__process_func = process_func
  self.__process_func_args = arg
  self.__process_thread = nil
end

--------------------------------------------------------------------------------
-- returns true when the current process currently is running
function ProcessSlicer:running()
  return (self.__process_thread ~= nil)
end

--------------------------------------------------------------------------------
-- start a process
function ProcessSlicer:start()
  assert(not self:running(), "process already running")
  
  self.__process_thread = coroutine.create(self.__process_func)
  
  renoise.tool().app_idle_observable:add_notifier(
    ProcessSlicer.__on_idle, self)
end

--------------------------------------------------------------------------------
-- stop a running process
function ProcessSlicer:stop()
  assert(self:running(), "process not running")
  renoise.tool().app_idle_observable:remove_notifier(
    ProcessSlicer.__on_idle, self)
  self.__process_thread = nil
end

--------------------------------------------------------------------------------
-- function that gets called from Renoise to do idle stuff. switches back 
-- into the processing function or detaches the thread
function ProcessSlicer:__on_idle()
  assert(self.__process_thread ~= nil, "ProcessSlicer internal error: "..
    "expected no idle call with no thread running")
  
  -- continue or start the process while its still active
  if (coroutine.status(self.__process_thread) == 'suspended') then
    local succeeded, error_message = coroutine.resume(
      self.__process_thread, unpack(self.__process_func_args))
    
    if (not succeeded) then
      -- stop the process on errors
      self:stop()
      -- and forward the error to the main thread
      error(error_message) 
    end
    
  -- stop when the process function completed
  elseif (coroutine.status(self.__process_thread) == 'dead') then
    self:stop()
  end
end

So basically:

--include the ProcessSlicer class (in this case we store it in Process_slicer.lua)
require "Process_slicer" 

--Pass your cpu_heavy_function() into a ProcessSlicer constructor and start the slicer with the :start() method on the object you just created.
function sliced_func()
  --constructor
  local slicer = ProcessSlicer(function() cpu_heavy_function() end)
  --call method to start
  slicer:start()
end

packaged up in a function here sliced_func() that can be called elsewhere, or by a menu/ shortcut.

Note: As these functions were placed at the end of the original main.lua, they were defined as global and not local to account for upvalues.