ardour/share/scripts/lfo_automation.lua

188 lines
7.9 KiB
Lua

ardour {
["type"] = "EditorAction",
name = "Add LFO automation to region",
version = "0.2.1",
license = "MIT",
author = "Daniel Appelt",
description = [[Add LFO-like plugin automation to any automatable parameter below a 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
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
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)
-- Clear events in target automation-list for the selected region.
al:clear(region:position() - region:start(), region:position() - region:start() + region:length())
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. Using al:add seems to lead
-- to unwanted extraneous events. al:editor_add does not exhibit these side effects.
al:editor_add(pos, lower + v * (upper - lower), false)
end
last = v
end
end
-- remove dense events
al:thin (20) -- threshold of area below curve
-- TODO: display the modified automation lane in the time line in order to make the change visible!
-- Save undo
Session:add_command(al:memento_command(before, al:get_state()))
Session:commit_reversible_command(nil)
region, al, lut = nil, nil, nil
end
end
function icon (params) return function (ctx, width, height, fg)
local yc = height * .5
local x0 = math.ceil (width * .1)
local x1 = math.floor (width * .9)
ctx:set_line_width (1)
ctx:set_source_rgba (ARDOUR.LuaAPI.color_to_rgba (fg))
ctx:move_to (x0, yc * 1.5)
for x = x0, x1 do
ctx:line_to (x, yc + math.cos (3 * math.pi * (x-x0) / (x1-x0)) * yc * .5)
end
ctx:stroke ()
end end