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