Hello,
For about a week (as a total novice) I have been slowly piecing-together and refining a kind of “clipboard” Lua script that will allow users to more-or-less ‘copy and paste’ selected regions from one session to another… → whilst critically preserving all region gain (scale_amplitude) and envelope data.
(–Credits and thanks to my starting point as provided by some standalone ‘export, import and embed’ scripts by @podcast a few years back, and a ‘create crossfades’ script by @laex.)
Anyway, I have hit a major, annoying issue that I wish to overcome, but don’t know how… In short, when you ‘paste’ (i.e. import or embed) regions onto a track using this script, for each region there is a new, unique entry generated into the Source list. This not only visually clutters the list, but if you choose import over embed, a new, entire source file is imported over and over again (i.e. once per region ‘pasted’), even if the regions share the same, original source! This can needlessly skyrocket SSD/HDD use, and is therefore unacceptable as an outcome/approach.
TL;DR: here are my main questions:
-
Is there a way to prevent Editor:do_import() or Editor:do_embed() from creating new Source entries when the source file has already been imported once into the session?
-
Is maybe there a way to programmatically check whether a specific source file (by path or name) is already part of the session’s source list, and then just reuse it? -ESP. when that source file was JUST imported or embedded and the overall Lua script is still executing?
-
Also, why do RegionFactory:regions() entries seem completely opaque on my Ardour 8.12 macOS build (I’m using Mojave)? -I ran tests to call methods like :name(), :source(0), and :to_audioregion() on each object returned from RegionFactory:regions(), and every single one failed…
Feel free to download and try the fully functional demo for yourself. Any feedback is welcome.
-- Thanks to "mbirgin" and "Alexander Lang" for providing the starting basis for this script's functionality.
ardour {
["type"] = "EditorAction",
name = "Ardour Clipboard (DEMO)",
license = "GPL",
author = "J.K.Lookinland & ChatGPT",
description = [[ This script allows you to 'copy and paste' selected audio regions between sessions. Region gain and envelope automation are preserved. Also, an optional 'crossfading' can be applied to overlapping regions during 'pasting' (i.e. importing or embedding). (2025-04-05)]]
}
function factory(params)
return function()
local dialog_options = {
{ type = "radio", key = "action", title = "Choose Action", values = {
["Copy Regions"] = "export",
["Paste (Import)"] = "import",
["Paste (Embed)"] = "embed"
}, default = "export" },
{ type = "dropdown", key = "fadeShape", title = "Select Crossfade Shape", values = {
["Option 1 - Linear (for highly correlated material)"] = "FadeLinear",
["Option 2 - Constant Power"] = "FadeConstantPower",
["Option 3 - Symmetric"] = "FadeSymmetric",
["Option 4 - Slow"] = "FadeSlow",
["Option 5 - Fast"] = "FadeFast",
["Option 6 - Don't Apply Crossfades"] = "None",
}, default = "Option 2 - Constant Power" },
{ type = "heading", title = "⚠ Crossfades are only applied during pasting." }
}
local od = LuaDialog.Dialog("Ardour Clipboard (DEMO) Options", dialog_options)
local rv = od:run()
if not rv then return end
local action = rv["action"]
local selected_fade_shape = rv["fadeShape"]
local FADE_SHAPE = nil
if selected_fade_shape ~= "None" then
FADE_SHAPE = ARDOUR.FadeShape[selected_fade_shape] or ARDOUR.FadeShape.FadeConstantPower
end
local tmpfile = "/tmp/ArdourClipboard.tsv"
if action == "export" then
local sel = Editor:get_selection()
local region_count = 0
if sel and sel.regions then
for _ in sel.regions:regionlist():iter() do
region_count = region_count + 1
end
end
if region_count == 0 then
LuaDialog.Message("Nothing to Copy!", "No audio regions are selected!\n\nPlease select one or more regions before copying.", LuaDialog.MessageType.Warning, LuaDialog.ButtonType.Close):run()
return
end
local file = io.open(tmpfile, "w")
for r in sel.regions:regionlist():iter() do
local path1 = r:source(0):to_filesource():path()
local path2 = (r:master_sources():size() > 1) and r:source(1):to_filesource():path() or ""
local ar = r:to_audioregion()
local scale_amplitude = (ar and not ar:isnil()) and ar:scale_amplitude() or 1.0
local envelope_data = ""
if ar and not ar:isnil() then
local al = ar:envelope()
if not al:isnil() then
for ev in al:events():iter() do
envelope_data = envelope_data .. string.format("%d:%.6f,", ev.when:samples(), ev.value)
end
envelope_data = envelope_data:gsub(",%$", "")
else
envelope_data = "No envelope"
end
end
file:write(string.format("%d\t%d\t%d\t%s\t%s\t%.6f\t%s\n",
r:start():samples(), r:position():samples(), r:length():samples(),
path1, path2, scale_amplitude, envelope_data))
end
file:close()
LuaDialog.Message("Copying Complete!", "Selected region data was copied to: " .. tmpfile .. "\n\nYou may now paste the regions elsewhere.", LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run()
return
end
local keyTable = {}
for line in io.lines(tmpfile) do
local start, pos, len, path1, path2, scale_amp, envelope =
line:match("(%d+)%s+(%d+)%s+(%d+)%s+([^\t]+)%s+([^\t]*)%s+([%d%.%-]+)%s+(.*)")
table.insert(keyTable, {
start = tonumber(start),
pos = tonumber(pos),
len = tonumber(len),
path1 = path1,
path2 = path2,
scale_amp = tonumber(scale_amp),
envelope = envelope
})
end
table.sort(keyTable, function(a, b) return a.pos > b.pos end)
local sel = Editor:get_selection()
local gtrack = nil
for route in sel.tracks:routelist():iter() do
gtrack = route:to_track()
break
end
if not gtrack then
LuaDialog.Message("Error!", "Please select a track for pasting!", LuaDialog.MessageType.Warning, LuaDialog.ButtonType.Close):run()
return
end
local pl = gtrack:playlist()
local region_cache = {}
local importedRegions = {}
for _, t in ipairs(keyTable) do
local rg = nil
local files = C.StringVector()
files:push_back(t.path1)
if t.path2 and #t.path2 > 3 then files:push_back(t.path2) end
local gpos = Temporal.timepos_t(t.pos)
local ret = nil
if action == "embed" then
if not region_cache[t.path1] then
region_cache[t.path1] = files
else
files = region_cache[t.path1]
end
ret = Editor:do_embed(files, Editing.ImportMergeFiles, Editing.ImportToTrack, gpos, ARDOUR.PluginInfo(), gtrack)
local ret_pos_end = ret[4]
rg = pl:top_region_at(Temporal.timepos_t(ret_pos_end:samples() - 1))
else
ret = Editor:do_import(files, Editing.ImportMergeFiles, Editing.ImportToTrack,
ARDOUR.SrcQuality.SrcBest, ARDOUR.MidiTrackNameSource.SMFTrackName,
ARDOUR.MidiTempoMapDisposition.SMFTempoIgnore, gpos, ARDOUR.PluginInfo(), gtrack)
local ret_pos_end = ret[7]
rg = pl:top_region_at(Temporal.timepos_t(ret_pos_end:samples() - 1))
end
if rg then
rg:set_position(Temporal.timepos_t(0), Temporal.timepos_t(0))
rg:trim_to(Temporal.timepos_t(t.start), Temporal.timecnt_t(t.len))
rg:set_position(Temporal.timepos_t(t.pos), Temporal.timepos_t(0))
local ar = rg:to_audioregion()
if ar and not ar:isnil() then
ar:set_scale_amplitude(t.scale_amp)
if t.envelope and t.envelope ~= "No envelope" then
local al = ar:envelope()
if not al:isnil() then
al:clear_list()
for sample, value in t.envelope:gmatch("(%d+):([%d%.%-]+)") do
al:add(Temporal.timepos_t(tonumber(sample)), tonumber(value), false, true)
end
al:thin(20)
end
end
end
table.insert(importedRegions, rg)
end
end
if selected_fade_shape ~= "None" then
-- Apply crossfades
local DO_FADE_IN = true
local DO_FADE_OUT = true
local BRING_SELECTION_TO_FRONT = false
local ADJUST_LOWER_REGIONS_FADES = true
local MINIMAL_FADE_LENGTH = 64
local add_undo = false
for _, r in ipairs(importedRegions) do
local ar = r:to_audioregion()
if ar:isnil() then goto nextImported end
local rStart = r:position()
local rEnd = rStart + r:length()
local rPL = r:playlist()
r:to_stateful():clear_changes()
if BRING_SELECTION_TO_FRONT then
Editor:access_action("Region", "raise-region-to-top")
end
local regionsTouched = rPL:regions_touched(rStart, rEnd)
for touched in regionsTouched:iter() do
if touched == r then goto nextTouched end
local touchedAR = touched:to_audioregion()
if touchedAR:isnil() then goto nextTouched end
touched:to_stateful():clear_changes()
local arIsOnTop = ar:layer() >= touchedAR:layer()
local touchedStart = touched:position()
local touchedEnd = touchedStart + touched:length()
-- Handle fade-out where overlapping end
if DO_FADE_OUT and touchedStart > rStart then
local fadeLen = (rEnd - touchedStart):samples()
if BRING_SELECTION_TO_FRONT or arIsOnTop then
ar:set_fade_out_shape(FADE_SHAPE)
ar:set_fade_out_length(fadeLen)
ar:set_fade_out_active(true)
if ADJUST_LOWER_REGIONS_FADES then
touchedAR:set_fade_in_length(MINIMAL_FADE_LENGTH)
end
else
touchedAR:set_fade_in_shape(FADE_SHAPE)
touchedAR:set_fade_in_length(fadeLen)
touchedAR:set_fade_in_active(true)
if ADJUST_LOWER_REGIONS_FADES then
ar:set_fade_out_length(MINIMAL_FADE_LENGTH)
end
end
end
-- Handle fade-in where overlapping start
if DO_FADE_IN and touchedEnd < rEnd then
local fadeLen = (touchedEnd - rStart):samples()
if BRING_SELECTION_TO_FRONT or arIsOnTop then
ar:set_fade_in_shape(FADE_SHAPE)
ar:set_fade_in_length(fadeLen)
ar:set_fade_in_active(true)
if ADJUST_LOWER_REGIONS_FADES then
touchedAR:set_fade_out_length(MINIMAL_FADE_LENGTH)
end
else
touchedAR:set_fade_out_shape(FADE_SHAPE)
touchedAR:set_fade_out_length(fadeLen)
touchedAR:set_fade_out_active(true)
if ADJUST_LOWER_REGIONS_FADES then
ar:set_fade_in_length(MINIMAL_FADE_LENGTH)
end
end
end
if not Session:add_stateful_diff_command(touched:to_statefuldestructible()):empty() then
add_undo = true
end
::nextTouched::
end
if not Session:add_stateful_diff_command(r:to_statefuldestructible()):empty() then
add_undo = true
end
::nextImported::
end
if add_undo then
Session:commit_reversible_command(nil)
else
Session:abort_reversible_command()
end
end
local action_past = {
export = "copied",
import = "pasted (imported)",
embed = "pasted (embedded)"
}
LuaDialog.Message(
"Pasting Complete!",
"All selected regions have been successfully " .. (action_past[action] or action) .. ".\n\nRegion gain, envelopes, and crossfades were applied if applicable.",
LuaDialog.MessageType.Info,
LuaDialog.ButtonType.Close
):run()
end
end
[ PS: the ways in which I’ve achieved this might be super hacky and inefficient, or superfluous. I sincerely applaud those who actually know how to code. Regardless, I learned a lot this week… ]
Thanks,
-J