Only show user-presets in favorite sidebar
[ardour.git] / scripts / store_recall_mixer.lua
1 ardour {
2         ["type"] = "EditorAction",
3         name = "Mixer Store",
4         author = "Ardour Lua Taskforce",
5         description = [[
6         Stores the current Mixer state as a file
7         that can be read and recalled arbitrarily.
8         Supports: processor settings, grouping,
9         mute, solo, gain, trim, pan and processor ordering,
10         plus re-adding certain deleted plugins.
11         ]]
12 }
13
14 function factory() return function()
15
16         local invalidate = {}
17         local path = ARDOUR.LuaAPI.build_filename(Session:path(), "export", "params.lua")
18
19         function mismatch_dialog(mismatch_str, checkbox_str)
20                 --string.format("Track didn't match ID: %d, but did match track in session: %s", 999, 'track')
21                 local dialog = {
22                         { type = "label", colspan = 5, title = mismatch_str },
23                         { type = "checkbox", col=1, colspan = 1, key = "use", default = true, title = checkbox_str },
24                 }
25                 local mismatch_return = LuaDialog.Dialog("", dialog):run()
26                 if mismatch_return then
27                         return mismatch_return['use']
28                 else
29                         return false
30                 end
31         end
32
33         function get_processor_by_name(track, name)
34                 local i = 0
35                 local proc = track:nth_processor(i)
36                         repeat
37                                 if(proc:display_name() == name) then
38                                         return proc
39                                 else
40                                         i = i + 1
41                                 end
42                                 proc = track:nth_processor(i)
43                         until proc:isnil()
44                 end
45
46         function new_plugin(name)
47                 for x = 0, 6 do
48                         local plugin = ARDOUR.LuaAPI.new_plugin(Session, name, x, "")
49                         if not(plugin:isnil()) then return plugin end
50                 end
51         end
52
53         function group_by_id(id)
54                 local id  = tonumber(id)
55                 for g in Session:route_groups():iter() do
56                         local group_id = tonumber(g:to_stateful():id():to_s())
57                         if group_id == id then return g end
58                 end
59         end
60
61         function group_by_name(name)
62                 for g in Session:route_groups():iter() do
63                         if g:name() == name then return g end
64                 end
65         end
66
67         function route_groupid_interrogate(t)
68                 local group = false
69                 for g in Session:route_groups():iter() do
70                         for r in g:route_list():iter() do
71                                 if r:name() == t:name() then group = g:to_stateful():id():to_s() end
72                         end
73                 end return group
74         end
75
76         function route_group_interrogate(t)
77                 for g in Session:route_groups():iter() do
78                         for r in g:route_list():iter() do
79                                 if r:name() == t:name() then return g end
80                         end
81                 end
82         end
83
84         function empty_last_store()  --empty current file from last run
85                 local file = io.open(path, "w")
86                 file:write("")
87                 file:close()
88         end
89
90         function mark_tracks(selected)
91
92                 empty_last_store()
93
94                 local route_string = [[instance = {
95                          route_id = %d,
96                          route_name = '%s',
97                          gain_control = %f,
98                          trim_control = %f,
99                          pan_control = %s,
100                          muted = %s,
101                          soloed = %s,
102                          order = {%s},
103                          cache = {%s},
104                          group = %s,
105                          group_name = '%s'
106                 }]]
107
108                 local group_string = [[instance = {
109                          group_id = %s,
110                          name = '%s',
111                          routes = {%s},
112                 }]]
113
114                 local processor_string = [[instance = {
115                          plugin_id = %d,
116                          display_name = '%s',
117                          owned_by_route_name = '%s',
118                          owned_by_route_id = %d,
119                          parameters = {%s},
120                          active = %s,
121                 }]]
122
123                 local group_route_string = " [%d] = %s,"
124                 local proc_order_string  = " [%d] = %d,"
125                 local proc_cache_string  = " [%d] = '%s',"
126                 local params_string      = " [%d] = %f,"
127
128                 --ensure easy-to-read formatting doesn't make it through
129                 local route_string     = string.gsub(route_string, "[\n\t]", "")
130                 local group_string     = string.gsub(group_string, "[\n\t]", "")
131                 local processor_string = string.gsub(processor_string, "[\n\t]", "")
132
133                 local sel = Editor:get_selection ()
134                 local groups_to_write = {}
135                 local i = 0
136
137                 local tracks = Session:get_routes()
138
139                 if selected then tracks = sel.tracks:routelist() end
140
141                 for r in tracks:iter() do
142                         local group = route_group_interrogate(r)
143                         if group then
144                                 local already_there = false
145                                 for _, v in pairs(groups_to_write) do
146                                         if group == v then
147                                                 already_there = true
148                                         end
149                                 end
150                                 if not(already_there) then
151                                         groups_to_write[#groups_to_write + 1] = group
152                                 end
153                         end
154                 end
155
156                 for _, g in pairs(groups_to_write) do
157                         local tmp_str = ""
158                         for t in g:route_list():iter() do
159                                 tmp_str = tmp_str .. string.format(group_route_string, i, t:to_stateful():id():to_s())
160                                 i = i + 1
161                         end
162                         local group_str = string.format(
163                                 group_string,
164                                 g:to_stateful():id():to_s(),
165                                 g:name(),
166                                 tmp_str
167                         )
168
169                         file = io.open(path, "a")
170                         file:write(group_str, "\r\n")
171                         file:close()
172                 end
173
174                 for r in tracks:iter() do
175                         if r:is_monitor () or r:is_auditioner () then goto nextroute end -- skip special routes
176
177                         local order = ARDOUR.ProcessorList()
178                         local x = 0
179                         repeat
180                                 local proc = r:nth_processor(x)
181                                 if not proc:isnil() then
182                                         order:push_back(proc)
183                                 end
184                                 x = x + 1
185                         until proc:isnil()
186
187                         local route_group = route_group_interrogate(r)
188                         if route_group then route_group = route_group:name() else route_group = "" end
189                         local rid = r:to_stateful():id():to_s()
190                         local pan = r:pan_azimuth_control()
191                         if pan:isnil() then pan = false else pan = pan:get_value() end --sometimes a route doesn't have pan, like the master.
192
193                         local order_nmbr = 0
194                         local tmp_order_str, tmp_cache_str = "", ""
195                         for p in order:iter() do
196                                 local pid = p:to_stateful():id():to_s()
197                                 if not(string.find(p:display_name(), "latcomp")) then
198                                         tmp_order_str = tmp_order_str .. string.format(proc_order_string, order_nmbr, pid)
199                                         tmp_cache_str = tmp_cache_str .. string.format(proc_cache_string, pid, p:display_name())
200                                 end
201                                 order_nmbr = order_nmbr + 1
202                         end
203
204                         local route_str = string.format(
205                                         route_string,
206                                         rid,
207                                         r:name(),
208                                         r:gain_control():get_value(),
209                                         r:trim_control():get_value(),
210                                         tostring(pan),
211                                         r:muted(),
212                                         r:soloed(),
213                                         tmp_order_str,
214                                         tmp_cache_str,
215                                         route_groupid_interrogate(r),
216                                         route_group
217                                 )
218
219                         file = io.open(path, "a")
220                         file:write(route_str, "\n")
221                         file:close()
222
223                         local i = 0
224                         while true do
225                                 local params = {}
226                                 local proc = r:nth_plugin (i)
227                                 if proc:isnil () then break end
228                                 local active = proc:active()
229                                 local id = proc:to_stateful():id():to_s()
230                                 local plug = proc:to_insert ():plugin (0)
231                                 local n = 0 -- count control-ports
232                                 for j = 0, plug:parameter_count () - 1 do -- iterate over all plugin parameters
233                                         if plug:parameter_is_control (j) then
234                                                 local label = plug:parameter_label (j)
235                                                 if plug:parameter_is_input (j) and label ~= "hidden" and label:sub (1,1) ~= "#" then
236                                                         local _, _, pd = ARDOUR.LuaAPI.plugin_automation(proc, n)
237                                                         local val = ARDOUR.LuaAPI.get_processor_param(proc, j, true)
238                                                         --print(r:name(), "->", proc:display_name(), label, val)
239                                                         params[n] = val
240                                                 end
241                                                 n = n + 1
242                                         end
243                                 end
244                                 i = i + 1
245
246                                 local tmp_params_str = ""
247                                 for k, v in pairs(params) do
248                                         tmp_params_str = tmp_params_str .. string.format(params_string, k, v)
249                                 end
250
251                                 local proc_str = string.format(
252                                                 processor_string,
253                                                 id,
254                                                 proc:display_name(),
255                                                 r:name(),
256                                                 r:to_stateful():id():to_s(),
257                                                 tmp_params_str,
258                                                 active
259                                         )
260                                 file = io.open(path, "a")
261                                 file:write(proc_str, "\n")
262                                 file:close()
263                         end
264                         ::nextroute::
265                 end
266         end
267
268         function recall(debug, dry_run)
269                 local file = io.open(path, "r")
270                 assert(file, "File not found!")
271                 local bypass_routes = {}
272
273                 local i = 0
274                 for l in file:lines() do
275                         --print(i, l)
276
277                         local exec_line = dry_run["dothis-"..i]
278                         local skip_line = false
279                         if not(exec_line == nil) and not(exec_line) then
280                                 skip_line = true
281                         end
282
283                         local plugin, route, group = false, false, false
284                         local f = load(l)
285
286                         if debug then
287                                 print(i, string.sub(l, 0, 29), f)
288                         end
289
290                         if f then f() end
291
292                         if instance["route_id"]  then route = true end
293                         if instance["plugin_id"] then plugin = true end
294                         if instance["group_id"]  then group = true end
295
296                         if group then
297                                 if skip_line then goto nextline end
298
299                                 local g_id   = instance["group_id"]
300                                 local routes = instance["routes"]
301                                 local name   = instance["name"]
302                                 local group  = group_by_id(g_id)
303                                 if not(group) then
304                                         local group = Session:new_route_group(name)
305                                         for _, v in pairs(routes) do
306                                                 local rt = Session:route_by_id(PBD.ID(v))
307                                                 if rt:isnil() then rt = Session:route_by_name(name) end
308                                                 if not(rt:isnil()) then group:add(rt) end
309                                         end
310                                 end
311                         end
312
313                         if route then
314                                 local substitution = tonumber(dry_run["destination-"..i])
315                                 if skip_line or (substitution == 0) then
316                                         bypass_routes[#bypass_routes + 1] = instance["route_id"]
317                                         goto nextline
318                                 end
319
320                                 local old_order = ARDOUR.ProcessorList()
321                                 local route_id = instance["route_id"]
322                                 local r_id = PBD.ID(instance["route_id"])
323                                 local muted, soloed = instance["muted"], instance["soloed"]
324                                 local order = instance["order"]
325                                 local cache = instance["cache"]
326                                 local group = instance["group"]
327                                 local group_name = instance["group_name"]
328                                 local name  = instance["route_name"]
329                                 local gc, tc, pc = instance["gain_control"], instance["trim_control"], instance["pan_control"]
330
331                                 if not(substitution == instance["route_id"]) then
332                                         print('SUBSTITUTION FOR: ', name, substitution, Session:route_by_id(PBD.ID(substitution)):name())
333                                         --bypass_routes[#bypass_routes + 1] = route_id
334                                         was_subbed = true
335                                         r_id = PBD.ID(substitution)
336                                 end
337
338                                 local rt = Session:route_by_id(r_id)
339                                 if rt:isnil() then rt = Session:route_by_name(name) end
340                                 if rt:isnil() then goto nextline end
341
342                                 local cur_group_id = route_groupid_interrogate(rt)
343                                 if not(group) and (cur_group_id) then
344                                         local g = group_by_id(cur_group_id)
345                                         if g then g:remove(rt) end
346                                 end
347
348                                 local rt_group = group_by_name(group_name)
349                                 if rt_group then rt_group:add(rt) end
350
351                                 well_known = {'PRE', 'Trim', 'EQ', 'Comp', 'Fader', 'POST'}
352
353                                 for k, v in pairs(order) do
354                                         local proc = Session:processor_by_id(PBD.ID(1))
355                                         if not(was_subbed) then
356                                                 proc = Session:processor_by_id(PBD.ID(v))
357                                         end
358                                         if proc:isnil() then
359                                                 for id, name in pairs(cache) do
360                                                         if v == id then
361                                                                 proc = new_plugin(name)
362                                                                 for _, control in pairs(well_known) do
363                                                                         if name == control then
364                                                                                 proc = get_processor_by_name(rt, control)
365                                                                                 invalidate[v] = proc:to_stateful():id():to_s()
366                                                                                 goto nextproc
367                                                                         end
368                                                                 end
369                                                                 if not(proc) then goto nextproc end
370                                                                 if not(proc:isnil()) then
371                                                                         rt:add_processor_by_index(proc, 0, nil, true)
372                                                                         invalidate[v] = proc:to_stateful():id():to_s()
373                                                                 end
374                                                         end
375                                                 end
376                                         end
377                                         ::nextproc::
378                                         if proc and not(proc:isnil()) then old_order:push_back(proc) end
379                                 end
380                                 rt:reorder_processors(old_order, nil)
381                                 if muted  then rt:mute_control():set_value(1, 1) else rt:mute_control():set_value(0, 1) end
382                                 if soloed then rt:solo_control():set_value(1, 1) else rt:solo_control():set_value(0, 1) end
383                                 rt:gain_control():set_value(gc, 1)
384                                 rt:trim_control():set_value(tc, 1)
385                                 if pc ~= false then rt:pan_azimuth_control():set_value(pc, 1) end
386                         end
387
388                         if plugin then
389                                 if skip_line then goto nextline end
390
391                                 --if the plugin is owned by a route
392                                 --we decided not to use, skip it
393                                 for _, v in pairs(bypass_routes) do
394                                         if instance["owned_by_route_id"] == v then
395                                                 goto nextline
396                                         end
397                                 end
398
399                                 local enable = {}
400                                 local params = instance["parameters"]
401                                 local p_id   = instance["plugin_id"]
402                                 local act    = instance["active"]
403
404                                 for k, v in pairs(invalidate) do --invalidate any deleted plugin's id
405                                         if p_id == k then
406                                                 p_id = v
407                                         end
408                                 end
409
410                                 local proc = Session:processor_by_id(PBD.ID(p_id))
411                                 if proc:isnil() then goto nextline end
412                                 local plug = proc:to_insert():plugin(0)
413
414                                 for k, v in pairs(params) do
415                                         local label = plug:parameter_label(k)
416                                         if string.find(label, "Assign") or string.find(label, "Enable") then --@ToDo: Check Plugin type == LADSPA or VST?
417                                                 enable[k] = v --queue any assignments/enables for after the initial parameter recalling to duck the 'in-on-change' feature
418                                         end
419                                         ARDOUR.LuaAPI.set_processor_param(proc, k, v)
420                                 end
421
422                                 for k, v in pairs(enable) do
423                                         ARDOUR.LuaAPI.set_processor_param(proc, k, v)
424                                 end
425                                 if act then proc:activate() else proc:deactivate() end
426                         end
427
428                         ::nextline::
429                         i = i + 1
430
431                 end
432         end
433
434         function dry_run(debug)
435                 --returns a dialog-able table of
436                 --everything we do (logically)
437                 --in the recall function
438                 local route_values = {['----'] = "0"}
439                 for r in Session:get_routes():iter() do
440                         route_values[r:name()] =  r:to_stateful():id():to_s()
441                 end
442
443                 local i = 0
444                 local dry_table = {
445                         {type = "label", align = "left", key =  "col-0-title" , col = 0, colspan = 1, title = 'Source Settings:'},
446                         {type = "label", align = "left", key =  "col-0-title" , col = 1, colspan = 1, title = 'Actions:'},
447                         {type = "label", align = "left", key =  "col-2-title" , col = 2, colspan = 1, title = 'Destination:'},
448                         {type = "label", align = "left", key =  "col-2-title" , col = 3, colspan = 1, title = 'Do this?'},
449                 }
450                 local file = io.open(path, "r")
451                 assert(file, "File not found!")
452
453                 for l in file:lines() do
454                         local do_plugin, do_route, do_group = false, false, false
455                         local f = load(l)
456
457                         if debug then
458                                 print(i, string.sub(l, 0, 29), f)
459                         end
460
461                         if f then f() end
462
463                         if instance["route_id"]  then do_route = true end
464                         if instance["plugin_id"] then do_plugin = true end
465                         if instance["group_id"]  then do_group = true end
466
467                         if do_group then
468                                 local group_id   = instance["group_id"]
469                                 local group_name = instance["name"]
470                                 local dlg_title, action_title  = "", ""
471
472                                 local group_ptr  = group_by_id(group_id)
473
474                                 if not(group_ptr) then
475                                         new_group = Session:new_route_group(group_name)
476                                         dlg_title = string.format("Cannot Find: (Group) %s.", group_name, new_group:name())
477                                         action_title = "will create and use settings"
478                                 else
479                                         dlg_title = string.format("Found by ID: (Group) %s.", group_ptr:name())
480                                         action_title = "will use group settings"
481                                 end
482                                 table.insert(dry_table, {
483                                         type = "label", align = "left", key =  "group-"..i , col = 0, colspan = 1, title = dlg_title
484                                 })
485                                 table.insert(dry_table, {
486                                         type = "label", align = "left", key =  "group-"..i , col = 1, colspan = 1, title = action_title
487                                 })
488                                 table.insert(dry_table, {
489                                         type = "checkbox", col=3, colspan = 1, key = "dothis-"..i, default = true, title = "line:"..i
490                                 })
491                         end
492
493                         if do_route then
494                                 local route_id   = instance["route_id"]
495                                 local route_name = instance["route_name"]
496                                 local dlg_title = ""
497
498                                 local route_ptr = Session:route_by_id(PBD.ID(route_id))
499
500                                 if route_ptr:isnil() then
501                                         route_ptr = Session:route_by_name(route_name)
502                                         if not(route_ptr:isnil()) then
503                                                 dlg_title = string.format("Found by Name: (Rotue) %s", route_ptr:name())
504                                                 action_title = "will use route settings"
505                                         else
506                                                 dlg_title = string.format("Cannot Find: (Route) %s", route_name)
507                                                 action_title = "will be ignored"
508                                         end
509                                 else
510                                         dlg_title = string.format("Found by ID: (Route) %s", route_ptr:name())
511                                         action_title = "will use route settings"
512                                 end
513                                 if route_ptr:isnil() then name = route_name else name = route_ptr:name() end
514                                 table.insert(dry_table, {
515                                         type = "label", align = "left", key = "route-"..i , col = 0, colspan = 1, title = dlg_title
516                                 })
517                                 table.insert(dry_table, {
518                                         type = "label", align = "left", key = "action-"..i , col = 1, colspan = 1, title = action_title
519                                 })
520                                 table.insert(dry_table, {
521                                         type = "dropdown", align = "left", key = "destination-"..i, col = 2, colspan = 1, title = "", values = route_values, default = name or "----"
522                                 })
523                                 table.insert(dry_table, {
524                                         type = "checkbox", col=3, colspan = 1, key = "dothis-"..i, default = true, title = "line"..i
525                                 })
526                         end
527                         i = i + 1
528                 end
529                 return dry_table
530         end
531
532         local dialog_options = {
533                 { type = "label", colspan = 5, title = "" },
534                 { type = "radio", col = 1, colspan = 7, key = "select", title = "", values ={ ["Store"] = "store", ["Recall"] = "recall" }, default = "Store"},
535                 { type = "label", colspan = 5, title = "" },
536         }
537
538         local store_options = {
539                 { type = "label", colspan = 5, title = "" },
540                 { type = "checkbox", col=1, colspan = 1, key = "selected", default = false, title = "Selected tracks only"},
541                 { type = "entry", col=2, colspan = 10, key = "filename", default = "params", title = "Store name" },
542                 { type = "label", colspan = 5, title = "" },
543         }
544
545         local recall_options = {
546                 { type = "label", colspan = 5, title = "" },
547                 { type = "file", col =1, colspan = 10, key = "file", title = "Select a File",  path = ARDOUR.LuaAPI.build_filename(Session:path(), "export", "params.lua") },
548                 { type = "label", colspan = 5, title = "" },
549         }
550
551         local rv = LuaDialog.Dialog("Mixer Store:", dialog_options):run()
552
553         if rv then
554                 local choice = rv["select"]
555                 if choice == "store" then
556                         local srv = LuaDialog.Dialog("Mixer Store:", store_options):run()
557                         if srv then
558                                 empty_last_store() --ensures that params.lua will exist for the recall dialog
559                                 path = ARDOUR.LuaAPI.build_filename(Session:path(), "export", srv["filename"] .. ".lua")
560                                 mark_tracks(srv['selected'])
561                         end
562                 end
563
564                 if choice == "recall" then
565                         local rrv = LuaDialog.Dialog("Mixer Store:", recall_options):run()
566                         if rrv then
567                                 if rrv['file'] ~= path then path = rrv['file'] end
568                                 --recall(true)
569                                 local dry_return = LuaDialog.Dialog("Mixer Store:", dry_run(true)):run()
570                                 if dry_return then recall(true, dry_return) end
571                         end
572                 end
573         end
574 collectgarbage()
575 end end