summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorDaniel Appelt <daniel.appelt@gmail.com>2019-11-14 10:08:13 +0100
committerRobin Gareus <robin@gareus.org>2019-11-14 22:43:21 +0100
commit5fb949267dde724519c505106dcbaf8ec78e3775 (patch)
tree6b610ef4edbcf88a1572645b2d198236aca12da0 /scripts
parent3c446a52753b1575b7e4a65cdf02efca01f5cdad (diff)
Add script to create LFO-like plugin automation
Diffstat (limited to 'scripts')
-rw-r--r--scripts/lfo_automation.lua173
1 files changed, 173 insertions, 0 deletions
diff --git a/scripts/lfo_automation.lua b/scripts/lfo_automation.lua
new file mode 100644
index 0000000000..7907ae0748
--- /dev/null
+++ b/scripts/lfo_automation.lua
@@ -0,0 +1,173 @@
+ardour {
+ ["type"] = "EditorAction",
+ name = "Add LFO automation to region",
+ version = "0.1.1",
+ license = "MIT",
+ author = "Daniel Appelt",
+ description = [[Add LFO-like plugin automation to selected region]]
+}
+
+function factory (unused_params)
+ return function ()
+ -- Retrieve the first selected region
+ -- TODO: the following statement should do just that, no!?
+ -- local region = Editor:get_selection().regions:regionlist():front()
+ local region = nil
+ for r in Editor:get_selection().regions:regionlist():iter() do
+ if region == nil then region = r end
+ end
+
+ -- Bail out if no region was selected
+ if region == nil then
+ LuaDialog.Message("Add LFO automation to region", "Please select a region first!",
+ LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run()
+ return
+ end
+
+ -- Identify the track the region belongs to. There really is no better way?!
+ local track = nil
+ for route in Session:get_tracks():iter() do
+ for r in route:to_track():playlist():region_list():iter() do
+ if r == region then track = route:to_track() end
+ end
+ end
+
+ -- Get a list of all available plugin parameters on the track. This looks ugly. For the original code
+ -- see https://github.com/Ardour/ardour/blob/master/scripts/midi_cc_to_automation.lua
+ local targets = {}
+ local i = 0
+ while true do -- iterate over all plugins on the route
+ if track:nth_plugin(i):isnil() then break end
+
+ local proc = track:nth_plugin(i) -- ARDOUR.LuaAPI.plugin_automation() expects a proc not a plugin
+ local plug = proc:to_insert():plugin(0)
+ local plug_label = i .. ": " .. plug:name() -- Handle ambiguity if there are multiple plugin instances
+ local n = 0 -- Count control-ports separately. ARDOUR.LuaAPI.plugin_automation() only returns those.
+ for j = 0, plug:parameter_count() - 1 do -- Iterate over all parameters
+ if plug:parameter_is_control(j) then
+ local label = plug:parameter_label(j)
+ if plug:parameter_is_input(j) and label ~= "hidden" and label:sub(1,1) ~= "#" then
+ -- We need two return values: the plugin-instance and the parameter-id. We use a function to
+ -- return both values in order to avoid another sub-menu level in the dropdown.
+ local nn = n -- local scope for return value function
+ targets[plug_label] = targets[plug_label] or {}
+ targets[plug_label][label] = function() return {["p"] = proc, ["n"] = nn} end
+ end
+ n = n + 1
+ end
+ end
+
+ i = i + 1
+ end
+
+ -- Bail out if there are no plugin parameters
+ if next(targets) == nil then
+ LuaDialog.Message("Add LFO automation to region", "No plugin parameters found.",
+ LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run()
+ region, track, targets = nil, nil, nil
+ collectgarbage()
+ return
+ end
+
+ -- Display dialog to select (plugin and) plugin parameter, and LFO cycle type + min / max
+ local dialog_options = {
+ { type = "heading", title = "Add LFO automation to region", align = "left"},
+ { type = "dropdown", key = "param", title = "Plugin parameter", values = targets },
+ { type = "dropdown", key = "wave", title = "Waveform", values = {
+ ["Ramp up"] = 1, ["Ramp down"] = 2, ["Triangle"] = 3, ["Sine"] = 4,
+ ["Exp up"] = 5, ["Exp down"] = 6, ["Log up"] = 7, ["Log down"] = 8 } },
+ { type = "number", key = "cycles", title = "No. of cycles", min = 1, max = 16, step = 1, digits = 0 },
+ { type = "slider", key = "min", title = "Minimum in %", min = 0, max = 100, digits = 1 },
+ { type = "slider", key = "max", title = "Maximum in %", min = 0, max = 100, digits = 1, default = 100 }
+ }
+ local rv = LuaDialog.Dialog("Select target", dialog_options):run()
+
+ -- Return if the user cancelled
+ if not rv then
+ region, track, targets = nil, nil, nil
+ collectgarbage()
+ return
+ end
+
+ -- Parse user response
+ assert(type(rv["param"]) == "function")
+ local pp = rv["param"]() -- evaluate function, retrieve table {["p"] = proc, ["n"] = nn}
+ local al, _, pd = ARDOUR.LuaAPI.plugin_automation(pp["p"], pp["n"])
+ local wave = rv["wave"]
+ local cycles = rv["cycles"]
+ -- Compute minimum and maximum requested parameter values
+ local lower = pd.lower + rv["min"] / 100 * (pd.upper - pd.lower)
+ local upper = pd.lower + rv["max"] / 100 * (pd.upper - pd.lower)
+ track, targets, rv, pd = nil, nil, nil, nil
+ assert(not al:isnil())
+
+ -- Define lookup tables for our waves. Empty ones will be calculated in a separate step.
+ -- TODO: at this point we already know which one is needed, still we compute all.
+ local lut = {
+ { 0, 1 }, -- ramp up
+ { 1, 0 }, -- ramp down
+ { 0, 1, 0 }, -- triangle
+ {}, -- sine
+ {}, -- exp up
+ {}, -- exp down
+ {}, -- log up
+ {} -- log down
+ }
+
+ -- Calculate missing look up tables
+ local log_min = math.exp(-2 * math.pi)
+ for i = 0, 20 do
+ -- sine
+ lut[4][i+1] = 0.5 * math.sin(i * math.pi / 10) + 0.5
+ -- exp up
+ lut[5][i+1] = math.exp(-2 * math.pi + i * math.pi / 10)
+ -- log up
+ lut[7][i+1] = -math.log(1 + (i / log_min - i) / 20) / math.log(log_min)
+ end
+ -- "down" variants just need the values in reverse order
+ for i = 21, 1, -1 do
+ -- exp down
+ lut[6][22-i] = lut[5][i]
+ -- log down
+ lut[8][22-i] = lut[7][i]
+ end
+
+ -- Initialize undo
+ Session:begin_reversible_command("Add LFO automation to region")
+ local before = al:get_state() -- save previous state (for undo)
+ al:clear_list() -- clear target automation-list
+
+ local values = lut[wave]
+ local last = nil
+ for i = 0, cycles - 1 do
+ -- cycle length = region:length() / cycles
+ local cycle_start = region:position() - region:start() + i * region:length() / cycles
+ local offset = region:length() / cycles / (#values - 1)
+
+ for k, v in pairs(values) do
+ local pos = cycle_start + (k - 1) * offset
+ if k == 1 and v ~= last then
+ -- Move event one sample further. A larger offset might be needed to avoid unwanted effects.
+ pos = pos + 1
+ end
+
+ if k > 1 or v ~= last then
+ -- Create automation point re-scaled to parameter target range. Do not create a new point
+ -- at cycle start if the last cycle ended on the same value.
+ al:add(pos, lower + v * (upper - lower), false, true)
+ end
+ last = v
+ end
+ end
+
+ -- TODO: display the modified automation lane in the time line in order to make the change visible!
+
+ -- Save undo
+ -- TODO: in Ardour 5.12 this does not lead to an actual entry in the undo list?!
+ Session:add_command(al:memento_command(before, al:get_state()))
+ Session:commit_reversible_command(nil)
+
+ region, al, lut = nil, nil, nil
+ collectgarbage()
+ end
+end