Lua: shared pointer is nil

I recently posted a Lua script which automates bouncing MIDI Instrument tracks to audio tracks (MIDI Track freeze), which - so far - worked fine for me, but I’m trying to enhance some points and make it more flexible. On my way, tried to encapsulate the two main processes (“freeze” and “unfreeze”) inside functions.
Now I get the following issue:
Inside the freeze() function, there’s a line, that creates a new audio track and a second one, that gets the playlist for that audio track to put in the bounced audio region.

-- Create new audio track for bounced audio
local freezeTrack = Session:new_audio_track (output_count, output_count, nil, 1, audioTrackname, order, ARDOUR.TrackMode.Normal, true)

-- Get playlist for new audio track
local playlist = freezeTrack:front():playlist()

This worked fine in my old script, but encapsuled inside a function I get an error message regarding shared pointer is nil whenever I …

  1. freeze a track
  2. unfreeze the same track again
  3. select the SAME track again, and.
  4. …try to freeze it once again

Here’s a gif, to demonstrate, what I mean:
shared pointer

Okay, I know, that this is not a usual process, but it should work though.
Any hints on, how to solve that?

PS:
Here’s the full script:

ardour {
	    ["type"]    = "EditorAction",
	    name        = "MIDI Track Freeze | Unfreeze 3 TEST",
        author      = "Toxonic",
        license     = "MIT",
        description = [[This script is supposed to provide a "Track Freeze" function for MIDI Tracks, as you can find in severeal well-known DAWs. The goal is, to save CPU ressources in projects with many MIDI instrument tracks, while they don't need to be edited. The contents of these tracks can be "frozen" (bounced) to an Audio track with a special Prefix ('*** ') In a next step, the 'source' MIDI Track gets deactivated and hidden in the track view. If you need to edit it again, you can 'unfreeze' the track again by selecting the Audio-Freeze-Track and run the script again. The Audio-Freeze-Track will then be removed and the MIDI Track will be re-activated and unhidden.]]
	}

	function factory (unused_params)
	    return function ()

            -- Set up variables
		    local selCount = Editor:get_selection().tracks:routelist():size() -- Number of selected routes
		    local get_route = Session:route_by_selected_count(0) -- Get the (first) selected route
		    local track = get_route:to_track()  -- Cast the selected route to a track
            local groups = Session:route_groups() -- Variable containing all route groups

-- INFO MESSAGES FUNCTION
		    function dialog (text)
		        LuaDialog.Message ("Information", text, LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run ()
		    end 

-- FREEZE FUNCTION
            function freeze ()  
                
                -- Set up necessary variables to go on				    
                local order = get_route:presentation_info_ptr():order() -- Get order number of MIDI track
                local output_count = get_route:n_outputs():n_audio() -- Get audio output channels # of MIDI Track to freeze      
                local audioTrackname = "*** ".. get_route:name() -- Audio trackname with "*** "-prefix 
                      
                -- Bounce full MIDI track to audio
			    track:bounce(ARDOUR.InterThreadInfo (),audioTrackname)

                -- Deactivate MIDI track
                get_route:set_active (false, nil)

                -- Hide MIDI track
			    local rtav = Editor:rtav_from_route(get_route)
			    Editor:hide_track_in_display (rtav:to_timeaxisview(), false)

                -- Create new audio track for bounced audio
                local freezeTrack = Session:new_audio_track (output_count, output_count, nil, 1, audioTrackname, order, ARDOUR.TrackMode.Normal, true)

                -- Get playlist for new audio track
                local playlist = freezeTrack:front():playlist()

		        -- Get regions of this session to import "frozen" audio region
                local rl = ARDOUR.RegionFactory.regions()
                local reg_at = 0
                local reg_id

			    -- Iterate over them
			    for id, region in rl:iter() do
  
                    -- Check regions for a specific name...
				    if region:name() == audioTrackname then
                            
                        -- ... if it's a match, save ID of the region until the most current version of the frozen" audio region
                        if math.max(reg_at, tonumber(id:to_s())) == tonumber(id:to_s()) then 
                            reg_at = tonumber(id:to_s())  
                            reg_id = id
                        end
				    end
			    end

                -- ..add the corresponding region to the playlist...
                playlist:add_region (rl:at(reg_id), Temporal.timepos_t(0), 1, false)          

                --[[ If a Multiout-Instrument-Track selected, connect all outputs of the new Freeze-Audio-Track to the inputs of the group-tracks
                local m_route = Session:route_by_name(audioTrackname) 
                local m_track = m_route:to_track()
                local groups = Session:route_groups()
                local i = 0

                for x,y in groups:iter() do
	                if x:name() == get_route:name() then	
                        print("Multitrack")	            
		                for a,b in x:route_list():iter() do
			                m_track:output():connect(m_track:output():nth(i), a:to_track():input():nth(0):name(), nil)
                            print(a:to_track():input():nth(0):name())
			                i = i+1
		                end
	                end
                end --]]

            end
-- END OF FREEZE FUNCTION

-- UNFREEZE FUNCTION
            function unfreeze()
                
                -- Set up necessary variables
                local midiTrackname = string.gsub(get_route:name(),"*** ","")
                local midiTrack = Session:route_by_name(midiTrackname)

                -- Remove the freeze-Audio track
                Editor:access_action("Editor", "remove-track")

                -- Activate the MIDI source track ... 
                midiTrack:set_active (true, nil)

                -- ...and make it visible again
                local rtav = Editor:rtav_from_route(midiTrack)
			    Editor:show_track_in_display (rtav:to_timeaxisview(), false)

            end
--END OF UNFREEZE FUNCTION

--[[*********** MAIN ************]]--

            -- Stop playback
		    if Session:transport_stopped() == false then Session:request_stop(false, false, 0)
		    end

            -- Check if selection is valid for "freezing" 
		    if selCount < 1 then dialog("No track selected.\n\nPlease select a MIDI track to freeze or a valid Audio-Freeze-Track to unfreeze (Prefix '*** ').")
                do return end
		    elseif selCount > 1 then 
                dialog("Multiple tracks selected.\n\nPlease select a MIDI track to freeze or a valid Audio-Freeze-Track to unfreeze (Prefix '*** ').")           
                do return end 
            -- Check if selection is valid for "unfreezing""
            elseif selCount == 1 and get_route:data_type():to_string() == "audio" then 
	            if string.sub(get_route:name(),1,4) ~= "*** " then 
                    dialog("This Audio track is not a valid Audio-Freeze-Track.\n\nPlease select an Audio-Freeze-Track with the prefix '*** ' to proceed with unfreezing")
                    do return end
                end
            end
                --[[*********** unfreeze action ************]]--
            if selCount == 1 and get_route:data_type():to_string() == "audio" and string.sub(get_route:name(),1,4) == "*** " then 
                    unfreeze() -- If selection is valid, call unfreeze() function  
                --[[*********** freeze action ************]]--
            elseif selCount == 1 and get_route:data_type():to_string() == "midi" then                               
				    freeze() -- If selection is valid, call freeze() function                             
            end
        end
    end

A guess:

  • unfreeze deletes a track – but a reference to that track remains (in some Lua state variable).
  • a later freeze tries to re-create that track (same name) and fails to do that because a track with that name still exists under the hood.

A few hints to try to track this down:

After unfreeze, can you check if ports of the deleted track still exist (Window > Audio Connections)?

Does it work if you restart Ardour after each operation? freeze; save/quit, restart; unfreeze, (save/quit, restart) freeze. If so that will help to determine which part retains the references.

I hazard a guess that it is the first freeze operation that keeps the reference.
Perhaps add an explicit collectgarbage() call at the end of the “freeze” function.

1 Like

Hey and thank you for yor reply! :slight_smile:
I had a look at the audio connections, but after unfreeze the audioconnections of the former audiotrack vanish immideately.
restarting the session works, i can freeze and unfreeze a track and after saving and restarting the session I can freeze the same track again with now issues.
collectgarbage() at the end of the freeze function didn’t change anything. :frowning:

I’m not familiar with Lua error reporting but the message you get says string ardour {…"]:45: which possibly could indicate line 45 in your code.

Looking at that, and what the video shows, it probably the “local freezeTrack = Session:new_audio_track” line that’s unable to create the track for some reason, which would mean that freezeTrack ends up being nil and causing an error in the subsequent “local playlist” line.

That’s just my uneducated guess, though.

From Paul’s “creating a new track with the same order number is far from trivial” reply in your previous post ( Accessing track order with Lua - #3 by toxonic ) it could be the “order” variable that’s the culprit.
You should print out the content of order, output_count and audioTrackname to see whether they contain expected values or not.

Yeah, I already did this, and to validate, i put the line

print("name: ".. audioTrackname,"output #: " .. output_count, "order: " .. order)

right after the variable definitions at the beginning of the freeze function, and another line

print("Freezetrack name: "..freezeTrack:front():name())

right after the creation of the new audio track (defined in the variable “freezeTrack”).
I also added a collectgarbage("collect") at the end of both functions, but this doesn’t change anything.
After the first time, I run the script on the track, i get the following lines (as expected):

name: *** ZynAddSubFX output #: 2 order: 4
Freezetrack name: *** ZynAddSubFX

After unfreezing the track and trying to freeze the SAME track again, I get:

name: *** ZynAddSubFX output #: 2 order: 4
LuaException: [string "ardour {..."]:46: shared_ptr is nil

where line 46 is where I print out the freezeTrack name (see above).
What is interesting:
Whenever there is a step between unfreezing and re-freezing the SAME track, it works fine again.
For example, when I haven’t any track selected after unfreezing (which leads to an information dialog, that there’s no track selected) and then select the track again to re-freeze it, it works as expected.
I also tried to “reset” the freezTrack variable by assigning it to a nil value at the beginning of the freeze function, but without success.
This is driving me nuts… Suggestions are highly appreciated! :slight_smile:

Upon getting that error the log is saying:
[ERROR]: unable to create port ‘*** General MIDI Synth/audio_in 1’: failed constructor

If that’s any help.

EDIT: Also, after that unfreeze qjackctl > Connections is saying that the “*** General MIDI Synth” ports are still there even though that track has been removed. Which is what Robin was alluding to I believe. Ardour: Window > Audio Connections doesn’t mention those ports though.

EDIT: Putting a collectgarbage() before the freeze()/unfreeze() calls seems to fix it.

            collectgarbage()

                --[[*********** unfreeze action ************]]--
            if selCount == 1 and get_route:data_type():to_string() == "audio" and string.sub(get_route:name(),1,4) == "*** " then 
                    unfreeze() -- If selection is valid, call unfreeze() function  
                --[[*********** freeze action ************]]--
            elseif selCount == 1 and get_route:data_type():to_string() == "midi" then                               
				    freeze() -- If selection is valid, call freeze() function                             
            end

I mean, it fixes the errors and I’m able to freeze/unfreeze multiple times. Those ports still remain after the unfreeze in qjackctl so I guess their presence isn’t a problem.

2 Likes

Yeah, that does the trick! Thank you for your support again! :slight_smile:
Although, I’d love to understand, why this works and why the ports of the removed track still persist…
However, awesome! Thank you again! :wink:

Lifetime of objects inside Ardour is managed by reference-counting.

A given object (here: a route) is used in many different contexts: Realtime thread has a reference (to process), Editor has a reference, the Mixer GUI has one, etc.

Deleting a track asks each context to drop the reference. The object is only destroyed once it is no longer used (when the reference count is zero). As long as some context still retains a reference, the object and all its members (here: I/O ports) still exist.

Now. Lua does lazy garbage collection. It can happen that a value that was assigned to a variable still exists at a later time. Allocated memory is not immediately released when it is no longer used. Memory is usually reused (this improves performance).

So, in your case the script still holds a shared-pointer to an ARDOUR::Route. The reference count of the object is not zero and hence it is not destroyed. The Route’s port also still exist, and trying to re-register that port fails.

Since your script does nothing else, no additional memory is required, and hence there is no automatic garbage collection. This is solved by an explicit collectgarbage() call.

2 Likes

Great man, thank you for the explanation! :slight_smile:

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