├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── cfillion_Apply render preset.lua ├── cfillion_Automation item selection bundle.lua ├── cfillion_Big Repeat Button.lua ├── cfillion_Bypass all input FX for selected tracks.lua ├── cfillion_Copy and paste project markers and regions.lua ├── cfillion_Copy current position to clipboard.lua ├── cfillion_Delete last marker left of the play or edit cursor.lua ├── cfillion_Enable or disable automation item loop.lua ├── cfillion_Export markers to mkvmerge simple chapter format.lua ├── cfillion_GFX Input Inspector.lua ├── cfillion_Insert empty space from time selection at edit cursor (moving later items).lua ├── cfillion_Insert regions at markers and vice versa in time selection.lua ├── cfillion_Linear ramp selected envelope points.lua ├── cfillion_Monitoring FX bypass bundle.lua ├── cfillion_Move right edge of selected automation items to end of time selection.lua ├── cfillion_Move selected items to a FIPM lane.lua ├── cfillion_Move selected items to one FIPM lane per color.lua ├── cfillion_Normalize peaks display gain.lua ├── cfillion_Nudge start or end of regions in time selection.lua ├── cfillion_Project underrun monitor (xrun).lua ├── cfillion_Remove hardware outputs of selected tracks.lua ├── cfillion_Rename selected tracks from clipboard lines.lua ├── cfillion_Repair missing JSFX files in current project.lua ├── cfillion_Reset stereo width of selected tracks to 100%.lua ├── cfillion_Select destination tracks of selected tracks sends recursively.lua ├── cfillion_Select source tracks of selected tracks receives recursively.lua ├── cfillion_Select track FX by name.lua ├── cfillion_Set item end to cursor and resize trailing MIDI notes.lua ├── cfillion_Set space between selected notes from grid size.lua ├── cfillion_Set take playback rate from semitones.lua ├── cfillion_Set timecode at edit cursor.lua ├── cfillion_Show ReaPack about dialog for the focused JSFX.lua ├── cfillion_Show all saved nudge settings.lua ├── cfillion_Show distance between play cursor and nearest marker.lua ├── cfillion_Split selected automation items at project markers and regions.lua ├── cfillion_Split selected non-locked items at edit cursor.lua ├── cfillion_Step sequencing (replace mode).jsfx ├── cfillion_Step sequencing (replace mode).lua ├── cfillion_Toggle MIDI preview on transport change.lua ├── cfillion_Toggle input FX bypass for selected tracks.lua ├── cfillion_Toggle take FX bypass for selected items.lua ├── cfillion_Toggle track FX bypass by name.lua ├── cfillion_Toggle visibility of empty non-folder tracks.lua ├── delete_empty_tracks.lua ├── explode_selected_tracks_to_mono_tracks.lua ├── instrument_track.lua ├── ireascript.lua ├── open_terminal_in_project_directory.lua ├── profiler.lua ├── select_items_right_of_selection.lua ├── send_track_audio.lua ├── send_track_midi.lua ├── set_channel_count.lua ├── song_switcher.lua ├── song_switcher_signal.lua └── song_switcher_www ├── Tupfile ├── Tupfile.ini ├── package.json └── src ├── client.coffee ├── main.coffee ├── song_switcher.slim ├── style.sass └── timeline.coffee /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 'cfillion' 2 | custom: 3 | - "https://reapack.com/donate" 4 | - "https://paypal.me/cfillion" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tup/ 2 | build/ 3 | dist/ 4 | node_modules/ 5 | ireascript.lua.history 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository now lives at https://codeberg.org/cfillion/reascripts. 2 | 3 | # Custom scripts for REAPER 4 | 5 | [![Donate](https://www.paypalobjects.com/webstatic/en_US/btn/btn_donate_74x21.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T3DEWBQJAV7WL&lc=CA&item_name=ReaScripts&no_note=0&cn=Custom%20message&no_shipping=1¤cy_code=CAD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) 6 | 7 | Development area of my REAPER scripts. Stable releases are distributed in 8 | [ReaPack](https://reapack.com) through the [ReaTeam Scripts](https://github.com/ReaTeam/ReaScripts) repository. 9 | -------------------------------------------------------------------------------- /cfillion_Automation item selection bundle.lua: -------------------------------------------------------------------------------- 1 | -- @description Automation item selection bundle 2 | -- @version 1.3.1 3 | -- @changelog 4 | -- Improve mouse cursor hit detection when envelope is not shown in lane 5 | -- @author cfillion 6 | -- @provides 7 | -- . > cfillion_Select and move to next automation item.lua 8 | -- . > cfillion_Select and move to next automation item in pool.lua 9 | -- . > cfillion_Select and move to previous automation item.lua 10 | -- . > cfillion_Select and move to previous automation item in pool.lua 11 | -- 12 | -- . > cfillion_Add next automation item to selection.lua 13 | -- . > cfillion_Add next automation item in pool to selection.lua 14 | -- . > cfillion_Add previous automation item to selection.lua 15 | -- . > cfillion_Add previous automation item in pool to selection.lua 16 | -- . > cfillion_Add all automation items under edit cursor to selection.lua 17 | -- . > cfillion_Add all automation items under mouse cursor to selection.lua 18 | -- 19 | -- . > cfillion_Select all automation items.lua 20 | -- . > cfillion_Select all automation items (all tracks).lua 21 | -- . > cfillion_Select all automation items in pool.lua 22 | -- . > cfillion_Select all automation items in pool (all tracks).lua 23 | -- . > cfillion_Select all automation items under edit cursor.lua 24 | -- . > cfillion_Select all automation items under edit cursor (all tracks).lua 25 | -- . > cfillion_Select all automation items under mouse cursor (any envelope).lua 26 | -- . > cfillion_Select all automation items in time selection.lua 27 | -- . > cfillion_Select all automation items in time selection (all tracks).lua 28 | -- 29 | -- . > cfillion_Unselect all automation items.lua 30 | -- . > cfillion_Unselect all automation items (all tracks).lua 31 | -- . > cfillion_Unselect all automation items in pool.lua 32 | -- . > cfillion_Unselect all automation items in pool (all tracks).lua 33 | -- . > cfillion_Unselect all automation items under edit cursor.lua 34 | -- . > cfillion_Unselect all automation items under edit cursor (all tracks).lua 35 | -- . > cfillion_Unselect all automation items under mouse cursor (any envelope).lua 36 | -- . > cfillion_Unselect all automation items in time selection.lua 37 | -- . > cfillion_Unselect all automation items in time selection (all tracks).lua 38 | -- @about 39 | -- # Automation item selection bundle 40 | -- 41 | -- This package provides many actions for selecting or unselecting 42 | -- automation items in the selected envelope lane. See the Contents tab for 43 | -- the list and for the exact name of the actions. 44 | -- 45 | -- - Actions for selecting and moving to the next or previous AIs 46 | -- - Actions for preserving the current selection 47 | -- - Actions for cycling through the AIs in the selected pool 48 | -- - Actions for selecting or unselecting all AIs, AIs in the selected pool, 49 | -- under the edit cursor and under the mouse cursor 50 | -- @link 51 | -- cfillion's website https://cfillion.ca 52 | -- Original request https://github.com/reaper-oss/sws/issues/899 53 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD&item_name=ReaScript%3A+Automation+item+selection+bundle 54 | 55 | local UNDO_STATE_TRACKCFG = 1 56 | 57 | local name = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 58 | 59 | local moveMode = name:match('move to') 60 | local poolMode = name:match('pool') 61 | local addToSelMode = name:match('Add.+to selection') 62 | local prevMode = name:match('previous') 63 | local entireBucketMode = name:match('all automation') 64 | local unselectMode = name:match('Unselect') 65 | local editCursorMode = name:match('under edit cursor') 66 | local mouseCursorMode = name:match('under mouse cursor') 67 | local allTracksMode = name:match('all tracks') or name:match('any envelope') 68 | local timeMode = name:match('time selection') 69 | 70 | function testUnderMouse(env) 71 | local _, chunk = reaper.GetEnvelopeStateChunk(env, '') 72 | local inLane = chunk:match('VIS %d (%d) %d') == '1' 73 | 74 | local window, segment, details = reaper.BR_GetMouseCursorContext() 75 | 76 | if inLane then 77 | return reaper.BR_GetMouseCursorContext_Envelope() == env 78 | elseif segment == 'track' then 79 | -- BR_GetMouseCursorContext_Envelope doesn't return the envelope if it's 80 | -- not shown in a lane but over the items and the cursor is not right over 81 | -- a envelope segment or point. 82 | 83 | local track = reaper.Envelope_GetParentTrack(env) 84 | 85 | return reaper.BR_GetMouseCursorContext_Track() == track 86 | else 87 | return false 88 | end 89 | end 90 | 91 | function testCursorPosition(env, startTime, endTime) 92 | local curPos 93 | 94 | if editCursorMode then 95 | curPos = reaper.GetCursorPosition() 96 | elseif mouseCursorMode then 97 | if testUnderMouse(env) then 98 | curPos = reaper.BR_GetMouseCursorContext_Position() 99 | else 100 | return false 101 | end 102 | else 103 | return true 104 | end 105 | 106 | return startTime <= curPos and endTime >= curPos 107 | end 108 | 109 | function testTimeSelection(env, startTime, endTime) 110 | if not timeMode then 111 | return true 112 | end 113 | 114 | if not tstart then 115 | tstart, tend = reaper.GetSet_LoopTimeRange(false, false, 0, 0, false) 116 | end 117 | 118 | return tstart ~= tend and startTime <= tend and endTime >= tstart 119 | end 120 | 121 | function enumSelectedEnvelope() 122 | local env = reaper.GetSelectedEnvelope(0) 123 | if not env then return function() end end 124 | 125 | local i, count = -1, reaper.CountAutomationItems(env) 126 | 127 | return function() 128 | i = i + 1 129 | 130 | if i < count then 131 | return env, i 132 | end 133 | end 134 | end 135 | 136 | function enumAllEnvelopes() 137 | local trackCount = reaper.CountTracks(0) 138 | 139 | local ti, ei, ai = 0, 0, 0 140 | local track, env 141 | local envCount, aiCount = 0, 0 142 | 143 | return function() 144 | while ai >= aiCount do 145 | while ei >= envCount do 146 | if ti < trackCount then 147 | track = reaper.GetTrack(0, ti) 148 | ti, ei = ti + 1, 0 149 | envCount = reaper.CountTrackEnvelopes(track) 150 | else 151 | return 152 | end 153 | end 154 | 155 | env = reaper.GetTrackEnvelope(track, ei) 156 | ei, ai = ei + 1, 0 157 | aiCount = reaper.CountAutomationItems(env) 158 | end 159 | 160 | local id = ai 161 | ai = ai + 1 162 | return env, id 163 | end 164 | end 165 | 166 | local buckets = {} 167 | local currentSel, currentBucket = {}, 0 168 | 169 | for env, i in (allTracksMode and enumAllEnvelopes or enumSelectedEnvelope)() do 170 | local selected = 1 == reaper.GetSetAutomationItemInfo(env, i, 'D_UISEL', 0, false) 171 | local bucketId = 0 172 | local startTime = reaper.GetSetAutomationItemInfo(env, i, 'D_POSITION', 0, false) 173 | local length = reaper.GetSetAutomationItemInfo(env, i, 'D_LENGTH', 0, false) 174 | local underCursor = testCursorPosition(env, startTime, startTime + length) 175 | local inTimeSel = testTimeSelection(env, startTime, startTime + length) 176 | 177 | if poolMode then 178 | bucketId = reaper.GetSetAutomationItemInfo(env, i, 'D_POOL_ID', 0, false) 179 | end 180 | 181 | if selected and poolMode and currentBucket == 0 then 182 | currentBucket = bucketId 183 | end 184 | 185 | if selected then 186 | table.insert(currentSel, {env=env, id=i}) 187 | end 188 | 189 | if (not selected or entireBucketMode or unselectMode) and underCursor and inTimeSel then 190 | local ai = {env=env, id=i, pos=startTime} 191 | 192 | if buckets[bucketId] then 193 | table.insert(buckets[bucketId], ai) 194 | else 195 | buckets[bucketId] = {ai} 196 | end 197 | end 198 | end 199 | 200 | local bucket = buckets[currentBucket] or {} 201 | if #bucket == 0 then 202 | reaper.defer(function() end) 203 | return 204 | end 205 | 206 | local target 207 | 208 | -- fallback target 209 | if prevMode then 210 | target = #bucket 211 | else 212 | target = 1 213 | end 214 | 215 | -- find next or previous target 216 | if #currentSel > 0 and not entireBucketMode then 217 | if prevMode then 218 | local firstSel = currentSel[1].id 219 | 220 | for ri=0,#bucket-1 do 221 | local bid = #bucket - ri 222 | if bucket[bid].id < firstSel then 223 | target = bid 224 | break 225 | end 226 | end 227 | else 228 | local lastSel = currentSel[#currentSel].id 229 | 230 | for i,ai in ipairs(bucket) do 231 | if ai.id > lastSel then 232 | target = i 233 | break 234 | end 235 | end 236 | end 237 | end 238 | 239 | reaper.Undo_BeginBlock() 240 | 241 | if not addToSelMode and not unselectMode then 242 | for _,ai in ipairs(currentSel) do 243 | reaper.GetSetAutomationItemInfo(ai.env, ai.id, 'D_UISEL', 0, true) 244 | end 245 | 246 | if moveMode then 247 | reaper.SetEditCurPos(bucket[target].pos, true, false) 248 | end 249 | end 250 | 251 | local sel = unselectMode and 0 or 1 252 | 253 | if entireBucketMode then 254 | for _,ai in ipairs(bucket) do 255 | reaper.GetSetAutomationItemInfo(ai.env, ai.id, 'D_UISEL', sel, true) 256 | end 257 | else 258 | reaper.GetSetAutomationItemInfo(bucket[target].env, bucket[target].id, 'D_UISEL', sel, true) 259 | end 260 | 261 | reaper.Undo_EndBlock(name, UNDO_STATE_TRACKCFG) 262 | -------------------------------------------------------------------------------- /cfillion_Big Repeat Button.lua: -------------------------------------------------------------------------------- 1 | -- @description Big Repeat Button 2 | -- @version 1.0.3 3 | -- @author cfillion 4 | -- @changelog 5 | -- Show the button even if the window cannot fit the text 6 | -- Fix mouse cursor click hit detection in small windows 7 | -- @link 8 | -- cfillion.ca https://cfillion.ca 9 | -- Request Thread https://forum.cockos.com/showthread.php?t=191477 10 | -- @screenshot https://i.imgur.com/BUripgE.gif 11 | -- @donation https://www.paypal.me/cfillion/10 12 | 13 | local HIGHLIGHT = 0.1 14 | local REPEAT_QUERY = -1 15 | local REPEAT_TOGGLE = 255 16 | 17 | local CENTER_H = 1 18 | local CENTER_V = 4 19 | 20 | local mouse_down = false 21 | 22 | function make_label(on) 23 | local status = function(on) 24 | if on then 25 | return "ON" 26 | else 27 | return "OFF" 28 | end 29 | end 30 | 31 | return string.format("Repeat %s", status(on)) 32 | end 33 | 34 | function loop() 35 | local margin = math.min(gfx.w, gfx.h) / 8 36 | local left = gfx.w - margin 37 | local width = left - margin 38 | local bottom = gfx.h - margin 39 | local height = bottom - margin 40 | 41 | local w, h = gfx.measurestr(make_label(false)) 42 | local diff = math.max(w - width, h - height) 43 | 44 | if diff > 0 and diff <= margin then 45 | margin = margin - diff 46 | left = left + diff 47 | bottom = bottom + diff 48 | width = width + (diff * 2) 49 | height = height + (diff * 2) 50 | end 51 | 52 | local mouse_hit = 53 | (gfx.mouse_x > margin and gfx.mouse_x < left) and 54 | (gfx.mouse_y > margin and gfx.mouse_y < bottom) 55 | 56 | if gfx.getchar() < 0 then 57 | return gfx.quit() 58 | elseif (gfx.mouse_cap & 1) ~= 0 and mouse_hit then 59 | mouse_down = true 60 | elseif mouse_down then 61 | reaper.GetSetRepeat(REPEAT_TOGGLE) 62 | mouse_down = false 63 | end 64 | 65 | local on = reaper.GetSetRepeat(REPEAT_QUERY) == 1 66 | 67 | gfx.r = 0; gfx.g = 0; gfx.b = 0 68 | 69 | if on then 70 | gfx.g = 0.3 71 | else 72 | gfx.r = 0.3 73 | end 74 | 75 | if mouse_hit then 76 | if mouse_down then 77 | gfx.b = 0.5 78 | else 79 | gfx.r = gfx.r + HIGHLIGHT 80 | gfx.g = gfx.g + HIGHLIGHT 81 | gfx.b = gfx.b + HIGHLIGHT 82 | end 83 | end 84 | 85 | gfx.rect(margin, margin, width, height, true) 86 | 87 | gfx.r = 0.9; gfx.g = 0.9; gfx.b = 1 88 | gfx.rect(margin, margin, width, height, false) 89 | 90 | gfx.x = margin; gfx.y = margin 91 | gfx.drawstr(make_label(on), CENTER_H | CENTER_V, left, bottom) 92 | 93 | gfx.update() 94 | reaper.defer(loop) 95 | end 96 | 97 | gfx.setfont(1, 'sans-serif', 62, 9) 98 | gfx.init("Big Repeat Button", 700, 300) 99 | loop() 100 | -------------------------------------------------------------------------------- /cfillion_Bypass all input FX for selected tracks.lua: -------------------------------------------------------------------------------- 1 | -- @description Bypass all input FX for selected tracks 2 | -- @author cfillion 3 | -- @version 2.0 4 | -- @changelog 5 | -- Split the original script into three actions: 6 | -- 7 | -- - Bypass all input FX for selected tracks 8 | -- - Unbypass all input FX for selected tracks 9 | -- - Toggle bypass all input FX for selected tracks 10 | -- @provides 11 | -- . 12 | -- [main] . > cfillion_Unbypass all input FX for selected tracks.lua 13 | -- [main] . > cfillion_Toggle bypass all input FX for selected tracks.lua 14 | -- @link http://forum.cockos.com/showthread.php?t=185229 15 | -- @about 16 | -- This script provides three actions for bypassing all input FX on selected tracks at once: 17 | -- 18 | -- - Bypass all input FX for selected tracks 19 | -- - Unbypass all input FX for selected tracks 20 | -- - Toggle bypass all input FX for selected tracks 21 | 22 | local UNDO_STATE_FX = 2 23 | 24 | local scriptName = ({reaper.get_action_context()})[2]:match('([^/\\_]+)%.lua$') 25 | local toggleMode = scriptName:match('Toggle') 26 | local enable = scriptName:match('Unbypass') ~= nil 27 | 28 | reaper.Undo_BeginBlock() 29 | 30 | for ti=0,reaper.CountSelectedTracks()-1 do 31 | local track = reaper.GetSelectedTrack(0, ti) 32 | for fi=0,reaper.TrackFX_GetRecCount(track) do 33 | fi = fi + 0x1000000 34 | 35 | if toggleMode then 36 | enable = not reaper.TrackFX_GetEnabled(track, fi) 37 | end 38 | 39 | reaper.TrackFX_SetEnabled(track, fi, enable) 40 | end 41 | end 42 | 43 | reaper.Undo_EndBlock('Bypass all input FX for selected tracks', UNDO_STATE_FX) 44 | -------------------------------------------------------------------------------- /cfillion_Copy and paste project markers and regions.lua: -------------------------------------------------------------------------------- 1 | -- @description Copy/paste project markers and/or regions 2 | -- @version 1.1.2 3 | -- @changelog Remove error message when pasting without any markers or regions in the clipboard 4 | -- @author cfillion 5 | -- @links 6 | -- cfillion.ca https://cfillion.ca/ 7 | -- Request Thread https://forum.cockos.com/showthread.php?t=201983 8 | -- @screenshots 9 | -- Basic usage https://i.imgur.com/dSyRnKe.gif 10 | -- Paste at edit cursor https://i.imgur.com/Zdu5VIF.gif 11 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD 12 | -- @provides 13 | -- . > cfillion_Copy project markers and regions in time selection.lua 14 | -- . > cfillion_Copy project markers in time selection.lua 15 | -- . > cfillion_Copy project regions in time selection.lua 16 | -- . > cfillion_Paste project markers and regions.lua 17 | -- . > cfillion_Paste project markers and regions at edit cursor.lua 18 | -- @about 19 | -- This script provides actions to copy and paste project markers and/or 20 | -- regions in the time selection. All markers and/or regions in the project 21 | -- are copied if no time selection is set. The IDs are automatically 22 | -- incremented when pasting. 23 | -- 24 | -- - Copy project markers and regions in time selection 25 | -- - Copy project markers in time selection 26 | -- - Copy project regions in time selection 27 | -- - Paste project markers and regions 28 | -- - Paste project markers and regions at edit cursor 29 | 30 | local UNDO_STATE_MISCCFG = 8 31 | local EXT_SECTION = 'cfillion_copy_paste_markers' 32 | 33 | local script_name = ({reaper.get_action_context()})[2]:match("([^/\\_]+)%.lua$") 34 | 35 | function copy() 36 | clear() 37 | 38 | local n = 0 39 | for i=0, reaper.CountProjectMarkers(0)-1 do 40 | local marker = {reaper.EnumProjectMarkers3(0, i)} 41 | table.remove(marker, 1) 42 | 43 | -- boolean isrgn, number pos, number rgnend, string name, number markrgnindexnumber, number color 44 | 45 | if testType(marker[1]) and testPos(marker[2], marker[3]) then 46 | n = n + 1 47 | reaper.SetExtState(EXT_SECTION, makeKey(n), serialize(marker), false) 48 | end 49 | end 50 | end 51 | 52 | function paste() 53 | local markers = readClipboard() 54 | local relative, offset = script_name:match('at edit cursor') 55 | local _, markerId, regionId = reaper.CountProjectMarkers(0) 56 | 57 | if #markers < 1 then 58 | return 59 | end 60 | 61 | reaper.Undo_BeginBlock() 62 | 63 | for i, marker in ipairs(markers) do 64 | -- paste at edit cursor 65 | if relative then 66 | if not offset then 67 | offset = marker[2] - reaper.GetCursorPosition() 68 | end 69 | 70 | marker[2] = marker[2] - offset 71 | 72 | if marker[1] then -- move the region end 73 | marker[3] = marker[3] - offset 74 | end 75 | end 76 | 77 | -- renumber the requested index 78 | if marker[1] then 79 | regionId = regionId + 1 80 | marker[5] = regionId 81 | else 82 | markerId = markerId + 1 83 | marker[5] = markerId 84 | end 85 | 86 | reaper.AddProjectMarker2(0, table.unpack(marker)) 87 | end 88 | 89 | reaper.Undo_EndBlock(script_name, UNDO_STATE_MISCCFG) 90 | end 91 | 92 | function clear() 93 | for key in clipboardIterator() do 94 | reaper.DeleteExtState(EXT_SECTION, key, false) 95 | end 96 | end 97 | 98 | function makeKey(i) 99 | return string.format("marker%03d", i) 100 | end 101 | 102 | function clipboardIterator() 103 | local i = 0 104 | return function() 105 | i = i + 1 106 | 107 | local key = makeKey(i) 108 | 109 | if reaper.HasExtState(EXT_SECTION, key) then 110 | return key 111 | end 112 | end 113 | end 114 | 115 | function serialize(tbl) 116 | local str = '' 117 | 118 | for _, value in ipairs(tbl) do 119 | str = str .. type(value) .. '\31' .. tostring(value) .. '\30' 120 | end 121 | 122 | return str 123 | end 124 | 125 | function unserialize(str) 126 | local type_map = { 127 | string = tostring, 128 | number = tonumber, 129 | boolean = function(v) return v == 'true' and true or false end, 130 | } 131 | 132 | local tbl = {} 133 | 134 | for type, value in str:gmatch('(.-)\31(.-)\30') do 135 | if not type_map[type] then 136 | error(string.format("unsupported value type: %s", type)) 137 | end 138 | 139 | table.insert(tbl, type_map[type](value)) 140 | end 141 | 142 | return tbl 143 | end 144 | 145 | function readClipboard() 146 | local markers = {} 147 | 148 | for key in clipboardIterator() do 149 | table.insert(markers, unserialize(reaper.GetExtState(EXT_SECTION, key))) 150 | end 151 | 152 | return markers 153 | end 154 | 155 | function testType(isrgn) 156 | return script_name:match(isrgn and 'region' or 'marker') 157 | end 158 | 159 | function testPos(startpos, endpos) 160 | local tstart, tend = reaper.GetSet_LoopTimeRange(false, false, 0, 0, false) 161 | 162 | if endpos == 0 then 163 | endpos = startpos 164 | end 165 | 166 | return startpos >= tstart and (tend == 0 or endpos <= tend) 167 | end 168 | 169 | (script_name:match('Copy') and copy or paste)() 170 | reaper.defer(function() end) -- disable automatic undo point 171 | -------------------------------------------------------------------------------- /cfillion_Copy current position to clipboard.lua: -------------------------------------------------------------------------------- 1 | -- @description Copy current cursor/playback position to clipboard 2 | -- @version 1.0.2 3 | -- @author cfillion 4 | -- @changelog update to use my new cross-platform clipboard API function (SWS v2.9.5+) 5 | 6 | local FORMAT = -1 7 | 8 | -- Possible formats: 9 | -- -1=project default 10 | -- 0=time 11 | -- 1=measures.beats + time 12 | -- 2=measures.beats 13 | -- 3=seconds 14 | -- 4=samples 15 | -- 5=h:m:s:f 16 | 17 | local function position() 18 | if reaper.GetPlayState() & 1 == 0 then 19 | return reaper.GetCursorPosition() 20 | else 21 | return reaper.GetPlayPosition2() 22 | end 23 | end 24 | 25 | reaper.CF_SetClipboard(reaper.format_timestr_len(position(), '', 0, FORMAT)) 26 | reaper.defer(function() end) -- disable creation of undo point 27 | -------------------------------------------------------------------------------- /cfillion_Delete last marker left of the play or edit cursor.lua: -------------------------------------------------------------------------------- 1 | -- @description Delete last marker left of the play or edit cursor 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @website cfillion.ca https://cfillion.ca 5 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD&item_name=ReaScript%3A+Delete+last+marker+left+of+the+play+or+edit+cursor 6 | 7 | local UNDO_STATE_MISCCFG = 8 8 | local SCRIPT_NAME = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 9 | 10 | local function position() 11 | if reaper.GetPlayState() & 1 == 0 then 12 | return reaper.GetCursorPosition() 13 | else 14 | return reaper.GetPlayPosition2() 15 | end 16 | end 17 | 18 | local marker = reaper.GetLastMarkerAndCurRegion(0, position()) 19 | 20 | if marker > -1 then 21 | reaper.Undo_BeginBlock() 22 | reaper.DeleteProjectMarkerByIndex(0, marker) 23 | reaper.Undo_EndBlock(SCRIPT_NAME, UNDO_STATE_MISCCFG) 24 | else 25 | reaper.defer(function() end) -- no undo point 26 | end 27 | -------------------------------------------------------------------------------- /cfillion_Enable or disable automation item loop.lua: -------------------------------------------------------------------------------- 1 | -- @description Enable or disable automation item loop 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @link cfillion.ca https://cfillion.ca 5 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD&item_name=ReaScript%3A+Enable+or+disable+automation+item+loop 6 | -- @provides 7 | -- . > cfillion_Enable automation item loop.lua 8 | -- . > cfillion_Disable automation item loop.lua 9 | 10 | local UNDO_STATE_TRACKCFG = 1 11 | 12 | local script_name = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 13 | local newLoop = script_name:match('Enable') and 1 or 0 14 | 15 | reaper.defer(function() end) 16 | 17 | local env = reaper.GetSelectedTrackEnvelope(0) 18 | if not env then return end 19 | 20 | local bucket = {} 21 | 22 | for i=0,reaper.CountAutomationItems(env)-1 do 23 | local selected = 1 == reaper.GetSetAutomationItemInfo(env, i, 'D_UISEL', 0, false) 24 | local looped = 0 ~= reaper.GetSetAutomationItemInfo(env, i, 'D_LOOPSRC', 0, false) 25 | 26 | if selected and looped ~= newLoop then 27 | table.insert(bucket, {id=i, loop=newLoop}) 28 | end 29 | end 30 | 31 | if #bucket < 1 then return end 32 | 33 | reaper.Undo_BeginBlock() 34 | 35 | for _,ai in ipairs(bucket) do 36 | reaper.GetSetAutomationItemInfo(env, ai.id, 'D_LOOPSRC', ai.loop, true) 37 | end 38 | 39 | reaper.Undo_EndBlock(script_name, UNDO_STATE_TRACKCFG) 40 | -------------------------------------------------------------------------------- /cfillion_Export markers to mkvmerge simple chapter format.lua: -------------------------------------------------------------------------------- 1 | -- @description Export markers to mkvmerge simple chapter format 2 | -- @author cfillion 3 | -- @version 1.0 4 | -- @website http://cfillion.ca 5 | -- @donation https://www.paypal.me/cfillion 6 | -- @about 7 | -- # Usage Instructions 8 | -- 9 | -- 1. Create your markers 10 | -- 2. Run this script 11 | -- 3. Paste the generated text into a text file 12 | -- 4. Run the following command to merge your video with the chapter text file: 13 | -- `mkvmerge input.mov --chapters chapters.txt --default-language eng -o output.mpv 14 | 15 | local next_index = 0 16 | 17 | reaper.ClearConsole() 18 | 19 | while true do 20 | -- marker = {retval, isrgn, pos, rgnend, name, markrgnindexnumber, color} 21 | local marker = {reaper.EnumProjectMarkers3(0, next_index)} 22 | 23 | next_index = marker[1] 24 | if next_index == 0 then break end 25 | 26 | if not marker[2] then -- it's not a region 27 | local time = marker[3] 28 | hour = math.floor(time / 3600) 29 | time = time % 3600 30 | min = math.floor(time / 60) 31 | time = time % 60 32 | sec = math.floor(time) 33 | ms = math.floor((time % 1) * 1000) 34 | 35 | reaper.ShowConsoleMsg( 36 | string.format("CHAPTER%02d=%02d:%02d:%02d.%03d\nCHAPTER%02dNAME=%s\n", 37 | marker[6], hour, min, sec, ms, marker[6], marker[5])) 38 | end 39 | end 40 | 41 | reaper.defer(function() end) -- disable undo point 42 | -------------------------------------------------------------------------------- /cfillion_GFX Input Inspector.lua: -------------------------------------------------------------------------------- 1 | -- @description GFX input inspector 2 | -- @author cfillion 3 | -- @version 2.1 4 | -- @changelog Add REAPER 6.65's gfx.char second return value 5 | 6 | local last_char, last_codepoint, last_printchar = 0, 0, '' 7 | local ismacos = reaper.GetOS():find('OSX') ~= nil 8 | 9 | local function modifiers() 10 | local mods = {} 11 | 12 | if gfx.mouse_cap & 4 ~= 0 then 13 | if ismacos then 14 | table.insert(mods, 'Cmd') 15 | else 16 | table.insert(mods, 'Ctrl') 17 | end 18 | end 19 | if ismacos and gfx.mouse_cap & 32 ~= 0 then 20 | table.insert(mods, 'Ctrl') 21 | end 22 | if gfx.mouse_cap & 16 ~= 0 then 23 | if ismacos then 24 | table.insert(mods, 'Opt') 25 | else 26 | table.insert(mods, 'Alt') 27 | end 28 | end 29 | if gfx.mouse_cap & 8 ~= 0 then 30 | table.insert(mods, 'Shift') 31 | end 32 | 33 | return table.concat(mods, '+') 34 | end 35 | 36 | local function buttons() 37 | local btns = {} 38 | 39 | if gfx.mouse_cap & 1 ~= 0 then 40 | table.insert(btns, 'left') 41 | end 42 | if gfx.mouse_cap & 64 ~= 0 then 43 | table.insert(btns, 'middle') 44 | end 45 | if gfx.mouse_cap & 2 ~= 0 then 46 | table.insert(btns, 'right') 47 | end 48 | 49 | return table.concat(btns, '+') 50 | end 51 | 52 | local function nl() 53 | gfx.y = gfx.y + 15 54 | end 55 | 56 | local function drawline(str) 57 | gfx.x = 10 58 | gfx.drawstr(str) 59 | nl() 60 | end 61 | 62 | local function loop() 63 | local char, codepoint = gfx.getchar() 64 | 65 | if char < 0 then 66 | gfx.quit() 67 | return 68 | end 69 | 70 | if char > 0 then 71 | last_char = char 72 | end 73 | if codepoint and codepoint > 0 then 74 | last_codepoint = codepoint 75 | last_printchar = utf8.char(codepoint) 76 | end 77 | 78 | gfx.y = 10 79 | 80 | drawline(('gfx.mouse_cap => 0x%02x (%d)'):format(gfx.mouse_cap, gfx.mouse_cap)) 81 | drawline((' modifiers => %s'):format(modifiers())) 82 | drawline((' buttons => %s'):format(buttons())) 83 | nl() 84 | drawline(('gfx.getchar() => 0x%08x'):format(last_char)) 85 | drawline((" => 0x%08x ('%s')"):format(last_codepoint, last_printchar)) 86 | 87 | gfx.update() 88 | reaper.defer(loop) 89 | end 90 | 91 | gfx.init('GFX input inspector', 320, 110) 92 | reaper.defer(loop) 93 | -------------------------------------------------------------------------------- /cfillion_Insert empty space from time selection at edit cursor (moving later items).lua: -------------------------------------------------------------------------------- 1 | -- @description Insert empty space from time selection at edit cursor (moving later items) 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @links 5 | -- cfillion.ca https://cfillion.ca/ 6 | -- Request Post https://forum.cockos.com/showthread.php?p=1942464 7 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD&item_name=ReaScript%3A+Insert+empty+space+from+time+selection+at+edit+cursor 8 | -- @about 9 | -- This is a wrapper around the native action "Time selection: Insert empty 10 | -- space at time selection (moving later items)". Empty space is inserted at 11 | -- edit cursor instead of at the start of the time selection. 12 | 13 | local tstart, tend = reaper.GetSet_LoopTimeRange(false, false, 0, 0, false) 14 | 15 | reaper.PreventUIRefresh(1) 16 | 17 | local curPos = reaper.GetCursorPosition() 18 | reaper.GetSet_LoopTimeRange(true, false, curPos, curPos + (tend - tstart), false) 19 | reaper.Main_OnCommand(40200, 0) -- Time selection: Insert empty space at time selection (moving later items) 20 | reaper.GetSet_LoopTimeRange(true, false, tstart, tend, false) 21 | 22 | reaper.PreventUIRefresh(-1) 23 | -------------------------------------------------------------------------------- /cfillion_Insert regions at markers and vice versa in time selection.lua: -------------------------------------------------------------------------------- 1 | -- @description Insert regions at markers and vice versa in time selection 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @link https://forum.cockos.com/showthread.php?t=185577 5 | 6 | reaper.Undo_BeginBlock() 7 | 8 | local UNDO_STATE_MISCCFG = 8 9 | 10 | local next_index, boundaries, last_marker = 0, {}, 0 11 | local ts_from, ts_to = reaper.GetSet_LoopTimeRange(false, false, 0, 0, false) 12 | 13 | while true do 14 | -- marker = {retval, isrgn, pos, rgnend, name, markrgnindexnumber, color} 15 | local marker = {reaper.EnumProjectMarkers3(0, next_index)} 16 | 17 | next_index = marker[1] 18 | if next_index == 0 then break end 19 | 20 | if not marker[2] then -- it's not a region 21 | if boundaries[last_marker] then 22 | boundaries[last_marker].to = marker[3] 23 | end 24 | 25 | last_marker = #boundaries + 1 26 | end 27 | 28 | boundaries[#boundaries + 1] = {isregion=marker[2], from=marker[3], 29 | to=marker[4], name=marker[5], id=marker[6], color=marker[7]} 30 | end 31 | 32 | local function exists(match) 33 | for _,boundary in ipairs(boundaries) do 34 | if boundary ~= match and boundary.from == match.from then 35 | return true 36 | end 37 | end 38 | 39 | return false 40 | end 41 | 42 | for _,boundary in ipairs(boundaries) do 43 | local withinTS = ts_to == 0 or (boundary.from >= ts_from and boundary.to <= ts_to) 44 | if boundary.to > 0 and withinTS and not exists(boundary) then 45 | reaper.AddProjectMarker2(0, not boundary.isregion, boundary.from, 46 | boundary.to, boundary.name, boundary.id, boundary.color) 47 | end 48 | end 49 | 50 | reaper.Undo_EndBlock('Insert regions at markers and vice versa in time selection', UNDO_STATE_MISCCFG) 51 | -------------------------------------------------------------------------------- /cfillion_Linear ramp selected envelope points.lua: -------------------------------------------------------------------------------- 1 | -- @description Linear ramp selected envelope points 2 | -- @author cfillion 3 | -- @version 1.0.1 4 | -- @changelog 5 | -- Fix value scaling in fader scaling mode 6 | -- Remember the window position, size and dock state 7 | -- @screenshot https://i.imgur.com/KwqzjfC.gif 8 | -- @donation https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD 9 | 10 | local EXT_SECTION = 'cfillion_ramp_envelope_points' 11 | local EXT_WINDOW_STATE = 'window_state' 12 | local MARGIN = 10 13 | 14 | local UNDO_STATE_TRACKCFG = 1 15 | 16 | local IDC_ARROW = 32512 17 | local IDC_SIZENS = 32645 18 | 19 | local HCENTER = 1 20 | local VCENTER = 1<<2 21 | 22 | local ADJ_LEFT = 1 23 | local ADJ_RIGHT = 0 24 | 25 | function enumEnvelopePoints() 26 | local pi = 0 27 | local count = reaper.CountEnvelopePoints(state.env) 28 | 29 | return function() 30 | local point = {reaper.GetEnvelopePoint(state.env, pi)} 31 | if point[1] then -- retval 32 | point[1] = pi 33 | pi = pi + 1 34 | return point 35 | end 36 | end 37 | end 38 | 39 | function getSelectedPoints() 40 | local points = {} 41 | 42 | for point in enumEnvelopePoints() do 43 | if point[6] then -- selected 44 | table.insert(points, point) 45 | end 46 | end 47 | 48 | return points 49 | end 50 | 51 | function loadState(count) 52 | if gfx.w ~= state.oldw or gfx.h ~= state.oldh then 53 | state.count = nil -- invalidate the state 54 | end 55 | 56 | local env = reaper.GetSelectedEnvelope(0) 57 | local stateCount = reaper.GetProjectStateChangeCount() 58 | 59 | if stateCount == state.count and state.env == env then 60 | return 61 | end 62 | 63 | state = { 64 | env = env, 65 | count = count, oldw = gfx.w, oldh = gfx.h 66 | } 67 | 68 | if not state.env then 69 | state.error = "No envelope selected" 70 | return 71 | end 72 | 73 | state.scalingMode = reaper.GetEnvelopeScalingMode(state.env) 74 | 75 | state.selectedPoints = getSelectedPoints() 76 | if #state.selectedPoints < 3 then 77 | state.error = "Select at least three points" 78 | return 79 | end 80 | 81 | local brenv = reaper.BR_EnvAlloc(state.env, false) 82 | local envprops = {reaper.BR_EnvGetProperties(brenv)} 83 | reaper.BR_EnvFree(brenv, false) 84 | 85 | state.firstPos = state.selectedPoints[1][2] 86 | state.lastPos = state.selectedPoints[#state.selectedPoints][2] 87 | 88 | state.timeSpan = state.lastPos - state.firstPos 89 | state.xscale = state.timeSpan / w 90 | 91 | state.minValue = reaper.ScaleToEnvelopeMode(state.scalingMode, envprops[7]) 92 | state.maxValue = reaper.ScaleToEnvelopeMode(state.scalingMode, envprops[8]) 93 | state.yscale = (state.maxValue - state.minValue) / h 94 | end 95 | 96 | function adjustValue(point) 97 | local value = point[3] 98 | 99 | if not adjustment or adjustment == 0 then 100 | return value 101 | end 102 | 103 | local force = (point[2] - state.firstPos) / state.timeSpan 104 | if adjustmentDir == ADJ_LEFT then 105 | force = 1 - force 106 | end 107 | 108 | value = value + (adjustment * force) 109 | return math.min(math.max(value, state.minValue), state.maxValue) 110 | end 111 | 112 | function applyAdjustment() 113 | reaper.Undo_BeginBlock() 114 | 115 | for _, point in ipairs(state.selectedPoints) do 116 | local value = adjustValue(point) 117 | reaper.SetEnvelopePoint(state.env, point[1], nil, value, nil, nil, nil, true) 118 | end 119 | 120 | reaper.Envelope_SortPoints(state.env) 121 | reaper.UpdateArrange() 122 | 123 | reaper.Undo_EndBlock(scriptName, UNDO_STATE_TRACKCFG) 124 | end 125 | 126 | function clearAdjustment() 127 | mouseDownY, adjustment = nil, nil 128 | end 129 | 130 | function setColor(color) 131 | gfx.r = (color >> 16 ) / 255 132 | gfx.g = (color >> 8 & 0xFF) / 255 133 | gfx.b = (color & 0xFF) / 255 134 | end 135 | 136 | function drawMessage(message, align) 137 | gfx.x, gfx.y = 0, 0 138 | setColor(0x000000) 139 | gfx.drawstr(message, align or (HCENTER | VCENTER), gfx.w, gfx.h) 140 | end 141 | 142 | function mouseEvents() 143 | if gfx.mouse_x > MARGIN and gfx.mouse_x - MARGIN < w and 144 | gfx.mouse_y > MARGIN and gfx.mouse_y - MARGIN < h then 145 | gfx.setcursor(IDC_SIZENS) 146 | 147 | if gfx.mouse_cap & 1 == 1 and not mouseDownY then 148 | mouseDownY, adjustment = gfx.mouse_y, 0 149 | 150 | if gfx.mouse_x >= w/2 then 151 | adjustmentDir = ADJ_RIGHT 152 | else 153 | adjustmentDir = ADJ_LEFT 154 | end 155 | end 156 | else 157 | gfx.setcursor(IDC_ARROW) 158 | end 159 | 160 | if gfx.mouse_cap & 1 == 1 then 161 | if mouseDownY then 162 | adjustment = (mouseDownY - gfx.mouse_y) * state.yscale 163 | end 164 | elseif adjustment then 165 | if adjustment ~= 0 then 166 | applyAdjustment() 167 | end 168 | 169 | clearAdjustment() 170 | end 171 | end 172 | 173 | function timeToPixel(time) 174 | return MARGIN + (time - state.firstPos) / state.xscale 175 | end 176 | 177 | function valueToPixel(value) 178 | return MARGIN + h - (value - state.minValue) / state.yscale 179 | end 180 | 181 | function drawPoint(i, point, nextPoint, adjust) 182 | local value = adjust and adjustValue(point) or point[3] 183 | local x = timeToPixel(point[2]) 184 | local y = valueToPixel(value) 185 | gfx.circle(x, y, 3, true) 186 | 187 | if nextPoint then 188 | local nextX = timeToPixel(nextPoint[2]) 189 | local nextY = valueToPixel(adjust and adjustValue(nextPoint) or nextPoint[3]) 190 | 191 | gfx.line(x, y, nextX, nextY) 192 | if not adjust then 193 | gfx.a = 0.5 194 | gfx.triangle(x, y, x, h + MARGIN, nextX, h + MARGIN, nextX-1, nextY) 195 | gfx.a = 1 196 | end 197 | end 198 | 199 | local humanValue = reaper.Envelope_FormatValue(state.env, value) 200 | gfx.x, gfx.y = x + 3, y + 3 201 | 202 | local strW, strY = gfx.measurestr(humanValue) 203 | if gfx.x + strW > w + MARGIN then 204 | gfx.x = w - strW + MARGIN 205 | end 206 | if gfx.y + strY > h + MARGIN then 207 | gfx.y = h - strY + MARGIN 208 | end 209 | 210 | gfx.drawstr(humanValue) 211 | end 212 | 213 | function drawEnvelope() 214 | setColor(0x3b7b39) 215 | for i, point in ipairs(state.selectedPoints) do 216 | drawPoint(i, point, state.selectedPoints[i + 1], false) 217 | end 218 | 219 | if adjustment then 220 | setColor(0x7600ff) 221 | for i, point in ipairs(state.selectedPoints) do 222 | drawPoint(i, point, state.selectedPoints[i + 1], true) 223 | end 224 | end 225 | end 226 | 227 | function loop() 228 | if gfx.getchar() < 0 then 229 | gfx.quit() 230 | return 231 | end 232 | 233 | w, h = gfx.w - MARGIN * 2, gfx.h - MARGIN * 2 234 | 235 | gfx.clear = 0xffffff 236 | 237 | setColor(0xe1e1e1) 238 | gfx.rect(MARGIN + 1, MARGIN + 1, w - 2, h - 2) 239 | 240 | loadState() 241 | 242 | if state.error then 243 | drawMessage(state.error) 244 | else 245 | mouseEvents() 246 | drawEnvelope() 247 | end 248 | 249 | gfx.r, gfx.g, gfx.b, gfx.a = 0, 0, 0, 1 250 | gfx.rect(MARGIN, MARGIN, w, h, false) 251 | 252 | gfx.update() 253 | reaper.defer(loop) 254 | end 255 | 256 | function previousWindowState() 257 | local state = tostring(reaper.GetExtState(EXT_SECTION, EXT_WINDOW_STATE)) 258 | return state:match("^(%d+) (%d+) (%d+) (-?%d+) (-?%d+)$") 259 | end 260 | 261 | function saveWindowState() 262 | local dockState, xpos, ypos = gfx.dock(-1, 0, 0, 0, 0) 263 | local w, h = gfx.w, gfx.h 264 | if dockState > 0 then 265 | w, h = previousWindowState() 266 | end 267 | 268 | reaper.SetExtState(EXT_SECTION, EXT_WINDOW_STATE, 269 | string.format("%d %d %d %d %d", w, h, dockState, xpos, ypos), true) 270 | end 271 | 272 | state = {} 273 | scriptName = ({reaper.get_action_context()})[2]:match("([^/\\_]+)%.lua$") 274 | 275 | local w, h, dockState, x, y = previousWindowState() 276 | 277 | if w then 278 | gfx.init(scriptName, w, h, dockState, x, y) 279 | else 280 | gfx.init(scriptName, 600, 100) 281 | end 282 | 283 | if reaper.GetAppVersion():match('OSX') then 284 | gfx.setfont(1, 'sans-serif', 12) 285 | else 286 | gfx.setfont(1, 'sans-serif', 15) 287 | end 288 | 289 | reaper.atexit(saveWindowState) 290 | loop() 291 | -------------------------------------------------------------------------------- /cfillion_Monitoring FX bypass bundle.lua: -------------------------------------------------------------------------------- 1 | local UNDO_STATE_FX = 2 -- track/master fx 2 | 3 | local name = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 4 | local mode = ({Bypass=false, Unbypass=true})[name:match('^(%w+)')] 5 | local fxIndex = tonumber(name:match("FX (%d+)")) 6 | 7 | if fxIndex then 8 | fxIndex = 0x1000000 + (fxIndex - 1) 9 | else 10 | error('could not extract slot from filename') 11 | end 12 | 13 | reaper.Undo_BeginBlock() 14 | 15 | local master = reaper.GetMasterTrack() 16 | 17 | if mode == nil then -- toggle 18 | mode = not reaper.TrackFX_GetEnabled(master, fxIndex) 19 | end 20 | 21 | reaper.TrackFX_SetEnabled(master, fxIndex, mode) 22 | 23 | reaper.Undo_EndBlock(name, UNDO_STATE_FX) 24 | 25 | -------------------------------------------------------------------------------- /cfillion_Move right edge of selected automation items to end of time selection.lua: -------------------------------------------------------------------------------- 1 | -- @description Move left/right edge of selected automation items to start/end of time selection 2 | 3 | local UNDO_STATE_TRACKCFG = 1 4 | 5 | local script_name = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 6 | local right_edge = script_name:match('right edge') 7 | 8 | reaper.defer(function() end) 9 | 10 | local tstart, tend = reaper.GetSet_LoopTimeRange(false, false, 0, 0, false) 11 | if tstart == tend then return end 12 | 13 | local env = reaper.GetSelectedTrackEnvelope(nil) 14 | if not env then return end 15 | 16 | local bucket = {} 17 | 18 | for i=0,reaper.CountAutomationItems(env)-1 do 19 | local selected = 1 == reaper.GetSetAutomationItemInfo(env, i, 'D_UISEL', 0, false) 20 | local startTime = reaper.GetSetAutomationItemInfo(env, i, 'D_POSITION', 0, false) 21 | local length = reaper.GetSetAutomationItemInfo(env, i, 'D_LENGTH', 0, false) 22 | 23 | if selected and startTime < tend then 24 | if right_edge then 25 | table.insert(bucket, {id=i, len=tend - startTime}) 26 | else 27 | local offset = startTime - tstart 28 | table.insert(bucket, {id=i, pos=tstart, len=length + offset, shift=offset}) 29 | end 30 | end 31 | end 32 | 33 | if #bucket < 1 then return end 34 | 35 | reaper.Undo_BeginBlock() 36 | 37 | for _,ai in ipairs(bucket) do 38 | if ai.pos then 39 | reaper.GetSetAutomationItemInfo(env, ai.id, 'D_POSITION', ai.pos, true) 40 | end 41 | 42 | reaper.GetSetAutomationItemInfo(env, ai.id, 'D_LENGTH', ai.len, true) 43 | 44 | if ai.shift then 45 | local off = reaper.GetSetAutomationItemInfo(env, ai.id, 'D_STARTOFFS', 0, false) 46 | reaper.GetSetAutomationItemInfo(env, ai.id, 'D_STARTOFFS', off - ai.shift, true) 47 | end 48 | end 49 | 50 | reaper.Undo_EndBlock(script_name, UNDO_STATE_TRACKCFG) 51 | -------------------------------------------------------------------------------- /cfillion_Move selected items to a FIPM lane.lua: -------------------------------------------------------------------------------- 1 | -- @description Move selected items to a FIPM lane 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @website 5 | -- cfillion.ca https://cfillion.ca 6 | -- Request Thread https://forum.cockos.com/showthread.php?t=200756 7 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD&item_name=FIPM+-+Move+selected+items+to+a+given+lane 8 | -- @about 9 | -- This script provides a set of two actions for moving the selected media 10 | -- items to the top or bottom FIPM lane of the track. Each lane is half of the 11 | -- track's height. 12 | -- 13 | -- FIPM (Free Item Positioning Mode) is enabled on the tracks as required. 14 | -- @provides 15 | -- [main] . > cfillion_Move selected items to top FIPM lane (half track height).lua 16 | -- [main] . > cfillion_Move selected items to bottom FIPM lane (half track height).lua 17 | 18 | local script_name = ({reaper.get_action_context()})[2]:match('([^/\\_]+).lua$') 19 | 20 | reaper.Undo_BeginBlock() 21 | 22 | for ii=0,reaper.CountSelectedMediaItems(0)-1 do 23 | local item = reaper.GetSelectedMediaItem(0, ii) 24 | local track = reaper.GetMediaItemTrack(item) 25 | 26 | if reaper.GetMediaTrackInfo_Value(track, 'B_FREEMODE') ~= 1 then 27 | reaper.SetMediaTrackInfo_Value(track, 'B_FREEMODE', 1) 28 | end 29 | 30 | local y, h = 0, 0.5 31 | if script_name:match('bottom') then 32 | y = 0.5 33 | end 34 | 35 | reaper.SetMediaItemInfo_Value(item, 'F_FREEMODE_Y', y) 36 | reaper.SetMediaItemInfo_Value(item, 'F_FREEMODE_H', h) 37 | end 38 | 39 | reaper.UpdateTimeline() 40 | reaper.Undo_EndBlock(script_name, -1) 41 | -------------------------------------------------------------------------------- /cfillion_Move selected items to one FIPM lane per color.lua: -------------------------------------------------------------------------------- 1 | -- @description Move selected items to one FIPM lane per color 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @website 5 | -- cfillion.ca https://cfillion.ca 6 | -- Request Post https://forum.cockos.com/showthread.php?p=1951990 7 | -- @screenshot https://i.imgur.com/uGN9Qy5.gif 8 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD 9 | -- @provides 10 | -- . 11 | -- . > cfillion_Move selected items to one FIPM lane per color (preserve height).lua 12 | -- @about 13 | -- This script moves selected items to different FIPM lanes according to the its color. 14 | 15 | local scriptName = ({reaper.get_action_context()})[2]:match('([^/\\_]+)%.lua$') 16 | local preserveHeight = scriptName:match('preserve') 17 | 18 | function normalize(color) 19 | -- ensure OS-independent color encoding (so that the lane order is always the same) 20 | local r, g, b = reaper.ColorFromNative(color) 21 | return r<<16 | g<<8 | b 22 | end 23 | 24 | function makeLanes(items) 25 | local colors = {} 26 | for color, _ in pairs(items) do 27 | table.insert(colors, color) 28 | end 29 | table.sort(colors) 30 | 31 | local lanes = {} 32 | for _, color in ipairs(colors) do 33 | table.insert(lanes, items[color]) 34 | end 35 | return lanes 36 | end 37 | 38 | local tracks = {} 39 | 40 | for ii=0,reaper.CountSelectedMediaItems(0)-1 do 41 | local item = reaper.GetSelectedMediaItem(0, ii) 42 | local track = reaper.GetMediaItemTrack(item) 43 | 44 | if not tracks[track] then 45 | tracks[track] = {} 46 | 47 | if reaper.GetMediaTrackInfo_Value(track, 'B_FREEMODE') ~= 1 then 48 | reaper.SetMediaTrackInfo_Value(track, 'B_FREEMODE', 1) 49 | end 50 | end 51 | 52 | local color = normalize(reaper.GetMediaItemInfo_Value(item, 'I_CUSTOMCOLOR')) 53 | 54 | if not tracks[track][color] then 55 | tracks[track][color] = {} 56 | end 57 | 58 | table.insert(tracks[track][color], item) 59 | end 60 | 61 | local bucket = {} 62 | 63 | for _, items in pairs(tracks) do 64 | local lanes = makeLanes(items) 65 | 66 | if #lanes > 0 then 67 | table.insert(bucket, lanes) 68 | end 69 | end 70 | 71 | if #bucket < 1 then 72 | reaper.defer(function() end) 73 | return 74 | end 75 | 76 | reaper.Undo_BeginBlock() 77 | 78 | for _, lanes in ipairs(bucket) do 79 | local laneHeight = 1 / #lanes 80 | local y = 0 81 | 82 | for lane, items in ipairs(lanes) do 83 | for _, item in ipairs(items) do 84 | reaper.SetMediaItemInfo_Value(item, 'F_FREEMODE_Y', y) 85 | 86 | if not preserveHeight then 87 | reaper.SetMediaItemInfo_Value(item, 'F_FREEMODE_H', laneHeight) 88 | end 89 | end 90 | 91 | if preserveHeight then 92 | local largest = 0 93 | 94 | for _, item in ipairs(items) do 95 | largest = math.max(largest, reaper.GetMediaItemInfo_Value(item, 'F_FREEMODE_H')) 96 | end 97 | 98 | y = y + largest 99 | else 100 | y = laneHeight * lane 101 | end 102 | end 103 | end 104 | 105 | reaper.UpdateTimeline() 106 | reaper.Undo_EndBlock(scriptName, -1) 107 | -------------------------------------------------------------------------------- /cfillion_Normalize peaks display gain.lua: -------------------------------------------------------------------------------- 1 | -- @description Normalize peaks display gain 2 | -- @version 1.0.1 3 | -- @changelog Mark project as modified after adjusting the peaks gain 4 | -- @author cfillion 5 | -- @website cfillion.ca https://cfillion.ca 6 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD&item_name=ReaScript%3A+Normalize+peaks+display+gian 7 | -- @screenshot https://i.imgur.com/UA2iB5m.gif 8 | -- @metapackage 9 | -- @provides 10 | -- [main] . > cfillion_Normalize peaks display gain (scan all items).lua 11 | -- [main] . > cfillion_Normalize peaks display gain (scan selected items).lua 12 | -- [main] . > cfillion_Normalize peaks display gain (scan selected tracks).lua 13 | -- @about 14 | -- This package provides actions to automatically adjust the per-project 15 | -- peaks display gain setting depending on the item contents. 16 | -- 17 | -- It can scan every items in the project, items on selected tracks, or only 18 | -- selected items. 19 | 20 | local function find(t, e) 21 | for k, v in ipairs(t) do 22 | if v == e then return k end 23 | end 24 | 25 | return nil 26 | end 27 | 28 | -- arguments: the getter function followed by the arguments it takes 29 | -- x is replaced with the index of the thing to get 30 | local x = {} 31 | local function iteratize(...) 32 | local args = {...} 33 | a = args 34 | local func = table.remove(args, 1) 35 | local indexPos = assert(find(args, x), 'cannot find x in the iterator arguments') 36 | 37 | local i = -1 38 | return function() 39 | i = i + 1 40 | args[indexPos] = i 41 | return func(table.unpack(args)) 42 | end 43 | end 44 | 45 | local getItem = { 46 | ['all items']=iteratize(reaper.GetMediaItem, 0, x), 47 | ['selected items']=iteratize(reaper.GetSelectedMediaItem, 0, x), 48 | ['selected tracks']=(function() 49 | local selTrackIter = iteratize(reaper.GetSelectedTrack, 0, x) 50 | local track, itemIter 51 | 52 | local function nextTrack() 53 | track = selTrackIter() 54 | itemIter = track and iteratize(reaper.GetTrackMediaItem, track, x) 55 | end 56 | 57 | nextTrack() 58 | 59 | return function() 60 | while track do 61 | local item = itemIter() 62 | 63 | if item then 64 | return item 65 | else 66 | nextTrack() 67 | end 68 | end 69 | end 70 | end)(), 71 | } 72 | 73 | -- config vars can't be undone so don't create a useless undo point... 74 | reaper.defer(function() end) 75 | 76 | local mode, gainFactor = ({reaper.get_action_context()})[2]:match("%(scan (.-)%)%.lua$") 77 | 78 | for item in getItem[mode] do 79 | local maxPeak = -reaper.NF_GetMediaItemMaxPeak(item) 80 | local itemFactor = 10 ^ (maxPeak / 20) 81 | 82 | if not gainFactor or gainFactor > itemFactor then 83 | gainFactor = itemFactor 84 | end 85 | end 86 | 87 | if gainFactor then 88 | gainFactor = math.max(1, gainFactor) 89 | reaper.SNM_SetDoubleConfigVar('projpeaksgain', gainFactor) 90 | reaper.UpdateArrange() 91 | reaper.MarkProjectDirty(0) 92 | end 93 | -------------------------------------------------------------------------------- /cfillion_Nudge start or end of regions in time selection.lua: -------------------------------------------------------------------------------- 1 | -- @description Nudge start or end of regions in time selection 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @donation https://www.paypal.me/cfillion 5 | -- @link Forum Thread https://forum.cockos.com/showthread.php?t=186689 6 | 7 | local TITLE = "Nudge start or end of regions in time selection" 8 | local UNDO_STATE_MISCCFG = 8 9 | 10 | local _, csv = reaper.GetUserInputs(TITLE, 2, 11 | "Nudge region start by:,Nudge region end by:", "0.0,0.0") 12 | local start_offset, end_offset = csv:match("^([^,]+),([^,]+)$") 13 | start_offset, end_offset = tonumber(start_offset), tonumber(end_offset) 14 | 15 | if not start_offset or not end_offset or start_offset == 0 or end_offset == 0 then 16 | return reaper.defer(function() end) -- no undo point 17 | end 18 | 19 | local next_index = 0 20 | local ts_from, ts_to = reaper.GetSet_LoopTimeRange(false, false, 0, 0, false) 21 | 22 | reaper.Undo_BeginBlock() 23 | 24 | while true do 25 | -- reg = {retval, isrgn, pos, rgnend, name, markrgnindexnumber} 26 | local reg = {reaper.EnumProjectMarkers(next_index)} 27 | 28 | next_index = reg[1] 29 | if next_index == 0 then break end 30 | 31 | local withinTS = ts_to == 0 or (reg[3] >= ts_from and reg[4] <= ts_to) 32 | if reg[2] and withinTS then -- it's a region 33 | reaper.SetProjectMarker(reg[6], reg[2], 34 | reg[3] + start_offset, reg[4] + end_offset, reg[5]) 35 | end 36 | end 37 | 38 | reaper.Undo_EndBlock(TITLE, UNDO_STATE_MISCCFG) 39 | -------------------------------------------------------------------------------- /cfillion_Project underrun monitor (xrun).lua: -------------------------------------------------------------------------------- 1 | -- @description Project underrun monitor (xrun) 2 | -- @version 2.1.1 3 | -- @author cfillion 4 | -- @changelog Internal code cleanup 5 | -- @website 6 | -- cfillion.ca https://cfillion.ca 7 | -- Request Thread https://forum.cockos.com/showthread.php?p=1942953 8 | -- @screenshot https://i.imgur.com/DBHf0w0.gif 9 | -- @donate https://reapack.com/donate 10 | -- @about 11 | -- # Project underrun monitor 12 | -- 13 | -- This script keeps track of the project time where an audio or media buffer 14 | -- underrun occured. Markers can optionally be created. The reported time and 15 | -- marker position accuracy is limited by the polling speed of ReaScripts 16 | -- which is around 30Hz. 17 | 18 | local SCRIPT_NAME = select(2, reaper.get_action_context()):match('([^/\\_]+)%.lua$') 19 | 20 | if not reaper.ImGui_GetBuiltinPath then 21 | reaper.MB('This script requires ReaImGui. \z 22 | Install it from ReaPack > Browse packages.', SCRIPT_NAME, 0) 23 | return 24 | end 25 | 26 | package.path = reaper.ImGui_GetBuiltinPath() .. '/?.lua' 27 | local ImGui = require 'imgui' '0.9' 28 | 29 | local EXT_SECTION = 'cfillion_underrun_monitor' 30 | local EXT_MARKER_TYPE = 'marker_type' 31 | local EXT_MARKER_WHEN = 'marker_when' 32 | 33 | local FLT_MIN, FLT_MAX = ImGui.NumericLimits_Float() 34 | 35 | local AUDIO_XRUN = 1 36 | local MEDIA_XRUN = 2 37 | 38 | local KIND_MAP = { 39 | [AUDIO_XRUN] = { 40 | display_name = 'Audio', 41 | marker_name = 'audio xrun', 42 | marker_color = ImGui.ColorConvertNative(0x1ff0000), 43 | }, 44 | [MEDIA_XRUN] = { 45 | display_name = 'Media', 46 | marker_name = 'media xrun', 47 | marker_color = ImGui.ColorConvertNative(0x1ffff00), 48 | } 49 | } 50 | 51 | local DEFAULT_SETTINGS = { 52 | [EXT_MARKER_TYPE] = AUDIO_XRUN|MEDIA_XRUN, 53 | [EXT_MARKER_WHEN] = 4, 54 | } 55 | 56 | local MARKER_TYPE_MENU = { 57 | {str = '(off)', val = 0 }, 58 | {str = 'any', val = AUDIO_XRUN|MEDIA_XRUN }, 59 | {str = 'audio', val = AUDIO_XRUN }, 60 | {str = 'media', val = MEDIA_XRUN }, 61 | } 62 | 63 | local MARKER_WHEN_MENU = { 64 | { str = 'play or record', val = 1|4 }, 65 | { str = 'playing', val = 1 }, 66 | { str = 'recording ', val = 4 }, 67 | } 68 | local EVENT_LOG_WAS_AT_BOTTOM = 1 69 | local EVENT_LOG_SCROLL_TO_BOTTOM = 2 70 | 71 | local prev_audio, prev_media = reaper.GetUnderrunTime() 72 | local last_project 73 | local ctx, clipper 74 | local event_log, event_log_flags, event_log_sel = {}, EVENT_LOG_WAS_AT_BOTTOM 75 | 76 | local xruns = { 77 | [AUDIO_XRUN] = { count = 0, position = nil }, 78 | [MEDIA_XRUN] = { count = 0, position = nil }, 79 | } 80 | 81 | local ctx = ImGui.CreateContext(SCRIPT_NAME) 82 | 83 | local font = ImGui.CreateFont('sans-serif', 84 | reaper.GetAppVersion():match('OSX') and 12 or 14) 85 | ImGui.Attach(ctx, font) 86 | 87 | local function position() 88 | if reaper.GetPlayState() & 1 == 0 then 89 | return reaper.GetCursorPosition() 90 | else 91 | return reaper.GetPlayPosition2() 92 | end 93 | end 94 | 95 | local function markerSettings() 96 | local playbackState = reaper.GetPlayState() 97 | local markerWhen = tonumber(reaper.GetExtState(EXT_SECTION, EXT_MARKER_WHEN)) 98 | 99 | if playbackState & markerWhen ~= 0 then 100 | return tonumber(reaper.GetExtState(EXT_SECTION, EXT_MARKER_TYPE)) 101 | else 102 | return 0 103 | end 104 | end 105 | 106 | local function addXrun(kind) 107 | local xrun = xruns[kind] 108 | xrun.count = xrun.count + 1 109 | xrun.position = position() 110 | 111 | local kind_info = KIND_MAP[kind] 112 | if markerSettings() & kind ~= 0 then 113 | reaper.AddProjectMarker2(nil, false, xrun.position, 0, 114 | kind_info.marker_name, -1, kind_info.marker_color) 115 | end 116 | 117 | event_log[#event_log + 1] = { kind=kind, position=xrun.position, time=os.time() } 118 | if event_log_flags & EVENT_LOG_WAS_AT_BOTTOM ~= 0 then 119 | event_log_flags = event_log_flags | EVENT_LOG_SCROLL_TO_BOTTOM 120 | end 121 | end 122 | 123 | local function probeUnderruns() 124 | local audio_xrun, media_xrun, curtime = reaper.GetUnderrunTime() 125 | 126 | if audio_xrun > 0 and audio_xrun ~= prev_audio then 127 | prev_audio = audio_xrun 128 | addXrun(AUDIO_XRUN) 129 | end 130 | 131 | if media_xrun > 0 and media_xrun ~= prev_media then 132 | prev_media = media_xrun 133 | addXrun(MEDIA_XRUN) 134 | end 135 | end 136 | 137 | local function eraseMarkers(kind) 138 | if not last_project or not reaper.ValidatePtr(last_project, 'ReaProject*') then return end 139 | 140 | local index = 0 141 | 142 | -- integer retval, boolean isrgn, number pos, number rgnend, string name, number markrgnindexnumber 143 | while true do 144 | local marker = {reaper.EnumProjectMarkers2(last_project, index)} 145 | if marker[1] < 1 then break end 146 | 147 | if not marker[2] and marker[5] == kind.marker_name then 148 | reaper.DeleteProjectMarkerByIndex(last_project, index) 149 | else 150 | index = index + 1 151 | end 152 | end 153 | end 154 | 155 | local function reset(xrunType) 156 | for kind, info in pairs(KIND_MAP) do 157 | if xrunType & kind ~= 0 then 158 | xruns[kind].position = nil 159 | eraseMarkers(info) 160 | end 161 | end 162 | end 163 | 164 | local function detectProjectChange() 165 | local current_project = reaper.EnumProjects(-1, '') 166 | 167 | if last_project ~= current_project then 168 | reset(AUDIO_XRUN | MEDIA_XRUN) 169 | last_project = current_project 170 | end 171 | end 172 | 173 | local function formatPosition(time) 174 | if time then 175 | return reaper.format_timestr(time, '') 176 | else 177 | return '(never)' 178 | end 179 | end 180 | 181 | local function combo(key, choices) 182 | local value = tonumber(reaper.GetExtState(EXT_SECTION, key)) 183 | local label = '' 184 | 185 | for _, choice in ipairs(choices) do 186 | if value == choice.val then 187 | label = choice.str 188 | break 189 | end 190 | end 191 | 192 | if ImGui.BeginCombo(ctx, '##' .. key, label) then 193 | for _, choice in ipairs(choices) do 194 | if ImGui.Selectable(ctx, choice.str, value == choice.val) then 195 | reaper.SetExtState(EXT_SECTION, key, choice.val, true) 196 | end 197 | end 198 | ImGui.EndCombo(ctx) 199 | end 200 | end 201 | 202 | local function drawXrun(name, kind) 203 | local time = xruns[kind].position 204 | local disabledCursor = function() 205 | if not time and ImGui.IsItemHovered(ctx) then 206 | ImGui.SetMouseCursor(ctx, ImGui.MouseCursor_NotAllowed) 207 | end 208 | end 209 | 210 | ImGui.PushID(ctx, kind) 211 | ImGui.AlignTextToFramePadding(ctx) 212 | ImGui.Text(ctx, ('Last %s xrun position:'):format(name)) 213 | ImGui.SameLine(ctx, 179) 214 | ImGui.SetNextItemWidth(ctx, 115) 215 | ImGui.InputText(ctx, '##time', formatPosition(time), ImGui.InputTextFlags_ReadOnly) 216 | ImGui.SameLine(ctx) 217 | if not time then 218 | local frameBg = ImGui.GetStyleColor(ctx, ImGui.Col_FrameBg) 219 | ImGui.PushStyleColor(ctx, ImGui.Col_Button, frameBg) 220 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonActive, frameBg) 221 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonHovered, frameBg) 222 | end 223 | if ImGui.Button(ctx, 'Jump') and time then reaper.SetEditCurPos(time, true, false) end 224 | disabledCursor() 225 | ImGui.SameLine(ctx) 226 | if ImGui.Button(ctx, 'Reset') and time then reset(kind) end 227 | disabledCursor() 228 | if not time then 229 | ImGui.PopStyleColor(ctx, 3) 230 | end 231 | ImGui.PopID(ctx) 232 | end 233 | 234 | local function drawEventLog() 235 | local table_flags = ImGui.TableFlags_Borders | 236 | ImGui.TableFlags_RowBg | 237 | ImGui.TableFlags_SizingStretchSame | 238 | ImGui.TableFlags_ScrollY 239 | if not ImGui.BeginTable(ctx, 'Event log', 3, table_flags, 0, 100) then return end 240 | ImGui.TableSetupScrollFreeze(ctx, 0, 1) 241 | ImGui.TableSetupColumn(ctx, 'Time') 242 | ImGui.TableSetupColumn(ctx, 'Kind') 243 | ImGui.TableSetupColumn(ctx, 'Position') 244 | ImGui.TableHeadersRow(ctx) 245 | 246 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_ItemSpacing, 4, 4) -- selectable padding 247 | 248 | if not ImGui.ValidatePtr(clipper, 'ImGui_ListClipper*') then 249 | clipper = ImGui.CreateListClipper(ctx) 250 | end 251 | ImGui.ListClipper_Begin(clipper, #event_log) 252 | while ImGui.ListClipper_Step(clipper) do 253 | local display_start, display_end = ImGui.ListClipper_GetDisplayRange(clipper) 254 | for i = display_start, display_end - 1 do 255 | ImGui.PushID(ctx, i) 256 | local entry = event_log[i + 1] 257 | ImGui.TableNextRow(ctx) 258 | ImGui.TableNextColumn(ctx) 259 | ImGui.Text(ctx, os.date('%Y-%m-%d %H:%M:%S', entry.time)) 260 | ImGui.TableNextColumn(ctx) 261 | local kinds = {} 262 | for flag, info in pairs(KIND_MAP) do 263 | if entry.kind & flag ~= 0 then kinds[#kinds + 1] = info.display_name end 264 | end 265 | if ImGui.Selectable(ctx, table.concat(kinds, ', '), event_log_sel == i, 266 | ImGui.SelectableFlags_SpanAllColumns) then 267 | event_log_sel = i 268 | reaper.SetEditCurPos(entry.position, true, false) 269 | end 270 | ImGui.TableNextColumn(ctx) 271 | ImGui.Text(ctx, formatPosition(entry.position)) 272 | ImGui.PopID(ctx) 273 | end 274 | end 275 | 276 | if event_log_flags & EVENT_LOG_SCROLL_TO_BOTTOM ~= 0 then 277 | ImGui.SetScrollHereY(ctx) 278 | event_log_flags = event_log_flags & ~EVENT_LOG_SCROLL_TO_BOTTOM 279 | end 280 | if ImGui.GetScrollY(ctx) >= ImGui.GetScrollMaxY(ctx) then 281 | event_log_flags = event_log_flags | EVENT_LOG_WAS_AT_BOTTOM 282 | else 283 | event_log_flags = event_log_flags & ~EVENT_LOG_WAS_AT_BOTTOM 284 | end 285 | 286 | ImGui.PopStyleVar(ctx) 287 | ImGui.EndTable(ctx) 288 | end 289 | 290 | local function draw() 291 | drawXrun('audio', AUDIO_XRUN) 292 | drawXrun('media', MEDIA_XRUN) 293 | ImGui.Spacing(ctx) 294 | 295 | ImGui.AlignTextToFramePadding(ctx) 296 | ImGui.Text(ctx, 'Create markers on') 297 | ImGui.SameLine(ctx) 298 | ImGui.SetNextItemWidth(ctx, 80) 299 | combo(EXT_MARKER_TYPE, MARKER_TYPE_MENU) 300 | ImGui.SameLine(ctx) 301 | ImGui.Text(ctx, 'xruns, when') 302 | ImGui.SameLine(ctx) 303 | ImGui.SetNextItemWidth(ctx, -FLT_MIN) 304 | combo(EXT_MARKER_WHEN, MARKER_WHEN_MENU) 305 | 306 | if ImGui.CollapsingHeader(ctx, 'Event log') then 307 | drawEventLog() 308 | 309 | local no_events = #event_log < 1 310 | if no_events then 311 | ImGui.BeginDisabled(ctx) 312 | end 313 | if ImGui.Button(ctx, 'Clear') then 314 | event_log, event_log_flags, event_log_sel = {}, EVENT_LOG_WAS_AT_BOTTOM 315 | for k, v in pairs(xruns) do xruns[k].count = 0 end 316 | end 317 | ImGui.SameLine(ctx) 318 | if no_events then 319 | ImGui.Text(ctx, 'No audio or media xrun events recorded') 320 | ImGui.EndDisabled(ctx) 321 | else 322 | ImGui.Text(ctx, ('%d xrun events recorded (%d audio, %d media)') 323 | :format(#event_log, xruns[AUDIO_XRUN].count, xruns[MEDIA_XRUN].count)) 324 | end 325 | end 326 | end 327 | 328 | local function about() 329 | local owner = reaper.ReaPack_GetOwner((select(2, reaper.get_action_context()))) 330 | 331 | if not owner then 332 | reaper.MB(( 333 | 'This feature is unavailable because "%s" \z 334 | was not installed using ReaPack.' 335 | ):format(SCRIPT_NAME), SCRIPT_NAME, 0) 336 | return 337 | end 338 | 339 | reaper.ReaPack_AboutInstalledPackage(owner) 340 | reaper.ReaPack_FreeEntry(owner) 341 | end 342 | 343 | local function contextMenu() 344 | local dock_id = ImGui.GetWindowDockID(ctx) 345 | local popup_flags = ImGui.PopupFlags_MouseButtonRight | 346 | ImGui.PopupFlags_NoOpenOverItems 347 | if not ImGui.BeginPopupContextWindow(ctx, nil, popup_flags) then return end 348 | if ImGui.BeginMenu(ctx, 'Dock window') then 349 | if ImGui.MenuItem(ctx, 'Floating', nil, dock_id == 0) then 350 | set_dock_id = 0 351 | end 352 | for i = 0, 15 do 353 | if ImGui.MenuItem(ctx, ('Docker %d'):format(i + 1), nil, dock_id == ~i) then 354 | set_dock_id = ~i 355 | end 356 | end 357 | ImGui.EndMenu(ctx) 358 | end 359 | ImGui.Separator(ctx) 360 | if ImGui.MenuItem(ctx, 'About/help', 'F1', false, reaper.ReaPack_GetOwner ~= nil) then 361 | about() 362 | end 363 | if ImGui.MenuItem(ctx, 'Close', 'Escape') then 364 | exit = true 365 | end 366 | ImGui.EndPopup(ctx) 367 | end 368 | 369 | function loop() 370 | detectProjectChange() 371 | probeUnderruns() 372 | 373 | ImGui.PushFont(ctx, font) 374 | ImGui.PushStyleColor(ctx, ImGui.Col_ChildBg, 0xffffffff) 375 | ImGui.PushStyleColor(ctx, ImGui.Col_WindowBg, 0xffffffff) 376 | 377 | --ImGui.SetNextWindowSize(ctx, 412, 140) 378 | if set_dock_id then 379 | ImGui.SetNextWindowDockID(ctx, set_dock_id) 380 | set_dock_id = nil 381 | end 382 | local visible, open = ImGui.Begin(ctx, SCRIPT_NAME, true, 383 | ImGui.WindowFlags_AlwaysAutoResize) 384 | if visible then 385 | ImGui.PushStyleColor(ctx, ImGui.Col_Border, 0x2a2a2aff) 386 | ImGui.PushStyleColor(ctx, ImGui.Col_Button, 0xdcdcdcff) 387 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonActive, 0x787878ff) 388 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonHovered, 0xdcdcdcff) 389 | ImGui.PushStyleColor(ctx, ImGui.Col_FrameBg, 0xffffffff) 390 | ImGui.PushStyleColor(ctx, ImGui.Col_FrameBgHovered, 0x96afe1ff) 391 | ImGui.PushStyleColor(ctx, ImGui.Col_Header, 0x96afe180) 392 | ImGui.PushStyleColor(ctx, ImGui.Col_HeaderHovered, 0x96afe1ff) 393 | ImGui.PushStyleColor(ctx, ImGui.Col_PopupBg, 0xffffffff) 394 | ImGui.PushStyleColor(ctx, ImGui.Col_ScrollbarBg, 0xacacacff) 395 | ImGui.PushStyleColor(ctx, ImGui.Col_TableBorderLight, 0x999999ff) 396 | ImGui.PushStyleColor(ctx, ImGui.Col_TableHeaderBg, 0xdcdcdcff) 397 | ImGui.PushStyleColor(ctx, ImGui.Col_Text, 0x2a2a2aff) 398 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_FrameBorderSize, 1) 399 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_FramePadding, 7, 4) 400 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_ItemSpacing, 7, 7) 401 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_ScrollbarSize, 12) 402 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_WindowPadding, 10, 10) 403 | 404 | contextMenu() 405 | draw() 406 | 407 | ImGui.PopStyleVar(ctx, 5) 408 | ImGui.PopStyleColor(ctx, 13) 409 | ImGui.End(ctx) 410 | end 411 | 412 | ImGui.PopStyleColor(ctx, 2) 413 | ImGui.PopFont(ctx) 414 | 415 | if ImGui.IsKeyPressed(ctx, ImGui.Key_F1) then about() end 416 | if ImGui.IsKeyPressed(ctx, ImGui.Key_Escape) or exit then open = false end 417 | 418 | if open then 419 | reaper.defer(loop) 420 | end 421 | end 422 | 423 | for key, default in pairs(DEFAULT_SETTINGS) do 424 | if not reaper.HasExtState(EXT_SECTION, key) then 425 | reaper.SetExtState(EXT_SECTION, key, default, true) 426 | end 427 | end 428 | 429 | reaper.defer(loop) 430 | reaper.atexit(function() reset(AUDIO_XRUN|MEDIA_XRUN) end) 431 | -------------------------------------------------------------------------------- /cfillion_Remove hardware outputs of selected tracks.lua: -------------------------------------------------------------------------------- 1 | -- @description Remove hardware outputs of selected tracks 2 | -- @author cfillion 3 | -- @version 1.0.1 4 | -- @changelog Optimize processing in projects with high track counts 5 | -- @link http://forum.cockos.com/showthread.php?t=189761 6 | -- @donation https://reapack.com/donate 7 | 8 | local SCRIPT_NAME = select(2, reaper.get_action_context()):match('([^/\\_]+).lua$') 9 | local UNDO_STATE_TRACKCFG = 1 10 | local HARDWARE_OUT = 1 -- > 0 11 | 12 | local seltracks = reaper.CountSelectedTracks() 13 | if seltracks < 1 then return reaper.defer(function() end) end 14 | 15 | reaper.PreventUIRefresh(1) 16 | reaper.Undo_BeginBlock() 17 | 18 | for ti = 0, seltracks - 1 do 19 | local track = reaper.GetSelectedTrack(nil, ti) 20 | for si = 0, reaper.GetTrackNumSends(track, HARDWARE_OUT) - 1 do 21 | reaper.RemoveTrackSend(track, HARDWARE_OUT, 0) 22 | end 23 | end 24 | 25 | reaper.Undo_EndBlock(SCRIPT_NAME, UNDO_STATE_TRACKCFG) 26 | reaper.PreventUIRefresh(-1) 27 | -------------------------------------------------------------------------------- /cfillion_Rename selected tracks from clipboard lines.lua: -------------------------------------------------------------------------------- 1 | -- @description Rename selected tracks from clipboard lines 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @website 5 | -- cfillion.ca https://cfillion.ca 6 | -- Request Post https://forum.cockos.com/showpost.php?p=2029104 7 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD&item_name=ReaScript%3A+Rename+selected+tracks+from+clipboard+lines 8 | 9 | local script_name = ({reaper.get_action_context()})[2]:match("([^/\\_]+)%.lua$") 10 | local UNDO_STATE_TRACKCFG = 1 11 | local clipboard, index = reaper.CF_GetClipboard(''), 0 12 | 13 | if clipboard:len() < 1 or reaper.CountSelectedTracks(0) < 1 then 14 | reaper.defer(function() end) -- no undo point 15 | end 16 | 17 | reaper.Undo_BeginBlock() 18 | 19 | for line in clipboard:gmatch("([^\r\n]*)[\r\n]*") do 20 | local track = reaper.GetSelectedTrack(0, index) 21 | if not track then break end 22 | 23 | reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', line, true) 24 | index = index + 1 25 | end 26 | 27 | reaper.Undo_EndBlock(script_name, UNDO_STATE_TRACKCFG) 28 | -------------------------------------------------------------------------------- /cfillion_Repair missing JSFX files in current project.lua: -------------------------------------------------------------------------------- 1 | -- @description Repair missing JSFX files in the current project 2 | -- @author cfillion 3 | -- @version 1.0 4 | -- @link https://cfillion.ca 5 | -- @donation https://www.paypal.me/cfillion/10 6 | -- @about 7 | -- # Repair missing JSFX files in the current project 8 | -- 9 | -- This script scans the current project file for broken JSFX references 10 | -- and prompts for the new location of each unique missing JSFX found. 11 | -- 12 | -- A fixed copy of the project file is created (OriginalName-jsfix.RPP). 13 | -- 14 | -- This script supports JSFX located either in the resource folder or 15 | -- in the project directory. 16 | 17 | local global_dir = reaper.GetResourcePath() .. '/Effects/' 18 | local title = ({reaper.get_action_context()})[2]:match('([^/\\_]+).lua$') 19 | local bucket = {} 20 | 21 | -- no undo point for this script 22 | reaper.defer(function() end) 23 | 24 | function file_exists(fn) 25 | local file = io.open(fn, 'rb') 26 | 27 | if file then 28 | io.close(file) 29 | return true 30 | else 31 | return false 32 | end 33 | end 34 | 35 | function saw(jsfx) 36 | for i,entry in ipairs(bucket) do 37 | if jsfx == entry.name then 38 | return true 39 | end 40 | end 41 | 42 | return false 43 | end 44 | 45 | -- http://stackoverflow.com/a/34953646 46 | function escape(pattern) 47 | return pattern:gsub("([^%w])", "%%%1") 48 | end 49 | 50 | if reaper.IsProjectDirty(0) ~= 0 then 51 | local btn = reaper.ShowMessageBox([[Project is modified. Do you want to save changes before repairing missing JSFX files? 52 | Unsaved changes will not be included in the fixed copy.]], title, 3) 53 | 54 | if btn == 6 then 55 | reaper.Main_SaveProject() 56 | elseif btn == 2 then 57 | return 58 | end 59 | end 60 | 61 | local _, project_fn = reaper.EnumProjects(-1, '', 0) 62 | local project_dir = project_fn:match('^(.-)[^/\\]+$') .. 'Effects/' 63 | 64 | local project_io, error = io.open(project_fn, 'rb') 65 | if not project_io then 66 | reaper.ShowMessageBox( 67 | string.format('Could not open project file "%s" (%s).', 68 | project_fn, error), title, 0) 69 | return 70 | end 71 | 72 | local chunk, modified = project_io:read('*all'), false 73 | project_io:close() 74 | 75 | function scan(pattern, start, stop) 76 | local from, to 77 | 78 | while true do 79 | from, to = chunk:find('/" then 87 | dir = project_dir 88 | file = jsfx:sub(11) 89 | else 90 | dir = global_dir 91 | file = jsfx 92 | end 93 | 94 | if not saw(jsfx) and not file_exists(dir .. file) then 95 | table.insert(bucket, {name=jsfx, path=dir, token=token}) 96 | end 97 | end 98 | end 99 | 100 | scan('"[^\n]-"', 2, -2) 101 | scan('[^"\x20]+', 1, -1) 102 | 103 | if #bucket == 0 then 104 | reaper.ShowMessageBox('No missing JSFX were found. Nothing to do!', title, 0) 105 | return 106 | end 107 | 108 | for i,old in ipairs(bucket) do 109 | local title = string.format('Looking for "%s"', old.name) 110 | local ok, new = reaper.GetUserFileNameForRead(old.path, title, '*.jsfx') 111 | 112 | if ok then 113 | new = new:sub(old.path:len() + 1) 114 | if old.path == project_dir then 115 | new = '/' .. new 116 | end 117 | 118 | old_pt = string.format(' cfillion_Select destination tracks of selected tracks sends recursively (background).lua 13 | 14 | -- This file is also used by cfillion_Select source tracks of selected tracks sends recursively.lua 15 | 16 | local scriptName = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 17 | local background = scriptName:match("background") 18 | local destination = scriptName:match("destination") 19 | 20 | local selected = {} 21 | 22 | local function inArray(haystack, match) 23 | for i,item in ipairs(haystack) do 24 | if item == match then 25 | return true 26 | end 27 | end 28 | 29 | return false 30 | end 31 | 32 | local function enumSendTracks(track) 33 | local sendReceive = destination and 0 or -1 34 | local trackType = destination and 1 or 0 35 | local i, max = -1, reaper.GetTrackNumSends(track, sendReceive) - 1 36 | 37 | return function() 38 | while i < max do 39 | i = i + 1 40 | 41 | local muted = reaper.GetTrackSendInfo_Value(track, sendReceive, i, 'B_MUTE') == 1 42 | 43 | if not muted then 44 | return reaper.BR_GetMediaTrackSendInfo_Track( 45 | track, sendReceive, i, trackType) 46 | end 47 | end 48 | end 49 | end 50 | 51 | local function enumDirectChildTracks(track) 52 | local index = reaper.GetMediaTrackInfo_Value(track, 'IP_TRACKNUMBER') 53 | local depth = reaper.GetMediaTrackInfo_Value(track, 'I_FOLDERDEPTH') 54 | local count = reaper.CountTracks(0) 55 | 56 | return function() 57 | while depth > 0 and index < count do 58 | track = reaper.GetTrack(0, index) 59 | 60 | local direct = depth == 1 61 | local parentSend = reaper.GetMediaTrackInfo_Value(track, 'B_MAINSEND') == 1 62 | 63 | index = index + 1 64 | depth = depth + reaper.GetMediaTrackInfo_Value(track, 'I_FOLDERDEPTH') 65 | 66 | if direct and parentSend then 67 | return track 68 | end 69 | end 70 | end 71 | end 72 | 73 | local function highlight(track, select, seenTracks) 74 | if not seenTracks then seenTracks = {} end 75 | if inArray(seenTracks, track) then return end 76 | table.insert(seenTracks, track) 77 | 78 | for target in enumSendTracks(track) do 79 | reaper.SetTrackSelected(target, select) 80 | 81 | if select then 82 | highlight(target, select, seenTracks) 83 | end 84 | end 85 | 86 | if destination then 87 | local parentSend = reaper.GetMediaTrackInfo_Value(track, 'B_MAINSEND') == 1 88 | local parent = parentSend and reaper.GetParentTrack(track) 89 | if parent then 90 | reaper.SetTrackSelected(parent, select) 91 | highlight(parent, select, seenTracks) 92 | end 93 | else 94 | for child in enumDirectChildTracks(track) do 95 | reaper.SetTrackSelected(child, select) 96 | highlight(child, select, seenTracks) 97 | end 98 | end 99 | end 100 | 101 | local function main() 102 | for i=0,reaper.CountSelectedTracks(0)-1 do 103 | local track = reaper.GetSelectedTrack(0, i) 104 | 105 | if not inArray(selected, track) then 106 | table.insert(selected, track) 107 | end 108 | end 109 | 110 | for i,track in ipairs(selected) do 111 | local valid = reaper.ValidatePtr(track, 'MediaTrack*') 112 | local isSelected = valid and reaper.IsTrackSelected(track) 113 | 114 | if isSelected then 115 | highlight(track, true) 116 | else 117 | -- background mode: unselect destination of unselected tracks 118 | table.remove(selected, i) 119 | 120 | if valid then 121 | highlight(track, false) 122 | end 123 | end 124 | end 125 | 126 | reaper.defer(background and main or function() end) 127 | end 128 | 129 | main() 130 | -------------------------------------------------------------------------------- /cfillion_Select source tracks of selected tracks receives recursively.lua: -------------------------------------------------------------------------------- 1 | -- @description Select source tracks of selected tracks receives recursively 2 | -- @version 1.2 3 | -- @author cfillion 4 | -- @changelog 5 | -- Fix a bug causing some tracks to not be selected in a single shot 6 | -- Ignore muted receives 7 | -- Select tracks down the folder hierarchy (obeying parent send) 8 | -- @donation https://www.paypal.me/cfillion 9 | -- @metapackage 10 | -- @provides 11 | -- [main] cfillion_Select destination tracks of selected tracks sends recursively.lua > cfillion_Select source tracks of selected tracks receives recursively.lua 12 | -- [main] cfillion_Select destination tracks of selected tracks sends recursively.lua > cfillion_Select source tracks of selected tracks receives recursively (background).lua 13 | 14 | -- The contents of this script is in `cfillion_Select destination tracks of selected tracks sends recursively.lua`. 15 | -------------------------------------------------------------------------------- /cfillion_Select track FX by name.lua: -------------------------------------------------------------------------------- 1 | -- @description Select track FX by name 2 | -- @author cfillion 3 | -- @version 1.0 4 | -- @provides 5 | -- . 6 | -- [main] . > cfillion_Select track FX by name (create action).lua 7 | -- @donation Donate via PayPal https://paypal.me/cfillion 8 | -- @about 9 | -- # Select track FX by name 10 | -- 11 | -- This script asks for a string to match against all track FX in the current 12 | -- project, matching tracks or selected tracks. The search is case insensitive. 13 | -- The first matching effect in each track is selected in the FX chain. 14 | -- 15 | -- This script can also be used to create custom actions that select matching 16 | -- track effects without always requesting user input. 17 | 18 | if not reaper.GetTrackName then 19 | -- for REAPER prior to v5.30 (native GetTrackName returns "Track N" when it's empty) 20 | function reaper.GetTrackName(track, _) 21 | return reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', '', false) 22 | end 23 | end 24 | 25 | if not reaper.CF_SelectTrackFX then 26 | -- for SWS prior to v2.12 27 | function reaper.CF_SelectTrackFX(track, fx_index) 28 | if reaper.TrackFX_GetChainVisible(track) ~= -1 then -- the FX chain is open 29 | reaper.TrackFX_Show(track, fx_index, 1); 30 | return 31 | end 32 | 33 | local _, chunk = reaper.GetTrackStateChunk(track, '', false) 34 | local new_chunk = chunk:gsub('\nLASTSEL %d+\n', 35 | string.format('\nLASTSEL %d\n', fx_index), 1) 36 | 37 | if chunk ~= new_chunk then 38 | reaper.SetTrackStateChunk(track, new_chunk, false) 39 | end 40 | end 41 | end 42 | 43 | local function matchTrack(track, filter) 44 | if filter == '/selected' then 45 | return reaper.IsTrackSelected(track) 46 | else 47 | local _, name = reaper.GetTrackName(track, '') 48 | return name:lower():find(filter) 49 | end 50 | end 51 | 52 | local function prompt() 53 | local default_track_filter = '' 54 | if reaper.CountSelectedTracks() > 0 then 55 | default_track_filter = '/selected' 56 | end 57 | 58 | local ok, csv = reaper.GetUserInputs(script_name, 2, 59 | "Select track FX matching:,On tracks (name or /selected):,extrawidth=100", 60 | ',' .. default_track_filter) 61 | if not ok or csv:len() <= 1 then return end 62 | 63 | local fx_filter, track_filter = csv:match("^(.*),(.*)$") 64 | return fx_filter:lower(), track_filter:lower() 65 | end 66 | 67 | local function sanitizeFilename(name) 68 | -- replace special characters that are reserved on Windows 69 | return name:gsub("[*\\:<>?/|\"%c]+", '-') 70 | end 71 | 72 | local function createAction() 73 | local fx_filter_fn = sanitizeFilename(fx_filter) 74 | local action_name = string.format('Select track FX by name - %s', fx_filter_fn) 75 | local output_fn = string.format('%s/Scripts/%s.lua', 76 | reaper.GetResourcePath(), action_name) 77 | local base_name = script_path:match('([^/\\]+)$') 78 | local rel_path = script_path:sub(reaper.GetResourcePath():len() + 2) 79 | 80 | local code = string.format([[ 81 | -- This file was created by %s on %s 82 | 83 | fx_filter = %q 84 | track_filter = %q 85 | dofile(string.format(%q, reaper.GetResourcePath())) 86 | ]], base_name, os.date('%c'), fx_filter, track_filter, '%s/'..rel_path) 87 | 88 | local file = assert(io.open(output_fn, 'w')) 89 | file:write(code) 90 | file:close() 91 | 92 | if reaper.AddRemoveReaScript(true, 0, output_fn, true) == 0 then 93 | reaper.ShowMessageBox( 94 | 'Failed to create or register the new action.', script_name, 0) 95 | return 96 | end 97 | 98 | reaper.ShowMessageBox( 99 | string.format('Created the action "%s".', action_name), script_name, 0) 100 | end 101 | 102 | script_path = ({reaper.get_action_context()})[2] 103 | script_name = script_path:match("([^/\\_]+)%.lua$") 104 | 105 | if not fx_filter or not track_filter then 106 | fx_filter, track_filter = prompt() 107 | 108 | if not fx_filter then 109 | reaper.defer(function() end) -- no undo point if nothing to do 110 | return 111 | end 112 | end 113 | 114 | if script_name == 'Select track FX by name (create action)' then 115 | createAction() 116 | return 117 | end 118 | 119 | reaper.Undo_BeginBlock() 120 | 121 | for ti=0,reaper.CountTracks()-1 do 122 | local track = reaper.GetTrack(0, ti) 123 | 124 | if matchTrack(track, track_filter) then 125 | local do_select 126 | 127 | for fi=0,reaper.TrackFX_GetCount(track)-1 do 128 | local _, fx_name = reaper.TrackFX_GetFXName(track, fi, '') 129 | if fx_name:lower():find(fx_filter) then 130 | do_select = fi 131 | break 132 | end 133 | end 134 | 135 | if do_select then 136 | reaper.CF_SelectTrackFX(track, do_select) 137 | end 138 | end 139 | end 140 | 141 | reaper.Undo_EndBlock( 142 | string.format("Select track FX matching '%s'", fx_filter), -1) 143 | -------------------------------------------------------------------------------- /cfillion_Set item end to cursor and resize trailing MIDI notes.lua: -------------------------------------------------------------------------------- 1 | -- @description Set item end to cursor and resize trailing MIDI notes 2 | -- @author cfillion 3 | -- @version 1.0.1 4 | -- @changelog Avoid creating zero-length items when cursor is at the item's start 5 | -- @website 6 | -- cfillion.ca https://cfillion.ca/ 7 | -- Request Thread https://forum.cockos.com/showthread.php?t=199045 8 | -- @donate https://www.paypal.me/cfillion 9 | -- @about 10 | -- Similar to the native "Item: Set item end to cursor" action except for these differences: 11 | -- 12 | -- - MIDI takes are resized 13 | -- - Note touching the item end are resized 14 | 15 | function extendMIDI(take, from, to) 16 | local index = 0 17 | 18 | from = reaper.MIDI_GetPPQPosFromProjTime(take, from) 19 | to = reaper.MIDI_GetPPQPosFromProjTime(take, to) 20 | 21 | while true do 22 | local retval, _, _, startppqpos, endppqpos = reaper.MIDI_GetNote(take, index) 23 | if not retval then break end 24 | 25 | if startppqpos <= from and endppqpos >= from then 26 | reaper.MIDI_SetNote(take, index, nil, nil, nil, to) 27 | end 28 | 29 | index = index + 1 30 | end 31 | 32 | reaper.MIDI_Sort(take) 33 | end 34 | 35 | function extendItem(item, to) 36 | local start = reaper.GetMediaItemInfo_Value(item, 'D_POSITION') 37 | local from = start + reaper.GetMediaItemInfo_Value(item, 'D_LENGTH') 38 | local resized = false 39 | 40 | if start >= to then return end 41 | 42 | for takeIndex = 0, reaper.CountTakes(item) - 1 do 43 | local take = reaper.GetMediaItemTake(item, takeIndex) 44 | 45 | if reaper.TakeIsMIDI(take) then 46 | if not resized then 47 | reaper.MIDI_SetItemExtents(item, 48 | reaper.TimeMap_timeToQN(start), reaper.TimeMap_timeToQN(to)) 49 | resized = true 50 | end 51 | 52 | extendMIDI(take, from, to) 53 | end 54 | end 55 | 56 | if not resized then 57 | -- item is not a MIDI item 58 | local newLen = to - start 59 | reaper.SetMediaItemInfo_Value(item, 'D_LENGTH', newLen) 60 | end 61 | end 62 | 63 | local selItems = reaper.CountSelectedMediaItems(0) 64 | local cursorPos = reaper.GetCursorPosition() 65 | 66 | if selItems < 1 then 67 | reaper.defer(function() end) -- disable implicit undo point 68 | return 69 | end 70 | 71 | reaper.Undo_BeginBlock() 72 | 73 | for itemIndex = 0, selItems - 1 do 74 | local item = reaper.GetSelectedMediaItem(0, itemIndex) 75 | extendItem(item, cursorPos) 76 | end 77 | 78 | local name = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 79 | local UNDO_STATE_ITEMS = 4 -- track items 80 | reaper.Undo_EndBlock(name, UNDO_STATE_ITEMS) 81 | -------------------------------------------------------------------------------- /cfillion_Set space between selected notes from grid size.lua: -------------------------------------------------------------------------------- 1 | -- @description Set space between selected notes from grid size 2 | -- @version 1.0.1 3 | -- @changelog Fix behavior when multiple notes are at the same position. 4 | -- @author cfillion 5 | -- @link Forum Thread https://forum.cockos.com/showthread.php?t=187255 6 | -- @donation https://www.paypal.me/cfillion 7 | 8 | local editor = reaper.MIDIEditor_GetActive() 9 | local take = reaper.MIDIEditor_GetTake(editor) 10 | if not take or not reaper.ValidatePtr(take, 'MediaItem_Take*') then 11 | return reaper.defer(function() end) 12 | end 13 | 14 | local grid = reaper.MIDI_GetGrid(take) 15 | local ni, last_end, changes = -1, nil, {} 16 | 17 | while true do 18 | ni = reaper.MIDI_EnumSelNotes(take, ni) 19 | if ni == -1 then break end 20 | 21 | local _, _, _, startpos, endpos = reaper.MIDI_GetNote(take, ni) 22 | 23 | if last_end then 24 | local originalStartPos = reaper.MIDI_GetProjQNFromPPQPos(take, startpos) 25 | startpos = last_end + grid 26 | endpos = reaper.MIDI_GetProjQNFromPPQPos(take, endpos) 27 | 28 | if originalStartPos ~= startpos then 29 | endpos = endpos - (originalStartPos - startpos) 30 | 31 | table.insert(changes, {ni=ni, 32 | newstart=reaper.MIDI_GetPPQPosFromProjQN(take, startpos), 33 | newend=reaper.MIDI_GetPPQPosFromProjQN(take, endpos), 34 | }) 35 | end 36 | else 37 | endpos = reaper.MIDI_GetProjQNFromPPQPos(take, endpos) 38 | end 39 | 40 | last_end = endpos 41 | end 42 | 43 | if #changes == 0 then 44 | return reaper.defer(function() end) 45 | end 46 | 47 | reaper.Undo_BeginBlock() 48 | 49 | for _,change in ipairs(changes) do 50 | reaper.MIDI_SetNote(take, change.ni, nil, nil, 51 | change.newstart, change.newend, nil, nil, nil, true) 52 | end 53 | 54 | reaper.MIDI_Sort(take) 55 | 56 | local pointName = ({reaper.get_action_context()})[2]:match('([^/\\_]+).lua$') 57 | reaper.Undo_EndBlock(pointName, -1) 58 | -------------------------------------------------------------------------------- /cfillion_Set take playback rate from semitones.lua: -------------------------------------------------------------------------------- 1 | -- @description Set take playback rate from semitones 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @links 5 | -- cfillion.ca https://cfillion.ca/ 6 | -- Request Thread https://forum.cockos.com/showthread.php?t=201842 7 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD&item_name=ReaScript%3A+Set+take+playback+rate+from+semitones 8 | -- @about 9 | -- # Set take playback rate from semitones 10 | -- 11 | -- This script sets the playback rate of selected takes from semitones input 12 | -- without preserving pitch. 13 | 14 | local UNDO_STATE_ITEMS = 4 15 | 16 | function rate2pitch(mul) 17 | return 12 * math.log(mul, 2) 18 | end 19 | 20 | function pitch2rate(semitones) 21 | return 2 ^ (semitones / 12) 22 | end 23 | 24 | function currentSemitones() 25 | local item = reaper.GetSelectedMediaItem(0, 0) 26 | local take = reaper.GetActiveTake(item) 27 | local rate = reaper.GetMediaItemTakeInfo_Value(take, 'D_PLAYRATE') 28 | 29 | return tostring(rate2pitch(rate)) 30 | end 31 | 32 | if reaper.CountSelectedMediaItems() < 1 then 33 | reaper.defer(function() end) 34 | return 35 | end 36 | 37 | local script_name = ({reaper.get_action_context()})[2]:match("([^/\\_]+)%.lua$") 38 | local retval, csv = reaper.GetUserInputs(script_name, 1, 'Semitones:', currentSemitones()) 39 | local semitones = tonumber(csv) 40 | 41 | if not retval or not semitones then 42 | reaper.defer(function() end) 43 | return 44 | end 45 | 46 | local rate = pitch2rate(semitones) 47 | 48 | reaper.Undo_BeginBlock() 49 | 50 | for i=0, reaper.CountSelectedMediaItems() - 1 do 51 | local item = reaper.GetSelectedMediaItem(0, i) 52 | local take = reaper.GetActiveTake(item) 53 | 54 | reaper.SetMediaItemTakeInfo_Value(take, 'D_PLAYRATE', rate) 55 | reaper.SetMediaItemTakeInfo_Value(take, 'B_PPITCH', 0) 56 | end 57 | 58 | reaper.Undo_EndBlock(script_name, UNDO_STATE_ITEMS) 59 | reaper.UpdateArrange() 60 | -------------------------------------------------------------------------------- /cfillion_Set timecode at edit cursor.lua: -------------------------------------------------------------------------------- 1 | -- @description Set timecode at edit cursor 2 | -- @author cfillion 3 | -- @version 1.2 4 | -- @changelog Add a "set to 0" action 5 | -- @provides 6 | -- . 7 | -- [main] . > cfillion_Set timecode at edit cursor (seconds).lua 8 | -- [main] . > cfillion_Set timecode at edit cursor (frames).lua 9 | -- [main] . > cfillion_Set timecode at edit cursor (set to 0).lua 10 | -- @link 11 | -- cfillion.ca https://cfillion.ca 12 | -- Request Thread https://forum.cockos.com/showthread.php?t=202578 13 | -- @screenshot https://i.imgur.com/uly6oy5.gif 14 | -- @donation https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD 15 | 16 | local SCRIPT_NAME = ({reaper.get_action_context()})[2]:match("([^/\\_]+)%.lua$") 17 | 18 | local MODE = ({ 19 | ['time' ] = 0, 20 | ['seconds' ] = 3, 21 | ['frames' ] = 5, 22 | ['set to 0'] = -1, 23 | })[SCRIPT_NAME:match('%(([^%)]+)%)') or 'time'] 24 | 25 | assert(MODE, "Internal error: unknown timecode format") 26 | assert(reaper.SNM_GetDoubleConfigVar, "SWS is required to use this script") 27 | 28 | local curpos = reaper.GetCursorPosition() 29 | local timecode = 0 30 | 31 | if MODE >= 0 then 32 | timecode = reaper.format_timestr_pos(curpos, '', MODE) 33 | local ok, csv = reaper.GetUserInputs(SCRIPT_NAME, 1, "Timecode,extrawidth=50", timecode) 34 | 35 | if not ok then 36 | reaper.defer(function() end) 37 | return 38 | end 39 | 40 | timecode = reaper.parse_timestr_len(csv, 0, MODE) 41 | end 42 | 43 | reaper.SNM_SetDoubleConfigVar('projtimeoffs', timecode - curpos) 44 | reaper.UpdateTimeline() 45 | -------------------------------------------------------------------------------- /cfillion_Show ReaPack about dialog for the focused JSFX.lua: -------------------------------------------------------------------------------- 1 | -- @description Show ReaPack about dialog for the focused JSFX 2 | -- @author cfillion 3 | -- @version 0.1 4 | -- @link https://cfillion.ca 5 | -- @donation https://www.paypal.me/cfillion 6 | 7 | local script = ({reaper.get_action_context()})[2]:match('([^/\\_]+).lua$') 8 | 9 | function ShowMsg(msg) 10 | reaper.MB(msg, script, 0); 11 | end 12 | 13 | if not reaper.ReaPack_AboutInstalledPackage then 14 | return ShowMsg('ReaPack v1.2+ is required to use this script.') 15 | end 16 | 17 | local fxtype, trackidx, itemidx, fxidx = reaper.GetFocusedFX() 18 | 19 | local getchunk = ({ 20 | function() 21 | local track 22 | if trackidx == 0 then 23 | track = reaper.GetMasterTrack(0) 24 | else 25 | track = reaper.GetTrack(0, trackidx - 1) 26 | end 27 | 28 | return reaper.GetTrackStateChunk(track, '') 29 | end, 30 | function() 31 | local track = reaper.GetTrack(0, trackidx - 1) 32 | local item = reaper.GetTrackMediaItem(track, itemidx) 33 | return reaper.GetItemStateChunk(item, '', false) 34 | end, 35 | })[fxtype] 36 | if not getchunk then return end 37 | 38 | local ok, chunk = getchunk() 39 | if not ok then return end 40 | 41 | local name, type = ({function() 42 | local index = 0 43 | for type, file in chunk:gmatch("BYPASS %d+ %d+ %d+\n<([^%s]+) ([^\n]+)") do 44 | if index == fxidx & 0xFFFF then 45 | if type == 'JS' then 46 | if file:sub(1, 1) == '"' then 47 | return file:match('^"([^"]+)"'), type 48 | else 49 | return file:match('^[^%s]+'), type 50 | end 51 | else 52 | return nil, type 53 | end 54 | end 55 | 56 | index = index + 1 57 | end 58 | end})[1]() 59 | if not name then 60 | return ShowMsg(string.format('Documentation for %s plugins is not supported. Try again with a JSFX focused.', type)) 61 | end 62 | 63 | local owner = reaper.ReaPack_GetOwner(string.format('Effects/%s', name)) 64 | if not owner then 65 | return ShowMsg(string.format("Documentation cannot be found because ReaPack does not know about the JSFX at '%s'.", name)) 66 | end 67 | 68 | reaper.ReaPack_AboutInstalledPackage(owner) 69 | reaper.ReaPack_FreeEntry(owner) 70 | -------------------------------------------------------------------------------- /cfillion_Show all saved nudge settings.lua: -------------------------------------------------------------------------------- 1 | -- @description Show all saved nudge settings 2 | -- @version 3.0.3 3 | -- @changelog Internal code cleanup 4 | -- @author cfillion 5 | -- @link cfillion.ca https://cfillion.ca 6 | -- @donation https://reapack.com/donate 7 | -- @screenshot https://i.imgur.com/5o3OIyf.png 8 | -- @provides 9 | -- [main] . 10 | -- [main] . > cfillion_Nudge left by selected saved nudge dialog settings.lua 11 | -- [main] . > cfillion_Nudge right by selected saved nudge dialog settings.lua 12 | -- @about 13 | -- # Show all saved nudge settings 14 | -- 15 | -- This script allows viewing, editing and using all nudge setting presets in 16 | -- a single window. 17 | -- 18 | -- The edit feature opens the native nudge settings dialog with the current 19 | -- settings filled. The new settings are automatically saved into the selected 20 | -- 1-8 slot once the native dialog is closed. Trigger the edit feature a second 21 | -- time to close the native dialog without saving. 22 | -- 23 | -- In addition to the GUI script, two additional actions are provided to nudge 24 | -- left or right by the last selected settings in the script interface. 25 | -- 26 | -- ## Keyboard Shortcuts 27 | -- 28 | -- - Switch to a different nudge setting with the **0-8** keys 29 | -- - Edit the current nudge setting by pressing **n** 30 | -- - Nudge with the **left/right arrow** keys 31 | -- - Close the script with **Escape** 32 | -- 33 | -- ## Caveats 34 | -- 35 | -- The "Last" tab may be out of sync with the effective last nudge settings. 36 | -- This is because the native "Nudge left/right by saved nudge dialog settings X" 37 | -- actions do not save the nudge settings in reaper.ini when they change the 38 | -- last used settings. 39 | -- 40 | -- There is no reliable way for a script to detect whether the last nudge 41 | -- settings are out of sync. A workaround for forcing REAPER to save its 42 | -- settings is to open and close the native nudge dialog. 43 | -- 44 | -- Furthermore, REAPER does not store the nudge amout when using the "Set" mode 45 | -- in the native nudge dialog. The script displays "N/A" in this case and the 46 | -- nudge left/right actions are unavailable. 47 | 48 | local r = reaper 49 | 50 | local WHAT_MAP = {'position', 'left trim', 'left edge', 'right trim', 'contents', 51 | 'duplicate', 'edit cursor', 'end position'} 52 | 53 | local UNIT_MAP = {'milliseconds', 'seconds', 'grid units', 'notes', 54 | [17]='measures.beats', [18]='samples', [19]='frames', [20]='pixels', 55 | [21]='item lengths', [22]='item selections'} 56 | 57 | local NOTE_MAP = {'1/256', '1/128', '1/64', '1/32T', '1/32', '1/16T', '1/16', 58 | '1/8T', '1/8', '1/4T', '1/4', '1/2', 'whole'} 59 | 60 | local NUDGEDLG_ACTION = 41228 61 | local SAVE_ACTIONS = {last=0, bank1=41271, bank2=41283} 62 | local LNUDGE_ACTIONS = {last=41250, bank1=41279, bank2=41291} 63 | local RNUDGE_ACTIONS = {last=41249, bank1=41275, bank2=41287} 64 | 65 | local EXT_SECTION, EXT_LAST_SLOT = 'cfillion_show_nudge_settings', 'last_slot' 66 | 67 | local isEditing = false 68 | local saved = 0 69 | local setting = {} 70 | 71 | local scriptName = select(2, r.get_action_context()):match('([^/\\_]+).lua$') 72 | local iniFile = r.get_ini_file() 73 | 74 | if not reaper.ImGui_GetBuiltinPath then 75 | reaper.MB('This script requires ReaImGui. \z 76 | Install it from ReaPack > Browse packages.', scriptName, 0) 77 | return 78 | end 79 | 80 | package.path = reaper.ImGui_GetBuiltinPath() .. '/?.lua' 81 | local ImGui = require 'imgui' '0.9' 82 | local FLT_MIN, FLT_MAX = ImGui.NumericLimits_Float() 83 | local ctx 84 | 85 | function iniRead(key, n) 86 | if n > 0 then 87 | key = ('%s_%d'):format(key, n) 88 | end 89 | 90 | return tonumber((select(2, 91 | r.BR_Win32_GetPrivateProfileString('REAPER', key, '0', iniFile)))) 92 | end 93 | 94 | function boolValue(val, off, on) 95 | if not off then off = 'OFF' end 96 | if not on then on = 'ON' end 97 | return val == 0 and off or on 98 | end 99 | 100 | function mapValue(val, strings) 101 | return strings[val] or ('%d (Unknown)'):format(val) 102 | end 103 | 104 | function isAny(val, ...) 105 | for _,n in ipairs{...} do 106 | if val == n then 107 | return true 108 | end 109 | end 110 | 111 | return false 112 | end 113 | 114 | function snapTo(unit) 115 | if isAny(unit, 3, 21, 22) then 116 | return 'grid' 117 | elseif unit == 17 then -- measures.beats 118 | return 'bar' 119 | end 120 | 121 | return 'unit' 122 | end 123 | 124 | function loadSetting(n, reload) 125 | if setting.n == n and not reload then return end 126 | 127 | setting = {n=n} 128 | r.SetExtState(EXT_SECTION, EXT_LAST_SLOT, n, true) 129 | 130 | local nudge = iniRead('nudge', n) 131 | setting.mode = nudge & 1 132 | setting.what = (nudge >> 12) + 1 133 | setting.unit = (nudge >> 4 & 0xFF) + 1 134 | setting.snap = nudge & 2 135 | setting.rel = nudge & 4 136 | 137 | if setting.unit >= 4 and setting.unit <= 16 then 138 | setting.note = setting.unit - 3 139 | setting.unit = 4 140 | end 141 | 142 | if setting.mode == 0 then 143 | setting.amount = iniRead('nudgeamt', n) 144 | else 145 | setting.amount = '(N/A)' 146 | end 147 | 148 | setting.copies = iniRead('nudgecopies', n) 149 | end 150 | 151 | function action(ids) 152 | local base 153 | 154 | if setting.n == 0 then 155 | return ids.last 156 | elseif setting.n < 5 then 157 | base = ids.bank1 158 | else 159 | base = ids.bank2 160 | end 161 | 162 | return base + ((setting.n - 1) % 4) 163 | end 164 | 165 | function nudge(actions) 166 | if setting.mode ~= 1 then 167 | r.Main_OnCommand(action(actions), 0) 168 | end 169 | end 170 | 171 | function setAsLast() 172 | local selection, count = {}, r.CountSelectedMediaItems(0) 173 | 174 | for i=0,count - 1 do 175 | local item = r.GetSelectedMediaItem(0, 0) 176 | table.insert(selection, item) 177 | r.SetMediaItemSelected(item, false) 178 | end 179 | 180 | nudge(RNUDGE_ACTIONS) 181 | 182 | for _,item in ipairs(selection) do 183 | r.SetMediaItemSelected(item, true) 184 | end 185 | end 186 | 187 | function editCurrent() 188 | if isEditing then 189 | -- prevent saveCurrent() from being called when the nudge dialog is manually closed 190 | isEditing = false 191 | r.Main_OnCommand(NUDGEDLG_ACTION, 0) 192 | loadSetting(setting.n, true) 193 | return 194 | elseif setting.n > 0 then 195 | setAsLast() 196 | end 197 | 198 | r.Main_OnCommand(NUDGEDLG_ACTION, 0) 199 | end 200 | 201 | function saveCurrent() 202 | if setting.n > 0 then 203 | r.Main_OnCommand(action(SAVE_ACTIONS), 0) 204 | saved = os.time() 205 | end 206 | 207 | loadSetting(setting.n, true) 208 | end 209 | 210 | function help() 211 | if not r.ReaPack_GetOwner then 212 | r.MB('This feature requires ReaPack v1.2 or newer.', scriptName, 0) 213 | return 214 | end 215 | 216 | local owner = r.ReaPack_GetOwner((select(2, r.get_action_context()))) 217 | 218 | if not owner then 219 | r.MB( 220 | ('This feature is unavailable because "%s" was not installed using ReaPack.') 221 | :format(scriptName), scriptName, 0) 222 | return 223 | end 224 | 225 | r.ReaPack_AboutInstalledPackage(owner) 226 | r.ReaPack_FreeEntry(owner) 227 | end 228 | 229 | function detectEdit() 230 | local state = r.GetToggleCommandState(NUDGEDLG_ACTION) == 1 231 | 232 | if isEditing and not state then 233 | saveCurrent() 234 | end 235 | 236 | isEditing = state 237 | end 238 | 239 | function calcItemWidth(text, hasFrame) 240 | local framePadding = hasFrame and ImGui.GetStyleVar(ctx, ImGui.StyleVar_FramePadding) or 0 241 | return ImGui.CalcTextSize(ctx, text) + (framePadding * 2) 242 | end 243 | 244 | function rtlPos(...) 245 | local args = {...} 246 | local spacing = ImGui.GetStyleVar(ctx, ImGui.StyleVar_ItemSpacing) 247 | local width = 3 248 | for _, itemWidth in ipairs(args) do 249 | width = width + itemWidth 250 | end 251 | width = width + (spacing * #args) 252 | return ImGui.GetWindowWidth(ctx) - width 253 | end 254 | 255 | function button(label, shortcut, active) 256 | if active then 257 | local color = 0x96afe1ff 258 | ImGui.PushStyleColor(ctx, ImGui.Col_Button, color) 259 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonHovered, color) 260 | end 261 | local rv = ImGui.Button(ctx, label) or 262 | ImGui.IsKeyPressed(ctx, shortcut) 263 | if active then 264 | ImGui.PopStyleColor(ctx, 2) 265 | end 266 | return rv 267 | end 268 | 269 | function draw() 270 | local RO = ImGui.InputTextFlags_ReadOnly 271 | 272 | for n = 0, 8 do 273 | if button(n == 0 and 'Last' or n, ImGui.Key_0 + n, setting.n == n) then 274 | loadSetting(n) 275 | end 276 | ImGui.SameLine(ctx) 277 | end 278 | 279 | local editWidth, helpWidth, savedWidth = 280 | calcItemWidth('Edit', true), calcItemWidth('?', true), calcItemWidth('Saved!') 281 | 282 | if saved > os.time() - 2 then 283 | ImGui.SameLine(ctx, rtlPos(savedWidth, editWidth, helpWidth)) 284 | ImGui.Text(ctx, 'Saved!') 285 | ImGui.SameLine(ctx) 286 | end 287 | ImGui.SameLine(ctx, rtlPos(editWidth, helpWidth)) 288 | if button('Edit', ImGui.Key_N, isEditing) then editCurrent() end 289 | ImGui.SameLine(ctx, rtlPos(helpWidth)) 290 | if button('?', ImGui.Key_F1) then help() end 291 | 292 | ImGui.SetNextItemWidth(ctx, 70) 293 | ImGui.InputText(ctx, '##mode', boolValue(setting.mode, 'Nudge', 'Set'), RO) 294 | ImGui.SameLine(ctx) 295 | 296 | ImGui.SetNextItemWidth(ctx, 100) 297 | ImGui.InputText(ctx, '##what', mapValue(setting.what, WHAT_MAP), RO) 298 | ImGui.SameLine(ctx) 299 | 300 | ImGui.Text(ctx, boolValue(setting.mode, 'by:', 'to:')) 301 | ImGui.SameLine(ctx) 302 | 303 | ImGui.SetNextItemWidth(ctx, 70) 304 | ImGui.InputText(ctx, '##amount', setting.amount, RO) 305 | ImGui.SameLine(ctx) 306 | 307 | if setting.note then 308 | ImGui.SetNextItemWidth(ctx, 50) 309 | ImGui.InputText(ctx, '##note', mapValue(setting.note, NOTE_MAP), RO) 310 | ImGui.SameLine(ctx) 311 | end 312 | 313 | ImGui.SetNextItemWidth(ctx, -FLT_MIN) 314 | ImGui.InputText(ctx, '##unit', mapValue(setting.unit, UNIT_MAP), RO) 315 | 316 | ImGui.AlignTextToFramePadding(ctx) 317 | ImGui.Text(ctx, 318 | ('Snap to %s: %s'):format(snapTo(setting.unit), boolValue(setting.snap))) 319 | ImGui.SameLine(ctx, nil, 20) 320 | 321 | if setting.mode == 1 and isAny(setting.what, 1, 6, 8) then 322 | ImGui.Text(ctx, ('Relative set: %s'):format(boolValue(setting.rel))) 323 | elseif setting.what == 6 then 324 | ImGui.Text(ctx, ('Copies: %s'):format(setting.copies)) 325 | end 326 | 327 | if setting.mode == 0 then 328 | local leftText, rightText = '< Nudge left', 'Nudge right >' 329 | local leftWidth, rightWidth = 330 | calcItemWidth(leftText, true), calcItemWidth(rightText, true) 331 | ImGui.SameLine(ctx, rtlPos(leftWidth, rightWidth)) 332 | if button(leftText, ImGui.Key_LeftArrow) then nudge(LNUDGE_ACTIONS) end 333 | ImGui.SameLine(ctx, rtlPos(rightWidth)) 334 | if button(rightText, ImGui.Key_RightArrow) then nudge(RNUDGE_ACTIONS) end 335 | else 336 | local text = '(Nudge unavailable in Set mode)' 337 | ImGui.SameLine(ctx, rtlPos(calcItemWidth(text))) 338 | ImGui.TextDisabled(ctx, text) 339 | end 340 | end 341 | 342 | function contextMenu() 343 | local dock_id = ImGui.GetWindowDockID(ctx) 344 | if not ImGui.BeginPopupContextWindow(ctx) then return end 345 | if ImGui.BeginMenu(ctx, 'Dock window') then 346 | if ImGui.MenuItem(ctx, 'Floating', nil, dock_id == 0) then 347 | set_dock_id = 0 348 | end 349 | for i = 0, 15 do 350 | if ImGui.MenuItem(ctx, ('Docker %d'):format(i + 1), nil, dock_id == ~i) then 351 | set_dock_id = ~i 352 | end 353 | end 354 | ImGui.EndMenu(ctx) 355 | end 356 | ImGui.Separator(ctx) 357 | if ImGui.MenuItem(ctx, 'About/help', 'F1', false, r.ReaPack_GetOwner ~= nil) then 358 | help() 359 | end 360 | if ImGui.MenuItem(ctx, 'Close', 'Escape') then 361 | exit = true 362 | end 363 | ImGui.EndPopup(ctx) 364 | end 365 | 366 | function loop() 367 | detectEdit() 368 | 369 | ImGui.PushFont(ctx, font) 370 | ImGui.PushStyleColor(ctx, ImGui.Col_ChildBg, 0xffffffff) 371 | ImGui.PushStyleColor(ctx, ImGui.Col_WindowBg, 0xffffffff) 372 | 373 | ImGui.SetNextWindowSize(ctx, 475, 117, ImGui.Cond_FirstUseEver) 374 | ImGui.SetNextWindowSizeConstraints(ctx, 400, 112, FLT_MAX, FLT_MAX) 375 | if set_dock_id then 376 | ImGui.SetNextWindowDockID(ctx, set_dock_id) 377 | set_dock_id = nil 378 | end 379 | local visible, open = ImGui.Begin(ctx, scriptName, true, 380 | ImGui.WindowFlags_NoScrollbar | ImGui.WindowFlags_NoScrollWithMouse) 381 | if visible then 382 | ImGui.PushStyleColor(ctx, ImGui.Col_Border, 0x2a2a2aff) 383 | ImGui.PushStyleColor(ctx, ImGui.Col_Button, 0xdcdcdcff) 384 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonActive, 0x787878ff) 385 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonHovered, 0xdcdcdcff) 386 | ImGui.PushStyleColor(ctx, ImGui.Col_FrameBg, 0xffffffff) 387 | ImGui.PushStyleColor(ctx, ImGui.Col_HeaderHovered, 0x96afe1ff) 388 | ImGui.PushStyleColor(ctx, ImGui.Col_PopupBg, 0xffffffff) 389 | ImGui.PushStyleColor(ctx, ImGui.Col_Text, 0x2a2a2aff) 390 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_FrameBorderSize, 1) 391 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_FramePadding, 7, 4) 392 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_ItemSpacing, 7, 7) 393 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_WindowPadding, 10, 10) 394 | 395 | contextMenu() 396 | draw() 397 | 398 | ImGui.PopStyleVar(ctx, 4) 399 | ImGui.PopStyleColor(ctx, 8) 400 | 401 | ImGui.End(ctx) 402 | end 403 | 404 | ImGui.PopStyleColor(ctx, 2) 405 | ImGui.PopFont(ctx) 406 | 407 | if exit or ImGui.IsKeyPressed(ctx, ImGui.Key_Escape) then 408 | open = false 409 | end 410 | 411 | if open then 412 | r.defer(loop) 413 | end 414 | end 415 | 416 | function previousSlot() 417 | local slot = tonumber(r.GetExtState(EXT_SECTION, EXT_LAST_SLOT)) 418 | 419 | if slot and slot >= 0 and slot <= 8 then 420 | return slot 421 | else 422 | return 0 423 | end 424 | end 425 | 426 | loadSetting(previousSlot()) 427 | 428 | if scriptName:match('Nudge.+by selected') then 429 | r.defer(function() end) -- disable automatic undo point 430 | nudge(scriptName:match('left') and LNUDGE_ACTIONS or RNUDGE_ACTIONS) 431 | return 432 | end 433 | 434 | ctx = ImGui.CreateContext(scriptName) 435 | 436 | local size = r.GetAppVersion():match('OSX') and 12 or 14 437 | font = ImGui.CreateFont('sans-serif', size) 438 | ImGui.Attach(ctx, font) 439 | 440 | r.defer(loop) 441 | -------------------------------------------------------------------------------- /cfillion_Show distance between play cursor and nearest marker.lua: -------------------------------------------------------------------------------- 1 | -- @description Show distance between play cursor and nearest marker 2 | -- @version 0.2 3 | -- @author cfillion 4 | 5 | local next_index, offset = 0, nil 6 | 7 | local cur_pos = (function() 8 | if (reaper.GetPlayState() & 1) == 1 then 9 | return reaper.GetPlayPosition() 10 | else 11 | return reaper.GetCursorPosition() 12 | end 13 | end)() 14 | 15 | while true do 16 | local retval, isregion, pos = reaper.EnumProjectMarkers(next_index) 17 | next_index = retval 18 | if next_index == 0 then break end 19 | 20 | if not isregion then 21 | this_offset = cur_pos - pos 22 | if not offset or math.abs(this_offset) < math.abs(offset) then 23 | offset = this_offset 24 | end 25 | end 26 | end 27 | 28 | if not offset then return end 29 | 30 | reaper.ShowConsoleMsg(string.format('Distance between edit/play cursor to nearest marker is %fs.\n', offset)) 31 | -------------------------------------------------------------------------------- /cfillion_Split selected automation items at project markers and regions.lua: -------------------------------------------------------------------------------- 1 | -- @description Split selected automation items at markers and/or regions 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @links cfillion.ca https://cfillion.ca 5 | -- @donate https://www.paypal.com/cgi-bin/webscr?business=T3DEWBQJAV7WL&cmd=_donations¤cy_code=CAD 6 | -- @provides 7 | -- . > cfillion_Split selected automation items at markers and regions.lua 8 | -- . > cfillion_Split selected automation items at markers.lua 9 | -- . > cfillion_Split selected automation items at regions.lua 10 | 11 | local UNDO_STATE_TRACKCFG = 1 12 | local SCRIPT_NAME = ({reaper.get_action_context()})[2]:match("([^/\\_]+)%.lua$") 13 | 14 | function testType(isrgn) 15 | return SCRIPT_NAME:match(isrgn and 'region' or 'marker') 16 | end 17 | 18 | function intersectSplits(env, from, to) 19 | local splits = {} 20 | 21 | for _, point in ipairs(splitPoints) do 22 | if point > from and point < to then 23 | table.insert(splits, point) 24 | end 25 | end 26 | 27 | return #splits > 0 and splits 28 | end 29 | 30 | splitPoints = (function() 31 | local points, marker = {}, {0} 32 | 33 | repeat 34 | -- integer retval, boolean isrgn, number pos, number rgnend, string name, number markrgnindexnumber 35 | marker = {reaper.EnumProjectMarkers(marker[1])} 36 | 37 | if testType(marker[2]) then 38 | table.insert(points, marker[3]) 39 | 40 | if marker[2] then 41 | table.insert(points, marker[4]) 42 | end 43 | end 44 | until marker[1] == 0 45 | 46 | -- sorting to ensure region end points won't be before an earlier marker 47 | table.sort(points) 48 | 49 | return points 50 | end)() 51 | 52 | local bucket = {} 53 | 54 | for ti=0,reaper.CountTracks(0)-1 do 55 | local track = reaper.GetTrack(0, ti) 56 | 57 | for ei=0,reaper.CountTrackEnvelopes(track)-1 do 58 | local env = reaper.GetTrackEnvelope(track, ei) 59 | 60 | for ai=0,reaper.CountAutomationItems(env)-1 do 61 | local selected = 1 == reaper.GetSetAutomationItemInfo(env, ai, 'D_UISEL', 0, false) 62 | local startTime = reaper.GetSetAutomationItemInfo(env, ai, 'D_POSITION', 0, false) 63 | local length = reaper.GetSetAutomationItemInfo(env, ai, 'D_LENGTH', 0, false) 64 | 65 | local splits = selected and intersectSplits(env, startTime, startTime+length) 66 | 67 | if splits then 68 | table.insert(bucket, {env=env, id=ai, pos=startTime, len=length, splits=splits}) 69 | end 70 | end 71 | end 72 | end 73 | 74 | if #bucket < 1 then 75 | reaper.defer(function() end) 76 | end 77 | 78 | reaper.Undo_BeginBlock() 79 | 80 | local reselect = {} 81 | 82 | for _, ai in ipairs(bucket) do 83 | local poolId = reaper.GetSetAutomationItemInfo(ai.env, ai.id, 'D_POOL_ID', 0, false) 84 | table.insert(reselect, ai) 85 | 86 | for id, point in ipairs(ai.splits) do 87 | local length = (ai.splits[id+1] or (ai.pos+ai.len)) - point 88 | local offset = point - ai.pos 89 | 90 | if id == 1 then 91 | reaper.GetSetAutomationItemInfo(ai.env, ai.id, 'D_LENGTH', offset, true) 92 | end 93 | 94 | local newId = reaper.InsertAutomationItem(ai.env, poolId, point, length) 95 | reaper.GetSetAutomationItemInfo(ai.env, newId, 'D_STARTOFFS', offset, true) 96 | 97 | table.insert(reselect, {env=ai.env, id=newId}) 98 | end 99 | end 100 | 101 | for _, ai in ipairs(reselect) do 102 | reaper.GetSetAutomationItemInfo(ai.env, ai.id, 'D_UISEL', 1, true) 103 | end 104 | 105 | reaper.Undo_EndBlock(SCRIPT_NAME, UNDO_STATE_TRACKCFG) 106 | -------------------------------------------------------------------------------- /cfillion_Split selected non-locked items at edit cursor.lua: -------------------------------------------------------------------------------- 1 | local self = ({reaper.get_action_context()})[2]:match('([^/\\_]+).lua$') 2 | local bucket, cursor = {}, reaper.GetCursorPosition() 3 | local UNDO_STATE_ITEMS = 4 4 | 5 | for i = 0, reaper.CountSelectedMediaItems() - 1 do 6 | local item = reaper.GetSelectedMediaItem(0, i) 7 | local locked = reaper.GetMediaItemInfo_Value(item, 'C_LOCK') & 1 > 0 8 | local length = reaper.GetMediaItemInfo_Value(item, 'D_LENGTH') 9 | local pos = reaper.GetMediaItemInfo_Value(item, 'D_POSITION') 10 | 11 | if not locked and pos <= cursor and pos + length >= cursor then 12 | table.insert(bucket, item) 13 | end 14 | end 15 | 16 | if #bucket == 0 then 17 | reaper.defer(function() end) 18 | return 19 | end 20 | 21 | reaper.Undo_BeginBlock() 22 | 23 | for _, item in pairs(bucket) do 24 | reaper.SplitMediaItem(item, cursor) 25 | end 26 | 27 | reaper.UpdateArrange() 28 | reaper.Undo_EndBlock(self, UNDO_STATE_ITEMS) 29 | -------------------------------------------------------------------------------- /cfillion_Step sequencing (replace mode).jsfx: -------------------------------------------------------------------------------- 1 | noindex: true 2 | 3 | desc:Step sequencing (replace mode) by cfillion 4 | 5 | options:gmem=cfillion_stepRecordReplace 6 | options:no_meter 7 | 8 | slider1:-1<-1,2147483647,1>-Last cursor position 9 | 10 | /* Memory structure: 11 | 12 | // private 13 | struct Note { int chan; int pitch; int vel; int isDown; }; 14 | struct { int size; Note data[]; } notes; 15 | 16 | // public 17 | struct Chord { int size; Note data[]; }; 18 | struct { int size; Chord data[]; } gmem; 19 | */ 20 | 21 | @block 22 | NOTE_ON = $x90; 23 | NOTE_OFF = $x80; 24 | 25 | NOTE_CHAN = 1; 26 | NOTE_PITCH = 2; 27 | NOTE_VEL = 3; 28 | NOTE_ISDOWN = 4; 29 | NOTE_SIZE = NOTE_ISDOWN; // sizeof(Note) 30 | 31 | function gmem_memcpy(dest, source, size) ( 32 | loop(size, 33 | gmem[dest] = 0[source]; 34 | dest += 1; source += 1; 35 | ); 36 | ); 37 | 38 | function flushNoteBuffer() local(ni, bi) ( 39 | ni = 0; bi = gmem[0]; 40 | gmem[bi] = notes[0]; 41 | 42 | while(ni < notes[0]) ( 43 | gmem_memcpy(bi, notes + ni, NOTE_SIZE); 44 | bi += NOTE_SIZE; ni += NOTE_SIZE; 45 | ); 46 | 47 | gmem[0] = bi; 48 | notes[0] = 0; 49 | ); 50 | 51 | while(midirecv(offset, msg, pitch, vel)) ( 52 | type = msg & 0xf0; 53 | type == NOTE_ON && vel == 0 ? type = NOTE_OFF; 54 | chan = msg & 0x0f; 55 | 56 | type == NOTE_ON ? ( 57 | notes[notes[0] + NOTE_CHAN ] = chan; 58 | notes[notes[0] + NOTE_PITCH ] = pitch; 59 | notes[notes[0] + NOTE_VEL ] = vel; 60 | notes[notes[0] + NOTE_ISDOWN] = 1; 61 | notes[0] += NOTE_SIZE; 62 | ); 63 | 64 | releasedNotes = 0; 65 | type == NOTE_OFF ? ( 66 | ni = 0; 67 | while(ni < notes[0]) ( 68 | notes[ni + NOTE_PITCH] == pitch ? notes[ni + NOTE_ISDOWN] = 0; 69 | !notes[ni + NOTE_ISDOWN] ? releasedNotes += NOTE_SIZE; 70 | ni += NOTE_SIZE; 71 | ); 72 | ); 73 | 74 | allNotesReleased = notes[0] > 0 && releasedNotes == notes[0]; 75 | allNotesReleased && gmem[0] > 0 ? flushNoteBuffer(); 76 | 77 | midisend(offset, msg, pitch, vel); 78 | ); 79 | -------------------------------------------------------------------------------- /cfillion_Step sequencing (replace mode).lua: -------------------------------------------------------------------------------- 1 | -- @description Step sequencing (replace mode) 2 | -- @author cfillion 3 | -- @version 1.1.3 4 | -- @changelog Internal code cleanup 5 | -- @provides 6 | -- . 7 | -- [main] . > cfillion_Step sequencing (options).lua 8 | -- [effect] cfillion_Step sequencing (replace mode).jsfx 9 | -- @screenshot 10 | -- Inserting and replacing notes https://i.imgur.com/4azf7CN.gif 11 | -- Options menu https://i.imgur.com/YFHLRWM.png 12 | -- @donation https://reapack.com/donate 13 | -- @about 14 | -- ## Step sequencing (replace mode) 15 | -- 16 | -- This script is an alternative to the native step recording feature. Existing notes under the edit cursor are replaced (lowest first). The MIDI editor's active note row is updated as new notes are played. 17 | -- 18 | -- An options action is provided to individually toggle replacing channel/pitch/velocity and skipping unselected notes. 19 | -- 20 | -- Note that this script automatically inserts and removes an helper JSFX in the active track's input FX chain in order to receive live MIDI input. 21 | 22 | local ImGui 23 | if reaper.ImGui_GetBuiltinPath then 24 | package.path = reaper.ImGui_GetBuiltinPath() .. '/?.lua' 25 | ImGui = require 'imgui' '0.9' 26 | end 27 | 28 | local MB_OK = 0 29 | local MIDI_EDITOR_SECTION = 32060 30 | local NATIVE_STEP_RECORD = 40481 31 | local NOTE_BUFFER_START = 1 32 | 33 | local EXT_SECTION = 'cfillion_stepRecordReplace' 34 | local EXT_MODE_KEY = 'mode' 35 | 36 | local MODE_CHAN = 1<<0 37 | local MODE_PITCH = 1<<1 38 | local MODE_VEL = 1<<2 39 | local MODE_SEL = 1<<3 40 | 41 | local UNDO_STATE_FX = 1<<1 42 | local UNDO_STATE_ITEMS = 1<<2 43 | 44 | local jsfx 45 | local jsfxName = 'ReaTeam Scripts/MIDI Editor/cfillion_Step sequencing (replace mode).jsfx' 46 | local scriptName, scriptSection, scriptId = select(2, reaper.get_action_context()) 47 | scriptName = scriptName:match("([^/\\_]+)%.lua$") 48 | local debug = false 49 | 50 | local function printf(...) 51 | if debug then 52 | reaper.ShowConsoleMsg(string.format(...)) 53 | end 54 | end 55 | 56 | local function getActiveTake() 57 | local me = reaper.MIDIEditor_GetActive() 58 | 59 | if me then 60 | return reaper.MIDIEditor_GetTake(me), me 61 | end 62 | end 63 | 64 | local function getMode() 65 | local mode 66 | 67 | if reaper.HasExtState(EXT_SECTION, EXT_MODE_KEY) then 68 | mode = tonumber(reaper.GetExtState(EXT_SECTION, EXT_MODE_KEY)) 69 | end 70 | 71 | if not mode then 72 | mode = MODE_CHAN | MODE_PITCH | MODE_VEL 73 | end 74 | 75 | return mode 76 | end 77 | 78 | local function projects() 79 | local i = -1 80 | 81 | return function() 82 | i = i + 1 83 | return reaper.EnumProjects(i) 84 | end 85 | end 86 | 87 | local function findJSFX() 88 | local i, offset = 0, 0x1000000 89 | local guid = reaper.TrackFX_GetFXGUID(jsfx.track, offset + i) 90 | 91 | while guid do 92 | if guid == jsfx.guid then 93 | return i 94 | end 95 | 96 | i = i + 1 97 | guid = reaper.TrackFX_GetFXGUID(jsfx.track, offset + i) 98 | end 99 | end 100 | 101 | local function findNotesAtTime(take, ppqTime, onlySelected) 102 | local notes, ni = {}, onlySelected and -1 or 0 103 | 104 | while true do 105 | if onlySelected then 106 | ni = reaper.MIDI_EnumSelNotes(take, ni) 107 | if ni < 0 then break end 108 | end 109 | 110 | local note = {reaper.MIDI_GetNote(take, ni)} 111 | if not note[1] then break end 112 | 113 | note[1] = ni 114 | if not onlySelected then 115 | ni = ni + 1 116 | end 117 | 118 | if note[4] <= ppqTime and note[5] > ppqTime then 119 | table.insert(notes, note) 120 | end 121 | end 122 | 123 | -- sort notes by ascending pitch 124 | table.sort(notes, function(a, b) return a[7] < b[7] end) 125 | 126 | return notes 127 | end 128 | 129 | local function findNextSelNotePos(take, ni, ppqTime) 130 | while true do 131 | ni = reaper.MIDI_EnumSelNotes(take, ni) 132 | if ni < 0 then break end 133 | 134 | local note = {reaper.MIDI_GetNote(take, ni)} 135 | if not note[1] then break end 136 | 137 | if note[4] > ppqTime then 138 | return note[4] 139 | end 140 | end 141 | end 142 | 143 | local function getParentProject(track) 144 | local search = reaper.GetMediaTrackInfo_Value(track, 'P_PROJECT') 145 | 146 | for project in projects() do 147 | local master = reaper.GetMasterTrack(project) 148 | if search == reaper.GetMediaTrackInfo_Value(master, 'P_PROJECT') then 149 | return project 150 | end 151 | end 152 | end 153 | 154 | local function updateJSFXCursor(ppq) 155 | local index = findJSFX() 156 | reaper.TrackFX_SetParam(jsfx.track, index | 0x1000000, 0, ppq) 157 | jsfx.ppqTime = ppq 158 | end 159 | 160 | local function teardownJSFX() 161 | if not jsfx or not reaper.ValidatePtr2(nil, jsfx.project, 'ReaProject*') or 162 | not reaper.ValidatePtr2(jsfx.project, jsfx.track, 'MediaTrack*') then return end 163 | 164 | local index = findJSFX() 165 | if index then 166 | reaper.TrackFX_Delete(jsfx.track, index | 0x1000000) 167 | end 168 | 169 | jsfx = nil 170 | end 171 | 172 | local function installJSFX(take) 173 | local track = reaper.GetMediaItemTake_Track(take) 174 | if jsfx and track == jsfx.track then return true end 175 | 176 | local project = getParentProject(track) 177 | reaper.Undo_BeginBlock2(project) 178 | 179 | teardownJSFX() 180 | 181 | local index = reaper.TrackFX_AddByName(track, jsfxName, true, 1) 182 | jsfx = { 183 | guid = reaper.TrackFX_GetFXGUID(track, index | 0x1000000), 184 | project = project, 185 | track = track, 186 | } 187 | reaper.gmem_write(0, NOTE_BUFFER_START) 188 | 189 | -- Initialize JSFX with current cursor position for undo. 190 | local curPos = reaper.GetCursorPositionEx(project) 191 | local ppqTime = reaper.MIDI_GetPPQPosFromProjTime(take, curPos) 192 | updateJSFXCursor(ppqTime) 193 | 194 | reaper.Undo_EndBlock2(jsfx.project, 195 | 'Install step sequencing (replace mode) input FX', UNDO_STATE_FX) 196 | 197 | return index >= 0 198 | end 199 | 200 | local function readNoteBuffer() 201 | local chords = {} 202 | 203 | local bi = NOTE_BUFFER_START 204 | local be = reaper.gmem_read(0) - 1 205 | local function nextIndex() 206 | local i = bi 207 | bi = bi + 1 208 | return i 209 | end 210 | 211 | while bi < be do 212 | local noteSize = 4 213 | local noteCount = reaper.gmem_read(nextIndex()) / noteSize 214 | printf("received chord\tnotes=%s\n", noteCount) 215 | 216 | local notes = {} 217 | for ni = 1, noteCount do 218 | local note = { 219 | chan = reaper.gmem_read(nextIndex()), 220 | pitch = reaper.gmem_read(nextIndex()), 221 | vel = reaper.gmem_read(nextIndex()), 222 | isDown = reaper.gmem_read(nextIndex()), -- unused 223 | } 224 | 225 | printf(">\tnote %d\tchan=%s vel=%s\n", note.pitch, note.chan, note.vel) 226 | table.insert(notes, note) 227 | end 228 | 229 | table.sort(notes, function(a, b) return a.pitch < b.pitch end) 230 | table.insert(chords, notes) 231 | end 232 | 233 | reaper.gmem_write(0, NOTE_BUFFER_START) 234 | 235 | return chords 236 | end 237 | 238 | local function insertReplaceNotes(take, newNotes) 239 | local updated = false 240 | local qnGrid = reaper.MIDI_GetGrid(take) 241 | local curPos = reaper.GetCursorPositionEx(jsfx.project) 242 | local ppqTime = reaper.MIDI_GetPPQPosFromProjTime(take, curPos) 243 | local ppqNextTime = ppqTime 244 | local mode = getMode() 245 | local notesUnderCursor = findNotesAtTime(take, ppqTime, mode & MODE_SEL ~= 0) 246 | 247 | if ppqTime ~= jsfx.ppqTime then 248 | -- We're about to insert notes at a position different than the JSFX 249 | -- has stored. Explicitly create an undo point with the current cursor 250 | -- position so that if the soon-to-be-inserted notes are undone, the 251 | -- cursor is restored to this position. 252 | reaper.Undo_BeginBlock2(jsfx.project) 253 | updateJSFXCursor(ppqTime) 254 | reaper.Undo_EndBlock2(jsfx.project, 255 | 'Move cursor before step sequencing input', UNDO_STATE_FX) 256 | end 257 | 258 | reaper.Undo_BeginBlock2(jsfx.project) 259 | -- replace existing notes (lowest first) 260 | for ni = 1, math.min(#newNotes, #notesUnderCursor) do 261 | local note = notesUnderCursor[ni] 262 | 263 | ppqNextTime = math.max(ppqNextTime, note[5]) 264 | if mode & MODE_SEL ~= 0 then 265 | local nextSelPos = findNextSelNotePos(take, note[1], note[4]) 266 | if nextSelPos then ppqNextTime = nextSelPos end 267 | end 268 | 269 | if mode & MODE_CHAN ~= 0 then 270 | note[6] = newNotes[ni].chan 271 | end 272 | if mode & MODE_PITCH ~= 0 then 273 | note[7] = newNotes[ni].pitch 274 | end 275 | if mode & MODE_VEL ~= 0 then 276 | note[8] = newNotes[ni].vel 277 | end 278 | 279 | table.insert(note, 1, take) 280 | table.insert(note, true) -- noSort 281 | reaper.MIDI_SetNote(table.unpack(note)) 282 | updated = true 283 | end 284 | 285 | -- add any remaining notes 286 | for ni = #notesUnderCursor + 1, #newNotes do 287 | local note = newNotes[ni] 288 | local qnTime = reaper.MIDI_GetProjQNFromPPQPos(take, ppqTime) 289 | local ppqEnd = reaper.MIDI_GetPPQPosFromProjQN(take, qnTime + qnGrid) 290 | reaper.MIDI_InsertNote(take, true, false, ppqTime, ppqEnd, 291 | note.chan, note.pitch, note.vel, true) 292 | ppqNextTime = math.max(ppqNextTime, ppqEnd) 293 | if mode & MODE_SEL ~= 0 then 294 | local nextSelPos = findNextSelNotePos(take, -1, ppqTime) 295 | if nextSelPos then ppqNextTime = nextSelPos end 296 | end 297 | updated = true 298 | end 299 | 300 | if updated then 301 | reaper.MIDI_Sort(take) 302 | updateJSFXCursor(ppqNextTime) 303 | local item = reaper.GetMediaItemTake_Item(take) 304 | reaper.UpdateItemInProject(item) 305 | reaper.MarkTrackItemsDirty(jsfx.track, item) 306 | end 307 | 308 | if ppqNextTime > ppqTime then 309 | local nextTime = reaper.MIDI_GetProjTimeFromPPQPos(take, ppqNextTime) 310 | reaper.SetEditCurPos2(jsfx.project, nextTime, false, false) 311 | end 312 | 313 | reaper.Undo_EndBlock2(jsfx.project, 314 | 'Insert notes via step sequencing (replace mode)', 315 | UNDO_STATE_FX | UNDO_STATE_ITEMS) 316 | end 317 | 318 | local function loop() 319 | local take, me = getActiveTake() 320 | if not take then 321 | reaper.defer(loop) 322 | teardownJSFX() 323 | return 324 | end 325 | 326 | if not installJSFX(take) then 327 | reaper.MB('Fatal error: Failed to install helper effect in the input chain.', 328 | scriptName, MB_OK) 329 | return 330 | end 331 | 332 | if 0 < reaper.GetToggleCommandStateEx(MIDI_EDITOR_SECTION, NATIVE_STEP_RECORD) then 333 | return -- terminate the script 334 | end 335 | 336 | local index = findJSFX() 337 | if not index then 338 | -- The JSFX instance we think is installed is invalid. It was likely removed via 339 | -- undo. Terminate the script. 340 | return 341 | end 342 | local fxPPQTime = reaper.TrackFX_GetParam(jsfx.track, index | 0x1000000, 0) 343 | if fxPPQTime > -1 and jsfx.ppqTime and jsfx.ppqTime ~= fxPPQTime then 344 | -- Note insertion was undone. Restore cursor position based on undo point. 345 | local curPos = reaper.GetCursorPositionEx(jsfx.project) 346 | local fxTime = reaper.MIDI_GetProjTimeFromPPQPos(take, fxPPQTime) 347 | reaper.MoveEditCursor(fxTime - curPos, false) 348 | jsfx.ppqTime = fxPPQTime 349 | end 350 | 351 | local chords, lastNote = readNoteBuffer() 352 | for _, newNotes in ipairs(chords) do 353 | insertReplaceNotes(take, newNotes) 354 | lastNote = newNotes[1] 355 | end 356 | 357 | if lastNote then 358 | reaper.MIDIEditor_SetSetting_int(me, 'active_note_row', lastNote.pitch) 359 | end 360 | 361 | reaper.defer(loop) 362 | end 363 | 364 | local function gfxdo(callback) 365 | local app = reaper.GetAppVersion() 366 | if app:match('OSX') or app:match('linux') then 367 | return callback() 368 | end 369 | 370 | local curx, cury = reaper.GetMousePosition() 371 | gfx.init("", 0, 0, 0, curx, cury) 372 | 373 | if reaper.JS_Window_SetStyle then 374 | local window = reaper.JS_Window_GetFocus() 375 | local winx, winy = reaper.JS_Window_ClientToScreen(window, 0, 0) 376 | gfx.x = gfx.x - (winx - curx) 377 | gfx.y = gfx.y - (winy - cury) 378 | reaper.JS_Window_SetStyle(window, "POPUP") 379 | reaper.JS_Window_SetOpacity(window, 'ALPHA', 0) 380 | end 381 | 382 | local value = callback() 383 | gfx.quit() 384 | return value 385 | end 386 | 387 | local function optionsMenu(mode, items) 388 | local ctx = ImGui.CreateContext(scriptName, 389 | ImGui.ConfigFlags_NavEnableKeyboard | ImGui.ConfigFlags_NoSavedSettings) 390 | 391 | local size = reaper.GetAppVersion():match('OSX') and 12 or 14 392 | local font = ImGui.CreateFont('sans-serif', size) 393 | ImGui.Attach(ctx, font) 394 | 395 | local function loop() 396 | if ImGui.IsWindowAppearing(ctx) then 397 | ImGui.SetNextWindowPos(ctx, 398 | ImGui.PointConvertNative(ctx, reaper.GetMousePosition())) 399 | ImGui.OpenPopup(ctx, scriptName) 400 | end 401 | 402 | if ImGui.BeginPopup(ctx, scriptName, ImGui.WindowFlags_TopMost) then 403 | ImGui.PushFont(ctx, font) 404 | 405 | for id, item in ipairs(items) do 406 | if type(item) == 'table' then 407 | if ImGui.MenuItem(ctx, item[2], nil, mode & item[1]) then 408 | mode = mode ~ item[1] 409 | reaper.SetExtState(EXT_SECTION, EXT_MODE_KEY, mode, true) 410 | end 411 | else 412 | ImGui.Separator(ctx) 413 | end 414 | end 415 | ImGui.PopFont(ctx) 416 | ImGui.EndPopup(ctx) 417 | reaper.defer(loop) 418 | end 419 | end 420 | 421 | reaper.defer(loop) 422 | end 423 | 424 | local function legacyOptionsMenu(mode, items) 425 | local menu = {} 426 | 427 | for id, item in ipairs(items) do 428 | if type(item) == 'table' then 429 | local checkbox = mode & item[1] ~= 0 and '!' or '' 430 | table.insert(menu, checkbox .. item[2]) 431 | else 432 | table.insert(menu, item) 433 | end 434 | end 435 | 436 | local choice = gfx.showmenu(table.concat(menu, '|')) 437 | if not items[choice] then return end 438 | 439 | mode = mode ~ items[choice][1] 440 | reaper.SetExtState(EXT_SECTION, EXT_MODE_KEY, mode, true) 441 | end 442 | 443 | if scriptName:match('%(options%)') then 444 | local mode, items = getMode(), { 445 | {MODE_CHAN, 'Replace channel'}, 446 | {MODE_PITCH, 'Replace pitch'}, 447 | {MODE_VEL, 'Replace velocity'}, 448 | '|', 449 | {MODE_SEL, 'Skip unselected notes'}, 450 | } 451 | 452 | if ImGui then 453 | optionsMenu(mode, items) 454 | else 455 | gfxdo(function() legacyOptionsMenu(mode, items) end) 456 | end 457 | return 458 | end 459 | 460 | if reaper.GetToggleCommandStateEx(scriptSection, scriptId) > 0 then 461 | return 462 | end 463 | 464 | if 0 < reaper.GetToggleCommandStateEx(MIDI_EDITOR_SECTION, NATIVE_STEP_RECORD) then 465 | reaper.MIDIEditor_LastFocused_OnCommand(NATIVE_STEP_RECORD, false) 466 | end 467 | 468 | reaper.gmem_attach('cfillion_stepRecordReplace') 469 | reaper.SetToggleCommandState(scriptSection, scriptId, 1) 470 | reaper.RefreshToolbar2(scriptSection, scriptId) 471 | reaper.atexit(function() 472 | reaper.SetToggleCommandState(scriptSection, scriptId, 0) 473 | reaper.RefreshToolbar2(scriptSection, scriptId) 474 | 475 | teardownJSFX() 476 | 477 | reaper.gmem_write(0, 0) -- disable the global note buffer 478 | end) 479 | 480 | loop() 481 | -------------------------------------------------------------------------------- /cfillion_Toggle MIDI preview on transport change.lua: -------------------------------------------------------------------------------- 1 | -- @description Toggle MIDI preview on transport change 2 | -- @author cfillion 3 | -- @version 1.1 4 | -- @changelog 5 | -- Fixed preview toggling when reopening the midi editor 6 | -- @links 7 | -- https://cfillion.tk 8 | -- Forum Thread https://forum.cockos.com/showthread.php?t=169896 9 | -- @donation https://www.paypal.me/cfillion 10 | -- 11 | -- Send patches at . 12 | 13 | local TOGGLE_CMD, last_state, do_toggle = 40041, false, false 14 | 15 | function main_loop() 16 | local state = reaper.GetPlayState() == 1 17 | 18 | if do_toggle and reaper.MIDIEditor_GetActive() then 19 | reaper.MIDIEditor_LastFocused_OnCommand(TOGGLE_CMD, 0) 20 | do_toggle = false 21 | end 22 | 23 | if state ~= last_state then 24 | do_toggle = not do_toggle 25 | last_state = state 26 | end 27 | 28 | reaper.defer(main_loop) 29 | end 30 | 31 | reaper.defer(main_loop) 32 | -------------------------------------------------------------------------------- /cfillion_Toggle input FX bypass for selected tracks.lua: -------------------------------------------------------------------------------- 1 | -- @description Toggle input FX bypass for selected tracks (8 actions) 2 | -- @version 1.0 3 | -- @author cfillion 4 | -- @metapackage 5 | -- @provides 6 | -- [main] . > cfillion_Toggle input FX 1 bypass for selected tracks.lua 7 | -- [main] . > cfillion_Toggle input FX 2 bypass for selected tracks.lua 8 | -- [main] . > cfillion_Toggle input FX 3 bypass for selected tracks.lua 9 | -- [main] . > cfillion_Toggle input FX 4 bypass for selected tracks.lua 10 | -- [main] . > cfillion_Toggle input FX 5 bypass for selected tracks.lua 11 | -- [main] . > cfillion_Toggle input FX 6 bypass for selected tracks.lua 12 | -- [main] . > cfillion_Toggle input FX 7 bypass for selected tracks.lua 13 | -- [main] . > cfillion_Toggle input FX 8 bypass for selected tracks.lua 14 | 15 | local UNDO_STATE_FX = 2 -- track/master fx 16 | 17 | local name = ({reaper.get_action_context()})[2]:match("([^/\\_]+)%.lua$") 18 | local fxIndex = tonumber(name:match("FX (%d+)")) 19 | 20 | if fxIndex then 21 | fxIndex = 0x1000000 + (fxIndex - 1) 22 | else 23 | error('could not extract slot from filename') 24 | end 25 | 26 | reaper.Undo_BeginBlock() 27 | 28 | for ti=0,reaper.CountSelectedTracks()-1 do 29 | local track = reaper.GetSelectedTrack(0, ti) 30 | 31 | reaper.TrackFX_SetEnabled(track, fxIndex, 32 | not reaper.TrackFX_GetEnabled(track, fxIndex)) 33 | end 34 | 35 | reaper.Undo_EndBlock(name, UNDO_STATE_FX) 36 | -------------------------------------------------------------------------------- /cfillion_Toggle take FX bypass for selected items.lua: -------------------------------------------------------------------------------- 1 | -- @description Toggle take FX bypass for selected items (5 slots) 2 | -- @version 1.0.1 3 | -- @changelog More efficient packaging of the slots for ReaPack. 4 | -- @author cfillion 5 | -- @link Request Thread https://forum.cockos.com/showthread.php?t=181160 6 | -- @metapackage 7 | -- @provides 8 | -- [main] . > cfillion_Toggle take FX 1 bypass for selected items.lua 9 | -- [main] . > cfillion_Toggle take FX 2 bypass for selected items.lua 10 | -- [main] . > cfillion_Toggle take FX 3 bypass for selected items.lua 11 | -- [main] . > cfillion_Toggle take FX 4 bypass for selected items.lua 12 | -- [main] . > cfillion_Toggle take FX 5 bypass for selected items.lua 13 | 14 | local name = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 15 | local fxIndex = tonumber(name:match("FX (%d+)")) 16 | 17 | if fxIndex then 18 | fxIndex = fxIndex - 1 19 | else 20 | error('could not extract slot from filename') 21 | end 22 | 23 | reaper.Undo_BeginBlock() 24 | 25 | for i=0,reaper.CountSelectedMediaItems()-1 do 26 | local item = reaper.GetSelectedMediaItem(0, i) 27 | local take = reaper.GetActiveTake(item) 28 | 29 | reaper.TakeFX_SetEnabled(take, fxIndex, 30 | not reaper.TakeFX_GetEnabled(take, fxIndex)) 31 | end 32 | 33 | reaper.Undo_EndBlock(name, 1) 34 | -------------------------------------------------------------------------------- /cfillion_Toggle track FX bypass by name.lua: -------------------------------------------------------------------------------- 1 | -- @description Toggle track FX bypass by name 2 | -- @author cfillion 3 | -- @version 2.0 4 | -- @changelog 5 | -- Add an option to include input and monitoring effects [cfillion/reascripts#6] 6 | -- Fix truncated labels by replacing the GetUserInputs prompt with a ReaImGui interface 7 | -- @provides 8 | -- . 9 | -- [main] . > cfillion_Toggle track FX bypass by name (create action).lua 10 | -- @link http://forum.cockos.com/showthread.php?t=184623 11 | -- @screenshot 12 | -- Basic Usage https://i.imgur.com/jVgwbi3.gif 13 | -- Undo Points https://i.imgur.com/dtNwlsn.png 14 | -- @about 15 | -- # Toggle track FX bypass by name 16 | -- 17 | -- This script asks for a string to match against all track FX in the current 18 | -- project, matching tracks or selected tracks. The search is case insensitive. 19 | -- Bypass is toggled for all matching FXs. Undo points are consolidated into one. 20 | -- 21 | -- This script can also be used to create custom actions that bypass matching 22 | -- track effects without always requesting user input. 23 | 24 | local script_path = select(2, reaper.get_action_context()) 25 | local script_name = script_path:match("([^/\\_]+)%.lua$") 26 | 27 | if not reaper.GetTrackName then 28 | -- for REAPER prior to v5.30 (native GetTrackName returns "Track N" when it's empty) 29 | function reaper.GetTrackName(track, _) 30 | return reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', '', false) 31 | end 32 | end 33 | 34 | local function matchTrack(track, filter) 35 | if filter == '/selected' then 36 | return reaper.IsTrackSelected(track) 37 | elseif filter == '/master' then 38 | return reaper.GetMasterTrack(nil) == track 39 | else 40 | local _, name = reaper.GetTrackName(track, '') 41 | return name:lower():find(filter) 42 | end 43 | end 44 | 45 | local function sanitizeFilename(name) 46 | if #name < 1 then return '(any)' end 47 | -- replace special characters that are reserved on Windows 48 | return name:gsub("[*\\:<>?/|\"%c]+", '-') 49 | end 50 | 51 | local function createAction() 52 | local fx_filter_fn = sanitizeFilename(fx_filter) 53 | local action_name = ('Toggle track FX bypass by name - %s'):format(fx_filter_fn) 54 | local output_fn = ('%s/Scripts/%s.lua'):format( 55 | reaper.GetResourcePath(), action_name) 56 | local base_name = script_path:match('([^/\\]+)$') 57 | local rel_path = script_path:sub(reaper.GetResourcePath():len() + 2) 58 | 59 | local code = ([[ 60 | -- This file was created by %s on %s 61 | 62 | fx_filter, track_filter, search_record = %q, %q, %q 63 | dofile((%q):format(reaper.GetResourcePath())) 64 | ]]):format(base_name, os.date('%c'), fx_filter, track_filter, search_record, 65 | '%s/'..rel_path) 66 | 67 | local file = assert(io.open(output_fn, 'w')) 68 | file:write(code) 69 | file:close() 70 | 71 | if reaper.AddRemoveReaScript(true, 0, output_fn, true) == 0 then 72 | reaper.ShowMessageBox( 73 | 'Failed to create or register the new action.', script_name, 0) 74 | return 75 | end 76 | 77 | reaper.ShowMessageBox( 78 | ('Created the action "%s".'):format(action_name), script_name, 0) 79 | end 80 | 81 | local function matchToggleFX(track, fi, fx_filter) 82 | local fx_name = select(2, reaper.TrackFX_GetFXName(track, fi, '')) 83 | if fx_name:lower():find(fx_filter) then 84 | reaper.TrackFX_SetEnabled(track, fi, 85 | not reaper.TrackFX_GetEnabled(track, fi)) 86 | end 87 | end 88 | 89 | local function run() 90 | local fx_filter, track_filter = fx_filter:lower(), track_filter:lower() 91 | 92 | reaper.PreventUIRefresh(1) 93 | reaper.Undo_BeginBlock() 94 | 95 | for ti = 0, reaper.CountTracks() do 96 | local track = reaper.CSurf_TrackFromID(ti, false) 97 | 98 | if matchTrack(track, track_filter) then 99 | for fi = 0, reaper.TrackFX_GetCount(track) - 1 do 100 | matchToggleFX(track, fi, fx_filter) 101 | end 102 | if search_record then 103 | for fi = 0, reaper.TrackFX_GetRecCount(track) - 1 do 104 | matchToggleFX(track, 0x1000000 + fi, fx_filter) 105 | end 106 | end 107 | end 108 | end 109 | 110 | reaper.Undo_EndBlock( 111 | ("Toggle track FX bypass matching '%s'"):format(fx_filter), -1) 112 | reaper.PreventUIRefresh(-1) 113 | end 114 | 115 | if fx_filter and track_filter then return run() end -- user action 116 | 117 | fx_filter, track_filter = '', '' 118 | local mode = (function() 119 | if reaper.CountSelectedTracks() > 0 then 120 | return 1 121 | elseif reaper.IsTrackSelected(reaper.GetMasterTrack(nil)) then 122 | return 2 123 | else 124 | return 0 125 | end 126 | end)() 127 | search_record = false 128 | 129 | if reaper.CountSelectedTracks() == 1 then 130 | local sel_track = reaper.GetSelectedTrack(nil, 0) 131 | track_filter = select(2, 132 | reaper.GetSetMediaTrackInfo_String(sel_track, 'P_NAME', '', false)) 133 | end 134 | 135 | local is_create_action = 136 | script_name == 'Toggle track FX bypass by name (create action)' 137 | 138 | if not reaper.ImGui_GetBuiltinPath then 139 | reaper.MB('This script requires ReaImGui. \z 140 | Install it from ReaPack > Browse packages.', script_name, 0) 141 | return 142 | end 143 | 144 | package.path = reaper.ImGui_GetBuiltinPath() .. '/?.lua' 145 | local ImGui = require 'imgui' '0.9' 146 | local ctx = ImGui.CreateContext('Toggle track FX bypass by name') 147 | local font = ImGui.CreateFont('sans-serif', 148 | reaper.GetAppVersion():match('OSX') and 12 or 14) 149 | ImGui.Attach(ctx, font) 150 | 151 | local function helpTooltip(ctx, desc) 152 | ImGui.TextDisabled(ctx, '(?)') 153 | ImGui.SetItemTooltip(ctx, desc) 154 | end 155 | 156 | local function loop() 157 | ImGui.PushFont(ctx, font) 158 | local visible, open = ImGui.Begin(ctx, script_name .. '###window', true, 159 | ImGui.WindowFlags_AlwaysAutoResize) 160 | if visible then 161 | ImGui.Text(ctx, 'Toggle bypass of effects matching:') 162 | if ImGui.IsWindowAppearing(ctx) then ImGui.SetKeyboardFocusHere(ctx) end 163 | fx_filter = select(2, ImGui.InputText(ctx, 'Effect name', fx_filter)) 164 | ImGui.SameLine(ctx) 165 | helpTooltip(ctx, 'Search is case-insensitive.') 166 | 167 | ImGui.Spacing(ctx) 168 | 169 | ImGui.Text(ctx, '...on tracks:') 170 | if ImGui.RadioButton(ctx, 'Selected tracks', mode == 1) then 171 | mode = 1 172 | end 173 | if ImGui.RadioButton(ctx, 'Master track', mode == 2) then 174 | mode = 2 175 | end 176 | if ImGui.RadioButton(ctx, 'Track name matching:', mode == 0) then 177 | mode = 0 178 | ImGui.SetKeyboardFocusHere(ctx) 179 | end 180 | ImGui.BeginDisabled(ctx, mode ~= 0) 181 | track_filter = select(2, ImGui.InputText(ctx, 'Track name', track_filter)) 182 | ImGui.EndDisabled(ctx) 183 | ImGui.SameLine(ctx) 184 | helpTooltip(ctx, 'Search is case-insensitive. Leave empty to search all tracks.') 185 | 186 | ImGui.Spacing(ctx) 187 | 188 | search_record = select(2, ImGui.Checkbox(ctx, 189 | 'Include input and monitoring effects', search_record)) 190 | 191 | ImGui.Spacing(ctx) 192 | 193 | local btn_spacing = ImGui.GetStyleVar(ctx, ImGui.StyleVar_ItemSpacing) 194 | local btn_w = (ImGui.GetContentRegionAvail(ctx) - btn_spacing) // 2 195 | if ImGui.Button(ctx, is_create_action and 'Create action' or 'OK', btn_w) or 196 | ImGui.IsKeyPressed(ctx, ImGui.Key_Enter) or 197 | ImGui.IsKeyPressed(ctx, ImGui.Key_KeypadEnter) then 198 | if mode == 1 then 199 | track_filter = '/selected' 200 | elseif mode == 2 then 201 | track_filter = '/master' 202 | end 203 | (is_create_action and createAction or run)() 204 | open = false 205 | end 206 | ImGui.SameLine(ctx) 207 | if ImGui.Button(ctx, 'Cancel', btn_w) or 208 | ImGui.IsKeyPressed(ctx, ImGui.Key_Escape) then 209 | open = false 210 | end 211 | ImGui.End(ctx) 212 | end 213 | if open then 214 | reaper.defer(loop) 215 | end 216 | ImGui.PopFont(ctx) 217 | end 218 | 219 | reaper.defer(loop) 220 | -------------------------------------------------------------------------------- /cfillion_Toggle visibility of empty non-folder tracks.lua: -------------------------------------------------------------------------------- 1 | -- @description Toggle visibility of empty non-folder tracks 2 | -- @version 1.1 3 | -- @author cfillion 4 | -- @link 5 | -- cfillion.ca https://cfillion.ca/ 6 | -- Forum Thread https://forum.cockos.com/showthread.php?t=187651 7 | -- @donation https://www.paypal.me/cfillion 8 | -- @provides 9 | -- [main] . 10 | -- [main] . > cfillion_Toggle visibility of empty non-folder tracks matching '•'.lua 11 | -- @changelog 12 | -- Fixed embarassing typo in the name ('visiblity' -> 'visibility') 13 | -- Added a "matching '•'" variant of the script at the request of blumpy 14 | -- @about 15 | -- # Toggle visibility of empty non-folder tracks 16 | -- 17 | -- This script toggles the TCP and MCP visibility of tracks matching all of the 18 | -- following criteria: 19 | -- 20 | -- - Track has no FX on it 21 | -- - Track does not contain any media item 22 | -- - Track has no envelopes 23 | -- - Track is not a folder 24 | -- - Track is not record armed 25 | -- 26 | -- A variant of the script is provided which additionally requires tracks to 27 | -- contain the character • (bullet) in their name. Custom variants can be 28 | -- created by copying the script with the desired track name search string 29 | -- between the apostrophes in the filename. 30 | 31 | local UNDO_STATE_TRACKCFG = 1 32 | local script_name = ({reaper.get_action_context()})[2]:match("([^/\\_]+).lua$") 33 | local empty_tracks, setVisible = {}, 0 34 | local match = script_name:match("matching '([^']+)'") 35 | 36 | for ti=0,reaper.CountTracks()-1 do 37 | local track = reaper.GetTrack(0, ti) 38 | 39 | local fx_count = reaper.TrackFX_GetCount(track) 40 | local item_count = reaper.CountTrackMediaItems(track) 41 | local env_count = reaper.CountTrackEnvelopes(track) 42 | local depth = reaper.GetMediaTrackInfo_Value(track, "I_FOLDERDEPTH") 43 | local is_armed = reaper.GetMediaTrackInfo_Value(track, "I_RECARM") 44 | local name = ({reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', '', false)})[2] 45 | local matches = (function() 46 | if not match or name:find(match) then 47 | return 0 48 | else 49 | return 1 50 | end 51 | end)() 52 | 53 | if fx_count + item_count + env_count + math.max(depth, 0) + is_armed + matches == 0 then 54 | local mcpVis = reaper.GetMediaTrackInfo_Value(track, 'B_SHOWINMIXER') 55 | local tcpVis = reaper.GetMediaTrackInfo_Value(track, 'B_SHOWINMIXER') 56 | 57 | if mcpVis + tcpVis == 0 then 58 | setVisible = 1 59 | end 60 | 61 | table.insert(empty_tracks, track) 62 | end 63 | end 64 | 65 | if #empty_tracks == 0 then return reaper.defer(function() end) end 66 | 67 | reaper.Undo_BeginBlock() 68 | 69 | for i,track in ipairs(empty_tracks) do 70 | reaper.SetMediaTrackInfo_Value(track, 'B_SHOWINMIXER', setVisible) 71 | reaper.SetMediaTrackInfo_Value(track, 'B_SHOWINTCP', setVisible) 72 | end 73 | 74 | reaper.Undo_EndBlock(script_name, UNDO_STATE_TRACKCFG) 75 | 76 | reaper.TrackList_AdjustWindows(false) 77 | reaper.UpdateArrange() 78 | -------------------------------------------------------------------------------- /delete_empty_tracks.lua: -------------------------------------------------------------------------------- 1 | -- delete_empty_tracks.lua v0.1 by Christian Fillion (cfillion) 2 | 3 | reaper.PreventUIRefresh(1) 4 | reaper.Undo_BeginBlock() 5 | 6 | local track_index, track_count = 0, reaper.CountTracks() 7 | local bucket, bucket_index = {}, 0 8 | 9 | while track_index < track_count do 10 | local track = reaper.GetTrack(0, track_index) 11 | 12 | local fx_count = reaper.TrackFX_GetCount(track) 13 | local item_count = reaper.CountTrackMediaItems(track) 14 | local env_count = reaper.CountTrackEnvelopes(track) 15 | local depth = reaper.GetMediaTrackInfo_Value(track, "I_FOLDERDEPTH") 16 | local is_armed = reaper.GetMediaTrackInfo_Value(track, "I_RECARM") 17 | 18 | if fx_count + item_count + env_count + math.max(depth, 0) + is_armed == 0 then 19 | bucket[bucket_index] = track 20 | bucket_index = bucket_index + 1 21 | end 22 | 23 | track_index = track_index + 1 24 | end 25 | 26 | if bucket_index > 0 then 27 | local dialog_btn = reaper.ShowMessageBox( 28 | string.format("Delete %d empty tracks?", bucket_index), 29 | "Confirmation", 1 30 | ) 31 | 32 | if dialog_btn == 1 then 33 | local track_index = 0 34 | 35 | while track_index < bucket_index do 36 | reaper.DeleteTrack(bucket[track_index]) 37 | track_index = track_index + 1 38 | end 39 | end 40 | end 41 | 42 | reaper.Undo_EndBlock("Delete Empty Tracks", 1) 43 | reaper.PreventUIRefresh(-1) 44 | -------------------------------------------------------------------------------- /explode_selected_tracks_to_mono_tracks.lua: -------------------------------------------------------------------------------- 1 | -- Explode selected tracks to mono tracks 2 | -- https://forum.cockos.com/showthread.php?p=1647321 3 | -- @version 1.1 4 | -- @author cfillion 5 | 6 | local tracks = reaper.CountSelectedTracks(0) 7 | 8 | if tracks < 1 then 9 | reaper.ShowMessageBox("Select some tracks and retry.", "Selection is empty!", 0) 10 | return 11 | end 12 | 13 | reaper.PreventUIRefresh(1) 14 | reaper.Undo_BeginBlock() 15 | 16 | for trackIndex=0,tracks-1 do 17 | local parent = reaper.GetSelectedTrack(0, trackIndex) 18 | local chans = reaper.GetMediaTrackInfo_Value(parent, "I_NCHAN") 19 | local trackId = reaper.GetMediaTrackInfo_Value(parent, "IP_TRACKNUMBER") 20 | local _, name = reaper.GetSetMediaTrackInfo_String(parent, "P_NAME", "", false) 21 | 22 | reaper.SetMediaTrackInfo_Value(parent, "B_MAINSEND", 0) 23 | 24 | for chanIndex=0,chans-1 do 25 | local insertIndex = trackId + chanIndex 26 | reaper.InsertTrackAtIndex(insertIndex, true) 27 | track = reaper.GetTrack(0, insertIndex) 28 | 29 | local send = reaper.CreateTrackSend(parent, track) 30 | reaper.SetTrackSendInfo_Value(parent, 0, send, "I_SRCCHAN", chanIndex | 1024) 31 | reaper.GetSetMediaTrackInfo_String(track, "P_NAME", 32 | string.format("Ch. %d - %s", chanIndex + 1, name), true) 33 | end 34 | end 35 | 36 | reaper.Undo_EndBlock("Explode selected tracks to mono tracks", 1) 37 | 38 | reaper.PreventUIRefresh(-1) 39 | reaper.TrackList_AdjustWindows(false) 40 | -------------------------------------------------------------------------------- /instrument_track.lua: -------------------------------------------------------------------------------- 1 | -- instrument_track.lua v0.2 by Christian Fillion (cfillion) 2 | -- https://forum.cockos.com/showthread.php?t=165984 3 | 4 | MAX_CHANNEL_COUNT = 32 -- set this to 64 if you need to use two MIDI busses 5 | MAX_MIDI_BUS = MAX_CHANNEL_COUNT / (16 * 2) 6 | 7 | function GetTrackMidiReceives(track) 8 | local list, index, recvIndex, recvCount 9 | 10 | list, index, recvIndex = {}, 0, 0 11 | recvCount = reaper.GetTrackNumSends(track, -1) 12 | 13 | while recvIndex < recvCount do 14 | local dstBus, dstChan 15 | 16 | dstBus = reaper.BR_GetSetTrackSendInfo( 17 | track, -1, recvIndex, "I_MIDI_DSTBUS", false, 0) 18 | dstChan = reaper.BR_GetSetTrackSendInfo( 19 | track, -1, recvIndex, "I_MIDI_DSTCHAN", false, 0) 20 | 21 | if dstBus + dstChan > -1 then 22 | if dstBus == 0 then dstBus = 1 end 23 | list[index] = {BUS=dstBus, CHAN=dstChan} 24 | index = index + 1 25 | end 26 | 27 | recvIndex = recvIndex + 1 28 | end 29 | 30 | return list 31 | end 32 | 33 | function GetUnusedMidiChannel(sends) 34 | local index, busses 35 | 36 | busses = {} 37 | 38 | index = 1 39 | while index <= MAX_MIDI_BUS do 40 | busses[index] = 0 41 | index = index + 1 42 | end 43 | 44 | for _,send in pairs(sends) do 45 | local bus = math.floor(send["BUS"]) 46 | local chan = math.floor(send["CHAN"]) 47 | 48 | if busses[bus] ~= nil and busses[bus] < chan then 49 | busses[bus] = chan 50 | end 51 | end 52 | 53 | for i,chan in pairs(busses) do 54 | if chan <= 15 then 55 | return i,chan+1 56 | end 57 | end 58 | 59 | return -1, -1 60 | end 61 | 62 | function GetUnusedSlot() 63 | local trackId, smplId, sampler, sends, bus, chan 64 | 65 | bus, chan = -1, -1 66 | 67 | while chan == -1 do 68 | trackId, smplId, sampler = GetLastSampler() 69 | 70 | if trackId == -1 then 71 | smplId = 1 72 | sampler = InsertSamplerAt(0, 1) 73 | end 74 | 75 | sends = GetTrackMidiReceives(sampler) 76 | bus, chan = GetUnusedMidiChannel(sends) 77 | 78 | if chan == -1 then 79 | if smplId == 1 then 80 | -- append the omitted sampler ID to the first sampler track 81 | reaper.GetSetMediaTrackInfo_String(sampler, "P_NAME", "Sampler 1", true) 82 | end 83 | 84 | InsertSamplerAt(trackId+1, smplId+1) 85 | end 86 | end 87 | 88 | return sampler, smplId, bus, chan 89 | end 90 | 91 | function GetLastSampler() 92 | local index, trackCount = 0, reaper.GetNumTracks() 93 | local trackId, lastId, sampler = -1, 0, nil 94 | 95 | while index < trackCount do 96 | local track = reaper.GetTrack(0, index) 97 | local _, name = reaper.GetSetMediaTrackInfo_String( 98 | track, "P_NAME", "", false) 99 | 100 | local match = string.find(name, "^Sampler%s?") 101 | if match ~= nil then 102 | local id = tonumber(string.sub(name, 9)) 103 | if id == nil then id = 1 end 104 | 105 | if lastId < id then 106 | trackId = index 107 | lastId = id 108 | sampler = track 109 | end 110 | end 111 | 112 | index = index + 1 113 | end 114 | 115 | return trackId, lastId, sampler 116 | end 117 | 118 | function InsertSamplerAt(index, id) 119 | reaper.InsertTrackAtIndex(index, false) 120 | track = reaper.GetTrack(0, index) 121 | 122 | local name = "Sampler" 123 | if id > 1 then name = string.format("%s %d", name, id) end 124 | 125 | reaper.GetSetMediaTrackInfo_String(track, "P_NAME", name, true) 126 | reaper.SetMediaTrackInfo_Value(track, "B_MAINSEND", 0) 127 | reaper.SetMediaTrackInfo_Value(track, "B_SHOWINTCP", 0) 128 | reaper.SetMediaTrackInfo_Value(track, "I_NCHAN", MAX_CHANNEL_COUNT) 129 | 130 | -- set recording mode to multichannel output 131 | reaper.SetMediaTrackInfo_Value(track, "I_RECMODE", 10) 132 | 133 | return track 134 | end 135 | 136 | function MidiToAudioChannel(bus, chan) 137 | if bus == 1 then bus = 0 end 138 | return (bus * 16) + (chan - 1) * 2 139 | end 140 | 141 | function GetInsertionPoint() 142 | local selectionSize = reaper.CountSelectedTracks(0) 143 | if selectionSize == 0 then 144 | return reaper.GetNumTracks() 145 | end 146 | 147 | track = reaper.GetSelectedTrack(0, selectionSize - 1) 148 | return reaper.GetMediaTrackInfo_Value(track, "IP_TRACKNUMBER") 149 | end 150 | 151 | reaper.PreventUIRefresh(1) 152 | reaper.Undo_BeginBlock() 153 | 154 | local sampler, smplId, bus, chan = GetUnusedSlot() 155 | local audioChan = MidiToAudioChannel(bus, chan) 156 | local insertPos = GetInsertionPoint() 157 | 158 | -- create AUDIO track 159 | reaper.InsertTrackAtIndex(insertPos, true) 160 | audioTrack = reaper.GetTrack(0, insertPos) 161 | reaper.SetMediaTrackInfo_Value(audioTrack, "I_FOLDERDEPTH", 1) 162 | reaper.SetMediaTrackInfo_Value(audioTrack, "I_HEIGHTOVERRIDE", 1) 163 | reaper.SetMediaTrackInfo_Value(audioTrack, "I_RECMODE", 1) 164 | reaper.GetSetMediaTrackInfo_String(audioTrack, "P_NAME", 165 | string.format("#%d %d/%d", smplId, audioChan+1, audioChan+2), true) 166 | 167 | reaper.SNM_AddReceive(sampler, audioTrack, -1) 168 | reaper.BR_GetSetTrackSendInfo( 169 | audioTrack, -1, 0, "I_SRCCHAN", true, audioChan) 170 | reaper.BR_GetSetTrackSendInfo( 171 | audioTrack, -1, 0, "I_DSTCHAN", true, 0) 172 | reaper.BR_GetSetTrackSendInfo( 173 | audioTrack, -1, 0, "I_MIDI_SRCCHAN", true, -1) 174 | 175 | -- create MIDI track 176 | reaper.InsertTrackAtIndex(insertPos+1, true) 177 | midiTrack = reaper.GetTrack(0, insertPos+1) 178 | reaper.SetMediaTrackInfo_Value(midiTrack, "B_SHOWINMIXER", 0) 179 | reaper.SetMediaTrackInfo_Value(midiTrack, "I_FOLDERDEPTH", -1) 180 | reaper.SetMediaTrackInfo_Value(midiTrack, "I_RECMON", 1) 181 | reaper.GetSetMediaTrackInfo_String(midiTrack, "P_NAME", 182 | string.format("-> #%d B:%d C:%d", smplId, bus, chan), true) 183 | reaper.SNM_AddReceive(midiTrack, sampler, 0) 184 | reaper.BR_GetSetTrackSendInfo( 185 | midiTrack, 0, 0, "I_SRCCHAN", true, -1) 186 | reaper.BR_GetSetTrackSendInfo( 187 | midiTrack, 0, 0, "I_MIDI_SRCBUS", true, 0) 188 | reaper.BR_GetSetTrackSendInfo( 189 | midiTrack, 0, 0, "I_MIDI_SRCCHAN", true, 0) 190 | reaper.BR_GetSetTrackSendInfo( 191 | midiTrack, 0, 0, "I_MIDI_DSTBUS", true, bus) 192 | reaper.BR_GetSetTrackSendInfo( 193 | midiTrack, 0, 0, "I_MIDI_DSTCHAN", true, chan) 194 | 195 | reaper.Undo_EndBlock("Create Instrument Track", 1) 196 | 197 | reaper.PreventUIRefresh(-1) 198 | reaper.TrackList_AdjustWindows(false) 199 | -------------------------------------------------------------------------------- /open_terminal_in_project_directory.lua: -------------------------------------------------------------------------------- 1 | -- escape function from shell.lua, by Peter Odding 2 | -- https://github.com/lua-shellscript/lua-shellscript/wiki/shell.string 3 | function escape(...) 4 | local command = type(...) == 'table' and ... or { ... } 5 | 6 | for i, s in ipairs(command) do 7 | s = (tostring(s) or ''):gsub('"', '\\"') 8 | 9 | if s:find '[^A-Za-z0-9_."/-]' then 10 | s = '"' .. s .. '"' 11 | elseif s == '' then 12 | s = '""' 13 | end 14 | 15 | command[i] = s 16 | end 17 | 18 | return table.concat(command, ' ') 19 | end 20 | 21 | -- dirname from nativeclient-sdk (chromium) 22 | -- https://code.google.com/p/nativeclient-sdk/source/browse/trunk/src/nacltoons/data/res/path.lua 23 | function dirname(filename) 24 | while true do 25 | if filename == "" or string.sub(filename, -1) == "/" then 26 | break 27 | end 28 | filename = string.sub(filename, 1, -2) 29 | end 30 | if filename == "" then 31 | filename = "." 32 | end 33 | 34 | return filename 35 | end 36 | 37 | local _, projectFile = reaper.EnumProjects(-1, '') 38 | local path 39 | 40 | if string.len(projectFile) == 0 then 41 | path = reaper.GetProjectPath('') 42 | else 43 | path = dirname(projectFile) 44 | end 45 | 46 | os.execute("open -a Terminal.app " .. escape(path)) 47 | reaper.defer(function() end) 48 | -------------------------------------------------------------------------------- /select_items_right_of_selection.lua: -------------------------------------------------------------------------------- 1 | -- Already exists in SWS: Xenakios/SWS: Select items to end of track 2 | 3 | function select_items(cmpCallback) 4 | local targetItems = {} 5 | 6 | for si=0,reaper.CountSelectedMediaItems(0)-1 do 7 | local selectedItem = reaper.GetSelectedMediaItem(0, si) 8 | local track = reaper.GetMediaItemTrack(selectedItem) 9 | local selectedPos = get_item_pos(selectedItem) 10 | 11 | for i=0,reaper.CountTrackMediaItems(track)-1 do 12 | local item = reaper.GetTrackMediaItem(track, i) 13 | local itemPos = get_item_pos(item) 14 | 15 | if cmpCallback(selectedPos, itemPos) then 16 | targetItems[#targetItems + 1] = item 17 | end 18 | end 19 | end 20 | 21 | for _,item in ipairs(targetItems) do 22 | reaper.SetMediaItemSelected(item, true) 23 | end 24 | end 25 | 26 | function get_item_pos(item) 27 | local start = reaper.GetMediaItemInfo_Value(item, "D_POSITION") 28 | local length = reaper.GetMediaItemInfo_Value(item, "D_LENGTH") 29 | return {item_start=start, item_end=start + length} 30 | end 31 | 32 | reaper.Undo_BeginBlock() 33 | 34 | select_items(function(selectedPos, targetPos) 35 | return targetPos.item_end < selectedPos.item_start 36 | end) 37 | 38 | reaper.UpdateArrange() 39 | reaper.Undo_EndBlock("Select items to the left of selected item", 1) 40 | -------------------------------------------------------------------------------- /send_track_audio.lua: -------------------------------------------------------------------------------- 1 | -- send_track_audio.lua v0.1 by Christian Fillion (cfillion) 2 | -- https://forum.cockos.com/showthread.php?p=1576038 3 | 4 | local selectionSize = reaper.CountSelectedTracks(0) 5 | 6 | if selectionSize < 1 then 7 | reaper.Main_OnCommand(40001, 0) -- Track: Insert new track 8 | return 9 | end 10 | 11 | reaper.PreventUIRefresh(1) 12 | reaper.Undo_BeginBlock() 13 | 14 | local insertPos, index = reaper.GetNumTracks(), 0 15 | reaper.InsertTrackAtIndex(insertPos, true) 16 | local track = reaper.GetTrack(0, insertPos) 17 | 18 | while index < selectionSize do 19 | reaper.SNM_AddReceive(track, reaper.GetSelectedTrack(0, index), 0) 20 | reaper.BR_GetSetTrackSendInfo( 21 | track, 0, index, "I_SRCCHAN", true, 0) 22 | reaper.BR_GetSetTrackSendInfo( 23 | track, 0, index, "I_DSTCHAN", true, 0) 24 | reaper.BR_GetSetTrackSendInfo( 25 | track, 0, index, "I_MIDI_SRCCHAN", true, -1) 26 | 27 | index = index + 1 28 | end 29 | 30 | reaper.SetOnlyTrackSelected(track) 31 | 32 | reaper.Undo_EndBlock("Create Send Track", 1) 33 | 34 | reaper.PreventUIRefresh(-1) 35 | reaper.TrackList_AdjustWindows(false) 36 | -------------------------------------------------------------------------------- /send_track_midi.lua: -------------------------------------------------------------------------------- 1 | -- send_track_midi.lua v0.1 by Christian Fillion (cfillion) 2 | -- https://forum.cockos.com/showpost.php?p=1580440 3 | 4 | local selectionSize = reaper.CountSelectedTracks(0) 5 | 6 | if selectionSize < 1 then 7 | reaper.Main_OnCommand(40001, 0) -- Track: Insert new track 8 | return 9 | end 10 | 11 | reaper.PreventUIRefresh(1) 12 | reaper.Undo_BeginBlock() 13 | 14 | local insertPos, index = reaper.GetNumTracks(), 0 15 | reaper.InsertTrackAtIndex(insertPos, true) 16 | local track = reaper.GetTrack(0, insertPos) 17 | 18 | while index < selectionSize do 19 | reaper.SNM_AddReceive(track, reaper.GetSelectedTrack(0, index), 0) 20 | reaper.BR_GetSetTrackSendInfo( 21 | track, 0, index, "I_SRCCHAN", true, -1) 22 | reaper.BR_GetSetTrackSendInfo( 23 | track, 0, index, "I_MIDI_SRCCHAN", true, 0) 24 | reaper.BR_GetSetTrackSendInfo( 25 | track, 0, index, "I_MIDI_DSTCHAN", true, 0) 26 | 27 | index = index + 1 28 | end 29 | 30 | reaper.SetOnlyTrackSelected(track) 31 | 32 | reaper.Undo_EndBlock("Create MIDI Send Track", 1) 33 | 34 | reaper.PreventUIRefresh(-1) 35 | reaper.TrackList_AdjustWindows(false) 36 | -------------------------------------------------------------------------------- /set_channel_count.lua: -------------------------------------------------------------------------------- 1 | local tracks = reaper.CountSelectedTracks(0) 2 | 3 | if tracks == 0 then 4 | reaper.ShowMessageBox("Select some tracks and retry.", "Selection is empty!", 0) 5 | return 6 | end 7 | 8 | local ok, channels = reaper.GetUserInputs("Set Channel Count", 1, "Channel count for selected tracks:", "2") 9 | channels = tonumber(channels) 10 | 11 | if ok == false or channels == nil then 12 | return 13 | elseif channels % 2 ~= 0 then 14 | channels = channels + 1 15 | end 16 | 17 | channels = math.max(2, math.min(channels, 64)) 18 | 19 | for index=0,tracks-1 do 20 | local track = reaper.GetSelectedTrack(0, index) 21 | reaper.SetMediaTrackInfo_Value(track, "I_NCHAN", channels) 22 | end 23 | -------------------------------------------------------------------------------- /song_switcher.lua: -------------------------------------------------------------------------------- 1 | local SCRIPT_NAME = 'Song switcher' 2 | 3 | if not reaper.ImGui_GetBuiltinPath then 4 | return reaper.MB('ReaImGui is not installed or too old.', SCRIPT_NAME, 0) 5 | end 6 | package.path = reaper.ImGui_GetBuiltinPath() .. '/?.lua' 7 | local ImGui = require 'imgui' '0.9.3' 8 | 9 | local EXT_SECTION = 'cfillion_song_switcher' 10 | local EXT_SWITCH_MODE = 'onswitch' 11 | local EXT_LAST_DOCK = 'last_dock' 12 | local EXT_STATE = 'state' 13 | 14 | local FLT_MIN, FLT_MAX = ImGui.NumericLimits_Float() 15 | 16 | local SWITCH_SEEK = 1<<0 17 | local SWITCH_STOP = 1<<1 18 | local SWITCH_SCROLL = 1<<2 19 | 20 | local UNDO_STATE_TRACKCFG = 1 21 | 22 | local scrollTo, setDock 23 | -- initialized in reset() 24 | local currentIndex, nextIndex, invalid, filterPrompt 25 | local signals = {} 26 | local prevPlayPos 27 | 28 | local fonts = { 29 | small = ImGui.CreateFont('sans-serif', 13), 30 | large = ImGui.CreateFont('sans-serif', 28), 31 | huge = ImGui.CreateFont('sans-serif', 38), 32 | } 33 | 34 | local ctx = ImGui.CreateContext(SCRIPT_NAME) 35 | 36 | for key, font in pairs(fonts) do 37 | ImGui.Attach(ctx, font) 38 | end 39 | 40 | local function parseSongName(trackName) 41 | local number, separator, name = string.match(trackName, '^(%d+)(%W+)(.+)$') 42 | number = tonumber(number) 43 | 44 | if number and separator and name then 45 | return {number=number, separator=separator, name=name} 46 | end 47 | end 48 | 49 | local function compareSongs(a, b) 50 | local aparts, bparts = parseSongName(a.name), parseSongName(b.name) 51 | 52 | if aparts.number == bparts.number then 53 | return aparts.name < bparts.name 54 | else 55 | return aparts.number < bparts.number 56 | end 57 | end 58 | 59 | local function loadTracks() 60 | local songs = {} 61 | local depth = 0 62 | local isSong = false 63 | 64 | for index=0,reaper.GetNumTracks()-1 do 65 | local track = reaper.GetTrack(0, index) 66 | 67 | local track_depth = reaper.GetMediaTrackInfo_Value(track, 'I_FOLDERDEPTH') 68 | 69 | if depth == 0 and track_depth == 1 then 70 | local _, name = reaper.GetSetMediaTrackInfo_String(track, 'P_NAME', '', false) 71 | 72 | if parseSongName(name) then 73 | isSong = true 74 | table.insert(songs, {name=name, folder=track, tracks={track}, uniqId=#songs}) 75 | else 76 | isSong = false 77 | end 78 | elseif depth >= 1 and isSong then 79 | local song = songs[#songs] 80 | song.tracks[#song.tracks + 1] = track 81 | 82 | for itemIndex=0,reaper.CountTrackMediaItems(track)-1 do 83 | local item = reaper.GetTrackMediaItem(track, itemIndex) 84 | local pos = reaper.GetMediaItemInfo_Value(item, 'D_POSITION') 85 | local endTime = pos + reaper.GetMediaItemInfo_Value(item, 'D_LENGTH') 86 | 87 | if not song.startTime or song.startTime > pos then 88 | song.startTime = pos 89 | end 90 | if not song.endTime or song.endTime < endTime then 91 | song.endTime = endTime 92 | end 93 | end 94 | end 95 | 96 | depth = depth + track_depth 97 | end 98 | 99 | for _,song in ipairs(songs) do 100 | if not song.startTime then song.startTime = 0 end 101 | if not song.endTime then song.endTime = reaper.GetProjectLength() end 102 | end 103 | 104 | table.sort(songs, compareSongs) 105 | return songs 106 | end 107 | 108 | local function isSongValid(song) 109 | for _,track in ipairs(song.tracks) do 110 | if not pcall(reaper.GetTrackNumMediaItems, track) then 111 | return false 112 | end 113 | end 114 | 115 | return true 116 | end 117 | 118 | local function setSongEnabled(song, enabled) 119 | if song == nil then return end 120 | 121 | invalid = not isSongValid(song) 122 | if invalid then return false end 123 | 124 | local on, off = 1, 0 125 | 126 | if not enabled then 127 | on, off = 0, 1 128 | end 129 | 130 | reaper.SetMediaTrackInfo_Value(song.folder, 'B_MUTE', off) 131 | 132 | for _,track in ipairs(song.tracks) do 133 | reaper.SetMediaTrackInfo_Value(track, 'B_SHOWINMIXER', on) 134 | reaper.SetMediaTrackInfo_Value(track, 'B_SHOWINTCP', on) 135 | end 136 | 137 | return true 138 | end 139 | 140 | local function updateState() 141 | local song = songs[currentIndex] or {name='', startTime=0, endTime=0} 142 | 143 | local state = ("%d\t%d\t%s\t%f\t%f\t%s"):format( 144 | currentIndex, #songs, song.name, song.startTime, song.endTime, 145 | tostring(invalid) 146 | ) 147 | reaper.SetExtState(EXT_SECTION, EXT_STATE, state, false) 148 | end 149 | 150 | local function getSwitchMode() 151 | local mode = tonumber(reaper.GetExtState(EXT_SECTION, EXT_SWITCH_MODE)) 152 | return mode and mode or 0 153 | end 154 | 155 | local function setSwitchMode(mode) 156 | reaper.SetExtState(EXT_SECTION, EXT_SWITCH_MODE, tostring(mode), true) 157 | end 158 | 159 | local function setNextIndex(index) 160 | if songs[index] then 161 | nextIndex = index 162 | scrollTo = index 163 | highlightTime = ImGui.GetTime(ctx) 164 | end 165 | end 166 | 167 | local function setCurrentIndex(index) 168 | reaper.PreventUIRefresh(1) 169 | 170 | if currentIndex < 1 then 171 | for _,song in ipairs(songs) do 172 | setSongEnabled(song, false) 173 | end 174 | elseif index ~= currentIndex then 175 | setSongEnabled(songs[currentIndex], false) 176 | end 177 | 178 | local mode = getSwitchMode() 179 | 180 | if mode & SWITCH_STOP ~= 0 then 181 | reaper.CSurf_OnStop() 182 | end 183 | 184 | local song = songs[index] 185 | local disableOk = not invalid 186 | local enableOk = setSongEnabled(song, true) 187 | 188 | if enableOk or disableOk then 189 | currentIndex = index 190 | setNextIndex(index) 191 | 192 | if mode & SWITCH_SEEK ~= 0 then 193 | reaper.SetEditCurPos(song.startTime, true, true) 194 | end 195 | 196 | if mode & SWITCH_SCROLL ~= 0 then 197 | reaper.GetSet_ArrangeView2(0, true, 0, 0, song.startTime, song.endTime + 5) 198 | end 199 | end 200 | 201 | reaper.PreventUIRefresh(-1) 202 | 203 | reaper.TrackList_AdjustWindows(false) 204 | reaper.UpdateArrange() 205 | 206 | filterPrompt = false 207 | updateState() 208 | end 209 | 210 | local function trySetCurrentIndex(index) 211 | if songs[index] then 212 | setCurrentIndex(index) 213 | end 214 | end 215 | 216 | local function moveSong(from, to) 217 | local target = songs[from] 218 | songs[from] = songs[to] 219 | songs[to] = target 220 | 221 | if currentIndex == from then 222 | currentIndex = to 223 | elseif to <= currentIndex and from > currentIndex then 224 | currentIndex = currentIndex + 1 225 | elseif from < currentIndex and to >= currentIndex then 226 | currentIndex = currentIndex - 1 227 | end 228 | 229 | if nextIndex == from then 230 | nextIndex = to 231 | elseif to <= nextIndex and from > nextIndex then 232 | nextIndex = nextIndex + 1 233 | elseif from < nextIndex and to >= nextIndex then 234 | nextIndex = nextIndex - 1 235 | end 236 | 237 | reaper.Undo_BeginBlock() 238 | reaper.PreventUIRefresh(1) 239 | local maxNumLength = math.max(2, tostring(#songs):len()) 240 | for index, song in ipairs(songs) do 241 | local nameParts = parseSongName(song.name) 242 | local newName = string.format('%0' .. maxNumLength .. 'd%s%s', 243 | index, nameParts.separator, nameParts.name) 244 | song.name = newName 245 | 246 | if reaper.ValidatePtr(song.folder, 'MediaTrack*') then 247 | reaper.GetSetMediaTrackInfo_String(song.folder, 'P_NAME', newName, true) 248 | end 249 | end 250 | reaper.PreventUIRefresh(-1) 251 | reaper.Undo_EndBlock('Song switcher: Change song order', UNDO_STATE_TRACKCFG) 252 | end 253 | 254 | local function findSong(buffer) 255 | if string.len(buffer) == 0 then return end 256 | buffer = string.upper(buffer) 257 | 258 | local index = 0 259 | local song = songs[index] 260 | 261 | for index, song in ipairs(songs) do 262 | local name = string.upper(song.name) 263 | 264 | if string.find(name, buffer, 0, true) ~= nil then 265 | return index, song 266 | end 267 | end 268 | end 269 | 270 | local function reset() 271 | songs = loadTracks() 272 | 273 | local activeIndex, activeCount, visibleCount = nil, 0, 0 274 | 275 | for index,song in ipairs(songs) do 276 | local muted = reaper.GetMediaTrackInfo_Value(song.folder, 'B_MUTE') 277 | 278 | if muted == 0 then 279 | if activeIndex == nil then 280 | activeIndex = index 281 | end 282 | 283 | activeCount = activeCount + 1 284 | end 285 | 286 | if activeIndex ~= index then 287 | for _,track in ipairs(song.tracks) do 288 | local tcp = reaper.GetMediaTrackInfo_Value(track, 'B_SHOWINTCP') 289 | local mixer = reaper.GetMediaTrackInfo_Value(track, 'B_SHOWINMIXER') 290 | 291 | if tcp == 1 or mixer == 1 then 292 | visibleCount = visibleCount + 1 293 | end 294 | end 295 | end 296 | end 297 | 298 | filterPrompt, invalid = false, false 299 | currentIndex, nextIndex, scrollTo = 0, 0, 0 300 | highlightTime = ImGui.GetTime(ctx) 301 | prevPlayPos = nil 302 | 303 | -- clear previous pending external commands 304 | for signal, _ in pairs(signals) do 305 | reaper.DeleteExtState(EXT_SECTION, signal, false) 306 | end 307 | 308 | if activeCount == 1 then 309 | if visibleCount == 0 then 310 | currentIndex = activeIndex 311 | nextIndex = activeIndex 312 | scrollTo = activeIndex 313 | 314 | updateState() 315 | else 316 | setCurrentIndex(activeIndex) 317 | end 318 | else 319 | updateState() 320 | end 321 | end 322 | 323 | local function execRemoteActions() 324 | for signal, handler in pairs(signals) do 325 | if reaper.HasExtState(EXT_SECTION, signal) then 326 | local value = reaper.GetExtState(EXT_SECTION, signal) 327 | reaper.DeleteExtState(EXT_SECTION, signal, false); 328 | handler(value) 329 | end 330 | end 331 | end 332 | 333 | local function getParentProject(track) 334 | local search = reaper.GetMediaTrackInfo_Value(track, 'P_PROJECT') 335 | 336 | if reaper.JS_Window_HandleFromAddress then 337 | return reaper.JS_Window_HandleFromAddress(search) 338 | end 339 | 340 | for i = 0, math.huge do 341 | local project = reaper.EnumProjects(i) 342 | if not project then break end 343 | 344 | local master = reaper.GetMasterTrack(project) 345 | if search == reaper.GetMediaTrackInfo_Value(master, 'P_PROJECT') then 346 | return project 347 | end 348 | end 349 | end 350 | 351 | local function execTakeMarkers() 352 | if not reaper.GetNumTakeMarkers then return end -- REAPER v5 353 | 354 | local song = songs[currentIndex] 355 | local track = song and song.tracks[1] 356 | local valid, numItems = pcall(reaper.GetTrackNumMediaItems, track) 357 | if not valid then return end -- validates track across all tabs 358 | 359 | local proj = getParentProject(track) 360 | if reaper.GetPlayStateEx(proj) & 3 ~= 1 then return end -- not playing or paused 361 | 362 | local playPos = reaper.GetPlayPositionEx(proj) 363 | if playPos == prevPlayPos then return end 364 | 365 | local minPos, maxPos = playPos, playPos 366 | if prevPlayPos and playPos > prevPlayPos and (playPos - prevPlayPos) < 0.1 then 367 | minPos = prevPlayPos 368 | end 369 | prevPlayPos = playPos 370 | 371 | for ii = 0, numItems - 1 do 372 | local item = reaper.GetTrackMediaItem(track, ii) 373 | local mute = reaper.GetMediaItemInfo_Value(item, 'B_MUTE') 374 | local pos = reaper.GetMediaItemInfo_Value(item, 'D_POSITION') 375 | local len = reaper.GetMediaItemInfo_Value(item, 'D_LENGTH') 376 | local take = reaper.GetActiveTake(item) 377 | 378 | if take and mute == 0 and pos <= minPos and pos + len > maxPos then 379 | local offs = reaper.GetMediaItemTakeInfo_Value(take, 'D_STARTOFFS') 380 | 381 | for mi = 0, reaper.GetNumTakeMarkers(take) - 1 do 382 | local time, name = reaper.GetTakeMarker(take, mi) 383 | time = time + pos - offs 384 | if time >= minPos and time <= maxPos and name:sub(1, 1) == '!' then 385 | for action in name:sub(2):gmatch('%S+') do 386 | local action = reaper.NamedCommandLookup(action) 387 | if action ~= 0 then reaper.Main_OnCommandEx(action, 0, proj) end 388 | end 389 | end 390 | end 391 | end 392 | end 393 | end 394 | 395 | function drawName(song) 396 | local name = song and song.name or 'No song selected' 397 | 398 | ImGui.PushStyleColor(ctx, ImGui.Col_Text, 399 | ImGui.GetStyleColor(ctx, (song and ImGui.Col_Text or ImGui.Col_TextDisabled))) 400 | ImGui.PushStyleColor(ctx, ImGui.Col_Button, invalid and 0xff0000ff or 0) 401 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonHovered, 0x323232ff) 402 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonActive, 0x3232327f) 403 | if ImGui.Button(ctx, ('%s###song_name'):format(name), -FLT_MIN) then 404 | filterPrompt = true 405 | end 406 | ImGui.PopStyleColor(ctx, 4) 407 | end 408 | 409 | local function drawFilter() 410 | ImGui.SetNextItemWidth(ctx, -FLT_MIN) 411 | ImGui.SetKeyboardFocusHere(ctx) 412 | local rv, filter = ImGui.InputText(ctx, '##name_fiter', '', ImGui.InputTextFlags_EnterReturnsTrue) 413 | if ImGui.IsItemDeactivated(ctx) then 414 | filterPrompt = false 415 | end 416 | if rv then 417 | local index, _ = findSong(filter) 418 | 419 | if index then 420 | setCurrentIndex(index) 421 | end 422 | end 423 | end 424 | 425 | local function formatTime(time) 426 | return reaper.format_timestr(time, '') 427 | end 428 | 429 | local function songList(y) 430 | local flags = ImGui.TableFlags_Borders | 431 | ImGui.TableFlags_RowBg | 432 | ImGui.TableFlags_ScrollY | 433 | ImGui.TableFlags_Hideable | 434 | ImGui.TableFlags_Resizable | 435 | ImGui.TableFlags_Reorderable 436 | if not ImGui.BeginTable(ctx, 'song_list', 4, flags, -FLT_MIN, -FLT_MIN) then return end 437 | 438 | ImGui.TableSetupColumn(ctx, '#. Name', ImGui.TableColumnFlags_WidthStretch) 439 | ImGui.TableSetupColumn(ctx, 'Start', ImGui.TableColumnFlags_WidthFixed) 440 | ImGui.TableSetupColumn(ctx, 'End', ImGui.TableColumnFlags_WidthFixed) 441 | ImGui.TableSetupColumn(ctx, 'Length', ImGui.TableColumnFlags_WidthFixed) 442 | ImGui.TableSetupScrollFreeze(ctx, 0, 1) 443 | ImGui.TableHeadersRow(ctx) 444 | 445 | local swap 446 | for index, song in ipairs(songs) do 447 | ImGui.TableNextRow(ctx) 448 | 449 | ImGui.TableNextColumn(ctx) 450 | local color = ImGui.GetStyleColor(ctx, ImGui.Col_Header) 451 | local isCurrent, isNext = index == currentIndex, index == nextIndex 452 | if isNext and not isCurrent then 453 | -- swap blue <-> green 454 | color = (color & 0xFF0000FF) | (color & 0x00FF0000) >> 8 | (color & 0x0000FF00) << 8 455 | if (math.floor(highlightTime - ImGui.GetTime(ctx)) & 1) == 0 then 456 | color = (color & ~0xff) | 0x1a 457 | end 458 | end 459 | ImGui.PushStyleColor(ctx, ImGui.Col_Header, color) 460 | if ImGui.Selectable(ctx, ('%s###%d'):format(song.name, song.uniqId), 461 | isCurrent or isNext, 462 | ImGui.SelectableFlags_SpanAllColumns) then 463 | setCurrentIndex(index) 464 | end 465 | if ImGui.IsItemActive(ctx) and not ImGui.IsItemHovered(ctx) then 466 | local mouseDelta = select(2, ImGui.GetMouseDragDelta(ctx, nil, nil, ImGui.MouseButton_Left)) 467 | local newIndex = index + (mouseDelta < 0 and -1 or 1) 468 | if newIndex > 0 and newIndex <= #songs then 469 | swap = { from=index, to=newIndex } 470 | ImGui.ResetMouseDragDelta(ctx, ImGui.MouseButton_Left) 471 | end 472 | end 473 | ImGui.PopStyleColor(ctx) 474 | 475 | ImGui.TableNextColumn(ctx) 476 | ImGui.Text(ctx, formatTime(song.startTime)) 477 | 478 | ImGui.TableNextColumn(ctx) 479 | ImGui.Text(ctx, formatTime(song.endTime)) 480 | 481 | ImGui.TableNextColumn(ctx) 482 | ImGui.Text(ctx, formatTime(song.endTime - song.startTime)) 483 | 484 | if index == scrollTo then 485 | ImGui.SetScrollHereY(ctx, 1) 486 | end 487 | end 488 | 489 | ImGui.EndTable(ctx) 490 | scrollTo = nil 491 | 492 | if swap then 493 | moveSong(swap.from, swap.to) 494 | end 495 | end 496 | 497 | local function switchModeMenu() 498 | local mode = getSwitchMode() 499 | if ImGui.MenuItem(ctx, 'Stop playback', nil, mode & SWITCH_STOP ~= 0) then 500 | setSwitchMode(mode ~ SWITCH_STOP) 501 | end 502 | if ImGui.MenuItem(ctx, 'Seek to first item', nil, mode & SWITCH_SEEK ~= 0) then 503 | setSwitchMode(mode ~ SWITCH_SEEK) 504 | end 505 | if ImGui.MenuItem(ctx, 'Scroll to first item', nil, mode & SWITCH_SCROLL ~= 0) then 506 | setSwitchMode(mode ~ SWITCH_SCROLL) 507 | end 508 | end 509 | 510 | local function switchModeButton() 511 | ImGui.SmallButton(ctx, 'onswitch') 512 | if ImGui.BeginPopupContextItem(ctx, 'onswitch_menu', ImGui.PopupFlags_MouseButtonLeft) then 513 | switchModeMenu() 514 | ImGui.EndPopup(ctx) 515 | end 516 | end 517 | 518 | local function toggleDock(dockId) 519 | dockId = dockId or ImGui.GetWindowDockID(ctx) 520 | if dockId >= 0 then 521 | local lastDock = tonumber(reaper.GetExtState(EXT_SECTION, EXT_LAST_DOCK)) 522 | if not lastDock or lastDock < 0 or lastDock > 16 then lastDock = 0 end 523 | setDock = ~lastDock 524 | else 525 | reaper.SetExtState(EXT_SECTION, EXT_LAST_DOCK, tostring(~dockId), true) 526 | setDock = 0 527 | end 528 | end 529 | 530 | local function contextMenu() 531 | local dockId = ImGui.GetWindowDockID(ctx) 532 | if not ImGui.BeginPopupContextWindow(ctx, 'context_menu') then return end 533 | 534 | if ImGui.MenuItem(ctx, 'Dock window', nil, dockId ~= 0) then 535 | toggleDock(dockId) 536 | end 537 | if ImGui.MenuItem(ctx, 'Reset data') then 538 | reset() 539 | end 540 | if ImGui.BeginMenu(ctx, 'When switching to a song...') then 541 | switchModeMenu() 542 | ImGui.EndMenu(ctx) 543 | end 544 | ImGui.Separator(ctx) 545 | if #songs > 0 then 546 | for index, song in ipairs(songs) do 547 | if ImGui.MenuItem(ctx, song.name, nil, index == currentIndex) then 548 | setCurrentIndex(index) 549 | end 550 | end 551 | ImGui.Separator(ctx) 552 | end 553 | if ImGui.MenuItem(ctx, 'Help') then 554 | about() 555 | end 556 | 557 | ImGui.EndPopup(ctx) 558 | end 559 | 560 | function about() 561 | local owner = reaper.ReaPack_GetOwner((select(2, reaper.get_action_context()))) 562 | if owner then 563 | reaper.ReaPack_AboutInstalledPackage(owner) 564 | reaper.ReaPack_FreeEntry(owner) 565 | else 566 | reaper.ShowMessageBox('Song switcher must be installed through ReaPack to use this feature.', SCRIPT_NAME, 0) 567 | end 568 | end 569 | 570 | local function navButtons() 571 | local pad_x, pad_y = 8, 8 572 | local dl = ImGui.GetWindowDrawList(ctx) 573 | local x1, y1 = ImGui.GetItemRectMin(ctx) 574 | local x2, y2 = ImGui.GetItemRectMax(ctx) 575 | local size = y2 - y1 576 | 577 | local col_text = ImGui.GetColor(ctx, ImGui.Col_Text) 578 | local col_hover = ImGui.GetColor(ctx, ImGui.Col_ButtonHovered) 579 | local col_active = ImGui.GetColor(ctx, ImGui.Col_ButtonActive) 580 | 581 | local function btn(isPrev) 582 | ImGui.SetCursorScreenPos(ctx, isPrev and x1 or (x2 - size), y1) 583 | 584 | if ImGui.InvisibleButton(ctx, isPrev and 'prev' or 'next', size, size) then 585 | setCurrentIndex(currentIndex + (isPrev and -1 or 1)) 586 | end 587 | 588 | local color = ImGui.IsItemActive(ctx) and col_active 589 | or ImGui.IsItemHovered(ctx) and col_hover 590 | or col_text 591 | 592 | local min_x, min_y = ImGui.GetItemRectMin(ctx) 593 | local max_x, max_y = ImGui.GetItemRectMax(ctx) 594 | local mid_y = min_y + ((max_y - min_y) / 2) 595 | min_x, min_y = min_x + pad_x, min_y + pad_y 596 | max_x, max_y = max_x - pad_x, max_y - pad_y 597 | 598 | if isPrev then 599 | ImGui.DrawList_AddTriangleFilled(dl, 600 | min_x, mid_y, max_x, min_y, max_x, max_y, color) 601 | else 602 | ImGui.DrawList_AddTriangleFilled(dl, 603 | min_x, min_y, max_x, mid_y, min_x, max_y, color) 604 | end 605 | end 606 | 607 | if currentIndex > 1 then btn(true) end 608 | if songs[currentIndex + 1] then btn(false) end 609 | end 610 | 611 | local function keyInput(input) 612 | if ImGui.IsAnyItemActive(ctx) then return end 613 | 614 | if ImGui.Shortcut(ctx, ImGui.Key_UpArrow, ImGui.InputFlags_Repeat) or 615 | ImGui.Shortcut(ctx, ImGui.Key_LeftArrow, ImGui.InputFlags_Repeat) then 616 | setNextIndex(nextIndex - 1) 617 | elseif ImGui.Shortcut(ctx, ImGui.Key_DownArrow, ImGui.InputFlags_Repeat) or 618 | ImGui.Shortcut(ctx, ImGui.Key_RightArrow, ImGui.InputFlags_Repeat) then 619 | setNextIndex(nextIndex + 1) 620 | elseif ImGui.Shortcut(ctx, ImGui.Key_PageUp) or 621 | ImGui.Shortcut(ctx, ImGui.Key_KeypadSubtract) then 622 | trySetCurrentIndex(currentIndex - 1) 623 | elseif ImGui.Shortcut(ctx, ImGui.Key_PageDown) or 624 | ImGui.Shortcut(ctx, ImGui.Key_KeypadAdd) then 625 | trySetCurrentIndex(currentIndex + 1) 626 | elseif ImGui.Shortcut(ctx, ImGui.Key_Insert) or 627 | ImGui.Shortcut(ctx, ImGui.Key_NumLock) then 628 | reset() 629 | elseif ImGui.Shortcut(ctx, ImGui.Key_Enter) or 630 | ImGui.Shortcut(ctx, ImGui.Key_KeypadEnter) then 631 | if nextIndex == currentIndex then 632 | filterPrompt = true 633 | else 634 | setCurrentIndex(nextIndex) 635 | end 636 | end 637 | end 638 | 639 | local function buttonSize(label) 640 | return ImGui.CalcTextSize(ctx, label) + 641 | (ImGui.GetStyleVar(ctx, ImGui.StyleVar_FramePadding) * 2) 642 | end 643 | 644 | local function toolbar() 645 | local y_bak = ImGui.GetCursorPosY(ctx) 646 | local x1, y1 = ImGui.GetItemRectMin(ctx) 647 | local x2, y2 = ImGui.GetItemRectMax(ctx) 648 | local btn_height = ImGui.GetFontSize(ctx) 649 | 650 | local frame_padding_x, frame_padding_y = ImGui.GetStyleVar(ctx, ImGui.StyleVar_FramePadding) 651 | local item_spacing_x, item_spacing_y = ImGui.GetStyleVar(ctx, ImGui.StyleVar_ItemSpacing) 652 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_FramePadding, frame_padding_x, math.floor(frame_padding_y * 0.60)) 653 | ImGui.PushStyleVar(ctx, ImGui.StyleVar_ItemSpacing, item_spacing_x, math.floor(item_spacing_y * 0.60)) 654 | ImGui.PushFont(ctx, nil) 655 | 656 | ImGui.SetCursorScreenPos(ctx, x1, y1) 657 | local dockLabel = ImGui.IsWindowDocked(ctx) and 'undock' or 'dock' 658 | if ImGui.SmallButton(ctx, ('%s###dock'):format(dockLabel)) then 659 | toggleDock() 660 | end 661 | 662 | ImGui.SetCursorScreenPos(ctx, x1, y2 - btn_height) 663 | switchModeButton() 664 | 665 | ImGui.SetCursorScreenPos(ctx, x2 - buttonSize('reset'), y1) 666 | ImGui.PushStyleColor(ctx, ImGui.Col_ButtonHovered, 0xff4242ff) 667 | if ImGui.SmallButton(ctx, 'reset') then reset() end 668 | ImGui.PopStyleColor(ctx) 669 | 670 | ImGui.SetCursorScreenPos(ctx, x2 - buttonSize('help'), y2 - btn_height) 671 | if ImGui.SmallButton(ctx, 'help') then about() end 672 | 673 | ImGui.PopFont(ctx) 674 | ImGui.PopStyleVar(ctx, 2) 675 | 676 | ImGui.SetCursorPosY(ctx, y_bak) 677 | end 678 | 679 | local function mainWindow() 680 | contextMenu() 681 | keyInput() 682 | 683 | local avail_y = select(2, ImGui.GetContentRegionAvail(ctx)) 684 | local fullUI = avail_y > 50 and ImGui.GetScrollMaxY(ctx) <= avail_y 685 | 686 | filterPrompt = filterPrompt and ImGui.IsWindowFocused(ctx) 687 | -- cache to not call toolbar() on the frame when drawFilter() clears it 688 | local filterPrompt = filterPrompt 689 | 690 | ImGui.PushFont(ctx, fullUI and fonts.large or fonts.huge) 691 | if filterPrompt then 692 | drawFilter() 693 | else 694 | ImGui.SetNextItemAllowOverlap(ctx) 695 | drawName(songs[currentIndex]) 696 | if not fullUI then 697 | navButtons() 698 | end 699 | end 700 | ImGui.PopFont(ctx) 701 | 702 | if fullUI then 703 | if not filterPrompt then 704 | toolbar() 705 | end 706 | ImGui.Spacing(ctx) 707 | songList() 708 | end 709 | end 710 | 711 | local function loop() 712 | execRemoteActions() 713 | execTakeMarkers() 714 | 715 | ImGui.PushFont(ctx, fonts.small) 716 | ImGui.SetNextWindowSize(ctx, 500, 300, setDock and ImGui.Cond_Always or ImGui.Cond_FirstUseEver) 717 | if setDock then 718 | ImGui.SetNextWindowDockID(ctx, setDock) 719 | setDock = nil 720 | end 721 | local visible, open = ImGui.Begin(ctx, SCRIPT_NAME, true, ImGui.WindowFlags_NoScrollbar) 722 | if visible then 723 | mainWindow() 724 | ImGui.End(ctx) 725 | end 726 | ImGui.PopFont(ctx) 727 | 728 | if open then 729 | reaper.defer(loop) 730 | end 731 | end 732 | 733 | signals.relative_move = function(move) 734 | move = tonumber(move) 735 | 736 | if move then 737 | trySetCurrentIndex(currentIndex + move) 738 | end 739 | end 740 | 741 | signals.absolute_move = function(index) 742 | trySetCurrentIndex(tonumber(index)) 743 | end 744 | 745 | signals.activate_queued = function() 746 | if currentIndex ~= nextIndex then 747 | setCurrentIndex(nextIndex) 748 | end 749 | end 750 | 751 | signals.relative_queue = function(move) 752 | move = tonumber(move) 753 | 754 | if move then 755 | setNextIndex(nextIndex + move) 756 | end 757 | end 758 | 759 | signals.absolute_queue = function(index) 760 | setNextIndex(tonumber(index)) 761 | end 762 | 763 | signals.filter = function(filter) 764 | local index = findSong(filter) 765 | 766 | if index then 767 | setCurrentIndex(index) 768 | end 769 | end 770 | 771 | signals.reset = reset 772 | 773 | -- GO!! 774 | reset() 775 | reaper.defer(loop) 776 | 777 | reaper.atexit(function() 778 | reaper.DeleteExtState(EXT_SECTION, EXT_STATE, false) 779 | end) 780 | -------------------------------------------------------------------------------- /song_switcher_signal.lua: -------------------------------------------------------------------------------- 1 | -- @noindex 2 | 3 | -- This script is part of cfillion_Song switcher.lua 4 | 5 | local CC, ABS_MODE = {}, 0 6 | local script_name = select(2, reaper.get_action_context()):match('([^/\\_]+)%.lua$') 7 | local action_name = script_name:match(' %- (.-)$') 8 | local action = ({ 9 | ['Reset data' ] = {'reset', 'true'}, 10 | ['Switch to queued song' ] = {'activate_queued', 'true'}, 11 | ['Switch to previous song'] = {'relative_move', '-1'}, 12 | ['Switch to next song' ] = {'relative_move', '1'}, 13 | ['Queue previous song' ] = {'relative_queue', '-1'}, 14 | ['Queue next song' ] = {'relative_queue', '1'}, 15 | ['Switch song by MIDI CC' ] = {CC, 'absolute_move', 'relative_move' }, 16 | ['Queue song by MIDI CC' ] = {CC, 'absolute_queue', 'relative_queue'}, 17 | })[action_name] 18 | if not action then error(('unknown action "%s"'):format(action_name or script_name)) end 19 | 20 | if action[1] == CC then 21 | local is_new_value, filename, sectionID, cmdID, mode, resolution, value = reaper.get_action_context() 22 | action = { action[mode == ABS_MODE and 2 or 3], tostring(value) } 23 | end 24 | 25 | reaper.SetExtState('cfillion_song_switcher', action[1], action[2], false) 26 | reaper.defer(function() end) -- no undo point 27 | -------------------------------------------------------------------------------- /song_switcher_www/Tupfile: -------------------------------------------------------------------------------- 1 | export GEM_HOME 2 | 3 | NODE_BIN = "`npm bin`" 4 | 5 | BROWSERIFY = $(NODE_BIN)/browserify -s SongSwitcherWWW -p bundle-collapser/plugin 6 | COFFEE = $(NODE_BIN)/coffee --bare 7 | UGLIFY = $(NODE_BIN)/uglifyjs -m --mangle-props --mangle-regex='/^_/' 8 | SASS = $GEM_HOME/bin/sass -C --sourcemap=none -t compressed 9 | SLIM = $GEM_HOME/bin/slimrb 10 | PREFIXER = $(NODE_BIN)/postcss --use autoprefixer 11 | 12 | # script files 13 | : foreach src/*.coffee |> ^o^ $(COFFEE) -o build/modules %f |> build/modules/%B.js 14 | : build/modules/*.js |> $(BROWSERIFY) build/modules/main.js | $(UGLIFY) -o %o |> build/script.js 15 | 16 | # stylesheet 17 | : src/style.sass |> $(SASS) %f | $(PREFIXER) -o %o |> build/%B.css 18 | 19 | # html 20 | : src/song_switcher.slim | build/style.css build/script.js |> $(SLIM) %f > %o |> dist/song_switcher.html 21 | -------------------------------------------------------------------------------- /song_switcher_www/Tupfile.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfillion/reascripts/ded29dd01a49ce9d7d1f282f2a3ad6a0dce478cc/song_switcher_www/Tupfile.ini -------------------------------------------------------------------------------- /song_switcher_www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "song_switcher", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "deep-equal": "^1.0" 7 | }, 8 | "devDependencies": { 9 | "autoprefixer": "^6.7", 10 | "browserify": "^14.5.0", 11 | "bundle-collapser": "^1.3.0", 12 | "coffee-script": "^1.12.7", 13 | "postcss-cli": "^2.6", 14 | "uglify-js": "^2.8.29" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /song_switcher_www/src/client.coffee: -------------------------------------------------------------------------------- 1 | EXT_SECTION = 'cfillion_song_switcher' 2 | EXT_STATE = 'state' 3 | EXT_REL_MOVE = 'relative_move' 4 | EXT_FILTER = 'filter' 5 | EXT_RESET = 'reset' 6 | CMD_UPDATE = "TRANSPORT;MARKER;GET/EXTSTATE/#{EXT_SECTION}/#{EXT_STATE}" 7 | 8 | EventEmitter = require('events').EventEmitter 9 | equal = require 'deep-equal' 10 | 11 | parseTime = (str) -> 12 | # workaround for Javascript's weird floating point approximation 13 | Math.trunc(parseFloat(str) * 1000) / 1000 14 | 15 | class State 16 | constructor: (data) -> 17 | if typeof(data) == 'object' 18 | @_unpack data 19 | else 20 | @_fallback data || '## No data from host ##' 21 | 22 | _unpack: (data) -> 23 | i = 0 24 | @currentIndex = parseInt data[i++] 25 | @songCount = parseInt data[i++] 26 | @title = data[i++] || '## No song selected ##' 27 | @startTime = parseTime data[i++] 28 | @endTime = parseTime data[i++] 29 | @invalid = data[i++] == 'true' 30 | 31 | _fallback: (title) -> 32 | @currentIndex = @songCount = @startTime = @endTime = 0 33 | [@title, @invalid] = [title, true] 34 | 35 | class Marker 36 | constructor: (data) -> 37 | i = 1 38 | @name = data[i++] 39 | @id = data[i++] 40 | @time = parseTime data[i++] 41 | @rawColor = parseInt data[i++] 42 | @color = '#' + Number(@rawColor & 0xFFFFFF).toString(16).padStart(6, '0') if @rawColor 43 | 44 | class Client extends EventEmitter 45 | makeSetExtState = (key, value) -> 46 | "SET/EXTSTATE/#{EXT_SECTION}/#{key}/#{encodeURIComponent value}" 47 | 48 | constructor: (timer) -> 49 | @data = {} 50 | @_resetData [] # pass an empty state different from fallback for initialization 51 | 52 | (fetch_loop = => 53 | @_send '' 54 | setTimeout fetch_loop, timer 55 | )() 56 | 57 | play: -> 58 | @_send 40044 # Transport: Play/stop 59 | 60 | relativeMove: (move) -> 61 | @_send makeSetExtState(EXT_REL_MOVE, move) 62 | 63 | setFilter: (filter) -> 64 | @_send makeSetExtState(EXT_FILTER, filter) 65 | 66 | seek: (time) -> 67 | @_send "SET/POS/#{time}" 68 | 69 | panic: -> 70 | @_send 40345 # Send all notes off to all MIDI outputs/plug-ins 71 | 72 | reset: -> 73 | @_send makeSetExtState(EXT_RESET, 'true') 74 | 75 | _send: (cmd) -> 76 | req = new XMLHttpRequest 77 | req.onreadystatechange = => 78 | if(req.readyState == XMLHttpRequest.DONE) 79 | if req.status == 200 80 | @_parse req.responseText 81 | else 82 | @_resetData '## Network error ##' 83 | req.open 'GET', "/_/#{cmd};#{CMD_UPDATE}", true 84 | req.send null 85 | 86 | _resetData: (state) -> 87 | @_editData (set) -> 88 | set 'playState', false 89 | set 'position', 0 90 | set 'state', new State(state) 91 | set 'markerList', [] 92 | 93 | _parse: (response) -> 94 | markers = [] 95 | @_editData (set) -> 96 | for l in response.split('\n') 97 | tok = l.split '\t' 98 | 99 | switch tok[0] 100 | when 'TRANSPORT' 101 | set 'playState', parseInt(tok[1]) 102 | set 'position', parseTime(tok[2]) 103 | when 'MARKER' 104 | markers.push new Marker(tok) 105 | when 'EXTSTATE' 106 | if tok[1] == EXT_SECTION && tok[2] == EXT_STATE 107 | set 'state', if tok[3].length 108 | new State simple_unescape(tok[3]).split('\t') 109 | else 110 | new State 111 | set 'markerList', markers 112 | 113 | _editData: (cb) -> 114 | modified = [] 115 | cb (key, value) => 116 | unless equal @data[key], value 117 | @data[key] = value 118 | modified.push key 119 | @emit "#{key}Changed", @data[key] for key in modified 120 | 121 | module.exports = Client 122 | -------------------------------------------------------------------------------- /song_switcher_www/src/main.coffee: -------------------------------------------------------------------------------- 1 | Client = require './client' 2 | Timeline = require './timeline' 3 | 4 | LS_LOCK = 'cfillion_song_switcher_lock' 5 | 6 | class SongSwitcherWWW 7 | constructor: -> 8 | @_client = new Client 100 9 | 10 | @_timeline = new Timeline document.getElementById('timeline') 11 | @_lockOverlay = document.getElementById 'lock-overlay' 12 | 13 | @_ctrlBar = document.getElementById 'controls' 14 | @_prevBtn = document.getElementById 'prev' 15 | @_nextBtn = document.getElementById 'next' 16 | @_playBtn = document.getElementById 'play' 17 | @_panicBtn = document.getElementById 'panic' 18 | @_resetBtn = document.getElementById 'reset' 19 | @_lockBtn = document.getElementById 'lock' 20 | @_songBox = document.getElementById 'song_box' 21 | @_songName = document.getElementById 'title' 22 | @_filter = document.getElementById 'filter' 23 | 24 | @_setText @_songName, '## Awaiting data ##' 25 | @_setClass @_ctrlBar, 'invalid', false 26 | @_timeline.update @_client.data 27 | 28 | @_client.on 'playStateChanged', (state) => 29 | @_setClass @_playBtn, 'active', state > 0 30 | @_setClass @_playBtn, 'record', state & 4 31 | @_setClass @_playBtn, 'paused', state & 2 32 | @_client.on 'stateChanged', (state) => 33 | @_setVisible @_prevBtn, state.currentIndex > 1 34 | @_setVisible @_nextBtn, state.currentIndex < state.songCount 35 | @_setClass @_ctrlBar, 'invalid', state.invalid 36 | @_setText @_songName, state.title 37 | @_timeline.update @_client.data 38 | @_client.on 'positionChanged', => @_timeline.update @_client.data 39 | @_client.on 'markerListChanged', => @_timeline.update @_client.data 40 | 41 | @_timeline.on 'seek', (time) => @_client.seek time 42 | 43 | @_prevBtn.addEventListener 'click', => @_client.relativeMove -1 44 | @_nextBtn.addEventListener 'click', => @_client.relativeMove 1 45 | @_playBtn.addEventListener 'click', => @_client.play() 46 | @_panicBtn.addEventListener 'click', => @_client.panic() 47 | @_resetBtn.addEventListener 'click', => @_client.reset() 48 | @_lockBtn.addEventListener 'click', => 49 | if @_isLocked() 50 | return unless confirm('Are you sure?') 51 | localStorage.removeItem LS_LOCK 52 | else 53 | localStorage.setItem LS_LOCK, true 54 | 55 | @_toggleLock() 56 | @_songName.addEventListener 'click', => 57 | @_setClass @_songBox, 'edit', true 58 | @_filter.focus() 59 | @_filter.addEventListener 'blur', => @_closeFilter() 60 | @_filter.addEventListener 'keypress', (e) => 61 | if e.keyCode == 8 && !@_filter.value.length 62 | @_closeFilter() 63 | else if(e.keyCode != 13) 64 | return 65 | 66 | if(@_filter.value.length > 0) 67 | @_client.setFilter @_filter.value 68 | 69 | @_closeFilter() 70 | 71 | window.addEventListener 'resize', => @_timeline.update @_client.data 72 | window.addEventListener 'keydown', (e) => 73 | if !@_isLocked() && e.keyCode == 32 && e.target == document.body 74 | @_client.play() 75 | window.addEventListener 'beforeunload', (e) => 76 | if @_isLocked() 77 | text = 'Are you sure?' 78 | e.returnValue = text 79 | 80 | if localStorage.getItem LS_LOCK 81 | @_toggleLock() 82 | 83 | _setText: (node, text) -> 84 | if(textNode = node.lastChild) 85 | textNode.nodeValue = text 86 | else 87 | node.appendChild document.createTextNode(text) 88 | 89 | _setClass: (node, klass, enable = true) -> 90 | if(enable) 91 | node.classList.add klass 92 | else 93 | node.classList.remove klass 94 | 95 | _setVisible: (node, visible) -> 96 | @_setClass node, 'hidden', !visible 97 | 98 | _closeFilter: -> 99 | @_setClass @_songBox, 'edit', false 100 | @_filter.value = '' 101 | document.activeElement.blur() # close android keyboard 102 | 103 | _isLocked: -> 104 | @_lockBtn.classList.contains 'active' 105 | 106 | _toggleLock: -> 107 | @_lockOverlay.classList.toggle 'hidden' 108 | @_lockBtn.classList.toggle 'active' 109 | 110 | module.exports = SongSwitcherWWW 111 | -------------------------------------------------------------------------------- /song_switcher_www/src/song_switcher.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | /! This file is part of cfillion_Song switcher.lua 3 | html 4 | head 5 | meta charset="utf-8" 6 | meta name="apple-mobile-web-app-capable" content="yes" 7 | meta content="width=device-width, initial-scale=1" name="viewport" 8 | title Song switcher remote control 9 | style == File.read 'build/style.css' 10 | script src="/main.js" 11 | body 12 | #lock-overlay.hidden 13 | .vlayout 14 | div.spacer 15 | /! Top spacer 16 | 17 | #controls.hlayout.invalid 18 | #prev.button.hidden ◀ 19 | 20 | #song_box 21 | span#title Oops! Javascript is blocked or disabled. 22 | input id="filter" type="text" spellcheck="false" 23 | 24 | #next.button.hidden ▶ 25 | 26 | canvas#timeline 27 | 28 | #play.button Play/Stop 29 | 30 | #footer.hlayout 31 | #panic.button MIDI panic 32 | #reset.button Reset data 33 | #lock.button Lock 34 | #about 35 | a> target="_blank" href="https://forum.cockos.com/showthread.php?t=181159" 36 | | Song switcher remote control 37 | | by cfillion 38 | 39 | script 40 | == File.read 'build/script.js' 41 | | new SongSwitcherWWW() 42 | -------------------------------------------------------------------------------- /song_switcher_www/src/style.sass: -------------------------------------------------------------------------------- 1 | $alt-bg: #2e2e2e 2 | $margin: 20px 3 | $padding: 10px 4 | 5 | html, body, .vlayout 6 | height: 100% 7 | margin: 0 8 | 9 | body 10 | background-color: black 11 | box-sizing: border-box 12 | color: white 13 | font-family: sans-serif 14 | font-size: 50px 15 | padding: ($margin - $padding) $margin 0 $margin 16 | text-align: center 17 | user-select: none 18 | 19 | div 20 | cursor: default 21 | padding: $padding 0 $padding 0 22 | 23 | .vlayout 24 | display: flex 25 | flex-direction: column 26 | justify-content: space-between 27 | 28 | & > * 29 | flex-shrink: 0 30 | margin-bottom: $padding 31 | 32 | .hlayout 33 | align-items: center 34 | display: flex 35 | 36 | .vlayout, .hlayout, #controls, #song_box 37 | padding: 0 38 | 39 | #lock-overlay 40 | position: fixed 41 | top: 0 42 | left: 0 43 | right: 0 44 | bottom: 0 45 | 46 | #song_box 47 | flex-grow: 1 48 | word-wrap: break-word 49 | 50 | /* min-width fixes this flexbox overflow issue with word-wrap on firefox 51 | https://bugzilla.mozilla.org/show_bug.cgi?id=1136818 */ 52 | min-width: $padding 53 | 54 | span 55 | display: block 56 | 57 | input 58 | background-color: $alt-bg 59 | border-style: none 60 | color: inherit 61 | display: none 62 | font-size: inherit 63 | text-align: inherit 64 | width: 100% 65 | 66 | &.edit 67 | span 68 | display: none 69 | 70 | input 71 | display: block 72 | 73 | #prev, #next 74 | min-width: 50px 75 | border: none 76 | 77 | .button 78 | border: 1px white solid 79 | padding: $padding 80 | 81 | &:hover:not(.active) 82 | background-color: $alt-bg 83 | 84 | &:active:not(.active) 85 | background-color: #3c5a64 86 | color: inherit 87 | 88 | &.active 89 | background-color: #7ca5d7 90 | color: black 91 | 92 | #play 93 | font-size: 0.7em 94 | 95 | &.record 96 | background-color: #af5f5f 97 | 98 | &.paused 99 | background-color: #dd9815 100 | 101 | #timeline 102 | $height: 100px 103 | height: $height 104 | image-rendering: pixelated 105 | line-height: $height 106 | width: 100% 107 | 108 | #lock 109 | z-index: 1000 110 | 111 | #footer 112 | align-items: flex-end 113 | font-size: 12px 114 | 115 | .button 116 | padding: $padding ($padding * 1.5) $padding ($padding * 1.5) 117 | margin-right: $padding 118 | 119 | #about 120 | flex-grow: 1 121 | padding: 0 122 | text-align: right 123 | z-index: 1000 124 | 125 | .invalid 126 | background-color: red 127 | 128 | .hidden 129 | visibility: hidden 130 | 131 | a 132 | color: #7ca5d7 133 | 134 | @media (max-height: 425px) 135 | .spacer 136 | display: none 137 | 138 | @media (max-height: 320px) 139 | body 140 | font-size: 40px 141 | 142 | #timeline 143 | height: 80px 144 | -------------------------------------------------------------------------------- /song_switcher_www/src/timeline.coffee: -------------------------------------------------------------------------------- 1 | RULER_BACKGROUND = '#2e2e2e' 2 | TIME_BACKGROUND = 'black' 3 | FONT_SIZE = 15 4 | FONT_FAMILY = 'sans-serif' 5 | ALIGN_LEFT = 1 6 | ALIGN_RIGHT = -1 7 | PADDING = 20 8 | CURSOR_COLOR = 'yellow' 9 | CURSOR_WIDTH = 3 10 | GRID_COLOR = '#888888' 11 | GRID_WIDTH = 1 12 | MARKER_FG = 'white' 13 | MARKER_BG = 'red' 14 | MARKER_WIDTH = 2 15 | TYPE_CURSOR = 1 16 | TYPE_GRID = 2 17 | TYPE_MARKER = 3 18 | SNAP_THRESHOLD = 50 19 | SEEK_COLOR = 'orange' 20 | SEEK_DELAY = 500 # in milliseconds 21 | 22 | EventEmitter = require('events').EventEmitter 23 | 24 | class Timeline extends EventEmitter 25 | constructor: (@_canvas) -> 26 | @_snapPoints = [] 27 | @_rulerTop = FONT_SIZE 28 | 29 | @_ctx = @_canvas.getContext '2d' 30 | 31 | @_hasTouch = 'ontouchstart' of window 32 | mousedown = if @_hasTouch then 'touchstart' else 'mousedown' 33 | mousemove = if @_hasTouch then 'touchmove' else 'mousemove' 34 | mouseup = if @_hasTouch then 'touchend' else 'mouseup' 35 | click = if @_hasTouch then 'touchend' else 'click' 36 | 37 | @_canvas.addEventListener mousedown, => 38 | [@_mouseTime, @_disableSnap] = [new Date(), false] 39 | 40 | window.addEventListener mouseup, => 41 | @_mouseTime = null 42 | if @_seekPreview 43 | @_seekPreview = null 44 | @update @_data 45 | 46 | window.addEventListener mousemove, (e) => 47 | [x, y] = @_mousePos e 48 | if @_mouseTime && @_mouseTime < (new Date()) - SEEK_DELAY 49 | pos = Math.max 1, Math.min(x, @_canvas.width) 50 | @_disableSnap = true 51 | @_seekPreview = @_pxToTime pos 52 | @update @_data 53 | 54 | if @_isMouseOver x, y 55 | # prevent scrolling on mobile when zoomed 56 | e.preventDefault() 57 | false 58 | 59 | @_canvas.addEventListener click, (e) => 60 | [x, y] = @_mousePos e 61 | return unless @_isMouseOver x, y 62 | x = @_snap x unless @_disableSnap 63 | @emit 'seek', @_pxToTime(x) + @_data.state.startTime 64 | 65 | update: (@_data) -> 66 | @_snapPoints.length = 0 67 | @_resize() 68 | 69 | @_ctx.textBaseline = 'hanging' 70 | 71 | @_ctx.fillStyle = RULER_BACKGROUND 72 | @_ctx.fillRect 0, @_rulerTop, @_canvas.width, @_rulerHeight 73 | 74 | @_gridLine 0 75 | @_gridLine @_data.state.endTime - @_data.state.startTime 76 | 77 | for marker in @_data.markerList \ 78 | when marker.time >= @_data.state.startTime and marker.time <= @_data.state.endTime 79 | @_marker marker 80 | 81 | @_editCursor @_data.position - @_data.state.startTime 82 | 83 | if @_data.position < @_data.state.startTime 84 | @_outOfBounds ALIGN_LEFT 85 | else if @_data.position > @_data.state.endTime 86 | @_outOfBounds ALIGN_RIGHT 87 | 88 | @_ctx.strokeStyle = @_ctx.fillStyle = SEEK_COLOR 89 | @_rulerTick @_seekPreview if @_seekPreview 90 | 91 | @_snapPoints.sort (a, b) -> a - b 92 | 93 | _resize: -> 94 | [@_canvas.width, @_canvas.height] = [@_canvas.clientWidth, @_canvas.clientHeight] 95 | 96 | @_rulerHeight = @_canvas.height - (@_rulerTop * 2) 97 | @_rulerBottom = @_rulerTop + @_rulerHeight 98 | 99 | @_scale = (@_data.state.endTime - @_data.state.startTime) / @_canvas.width 100 | @_scale ||= 1 / Math.pow(2,32) 101 | 102 | _editCursor: (time) -> 103 | pos = @_timeToPx time 104 | 105 | @_ctx.strokeStyle = @_ctx.fillStyle = CURSOR_COLOR 106 | @_ctx.lineWidth = CURSOR_WIDTH 107 | 108 | @_ctx.beginPath() 109 | @_ctx.moveTo pos - @_rulerTop, 0 110 | @_ctx.lineTo pos, @_rulerTop + CURSOR_WIDTH 111 | @_ctx.lineTo pos + @_rulerTop, 0 112 | @_ctx.fill() 113 | 114 | @_rulerTick time, false 115 | 116 | _gridLine: (time) -> 117 | @_ctx.strokeStyle = @_ctx.fillStyle = GRID_COLOR 118 | @_ctx.lineWidth = GRID_WIDTH 119 | @_rulerTick time 120 | 121 | _marker: (marker) -> 122 | time = marker.time - @_data.state.startTime 123 | pos = @_timeToPx time 124 | 125 | blankerPos = pos - (MARKER_WIDTH * 2) 126 | @_ctx.fillStyle = RULER_BACKGROUND 127 | @_ctx.fillRect blankerPos, @_rulerTop, @_canvas.width - pos, @_rulerHeight 128 | 129 | @_ctx.strokeStyle = @_ctx.fillStyle = marker.color || MARKER_BG 130 | @_ctx.lineWidth = MARKER_WIDTH 131 | @_rulerTick time 132 | 133 | if marker.name.length > 0 134 | @_ctx.font = "bold #{FONT_SIZE}px #{FONT_FAMILY}" 135 | boxWidth = @_ctx.measureText(marker.name).width + (MARKER_WIDTH * 2) 136 | @_ctx.fillRect pos, @_rulerTop, boxWidth, FONT_SIZE 137 | 138 | @_ctx.fillStyle = MARKER_FG 139 | @_ctx.fillText marker.name, pos + MARKER_WIDTH, @_rulerTop + 2 140 | 141 | _rulerTick: (time, ruler = true) -> 142 | pos = @_timeToPx time 143 | labelYpos = if ruler then 0 else @_rulerBottom + 3 144 | 145 | @_snapPoints.push pos if ruler 146 | 147 | @_ctx.beginPath() 148 | @_ctx.moveTo pos, @_rulerTop 149 | @_ctx.lineTo pos, @_rulerBottom 150 | @_ctx.stroke() 151 | 152 | @_ctx.font = "#{FONT_SIZE}px #{FONT_FAMILY}" 153 | 154 | [oldFill, @_ctx.fillStyle] = [@_ctx.fillStyle, TIME_BACKGROUND] 155 | label = @_formatTime time, not ruler 156 | [labelXpos, labelWidth] = @_ensureVisible pos, label, not ruler 157 | @_ctx.fillRect labelXpos, labelYpos, labelWidth, FONT_SIZE 158 | @_ctx.fillStyle = oldFill 159 | @_ctx.fillText label, labelXpos, labelYpos + 1 # don't clip above the canvas top 160 | 161 | _ensureVisible: (pos, text, center) -> 162 | width = @_ctx.measureText(text).width 163 | pos -= width / 2 if center 164 | 165 | if (right = pos + width) > @_canvas.width 166 | pos -= right - @_canvas.width 167 | 168 | pos = Math.max 0, pos 169 | [pos, width] 170 | 171 | _outOfBounds: (dir) -> 172 | pos = PADDING 173 | pos = @_canvas.width - pos if dir == ALIGN_RIGHT 174 | 175 | height = @_rulerHeight / 2.5 176 | width = height 177 | top = (@_canvas.height - height) / 2 178 | 179 | @_ctx.lineWidth = 3 180 | 181 | @_ctx.beginPath() 182 | @_ctx.moveTo pos + (width * dir), top 183 | @_ctx.lineTo pos, top + (height / 2) 184 | @_ctx.lineTo pos + (width * dir), top + height 185 | @_ctx.stroke() 186 | 187 | _timeToPx: (time) -> 188 | time / @_scale 189 | 190 | _pxToTime: (px) -> 191 | px * @_scale 192 | 193 | _formatTime: (time, showMs) -> 194 | sign = if time < 0 then '-' else '' 195 | min = Math.abs time / 60 196 | sec = Math.abs time % 60 197 | ms = Math.abs time * 1000 198 | 199 | pad = (padding, int) -> 200 | int = Math.trunc int 201 | (padding + int).slice -padding.length 202 | 203 | out = "#{sign}#{pad '00', min}:#{pad '00', sec}" 204 | out += ".#{pad '000', ms}" if showMs 205 | out 206 | 207 | _snap: (pos) -> 208 | [min, max] = [-1, @_snapPoints.length] 209 | 210 | while max - min > 1 211 | i = Math.round((min + max) / 2) 212 | point = @_snapPoints[i] 213 | if point <= pos 214 | min = i 215 | else 216 | max = i 217 | 218 | min = @_snapPoints[min] - pos 219 | max = @_snapPoints[max] - pos 220 | distance = Math.min Math.abs(min), Math.abs(max) 221 | 222 | if distance < SNAP_THRESHOLD 223 | (if distance == Math.abs(min) then min else max) + pos 224 | else 225 | pos 226 | 227 | _isMouseOver: (x, y) -> 228 | x > 0 && x <= @_canvas.width && y > 0 && y <= @_canvas.height 229 | 230 | _mousePos: (e) -> 231 | if @_hasTouch 232 | e = e.touches[0] || e.changedTouches[0] 233 | 234 | x = e.pageX - @_canvas.offsetLeft 235 | y = e.pageY - @_canvas.offsetTop 236 | [x, y] 237 | 238 | module.exports = Timeline 239 | --------------------------------------------------------------------------------