View Issue Details

IDProjectCategoryView StatusLast Update
0008784ardourbugspublic2021-11-14 10:00
ReporterWerner Back Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
PlatformMicrosoftOSWindowsOS Version10
Product Version6.8 
Summary0008784: LUA: Store Mixer Settings has pan wrong
DescriptionI used the "Store Mixer Settings" Lua script to save a "scene", but when I restore from that file the pans are set wrong. I checked the saved values and it looks as if the way the numbers are stored is wrong. It looks like the following:

...,pan_control=0,5,...
 but should look like
...,pan_control=0.5,0.5,...
(this example is for "center position").

If I correct this manually in the file the pans look alright.
Steps To ReproduceStore + recall mixer settings with LUA script.
TagsNo tags attached.

Activities

Werner Back

2021-08-03 10:53

reporter   ~0026087

Oh, just saw that it's a locale problem. LUA replaces the "." with "," in german environments.

Something like that should work:
            if pan ~= false then
                local strPan = tostring(pan)
                pan = strPan:gsub(",", ".")
            end

Werner Back

2021-08-04 11:26

reporter   ~0026089

Ah, I played a little with LUA (without understanding everything ;)) and it seems that this works:

Replace the following line:
if pan:isnil() then pan = false else pan = pan:get_value() end --sometimes a route doesn't have pan, like the master.

with
if pan:isnil() then pan = false else pan = ARDOUR.LuaAPI.ascii_dtostr(pan:get_value()) end --sometimes a route doesn't have pan, like the master.

Probably there are some more parameters where this problem occurs? I couldn't find out what "send_string" does? Guess that's Mixbus only, right? Since the LUA script's author is "Mixbus Team", is it possible, that this bug doesn't appear in Mixbus?

Werner Back

2021-08-05 06:21

reporter   ~0026090

This works for me now.
a_db2b41821.lua (10,885 bytes)   
ardour {
	["type"]    = "EditorAction",
	name        = "Store Mixer Settings WB1.1",
	author      = "Mixbus Team",
	description = [[
		Stores the current Mixer state as a file
		that can be read and recalled arbitrarily
		by it's companion script, Recall Mixer Settings.
	
		Supports: processor settings, grouping,
		mute, solo, gain, trim, pan and processor ordering,
		plus re-adding certain deleted plugins.
	]]
}

function factory () return function ()

	local user_cfg = ARDOUR.user_config_directory(-1)
	local local_path = ARDOUR.LuaAPI.build_filename(Session:path(), 'mixer_settings')
	local global_path = ARDOUR.LuaAPI.build_filename(user_cfg, 'mixer_settings')

	function exists(file)
		local ok, err, code = os.rename(file, file)
		if not ok then
			if code == 13 then -- Permission denied, but it exists
				return true
			end
		end return ok, err
	end

	function whoami()
		if not pcall(function() local first_check = Session:get_mixbus(0) end) then
			return "Ardour"
		else
			local second_check = Session:get_mixbus(11)
			if second_check:isnil() then
				return "Mixbus"
			else
				return "32C"
			end
		end
	end

	function isdir(path)
		return exists(path.."/")
	end

	function setup_paths()
		local global_ok, local_ok = false, false

		if not(isdir(global_path)) then
			global_ok, _, _ = os.execute('mkdir '.. global_path)
			if global_ok == 0 then
				global_ok = true
			end
		else
			global_ok = true
		end
		
		if not(isdir(local_path)) then
			local_ok, _, _ = os.execute('mkdir ' .. "\"" .. local_path .."\"")
			if local_ok == 0 then
				local_ok = true
			end
		else
			local_ok = true
		end
		return global_ok, local_ok
	end

	function get_processor_by_name(track, name)
		local i = 0
		local proc = track:nth_processor(i)
			repeat
				if(proc:display_name() == name) then
					return proc
				else
					i = i + 1
				end
				proc = track:nth_processor(i)
			until proc:isnil()
		end

	function group_by_id(id)
		local id  = tonumber(id)
		for g in Session:route_groups():iter() do
			local group_id = tonumber(g:to_stateful():id():to_s())
			if group_id == id then return g end
		end
	end

	function group_by_name(name)
		for g in Session:route_groups():iter() do
			if g:name() == name then return g end
		end
	end

	function route_groupid_interrogate(t)
		local group = false
		for g in Session:route_groups():iter() do
			for r in g:route_list():iter() do
				if r:name() == t:name() then group = g:to_stateful():id():to_s() end
			end
		end return group
	end

	function route_group_interrogate(t)
		for g in Session:route_groups():iter() do
			for r in g:route_list():iter() do
				if r:name() == t:name() then return g end
			end
		end
	end

	function empty_last_store(path)  --empty current file from last run
		local file = io.open(path, "w")
		--file:write(string.format("instance = { whoami = '%s' }", whoami())
		file:write("")
		file:close()
	end

	function mark_tracks(selected, path)

		empty_last_store(path)

		local route_string = [[instance = {
			route_id = %d,
			route_name = '%s',
			gain_control = %s,
			trim_control = %s,
			pan_control = %s,
			sends = {%s},
			muted = %s,
			soloed = %s,
			order = {%s},
			cache = {%s},
			group = %s,
			group_name = '%s',
			pi_order = %d
		}]]

		local group_string = [[instance = {
			group_id = %s,
			name = '%s',
			routes = {%s},
		}]]

		local processor_string = [[instance = {
			plugin_id = %d,
			type = %d,
			display_name = '%s',
			owned_by_route_name = '%s',
			owned_by_route_id = %d,
			parameters = {%s},
			active = %s,
		}]]

		local group_route_string = " [%d] = %s,"
		local proc_order_string  = " [%d] = %d,"
		local proc_cache_string  = " [%d] = {'%s', %d},"
		local params_string      = " [%d] = %s,"

		--ensure easy-to-read formatting doesn't make it through
		local route_string     = string.gsub(route_string, "[\n\t%s]", "")
		local group_string     = string.gsub(group_string, "[\n\t%s]", "")
		local processor_string = string.gsub(processor_string, "[\n\t%s]", "")

		local sel = Editor:get_selection ()
		local groups_to_write = {}
		local i = 0

		local tracks = Session:get_stripables()

		if selected then tracks = sel.tracks:routelist() end

		for r in tracks:iter() do
			local group = route_group_interrogate(r)
			if group then
				local already_there = false
				for _, v in pairs(groups_to_write) do
					if group == v then
						already_there = true
					end
				end
				if not(already_there) then
					groups_to_write[#groups_to_write + 1] = group
				end
			end
		end

		for _, g in pairs(groups_to_write) do
			local tmp_str = ""
			for t in g:route_list():iter() do
				tmp_str = tmp_str .. string.format(group_route_string, i, t:to_stateful():id():to_s())
				i = i + 1
			end
			local group_str = string.format(
				group_string,
				g:to_stateful():id():to_s(),
				g:name(),
				tmp_str
			)

			file = io.open(path, "a")
			file:write(group_str, "\r\n")
			file:close()
		end

		for r in tracks:iter() do
			if r:is_monitor () or r:is_auditioner () or not(r:to_vca():isnil()) then goto nextroute end -- skip special routes
			r = r:to_route()
			if r:isnil() then goto nextroute end
			local order = ARDOUR.ProcessorList()
			local x = 0
			repeat
				local proc = r:nth_processor(x)
				if not proc:isnil() then
					order:push_back(proc)
				end
				x = x + 1
			until proc:isnil()


			local route_group = route_group_interrogate(r)
			if route_group then route_group = route_group:name() else route_group = "" end
			local rid = r:to_stateful():id():to_s()
			local pan = r:pan_azimuth_control()
			if pan:isnil() then pan = false else pan = ARDOUR.LuaAPI.ascii_dtostr(pan:get_value()) end --sometimes a route doesn't have pan, like the master.

			-- Get send information, if any.
			local send_string = ""
			local i = 0
			repeat
				local fmt = "{%s, %s, %s, %s}"
				string.gsub(fmt, "[\n\t]", "")
				local values = {}
				for j, ctrl in pairs({
					r:send_level_controllable(i),
					r:send_enable_controllable(i),
					r:send_pan_azimuth_controllable(i),
					r:send_pan_azimuth_enable_controllable(i),
				}) do
					if not(ctrl:isnil()) then
						values[#values + 1] = ARDOUR.LuaAPI.ascii_dtostr(ctrl:get_value())
					else
						values[#values + 1] = "nil"
					end
				end
				send_string = send_string .. string.format(fmt, table.unpack(values))
				send_string = send_string .. ","
				i = i + 1
			until r:send_enable_controllable(i):isnil()

			--print("send_string="..send_string)

			local order_nmbr = 0
			local tmp_order_str, tmp_cache_str = "", ""
			for p in order:iter() do
				local ptype
				if not(p:to_insert():isnil()) then
					ptype = p:to_insert():plugin(0):get_info().type
				else
					ptype = 99
				end
				local pid = p:to_stateful():id():to_s()
				if not(string.find(p:display_name(), "latcomp")) then
					tmp_order_str = tmp_order_str .. string.format(proc_order_string, order_nmbr, pid)
					tmp_cache_str = tmp_cache_str .. string.format(proc_cache_string, pid, p:display_name(), ptype)
				end
				order_nmbr = order_nmbr + 1
			end

			local route_str = string.format(
					route_string,
					rid,
					r:name(),
					ARDOUR.LuaAPI.ascii_dtostr(r:gain_control():get_value()),
					ARDOUR.LuaAPI.ascii_dtostr(r:trim_control():get_value()),
					tostring(pan),
					send_string,
					r:muted(),
					r:soloed(),
					tmp_order_str,
					tmp_cache_str,
					route_groupid_interrogate(r),
					route_group,
					r:presentation_info_ptr():order()
				)

			file = io.open(path, "a")
			file:write(route_str, "\n")
			file:close()

			local i = 0
			while true do
				local params = {}
				local proc = r:nth_plugin (i)
				if proc:isnil () then break end
				local active = proc:active()
				local id = proc:to_stateful():id():to_s()
				local plug = proc:to_insert ():plugin (0)
				local ptype = proc:to_insert():plugin(0):get_info().type

				local n = 0
				for j = 0, plug:parameter_count() - 1 do -- Iterate over all plugin 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
							local _, _, pd = ARDOUR.LuaAPI.plugin_automation(proc, n)
							local val = ARDOUR.LuaAPI.get_processor_param(proc, n, true)

							-- Clamp values at plugin max and min
							if val < pd.lower then
								val = pd.lower
							end

							if val > pd.upper then
								val = pd.upper
							end

							--print(r:name(), "->", proc:display_name(), "(#".. n ..")",  label, val)
							params[n] = val
						end
						n = n + 1
					end
				end
				i = i + 1

				local tmp_params_str = ""
				for k, v in pairs(params) do
					tmp_params_str = tmp_params_str .. string.format(params_string, k, ARDOUR.LuaAPI.ascii_dtostr(v))
				end

				local proc_str = string.format(
						processor_string,
						id,
						ptype,
						proc:display_name(),
						r:name(),
						r:to_stateful():id():to_s(),
						tmp_params_str,
						active
					)
				file = io.open(path, "a")
				file:write(proc_str, "\n")
				file:close()
			end
			::nextroute::
		end
	end

	local store_options = {
		{ type = "label",    col=0, colspan=1, align="right", title = "Name:" },
		{ type = "entry",    col=1, colspan=1, align="left" , key = "filename", default = Session:name(), title=""},
		{ type = "label",    col=0, colspan=1, align="right", title = "Location:" },
		{
			type = "radio",  col=1, colspan=3, align="left", key = "store-dir", title = "", values =
			{
				['Global (accessible from any session)'] = 1, ['Local (this session only)'] = 2
			},
			default = 'Locally (this session only)'
		},
		{ type = "hseparator", title="", col=0, colspan = 3},
		{ type = "label",    col=0, colspan=1, align="right", title = "Selected Tracks Only:" },
		{ type = "checkbox", col=1, colspan=1, align="left",  key = "selected", default = false, title = ""},
		--{ type = "label", col=0, colspan=2, align="left", title = ''},
		--{ type = "label", col=0, colspan=2, align="left", title = "Global Path: " .. global_path},
		--{ type = "label", col=0, colspan=2, align="left", title = "Local Path: "  .. local_path},
	}

	local global_ok, local_ok = setup_paths()
print("global_ok="..tostring(global_ok))
print("local_ok="..tostring(local_ok))
	if global_ok and local_ok then
		local rv = LuaDialog.Dialog("Store Mixer Settings:", store_options):run()

		if not(rv) then return end

		local filename = rv['filename']
		if rv['store-dir'] == 1 then
			local store_path = ARDOUR.LuaAPI.build_filename(global_path, string.format("%s-%s.lua", filename, whoami()))
			local selected = rv['selected']
			mark_tracks(selected, store_path)
		end

		if rv['store-dir'] == 2 then
			local store_path = ARDOUR.LuaAPI.build_filename(local_path, string.format("%s-%s.lua", filename, whoami()))
			print(store_path)
			local selected = rv['selected']
			mark_tracks(selected, store_path)
		end
	end

end end
a_db2b41821.lua (10,885 bytes)   

Werner Back

2021-11-14 10:00

reporter   ~0026216

Further Problems: Storing fails if project name contains blanks. Fixed this.

Still not fixed and beyond my skills: if the Project contains a foldback, recalling a stored setting duplicates all foldback strips again and again. This leads to unusable projects because DSP load increases every time you load.

See the simple attached project files (diff them and you'll see). These were made in mixbus, but in ardour it's the same.
mixer_settings_store.lua (10,781 bytes)   
ardour {
	["type"]    = "EditorAction",
	name        = "Store Mixer SettingsWB1.1",
	author      = "Mixbus Team",
	description = [[
	Stores the current Mixer state as a file
	that can be read and recalled arbitrarily
	by it's companion script, Recall Mixer Settings.

	Supports: processor settings, grouping,
	mute, solo, gain, trim, pan and processor ordering,
	plus re-adding certain deleted plugins.
	]]
}

function factory () return function ()

	local user_cfg = ARDOUR.user_config_directory(-1)
	local local_path = ARDOUR.LuaAPI.build_filename(Session:path(), 'mixer_settings')
	local global_path = ARDOUR.LuaAPI.build_filename(user_cfg, 'mixer_settings')

	function exists(file)
		local ok, err, code = os.rename(file, file)
		if not ok then
			if code == 13 then -- Permission denied, but it exists
				return true
			end
		end return ok, err
	end

	function whoami()
		if not pcall(function() local first_check = Session:get_mixbus(0) end) then
			return "Ardour"
		else
			local second_check = Session:get_mixbus(11)
			if second_check:isnil() then
				return "Mixbus"
			else
				return "32C"
			end
		end
	end

	function isdir(path)
		return exists("\"" .. path .. "\"" .. "/")
	end

	function setup_paths()
		local global_ok, local_ok = false, false

		if not(isdir(global_path)) then
			global_ok, _, _ = os.execute('mkdir '.. "\"" .. global_path .. "\"")
			if global_ok == 0 then
				global_ok = true
			end
		else
			global_ok = true
		end
		if not(isdir(local_path)) then
			local_ok, _, _ = os.execute('mkdir '.. "\"" .. local_path .. "\"")
			if local_ok == 0 then
				local_ok = true
			end
		else
			local_ok = true
		end
		return global_ok, local_ok
	end

	function get_processor_by_name(track, name)
		local i = 0
		local proc = track:nth_processor(i)
			repeat
				if(proc:display_name() == name) then
					return proc
				else
					i = i + 1
				end
				proc = track:nth_processor(i)
			until proc:isnil()
		end

	function group_by_id(id)
		local id  = tonumber(id)
		for g in Session:route_groups():iter() do
			local group_id = tonumber(g:to_stateful():id():to_s())
			if group_id == id then return g end
		end
	end

	function group_by_name(name)
		for g in Session:route_groups():iter() do
			if g:name() == name then return g end
		end
	end

	function route_groupid_interrogate(t)
		local group = false
		for g in Session:route_groups():iter() do
			for r in g:route_list():iter() do
				if r:name() == t:name() then group = g:to_stateful():id():to_s() end
			end
		end return group
	end

	function route_group_interrogate(t)
		for g in Session:route_groups():iter() do
			for r in g:route_list():iter() do
				if r:name() == t:name() then return g end
			end
		end
	end

	function empty_last_store(path)  --empty current file from last run
		local file = io.open(path, "w")
		--file:write(string.format("instance = { whoami = '%s' }", whoami())
		file:write("")
		file:close()
	end

	function mark_tracks(selected, path)

		empty_last_store(path)

		local route_string = [[instance = {
			route_id = %d,
			route_name = '%s',
			gain_control = %s,
			trim_control = %s,
			pan_control = %s,
			sends = {%s},
			muted = %s,
			soloed = %s,
			order = {%s},
			cache = {%s},
			group = %s,
			group_name = '%s',
			pi_order = %d
		}]]

		local group_string = [[instance = {
			group_id = %s,
			name = '%s',
			routes = {%s},
		}]]

		local processor_string = [[instance = {
			plugin_id = %d,
			type = %d,
			display_name = '%s',
			owned_by_route_name = '%s',
			owned_by_route_id = %d,
			parameters = {%s},
			active = %s,
		}]]

		local group_route_string = " [%d] = %s,"
		local proc_order_string  = " [%d] = %d,"
		local proc_cache_string  = " [%d] = {'%s', %d},"
		local params_string      = " [%d] = %s,"

		--ensure easy-to-read formatting doesn't make it through
		local route_string     = string.gsub(route_string, "[\n\t%s]", "")
		local group_string     = string.gsub(group_string, "[\n\t%s]", "")
		local processor_string = string.gsub(processor_string, "[\n\t%s]", "")

		local sel = Editor:get_selection ()
		local groups_to_write = {}
		local i = 0

		local tracks = Session:get_stripables()

		if selected then tracks = sel.tracks:routelist() end

		for r in tracks:iter() do
			local group = route_group_interrogate(r)
			if group then
				local already_there = false
				for _, v in pairs(groups_to_write) do
					if group == v then
						already_there = true
					end
				end
				if not(already_there) then
					groups_to_write[#groups_to_write + 1] = group
				end
			end
		end

		for _, g in pairs(groups_to_write) do
			local tmp_str = ""
			for t in g:route_list():iter() do
				tmp_str = tmp_str .. string.format(group_route_string, i, t:to_stateful():id():to_s())
				i = i + 1
			end
			local group_str = string.format(
				group_string,
				g:to_stateful():id():to_s(),
				g:name(),
				tmp_str
			)

			file = io.open(path, "a")
			file:write(group_str, "\r\n")
			file:close()
		end

		for r in tracks:iter() do
			if r:is_monitor () or r:is_auditioner () or not(r:to_vca():isnil()) then goto nextroute end -- skip special routes
			r = r:to_route()
			if r:isnil() then goto nextroute end
			local order = ARDOUR.ProcessorList()
			local x = 0
			repeat
				local proc = r:nth_processor(x)
				if not proc:isnil() then
					order:push_back(proc)
				end
				x = x + 1
			until proc:isnil()


			local route_group = route_group_interrogate(r)
			if route_group then route_group = route_group:name() else route_group = "" end
			local rid = r:to_stateful():id():to_s()
			local pan = r:pan_azimuth_control()
			if pan:isnil() then pan = false else pan = pan:get_value() end --sometimes a route doesn't have pan, like the master.

			-- Get send information, if any.
			local send_string = ""
			local i = 0
			repeat
				local fmt = "{%s, %s, %s, %s}"
				string.gsub(fmt, "[\n\t]", "")
				local values = {}
				for j, ctrl in pairs({
					r:send_level_controllable(i),
					r:send_enable_controllable(i),
					r:send_pan_azimuth_controllable(i),
					r:send_pan_azimuth_enable_controllable(i),
				}) do
					if not(ctrl:isnil()) then
						values[#values + 1] = ctrl:get_value()
					else
						values[#values + 1] = "nil"
					end
				end
				send_string = send_string .. string.format(fmt, table.unpack(values))
				send_string = send_string .. ","
				i = i + 1
			until r:send_enable_controllable(i):isnil()

			print(send_string)

			local order_nmbr = 0
			local tmp_order_str, tmp_cache_str = "", ""
			for p in order:iter() do
				local ptype
				if not(p:to_insert():isnil()) then
					ptype = p:to_insert():plugin(0):get_info().type
				else
					ptype = 99
				end
				local pid = p:to_stateful():id():to_s()
				if not(string.find(p:display_name(), "latcomp")) then
					tmp_order_str = tmp_order_str .. string.format(proc_order_string, order_nmbr, pid)
					tmp_cache_str = tmp_cache_str .. string.format(proc_cache_string, pid, p:display_name(), ptype)
				end
				order_nmbr = order_nmbr + 1
			end

			local route_str = string.format(
					route_string,
					rid,
					r:name(),
					ARDOUR.LuaAPI.ascii_dtostr(r:gain_control():get_value()),
					ARDOUR.LuaAPI.ascii_dtostr(r:trim_control():get_value()),
					tostring(ARDOUR.LuaAPI.ascii_dtostr(pan)),
					send_string,
					r:muted(),
					r:soloed(),
					tmp_order_str,
					tmp_cache_str,
					route_groupid_interrogate(r),
					route_group,
					r:presentation_info_ptr():order()
				)

			file = io.open(path, "a")
			file:write(route_str, "\n")
			file:close()

			local i = 0
			while true do
				local params = {}
				local proc = r:nth_plugin (i)
				if proc:isnil () then break end
				local active = proc:active()
				local id = proc:to_stateful():id():to_s()
				local plug = proc:to_insert ():plugin (0)
				local ptype = proc:to_insert():plugin(0):get_info().type

				local n = 0
				for j = 0, plug:parameter_count() - 1 do -- Iterate over all plugin 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
							local _, _, pd = ARDOUR.LuaAPI.plugin_automation(proc, n)
							local val = ARDOUR.LuaAPI.get_processor_param(proc, n, true)

							-- Clamp values at plugin max and min
							if val < pd.lower then
								val = pd.lower
							end

							if val > pd.upper then
								val = pd.upper
							end

							print(r:name(), "->", proc:display_name(), "(#".. n ..")",  label, val)
							params[n] = val
						end
						n = n + 1
					end
				end
				i = i + 1

				local tmp_params_str = ""
				for k, v in pairs(params) do
					tmp_params_str = tmp_params_str .. string.format(params_string, k, ARDOUR.LuaAPI.ascii_dtostr(v))
				end

				local proc_str = string.format(
						processor_string,
						id,
						ptype,
						proc:display_name(),
						r:name(),
						r:to_stateful():id():to_s(),
						tmp_params_str,
						active
					)
				file = io.open(path, "a")
				file:write(proc_str, "\n")
				file:close()
			end
			::nextroute::
		end
	end

	local store_options = {
		{ type = "label",    col=0, colspan=1, align="right", title = "Name:" },
		{ type = "entry",    col=1, colspan=1, align="left" , key = "filename", default = Session:name(), title=""},
		{ type = "label",    col=0, colspan=1, align="right", title = "Location:" },
		{
			type = "radio",  col=1, colspan=3, align="left", key = "store-dir", title = "", values =
			{
				['Global (accessible from any session)'] = 1, ['Local (this session only)'] = 2
			},
			default = 'Locally (this session only)'
		},
		{ type = "hseparator", title="", col=0, colspan = 3},
		{ type = "label",    col=0, colspan=1, align="right", title = "Selected Tracks Only:" },
		{ type = "checkbox", col=1, colspan=1, align="left",  key = "selected", default = false, title = ""},
		--{ type = "label", col=0, colspan=2, align="left", title = ''},
		--{ type = "label", col=0, colspan=2, align="left", title = "Global Path: " .. global_path},
		--{ type = "label", col=0, colspan=2, align="left", title = "Local Path: "  .. local_path},
	}

	local global_ok, local_ok = setup_paths()

	if global_ok and local_ok then
		local rv = LuaDialog.Dialog("Store Mixer Settings:", store_options):run()

		if not(rv) then return end

		local filename = rv['filename']
		if rv['store-dir'] == 1 then
			local store_path = ARDOUR.LuaAPI.build_filename(global_path, string.format("%s-%s.lua", filename, whoami()))
			local selected = rv['selected']
			mark_tracks(selected, store_path)
		end

		if rv['store-dir'] == 2 then
			local store_path = ARDOUR.LuaAPI.build_filename(local_path, string.format("%s-%s.lua", filename, whoami()))
			print(store_path)
			local selected = rv['selected']
			mark_tracks(selected, store_path)
		end
	end

end end
mixer_settings_store.lua (10,781 bytes)   
mixer_settings_recall.lua (14,745 bytes)   
ardour {
    ["type"]    = "EditorAction",
    name        = "Recall Mixer SettingsWB4.2",
    author      = "Mixbus Team",
    description = [[
    Recalls mixer settings outined by files
    created by Store Mixer Settings.

    Allows for some room to change Source
    and Destination.
    ]]
}

function factory ()

    local acoraida_monicas_last_used_recall_file

    return function ()

    local user_cfg = ARDOUR.user_config_directory(-1)
    local local_path = ARDOUR.LuaAPI.build_filename(Session:path(), 'mixer_settings')
    local global_path = ARDOUR.LuaAPI.build_filename(user_cfg, 'mixer_settings')
	--local debug = 1
    local invalidate = {}

    function exists(file)
	local ok, err, code = os.rename(file, file)
	if not ok then
	    if code == 13 then -- Permission denied, but it exists
		return true
	    end
	end return ok, err
    end

    function whoami()
	if not pcall(function() local first_check = Session:get_mixbus(0) end) then
	    return "Ardour"
	else
	    local second_check = Session:get_mixbus(11)
	    if second_check:isnil() then
		return "Mixbus"
	    else
		return "32C"
	    end
	end
    end

    function isdir(path)
	return exists(path.."/")
    end

    function get_processor_by_name(track, name)
	local i = 0
	local proc = track:nth_processor(i)
	repeat
	    if(proc:display_name() == name) then
		return proc
	    else
		i = i + 1
	    end
	    proc = track:nth_processor(i)
	until proc:isnil()
    end

    function new_plugin(name, type)
	local plugin = ARDOUR.LuaAPI.new_plugin(Session, name, type, "")
	if not(plugin:isnil()) then return plugin end
    end

    function group_by_id(id)
	local id  = tonumber(id)
	for g in Session:route_groups():iter() do
	    local group_id = tonumber(g:to_stateful():id():to_s())
	    if group_id == id then return g end
	end
    end

    function group_by_name(name)
	for g in Session:route_groups():iter() do
	    if g:name() == name then return g end
	end
    end

    function route_groupid_interrogate(t)
	local group = false
	for g in Session:route_groups():iter() do
	    for r in g:route_list():iter() do
		if r:name() == t:name() then group = g:to_stateful():id():to_s() end
	    end
	end return group
    end

    function route_group_interrogate(t)
	for g in Session:route_groups():iter() do
	    for r in g:route_list():iter() do
		if r:name() == t:name() then return g end
	    end
	end
    end

    function recall(debug, path, dry_run)
	local file = io.open(path, "r")
	assert(file, "File not found!")
	local bypass_routes = {}

	local i = 0
	for l in file:lines() do
	    --print(i, l)

	    local create_groups = dry_run["create_groups"]
	    local skip_line = false

	    local plugin, route, group = false, false, false
	    local f = load(l)

	    if debug then
		--print('create_groups ' .. tostring(create_groups))
		--print(i, string.sub(l, 0, 29), f)
	    end
	    if f then f() end

	    if instance["route_id"]  then route = true end
	    if instance["plugin_id"] then plugin = true end
	    if instance["group_id"]  then group = true end
	    if group then
		local g_id   = instance["group_id"]
		local routes = instance["routes"]
		local name   = instance["name"]
		local mygroup  = group_by_id(g_id)
		if not(mygroup) then
		    if create_groups then
			local newgroup = Session:new_route_group(name)
			for _, v in pairs(routes) do
			    local rt = Session:route_by_id(PBD.ID(v))
			    if rt:isnil() then rt = Session:route_by_name(name) end
			    if not(rt:isnil()) then 
				newgroup:add(rt) 
			    end
			end
		    end
		end
	    end

	    if route then
		local substitution = tonumber(dry_run["destination-"..i])
		if substitution == 0 then
		    bypass_routes[#bypass_routes + 1] = instance["route_id"]
		    goto nextline
		end
		local old_order = ARDOUR.ProcessorList()
		local route_id = instance["route_id"]
		local r_id = PBD.ID(instance["route_id"])
		local muted, soloed = instance["muted"], instance["soloed"]
		local order = instance["order"]
		local cache = instance["cache"]
		local group = instance["group"]
		local group_name = instance["group_name"]
		local name  = instance["route_name"]
		local gc, tc, pc = instance["gain_control"], instance["trim_control"], instance["pan_control"]
		local sends = instance["sends"]
		if not(substitution == instance["route_id"]) then
		    print('SUBSTITUTION FOR: ', name, substitution, Session:route_by_id(PBD.ID(substitution)):name())
		    --bypass_routes[#bypass_routes + 1] = route_id
		    was_subbed = true
		    r_id = PBD.ID(substitution)
		end

		local rt = Session:route_by_id(r_id)
		if rt:isnil() then rt = Session:route_by_name(name) end
		if rt:isnil() then goto nextline end

		if sends then
		    for i, data in pairs(sends) do
			i = i-1
			for j, ctrl in pairs({
			    rt:send_level_controllable(i),
			    rt:send_enable_controllable(i),
			    rt:send_pan_azimuth_controllable(i),
			    rt:send_pan_azimuth_enable_controllable(i),
			}) do
			    if not(ctrl:isnil()) then
				local value = data[j]
				if value then
				    if debug then
					print("Setting " .. ctrl:name() .. " to value " .. value)
				    end
				    ctrl:set_value(value, PBD.GroupControlDisposition.NoGroup)
				end
			    end
			end
		    end
		end

		local cur_group_id = route_groupid_interrogate(rt)
		if not(group) and (cur_group_id) then
		    local g = group_by_id(cur_group_id)
		    if g then g:remove(rt) end
		end

		local rt_group = group_by_name(group_name)
		if rt_group then rt_group:add(rt) end

		well_known = {
		    'PRE', 
		    'Trim', 
		    'EQ', 
		    'Comp', 
		    'Fader', 
		    'POST',
		    "Input Stage",
		    "Mixbus Limiter"
		}
		protected_instrument = false
		for k, v in pairs(order) do
		    local proc = Session:processor_by_id(PBD.ID(1))
		    if not(was_subbed) then
			proc = Session:processor_by_id(PBD.ID(v))
		    end
		    if proc:isnil() then
			for id, sub_tbl in pairs(cache) do
			    local name = sub_tbl[1]
			    local type = sub_tbl[2]
			    if v == id then
				proc = new_plugin(name, type)
				for _, control in pairs(well_known) do
				    if name == control then
					proc = get_processor_by_name(rt, control)
					if proc and not(proc:isnil()) then
					    invalidate[v] = proc:to_stateful():id():to_s()
					    goto nextproc
					end
				    end
				end
				if not(proc) then goto nextproc end
				if not(proc:isnil()) then
				    rt:add_processor_by_index(proc, 0, nil, true)
				    invalidate[v] = proc:to_stateful():id():to_s()
				end
			    end
			end
		    end
		    ::nextproc::
		    if proc and not(proc:isnil()) then 
				old_order:push_back(proc) 
			end
		    if not(old_order:empty()) and not(protected_instrument) then
			if not(rt:to_track():to_midi_track():isnil()) then
			    if not(rt:the_instrument():isnil()) then
				protected_instrument = true
				old_order:push_back(rt:the_instrument())
			    end
			end
		    end
		end
		rt:reorder_processors(old_order, nil)
		if muted  then rt:mute_control():set_value(1, 1) else rt:mute_control():set_value(0, 1) end
		if soloed then rt:solo_control():set_value(1, 1) else rt:solo_control():set_value(0, 1) end
		rt:gain_control():set_value(gc, 1)
		rt:trim_control():set_value(tc, 1)
		if pc ~= false and not(rt:is_master()) then rt:pan_azimuth_control():set_value(pc, 1) end
	    end

	    if plugin then
		--if the plugin is owned by a route
		--we decided not to use, skip it
		for _, v in pairs(bypass_routes) do
		    if instance["owned_by_route_id"] == v then
			goto nextline
		    end
		end

		local enable = {}
		local params = instance["parameters"]
		local p_id   = instance["plugin_id"]
		local act    = instance["active"]

		for k, v in pairs(invalidate) do --invalidate any deleted plugin's id
		    if p_id == k then
			p_id = v
		    end
		end

		local proc = Session:processor_by_id(PBD.ID(p_id))
		if proc:isnil() then goto nextline end
		local plug = proc:to_insert():plugin(0)

		local ctl = 0
		for j = 0, plug:parameter_count() - 1 do
		    if plug:parameter_is_control(j) then
			local label = plug:parameter_label(j)
			value = params[ctl]
			if value then
			    if string.find(label, "Assign") or string.find(label, "Enable") then --@ToDo: Check Plugin type == LADSPA or VST?
				enable[ctl] = value -- Queue enable assignment for later
				goto skip_param
			    end
			    if not(ARDOUR.LuaAPI.set_processor_param(proc, ctl, value)) then
				print("Could not set ctrl port " .. ctl .. " to " .. value)
			    end
			end
			::skip_param::
			ctl = ctl + 1
		    end
		end

		for k, v in pairs(enable) do
		    ARDOUR.LuaAPI.set_processor_param(proc, k, v)
		end

		if act then proc:activate() else proc:deactivate() end
	    end

	    ::nextline::
	    i = i + 1

	end
    end

    function dry_run(debug, path)
	--returns a dialog-able table of
	--everything we do (logically)
	--in the recall function
	local route_values = {['----'] = "0"}
	for r in Session:get_routes():iter() do
	    route_values[r:name()] =  r:to_stateful():id():to_s()
	end

	local i = 0
	local dry_table = {
	    {type = "label", align="right", key="col-1-title", col=0, colspan=1, title = 'Source:'},
	    {type = "label", align="left", key="col-2-title", col=1, colspan=1, title = 'Destination:'},
	}
	local file = io.open(path, "r")
	assert(file, "File not found!")
	pad = 0
	for l in file:lines() do
	    local do_plugin, do_route, do_group = false, false, false
	    local f = load(l)

	    if debug then
		print(i, string.sub(l, 0, 29), f)
	    end

	    if f then f() end

	    if instance["route_id"]  then do_route = true end
	    if instance["plugin_id"] then do_plugin = true end
	    if instance["group_id"]  then do_group = true end

	    if do_group then
		local group_id   = instance["group_id"]
		local group_name = instance["name"]
		local dlg_title, action_title  = "", ""

		local group_ptr  = group_by_id(group_id)

		if not(group_ptr) then
		    dlg_title = string.format("(Group) %s.", group_name)
		    --action_title = "will create and use settings"
		else
		    dlg_title = string.format("(Group) %s.", group_ptr:name())
		    --action_title = "will use group settings"
		end
		table.insert(dry_table, {
		    order=pad, type = "label", align="right", key =  "group-"..i , col = 0, colspan = 1, title = dlg_title
		})
		pad = pad + 1
	    end

	    if do_route then
		local route_id   = instance["route_id"]
		local route_name = instance["route_name"]
		local dlg_title = ""

		local route_ptr = Session:route_by_id(PBD.ID(route_id))

		if route_ptr:isnil() then
		    route_ptr = Session:route_by_name(route_name)
		    if not(route_ptr:isnil()) then
			dlg_title = string.format("%s", route_ptr:name())
			--action_title = "will use route settings"
		    else
			dlg_title = string.format("%s", route_name)
			--action_title = "will be ignored"
		    end
		else
		    dlg_title = string.format("%s", route_ptr:name())
		    --action_title = "will use route settings"
		end
		if route_ptr:isnil() then name = route_name else name = route_ptr:name() end

		table.insert(dry_table, {
		    order=instance['pi_order']+pad, type = "label",    align="right", key = "route-"..i , col = 0, colspan = 1, title = dlg_title
		})
		table.insert(dry_table, {
		    type = "dropdown", align="left", key = "destination-"..i, col = 1, colspan = 1, title = "", values = route_values, default = name or "----"
		})
	    end
	    i = i + 1
	end
	table.insert(dry_table, {
	    type = "checkbox", col=0, colspan=2, align="left",  key = "create_groups", default = true, title = "Create Groups if necessary?"
	})
	return dry_table
    end

    local global_vs_local_dlg = {
	{ type = "label", col=0, colspan=20, align="left", title = "" },
	{
	    type = "radio", col=0, colspan=20, align="left", key = "recall-dir", title = "", values =
	    {
		['Pick from Global Settings'] = 1, ['Pick from Local Settings'] = 2, ['Last Used Recall File'] = 3,
	    },
	    default = 'Pick from Local Settings'
	},
	{ type = "label", col=0, colspan=20, align="left", title = ""},
    }

    local recall_options = {
	{ type = "label", col=0, colspan=10, align="left", title = "" },
	{ type = "file",  col=0, colspan=15, align="left", key = "file", title = "Select a Settings File",  path = ARDOUR.LuaAPI.build_filename(Session:path(), "export", "params.lua") },
	{ type = "label", col=0, colspan=10, align="left", title = "" },
    }

    local gvld = LuaDialog.Dialog("Recall Mixer Settings:", global_vs_local_dlg):run()

    if not(gvld) then
	return
    else
	if gvld['recall-dir'] == 1 then
	    local global_ok = isdir(global_path)
	    local global_default_path = ARDOUR.LuaAPI.build_filename(global_path, string.format("FactoryDefault-%s.lua", whoami()))
	    print(global_default_path)
	    if global_ok then
		recall_options[2]['path'] = global_default_path
		local rv = LuaDialog.Dialog("Recall Mixer Settings:", recall_options):run()
		if not(rv) then return end
		local dry_return = LuaDialog.Dialog("Recall Mixer Settings:", dry_run(false, rv['file'])):run()
		if dry_return then
		    acoraida_monicas_last_used_recall_file = rv['file']
		    recall(true, rv['file'], dry_return)
		else
		    return
		end
	    else
		LuaDialog.Message ("Recall Mixer Settings:",
		    global_path .. ' does not exist!\nPlease run Store Mixer Settings first.',
		    LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run()
	    end
	end

	if gvld['recall-dir'] == 2 then
	    local local_ok = isdir(local_path)
	    local local_default_path = ARDOUR.LuaAPI.build_filename(local_path, 'asdf')
	    print(local_default_path)
	    if local_ok then
		recall_options[2]['path'] = local_default_path
		local rv = LuaDialog.Dialog("Recall Mixer Settings:", recall_options):run()
		if not(rv) then return end
		local dry_return = LuaDialog.Dialog("Recall Mixer Settings:", dry_run(false, rv['file'])):run()
		if dry_return then
		    acoraida_monicas_last_used_recall_file = rv['file']
		    recall(true, rv['file'], dry_return)
		else
		    return
		end
	    else
		LuaDialog.Message ("Recall Mixer Settings:",
		    local_path .. 'does not exist!\nPlease run Store Mixer Settings first.',
		    LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run()
	    end
	end

	if gvld['recall-dir'] == 3 then
	    if acoraida_monicas_last_used_recall_file then
		local dry_return = LuaDialog.Dialog("Recall Mixer Settings:", dry_run(false, acoraida_monicas_last_used_recall_file)):run()
		if dry_return then
		    recall(true, acoraida_monicas_last_used_recall_file, dry_return)
		else
		    return
		end
	    else
		LuaDialog.Message ("Script has no record of last used file:",
		    'Please pick a recall file and then this option will be available',
		    LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run()
	    end
	end
    end

end end
mixer_settings_recall.lua (14,745 bytes)   
Simple.ardour (148,509 bytes)

Issue History

Date Modified Username Field Change
2021-08-02 14:09 Werner Back New Issue
2021-08-03 10:53 Werner Back Note Added: 0026087
2021-08-04 11:26 Werner Back Note Added: 0026089
2021-08-05 06:21 Werner Back File Added: a_db2b41821.lua
2021-08-05 06:21 Werner Back Note Added: 0026090
2021-11-14 10:00 Werner Back File Added: mixer_settings_store.lua
2021-11-14 10:00 Werner Back File Added: mixer_settings_recall.lua
2021-11-14 10:00 Werner Back File Added: Simple-fb-duplicated.ardour
2021-11-14 10:00 Werner Back File Added: Simple-fb-triplicated.ardour
2021-11-14 10:00 Werner Back File Added: Simple.ardour
2021-11-14 10:00 Werner Back Note Added: 0026216