Lua copy midi region whlie playing for "live looping"

Hi, I’m trying to make lua script that copies midi region several times after it, immediately after being recorded - yet another try to use Ardour for live looping. But I can’t get it to work cause the region is “written” to the playlist of a track only when I hit stop button. Is there a way to access that midi region without stopping playback or tell Ardour to write region to playlist?

This is my code so far:

ardour { ["type"] = "Snippet", name = "dup_midi_region" }

function factory() 
	return function()
		local route = Session:route_by_name("synth")
		local track = route:to_track()
		local playhead = Session:transport_frame()
		local loop = Session:locations():auto_loop_location()
		local proc = ARDOUR.LuaAPI.nil_proc() -- bounce w/o processing
		local itt = ARDOUR.InterThreadInfo() -- bounce progress info (unused)
		
		if loop then
			print("Loop set.")
		
			-- prepare undo operation
			Session:begin_reversible_command("dup_midi_region")
			local add_undo = false -- keep track if something has changed

			local loop_start = loop:start()
			local loop_end = loop:_end()
			local playlist = track:playlist()
			
			for r in playlist:region_list():iter() do
				print("region name: "..tostring(r:name()))
			end
		
			local loop_region = playlist:regions_touched(loop_start, loop_end)
			local region = track:bounce_range(loop_start, loop_end, itt, proc, false)
			
			print("region captured: "..tostring(region:captured()))

			if region then
				print("Region found. Copying.")
				playlist:add_region(region, playhead, 1, false, 0, 0, false)

			end
		
			-- create a diff of the performed work, add it to the session's undo stack
			-- and check if it is not empty
			if not Session:add_stateful_diff_command(playlist:to_statefuldestructible()):empty() then
				add_undo = true
			end
		
			-- all done, commit the combined Undo Operation
			if add_undo then
				-- the 'nil' Command here mean to use the collected diffs added above
				Session:commit_reversible_command (nil)
			else
				Session:abort_reversible_command ()
			end
		else
			print("Loop not set.")
		end
	end 
end

Btw I tried to do it by stopping and starting playback again in lua script using Editor:access_action(), but can’t figure out what action is for playback start (Roll didn’t work and PlaySelection and PlayPreroll aren’t suitable).

Thank you.

Alas, no.
The data is written to a temporary buffer. It can only be written to disk at a later time (disk i/o is not realtime-safe). Alignment also happens at rec-stop, and if you “stop and forget capture” it’s thrown away.
Besides the playlist cannot be modified click-free by while playing.

see http://manual.ardour.org/appendix/menu-actions-list/

Editor:access_action ("Transport", "Stop")
Editor:access_action ("Transport", "Roll") 
ARDOUR.LuaAPI.usleep (100000) -- wait for 100ms 

or you could directly call into backend:

Session:request_transport_speed (0.0, true)
Session:request_transport_speed (1.0, true)

Events will be queued for a later time, handled in realtime context.

I’ll have to think about this, but I don’t you can reasonably automate this from the GUI.

Best I can think of right now is subscribing to the TransportLooped or TransportStateChange signal, so the Lua script (type = “EditorHook”) is automatically invoked.

Thank you for the hints Robin. I got it working somehow - when I run it from Scripting window and playhead is at punch out position (see code below), it works - it copies 5 times region between punch in and out right after punch out position.

But when I save it as [“type”] = “session”, load it through Script Manager under Session Scripts, it doesn’t work - it seems as if it’s not even run, because I don’t see any regions created under Regions window (on right side of editor). Is this correct way to run it? I think I can’t use TransportLooped and TransportStateChange as you suggested, because I need it being run while rolling.

Current code:

ardour { ["type"] = "session", name = "dup_midi_region" }

function factory() 
        return function()
                local playhead = Session:transport_frame()
                print("playhead: "..tostring(playhead))
                local punch = Session:locations():auto_punch_location()

                if punch then
                        print("Punch set.")

                        local punch_in = punch:start()
                        local punch_out = punch:_end()
                        print("punch_in: "..tostring(punch_in))
                        print("punch_out: "..tostring(punch_out))

                        if playhead == punch_out then           
                                print("We are at punch out.")

                                Editor:access_action("Transport", "Stop")

                                local route = Session:route_by_name("synth")
                                local track = route:to_track()
                                local proc = ARDOUR.LuaAPI.nil_proc() -- bounce w/o processing
                                local itt = ARDOUR.InterThreadInfo() -- bounce progress info (unused)
                
                                -- prepare undo operation
                                Session:begin_reversible_command("dup_midi_region")
                                local add_undo = false -- keep track if something has changed
        
                                local playlist = track:playlist()
                        
                                local region = track:bounce_range(punch_in, punch_out, itt, proc, false)
                        
                                if region then
                                        print("Region found. Copying.")
                                        playlist:add_region(region, playhead, 5, false, 0, 0, false)
                                end
                
                                -- create a diff of the performed work, add it to the session's undo stack
                                -- and check if it is not empty
                                if not Session:add_stateful_diff_command(playlist:to_statefuldestructible()):empty() then
                                        add_undo = true
                                end
                
                                -- all done, commit the combined Undo Operation
                                if add_undo then
                                        -- the 'nil' Command here mean to use the collected diffs added above
                                        Session:commit_reversible_command(nil)
                                else
                                        Session:abort_reversible_command()
                                end

                                Editor:access_action("Transport", "Roll") 
                                ARDOUR.LuaAPI.usleep(100000) -- wait for 100ms 
                        end
                else
                        print("Punch not set.")
                end 
        end
end

hey, you information is very valuable. thank you very much. i’ve been looking for a way to do hands-free live looping with ardour, since I dont have the resouces to buy alk or ableton live.

i’m just new to ardour. I would like to know where do i have to paste that code in order to do a script in ardour.

Also, I would like to know what program language (or syntaxis) is used to write the Lua Scripts propperly. In order to make my owns.

We should be very, very clear. Ardour does not have a workflow like Ableton Live. If you’ve watched videos or other people using Live, you will not be able to use Ardour the way they use Live (assuming they were using Live the way most people do).

Ardour is currently primarily targetting workflows that involve recording people performing on instruments to a linear timeline, then editing and mixing the result.

People do use Ardour to do “in the box” composition, but it is not particularly well-designed for that right now, and certainly cannot do many of the things that Live can do in this area (Ardour has a few tricks of its own, but not in this sort of area of functionality).

That will change over time.

1 Like

ok, thank you very much mr. Paul fo answering me and for all of the effort you have put in this software.

I’ll keep that in mind. Blessings.

But just to answer your questions - the language of Lua scripts is Lua - it’s a simple scripting language commonly used for the purpose it’s used in Ardour (to write program extensions). Check this book: https://www.lua.org/pil/contents.html.

And here how to run lua scripts in ardour: https://manual.ardour.org/lua-scripting/

Please let me know if you get any success with it, i kinda postponed it for a while, since i couldn’t get it to work and now am using other program (seq64 - but it has it’s own gotchas).

I actually tried a couple of things along these lines. One attempt read in midi data from existing regions and these would be assigned to buttons on a Launchpad and also to midi channels and these would loop when triggered by the relevant button on the Launchpad. I posted a demo of this on here before. The Lua code got a bit out of hand though.

Another, simpler, attempt was to try and set something up to live loop instead. This script would record and loop midi. The idea was that the script would have two boolean parameters, “record” and “loop”, these parameters would be automated so that an automation lane would trigger a few bars of record and then this recorded loop would play back when “record” was off and “loop” was on. The script for this is here: script to record midi.

This script was inspired by method two that is discussed in this video: https://youtu.be/U6khXOMJPiY?t=375

Finally have the time to try your script - i had slight problems to install the script, but that’s solved (I couldn’t find “New Lua Proc” context menu and even the scripts subfolder in Ardours folder was missing - I’m using Ardour 6.5 - but just creating it and copying the script there was enough. After that it’s possible to find the script in plugins using Plugin Manager as any other plugin).

Now I can’t to get it work - as I wrote, I added the plugin via Plugin Manager to the midi track which I want to use for looping. Then I set up automation for loop and record parameters - drew the line (going from 0 to max and then back to 0 after few bars). I set the “automation state” to play (tried even touch or latch) and tried to loop something, but it didn’t work. So I tried to debug it and found out that the values of record and loop parameters in the script don’t change - they are still 0.0.

But I don’t know what to do now - did I miss something when setting it up, is that what I was supposed to do? Or what else can I do to debug it? Btw, thanks for the script - it seems it’s something I’m looking for.

Hey, that sounds quite strange, like the automation isn’t triggering the controls correctly. I went back to test this script on a recent build and noticed that, because of how the script has two midi outputs, the sound won’t be passed through for either the record or the loop depending on which midi output you wire into the instrument. I’ve just changed the code so that this isn’t the case any more. Perhaps it was this issue you were seeing? I recorded a quick demo of this script running in case you’re missing a step:

Something else odd that I noticed was that if you try to record the actual track it only seems to pick up the midi from the record section of the automation, I was able to get around this by routing all midi from the track to a separate track and moving the instrument to this new track, that way I could record all midi from both sections of automation.

Great, it works! Thanks for the quick reply and for the video - was helpful. My issue probably was that I didn’t put midi recorder before the synth. I updated the script and also put the recorder before synth and now it works, but probably the latter was the solution.

Btw, i didn’t find documentation to those midiin and midiout variables - are they some global variables that ardour provides? And do you think, would it also be possible to draw those captured notes to track - like when normally recording to a range? It would probably be good to have some visual feedback for live performances (but, maybe I could get used to not have it). I’m not asking you to implement it - I can try it myself, but since I don’t have experience with Ardour’s Lua API, I don’t know whether that’s even possible.

Thanks a lot again! :slight_smile:

Yea, I think you can do what you want. Here’s a quick demo video:

You set up the automation on one track and route it to another that you record on. You can then see the midi being recorded.

Thanks, that could maybe be usable, but my idea was that the midi region, as a whole, appears right at the moment, when the recorder stops recording and starts looping - sorry, my original description was bit confusing. So I guess, this will have to be done programmatically. I’m not sure, whether it even works like this in Ableton since the guy didn’t show the looping for midi, but regardless, it would be nice to have. Anyway, thanks for your help!

Ah okay, I can’t think of a neat way of doing that sorry. As for the midiin and midiout variables, yea they are variables set up by Ardour. The manual mentions a mididata variable, but the example scripts all use midiin and midiout.

No problem and thanks, I’m trying to figure it out.

What I hve is the code below, but it doesn’t work. In Ardour, I created a track, named it looper, created region, positioned it after record and loop automation graphs, pressed play, played something on synth whil being in recording mode, and when in looping mode, I can hear recorded notes, but they don’t show up in the empty region. Of course, I’m not sure, whether what I did is correct, especially the new_noteptr() part. So if someone could check the code, and tell whether I even am on the right way, I would really appreciate it.

Except adding the code to write the notes to region I also added code to play the notes even while not recording or looping.

ardour {
	["type"]    = "dsp",
	name        = "Midi Record",
	category    = "Midi", 
	license     = "Who knows",
	author      = "R8000",
	description = [[hello]]
}

function dsp_ioconfig ()
	return { {midi_in = 1, midi_out = 1, audio_in = -1, audio_out = -1}, }
end

function dsp_configure(ins, outs)
	n_out = outs
	n_out:set_midi(0)
end
local distanceFromRecordStart = -1
local distanceFromLoopStart = 0
local recordOnAtPreviousCheck = 0
local loopOnAtPreviousCheck = 0
local sizeOfLoop = 10000000000
local firstBar = 0
local tme = 0 -- sample-counter
local seq = 1 -- sequence-step
local spb = 0 -- samples per beat
local rateO = 0
local currentSample = 0
local currentOffset = 0
local beatsInABar = 4
local samplesPerBar = 0
--assuming 4 beats per bar



local midi_sequence1  =  { 
}
function dsp_params ()
 return
 {
	 {["type"] = "input", name = "Record", min = 0, max = 1, default = 0, enum = true, scalepoints  =
	 {
		 ["Off"] = 0,
		 ["On"] = 1
	 }
 	 },
	 {["type"] = "input", name = "Loop", min = 0, max = 1, default = 0, enum = true, scalepoints  =
	 {
		 ["Off"] = 0,
		 ["On"] = 1
	 }
 	 }
 }
end
function dsp_init (rate)

end

function string.starts(String,Start)
   return string.sub(String,1,string.len(Start))==Start
end
function dsp_dsp_midi_input ()
	return true
end


local copied = false
function dsp_run (_, _, n_samples)
	assert (type(midiin) == "table")
	assert (type(midiout) == "table")
         local ctrl = CtrlPorts:array()
	 local record = ctrl[1]
	 if(recordOnAtPreviousCheck <1 and record == 1) then
	   distanceFromRecordStart = 0	
	   recordOnAtPreviousCheck = 1
	   midi_sequence1 = {}
	 end
	 if(recordOnAtPreviousCheck == 1 and record < 1) then
		 sizeOfLoop = distanceFromRecordStart
         --print(sizeOfLoop)
	 end
	 recordOnAtPreviousCheck = record
	 local loop = ctrl[2]
	 if(loopOnAtPreviousCheck  < 1 and loop == 1) then
		 distanceFromLoopStart = 0
		 loopOnAtPreviousCheck = 1
         end
	 loopOnAtPreviousCheck = loop
	 
	local m = 1
    	for _,b in pairs(midiin) do
            local t = b["time"] -- t = [ 1 .. n_samples ]
    	    local d = b["data"] -- get midi-event

            local event_type
    	    if #d == 0 then event_type = -1 else event_type = d[1] >> 4 end

            midiout[m] = {}
    	    midiout[m]["time"] = t
            midiout[m]["data"] = d
    	    m = m + 1
        end
	if(record == 1) then
		local m = 1
        	for _,b in pairs (midiin) do
	            local t = b["time"]
        	    local d = b["data"]
	            local event_type
        	    if #d == 0 then event_type = -1 else event_type = d[1] >> 4 end

	            if (#d == 3 ) then -- note on
        	        local midiNoteAndTime = {time= t + distanceFromRecordStart, midi= d}
	                table.insert(midi_sequence1, midiNoteAndTime)
        	    end
	        end
	end
	if(loop == 1) then
		if not copied then
			local regions = Session:route_by_name("looper"):to_track():playlist():region_list()
			local region = regions:iter()():to_midiregion() -- first region
			local midi_model = region:model()
			local note_list = ARDOUR.LuaAPI.note_list(midi_model)
			local midi_command = midi_model:new_note_diff_command("copying midi notes")
			for i,b in pairs(midi_sequence1) do
				local new_note = ARDOUR.LuaAPI.new_noteptr(1, b["time"], 1,  b["midi"], 120)
				midi_command:add(new_note)
			end
			midi_model:apply_command(Session, midi_command)
			copied = true
		end
		
		if(distanceFromLoopStart > sizeOfLoop) then
			distanceFromLoopStart = 0
		end
		local m = 1
		for _, midiAndTime in pairs(midi_sequence1) do
			local offset = midiAndTime.time - distanceFromLoopStart 
			if(offset >=0 and offset < n_samples) then
				midiout[m] = {}
				midiout[m]["time"] = offset
				midiout[m]["data"] = midiAndTime.midi
				m = m + 1
			end
		end
	end
	distanceFromLoopStart = distanceFromLoopStart + n_samples
	distanceFromRecordStart = distanceFromRecordStart + n_samples
end

And also - is there a better way to “reload” the scripts than deleting the plugin and adding it again? Otherwise it doesn’t seem to get updated. And also - it seems the Error Log window doesn’t work - at least not in realtime - sometimes I can see some logs from the script there, but they aren’t written there while the plugin is running (or while Ardour is rolling), I think I have to restart Ardour, but sometimes even that doesn’t help.

Thank you.

Just to be more specific, this is the code I added to copy the notes to region:

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.