-- Reaper Script to add take management functions, like: -- - Take counter with unique number ("how often REC was pressed") -- - Write takenumber to name of recorded file (by changing tracknames) -- - Group recorded items after stop (all or just horizontally) -- - Lock to active take (in lanes) after record -- - Write clean takenames after record, for easy recognition. E.g. "T042 (Violin 1)" -- - Crossfade items after record (fill gaps resulting from overlapping takes) (needs SWS) -- Author: Ludwig Frühschütz -- Source: https://www.eleton-audio.de -- Git: https://files.eleton-audio.de/gitea/Ludwig/Reaper-Scripts.git -- License: GPL v3.0 -- Requires: Reaper 5 or 6 -- Requires: SWS Extensions for "Fill gaps" option (http://standingwaterstudios.com/) -- Basic gui stuff by forum user "spk77": https://forum.cockos.com/showthread.php?t=161557 -- Some adjustable settings: -- Initial values if no settings stored in project local groupItemsAfterRecord = true local groupItemsHAfterRecord = false local fillGapsAfterRecord = true local lockToActiveTakeAfterRecord = true local writeTakeNamesAfterRecord = true -- Nothing to adjust here anymore... local script_path local mouse local Menu local gui = {} -- contains some settings for the GUI local increaseTodo = false -- used to detect transition form "REC" to "STOP" and then inc. takenumber local quit = false local sws_present = false -- Send a message to the console function msg(m) reaper.ShowConsoleMsg(tostring(m) .. "\n") end -- Write Takenumber and Trackname to selected items active takenames function writeTakeNames() local k = 0 local thisItem = reaper.GetSelectedMediaItem(0, k) local takestring local name = ' ' -- try to get takenumber from rpp. If not present, leave at "-1" retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'nexttake') if retval > 0 then takestring = 'T' .. string.format('%03d', tonumber(val)) -- format string to prepend to takename, e.g. "T042" else takestring = 'T???' end -- run through all selected items while (thisItem) do local theTake = reaper.GetActiveTake(thisItem) local track = reaper.GetMediaItem_Track(thisItem) -- get trackname without takenumber name = select( 2, reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name, false) ) if name:sub(-5,-4) == '_T' then name = name:sub(1,-6) elseif name:sub(-4,-3) == '_T' then name = name:sub(1,-5) elseif name:sub(-3,-2) == '_T' then name = name:sub(1,-4) elseif name:sub(-2,-1) == '_T' then name = name:sub(1,-3) end reaper.GetSetMediaItemTakeInfo_String(theTake, 'P_NAME', takestring .. ' (' .. name .. ')', true) k = k+1 thisItem = reaper.GetSelectedMediaItem(0, k) end end -- Lock to active take in lanes (to not accidentally select one by clicking) function setLockToActiveTake() reaper.Main_OnCommand(41340, 0) -- cant get momentary state, so simply set it every time... end -- Get the highest group-ID in use function maxProjectGroupID() local floor = math.floor local all_item_count = reaper.CountMediaItems(0) local MaxGroupID = 0 for i = 0, all_item_count - 1 do local item = reaper.GetMediaItem(0, i) local item_group_id = floor(reaper.GetMediaItemInfo_Value(item, "I_GROUPID")) if item_group_id > MaxGroupID then MaxGroupID = item_group_id end end return MaxGroupID end -- Group all selected items function groupItems() local groupID = maxProjectGroupID() + 1 local k = 0 local thisItem = reaper.GetSelectedMediaItem(0, k) while (thisItem) do reaper.SetMediaItemInfo_Value(thisItem, "I_GROUPID", groupID) k = k+1 thisItem = reaper.GetSelectedMediaItem(0, k) end end -- Group selected items horizontally function groupItemsH() local init_sel_tracks = {} local groupID = maxProjectGroupID() + 1 local j = 0 local k = 0 -- store selected tracks before work... for i = 0, reaper.CountSelectedTracks(0)-1 do init_sel_tracks[i+1] = reaper.GetSelectedTrack(0, i) end local thisTrack = reaper.GetTrack(0, j) while (thisTrack) do local thisItem = reaper.GetTrackMediaItem(thisTrack, k) while (thisItem) do if reaper.IsMediaItemSelected(thisItem) then reaper.SetMediaItemInfo_Value(thisItem, "I_GROUPID", groupID) end k = k+1 thisItem = reaper.GetTrackMediaItem(thisTrack, k) end k = 0 j = j+1 thisTrack = reaper.GetTrack(0, j) groupID = maxProjectGroupID() + 1 end -- restore selected tracks after work reaper.Main_OnCommand(40297, 0) -- Unselect all tracks for _, track in ipairs(init_sel_tracks) do reaper.SetTrackSelected(track, true) end end function fillGapsBetweenItems() if not sws_present then return end local init_sel_tracks = {} local init_sel_items = {} local timesel_start local timesel_end -- store selected tracks and items before work... for i = 0, reaper.CountSelectedTracks(0)-1 do init_sel_tracks[i+1] = reaper.GetSelectedTrack(0, i) end for i = 0, reaper.CountSelectedMediaItems(0)-1 do init_sel_items[i+1] = reaper.GetSelectedMediaItem(0, i) end -- store time selection timesel_start, timesel_end = reaper.GetSet_LoopTimeRange(false, true, 0, 1, false) -- select track with selected items reaper.Main_OnCommand(40297, 0) -- Unselect all tracks local selected_items_count = reaper.CountSelectedMediaItems(0) for i = 0, selected_items_count - 1 do local item = reaper.GetSelectedMediaItem(0, i) local track = reaper.GetMediaItem_Track(item) reaper.SetTrackSelected(track, true) end -- Set time selection to selected items and extend it a little to the right and left, so it includes adjacent items reaper.Main_OnCommand( 40290, 0 ) -- set time selection to selected items reaper.Main_OnCommand( 40320, 0 ) -- nudge left edge of time selection left reaper.Main_OnCommand( 40323, 0 ) -- nudge right edge of time selection right -- Select adjacent items and fill gaps (and crossfade) reaper.Main_OnCommand( 40718, 0 ) -- select items on selected tracks and in time selection reaper.Main_OnCommand( reaper.NamedCommandLookup('_SWS_AWFILLGAPSQUICKXFADE'), 0 ) -- fill gaps between selected items and crossfade -- restore selected tracks and items after work reaper.Main_OnCommand(40297, 0) -- Unselect all tracks for _, track in ipairs(init_sel_tracks) do reaper.SetTrackSelected(track, true) end reaper.Main_OnCommand(40289, 0) -- Unselect all items for _, item in ipairs(init_sel_items) do reaper.SetMediaItemSelected(item, true) end reaper.GetSet_LoopTimeRange(true, true, timesel_start, timesel_end, false) end -- Increase the takenumber and write to tracknames function increaseTake() local i_track = 0 local track = reaper.GetTrack(0, i_track) local name = 'empty' local take = 1 local retval local val -- load takenumber from rpp retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'nexttake') if retval > 0 then take = tonumber(val) + 1 end -- variable exists? if not keep take=1 takestring = '_T' .. string.format('%03d', take) -- format string to append to tracks, e.g. "_T042" -- run through all tracks and append takenumber while(track) do name = select( 2, reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name, false) ) -- check if a takenumber already exists, even if without leading zeros if name:sub(-5,-4) == '_T' then name = name:sub(1,-6) elseif name:sub(-4,-3) == '_T' then name = name:sub(1,-5) elseif name:sub(-3,-2) == '_T' then name = name:sub(1,-4) elseif name:sub(-2,-1) == '_T' then name = name:sub(1,-3) end -- set new trackname and get next track reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name .. takestring, true) i_track = i_track + 1 track = reaper.GetTrack(0, i_track) end -- write new takenumber to rpp reaper.SetProjExtState(0, 'takenumber_window', 'nexttake', take) end -- decreases takenumber and write to tracknames function decreaseTake() local i_track = 0 local track = reaper.GetTrack(0, i_track) local name = 'empty' local take = 1 local retval local val -- load takenumber from rpp retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'nexttake') if retval > 0 then take = tonumber(val) - 1 end -- variable exists? if not keep take=1 takestring = '_T' .. string.format('%03d', take) -- format string to append to tracks, e.g. "_T042" -- run through all tracks and append takenumber while(track) do name = select( 2, reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name, false) ) -- check if a takenumber already exists, even without leading zeros if name:sub(-5,-4) == '_T' then name = name:sub(1,-6) elseif name:sub(-4,-3) == '_T' then name = name:sub(1,-5) elseif name:sub(-3,-2) == '_T' then name = name:sub(1,-4) elseif name:sub(-2,-1) == '_T' then name = name:sub(1,-3) end -- set new trackname and get next track reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name .. takestring, true) i_track = i_track + 1 track = reaper.GetTrack(0, i_track) end -- write new takenumber to rpp reaper.SetProjExtState(0, 'takenumber_window', 'nexttake', take) end -- Initialisation for takenumber and tracknames function initTracknames() local i_track = 0 local track = reaper.GetTrack(0, i_track) local name = 'empty' local take = 1 local retval local val -- try to get takenumber from rpp. If not present, set to "1" retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'nexttake') if retval > 0 then take = tonumber(val) else reaper.SetProjExtState(0, 'takenumber_window', 'nexttake', take) end takestring = '_T' .. string.format('%03d', take) -- format string to append to tracks, e.g. "_T042" -- run through all tracks and append takenumber while(track) do name = select( 2, reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name, false) ) -- check if a takenumber already exists, even without leading zeros if name:sub(-5,-4) == '_T' then name = name:sub(1,-6) elseif name:sub(-4,-3) == '_T' then name = name:sub(1,-5) elseif name:sub(-3,-2) == '_T' then name = name:sub(1,-4) elseif name:sub(-2,-1) == '_T' then name = name:sub(1,-3) end -- set new trackname and get next track reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name .. takestring, true) i_track = i_track + 1 track = reaper.GetTrack(0, i_track) end end -- Removes Takenumber from tracknames function cleanTracknames() local i_track = 0 local track = reaper.GetTrack(0, i_track) local name = 'empty' -- run through all tracks and remove takenumber while(track) do name = select( 2, reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name, false) ) -- check if a takenumber exists, even without leading zeros or without numbers if name:sub(-5,-4) == '_T' then name = name:sub(1,-6) elseif name:sub(-4,-3) == '_T' then name = name:sub(1,-5) elseif name:sub(-3,-2) == '_T' then name = name:sub(1,-4) elseif name:sub(-2,-1) == '_T' then name = name:sub(1,-3) end -- set new trackname and get next track reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name, true) i_track = i_track + 1 track = reaper.GetTrack(0, i_track) end end -- Read takenumber from name of first track and store it to rpp function getTakeFromTrackOne() local track = reaper.GetTrack(0, 0) local name = 'empty' name = select( 2, reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', name, false) ) -- reads takenumber, even without leading zeros if name:sub(-5,-4) == '_T' then reaper.SetProjExtState(0, 'takenumber_window', 'nexttake', tonumber(name:sub(-3,-1))) elseif name:sub(-4,-3) == '_T' then reaper.SetProjExtState(0, 'takenumber_window', 'nexttake', tonumber(name:sub(-2,-1))) elseif name:sub(-3,-2) == '_T' then reaper.SetProjExtState(0, 'takenumber_window', 'nexttake', tonumber(name:sub(-1,-1))) end end -- Store state to project variables function storeExtState() reaper.SetProjExtState(0, 'takenumber_window', 'dockState', gfx.dock(-1)) if groupItemsAfterRecord then reaper.SetProjExtState(0, 'takenumber_window', 'groupItems', '1') else reaper.SetProjExtState(0, 'takenumber_window', 'groupItems', '0') end if groupItemsHAfterRecord then reaper.SetProjExtState(0, 'takenumber_window', 'groupItemsH', '1') else reaper.SetProjExtState(0, 'takenumber_window', 'groupItemsH', '0') end if lockToActiveTakeAfterRecord then reaper.SetProjExtState(0, 'takenumber_window', 'lockToActiveTake', '1') else reaper.SetProjExtState(0, 'takenumber_window', 'lockToActiveTake', '0') end if fillGapsAfterRecord then reaper.SetProjExtState(0, 'takenumber_window', 'fillGaps', '1') else reaper.SetProjExtState(0, 'takenumber_window', 'fillGaps', '0') end if writeTakeNamesAfterRecord then reaper.SetProjExtState(0, 'takenumber_window', 'writeTakenames', '1') else reaper.SetProjExtState(0, 'takenumber_window', 'writeTakenames', '0') end end --------------------------- -- functions for graphics... --------------------------- -- Returns current script's path function get_script_path() local info = debug.getinfo(1,'S'); local script_path = info.source:match[[^@?(.*[\/])[^\/]-$]] return script_path end -- gui init stuff, including rightclick-menu function gui_init() -- Get "script path" script_path = get_script_path() --msg(script_path) -- Modify "package.path" package.path = package.path .. ";" .. script_path .. "?.lua" --msg(package.path) -- Import files ("classes", functions etc.)-- require "class" -- import "base class" mouse = require "mouse" Menu = require "menu class" -- Create "right click" menu -- -- Create a "Menu" instance rc_menu = Menu("rc_menu") -- Add menu items to "rc_menu" -- ">" at the start spawns a submenu, | at the end creates a spacer rc_menu:add_item({label = "Remove takenumber from tracknames"}) rc_menu:add_item({label = "Write takenumber to tracknames|"}) rc_menu:add_item({label = "Reset takenumber to one"}) rc_menu:add_item({label = "Get takenumber from first track"}) rc_menu:add_item({label = "Increase takenumber by one"}) rc_menu:add_item({label = "Decrease takenumber by one|"}) rc_menu:add_item({label = "Group all items of same take", toggleable = true, selected = groupItemsAfterRecord}) rc_menu:add_item({label = "Group items of same take horizontally|", toggleable = true, selected = groupItemsAfterRecordH}) rc_menu:add_item({label = "Crossfade after record (needs SWS)", toggleable = true, active = sws_present, selected = fillGapsAfterRecord and sws_present}) rc_menu:add_item({label = "Lock to active take after record", toggleable = true, selected = lockToActiveTakeAfterRecord}) rc_menu:add_item({label = "Write clean Takenames after record|", toggleable = true, selected = writeTakeNamesAfterRecord}) rc_menu:add_item({label = "Quit"}) -- Let's add a command to all created items: rc_menu.items[1].command = function() cleanTracknames() end rc_menu.items[2].command = function() initTracknames() end rc_menu.items[3].command = function() reaper.SetProjExtState(0, 'takenumber_window', 'nexttake', '1') initTracknames() end rc_menu.items[4].command = function() getTakeFromTrackOne() initTracknames() end rc_menu.items[5].command = function() increaseTake() end rc_menu.items[6].command = function() decreaseTake() end rc_menu.items[7].command = function() if rc_menu.items[7].selected then groupItemsAfterRecord = true groupItemsHAfterRecord = false rc_menu.items[8].selected = false else groupItemsAfterRecord = false end storeExtState() end rc_menu.items[8].command = function() if rc_menu.items[8].selected then groupItemsHAfterRecord = true groupItemsAfterRecord = false rc_menu.items[7].selected = false else groupItemsHAfterRecord = false end storeExtState() end rc_menu.items[9].command = function() if rc_menu.items[9].selected then fillGapsAfterRecord = true else fillGapsAfterRecord = false end storeExtState() end rc_menu.items[10].command = function() if rc_menu.items[10].selected then lockToActiveTakeAfterRecord = true else lockToActiveTakeAfterRecord = false end storeExtState() end rc_menu.items[11].command = function() if rc_menu.items[11].selected then writeTakeNamesAfterRecord = true else writeTakeNamesAfterRecord = false end storeExtState() end rc_menu.items[12].command = function() quit = true end end -- Draw GUI -- function drawGui() local take = 1 local retval local val local thisnext = 'Next' -- try to get takenumber from rpp. If not present, set to "1" retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'nexttake') if retval > 0 then take = tonumber(val) else reaper.SetProjExtState(0, 'takenumber_window', 'nexttake', take) end -- Are we in "record" mode? if reaper.GetAllProjectPlayStates(0) == 5 then increaseTodo = true -- after rec-stop we have to increase the takenumber thisnext = 'This' -- during record the label says "This Take: 42" gfx.clear = 255 + 20*256 + 20*65536 -- background is red else -- no, we're not if increaseTodo then -- we just stopped from record if writeTakeNamesAfterRecord then --so we do all the magic, here eventually we write nice takenames writeTakeNames() end increaseTake() -- and have to increase the takenumber if groupItemsAfterRecord then -- and do post-record-stuff groupItems() elseif groupItemsHAfterRecord then groupItemsH() end if lockToActiveTakeAfterRecord then setLockToActiveTake() end if fillGapsAfterRecord then fillGapsBetweenItems() end thisnext = 'Next' -- during "not-record" the label says "Next Take: 43" increaseTodo = false -- and we're done and reset this flag end gfx.clear = 3355443 -- background is dark grey end -- If window is docked, use different scaling if gfx.dock(-1) > 0 then -- landscape format if gfx.w > gfx.h then gfx.x = 20 gfx.y = ( gfx.h-select(2, gfx.measurestr("Take")) ) / 2 gui.settings.font_size = gfx.h / 2 gfx.setfont(1,"Arial", gui.settings.font_size) -- check if string fits into window, if not decrease fontsize a little, check again and loop while gfx.measurestr(thisnext .. " Take: " .. take) > (gfx.w - 2*gfx.x) do gui.settings.font_size = gui.settings.font_size / 1.1 gfx.setfont(1,"Arial", gui.settings.font_size) end gfx.printf(thisnext .. " Take: " .. take) -- portrait format else gfx.x = gfx.w/15 gfx.y = ( gfx.h-select(2, gfx.measurestr("Next\nTake\nT123")) ) / 2 gui.settings.font_size = gfx.w / 3 gfx.setfont(1,"Arial", gui.settings.font_size) gfx.printf(thisnext .. "\nTake:\n" .. take) end else -- window is not docked -- landscape format if gfx.w > gfx.h then gfx.x = gfx.w/20 gfx.y = ( gfx.h-select(2, gfx.measurestr("Take")) ) / 2 gui.settings.font_size = gfx.w / 6 gfx.setfont(1,"Arial", gui.settings.font_size) -- check if string fits into window, if not decrease fontsize a little, check again and loop while gfx.measurestr(thisnext .. " Take: " .. take) > (gfx.w - 2*gfx.x) do gui.settings.font_size = gui.settings.font_size / 1.1 gfx.setfont(1,"Arial", gui.settings.font_size) end gfx.printf(thisnext .. " Take: " .. take) -- portrait format else gfx.x = gfx.w/20 gfx.y = ( gfx.h-select(2, gfx.measurestr("Next\nTake\nT123")) ) / 2 gui.settings.font_size = gfx.w / 3 gfx.setfont(1,"Arial", gui.settings.font_size) gfx.printf(thisnext .. "\nTake:\n" .. take) end end end -- Called on script termination. Store stuff... function onExit() storeExtState() end -- Main init function function init() -- check for SWS extensions if reaper.NamedCommandLookup('_SWS_ABOUT') > 0 then sws_present = true end -- load settings from rpp local retval local val retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'groupItems') if retval > 0 then if tonumber(val) > 0 then groupItemsAfterRecord = true else groupItemsAfterRecord = false end end retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'groupItemsH') if retval > 0 then if tonumber(val) > 0 then groupItemsAfterRecordH = true else groupItemsAfterRecordH = false end end retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'lockToActiveTake') if retval > 0 then if tonumber(val) > 0 then lockToActiveTakeAfterRecord = true else lockToActiveTakeAfterRecord = false end end retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'fillGaps') if retval > 0 then if tonumber(val) > 0 then fillGapsAfterRecord = true else fillGapsAfterRecord = false end end retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'writeTakenames') if retval > 0 then if tonumber(val) > 0 then writeTakeNamesAfterRecord = true else writeTakeNamesAfterRecord = false end end -- init stuff... gui_init() initTracknames() -- Add stuff to "gui" table gui.settings = {} -- Add "settings" table to "gui" table gui.settings.font_size = 50 -- font size gui.settings.docker_id = 0 -- try 0, 1, 257, 513, 1027 etc. -- Initialize gfx window -- gfx.init("Rec Take", 350, 100, gui.settings.docker_id) gfx.setfont(1,"Arial", gui.settings.font_size) gfx.clear = 3355443 -- background is dark grey -- Restore docked state retval, val = reaper.GetProjExtState(0, 'takenumber_window', 'dockState') if retval > 0 then gfx.dock(val) end end -- Main loop -- function mainloop() -- mouseclicks? local RMB_state = mouse.cap(mouse.RB) local LMB_state = mouse.cap(mouse.LB) local mx = gfx.mouse_x local my = gfx.mouse_y if not mouse.last_RMB_state and gfx.mouse_cap&2 == 2 then -- right click pressed down -> show "right click menu" at mouse cursor and focus arranger afterwards rc_menu:show(mx, my) reaper.Main_OnCommand( reaper.NamedCommandLookup('_BR_FOCUS_ARRANGE_WND'), 0 ) -- focus arranger end if not mouse.last_LMB_state and gfx.mouse_cap&1 == 1 then -- left click pressed down -> only focus arranger reaper.Main_OnCommand( reaper.NamedCommandLookup('_BR_FOCUS_ARRANGE_WND'), 0 ) -- focus arranger end mouse.last_RMB_state = RMB_state -- store current right mouse button state mouse.last_LMB_state = LMB_state -- store current left mouse button state drawGui() gfx.update() if gfx.getchar() >= 0 and not quit then reaper.defer(mainloop) end end -- START HERE... reaper.atexit(onExit) init() mainloop()