1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
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
-- remove dense events
al:thin (20) -- threashold of area below curve
-- 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
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
|