MIDI Track Freeze

I always missed a kind of “track freezing / unfreezing” for Midi Tracks in Ardour, to save CPU ressources in projects, containing a lot of MIDI Instrument Tracks. You can find similar functions in many DAWs already.
So I made my first script in Lua, which is supposed to do that task.
Basically the script carries out the following steps:

  • check if the track selection is valid for a “freeze” or an “unfreeze” action (is a single Midi Track selected or an Audio Track with a special prefix ('*** ')?)
  • stop the transport, if it’s running.
  • in case of a freeze operation: bounce the MIDI track to an audio region, create a new audio track with a special prefix and load the newly created region to that track. Then deactivate and hide the MIDI source track.
  • in case of an unfreeze operation: remove the Audio-Freeze-Track and unhide / re-activate the MIDI source track.

Since I’m lacking of scripting experience, I guess, the code is quite messy, so suggestion on how to enhance the script would be highly appreciated. :slight_smile:
In the current version of the script, you’ll need to confirm the removal of the audio track containing the bounced audio, whenever you run an “unfreeze” action, I hope to find a way, to fix that soon.
At this point I want to say “thank you” for the awesome support here in this forum! :slight_smile:
Here’s the code, feel free to give me feedback or to enhance the code depending on your needs:

ardour {
	    ["type"]    = "EditorAction",
	    name        = "MIDI Track Freeze",
        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, which 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 ()
        
            -- function to get the MIDI track name from the Freeze Track name
            function cropString(strg)
                local trimmedString = string.gsub(strg,"*** ","")
                return trimmedString
            end
            
            -- Function to find prefix ("*** ")
            function findPrefix(strg)
                local prefix = string.sub(strg,1,4)
                return prefix
            end

            -- Function for Info Messages
		    function dialog (text)
		    LuaDialog.Message ("Information", text, LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run ()
		    end 

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

            -- Set up variables
		    local selCount = Editor:get_selection().tracks:routelist():size() -- Number of selected routes (tracks)
		    local r = Session:route_by_selected_count(0) -- Get the (first) selected route (track)
		    local track = r:to_track()  -- Cast the selected route to a track
            local i = 0
            local current_index

            -- Check if selection is valid for "freezing" or "unfreezing""
		    if selCount < 1 then dialog("No track selected.\nPlease select a MIDI track to freeze or a valid Audio-Freeze-Track to unfreeze (Prefix '*** ').")
		        elseif selCount > 1 then dialog("Multiple tracks selected.\nPlease select a MIDI track to freeze or a valid Audio-Freeze-Track to unfreeze (Prefix '*** ').")              
            end

            if selCount == 1 and r:data_type():to_string() == "midi" then -- Selection is valid for freezing!!!
            
            -- Set up necessary variables to go on
				    local trackname = r : name() -- trackname
                    local audioTrackname = "*** "..trackname -- Audio trackname
                    local order = r:presentation_info_ptr():order() -- Get order number of MIDI track
				    dialog("You have selected MIDI track ’" .. trackname.."’ on lane # " .. order .." to freeze.") -- Output Info

				    -- Bounce full MIDI track to audio
				    track:bounce(ARDOUR.InterThreadInfo (),audioTrackname)

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

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

                    -- Create new audio track for bounced audio
                    local freezeTrack = Session:new_audio_track (2, 2, 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()

			        -- Iterate over them
			        for x,y in rl:iter() do  
            
                        -- Increase iteration counter variable
                        i = i + 1
  
                        -- Check regions for a specific name...
				        if y:name() == audioTrackname then
                            
                            -- If it's a match, increase index for the most current version of the frozen" audio region
                            current_index = i 

				        end

			        end

                    -- Reset iterator variable again
                    i = 0
        
                    -- Iterate over regions again...
                    for x,y in rl:iter() do  
            
                        -- Increase iteration counter variable
                        i = i + 1
  
                        -- If iteration counter variable matches the index for the most current version of the "frozen" audio region...
				        if i == current_index then
                            
                            -- ..add the corresponding region to the playlist...
                            playlist:add_region (y, Temporal.timepos_t(0), 1, false)
                            break -- ... and stop the loop

				        end

			        end
            end

            if selCount == 1 and r:data_type():to_string() == "audio" then  -- selection is valid for unfreezing
                local trackname = r : name() -- trackname
                local midiTrackname = cropString(trackname)
                local midiTrack = Session:route_by_name(midiTrackname)
	            if findPrefix(trackname) ~=  "*** " then dialog("This Audio track is not a valid Audio-Freeze-Track.\nPlease select an Audio-Freeze-Track with the prefix '*** ' to proceed with unfreezing")
                        elseif selCount == 1 and r:data_type():to_string() == "audio" and findPrefix(trackname) == "*** " then -- Selection is a valid track

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


        end
    end
3 Likes

Here is a new version of the code, which supports multi-out MIDI instruments (for example DrumGizmo) and autoconnects each output channel of the multi-channel audio track after “freezing” to the single group tracks.
Have fun! :slight_smile:

ardour {
	    ["type"]    = "EditorAction",
	    name        = "MIDI Track Freeze | Unfreeze 2",
        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 ()
        
            -- function to get the MIDI track name from the Freeze Track name
            function cropString(strg)
                local trimmedString = string.gsub(strg,"*** ","")
                return trimmedString
            end
            
            -- Function to find prefix ("*** ")
            function findPrefix(strg)
                local prefix = string.sub(strg,1,4)
                return prefix
            end

            -- Function for Info Messages
		    function dialog (text)
		    LuaDialog.Message ("Information", text, LuaDialog.MessageType.Info, LuaDialog.ButtonType.Close):run ()
		    end 

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

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

            -- Check if selection is valid for "freezing" or "unfreezing""
		    if selCount < 1 then dialog("No track selected.\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.\nPlease select a MIDI track to freeze or a valid Audio-Freeze-Track to unfreeze (Prefix '*** ').")           
                do return end 
            end

            local trackname = r : name() -- trackname
            local audioTrackname = "*** "..trackname -- Audio trackname
        
--[[*********** FREEZE ACTION ************]]--
            if selCount == 1 and r:data_type():to_string() == "midi" then -- Selection is valid for freezing!!!
            
                    -- Set up necessary variables to go on				    
                    local order = r:presentation_info_ptr():order() -- Get order number of MIDI track
                    local output_count = r:n_outputs():n_audio() -- Get number of audio output channels of MIDI Track to freeze
				    dialog("You have selected MIDI track ’" .. trackname.."’ on lane # " .. order .." to freeze.") -- Output Info
                    
                    -- Un-solo MIDI Track if soloed
                    if r:soloed() then Editor:access_action("Editor", "track-solo-toggle") 
                    end

                    -- Un-mute MIDI Track if muted
                    if r:muted() then Editor:access_action("Editor", "track-mute-toggle") 
                    end

				    -- Bounce full MIDI track to audio
				    track:bounce(ARDOUR.InterThreadInfo (),audioTrackname)

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

                    -- Hide MIDI track
				    local rtav = Editor:rtav_from_route(r)
				    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 a_route = Session:route_by_name(audioTrackname) 
                local a_track = a_route:to_track()
                local groups = Session:route_groups()
                local i = 0

                for x,y in groups:iter() do
	                if x:name() == trackname then		            
		                for a,b in x:route_list():iter() do
			                a_track:output():connect(a_track:output():nth(i), a:to_track():input():nth(0):name(), nil)
			                i = i+1
		                end
	                end
                end

            end

            -- Is "unfreezable"?
            if selCount == 1 and r:data_type():to_string() == "audio" then 
                local trackname = r : name() -- trackname
                local midiTrackname = cropString(trackname)
                local midiTrack = Session:route_by_name(midiTrackname)
	            if findPrefix(trackname) ~= "*** " then dialog("This Audio track is not a valid Audio-Freeze-Track.\nPlease select an Audio-Freeze-Track with the prefix '*** ' to proceed with unfreezing")
                do return end

--[[*********** UNFREEZE ACTION ************]]--
                elseif selCount == 1 and r:data_type():to_string() == "audio" and findPrefix(trackname) == "*** " then -- Selection is a valid track for unfreezing

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


        end
    end

EDIT: Sorry, there was an error in the code, but I removed it and now it should work!

3 Likes

@toxonic I know this doesn’t answer your question (and I don’t like it either when folks “assume away” my problems), but I’m still wondering…

Does a MIDI track - even with a MIDI synth plugin - really consume much CPU capacity? Isn’t that pretty light duty in terms of processing resources?

I was under the impression that while the audio bounce of that plugin - both the process of creating the audio and then playing it back - could be a real drain, the raw data is not. And that’s why freezing audio tracks can make a meaningful difference in performance.

So rather than try to freeze the MIDI, why not simply bounce to create audio and then freeze the audio?

But the raw data set of instructions (which is all that the MIDI and automation data represent) round to virtually nothing in terms of CPU demands given today’s processors.

Perhaps I’m simply mistaken about this assumption?

Hi ans thanks for your reply!
I think, i don’t get your point. What do you mean, when you say “bounce to audio and then freeze the audiio”?
So, yeah, MIDI data isn’t a CPU hog by itself, but for example a track with a softwaresynthesizer like SurgeXt or ZynFusion running through a couple of FX plugins may consume quite a bit of ressources though. If you imagine a large project with multiple tracks of that kind, it could make sense, to render a couple of these tracks to audio (not only the synth plugin itself, but also all it’s plugins) if you temporaryly dont need to edit them. Then you can deactivate the Softwaresynth tracks, so not each single MIDI note needs to be processed by the synrh and all the plugins in the track.
And if you need to edit some MIDI notes again, just re-activate the MIDI Track and delete the no more used audio track.
Thats, what that script does.

EDIT:
I have to confess, that this might not be a big deal for new high end computers. I recently bought a MSI Vector GP 66 laptop with a quite fast i7 CPU, which easily deals with a lot of MIDI Instruments and Synths without coming close to a critical DSP load.
But not each one has such fast machines and I also have a 12 years old PC with an old AMD A8-3870 quadcore CPU and I often had problems with high DSP loads due to a lot of plugins and softsynths in a project.
Just to demonstrate, here’s a simple Test project, which I tried on my old PC.
Only 4 MIDI Softsynth tracks with some simple plugins on some of them - but “freezing” each of the tracks with my script decreased the DSP load from 13% to 6%.

Normal:

With MIDI track freeze:

1 Like

@toxonic So sorry! Yeah, I’m a moron … my suggestion as to how to deal with the CPU demand is exactly what your LUA script sets out to accomplish (duh).

Not only are you aware of everything I offered up, but you have actually proceeded to automate it. Carry on! :+1:

1 Like

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