|
| 1 | +--[[ |
| 2 | +ReaScript name: Save track template with items selected or within time selection |
| 3 | +Author: BuyOne |
| 4 | +Website: https://forum.cockos.com/member.php?u=134058 |
| 5 | +Version: 1.0 |
| 6 | +Changelog: Initial release |
| 7 | +Licence: WTFPL |
| 8 | +REAPER: at least v5.962 |
| 9 | +About: The script allows saving track template with selected |
| 10 | + items or items within time selection provided option |
| 11 | + 'Include track items in template' |
| 12 | + is enabled in 'Save track template' dialogue. |
| 13 | + REAPER native action saves all items on selected tracks. |
| 14 | + Without the option enabled the script is useless. |
| 15 | +
|
| 16 | + Inspired by a feature request at |
| 17 | + https://forum.cockos.com/showthread.php?t=277300 |
| 18 | +
|
| 19 | + The script works in 2 stages, first it triggers |
| 20 | + the native action 'Track: Save tracks as track template...' |
| 21 | + to allow you to save track template as normal. |
| 22 | +
|
| 23 | + Once 'Save track template' dialogue is closed the script |
| 24 | + immediately triggers custom 'Select .RTrackTemplate file' |
| 25 | + dialogue inviting you to select the template just saved |
| 26 | + in order to process it. |
| 27 | + After the newly saved template is selected and button 'Open' |
| 28 | + is clicked in the dialogue the script completes its task |
| 29 | + leaving the track template only containing items you selected. |
| 30 | +
|
| 31 | + 'Select .RTrackTemplate file' dialogue points to the |
| 32 | + /TrackTemplates folder inside REAPER resource directory |
| 33 | + which is the default location for saving track templates. |
| 34 | + If you saved a template elsewhere, simply navigate to that |
| 35 | + location. |
| 36 | + The script is blind to the path of the newly saved template |
| 37 | + file, to the actual save operation and to the state of the |
| 38 | + option 'Include track items in template', therefore |
| 39 | + 'Select .RTrackTemplate file' dialogue will appear even if |
| 40 | + you closed native 'Save track template' dialogue without |
| 41 | + saving a template or didn't enable the option to save items. |
| 42 | +
|
| 43 | + If after saving a template you closed 'Select .RTrackTemplate file' |
| 44 | + dialogue without allowing the script to process the template |
| 45 | + file you'll be given an opportunity to keep the data until |
| 46 | + the next run. |
| 47 | +
|
| 48 | + Items whose start coincides with time selection end |
| 49 | + or whose end coincides with time selection start |
| 50 | + aren't considered to be included in time selection. |
| 51 | +
|
| 52 | + Since REAPER saves folder parent track templates with all |
| 53 | + their children, time selection bounds and selected state |
| 54 | + apply to items on their children tracks, to the parent |
| 55 | + track they apply in case it has any items, so if the parent |
| 56 | + track is selected the children tracks don't have to be |
| 57 | + explicitly selected. |
| 58 | + |
| 59 | +]] |
| 60 | + |
| 61 | + |
| 62 | +local r = reaper |
| 63 | + |
| 64 | +function Msg(param, cap) -- caption second or none |
| 65 | +local cap = cap and type(cap) == 'string' and #cap > 0 and cap..' = ' or '' |
| 66 | +reaper.ShowConsoleMsg(cap..tostring(param)..'\n') |
| 67 | +end |
| 68 | + |
| 69 | + |
| 70 | +function Esc(str) |
| 71 | + if not str then return end -- prevents error |
| 72 | +-- isolating the 1st return value so that if vars are initialized in a row outside of the function the next var isn't assigned the 2nd return value |
| 73 | +local str = str:gsub('[%(%)%+%-%[%]%.%^%$%*%?%%]','%%%0') |
| 74 | +return str |
| 75 | +end |
| 76 | + |
| 77 | + |
| 78 | +function Error_Tooltip(text) |
| 79 | +local x, y = r.GetMousePosition() |
| 80 | +r.TrackCtl_SetToolTip(text:upper():gsub('.','%0 '), x, y, true) -- spaced out // topmost true |
| 81 | +local time_init = r.time_precise() |
| 82 | + repeat |
| 83 | + until r.time_precise()-time_init >= 0.7 |
| 84 | +end |
| 85 | + |
| 86 | + |
| 87 | +function ACT(comm_ID, midi) -- midi is boolean |
| 88 | +local comm_ID = comm_ID and r.NamedCommandLookup(comm_ID) |
| 89 | +local act = comm_ID and comm_ID ~= 0 and (midi and r.MIDIEditor_LastFocused_OnCommand(comm_ID, false) -- islistviewcommand false |
| 90 | +or r.Main_OnCommand(comm_ID, 0)) -- only if valid command_ID |
| 91 | +end |
| 92 | + |
| 93 | + |
| 94 | +function Get_Children_Tracks(tr, t) |
| 95 | +local cnt = 0 |
| 96 | + for i = r.CSurf_TrackToID(tr, false), r.CountTracks(0)-1 do -- mcpView false // starting loop from the 1st child |
| 97 | + local chld_tr = r.GetTrack(0, i) |
| 98 | + if r.GetTrackDepth(chld_tr) > 0 then |
| 99 | + t[#t+1] = chld_tr |
| 100 | + cnt = cnt+1 |
| 101 | + end |
| 102 | + end |
| 103 | +end |
| 104 | + |
| 105 | + |
| 106 | +function weed_out_item_chunks(itm_t, line, templ_t, idx) |
| 107 | +local GUID = line:match('<ITEM (.+)') or line:match('IGUID (.+)') |
| 108 | +-- search for a GUID of an item found in the line of the template code among the items slated for removal |
| 109 | +local found |
| 110 | + for k, v in ipairs(itm_t) do |
| 111 | + if v == GUID then |
| 112 | + table.remove(itm_t, k) -- to optimize so that next cycle is shorter |
| 113 | + found = true break |
| 114 | + end |
| 115 | + end |
| 116 | + if found then -- if turns out to be an item slated for removal |
| 117 | + local i = idx -- starting from the index of the item chunk first line in the template code |
| 118 | + repeat |
| 119 | + if templ_t[i+1] and not templ_t[i+1]:match('<TRACK') then |
| 120 | + templ_t[i] = '' -- remove lines by replacing with empty space unless next block is <TRACK because it will be preceded by closure of the previous <TRACK block which should be left intact to ensure integrity of the code, the template won't load otherwise |
| 121 | + end |
| 122 | + i = i+1 |
| 123 | + until templ_t[i]:match('<ITEM') or templ_t[i]:match('<TRACK') or i == #templ_t -- until the index of the next chunk block or template end |
| 124 | + end |
| 125 | +end |
| 126 | + |
| 127 | + |
| 128 | +function Process_Track_Template(templ_t, itm_t, filename) |
| 129 | + |
| 130 | +local idx_init |
| 131 | + for idx, line in ipairs(templ_t) do |
| 132 | + if line:match('<ITEM') then idx_init = idx end -- store table index of the first line in item chunk |
| 133 | + if idx_init and (line:match('<ITEM (.+)') or line:match('IGUID')) then -- in some track template files the <ITEM block start may not include the GUID so watch for the line where it appears inside the item chunk |
| 134 | + weed_out_item_chunks(itm_t, line, templ_t, idx_init) |
| 135 | + idx_init = nil |
| 136 | + end |
| 137 | + end |
| 138 | + |
| 139 | +local templ = '' |
| 140 | + for k, line in ipairs(templ_t) do |
| 141 | + if #line > 0 then -- not a line of a removed item chunk |
| 142 | + local lb = k == 1 and '' or '\n' -- only add line break from 2nd line onwards |
| 143 | + templ = templ..lb..line |
| 144 | + end |
| 145 | + end |
| 146 | + |
| 147 | +local f = io.open(filename, 'w') |
| 148 | +f:write(templ) |
| 149 | +f:close() |
| 150 | + |
| 151 | +end |
| 152 | + |
| 153 | + |
| 154 | +function Delete_Ext_States(sect) |
| 155 | +local i = 1 |
| 156 | + repeat |
| 157 | + r.DeleteExtState(sect, i, true) -- persist true |
| 158 | + i = i+1 |
| 159 | + until r.GetExtState(sect, i) == '' -- first key without stored value |
| 160 | +end |
| 161 | + |
| 162 | + |
| 163 | +local _, scr_name, sect_ID, cmd_ID, _,_,_ = r.get_action_context() |
| 164 | +local named_ID = r.ReverseNamedCommandLookup(cmd_ID) |
| 165 | +local state = r.GetExtState(named_ID, '1') -- check if there're stored data from previous run when the user didn't update the template file |
| 166 | +local path = r.GetResourcePath() |
| 167 | +local sep = path:match('[\\/]') |
| 168 | +local tr_tmpl_fld = path..sep..'TrackTemplates'..sep |
| 169 | + |
| 170 | +-- Allow user to process template file with stored data if there're any |
| 171 | + if #state > 0 then |
| 172 | + local resp = r.MB('Last time you didn\'t update the template file.\n\n\t Wish to update it now?\n\n After cancellation or assention and then\n\n\t declining the dialogue,\n\n'..(' '):rep(10)..'the stored data will be removed.', 'PROMPT', 4) |
| 173 | + if resp == 6 then -- OK |
| 174 | + ::RETRY:: |
| 175 | + local retval, filename = r.GetUserFileNameForRead(tr_tmpl_fld, 'Select a .RTrackTemplate file', '.RTrackTemplate') |
| 176 | + if retval and not filename:match('%.RTrackTemplate$') then |
| 177 | + local resp = r.MB(' Invalid file type. Wish to retry?\n\nIf not, the stored data will be removed.', 'ERROR', 4) |
| 178 | + if resp == 6 then -- OK |
| 179 | + goto RETRY |
| 180 | + end |
| 181 | + elseif retval then |
| 182 | + local templ_t = {} |
| 183 | + for line in io.lines(filename) do -- lines aren't followed by line break |
| 184 | + templ_t[#templ_t+1] = line |
| 185 | + end |
| 186 | + local itm_t = {} |
| 187 | + local i = 1 |
| 188 | + repeat -- construct item GUID table from extended states |
| 189 | + itm_t[#itm_t+1] = r.GetExtState(named_ID, i) |
| 190 | + i = i+1 |
| 191 | + until r.GetExtState(named_ID, i) == '' -- first key without stored value |
| 192 | + Process_Track_Template(templ_t, itm_t, filename) |
| 193 | + end |
| 194 | + end |
| 195 | + Delete_Ext_States(named_ID) |
| 196 | + return r.defer(function() do return end end) end |
| 197 | + |
| 198 | + |
| 199 | +-- Start main routine |
| 200 | + |
| 201 | +local sel_tr_cnt = r.CountSelectedTracks(0) |
| 202 | + |
| 203 | + if sel_tr_cnt == 0 then |
| 204 | + Error_Tooltip('\n\n no selected tracks \n\n') |
| 205 | + return r.defer(function() do return end end) end |
| 206 | + |
| 207 | +local resp = r.MB('\"YES\" — items within time selection on selected tracks\n\n\"NO\" — selected items on selected tracks', 'PROMPT', 3) |
| 208 | + if resp == 2 then -- Cancel |
| 209 | + return r.defer(function() do return end end) end |
| 210 | + |
| 211 | + |
| 212 | +local itm_t = {} |
| 213 | +local GET = r.GetMediaItemInfo_Value |
| 214 | + |
| 215 | + if resp == 6 then -- items within time selection |
| 216 | + local st, fin = r.GetSet_LoopTimeRange(false, false, 0, 0, false) -- isSet, isLoop, allowautoseek false |
| 217 | + if st == fin then |
| 218 | + r.MB('Time selection isn\'t set.', 'ERROR', 0) |
| 219 | + return r.defer(function() do return end end) end |
| 220 | + |
| 221 | + local tr_t = {} |
| 222 | + for i = 0, sel_tr_cnt-1 do |
| 223 | + local tr = r.GetSelectedTrack(0,i) |
| 224 | + tr_t[#tr_t+1] = tr |
| 225 | + Get_Children_Tracks(tr, tr_t) -- folder parent track templates are saved with their children so children track items have to be taken into account |
| 226 | + end |
| 227 | + |
| 228 | + local itm_cnt = 0 |
| 229 | + for _, tr in ipairs(tr_t) do |
| 230 | + local tr_itm_cnt = r.GetTrackNumMediaItems(tr) |
| 231 | + itm_cnt = itm_cnt+tr_itm_cnt |
| 232 | + for i = 0, tr_itm_cnt-1 do |
| 233 | + local itm = r.GetTrackMediaItem(tr, i) |
| 234 | + local itm_st = GET(itm, 'D_POSITION') |
| 235 | + local itm_end = itm_st + GET(itm, 'D_LENGTH') |
| 236 | + if itm_st >= fin or itm_end <= st then -- only items which are explicitly not within time selection |
| 237 | + local retval, GUID = r.GetSetMediaItemInfo_String(itm, 'GUID', '', false) -- setNewValue false |
| 238 | + itm_t[#itm_t+1] = GUID |
| 239 | + end |
| 240 | + end |
| 241 | + end |
| 242 | + |
| 243 | + local err = itm_cnt == 0 and 'No items on selected tracks.' or itm_cnt == #itm_t and 'No items within time selection\n\n'..(' '):rep(10)..'on selected tracks.' |
| 244 | + if err then |
| 245 | + r.MB(err, 'ERROR', 0) |
| 246 | + return r.defer(function() do return end end) end |
| 247 | + |
| 248 | + elseif resp == 7 then -- selected items |
| 249 | + |
| 250 | + local tr_t = {} |
| 251 | + for i = 0, sel_tr_cnt-1 do |
| 252 | + local tr = r.GetSelectedTrack(0,i) |
| 253 | + tr_t[#tr_t+1] = tr |
| 254 | + Get_Children_Tracks(tr, tr_t) -- folder parent track templates are saved with their children so children track items have to be taken into account |
| 255 | + end |
| 256 | + |
| 257 | + local itm_cnt = 0 |
| 258 | + for _, tr in ipairs(tr_t) do |
| 259 | + local tr_itm_cnt = r.GetTrackNumMediaItems(tr) |
| 260 | + itm_cnt = itm_cnt+tr_itm_cnt |
| 261 | + for i = 0, tr_itm_cnt-1 do |
| 262 | + local itm = r.GetTrackMediaItem(tr, i) |
| 263 | + if not r.IsMediaItemSelected(itm) then -- only collect non-selected items |
| 264 | + local retval, GUID = r.GetSetMediaItemInfo_String(itm, 'GUID', '', false) -- setNewValue false |
| 265 | + itm_t[#itm_t+1] = GUID |
| 266 | + end |
| 267 | + end |
| 268 | + end |
| 269 | + |
| 270 | + local err = itm_cnt == 0 and 'No items on selected tracks.' or itm_cnt == #itm_t and 'No selected items.' |
| 271 | + if err then |
| 272 | + r.MB(err, 'ERROR', 0) |
| 273 | + return r.defer(function() do return end end) end |
| 274 | + |
| 275 | + end |
| 276 | + |
| 277 | +-- this particular method has been preferred over using chunk to create a template file because track templates contain some values not present in chunks, in particular additional envelope point values, two item length values, item GUID appearing in the item chunk start besides its IGUID inside the chunk |
| 278 | +ACT(40392) -- Track: Save tracks as track template... |
| 279 | +::RETRY:: |
| 280 | +local retval, filename = r.GetUserFileNameForRead(tr_tmpl_fld, 'Select a .RTrackTemplate file', '.RTrackTemplate') |
| 281 | + |
| 282 | + if retval and not filename:match('%.RTrackTemplate$') then |
| 283 | + local resp = r.MB('Invalid file type. Wish to retry?', 'ERROR', 4) |
| 284 | + if resp == 6 then -- OK |
| 285 | + goto RETRY |
| 286 | + else retval = nil end -- to trigger the next prompt |
| 287 | + end |
| 288 | + |
| 289 | + if not retval then -- user closed the dialogue without file |
| 290 | + local s = ' ' |
| 291 | + local resp = r.MB(' If a template was saved its file was not updated.\n\n'..s:rep(7)..'Wish to keep the data until the next run?\n\n'..s:rep(8)..'(only relevant if a template was saved)\n\n'..s:rep(10)..'If so, at next run during this session\n\n'..s:rep(13)..'you\'ll be asked to update the file.', 'PROMPT', 4) |
| 292 | + if resp == 6 then -- OK // store GUIDs of items whose chunks are to be removed from track template |
| 293 | + for k, GUID in ipairs(itm_t) do |
| 294 | + r.SetExtState(named_ID, k, GUID, false) -- persist false |
| 295 | + end |
| 296 | + end |
| 297 | + return r.defer(function() do return end end) end |
| 298 | + |
| 299 | +local templ_t = {} |
| 300 | + for line in io.lines(filename) do -- lines aren't followed by line break |
| 301 | + templ_t[#templ_t+1] = line |
| 302 | + end |
| 303 | + |
| 304 | +Process_Track_Template(templ_t, itm_t, filename) |
| 305 | + |
| 306 | +do return r.defer(function() do return end end) end -- prevent undo point creation |
| 307 | + |
| 308 | + |
| 309 | + |
0 commit comments