├── .gitignore ├── icons.ttf ├── README.md ├── Toolbar.lua ├── Constants.lua ├── Input.lua ├── Reaffer.lua ├── Clipboard.lua ├── Debug.lua ├── App.lua ├── UndoRedo.lua ├── Util.lua ├── Editor.lua └── UI.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immortalx74/Reaffer/HEAD/icons.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reaffer 2 | ReaScript for Reaper, based on Ample Sound's riffer found in their guitar/bass VSTs. 3 | ![Reaffer](https://i.imgur.com/cfgMb1X.gif) -------------------------------------------------------------------------------- /Toolbar.lua: -------------------------------------------------------------------------------- 1 | ToolBar = 2 | { 3 | {icon = "a", tooltip = "Create MIDI item in first selected track, at edit cursor"}, 4 | {icon = "e", tooltip = "Select [S]"}, 5 | {icon = "f", tooltip = "Move [W]"}, 6 | {icon = "g", tooltip = "Draw [D]"}, 7 | {icon = "h", tooltip = "Erase [E]"}, 8 | {icon = "i", tooltip = "Undo [Ctrl + Z]"}, 9 | {icon = "j", tooltip = "Redo [Ctrl + Shift + Z]"}, 10 | {icon = "b", tooltip = "Cut [Ctrl + X]"}, 11 | {icon = "c", tooltip = "Copy [Ctrl + C]"}, 12 | {icon = "d", tooltip = "Paste [Ctrl + V]"} 13 | } -------------------------------------------------------------------------------- /Constants.lua: -------------------------------------------------------------------------------- 1 | e_Tool = 2 | { 3 | Create = 1, 4 | Select = 2, 5 | Move = 3, 6 | Draw = 4, 7 | Erase = 5, 8 | Undo = 6, 9 | Redo = 7, 10 | Cut = 8, 11 | Copy = 9, 12 | Paste = 10 13 | } 14 | 15 | e_NoteDisplay = 16 | { 17 | Pitch = 1, 18 | Fret = 2, 19 | PitchAndFret = 3, 20 | Velocity = 4, 21 | OffVelocity = 5, 22 | MIDIPitch = 6 23 | } 24 | 25 | e_Direction = 26 | { 27 | Left = 1, 28 | Right = 2 29 | } 30 | 31 | e_MouseButton = 32 | { 33 | Left = 1, 34 | Right = 2, 35 | Middle = 3 36 | } 37 | 38 | e_OpType = 39 | { 40 | NoOp = 1, 41 | Insert = 2, 42 | Delete = 3, 43 | ModifyPitchAndDuration = 4, 44 | ModifyVelocityAndOffVelocity = 5, 45 | Move = 6 46 | -- more here... 47 | } 48 | 49 | Colors = 50 | { 51 | lane = 0x606060FF, 52 | bg = 0x0F0F0FFF, 53 | active_tool = 0x3D85E0FF, 54 | note_preview = 0xFFFFFF88, 55 | note_preview_paste = 0xAAAAAA55, 56 | red = 0xFF0000FF, 57 | text = 0xFFFFFFFF, 58 | marquee_box = 0xFFFFFF44 59 | } -------------------------------------------------------------------------------- /Input.lua: -------------------------------------------------------------------------------- 1 | Input = {} 2 | 3 | function Input.GetShortcuts() 4 | if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Ctrl()) and not reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Shift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then 5 | UR.PopUndo() 6 | end 7 | if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Ctrl()) and reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Shift()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Z()) then 8 | UR.PopRedo() 9 | end 10 | if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Ctrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_C()) then 11 | Clipboard.Copy() 12 | end 13 | if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Ctrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_X()) then 14 | Clipboard.Cut() 15 | end 16 | if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Ctrl()) and reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_V()) then 17 | if #Clipboard.note_list > 0 then App.attempts_paste = true; end 18 | end 19 | 20 | if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_F1()) then 21 | Debug.enabled = not Debug.enabled 22 | end 23 | 24 | if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_S()) then 25 | App.active_tool = e_Tool.Select 26 | end 27 | if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_D()) then 28 | App.active_tool = e_Tool.Draw 29 | end 30 | if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_E()) then 31 | App.active_tool = e_Tool.Erase 32 | end 33 | if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_W()) then 34 | App.active_tool = e_Tool.Move 35 | end 36 | if reaper.ImGui_IsKeyPressed(App.ctx, reaper.ImGui_Key_Escape()) then 37 | App.attempts_paste = false 38 | end 39 | end -------------------------------------------------------------------------------- /Reaffer.lua: -------------------------------------------------------------------------------- 1 | -- Reaffer 2 | -- Based on Ample Sound's riffer found in their guitar/bass VSTs 3 | -- immortalx 4 | 5 | -- TODO 6 | -- When reducing measure count, keep all notes internally (even those on a higher measure count) and if necessary, 7 | -- shorten notes duration (ONLY visually) which happen to start on a valid measure, but their duration extends beyond the measure's boundary. 8 | -- 9 | -- Current dragging system allows each note's properties to be clamped individually 10 | -- Riffer works differently. It clamps all notes to the lowest possible change of that particular property (pitch, position, etc.) 11 | -- Sometimes the first behavior is desirable though. Maybe have a setting for this? 12 | -- 13 | -- There's currently no way to change tuning. Not an easy way to do that. 14 | -- What happens to the notes that fall outside of a string's range, 15 | -- when you change tuning during a session? Needs thinking 16 | -- 17 | -- Generated MIDI item is just a test. No reason to develop it further until articulations are implemented. 18 | 19 | package.path = reaper.ImGui_GetBuiltinPath() .. '/?.lua' 20 | require 'imgui' '0.8.7' 21 | 22 | function msg(txt) 23 | reaper.ShowConsoleMsg(tostring(txt) .. "\n") 24 | end 25 | 26 | local script_path = debug.getinfo(1).source:match("@?(.*[\\|/])") 27 | dofile(script_path .. "Constants.lua") 28 | dofile(script_path .. "App.lua") 29 | dofile(script_path .. "UI.lua") 30 | dofile(script_path .. "Toolbar.lua") 31 | dofile(script_path .. "Util.lua") 32 | dofile(script_path .. "Editor.lua") 33 | dofile(script_path .. "UndoRedo.lua") 34 | dofile(script_path .. "Clipboard.lua") 35 | dofile(script_path .. "Input.lua") 36 | dofile(script_path .. "Debug.lua") 37 | 38 | App.Init() 39 | reaper.defer(App.Loop) 40 | -------------------------------------------------------------------------------- /Clipboard.lua: -------------------------------------------------------------------------------- 1 | Clipboard = 2 | { 3 | note_list = {} 4 | } 5 | 6 | function Clipboard.Cut() 7 | if Clipboard.Copy() then 8 | Editor.EraseNotes(App.note_list_selected[1].offset, App.note_list_selected[1].string_idx) 9 | end 10 | end 11 | 12 | function Clipboard.Copy() 13 | if #App.note_list_selected == 0 then return false; end 14 | 15 | Util.ClearTable(Clipboard.note_list) 16 | Clipboard.note_list = Util.CopyTable(App.note_list_selected) 17 | table.sort(Clipboard.note_list, function (k1, k2) return k1.offset < k2.offset; end) 18 | return true 19 | end 20 | 21 | function Clipboard.Paste(cx, cy) 22 | local temp = {} 23 | for i, v in ipairs(Clipboard.note_list) do 24 | temp[#temp + 1] = Util.CopyNote(v) 25 | end 26 | 27 | local right_bound = Util.NumGridDivisions() 28 | local src_offset = temp[1].offset 29 | local diff = 0 30 | local dst_offset = 0 31 | 32 | for i, v in ipairs(temp) do 33 | diff = v.offset - src_offset 34 | dst_offset = cx + diff 35 | v.offset = dst_offset 36 | 37 | if v.offset + v.duration > right_bound then 38 | return 39 | end 40 | 41 | for j, w in ipairs(App.note_list) do 42 | if (v.string_idx == w.string_idx) and (Util.RangeOverlap(v.offset, v.offset + v.duration - 1, w.offset, w.offset + w.duration - 1)) then 43 | return 44 | end 45 | end 46 | end 47 | 48 | Util.ClearTable(App.note_list_selected) 49 | Util.ClearTable(App.note_list_selected.indices) 50 | 51 | for i, v in ipairs(temp) do 52 | App.note_list[#App.note_list + 1] = Util.CopyNote(v) 53 | App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(v) 54 | App.note_list_selected.indices[#App.note_list_selected.indices + 1] = #App.note_list 55 | end 56 | 57 | UR.PushUndo(e_OpType.Insert, temp) 58 | App.attempts_paste = false 59 | end -------------------------------------------------------------------------------- /Debug.lua: -------------------------------------------------------------------------------- 1 | Debug = {enabled = false} 2 | 3 | function Debug.ShowInfo() 4 | local opt = {"NoOp", "Insert", "Delete", "Pitch + dur", "Vel", "Move"} 5 | if reaper.ImGui_BeginListBox(App.ctx, "Undo stack", 300, 200 ) then 6 | if #UR.undo_stack > 0 then 7 | for i, v in ipairs(UR.undo_stack) do 8 | local rec = v.note_list 9 | reaper.ImGui_Text(App.ctx, "[Rec: " .. i .. ", Type: " .. opt[v.type] .. "]") 10 | for j, m in ipairs(rec) do 11 | reaper.ImGui_Text(App.ctx, " Note: " .. rec[j].offset .. "-" .. rec[j].string_idx) 12 | end 13 | end 14 | end 15 | reaper.ImGui_EndListBox(App.ctx) 16 | end 17 | 18 | reaper.ImGui_SameLine(App.ctx) 19 | 20 | if reaper.ImGui_BeginListBox(App.ctx, "Redo stack", 300, 200 ) then 21 | if #UR.redo_stack > 0 then 22 | for i, v in ipairs(UR.redo_stack) do 23 | local rec = v.note_list 24 | reaper.ImGui_Text(App.ctx, "[Rec: " .. i .. ", Type: " .. opt[v.type] .. "]") 25 | for j, m in ipairs(rec) do 26 | reaper.ImGui_Text(App.ctx, " Note: " .. rec[j].offset .. "-" .. rec[j].string_idx) 27 | end 28 | end 29 | end 30 | reaper.ImGui_EndListBox(App.ctx) 31 | end 32 | 33 | reaper.ImGui_SameLine(App.ctx) 34 | if reaper.ImGui_BeginListBox(App.ctx, "Clipboard", 300, 200 ) then 35 | if #Clipboard.note_list > 0 then 36 | for i, v in ipairs(Clipboard.note_list) do 37 | reaper.ImGui_Text(App.ctx, "Note: " .. v.offset .. "-" .. v.string_idx) 38 | end 39 | end 40 | reaper.ImGui_EndListBox(App.ctx) 41 | end 42 | 43 | if reaper.ImGui_BeginListBox(App.ctx, "Notes", 300, 200 ) then 44 | if #App.note_list > 0 then 45 | for i, v in ipairs(App.note_list) do 46 | reaper.ImGui_Text(App.ctx, i .. " - X:" .. v.offset .. " - Y:" .. v.string_idx .. " - Dur:" ..v.duration) 47 | end 48 | end 49 | reaper.ImGui_EndListBox(App.ctx) 50 | end 51 | 52 | reaper.ImGui_SameLine(App.ctx) 53 | if reaper.ImGui_BeginListBox(App.ctx, "Selected", 300, 200 ) then 54 | if #App.note_list_selected > 0 then 55 | for i, v in ipairs(App.note_list_selected) do 56 | local idx = App.note_list_selected.indices[i] 57 | reaper.ImGui_Text(App.ctx, idx .. " - X:" .. v.offset .. " - Y:" .. v.string_idx .. " - Dur:" ..v.duration) 58 | end 59 | end 60 | reaper.ImGui_EndListBox(App.ctx) 61 | end 62 | 63 | reaper.ImGui_SameLine(App.ctx) 64 | if reaper.ImGui_BeginListBox(App.ctx, "Last Clicked", 300, 50 ) then 65 | if App.last_note_clicked ~= nil then 66 | reaper.ImGui_Text(App.ctx, App.last_note_clicked.idx .. " - X:" .. App.last_note_clicked.offset .. " - Y:" .. App.last_note_clicked.string_idx .. " - Dur:" ..App.last_note_clicked.duration) 67 | end 68 | reaper.ImGui_EndListBox(App.ctx) 69 | end 70 | end -------------------------------------------------------------------------------- /App.lua: -------------------------------------------------------------------------------- 1 | App = 2 | { 3 | -- general 4 | window_title = "Reaffer", 5 | ctx, 6 | is_visible, 7 | is_open, 8 | icon_font, 9 | editor_state, 10 | mouse_x, 11 | mouse_y, 12 | mouse_prev_x, 13 | mouse_prev_y, 14 | scroll_x = 0, 15 | editor_win_x = 0, 16 | editor_win_y = 0, 17 | can_init_drag = false, 18 | current_pitch = 0, 19 | is_new_note = false, 20 | last_click_was_inside_editor = false, 21 | attempts_paste = false, 22 | begin_marquee = false, 23 | 24 | -- metrics 25 | window_w = 800, 26 | window_h = 600, 27 | window_indent, 28 | lane_v_spacing = 12, 29 | cb_strings_w = 36, 30 | cb_signature_w = 76, 31 | cb_quantize_w = 58, 32 | si_measures_w = 140, 33 | cb_note_sisplay_w = 112, 34 | editor_h = 160, 35 | grid_w = 34, 36 | left_margin = 50, 37 | top_margin = 30, 38 | note_w = 34, 39 | note_h = 12, 40 | 41 | -- settings 42 | audition_notes = true, 43 | swap_pitchfret_order = false, 44 | default_velocity = 80, 45 | default_off_velocity = 65, 46 | 47 | -- defaults 48 | wheel_delta = 50, 49 | scroll_margin = 50, 50 | scroll_speed = 0.25, 51 | active_tool = e_Tool.Select, 52 | num_strings = 6, 53 | num_measures = 4, 54 | quantize_cur_idx = 5, 55 | signature_cur_idx = 3, 56 | note_display_cur_idx = e_NoteDisplay.Pitch, 57 | 58 | -- data 59 | quantize = {"1/1", "1/2", "1/4", "1/8", "1/16", "1/32", "1/64"}, 60 | 61 | note_display = {"Pitch", "Fret", "Pitch&Fret", "Velocity", "Off Velocity", "MIDI Pitch"}, 62 | 63 | signature = { 64 | {caption = "2/4", beats = 2, subs = 4}, 65 | {caption = "3/4", beats = 3, subs = 4}, 66 | {caption = "4/4", beats = 4, subs = 4}, 67 | {caption = "5/4", beats = 5, subs = 4}, 68 | {caption = "6/4", beats = 6, subs = 4}, 69 | {caption = "7/4", beats = 7, subs = 4}, 70 | {caption = "8/4", beats = 8, subs = 4}, 71 | {caption = "6/8", beats = 3, subs = 4}, 72 | {caption = "3/4 Tri", beats = 3, subs = 3}, 73 | {caption = "4/4 Tri", beats = 4, subs = 3}, 74 | {caption = "8/4 Tri", beats = 8, subs = 3} 75 | }, 76 | 77 | instrument = 78 | { 79 | {num_strings = 4, open = {28, 33, 38, 43}, recent = {28, 33, 38, 43}, "E1", "A1", "D2", "G2"}, 80 | {num_strings = 5, open = {23, 28, 33, 38, 43}, recent = {23, 28, 33, 38, 43}, "B0", "E1", "A1", "D2", "G2"}, 81 | {num_strings = 6, open = {40, 45, 50, 55, 59, 64}, recent = {40, 45, 50, 55, 59, 64}, "E2", "A2", "D3", "G3", "B3", "E4"}, 82 | {num_strings = 7, open = {35, 40, 45, 50, 55, 59, 64}, recent = {35, 40, 45, 50, 55, 59, 64}, "B1", "E2", "A2", "D3", "G3", "B3", "E4"}, 83 | {num_strings = 8, open = {30, 35, 40, 45, 50, 55, 59, 64}, recent = {30, 35, 40, 45, 50, 55, 59, 64}, "F#1", "B1", "E2", "A2", "D3", "G3", "B3", "E4"}, 84 | {num_strings = 9, open = {25, 30, 35, 40, 45, 50, 55, 59, 64}, recent = {25, 30, 35, 40, 45, 50, 55, 59, 64}, "C#1", "F#1", "B1", "E2", "A2", "D3", "G3", "B3", "E4"}, 85 | {num_strings = 10, open = {52, 45, 38, 31, 24, 40, 45, 50, 55, 60}, recent = {52, 45, 38, 31, 24, 40, 45, 50, 55, 60}, "E3", "A2", "D2", "G1", "C1", "E2", "A2", "D3", "G3", "C4"} 86 | }, 87 | 88 | note_sequence = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}, 89 | 90 | note_list = 91 | { 92 | -- { offset = 6, string_idx = 2, pitch = 25, velocity = 127, off_velocity = 80, duration = 1}, 93 | }, 94 | 95 | -- table, storing last clicked note object. 96 | -- That's different from last selected, as you can "re-click" an already selected note. 97 | -- Also, stores an 'idx' field 98 | last_note_clicked, 99 | 100 | note_list_selected = { indices = {} }, 101 | marquee_box = {x1 = 0, y1 = 0, x2 = 0, y2 = 0} 102 | } 103 | 104 | function App.Init() 105 | local script_path = debug.getinfo(1).source:match("@?(.*[\\|/])") 106 | App.ctx = reaper.ImGui_CreateContext('Riffer script') 107 | App.icon_font = reaper.ImGui_CreateFont(script_path .. "icons.ttf", 14) 108 | reaper.ImGui_Attach(App.ctx, App.icon_font) 109 | Colors.text = reaper.ImGui_GetStyleColor(App.ctx, reaper.ImGui_Col_Text()) 110 | App.window_indent = reaper.ImGui_StyleVar_IndentSpacing() 111 | App.window_w = reaper.ImGui_GetWindowWidth(App.ctx) 112 | end 113 | 114 | function App.Loop() 115 | App.is_visible, App.is_open = reaper.ImGui_Begin(App.ctx, App.window_title, true) 116 | 117 | if App.is_visible then 118 | App.mouse_x, App.mouse_y = reaper.ImGui_GetMousePos(App.ctx) 119 | App.window_w = reaper.ImGui_GetWindowWidth(App.ctx) 120 | 121 | Input.GetShortcuts() 122 | 123 | UI.Render_CB_Strings() 124 | UI.Render_CB_Signature() 125 | UI.Render_CB_Quantize() 126 | UI.Render_SI_Measures() 127 | UI.Render_CB_NoteDisplay() 128 | UI.Render_BTN_Settings() 129 | UI.Render_TXT_Help() 130 | UI.Render_Editor() 131 | UI.Render_Toolbar() 132 | if Debug.enabled then Debug.ShowInfo(); end 133 | 134 | App.mouse_prev_x, App.mouse_prev_y = App.mouse_x, App.mouse_y 135 | reaper.ImGui_End(App.ctx) 136 | end 137 | 138 | if App.is_open then 139 | reaper.defer(App.Loop) 140 | else 141 | reaper.ImGui_DestroyContext(App.ctx) 142 | end 143 | end -------------------------------------------------------------------------------- /UndoRedo.lua: -------------------------------------------------------------------------------- 1 | UR = 2 | { 3 | last_op = nil, 4 | undo_stack = {}, 5 | redo_stack = {} 6 | -- {type = ?, note_list = { {offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } 7 | -- {type = ?, note_list = { {offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } 8 | 9 | -- NOTE If the type is move, include indices. 10 | -- {type = ?, indices = {}, note_list = { {offset = ?, etc}, {idx = ?, offset = ?, etc}, ... } } 11 | } 12 | 13 | function UR.PushUndo(type, note_list) 14 | local new_rec = {type = type, note_list = Util.CopyTable(note_list)} 15 | 16 | if type == e_OpType.Move then 17 | new_rec.indices = Util.CopyTable(note_list.indices) 18 | end 19 | 20 | UR.undo_stack[#UR.undo_stack + 1] = new_rec 21 | 22 | -- clear redo stack 23 | if #UR.redo_stack > 0 then 24 | Util.ClearTable(UR.redo_stack) 25 | end 26 | end 27 | 28 | function UR.PopUndo() 29 | if #UR.undo_stack == 0 then return; end 30 | 31 | local last_rec = UR.undo_stack[#UR.undo_stack] 32 | local type = last_rec.type 33 | 34 | if type == e_OpType.Delete then 35 | for i, v in ipairs(last_rec.note_list) do 36 | table.insert(App.note_list, v) 37 | end 38 | end 39 | 40 | if type == e_OpType.Insert then 41 | for i, v in ipairs(last_rec.note_list) do 42 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 43 | table.remove(App.note_list, idx) 44 | end 45 | end 46 | 47 | if type == e_OpType.ModifyPitchAndDuration then 48 | local temp = {} 49 | 50 | for i, v in ipairs(last_rec.note_list) do 51 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 52 | 53 | temp.pitch = App.note_list[idx].pitch 54 | temp.duration = App.note_list[idx].duration 55 | 56 | App.note_list[idx].pitch = v.pitch 57 | App.note_list[idx].duration = v.duration 58 | 59 | v.pitch = temp.pitch 60 | v.duration = temp.duration 61 | end 62 | end 63 | 64 | if type == e_OpType.ModifyVelocityAndOffVelocity then 65 | local temp = {} 66 | 67 | for i, v in ipairs(last_rec.note_list) do 68 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 69 | 70 | temp.velocity = App.note_list[idx].velocity 71 | temp.off_velocity = App.note_list[idx].off_velocity 72 | 73 | App.note_list[idx].velocity = v.velocity 74 | App.note_list[idx].off_velocity = v.off_velocity 75 | 76 | v.velocity = temp.velocity 77 | v.off_velocity = temp.off_velocity 78 | end 79 | end 80 | 81 | if type == e_OpType.Move then 82 | local temp = {} 83 | -- pitch may be modified by moving note to different string. So we account for that too 84 | for i, v in ipairs(last_rec.note_list) do 85 | -- local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 86 | local idx = last_rec.indices[i] 87 | 88 | temp.offset = App.note_list[idx].offset 89 | temp.string_idx = App.note_list[idx].string_idx 90 | temp.pitch = App.note_list[idx].pitch 91 | 92 | App.note_list[idx].offset = v.offset 93 | App.note_list[idx].string_idx = v.string_idx 94 | App.note_list[idx].pitch = v.pitch 95 | 96 | v.offset = temp.offset 97 | v.string_idx = temp.string_idx 98 | v.pitch = temp.pitch 99 | end 100 | end 101 | 102 | -- push to the redo stack, pop from the undo 103 | UR.redo_stack[#UR.redo_stack + 1] = last_rec 104 | UR.undo_stack[#UR.undo_stack] = nil 105 | 106 | Util.ClearTable(App.note_list_selected) 107 | App.last_note_clicked = nil 108 | end 109 | 110 | function UR.PopRedo() 111 | if #UR.redo_stack == 0 then return; end 112 | 113 | -- restore op 114 | local last_rec = UR.redo_stack[#UR.redo_stack] 115 | local type = last_rec.type 116 | 117 | if type == e_OpType.Insert then 118 | for i, v in ipairs(last_rec.note_list) do 119 | table.insert(App.note_list, v) 120 | end 121 | end 122 | 123 | if type == e_OpType.Delete then 124 | for i, v in ipairs(last_rec.note_list) do 125 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 126 | table.remove(App.note_list, idx) 127 | end 128 | end 129 | 130 | if type == e_OpType.ModifyPitchAndDuration then 131 | local temp = {} 132 | 133 | for i, v in ipairs(last_rec.note_list) do 134 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 135 | 136 | temp.pitch = App.note_list[idx].pitch 137 | temp.duration = App.note_list[idx].duration 138 | 139 | App.note_list[idx].pitch = v.pitch 140 | App.note_list[idx].duration = v.duration 141 | 142 | v.pitch = temp.pitch 143 | v.duration = temp.duration 144 | end 145 | end 146 | 147 | if type == e_OpType.ModifyVelocityAndOffVelocity then 148 | local temp = {} 149 | 150 | for i, v in ipairs(last_rec.note_list) do 151 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 152 | 153 | temp.velocity = App.note_list[idx].velocity 154 | temp.off_velocity = App.note_list[idx].off_velocity 155 | 156 | App.note_list[idx].velocity = v.velocity 157 | App.note_list[idx].off_velocity = v.off_velocity 158 | 159 | v.velocity = temp.velocity 160 | v.off_velocity = temp.off_velocity 161 | end 162 | end 163 | 164 | if type == e_OpType.Move then 165 | local temp = {} 166 | 167 | for i, v in ipairs(last_rec.note_list) do 168 | local idx = last_rec.indices[i] 169 | 170 | temp.offset = App.note_list[idx].offset 171 | temp.string_idx = App.note_list[idx].string_idx 172 | temp.pitch = App.note_list[idx].pitch 173 | 174 | App.note_list[idx].offset = v.offset 175 | App.note_list[idx].string_idx = v.string_idx 176 | App.note_list[idx].pitch = v.pitch 177 | 178 | v.offset = temp.offset 179 | v.string_idx = temp.string_idx 180 | v.pitch = temp.pitch 181 | end 182 | end 183 | 184 | -- push to the undo stack, pop from the redo 185 | UR.undo_stack[#UR.undo_stack + 1] = last_rec 186 | UR.redo_stack[#UR.redo_stack] = nil 187 | end -------------------------------------------------------------------------------- /Util.lua: -------------------------------------------------------------------------------- 1 | Util = {} 2 | 3 | function Util.HorSpacer(num_spacers) 4 | for i = 0, num_spacers - 1 do 5 | reaper.ImGui_SameLine(App.ctx) 6 | reaper.ImGui_Spacing(App.ctx) 7 | end 8 | reaper.ImGui_SameLine(App.ctx) 9 | end 10 | 11 | function Util.NumGridDivisions() 12 | return (App.num_measures * App.signature[App.signature_cur_idx].beats * App.signature[App.signature_cur_idx].subs) 13 | end 14 | 15 | function Util.Clamp(n, n_min, n_max) 16 | if n < n_min then n = n_min 17 | elseif n > n_max then n = n_max 18 | end 19 | 20 | return n 21 | end 22 | 23 | function Util.PackRGBA(r, g, b, a) 24 | return (r<<24 | g<<16 | b<<8 | a) 25 | end 26 | 27 | function Util.VelocityColor(v) 28 | v = Util.Clamp(v, 0, 127) 29 | local g = 23 30 | local bt = math.floor(127 - v) 31 | local rt = 127 - bt 32 | 33 | local r = math.floor(Util.Clamp((255 * rt) / 127, 0, 255)) 34 | local b = math.floor(Util.Clamp((255 * bt) / 127, 0, 255)) 35 | 36 | return Util.PackRGBA(r, g, b, 255) 37 | end 38 | 39 | function Util.NotePitchToName(note_pitch) 40 | local mul = math.floor(note_pitch / 12) 41 | local idx = note_pitch - (mul * 12) 42 | return App.note_sequence[idx + 1] .. mul - 1 43 | end 44 | 45 | function Util.NoteNameToPitch(note_name) 46 | 47 | local len = string.len(note_name) 48 | local wname 49 | 50 | if len == 2 then 51 | wname = string.sub(note_name, 1, 1) 52 | else 53 | wname = string.sub(note_name, 1, 2) 54 | end 55 | 56 | local idx = 0 57 | 58 | for i, v in ipairs(App.note_sequence) do 59 | if v == wname then 60 | idx = i 61 | break 62 | end 63 | end 64 | 65 | local oct = string.sub(note_name, len, len) 66 | local base = 12 -- pitch of C0 67 | return (base + idx - 1) + (oct * 12) 68 | end 69 | 70 | function Util.NotePitchToFret(pitch, string_idx) 71 | local open = App.instrument[App.num_strings - 3].open[App.num_strings - string_idx] 72 | return math.floor(pitch - open) 73 | end 74 | 75 | function Util.GetCellX() 76 | return math.floor((App.mouse_x - App.editor_win_x + App.scroll_x -15) / App.note_w) - 1 77 | end 78 | 79 | function Util.GetCellY() 80 | return math.floor((App.mouse_y - App.editor_win_y - App.top_margin + 5) / App.note_h) 81 | end 82 | 83 | function Util.IsCellEmpty(cx, cy, duration_inclusive) 84 | for i, note in ipairs(App.note_list) do 85 | local start_x = note.offset 86 | local end_x = start_x + note.duration - 1 87 | if duration_inclusive then 88 | if (cx >= start_x) and (cx <= end_x) and (note.string_idx == cy) then return false; end 89 | else 90 | if (cx == start_x) and (note.string_idx == cy) then return false; end 91 | end 92 | end 93 | return true 94 | end 95 | 96 | function Util.GetCellNearestOccupied(cx, cy, direction, note_idx) 97 | 98 | if direction == e_Direction.Right then 99 | local cur = Util.NumGridDivisions() 100 | 101 | for i, note in ipairs(App.note_list) do 102 | if (cy == note.string_idx) and (note.offset > cx) then 103 | if note.offset < cur and i ~= note_idx then 104 | cur = note.offset 105 | end 106 | end 107 | end 108 | return cur 109 | end 110 | 111 | if direction == e_Direction.Left then 112 | local cur = 0 113 | 114 | for i, note in ipairs(App.note_list) do 115 | if (cy == note.string_idx) and (note.offset < cx) then 116 | if note.offset + note.duration > cur and i ~= note_idx then 117 | cur = note.offset + note.duration 118 | end 119 | end 120 | end 121 | return cur 122 | end 123 | end 124 | 125 | function Util.RangeOverlap(a1, a2, b1, b2) 126 | if a2 < b1 or b2 < a1 then return false; end 127 | return true 128 | end 129 | 130 | function Util.IsNewPositionOnStringEmpty(note_idx, new_x, y) 131 | for i, v in ipairs(App.note_list) do 132 | if (y == v.string_idx) and (i ~= note_idx) and not (Util.IsNoteSelected(i)) then -- exclude notes on other strings, self and any selected notes 133 | if Util.RangeOverlap(new_x, new_x + App.note_list[note_idx].duration - 1, v.offset, v.offset + v.duration - 1) then 134 | return false 135 | end 136 | end 137 | end 138 | 139 | return true 140 | end 141 | 142 | function Util.CreateMIDI() 143 | local ppq = 960 144 | local q = {0.25, 0.5, 1, 2, 4, 8, 16} 145 | -- {"1/1", "1/2", "1/4", "1/8", "1/16", "1/32", "1/64"}, 146 | ratio = ppq / q[App.quantize_cur_idx] 147 | 148 | local track = reaper.GetSelectedTrack(0, 0) 149 | if track == nil then return; end 150 | 151 | local start_time_secs = reaper.GetCursorPositionEx(0) 152 | local end_time_secs = reaper.TimeMap2_beatsToTime(0, 0, App.num_measures) 153 | local new_item = reaper.CreateNewMIDIItemInProj(track, start_time_secs, start_time_secs + end_time_secs) 154 | local take = reaper.GetActiveTake(new_item) 155 | local start_ppq = reaper.MIDI_GetPPQPosFromProjTime(take, start_time_secs) 156 | 157 | local note_begin 158 | local note_end 159 | 160 | for i, note in ipairs(App.note_list) do 161 | note_begin = start_ppq + (note.offset * ratio) 162 | note_end = note_begin + (note.duration * ratio) 163 | reaper.MIDI_InsertNote(take, false, false, note_begin, note_end, 0, note.pitch, note.velocity) 164 | end 165 | end 166 | 167 | function Util.CopyNote(note) 168 | if note == nil then 169 | msg("note is nil") 170 | return 171 | end 172 | local t = {offset = note.offset, string_idx = note.string_idx, pitch = note.pitch, velocity = note.velocity, off_velocity = note.off_velocity, duration = note.duration} 173 | return t 174 | end 175 | 176 | function Util.ClearTable(t) 177 | for i, v in ipairs(t) do 178 | t[i] = nil 179 | end 180 | end 181 | 182 | function Util.CopyTable(t) 183 | local new_t = {} 184 | for i, v in ipairs(t) do 185 | new_t[i] = v 186 | end 187 | 188 | return new_t 189 | end 190 | 191 | function Util.IsNoteAtCellSelected(cx, cy) 192 | for i, v in ipairs(App.note_list_selected) do 193 | if (cx >= v.offset) and (cx < v.offset + v.duration) and (cy == v.string_idx) then 194 | return true 195 | end 196 | end 197 | 198 | return false 199 | end 200 | 201 | function Util.IsNoteSelected(note_idx) 202 | for i, v in ipairs(App.note_list_selected.indices) do 203 | if v == note_idx then return true; end 204 | end 205 | 206 | return false 207 | end 208 | 209 | function Util.GetNoteIndexAtCell(cx, cy) 210 | for i, v in ipairs(App.note_list) do 211 | if (cx >= v.offset) and (cx < v.offset + v.duration) and (cy == v.string_idx) then 212 | return i 213 | end 214 | end 215 | msg("not found") 216 | return 0 -- Not found 217 | end 218 | 219 | -- Refreshes note_list_selected with the (presumably) modified notes from note_list, after mouse release 220 | function Util.UpdateSelectedNotes() 221 | if #App.note_list == 0 or #App.note_list_selected == 0 then return; end 222 | for i, v in ipairs(App.note_list_selected) do 223 | local idx = App.note_list_selected.indices[i] 224 | App.note_list_selected[i] = Util.CopyNote(App.note_list[idx]) 225 | end 226 | end 227 | 228 | function Util.UpdateRecentPitch(string_idx, new_pitch) 229 | App.instrument[App.num_strings - 3].recent[string_idx] = new_pitch 230 | end 231 | 232 | function Util.ShiftOctaveIfOutsideRange(note, target_string_idx) 233 | if note.string_idx == target_string_idx then return; end 234 | 235 | local min_pitch = App.instrument[App.num_strings - 3].open[App.num_strings - target_string_idx] 236 | local max_pitch = min_pitch + 24 237 | 238 | 239 | if note.pitch > min_pitch and note.pitch > max_pitch then 240 | Editor.StopNote() 241 | note.pitch = note.pitch - 12 242 | App.current_pitch = note.pitch 243 | Editor.PlayNote() 244 | elseif note.pitch < min_pitch and note.pitch < max_pitch then 245 | Editor.StopNote() 246 | note.pitch = note.pitch + 12 247 | App.current_pitch = note.pitch 248 | Editor.PlayNote() 249 | end 250 | end -------------------------------------------------------------------------------- /Editor.lua: -------------------------------------------------------------------------------- 1 | Editor = {} 2 | 3 | function Editor.OnMouseButtonClick(mbutton, cx, cy) 4 | if App.attempts_paste then 5 | if mbutton == e_MouseButton.Left and Util.IsCellEmpty(cx, cy, true) then 6 | Clipboard.Paste(cx, cy) 7 | end 8 | return 9 | end 10 | 11 | App.can_init_drag = true 12 | App.last_click_was_inside_editor = true 13 | 14 | if mbutton == e_MouseButton.Left or mbutton == e_MouseButton.Right then 15 | if Util.IsCellEmpty(cx, cy, true) then 16 | Util.ClearTable(App.note_list_selected) 17 | Util.ClearTable(App.note_list_selected.indices) 18 | App.last_note_clicked = nil 19 | 20 | if App.active_tool == e_Tool.Draw then 21 | Editor.InsertNote(cx, cy) 22 | end 23 | else 24 | if (App.active_tool == e_Tool.Select) or (App.active_tool == e_Tool.Move) or (App.active_tool == e_Tool.Draw) then 25 | Editor.SelectNotes(cx, cy) 26 | Editor.PlayNote() 27 | elseif App.active_tool == e_Tool.Erase then 28 | Editor.EraseNotes(cx, cy) 29 | end 30 | end 31 | end 32 | end 33 | 34 | function Editor.OnMouseButtonRelease(mbutton) 35 | App.begin_marquee = false 36 | 37 | if App.last_click_was_inside_editor then 38 | App.can_init_drag = false 39 | App.last_click_was_inside_editor = false 40 | 41 | if mbutton == e_MouseButton.Left or mbutton == e_MouseButton.Right then 42 | Editor.StopNote() 43 | App.last_note_clicked = nil 44 | 45 | if #App.note_list_selected > 0 and not (App.is_new_note) then 46 | if UR.last_op == e_OpType.ModifyPitchAndDuration then 47 | UR.PushUndo(e_OpType.ModifyPitchAndDuration, App.note_list_selected) 48 | elseif UR.last_op == e_OpType.ModifyVelocityAndOffVelocity then 49 | UR.PushUndo(e_OpType.ModifyVelocityAndOffVelocity, App.note_list_selected) 50 | elseif UR.last_op == e_OpType.Move then 51 | UR.PushUndo(e_OpType.Move, App.note_list_selected) 52 | end 53 | end 54 | 55 | App.is_new_note = false 56 | Util.UpdateSelectedNotes() 57 | UR.last_op = e_OpType.NoOp 58 | end 59 | end 60 | end 61 | 62 | function Editor.OnMouseButtonDrag(mbutton) 63 | local cx = Util.GetCellX() 64 | local cy = Util.GetCellY() 65 | 66 | if App.last_click_was_inside_editor then 67 | if App.mouse_x > App.editor_win_x + App.window_w - App.scroll_margin then 68 | App.scroll_x = App.scroll_x + ((App.mouse_x - (App.editor_win_x + App.window_w - App.scroll_margin)) * App.scroll_speed) 69 | reaper.ImGui_SetScrollX(App.ctx, App.scroll_x) 70 | end 71 | 72 | if App.mouse_x < App.editor_win_x + App.left_margin + App.scroll_margin then 73 | App.scroll_x = App.scroll_x - ((App.editor_win_x + App.left_margin + App.scroll_margin - App.mouse_x) * App.scroll_speed) 74 | reaper.ImGui_SetScrollX(App.ctx, App.scroll_x) 75 | end 76 | 77 | if mbutton == e_MouseButton.Left and App.active_tool == e_Tool.Select then 78 | if not (App.begin_marquee) and App.last_note_clicked == nil then 79 | App.marquee_box.x1 = App.mouse_x 80 | App.marquee_box.y1 = App.mouse_y 81 | App.begin_marquee = true 82 | end 83 | 84 | if App.begin_marquee then 85 | Editor.MarqueeSelectNotes(cx, cy) 86 | end 87 | end 88 | end 89 | 90 | if App.last_note_clicked == nil then return; end 91 | App.can_init_drag = false 92 | 93 | -- dx/dy = difference from initial pos 94 | local dx = cx - App.last_note_clicked.duration - App.last_note_clicked.offset + 1 95 | local dy = App.last_note_clicked.string_idx - cy 96 | 97 | if mbutton == e_MouseButton.Left then 98 | if App.active_tool == e_Tool.Draw or App.active_tool == e_Tool.Select then 99 | Editor.ModifyPitchAndDuration(cx, cy, dx, dy) 100 | elseif App.active_tool == e_Tool.Move then 101 | Editor.MoveNotes(cx, cy, dx, dy) 102 | end 103 | elseif mbutton == e_MouseButton.Right then 104 | Editor.ModifyVelocityAndOffVelocity(cx, cy, dx, dy) 105 | end 106 | end 107 | 108 | function Editor.MarqueeSelectNotes(cx, cy) 109 | Util.ClearTable(App.note_list_selected) 110 | Util.ClearTable(App.note_list_selected.indices) 111 | 112 | local start_x = math.min(App.marquee_box.x1, App.marquee_box.x2) 113 | local start_y = math.min(App.marquee_box.y1, App.marquee_box.y2) 114 | local end_x = math.max(App.marquee_box.x1, App.marquee_box.x2) 115 | local end_y = math.max(App.marquee_box.y1, App.marquee_box.y2) 116 | 117 | local cell_x_min = math.floor((start_x - App.editor_win_x + App.scroll_x -15) / App.note_w) - 1 118 | local cell_y_min = math.floor((start_y - App.editor_win_y - App.top_margin + 5) / App.note_h) 119 | local cell_x_max = math.floor((end_x - App.editor_win_x + App.scroll_x -15) / App.note_w) - 1 120 | local cell_y_max = math.floor((end_y - App.editor_win_y - App.top_margin + 5) / App.note_h) 121 | 122 | for i, v in ipairs(App.note_list) do 123 | if not (Util.IsNoteAtCellSelected(v.offset, v.string_idx)) then 124 | if Util.RangeOverlap(v.offset, v.offset + v.duration - 1, cell_x_min, cell_x_max) and Util.RangeOverlap(v.string_idx, v.string_idx, cell_y_min, cell_y_max) then 125 | App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(v) 126 | App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i 127 | end 128 | end 129 | end 130 | end 131 | 132 | function Editor.SelectNotes(cx, cy) 133 | for i, note in ipairs(App.note_list) do 134 | if (cx >= note.offset) and (cx < note.offset + note.duration) and (cy == note.string_idx) then 135 | if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Ctrl()) then 136 | if not (Util.IsNoteAtCellSelected(note.offset, note.string_idx)) then 137 | App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) 138 | App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i 139 | end 140 | else 141 | if not (Util.IsNoteAtCellSelected(note.offset, note.string_idx)) then 142 | Util.ClearTable(App.note_list_selected) 143 | Util.ClearTable(App.note_list_selected.indices) 144 | App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(note) 145 | App.note_list_selected.indices[#App.note_list_selected.indices + 1] = i 146 | end 147 | end 148 | App.last_note_clicked = Util.CopyNote(note) 149 | App.last_note_clicked.idx = i 150 | App.current_pitch = App.last_note_clicked.pitch 151 | end 152 | end 153 | end 154 | 155 | function Editor.InsertNote(cx, cy) 156 | local recent_pitch = App.instrument[App.num_strings - 3].recent[App.num_strings - cy] 157 | local new_note = {offset = cx, string_idx = cy, pitch = recent_pitch, velocity = App.default_velocity, off_velocity = App.default_off_velocity, duration = 1} 158 | App.note_list[#App.note_list + 1] = new_note 159 | App.note_list_selected[#App.note_list_selected + 1] = Util.CopyNote(new_note) 160 | App.note_list_selected.indices[#App.note_list_selected.indices + 1] = #App.note_list 161 | App.last_note_clicked = Util.CopyNote(new_note) 162 | App.last_note_clicked.idx = #App.note_list 163 | App.current_pitch = App.last_note_clicked.pitch 164 | Editor.PlayNote() 165 | -- push undo here 166 | UR.PushUndo(e_OpType.Insert, {new_note}) 167 | App.is_new_note = true 168 | UR.last_op = e_OpType.Insert 169 | end 170 | 171 | function Editor.EraseNotes(cx, cy) 172 | if Util.IsNoteAtCellSelected(cx, cy) then 173 | -- push undo here (multiple) 174 | UR.PushUndo(e_OpType.Delete, App.note_list_selected) 175 | 176 | for i, v in ipairs(App.note_list_selected) do 177 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 178 | table.remove(App.note_list, idx) 179 | end 180 | else 181 | local idx = Util.GetNoteIndexAtCell(cx, cy) 182 | -- push undo here (single) 183 | UR.PushUndo(e_OpType.Delete, {App.note_list[idx]}) 184 | table.remove(App.note_list, idx) 185 | end 186 | 187 | UR.last_op = e_OpType.Delete 188 | Util.ClearTable(App.note_list_selected) 189 | Util.ClearTable(App.note_list_selected.indices) 190 | end 191 | 192 | function Editor.MoveNotes(cx, cy, dx, dy) 193 | if dx ~= 0 or dy ~= 0 then 194 | local all_fit = true 195 | local base_diff = dx + App.last_note_clicked.duration - 1 196 | local leftmost = Util.NumGridDivisions(); local topmost = App.num_strings - 1; local rightmost = 0; local bottommost = 0 197 | 198 | for i, v in ipairs(App.note_list_selected) do 199 | local idx = App.note_list_selected.indices[i] 200 | if App.note_list[idx].offset < leftmost then leftmost = App.note_list[idx].offset; end 201 | if App.note_list[idx].offset + App.note_list[idx].duration - 1 > rightmost then rightmost = App.note_list[idx].offset + App.note_list[idx].duration - 1; end 202 | if App.note_list[idx].string_idx < topmost then topmost = App.note_list[idx].string_idx; end 203 | if App.note_list[idx].string_idx > bottommost then bottommost = App.note_list[idx].string_idx; end 204 | 205 | if not Util.IsNewPositionOnStringEmpty(App.note_list_selected.indices[i], v.offset + base_diff, v.string_idx - dy) then 206 | all_fit = false 207 | break 208 | end 209 | end 210 | 211 | local base_x = App.note_list[App.last_note_clicked.idx].offset 212 | local base_y = App.note_list[App.last_note_clicked.idx].string_idx 213 | local l_bound = base_x - leftmost 214 | local r_bound = Util.NumGridDivisions() - rightmost + base_x 215 | local t_bound = base_y - topmost 216 | local b_bound = App.num_strings - bottommost + base_y 217 | 218 | if all_fit then 219 | if cx >= l_bound and cx < r_bound and cy >= t_bound and cy < b_bound then 220 | for i, v in ipairs(App.note_list_selected) do 221 | local idx = App.note_list_selected.indices[i] 222 | App.note_list[idx].offset = v.offset + dx + App.last_note_clicked.duration - 1 223 | 224 | local dst_string_idx = Util.Clamp(v.string_idx - dy, 0, App.num_strings - 1) 225 | Util.ShiftOctaveIfOutsideRange(App.note_list[idx], dst_string_idx) 226 | App.note_list[idx].string_idx = dst_string_idx 227 | end 228 | end 229 | 230 | UR.last_op = e_OpType.Move 231 | end 232 | else 233 | UR.last_op = e_OpType.NoOp 234 | end 235 | end 236 | 237 | function Editor.ModifyPitchAndDuration(cx, cy, dx, dy) 238 | if dx ~= 0 or dy ~= 0 then 239 | for i, v in ipairs(App.note_list_selected) do 240 | 241 | -- duration 242 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 243 | 244 | local nearest = Util.GetCellNearestOccupied(App.note_list[idx].offset, App.note_list[idx].string_idx, e_Direction.Right) 245 | if cx >= App.last_note_clicked.offset then 246 | App.note_list[idx].duration = Util.Clamp(v.duration + dx, 1, nearest - App.note_list[idx].offset) 247 | end 248 | -- pitch 249 | local pitch_min = App.instrument[App.num_strings - 3].open[App.num_strings - App.note_list[idx].string_idx] 250 | local pitch_max = pitch_min + 24 251 | App.note_list[idx].pitch = Util.Clamp(v.pitch + dy, pitch_min, pitch_max) 252 | Util.UpdateRecentPitch(App.num_strings - v.string_idx, App.note_list[idx].pitch) 253 | end 254 | 255 | local idx = Util.GetNoteIndexAtCell(App.last_note_clicked.offset, App.last_note_clicked.string_idx) 256 | 257 | if App.current_pitch ~= App.note_list[idx].pitch then 258 | Editor.StopNote() 259 | App.current_pitch = App.note_list[idx].pitch 260 | Editor.PlayNote() 261 | end 262 | 263 | UR.last_op = e_OpType.ModifyPitchAndDuration 264 | else 265 | UR.last_op = e_OpType.NoOp 266 | end 267 | end 268 | 269 | function Editor.ModifyVelocityAndOffVelocity(cx, cy, dx, dy) 270 | if dy ~= 0 then 271 | for i, v in ipairs(App.note_list_selected) do 272 | local idx = Util.GetNoteIndexAtCell(v.offset, v.string_idx) 273 | 274 | if reaper.ImGui_IsKeyDown(App.ctx, reaper.ImGui_Mod_Shift()) then 275 | App.note_list[idx].off_velocity = Util.Clamp(v.off_velocity + dy, 0, 127) 276 | else 277 | App.note_list[idx].velocity = Util.Clamp(v.velocity + dy, 0, 127) 278 | end 279 | end 280 | 281 | UR.last_op = e_OpType.ModifyVelocityAndOffVelocity 282 | else 283 | UR.last_op = e_OpType.NoOp 284 | end 285 | end 286 | 287 | function Editor.PlayNote() 288 | if App.last_note_clicked == nil or App.audition_notes == false then return; end 289 | reaper.StuffMIDIMessage(0, 0x90, App.current_pitch, App.last_note_clicked.velocity) 290 | end 291 | 292 | function Editor.StopNote() 293 | if App.last_note_clicked == nil or App.audition_notes == false then return; end 294 | reaper.StuffMIDIMessage(0, 0x80, App.current_pitch, App.last_note_clicked.velocity) 295 | end 296 | -------------------------------------------------------------------------------- /UI.lua: -------------------------------------------------------------------------------- 1 | UI = {} 2 | 3 | function UI.Render_Notes(draw_list) 4 | local note_x 5 | local note_y 6 | local str 7 | 8 | for i, note in ipairs(App.note_list) do 9 | if note.string_idx < App.num_strings then -- When switching from higher string count instrument to a lower one, notes are kept but hidden 10 | note_x = App.editor_win_x + 50 + (note.offset * App.note_w) - App.scroll_x 11 | note_y = App.editor_win_y + 30 + (note.string_idx * App.note_h) - 5 12 | 13 | reaper.ImGui_DrawList_AddRectFilled(draw_list, note_x, note_y, note_x + (App.note_w * note.duration) -1, note_y + App.note_h - 1, Util.VelocityColor(note.velocity), 6) 14 | if App.note_display_cur_idx == e_NoteDisplay.Pitch then 15 | str = Util.NotePitchToName(note.pitch) 16 | elseif App.note_display_cur_idx == e_NoteDisplay.Fret then 17 | str = Util.NotePitchToFret(note.pitch, note.string_idx) 18 | elseif App.note_display_cur_idx == e_NoteDisplay.PitchAndFret then -- display pitch + fret. Only if duration > 1. If duration == 1, just display the pitch 19 | if App.swap_pitchfret_order then 20 | str = Util.NotePitchToFret(note.pitch, note.string_idx) 21 | if note.duration > 1 then str = str .. "," .. Util.NotePitchToName(note.pitch); end 22 | else 23 | str = Util.NotePitchToName(note.pitch) 24 | if note.duration > 1 then str = str .. "," .. Util.NotePitchToFret(note.pitch, note.string_idx); end 25 | end 26 | elseif App.note_display_cur_idx == e_NoteDisplay.Velocity then 27 | str = note.velocity 28 | elseif App.note_display_cur_idx == e_NoteDisplay.OffVelocity then 29 | str = note.off_velocity 30 | elseif App.note_display_cur_idx == e_NoteDisplay.MIDIPitch then 31 | str = note.pitch 32 | end 33 | reaper.ImGui_DrawList_AddText(draw_list, note_x + 5, note_y - 2, Colors.text, str) 34 | 35 | if Util.IsNoteSelected(i) then 36 | reaper.ImGui_DrawList_AddRect(draw_list, note_x, note_y, note_x + (App.note_w * note.duration) - 1, note_y + App.note_h - 1, Colors.text, 40, reaper.ImGui_DrawFlags_None(), 1) 37 | end 38 | end 39 | end 40 | end 41 | 42 | function UI.Render_CB_Strings() 43 | reaper.ImGui_SetNextItemWidth(App.ctx, App.cb_strings_w) 44 | 45 | if reaper.ImGui_BeginCombo(App.ctx, "Strings##cb_strings", App.num_strings) then 46 | for i = 4, 10 do 47 | if reaper.ImGui_Selectable(App.ctx, i, App.num_strings == i) then App.num_strings = i; end 48 | end 49 | reaper.ImGui_EndCombo(App.ctx) 50 | end 51 | end 52 | 53 | function UI.Render_CB_Signature() 54 | Util.HorSpacer(3) 55 | reaper.ImGui_SetNextItemWidth(App.ctx, App.cb_signature_w) 56 | if reaper.ImGui_BeginCombo(App.ctx, "Signature##cb_signature", App.signature[App.signature_cur_idx].caption, reaper.ImGui_ComboFlags_HeightLarge()) then 57 | for i = 1, 11 do 58 | if reaper.ImGui_Selectable(App.ctx, App.signature[i].caption, App.signature_cur_idx == i) then App.signature_cur_idx = i; end 59 | end 60 | reaper.ImGui_EndCombo(App.ctx) 61 | end 62 | end 63 | 64 | function UI.Render_CB_Quantize() 65 | Util.HorSpacer(3) 66 | reaper.ImGui_SetNextItemWidth(App.ctx, App.cb_quantize_w) 67 | if reaper.ImGui_BeginCombo(App.ctx, "Quantize##cb_quantize", App.quantize[App.quantize_cur_idx]) then 68 | for i = 1, 7 do 69 | if reaper.ImGui_Selectable(App.ctx, App.quantize[i], App.quantize_cur_idx == i) then App.quantize_cur_idx = i; end 70 | end 71 | reaper.ImGui_EndCombo(App.ctx) 72 | end 73 | end 74 | 75 | function UI.Render_SI_Measures() 76 | Util.HorSpacer(3) 77 | reaper.ImGui_SetNextItemWidth(App.ctx, App.si_measures_w) 78 | local ret, val = reaper.ImGui_SliderInt(App.ctx, "Measures##si_measures", App.num_measures, 1, 64) 79 | App.num_measures = Util.Clamp(val, 1, 64) 80 | end 81 | 82 | function UI.Render_CB_NoteDisplay() 83 | Util.HorSpacer(3) 84 | reaper.ImGui_SetNextItemWidth(App.ctx, App.cb_note_sisplay_w) 85 | if reaper.ImGui_BeginCombo(App.ctx, "Note Display##cb_note_display", App.note_display[App.note_display_cur_idx]) then 86 | for i = 1, 6 do 87 | if reaper.ImGui_Selectable(App.ctx, App.note_display[i], App.note_display_cur_idx == i) then App.note_display_cur_idx = i; end 88 | end 89 | reaper.ImGui_EndCombo(App.ctx) 90 | end 91 | end 92 | 93 | function UI.Render_BTN_Settings() 94 | Util.HorSpacer(3) 95 | if reaper.ImGui_Button(App.ctx, "Settings...##btn_settings") then 96 | reaper.ImGui_OpenPopup(App.ctx, "Settings##win_settings") 97 | end 98 | 99 | local pwin_x, pwin_y = reaper.ImGui_GetWindowPos(App.ctx) 100 | local pwin_w, pwin_h = reaper.ImGui_GetWindowSize(App.ctx) 101 | reaper.ImGui_SetNextWindowPos(App.ctx, pwin_x + (pwin_w / 2) - 180, pwin_y + (pwin_h / 2) - 100) 102 | 103 | if reaper.ImGui_BeginPopupModal(App.ctx, "Settings##win_settings", true, reaper.ImGui_WindowFlags_AlwaysAutoResize()) then 104 | local _, set_audition_notes = reaper.ImGui_Checkbox(App.ctx, "Audition notes on entry/selection", App.audition_notes) 105 | App.audition_notes = set_audition_notes 106 | 107 | local _, set_swap_pitchfret_order = reaper.ImGui_Checkbox(App.ctx, "Swap order of Pitch&Fret note display", App.swap_pitchfret_order) 108 | App.swap_pitchfret_order = set_swap_pitchfret_order 109 | 110 | local _, set_default_velocity = reaper.ImGui_SliderInt(App.ctx, "Default note velocity", App.default_velocity, 0, 127) 111 | App.default_velocity = set_default_velocity 112 | 113 | local _, set_default_off_velocity = reaper.ImGui_SliderInt(App.ctx, "Default note off-velocity", App.default_off_velocity, 0, 127) 114 | App.default_off_velocity = set_default_off_velocity 115 | reaper.ImGui_EndPopup(App.ctx) 116 | end 117 | end 118 | 119 | function UI.Render_TXT_Help() 120 | Util.HorSpacer(3) 121 | reaper.ImGui_TextDisabled(App.ctx, "(?)") 122 | 123 | if reaper.ImGui_IsItemHovered(App.ctx) then 124 | reaper.ImGui_BeginTooltip(App.ctx) 125 | 126 | reaper.ImGui_Text(App.ctx, 127 | "Shortcuts:\n" .. 128 | "Select tool (S)\n" .. 129 | "Move tool (W)\n" .. 130 | "Draw tool (D)\n" .. 131 | "Erase tool (E)\n" .. 132 | "Undo (Ctrl + Z)\n" .. 133 | "Redo (Ctrl + Shift + Z)\n" .. 134 | "Cut (Ctrl + X)\n" .. 135 | "Copy (Ctrl + C)\n" .. 136 | "Paste (Ctrl + V)\n\n" .. 137 | "Usage:\n" .. 138 | "Left Click with the Draw tool to insert a note.\n" .. 139 | "Left Click + Drag horizontally to set duration \n" .. 140 | "Left Click + Drag vertically to set pitch\n" .. 141 | "Right Click + Drag vertically to set velocity\n" .. 142 | "Right Click + Shift + Drag to set off-velocity\n" .. 143 | "Left Click + Ctrl to select multiple notes\n" .. 144 | "The same actions can be performed with the Select tool, when clicking on existing notes.\n" .. 145 | "Click + Drag in an empty area with the Select tool to marquee-select notes.\n\n" .. 146 | "Click on the Create MIDI button to generate a MIDI item on the selected track, at cursor position (WIP)") 147 | 148 | reaper.ImGui_EndTooltip(App.ctx) 149 | end 150 | end 151 | 152 | function UI.Render_Toolbar() 153 | if reaper.ImGui_BeginChild(App.ctx, "Toolbar##win_toolbar", App.window_w, 20, false, reaper.ImGui_WindowFlags_NoScrollbar()) then 154 | reaper.ImGui_PushStyleColor(App.ctx, reaper.ImGui_Col_Button(), Colors.bg) 155 | for i = 1, 10 do 156 | reaper.ImGui_PushFont(App.ctx, App.icon_font) 157 | 158 | if i == App.active_tool then 159 | reaper.ImGui_PushStyleColor(App.ctx, reaper.ImGui_Col_Text(), Colors.active_tool) 160 | else 161 | reaper.ImGui_PushStyleColor(App.ctx, reaper.ImGui_Col_Text(), Colors.text) 162 | end 163 | if reaper.ImGui_Button(App.ctx, ToolBar[i].icon .. "##toolbar_button" .. i) then 164 | if i >= e_Tool.Select and i <= e_Tool.Erase then -- only the note editing tools can be active 165 | App.active_tool = i 166 | elseif i == e_Tool.Create then 167 | Util.CreateMIDI() 168 | elseif i == e_Tool.Undo then 169 | UR.PopUndo() 170 | elseif i == e_Tool.Redo then 171 | UR.PopRedo() 172 | elseif i == e_Tool.Cut then 173 | Clipboard.Cut() 174 | elseif i == e_Tool.Copy then 175 | Clipboard.Copy() 176 | elseif i == e_Tool.Paste then 177 | if #Clipboard.note_list > 0 then App.attempts_paste = true; end 178 | end 179 | end 180 | reaper.ImGui_PopStyleColor(App.ctx) 181 | reaper.ImGui_PopFont(App.ctx) 182 | 183 | if reaper.ImGui_IsItemHovered(App.ctx) then 184 | reaper.ImGui_BeginTooltip(App.ctx) 185 | reaper.ImGui_Text(App.ctx, ToolBar[i].tooltip) 186 | reaper.ImGui_EndTooltip(App.ctx) 187 | end 188 | 189 | reaper.ImGui_SameLine(App.ctx) 190 | if i == e_Tool.Create or i == e_Tool.Erase then 191 | Util.HorSpacer(3) 192 | end 193 | end 194 | reaper.ImGui_PopStyleColor(App.ctx) 195 | reaper.ImGui_EndChild(App.ctx) 196 | end 197 | end 198 | 199 | function UI.Render_Editor() 200 | App.editor_h = 50 + ((App.num_strings) * 12) 201 | local num_grid_divisions = Util.NumGridDivisions() 202 | local lane_w = num_grid_divisions * App.grid_w 203 | reaper.ImGui_SetNextWindowContentSize(App.ctx, lane_w + 45, App.editor_h - 20) 204 | if reaper.ImGui_BeginChild(App.ctx, "Editor##win_editor", App.window_w - App.window_indent, App.editor_h, true, reaper.ImGui_WindowFlags_HorizontalScrollbar() | reaper.ImGui_WindowFlags_NoMove()) then 205 | 206 | local draw_list = reaper.ImGui_GetWindowDrawList(App.ctx) 207 | App.scroll_x = reaper.ImGui_GetScrollX(App.ctx) 208 | App.editor_win_x, App.editor_win_y = reaper.ImGui_GetWindowPos(App.ctx) 209 | 210 | -- Scroll horizontally with mousewheel without holding SHIFT 211 | -- Works good on my desktop, but has issues on my laptop's trackpad 212 | -- Comment this block to scroll with SHIFT 213 | local mw = reaper.ImGui_GetMouseWheel(App.ctx) 214 | App.scroll_x = App.scroll_x - mw * App.wheel_delta 215 | reaper.ImGui_SetScrollX(App.ctx, App.scroll_x) 216 | App.scroll_x = reaper.ImGui_GetScrollX(App.ctx) -- get back clamped value from ImGui 217 | 218 | local lane_start_x = App.editor_win_x + App.left_margin - App.scroll_x 219 | local lane_end_x = lane_start_x + lane_w 220 | 221 | -- Lanes 222 | for i = 0, App.num_strings - 1 do 223 | reaper.ImGui_DrawList_AddLine(draw_list, lane_start_x, App.editor_win_y + App.top_margin + (i * App.lane_v_spacing), lane_end_x, App.editor_win_y + App.top_margin + (i * App.lane_v_spacing), Colors.lane) 224 | end 225 | 226 | -- Measures & beats lines and legends 227 | local measure_count = 1 228 | local beat_count = 1 229 | 230 | for i = 0, num_grid_divisions do 231 | if i % App.signature[App.signature_cur_idx].subs == 0 then 232 | 233 | if (i ~= 0 and i % (App.signature[App.signature_cur_idx].beats * App.signature[App.signature_cur_idx].subs) == 0) then 234 | reaper.ImGui_DrawList_AddLine(draw_list, App.editor_win_x + App.left_margin + (App.grid_w * i) - App.scroll_x, App.editor_win_y + App.top_margin, App.editor_win_x + App.left_margin + (App.grid_w * i) - App.scroll_x, App.editor_win_y + App.top_margin + ((App.num_strings - 1) * 12), Colors.lane) 235 | measure_count = measure_count + 1 236 | beat_count = 1 237 | end 238 | 239 | if i ~= num_grid_divisions then 240 | local txt = measure_count .. "-" .. beat_count 241 | reaper.ImGui_DrawList_AddTextEx(draw_list, nil, 11, App.editor_win_x + App.left_margin + (App.grid_w * i) - App.scroll_x, App.editor_win_y + App.top_margin - 20, Colors.text, txt) 242 | beat_count = beat_count + 1 243 | end 244 | 245 | else 246 | reaper.ImGui_DrawList_AddLine(draw_list, App.editor_win_x + App.left_margin + (App.grid_w * i) - App.scroll_x, App.editor_win_y + App.top_margin - 17, App.editor_win_x + App.left_margin + (App.grid_w * i) - App.scroll_x, App.editor_win_y + App.top_margin - 12, Colors.lane) 247 | end 248 | end 249 | 250 | -- Notes 251 | UI.Render_Notes(draw_list) 252 | 253 | -- Capture the boundaries of editor area and display note preview 254 | local rect_x1 = App.editor_win_x + App.left_margin 255 | local rect_y1 = App.editor_win_y + App.top_margin - 5 256 | local rect_x2 = lane_end_x - 1 257 | local rect_y2 = rect_y1 + 11 + (App.num_strings - 1) * App.lane_v_spacing 258 | 259 | -- debug draw editor mouse area 260 | -- reaper.ImGui_DrawList_AddRect(draw_list, rect_x1, rect_y1, rect_x2, rect_y2, Colors.red) 261 | 262 | if reaper.ImGui_IsWindowHovered(App.ctx) then 263 | local cell_x = Util.GetCellX() 264 | local cell_y = Util.GetCellY() 265 | 266 | if App.mouse_x > rect_x1 and App.mouse_x < rect_x2 and App.mouse_y > rect_y1 and App.mouse_y < rect_y2 then 267 | if Util.IsCellEmpty(cell_x, cell_y, true) then 268 | if App.attempts_paste then 269 | -- reaper.ImGui_DrawList_AddLine(draw_list, preview_x, App.editor_win_y + App.top_margin, preview_x, App.editor_win_y + App.top_margin + ((App.num_strings - 1) * App.lane_v_spacing), Colors.red) 270 | local leftmost = Clipboard.note_list[1].offset 271 | 272 | for i, v in ipairs(Clipboard.note_list) do 273 | local cur_x = App.editor_win_x + App.left_margin + ((v.offset + cell_x - leftmost) * App.note_w) - App.scroll_x 274 | local cur_y = App.editor_win_y + App.top_margin + (v.string_idx * App.note_h) - 5 275 | reaper.ImGui_DrawList_AddRectFilled(draw_list, cur_x, cur_y, cur_x + (v.duration * App.note_w) - 1, cur_y + App.note_h - 1, Colors.note_preview_paste, 40) 276 | end 277 | reaper.ImGui_BeginTooltip(App.ctx) 278 | reaper.ImGui_Text(App.ctx, "Select position to paste. [ESC] to cancel") 279 | reaper.ImGui_EndTooltip(App.ctx) 280 | else 281 | if not (App.begin_marquee) then 282 | local preview_x = App.editor_win_x + App.left_margin + (cell_x * App.note_w) - App.scroll_x 283 | local preview_y = App.editor_win_y + App.top_margin + (cell_y * App.note_h) - 5 284 | reaper.ImGui_DrawList_AddRectFilled(draw_list, preview_x, preview_y, preview_x + App.note_w - 1, preview_y + App.note_h - 1, Colors.note_preview, 40) 285 | end 286 | end 287 | end 288 | if reaper.ImGui_IsMouseClicked(App.ctx, 0) then 289 | Editor.OnMouseButtonClick(e_MouseButton.Left, cell_x, cell_y) 290 | end 291 | if reaper.ImGui_IsMouseClicked(App.ctx, 1) then 292 | Editor.OnMouseButtonClick(e_MouseButton.Right, cell_x, cell_y) 293 | end 294 | end 295 | if reaper.ImGui_IsMouseClicked(App.ctx, 0) then App.last_click_was_inside_editor = true; end -- NOTE putting this here so that marquee box can auto-scroll even when clicking outside the lanes 296 | end 297 | 298 | -- These have prob have to go out of the drawing function 299 | if reaper.ImGui_IsMouseReleased(App.ctx, 0) then 300 | Editor.OnMouseButtonRelease(e_MouseButton.Left) 301 | end 302 | 303 | if reaper.ImGui_IsMouseReleased(App.ctx, 1) then 304 | Editor.OnMouseButtonRelease(e_MouseButton.Right) 305 | end 306 | 307 | if reaper.ImGui_IsMouseDragging(App.ctx, 0) then 308 | Editor.OnMouseButtonDrag(e_MouseButton.Left) 309 | end 310 | 311 | if reaper.ImGui_IsMouseDragging(App.ctx, 1) then 312 | Editor.OnMouseButtonDrag(e_MouseButton.Right) 313 | end 314 | 315 | -- Mask rect 316 | reaper.ImGui_DrawList_AddRectFilled(draw_list, App.editor_win_x, App.editor_win_y + 2, App.editor_win_x + App.left_margin, App.editor_win_y + 140, Colors.bg) 317 | 318 | -- String legends 319 | for i = 0, App.num_strings - 1 do 320 | local str = App.instrument[App.num_strings - 3][App.num_strings - i] 321 | local len = string.len(str) 322 | local space 323 | if len == 2 then space = " " else space = " " end 324 | reaper.ImGui_DrawList_AddText(draw_list, App.editor_win_x + 8, App.editor_win_y + 23 + (i * App.lane_v_spacing), Colors.text, str .. space .. "*") 325 | end 326 | 327 | -- Marquee box 328 | if App.begin_marquee then 329 | App.marquee_box.x2 = App.mouse_x 330 | App.marquee_box.y2 = App.mouse_y 331 | 332 | reaper.ImGui_DrawList_AddRectFilled(draw_list, App.marquee_box.x1, App.marquee_box.y1, App.marquee_box.x2, App.marquee_box.y2, Colors.marquee_box) 333 | -- msg(App.marquee_box.x1 .. " , " .. App.marquee_box.y1 .. ", " .. App.marquee_box.x2 .. " , " .. App.marquee_box.y2) 334 | end 335 | 336 | reaper.ImGui_EndChild(App.ctx) 337 | end 338 | end --------------------------------------------------------------------------------