Heyo,
So I made this script the other day to help-out someone who had a couple of corrupted/problematic regions in their XML (.ardour) file, which prevented their snapshot from loading at all…
I used this script to simply eliminate them…
…and apparently their snapshot was able to open again!
It basically does the following:
- Scans the chosen .ardour file for <Source> entries.
- Presents them to the user.
- Recreates/overwrites the XML file with every original line except any <Source… or <Region… lines/blocks that referenced the selected source.
Here are some pictures of it:
So maybe this can help someone else later, too!
Download it here: XML Source Cleaner
-Or copy and paste from the code below…
~Peace,
-J
…
[PS: If anyone sees any fatal flaws in the code, please let me know! O___O]
ardour {
["type"] = "EditorAction",
name = "XML Source Cleaner",
license = "GPL",
author = "J.K.Lookinland & ALSA",
description = "Erase a specific source and its regions from a selected .ardour file. (2025-5-22)"
}
function factory () return function ()
-- Step 1: Introduce text and file-picker...
local dialog_opts = {
{ type = "heading", title = "Eliminate unwanted/problematic sources and regions from an XML file." },
{ type = "label", title = " " }, -- Spacer
{ type = "label", title = "This script allows you to remove a specific <Source> entry from an .ardour" },
{ type = "label", title = "session file, along with all associated <Region> entries that reference it." },
{ type = "label", title = " " }, -- Spacer
{ type = "heading", title = "HOW TO USE:" },
{ type = "label", title = "1. Select the .ardour file you would like to alter." },
{ type = "heading", title = "(⚠ WARNING: Make sure it is NOT the one currently in use!)" },
{ type = "label", title = "2. Select the source entry you'd like to erase (via the presented dropdown-menu.)" },
{ type = "label", title = "3. Confirm your selection, and hit OK!" },
{ type = "label", title = " " }, -- Spacer
{ type = "heading", title = "⚠ WARNING:" },
{ type = "heading", title = "This operation is destructive. It is STRONGLY recommended you run this" },
{ type = "heading", title = "on a COPY of the .ardour file you wish to alter, and NOT the original." },
{ type = "label", title = " " }, -- Spacer
{ type = "file", key = "filepath", title = "Select the .ardour Session File to Clean",
path = ARDOUR.LuaAPI.build_filename(Session:path(), Session:name() .. ".ardour") }
}
local dlg = LuaDialog.Dialog("XML Source Cleaner", dialog_opts)
local res = dlg:run()
if not res then return end
local filepath = res["filepath"]
-- Step 2: Find <Source> entries...
local sources = {}
local source_id_map = {}
local lines = {}
local f = io.open(filepath, "r")
if not f then
LuaDialog.Message("Error", "Could not open file: " .. filepath, LuaDialog.MessageType.Error):run()
return
end
for line in f:lines() do
table.insert(lines, line)
local id = line:match('<Source.-id="(%d+)"')
local name = line:match('name="([^"]+)"')
if id and name then
local label = name .. " [ID " .. id .. "]"
table.insert(sources, label)
source_id_map[label] = id
end
end
f:close()
if #sources == 0 then
LuaDialog.Message(
"Error!",
"No <Source> entries were found in this XML file!",
LuaDialog.MessageType.Error,
LuaDialog.ButtonType.Close
):run()
return
end
-- Step 3: Present a dropdown menu with found sources...
local dropdown_map = {} -- key = label, value = ID
for label, id in pairs(source_id_map) do
dropdown_map[label] = id
end
local sel_dlg = LuaDialog.Dialog("Select A Found Source...", {
{ type = "dropdown", key = "src", title = "Found Source(s)", values = dropdown_map, default = next(dropdown_map) }
})
local sel_res = sel_dlg:run()
if not sel_res then return end
local selected_id = sel_res["src"]
if not selected_id then
LuaDialog.Message(
"Error!",
"Unable to resolve source ID...",
LuaDialog.MessageType.Error,
LuaDialog.ButtonType.Close
):run()
return
end
-- Step 4: Confirm before proceeding...
local selected_label = nil
for label, id in pairs(dropdown_map) do
if id == selected_id then
selected_label = label
break
end
end
local confirm_dialog = LuaDialog.Dialog("Confirm Deletion...", {
{ type = "label", title = "You have selected the following source:" },
{ type = "label", title = selected_label },
{ type = "label", title = " " }, -- Spacer
{ type = "heading", title = "Pressing OK will remove this source and ALL region blocks that reference it." },
{ type = "label", title = "Do you wish to continue?" },
{ type = "label", title = " " } -- Spacer
})
local confirmed = confirm_dialog:run()
if not confirmed then return end
-- Step 5: Rebuild the XML without the selected <Source> and its <Region>(s)...
local new_lines = {}
local deleted_regions = 0
local skip_region = false
local skip_source = false
for i, line in ipairs(lines) do
-- Handle self-closing Region with source-0="id"
if line:match('<Region.-source%-0="' .. selected_id .. '".-/>') then
deleted_regions = deleted_regions + 1 -- Just count it, then onto the next line...
-- Start of multi-line <Region> that uses the selected source
elseif line:match('<Region.-source%-0="' .. selected_id .. '"') and not line:match("/>") then
skip_region = true -- Will pivot to the next elseif for the next line...
deleted_regions = deleted_regions + 1 -- Count it, then onto the next line...
-- Skip everything until we hit </Region>
elseif skip_region then
if line:match("</Region>") then -- Only flip skip_region back to false when we encounter the end of the larger region block...
skip_region = false
end
-- Handle <Source> tag (self-closing or block)
elseif line:match('<Source [^>]-id="' .. selected_id .. '"') then
if line:match("/>") then
-- A single-line, 'self-closing' source; skip it...
else
skip_source = true -- If the <Source... block is longer than one line, this will pivot to the next elseif for the next line...
end
-- Skip inside multiline Source block
elseif skip_source then
if line:match("</Source>") then -- Only flip skip_source back to false when we encounter the end of the larger source block...
skip_source = false
end
-- If nothing prior matches, we then keep and preserve the line of the XML:
else
table.insert(new_lines, line)
end
end
-- Step 6: Overwrite the file with cleaned contents...
local out = io.open(filepath, "w")
if not out then
LuaDialog.Message("Error", "Failed to open file for writing: " .. filepath, LuaDialog.MessageType.Error, LuaDialog.ButtonType.Close):run()
return
end
for _, line in ipairs(new_lines) do
out:write(line .. "\n")
end
out:close()
-- Step 7: Final confirmation!
local test_string = string.format("--> Removed source %s and %d associated region(s)!", tostring(selected_id), deleted_regions)
LuaDialog.Message(
"Cleanup Complete!",
test_string,
LuaDialog.MessageType.Info,
LuaDialog.ButtonType.Close
):run()
end end