LUA Scripting Hurdle: How to prevent multiple source imports/embeds?

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:

  1. 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?

  2. 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?

  3. 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… :confounded: :question:


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 :woozy_face: