diff --git a/MIDI Editor/talagan_OneSmallStep.lua b/MIDI Editor/talagan_OneSmallStep.lua index 48885c9cc..12391ff69 100644 --- a/MIDI Editor/talagan_OneSmallStep.lua +++ b/MIDI Editor/talagan_OneSmallStep.lua @@ -1,6 +1,6 @@ --[[ @description One Small Step : Alternative Step Input -@version 0.9.9 +@version 0.9.10 @author Ben 'Talagan' Babut @license MIT @metapackage @@ -44,8 +44,10 @@ [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Increase note len.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Decrease note len.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Cleanup helper JSFXs.lua + [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove operation marker.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove playback marker.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Playback.lua + [nomain] talagan_OneSmallStep/actions/talagan_OneSmallStep Toggle Debugger.lua [nomain] talagan_OneSmallStep/classes/**/*.lua [nomain] talagan_OneSmallStep/images/*.lua [effect] talagan_OneSmallStep/One Small Step Helper.jsfx @@ -54,10 +56,12 @@ @screenshot https://stash.reaper.fm/48269/oss_094.png @changelog - - [Rework] Changed toolbar icon color - - [Bug Fix] [Repitch Mode] Patched MIDIUtils API : successive snapped notes would be borked by the automatic overlap correction option (thanks @smandrap) - - [Bug Fix] [Write Mode] CommitBack action would be blocked by sustain pedal blocker if called from action (thanks @hipox !) - - [Bug Fix] [Write Mode] Sustain Pedal blocking system when (stepping back + miss) was broken + - [Feature] Compress/Stretch Submode + - [Feature] Stuff Submode + - [Enhance] Force item bound snapping if item grid snap is on + - [Rework] PPQ Precise operations + - [Rework] New code architecture and file hierarchy, big code rework + - [Rework] Addind debugging support for Visual Studio Code (using mavriq lua sockets, thanks @mavriq) @about # Purpose @@ -87,9 +91,17 @@ --]] -VERSION = "0.9.9" +VERSION = "0.9.10" DOC_URL = "https://bentalagan.github.io/onesmallstep-doc/index.html?ver=" .. VERSION +PATH = debug.getinfo(1,"S").source:match[[^@?(.*[\/])[^\/]-$]] + +------------------------------- +-- Path and modules + +package.path = PATH .. "talagan_OneSmallStep/" .. "?.lua" .. ";" .. package.path +package.path = PATH .. "talagan_OneSmallStep/classes/" .. "?.lua" .. ";" .. package.path + -------------------------------- -- Tell the script to be terminated if relaunched. @@ -98,11 +110,6 @@ if reaper.set_action_options ~= nil then reaper.set_action_options(1); end -------------------------------- --- Path and modules - -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])[^\/]-$]] .."?.lua;".. package.path - ------------------------------- -- Check dependencies @@ -112,42 +119,30 @@ local function CheckReapack(func_name, api_name, search_string) Right-click the entry in the next window and choose to install.", api_name .. " not installed", 0 ) reaper.ReaPack_BrowsePackages( search_string ) - exit(66) + return false end + return true end -CheckReapack("JS_ReaScriptAPI_Version", "JS_ReaScriptAPI", "js_ReaScriptAPI") -CheckReapack("ImGui_CreateContext", "ReaImGUI", "ReaImGui:") -CheckReapack("CF_ShellExecute", "SWS", "SWS/S&M Extension") - ---[[ - --- Code for installing sockmonkey72's MIDI Utilis library - --- But ATM we prefer embedding it - --- - for stability reasons --- - to avoid depending on another repository +if not CheckReapack("JS_ReaScriptAPI_Version", "JS_ReaScriptAPI", "js_ReaScriptAPI") then return end +if not CheckReapack("ImGui_CreateContext", "ReaImGUI", "ReaImGui:") then return end +if not CheckReapack("CF_ShellExecute", "SWS", "SWS/S&M Extension") then return end -local sm72reponame = "sockmonkey72 Scripts" -local sm72repourl = "https://github.com/jeremybernstein/ReaScripts/raw/main/index.xml" - -local repook, _, _, _ = reaper.ReaPack_GetRepositoryInfo(sm72reponame) -if not repook then - reaper.ReaPack_AddSetRepository(sm72reponame, sm72repourl, true, 0) - reaper.ReaPack_ProcessQueue(false) -end +-------------------------------- +-- Inner requirements -package.path = (reaper.GetResourcePath() .. '/Scripts/' .. sm72reponame .. '/MIDI/' .. "?.lua") .. ";" .. package.path -if not pcall(require, "MIDIUtils") then - local answer = reaper.MB( "MIDI Utils API is required and you need to install it . Right-click the entry in the next window and choose to install.", "MIDI Utils API not installed", 0 ) - reaper.ReaPack_BrowsePackages( "MIDI Utils API" ) - return -end +local E = require "engine_lib" +local DBG = require "modules/debugger" -]]-- +local S = E.S +local D = E.D +local MK = E.MK +local TGT = E.TGT +local F = E.F +local ED = E.ED -local engine_lib = require "talagan_OneSmallStep/classes/engine_lib"; +-- Get the debugger setting at launch +local DEBUGGER_IS_ON = S.getSetting("UseDebugger") ------------------------------- -- ImGui Backward compatibility @@ -164,7 +159,7 @@ local images = {}; local function getImage(image_name) if (not images[image_name]) or (not reaper.ImGui_ValidatePtr(images[image_name], 'ImGui_Image*')) then - local bin = require("./talagan_OneSmallStep/images/" .. image_name) + local bin = require("images/" .. image_name) images[image_name] = reaper.ImGui_CreateImageFromMem(bin) -- Prevent the GC from freeing this image reaper.ImGui_Attach(ctx, images[image_name]) @@ -199,11 +194,11 @@ function TT(str) end end -function to_frac(num) +function ToFrac(num) local W = math.floor(num) local F = num - W - local pn, n, N = 0, 1 - local pd, d, D = 1, 0 + local pn, n, N = 0, 1, 0 + local pd, d, D = 1, 0, 0 local x, err, q, Q repeat x = x and 1 / (x - q) or F @@ -251,9 +246,9 @@ local KnownNoteLengthSignatures = { ['3/256'] = { icon = "note_1", triplet = false, modif_label = ". x 1/128" }, } -function QNToLabel(ctx, qn, swing) +local function QNToLabel(ctx, qn, swing) - local n,d,e = to_frac(qn); + local n,d,e = ToFrac(qn); -- Do a reverse conversion to fraction -- And then lookup for what we know @@ -275,7 +270,7 @@ end -- Indicator for the current project grid note len -function ProjectGridLabel(ctx) +local function ProjectGridLabel(ctx) local _, qn, swingmode, swing = reaper.GetSetProjectGrid(0, false) if swingmode ~= 1 then swing = 0 @@ -291,7 +286,7 @@ function ProjectGridLabel(ctx) end -- Indicator for the current MIDI item note len -function ItemGridLabel(ctx,take) +local function ItemGridLabel(ctx,take) if not take then return end @@ -427,7 +422,7 @@ function RecordBadge(track) reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx)); - if (recarmed == 1) and not (engine_lib.getInputMode() == engine_lib.InputMode.None) and playState == 0 then + if (recarmed == 1) and not (S.getInputMode() == D.InputMode.None) and playState == 0 then local alpha = math.sin(reaper.time_precise()*4); local r1 = 200+math.floor(55 * alpha); local r2 = 120+math.floor(55 * alpha); @@ -454,7 +449,7 @@ function RecordIssues(track) reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx)); if not (recarmed == 1) then reaper.ImGui_TextColored(ctx, 0x808080FF, '[Track not armed]'); - elseif engine_lib.getInputMode() == engine_lib.InputMode.None then + elseif S.getInputMode() == D.InputMode.None then reaper.ImGui_TextColored(ctx, 0x808080FF, '[Input Mode is OFF]'); elseif not (playState == 0) then reaper.ImGui_TextColored(ctx, 0x808080FF, '[Reaper not ready]'); @@ -485,16 +480,16 @@ function TrackInfo(track) SL(); reaper.ImGui_TextColored(ctx, 0xA0A0FFFF, track_name .. " /"); SL(); - reaper.ImGui_TextColored(ctx, 0xFFA0A0FF, "No Item"); + reaper.ImGui_TextColored(ctx, 0xFFA0A0FF, "No Item") SL(); - RecordIssues(track); + RecordIssues(track) end -- MINIBAR : Input Mode function InputModeMiniBar() - local mode = engine_lib.getInputMode(); - local modifkey = engine_lib.getSetting("StepBackModifierKey") - local mkinfo = engine_lib.ModifierKeyLookup[modifkey]; + local mode = S.getInputMode(); + local modifkey = S.getSetting("StepBackModifierKey") + local mkinfo = D.ModifierKeyLookup[modifkey]; local pedalmanual = "\z The sustain pedal and the commit action :\n\n\z @@ -506,8 +501,8 @@ function InputModeMiniBar() \32 - Erase back held notes if they match the cursor\n\z \32 - Step back if no notes are held"; - if ButtonGroupImageButton('input_mode_keyboard_press', mode == engine_lib.InputMode.KeyboardPress) then - engine_lib.setInputMode(engine_lib.InputMode.KeyboardPress); + if ButtonGroupImageButton('input_mode_keyboard_press', mode == D.InputMode.KeyboardPress) then + S.setInputMode(D.InputMode.KeyboardPress); end TT("Input Mode : Keyboard Press (Fast mode)\n\z @@ -520,8 +515,8 @@ function InputModeMiniBar() \n\z" .. pedalmanual); SL(); - if ButtonGroupImageButton('input_mode_pedal', mode == engine_lib.InputMode.Punch) then - engine_lib.setInputMode(engine_lib.InputMode.Punch); + if ButtonGroupImageButton('input_mode_pedal', mode == D.InputMode.Punch) then + S.setInputMode(D.InputMode.Punch); end TT("Input Mode : Punch (Check mode)\n\z @@ -534,8 +529,8 @@ function InputModeMiniBar() \n\z" .. pedalmanual); SL(); - if ButtonGroupImageButton('input_mode_keyboard_release', mode == engine_lib.InputMode.KeyboardRelease) then - engine_lib.setInputMode(engine_lib.InputMode.KeyboardRelease) + if ButtonGroupImageButton('input_mode_keyboard_release', mode == D.InputMode.KeyboardRelease) then + S.setInputMode(D.InputMode.KeyboardRelease) end TT("Input Mode : Keyboard Release (Grope mode)\n\z @@ -553,36 +548,36 @@ end -- MINIBAR : Conf source function ConfSourceMiniBar() - local nlm = engine_lib.getNoteLenParamSource(); + local nlm = S.getNoteLenParamSource(); - if ButtonGroupImageButton('note_len_mode_oss', nlm == engine_lib.NoteLenParamSource.OSS) then - engine_lib.setNoteLenParamSource(engine_lib.NoteLenParamSource.OSS) + if ButtonGroupImageButton('note_len_mode_oss', nlm == D.NoteLenParamSource.OSS) then + S.setNoteLenParamSource(D.NoteLenParamSource.OSS) end TT('Note Length conf : One Small Step\n\nUse the params aside.'); SL(); - if ButtonGroupImageButton('note_len_mode_pgrid', nlm == engine_lib.NoteLenParamSource.ProjectGrid) then - engine_lib.setNoteLenParamSource(engine_lib.NoteLenParamSource.ProjectGrid) + if ButtonGroupImageButton('note_len_mode_pgrid', nlm == D.NoteLenParamSource.ProjectGrid) then + S.setNoteLenParamSource(D.NoteLenParamSource.ProjectGrid) end TT( "Note Length conf : Project\n\nUse the project's grid conf."); SL(); - if ButtonGroupImageButton('note_len_mode_inote', nlm == engine_lib.NoteLenParamSource.ItemConf) then - engine_lib.setNoteLenParamSource(engine_lib.NoteLenParamSource.ItemConf) + if ButtonGroupImageButton('note_len_mode_inote', nlm == D.NoteLenParamSource.ItemConf) then + S.setNoteLenParamSource(D.NoteLenParamSource.ItemConf) end TT( "Note Length conf : MIDI Item\n\nUse the MIDI item's own conf.\n\n('Notes' at the bottom of the MIDI editor)"); end -- MINIBAR : Note length function NoteLenMiniBar(with_fracs) - local nl = engine_lib.getNoteLen(); - for i,v in ipairs(engine_lib.NoteLenDefs) do + local nl = S.getNoteLen(); + for i,v in ipairs(D.NoteLenDefs) do if i > 1 then SL() end local icon = (with_fracs) and ('frac_' .. v.frac) or ('note_' .. v.id) if ButtonGroupImageButton(icon, nl == v.id, {corner = (with_fracs and 0 or 0.1)} ) then - engine_lib.setNoteLen(v.id) + S.setNoteLen(v.id) end end end @@ -590,43 +585,43 @@ end -- MINIBAR : Note length modifier function NoteLenModifierMiniBar(with_fracs) - local nmod = engine_lib.getNoteLenModifier(); + local nmod = S.getNoteLenModifier(); - if ButtonGroupImageButton(with_fracs and 'frac_3_2' or 'note_dotted', nmod == engine_lib.NoteLenModifier.Dotted, {corner = with_fracs and 0 or 0.1}) then - if nmod == engine_lib.NoteLenModifier.Dotted then - engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier.Straight); + if ButtonGroupImageButton(with_fracs and 'frac_3_2' or 'note_dotted', nmod == D.NoteLenModifier.Dotted, {corner = with_fracs and 0 or 0.1}) then + if nmod == D.NoteLenModifier.Dotted then + S.setNoteLenModifier(D.NoteLenModifier.Straight); else - engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier.Dotted); + S.setNoteLenModifier(D.NoteLenModifier.Dotted); end end TT(with_fracs and "3/2" or "Dotted"); SL(); - if ButtonGroupImageButton(with_fracs and 'frac_2_3' or 'note_triplet', nmod == engine_lib.NoteLenModifier.Triplet, {corner = with_fracs and 0 or 0.1}) then - if nmod == engine_lib.NoteLenModifier.Triplet then - engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier.Straight); + if ButtonGroupImageButton(with_fracs and 'frac_2_3' or 'note_triplet', nmod == D.NoteLenModifier.Triplet, {corner = with_fracs and 0 or 0.1}) then + if nmod == D.NoteLenModifier.Triplet then + S.setNoteLenModifier(D.NoteLenModifier.Straight); else - engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier.Triplet); + S.setNoteLenModifier(D.NoteLenModifier.Triplet); end end TT(with_fracs and "2/3" or "Triplet"); SL(); - if ButtonGroupImageButton(with_fracs and 'frac_1_n' or 'note_tuplet', nmod == engine_lib.NoteLenModifier.Tuplet, {corner = with_fracs and 0 or 0.1}) then - if nmod == engine_lib.NoteLenModifier.Tuplet then - engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier.Straight); + if ButtonGroupImageButton(with_fracs and 'frac_1_n' or 'note_tuplet', nmod == D.NoteLenModifier.Tuplet, {corner = with_fracs and 0 or 0.1}) then + if nmod == D.NoteLenModifier.Tuplet then + S.setNoteLenModifier(D.NoteLenModifier.Straight); else - engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier.Tuplet); + S.setNoteLenModifier(D.NoteLenModifier.Tuplet); end end TT(with_fracs and "1/n" or "N-tuplet"); SL() - if ButtonGroupImageButton('note_modified', nmod == engine_lib.NoteLenModifier.Modified ) then - if nmod == engine_lib.NoteLenModifier.Modified then - engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier.Straight); + if ButtonGroupImageButton('note_modified', nmod == D.NoteLenModifier.Modified ) then + if nmod == D.NoteLenModifier.Modified then + S.setNoteLenModifier(D.NoteLenModifier.Straight); else - engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier.Modified); + S.setNoteLenModifier(D.NoteLenModifier.Modified); end end TT(with_fracs and "n/m" or "Modified length"); @@ -640,14 +635,14 @@ function NTupletComboBox() reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx)); reaper.ImGui_PushID(ctx, "nlet_combo"); - local tuplet = ''..engine_lib.getTupletDivision(); + local tuplet = '' .. S.getTupletDivision(); reaper.ImGui_SetNextItemWidth(ctx,50); if reaper.ImGui_BeginCombo(ctx, '', tuplet) then for i,v in ipairs(combo_items) do local is_selected = (tuplet == v); if reaper.ImGui_Selectable(ctx, combo_items[i], is_selected) then - engine_lib.setTupletDivision(tonumber(v)); + S.setTupletDivision(tonumber(v)); end if is_selected then reaper.ImGui_SetItemDefaultFocus(ctx) @@ -663,7 +658,7 @@ end function NoteLenFactorComboBox(role) -- Numerator/Denominator local setting = "NoteLenFactor" .. role - local curval = engine_lib.getSetting(setting) + local curval = S.getSetting(setting) local combo_items = { } for i = 1, 32 do @@ -680,7 +675,7 @@ function NoteLenFactorComboBox(role) -- Numerator/Denominator local is_selected = (val == curval); if reaper.ImGui_Selectable(ctx, "" .. val, is_selected) then - engine_lib.setSetting(setting, val); + S.setSetting(setting, val); end if is_selected then reaper.ImGui_SetItemDefaultFocus(ctx) @@ -719,7 +714,7 @@ function PlayBackMeasureCountComboBox() reaper.ImGui_PushStyleColor(ctx, reaper.ImGui_Col_HeaderHovered(), 0x00C000FF); reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_FramePadding(), 5, 3.5); - local curm = engine_lib.getPlaybackMeasureCount(); + local curm = S.getPlaybackMeasureCount(); local label = function(mnum) return ((mnum == -1) and "Mk" or mnum); @@ -731,7 +726,7 @@ function PlayBackMeasureCountComboBox() local is_selected = (curm == i); if reaper.ImGui_Selectable(ctx, label(i), is_selected) then - engine_lib.setPlaybackMeasureCount(i); + S.setPlaybackMeasureCount(i); end if is_selected then reaper.ImGui_SetItemDefaultFocus(ctx) @@ -768,7 +763,7 @@ local function PlaybackSetMarkerButton() reaper.ImGui_PushID(ctx, "playback_marker"); reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_FramePadding(), 8, 4); if ButtonGroupImageButton("marker", false, { colorset = "Green" } ) then - engine_lib.setPlaybackMarkerAtCurrentPos(); + MK.setPlaybackMarkerAtCurrentPos(); end reaper.ImGui_PopStyleVar(ctx,1); @@ -794,9 +789,9 @@ local function MagnetMiniBar() for k,v in ipairs(snapElements) do reaper.ImGui_PushID(ctx, "snap_btn_" .. v.setting); reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_FramePadding(), 8, 4); - local navon = engine_lib.getSetting(v.setting); + local navon = S.getSetting(v.setting); if ButtonGroupImageButton("snap_btn_" .. v.image, navon, {colorset="Snap"}) then - engine_lib.setSetting(v.setting, not navon); + S.setSetting(v.setting, not navon); end TT("Navigation snap to " .. v.tt); reaper.ImGui_PopStyleVar(ctx,1); @@ -811,15 +806,15 @@ end function EditModeMiniBar() - local mode = engine_lib.getSetting("EditMode") - local amode = engine_lib.resolveOperationMode().mode + local mode = S.getSetting("EditMode") + local amode = ED.ResolveOperationMode().mode local modes = { - { name = engine_lib.EditMode.Write, tt = "Forward : Add notes\nBackward : Selective delete (remove notes if pressed)" }, - { name = engine_lib.EditMode.Insert, tt = "Forward : Add notes and shift later ones\nBackward : Delete or shorten notes and shift later ones back"}, - { name = engine_lib.EditMode.Replace, tt = "Forward : Delete (partially or fully) existing notes, and add new ones instead\nBackward : Delete (partially or fully) existing notes" }, - { name = engine_lib.EditMode.Repitch, tt = "Forward : Change the pitch of notes (number of pressed keys should match)\nBackward : Jump back to precedent note start" }, - { name = engine_lib.EditMode.Navigate, tt = "Forward : Navigate forward (using snap options)\nBackward : Navigate backward (using snap options)" }, + { name = D.EditMode.Write, tt = "Forward : Add notes\nBackward : Selective delete (remove notes if pressed)" }, + { name = D.EditMode.Insert, tt = "Forward : Add notes and shift later ones\nBackward : Delete or shorten notes and shift later ones back", alt = "Stretch/Compress"}, + { name = D.EditMode.Replace, tt = "Forward : Delete (partially or fully) existing notes, and add new ones instead\nBackward : Delete (partially or fully) existing notes", alt = "Stuff/Unstuff"}, + { name = D.EditMode.Repitch, tt = "Forward : Change the pitch of notes (number of pressed keys should match)\nBackward : Jump back to precedent note start" }, + { name = D.EditMode.Navigate, tt = "Forward : Navigate forward (using snap options)\nBackward : Navigate backward (using snap options)" }, } for k,v in pairs(modes) do @@ -841,9 +836,15 @@ function EditModeMiniBar() end if ButtonGroupImageButton(icon, ison, {colorset = colorset}) then - engine_lib.setSetting("EditMode", v.name) + S.setSetting("EditMode", v.name) + end + local tt = v.name .. " Mode\n\n" .. v.tt + + if v.alt then + tt = tt .. "\n\nClicking on the light indicator sets the operation marker\nand switches to " .. v.alt .. " mode" end - TT(v.name .. " Mode\n\n" .. v.tt) + + TT(tt) if k < #modes then SL() @@ -870,7 +871,7 @@ function SliderReset(setting) reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_ItemSpacing(), 2, 0); SL(); if reaper.ImGui_Button(ctx,"R##" .. setting) then - engine_lib.resetSetting(setting) + S.resetSetting(setting) end TT("Reset") reaper.ImGui_PopStyleVar(ctx); @@ -882,7 +883,7 @@ function SettingSlider(setting, in_label, out_label, tooltip, use_help_interroga reaper.ImGui_SetNextItemWidth(ctx, width) end - local spec = engine_lib.getSettingSpec(setting) + local spec = S.getSettingSpec(setting) local slider_func = nil if spec.type == 'int' then @@ -893,9 +894,9 @@ function SettingSlider(setting, in_label, out_label, tooltip, use_help_interroga error("Contact developer, forgot to handle type " .. spec.type) end - local change, v1 = slider_func(ctx, "##slider_" .. setting , engine_lib.getSetting(setting), spec.min, spec.max, in_label, reaper.ImGui_SliderFlags_NoInput()) + local change, v1 = slider_func(ctx, "##slider_" .. setting , S.getSetting(setting), spec.min, spec.max, in_label, reaper.ImGui_SliderFlags_NoInput()) if change then - engine_lib.setSetting(setting, v1); + S.setSetting(setting, v1); end if tooltip and not use_help_interrogation_for_tooltip then @@ -916,19 +917,45 @@ function SettingSlider(setting, in_label, out_label, tooltip, use_help_interroga end function TargetModeInfo() - local currentop = engine_lib.resolveOperationMode() + local currentop = ED.ResolveOperationMode() + + local _TT = function(msg, is_alt, alternative) + msg = msg .. "\n\n" .. "Click to set/move/remove operation marker\n\n" + if is_alt then + msg = msg .. "Switches back to " .. alternative .. " Mode" + else + msg = msg .. "Switches to " .. alternative .. " Mode" + end + return TT(msg) + end if currentop.mode == "Insert" then - if currentop.back then - reaper.ImGui_Image(ctx, getImage("indicator_insert_back"),20,20); TT("Insert back (delete and shift)"); SL(); + if currentop.use_alt then + if currentop.back then + reaper.ImGui_Image(ctx, getImage("indicator_compress"),20,20); _TT("Compress notes between marker and edit cursor", true, "Insert"); SL(); + else + reaper.ImGui_Image(ctx, getImage("indicator_stretch"),20,20); ; _TT("Stretch notes between marker and edit cursor", true, "Insert") SL(); + end else - reaper.ImGui_Image(ctx, getImage("indicator_insert_forward"),20,20); ; TT("Insert (add notes and shift)") SL(); + if currentop.back then + reaper.ImGui_Image(ctx, getImage("indicator_insert_back"),20,20); _TT("Insert back (delete and shift)", false, "Compress"); SL(); + else + reaper.ImGui_Image(ctx, getImage("indicator_insert_forward"),20,20); ; _TT("Insert (add notes and shift)", false, "Stretch") SL(); + end end elseif currentop.mode == "Replace" then - if currentop.back then - reaper.ImGui_Image(ctx, getImage("indicator_replace_back"),20,20); SL(); TT("Replace back (delete)") SL(); + if currentop.use_alt then + if currentop.back then + reaper.ImGui_Image(ctx, getImage("indicator_unstuff"),20,20); _TT("Stuff notes at the end of the zone between marker and edit cursor", true, "Replace"); SL(); + else + reaper.ImGui_Image(ctx, getImage("indicator_stuff"),20,20); _TT("Unstuff notes at the end of the zone between marker and edit cursor", true, "Replace"); SL(); + end else - reaper.ImGui_Image(ctx, getImage("indicator_replace_forward"),20,20); SL(); TT("Replace (add notes and remove/patch existing)") SL(); + if currentop.back then + reaper.ImGui_Image(ctx, getImage("indicator_replace_back"),20,20); SL(); _TT("Replace back (delete)", false, "Untuff"); SL(); + else + reaper.ImGui_Image(ctx, getImage("indicator_replace_forward"),20,20); SL(); _TT("Replace (add notes and remove/patch existing)", false, "Stuff"); SL(); + end end elseif currentop.mode == "Navigate" then if currentop.back then @@ -949,6 +976,11 @@ function TargetModeInfo() reaper.ImGui_Image(ctx, getImage("indicator_write_forward"),20,20); SL(); TT("Write (add notes)") SL(); end end + + if reaper.ImGui_IsItemClicked(ctx) then + MK.setOperationMarkerAtCurrentPos() + end + end @@ -965,8 +997,8 @@ function TargetLine(take) SL(); if not take then - if engine_lib.getSetting("AllowCreateItem") then - local track = engine_lib.TrackForEditionIfNoItemFound(); + if S.getSetting("AllowCreateItem") then + local track = TGT.TrackForEditionIfNoItemFound(); if track then TargetModeInfo() TrackInfo(track); @@ -976,10 +1008,13 @@ function TargetLine(take) else reaper.ImGui_TextColored(ctx, 0xA0A0A0FF, "No target item. Please select one."); end - ImGui_VerticalSpacer(ctx,0); else TargetModeInfo() - TakeInfo(take); + TakeInfo(take) + end + + if DEBUGGER_IS_ON then + SL(); reaper.ImGui_TextColored(ctx, 0xFF7070FF, "[DBG ON]") end end @@ -990,21 +1025,21 @@ function SettingsMiniBar() end end -function PlaybackMarkerSettingComboBox() +function MarkerPolicySettingComboBox(marker_name, policy_setting_name) local combo_items = { 'Hide/Restore', 'Keep visible', 'Remove' } - local curval = engine_lib.getSetting("PlaybackMarkerPolicyWhenClosed"); + local curval = S.getSetting(policy_setting_name) reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_FramePadding(), 5, 3.5); reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx)); - reaper.ImGui_PushID(ctx, "playback_marker_policy"); + reaper.ImGui_PushID(ctx, "playback_marker_policy_" .. policy_setting_name); reaper.ImGui_SetNextItemWidth(ctx, 120); if reaper.ImGui_BeginCombo(ctx, '', curval) then for i,v in ipairs(combo_items) do local is_selected = (curval == v); if reaper.ImGui_Selectable(ctx, combo_items[i], is_selected) then - engine_lib.setSetting("PlaybackMarkerPolicyWhenClosed", v); + S.setSetting(policy_setting_name, v); end if is_selected then reaper.ImGui_SetItemDefaultFocus(ctx) @@ -1017,16 +1052,15 @@ function PlaybackMarkerSettingComboBox() SL(); - reaper.ImGui_Text(ctx, "playback marker when closing"); + reaper.ImGui_Text(ctx, marker_name .. " when closing"); end - function AllowedModifierKeyCombinationsForEditMode() -- All combinations but remove ctrl pedal - local modkey = engine_lib.ModifierKeyLookup[engine_lib.getSetting("StepBackModifierKey")]; + local modkey = D.ModifierKeyLookup[S.getSetting("StepBackModifierKey")]; local filtered = {} - for k, v in ipairs(engine_lib.ModifierKeyCombinations) do + for k, v in ipairs(D.ModifierKeyCombinations) do if modkey.vkey ~= v.vkeys[1] and modkey.vkey ~= v.vkeys[2] then filtered[#filtered+1] = v end @@ -1036,17 +1070,17 @@ function AllowedModifierKeyCombinationsForEditMode() end function ClearConflictingModifierKeys() - local sbmk = engine_lib.getSetting("StepBackModifierKey") + local sbmk = S.getSetting("StepBackModifierKey") local editmodes = { "Navigate", "Replace", "Insert", "Repitch" } for k,v in ipairs(editmodes) do local setting = v.."ModifierKeyCombination" - local modk = engine_lib.getSetting(setting) - local combi = EngineLib.ModifierKeyCombinationLookup[modk]; + local modk = S.getSetting(setting) + local combi = D.ModifierKeyCombinationLookup[modk]; for _, vk in ipairs(combi.vkeys) do if sbmk == vk then - engine_lib.setSetting(setting, "none") + S.setSetting(setting, "none") end end end @@ -1055,8 +1089,8 @@ end function StepBackModifierKeyComboBox(callback) local setting = "StepBackModifierKey" - local modkey = engine_lib.ModifierKeyLookup[engine_lib.getSetting(setting)] or {}; - local combo_items = engine_lib.ModifierKeys; + local modkey = D.ModifierKeyLookup[S.getSetting(setting)] or {}; + local combo_items = D.ModifierKeys; local label = modkey.name; local curval = modkey.vkey; @@ -1074,7 +1108,7 @@ function StepBackModifierKeyComboBox(callback) end if reaper.ImGui_Selectable(ctx, v.name, is_selected) then - engine_lib.setSetting(setting, v.vkey); + S.setSetting(setting, v.vkey); ClearConflictingModifierKeys() end end @@ -1096,8 +1130,8 @@ function EditModeComboBox(editModeName, callback) local combo_items = AllowedModifierKeyCombinationsForEditMode() local setting = editModeName .. "ModifierKeyCombination" - local current_id = engine_lib.getSetting(setting) - local current_combi = engine_lib.ModifierKeyCombinationLookup[current_id] + local current_id = S.getSetting(setting) + local current_combi = D.ModifierKeyCombinationLookup[current_id] reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_FramePadding(), 5, 3.5); reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx)); @@ -1113,7 +1147,7 @@ function EditModeComboBox(editModeName, callback) end if reaper.ImGui_Selectable(ctx, v.label, is_selected) then - engine_lib.setSetting(setting, v.id); + S.setSetting(setting, v.id); -- TODO : Reset all collisions end end @@ -1133,8 +1167,8 @@ end function RepitchModeComboBox() local setting = "RepitchModeAffects" - local combo_items = engine_lib.getSettingSpec("RepitchModeAffects").inclusion - local curval = engine_lib.getSetting("RepitchModeAffects") + local combo_items = S.getSettingSpec("RepitchModeAffects").inclusion + local curval = S.getSetting("RepitchModeAffects") reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_FramePadding(), 5, 3.5) reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx) + 3) @@ -1149,7 +1183,7 @@ function RepitchModeComboBox() for i,v in ipairs(combo_items) do local is_selected = (curval == v); if reaper.ImGui_Selectable(ctx, combo_items[i], is_selected) then - engine_lib.setSetting(setting, v); + S.setSetting(setting, v); end if is_selected then reaper.ImGui_SetItemDefaultFocus(ctx) @@ -1159,9 +1193,6 @@ function RepitchModeComboBox() end reaper.ImGui_PopID(ctx); reaper.ImGui_PopStyleVar(ctx,1); --- SL(); --- reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx) - 3) --- reaper.ImGui_Text(ctx, ""); end function SettingsPanel() @@ -1178,29 +1209,29 @@ function SettingsPanel() local curval = nil; - curval = engine_lib.getSetting("AllowTargetingFocusedMidiEditors"); + curval = S.getSetting("AllowTargetingFocusedMidiEditors"); if reaper.ImGui_Checkbox(ctx, "Allow targeting items open in focused MIDI Editors", curval) then - engine_lib.setSetting("AllowTargetingFocusedMidiEditors", not curval); + S.setSetting("AllowTargetingFocusedMidiEditors", not curval); end - curval = engine_lib.getSetting("AllowTargetingNonSelectedItemsUnderCursor"); + curval = S.getSetting("AllowTargetingNonSelectedItemsUnderCursor"); if reaper.ImGui_Checkbox(ctx, "Allow targeting items on selected tracks if no item is selected", curval) then - engine_lib.setSetting("AllowTargetingNonSelectedItemsUnderCursor", not curval); + S.setSetting("AllowTargetingNonSelectedItemsUnderCursor", not curval); end - curval = engine_lib.getSetting("AllowCreateItem"); + curval = S.getSetting("AllowCreateItem"); if reaper.ImGui_Checkbox(ctx, "Allow creating new items if needed", curval) then - engine_lib.setSetting("AllowCreateItem", not curval); + S.setSetting("AllowCreateItem", not curval); end - curval = engine_lib.getSetting("SelectInputNotes"); + curval = S.getSetting("SelectInputNotes"); if reaper.ImGui_Checkbox(ctx, "Select input notes", curval) then - engine_lib.setSetting("SelectInputNotes", not curval); + S.setSetting("SelectInputNotes", not curval); end - curval = engine_lib.getSetting("CleanupJsfxAtClosing"); + curval = S.getSetting("CleanupJsfxAtClosing"); if reaper.ImGui_Checkbox(ctx, "Cleanup helper JSFXs when closing OSS", curval) then - engine_lib.setSetting("CleanupJsfxAtClosing", not curval); + S.setSetting("CleanupJsfxAtClosing", not curval); end reaper.ImGui_EndTabItem(ctx) @@ -1225,9 +1256,9 @@ function SettingsPanel() This setting allows to enter new notes overlapping sustained notes.", true, nil) SL() - curval = engine_lib.getSetting("KeyPressModeInertiaEnabled"); + local curval = S.getSetting("KeyPressModeInertiaEnabled"); if reaper.ImGui_Checkbox(ctx, "Enabled##kp_inertia", curval) then - engine_lib.setSetting("KeyPressModeInertiaEnabled", not curval); + S.setSetting("KeyPressModeInertiaEnabled", not curval); end ImGui_VerticalSpacer(ctx,5); @@ -1245,9 +1276,9 @@ function SettingsPanel() ImGui_VerticalSpacer(ctx,5); SEP("Sustain Pedal") - curval = engine_lib.getSetting("PedalRepeatEnabled"); + curval = S.getSetting("PedalRepeatEnabled"); if reaper.ImGui_Checkbox(ctx, "Pedal repeat every", curval) then - engine_lib.setSetting("PedalRepeatEnabled", not curval); + S.setSetting("PedalRepeatEnabled", not curval); end SL(); SettingSlider("PedalRepeatTime", "%.3f seconds", "and", "Repeat time for the pedal event when pressed", false, 120) @@ -1268,9 +1299,9 @@ function SettingsPanel() EditModeComboBox("Replace") EditModeComboBox("Repitch") - curval = engine_lib.getSetting("HideEditModeMiniBar"); + local curval = S.getSetting("HideEditModeMiniBar"); if reaper.ImGui_Checkbox(ctx, "Hide edit mode mini bar", curval) then - engine_lib.setSetting("HideEditModeMiniBar", not curval); + S.setSetting("HideEditModeMiniBar", not curval); end SL() reaper.ImGui_TextColored(ctx, 0xB0B0B0FF, "(?)"); @@ -1287,22 +1318,24 @@ function SettingsPanel() ImGui_VerticalSpacer(ctx,5); SEP("All edit modes") - curval = engine_lib.getSetting("AutoScrollArrangeView"); + local curval = S.getSetting("AutoScrollArrangeView"); if reaper.ImGui_Checkbox(ctx, "Auto-scroll arrange view after editing/navigating", curval) then - engine_lib.setSetting("AutoScrollArrangeView", not curval); + S.setSetting("AutoScrollArrangeView", not curval); end - curval = engine_lib.getSetting("AllowKeyEventNavigation"); + curval = S.getSetting("AllowKeyEventNavigation"); if reaper.ImGui_Checkbox(ctx, "Allow navigating on controller key press/release", curval) then - engine_lib.setSetting("AllowKeyEventNavigation", not curval); + S.setSetting("AllowKeyEventNavigation", not curval); end + MarkerPolicySettingComboBox("operation marker", "OperationMarkerPolicyWhenClosed") + ImGui_VerticalSpacer(ctx,5); SEP("Write mode") - curval = engine_lib.getSetting("DoNotRewindOnStepBackIfNothingErased"); + curval = S.getSetting("DoNotRewindOnStepBackIfNothingErased"); if reaper.ImGui_Checkbox(ctx, "Do not rewind on controller key press/release and nothing is erased", curval) then - engine_lib.setSetting("DoNotRewindOnStepBackIfNothingErased", not curval); + S.setSetting("DoNotRewindOnStepBackIfNothingErased", not curval); end ImGui_VerticalSpacer(ctx,5); @@ -1319,9 +1352,8 @@ function SettingsPanel() end if reaper.ImGui_BeginTabItem(ctx, 'Playback') then - - ImGui_VerticalSpacer(ctx,5); - PlaybackMarkerSettingComboBox(); + ImGui_VerticalSpacer(ctx,5) + MarkerPolicySettingComboBox("playback marker", "PlaybackMarkerPolicyWhenClosed") reaper.ImGui_EndTabItem(ctx) end @@ -1334,7 +1366,7 @@ function SettingsPanel() end function NoteLenOptions(grid_mode) - local nlmod = engine_lib.getNoteLenModifier(); + local nlmod = S.getNoteLenModifier() NoteLenMiniBar(grid_mode); SL() MiniBarSeparator(); SL() @@ -1345,19 +1377,23 @@ function NoteLenOptions(grid_mode) NoteLenModifierMiniBar(grid_mode); - if nlmod == engine_lib.NoteLenModifier.Tuplet then + if nlmod == D.NoteLenModifier.Tuplet then SL(); MiniBarSeparator(); SL() NTupletComboBox() - elseif nlmod == engine_lib.NoteLenModifier.Modified then + elseif nlmod == D.NoteLenModifier.Modified then SL(); MiniBarSeparator(); SL() AugmentedDiminishedMiniBars(not grid_mode) end end +local function Stop() + reaper.ImGui_DestroyContext(ctx); +end -function ui_loop() +local MainLoop +local function UiLoop() - engine_lib.TrackFocus(); + F.TrackFocus(); reaper.ImGui_PushStyleVar(ctx,reaper.ImGui_StyleVar_WindowPadding(),10,10); @@ -1367,7 +1403,7 @@ function ui_loop() reaper.ImGui_WindowFlags_TopMost(); -- Since we use a trick to give back the focus to reaper, we don't want the window to glitch. - reaper.ImGui_PushStyleColor(ctx, reaper.ImGui_Col_TitleBgActive(), 0x0A0A0AFF); + reaper.ImGui_PushStyleColor(ctx, reaper.ImGui_Col_TitleBgActive(), (DEBUGGER_IS_ON and 0xFF0000FF or 0x0A0A0AFF)) local visible, open = reaper.ImGui_Begin(ctx, 'One Small Step v' .. VERSION, true, flags); reaper.ImGui_PopStyleColor(ctx,1); @@ -1375,7 +1411,7 @@ function ui_loop() reaper.ImGui_SetConfigVar(ctx,reaper.ImGui_ConfigVar_HoverDelayNormal(), 1.0); -- Target display line - local take = engine_lib.TakeForEdition(); + local take = TGT.TakeForEdition(); TargetLine(take); @@ -1386,9 +1422,10 @@ function ui_loop() reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_ItemSpacing(), 2, 4); reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_ItemInnerSpacing(), 0, 0); - local nlm = engine_lib.getNoteLenParamSource(); - local amode = engine_lib.getSetting("EditMode") - local emode = engine_lib.resolveOperationMode().mode; + local nlm = S.getNoteLenParamSource(); + local amode = S.getSetting("EditMode") + local opmode = ED.ResolveOperationMode() + local emode = opmode.mode; SettingsMiniBar(); SL(); MiniBarSeparator(); SL(); @@ -1396,7 +1433,7 @@ function ui_loop() InputModeMiniBar(); SL(); MiniBarSeparator(); SL(); - if not engine_lib.getSetting("HideEditModeMiniBar") then + if not S.getSetting("HideEditModeMiniBar") then EditModeMiniBar(); SL(); MiniBarSeparator(); SL(); end @@ -1407,13 +1444,13 @@ function ui_loop() ConfSourceMiniBar(); SL(); MiniBarSeparator(); SL(); - if nlm == engine_lib.NoteLenParamSource.OSS then - NoteLenOptions(false) - elseif nlm == engine_lib.NoteLenParamSource.ProjectGrid then + if nlm == D.NoteLenParamSource.OSS then + NoteLenOptions(emode == "Replace" and opmode.use_alt) + elseif nlm == D.NoteLenParamSource.ProjectGrid then ProjectGridLabel(ctx); SL() XSeparator(); SL(); NoteLenOptions(true) - elseif nlm == engine_lib.NoteLenParamSource.ItemConf then + elseif nlm == D.NoteLenParamSource.ItemConf then ItemGridLabel(ctx,take); SL() XSeparator(); SL(); NoteLenOptions(true) @@ -1433,7 +1470,7 @@ function ui_loop() end if (reaper.time_precise() - focustimer > 0.5) then - engine_lib.RestoreFocus(); + F.RestoreFocus(); end else focustimer = nil; @@ -1446,45 +1483,47 @@ function ui_loop() reaper.ImGui_PopStyleVar(ctx); if open then - reaper.defer(main_loop) + reaper.defer(MainLoop) else - stop(); + Stop() end end -function updateToolbarButtonState(v) +local function UpdateToolbarButtonState(v) local _,_,sectionID,cmdID,_,_,_ = reaper.get_action_context(); reaper.SetToggleCommandState(sectionID,cmdID,v); reaper.RefreshToolbar2(sectionID, cmdID); end -function main_loop() - - local engine_ret = engine_lib.atLoop(); +function MainLoop() + local engine_ret = E.atLoop(); if engine_ret == -42 then reaper.ShowMessageBox("Could not install One Small Step's helper FX on the track.\n\nIf you've just installed One Small Step, please try to restart REAPER to let it refresh its JFSX repository.", "Oops !", 0); return; end - ui_loop(); + UiLoop(); end -function onReaperExit() - updateToolbarButtonState(0); - engine_lib.atExit(); +local function onReaperExit() + UpdateToolbarButtonState(0); + E.atExit(); end -function stop() - reaper.ImGui_DestroyContext(ctx); -end - -function start() +local function _start() focustimer = 0; -- Will force a focus restore - updateToolbarButtonState(1); - engine_lib.atStart(); + UpdateToolbarButtonState(1); + E.atStart(); reaper.atexit(onReaperExit); - reaper.defer(main_loop); + reaper.defer(MainLoop); end -start(); +local function start() + -- Defer everything so that we can benefit of the debugger + reaper.defer(_start) +end + +DBG.LaunchDebugStubIfNeeded() + +start() diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua index 7fb8881cb..143f88e6c 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua @@ -3,9 +3,8 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path; +local S = require "modules/settings"; local param = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$"); - -engine_lib.setSetting("EditMode", param) +S.setSetting("EditMode", param) diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode.lua index bac1a17dd..6171832a8 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode.lua @@ -3,11 +3,11 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; -local param = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$"); - -local mode = engine_lib.InputMode[param]; -if mode then - engine_lib.setInputMode(mode) -end +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path + +local S = require "modules/settings" +local D = require "modules/defines" + +local param = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$") + +S.setInputMode(D.InputMode[param]) diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len modifier.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len modifier.lua index a9cb80f76..012f992b6 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len modifier.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len modifier.lua @@ -3,8 +3,11 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; -local modifier = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$"); +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path -engine_lib.setNoteLenModifier(engine_lib.NoteLenModifier[modifier]) \ No newline at end of file +local S = require "modules/settings" +local D = require "modules/defines" + +local param = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$") + +S.setNoteLenModifier(D.NoteLenModifier[param]) \ No newline at end of file diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source.lua index 8c58b9e37..c824a55b1 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source.lua @@ -3,14 +3,11 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; -local mode = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$"); +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path -if mode == 'OSS' then - engine_lib.setNoteLenParamSource(engine_lib.NoteLenParamSource.OSS); -elseif mode == 'ItemConf' then - engine_lib.setNoteLenParamSource(engine_lib.NoteLenParamSource.ItemConf); -elseif mode == 'ProjectGrid' then - engine_lib.setNoteLenParamSource(engine_lib.NoteLenParamSource.ProjectGrid); -end +local S = require "modules/settings" +local D = require "modules/defines" + +local param = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$") + +S.setNoteLenParamSource(D.NoteLenParamSource[param]) \ No newline at end of file diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len.lua index 0bfc370e0..d8d1060bd 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len.lua @@ -3,8 +3,8 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; -local param = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$"); +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path +local S = require "modules/settings" +local param = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$") -engine_lib.setNoteLen(param); +S.setNoteLen(param) diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Cleanup helper JSFXs.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Cleanup helper JSFXs.lua index 27b83c9a9..3b7e26d8d 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Cleanup helper JSFXs.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Cleanup helper JSFXs.lua @@ -3,8 +3,9 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local helper_lib = require "classes/helper_lib"; +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path + +local helper_lib = require "helper_lib"; reaper.Undo_BeginBlock(); helper_lib.cleanupAllTrackFXs(); diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Decrease note len.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Decrease note len.lua index f980b9e8e..305cd0d70 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Decrease note len.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Decrease note len.lua @@ -3,7 +3,8 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path -engine_lib.decreaseNoteLen(); +local S = require "modules/settings" + +S.decreaseNoteLen() diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua index 1e84ab572..41cbd23f4 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua @@ -3,12 +3,13 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path + +local E = require "engine_lib"; local param = select(2, reaper.get_action_context()):match("%- ([^%s]*)%.lua$"); if not param or param == "" then param = "Commit" end -engine_lib.reaperAction(param) +E.reaperAction(param) diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Increase note len.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Increase note len.lua index 59c4a0296..86a06315f 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Increase note len.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Increase note len.lua @@ -3,7 +3,9 @@ -- @license MIT -- @description This is part of One Small Step -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path + +local S = require "modules/settings" + +S.increaseNoteLen() -engine_lib.increaseNoteLen(); diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Playback.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Playback.lua index f266569a5..c71734dc6 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Playback.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Playback.lua @@ -3,8 +3,10 @@ -- @license MIT -- @description This is part of One Small Step. Will replay the n last measures. -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path + +local S = require "modules/settings" +local MK = require "modules/markers" -- Give the possibility to this script to be duplicated and called -- With a param at the end of the lua file name (it overrides OSS config) @@ -19,20 +21,21 @@ if not (reaper.GetPlayState() == 0) then return; end -local rewindMeasureCount = ((param == nil) and engine_lib.getPlaybackMeasureCount() or tonumber(param)); +local rewindMeasureCount = ((param == nil) and S.getPlaybackMeasureCount() or tonumber(param)); -local pos = reaper.GetCursorPosition(); -local posqn = reaper.TimeMap2_timeToQN(0, pos); -local posm = reaper.TimeMap_QNToMeasures(0, posqn); +local pos = reaper.GetCursorPosition() +local posqn = reaper.TimeMap2_timeToQN(0, pos) +local posm = reaper.TimeMap_QNToMeasures(0, posqn) -local timeStart = 0; +local timeStart = 0 if rewindMeasureCount == -1 then - local mkid, mkpos = engine_lib.findPlaybackMarker(); + local mkid, mkpos = MK.findPlaybackMarker() + if mkid == nil then rewindMeasureCount = 0 else - timeStart = mkpos; + timeStart = mkpos end end @@ -50,8 +53,6 @@ if rewindMeasureCount >= 0 then timeStart = reaper.TimeMap2_QNToTime(0, measureStart); end - - -- In OSS manual, I encourage users to a tick the option that -- creates an undo point whenever the Edit cursor is moved -- This ensures that OSS undo works well (notes are cancelled and the edit cursor moves back to its previous position) @@ -62,7 +63,8 @@ end local SPBA = "OneSmallStep - Start Playback"; local EPBA = "OneSmallStep - End Playback"; -function startPlayback() +local waitEndOfPlayback +local function startPlayback() -- Move the cursor back and hit play reaper.Undo_BeginBlock(); reaper.SetEditCurPos(timeStart, true, true); @@ -71,7 +73,7 @@ function startPlayback() reaper.defer(waitEndOfPlayback); end -function onPlaybackEnd() +local function onPlaybackEnd() reaper.Undo_BeginBlock(); reaper.SetEditCurPos(pos, false, false); reaper.Undo_EndBlock(EPBA,0); @@ -86,7 +88,6 @@ function onPlaybackEnd() end function waitEndOfPlayback() - local ps = reaper.GetPlayState(); local curtime = reaper.GetPlayPosition(); local antiglitch = 0.1; @@ -96,10 +97,9 @@ function waitEndOfPlayback() else return; end - end -function stopPlayback() +local function stopPlayback() reaper.OnStopButton(); onPlaybackEnd(); end diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove operation marker.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove operation marker.lua new file mode 100644 index 000000000..456a82c37 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove operation marker.lua @@ -0,0 +1,9 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step. Will replay the n last measures. + +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path +local MK = require "modules/markers" + +MK.setOperationMarkerAtCurrentPos() diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove playback marker.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove playback marker.lua index 56f750b04..49588d733 100644 --- a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove playback marker.lua +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Set or remove playback marker.lua @@ -3,7 +3,7 @@ -- @license MIT -- @description This is part of One Small Step. Will replay the n last measures. -package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .."?.lua;".. package.path; -local engine_lib = require "classes/engine_lib"; +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path +local MK = require "modules/markers" -engine_lib.setPlaybackMarkerAtCurrentPos(); +MK.setPlaybackMarkerAtCurrentPos() diff --git a/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Toggle Debugger.lua b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Toggle Debugger.lua new file mode 100644 index 000000000..c5cc564e3 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Toggle Debugger.lua @@ -0,0 +1,13 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description Enables/Disables debugging with mobdebug + +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])actions[\/][^\/]-$]] .. "classes/" .. "?.lua;".. package.path + +local S = require "modules/settings" + +local don = S.getSetting("UseDebugger") +don = not don + +S.setSetting("UseDebugger", don) diff --git a/MIDI Editor/talagan_OneSmallStep/classes/engine_lib.lua b/MIDI Editor/talagan_OneSmallStep/classes/engine_lib.lua index 38786c7b7..c05eab200 100644 --- a/MIDI Editor/talagan_OneSmallStep/classes/engine_lib.lua +++ b/MIDI Editor/talagan_OneSmallStep/classes/engine_lib.lua @@ -8,1394 +8,87 @@ local upperDir = scriptDir:match( "((.*)[\\/](.+)[\\/])(.+)$" ); package.path = scriptDir .."?.lua;".. package.path --- Using sockmonkey72's library -local midi_utils = require 'lib/MIDIUtils' - -local helper_lib = require "helper_lib"; -local KeyActivityManager = require "KeyActivityManager"; -local KeyReleaseActivityManager = require "KeyReleaseActivityManager"; -local KeyPressActivityManager = require "KeyPressActivityManager"; - -local launchTime = reaper.time_precise(); -local IsMacos = (reaper.GetOS():find('OSX') ~= nil); - -------------- --- Defines - --- Tolerance to detect if events match -local TIME_TOLERANCE = 0.002 -local QN_TOLERANCE = 0.002 -local PPQ_TOLERANCE = 1 - -local NoteLenDefs = { - { id = "1", next = "1", prec = "1_2", frac = "4" , qn = 4 }, - { id = "1_2", next = "1", prec = "1_4", frac = "2" , qn = 2 }, - { id = "1_4", next = "1_2", prec = "1_8", frac = "1" , qn = 1 }, - { id = "1_8", next = "1_4", prec = "1_16", frac = "1_2" , qn = 0.5 }, - { id = "1_16", next = "1_8", prec = "1_32", frac = "1_4" , qn = 0.25 }, - { id = "1_32", next = "1_16", prec = "1_64", frac = "1_8" , qn = 0.125 }, - { id = "1_64", next = "1_32", prec = "1_64", frac = "1_16", qn = 0.0625 } -}; - -local NoteLenParamSource = { - OSS = 0, - ProjectGrid = 1, - ItemConf = 2 -} - -local InputMode = { - None = 0, - Punch = 1, - KeyboardRelease = 2, - Action = 3, -- Removed, merged with pedal - KeyboardPress = 4 -} - -local NoteLenModifier = { - Straight = 0, - Dotted = 1, - Triplet = 2, - Tuplet = 3, - Modified = 4 -} - -local EditMode = { - Write = "Write", - Navigate = "Navigate", - Insert = "Insert", - Repitch = "Repitch", - Replace = "Replace" -} - -local ActionTriggers = { - Commit = { action = "Commit", back = false }, - CommitBack = { action = "Commit", back = true }, - - Write = { action = "Write", back = false }, - Navigate = { action = "Navigate", back = false }, - Insert = { action = "Insert", back = false }, - Replace = { action = "Replace", back = false }, - Repitch = { action = "Repitch", back = false }, - - WriteBack = { action = "Write", back = true }, - NavigateBack = { action = "Navigate", back = true }, - InsertBack = { action = "Insert", back = true }, - ReplaceBack = { action = "Replace", back = true }, - RepitchBack = { action = "Repitch", back = true } -} - -local MacOSModifierKeys = { - { vkey = 16, name = 'Shift' }, - { vkey = 17, name = 'Cmd' }, - { vkey = 18, name = 'Opt' }, - { vkey = 91, name = 'Ctrl' } -}; - -local OtherOSModifierKeys = { - { vkey = 16, name = 'Shift' }, - { vkey = 17, name = 'Ctrl' }, - { vkey = 18, name = 'Alt' } -}; - -local ModifierKeys = IsMacos and MacOSModifierKeys or OtherOSModifierKeys; - -local NoteLenLookup = {}; -for i,v in ipairs(NoteLenDefs) do - NoteLenLookup[v.id] = v; -end - -local ModifierKeyLookup = {}; -for i,v in ipairs(ModifierKeys) do - ModifierKeyLookup[v.vkey] = v; -end - -local ModifierKeyCombinations = {{ label = "None", id = "none", vkeys = {} }} -for i=1, #ModifierKeys do - local m1 = ModifierKeys[i] - ModifierKeyCombinations[#ModifierKeyCombinations+1] = { label = m1.name, id = "" .. m1.vkey, vkeys = { m1.vkey } } -end -for i=1, #ModifierKeys do - local m1 = ModifierKeys[i] - for j=i+1, #ModifierKeys do - local m2 = ModifierKeys[j] - ModifierKeyCombinations[#ModifierKeyCombinations+1] = { label = m1.name .. "+" .. m2.name, id = "" .. m1.vkey .. "+" .. m2.vkey, vkeys = { m1.vkey, m2.vkey } } - end -end -local ModifierKeyCombinationLookup = {}; -for i,v in ipairs(ModifierKeyCombinations) do - ModifierKeyCombinationLookup[v.id] = v; -end - -------------- --- Settings - -local SettingDefs = { - StepBackModifierKey = { type = "int", default = IsMacOs and 16 or 16 }, - - WriteModifierKeyCombination = { type = "string", default = "none" }, - InsertModifierKeyCombination = { type = "string", default = IsMacos and "17" or "17" }, - NavigateModifierKeyCombination = { type = "string", default = IsMacos and "18" or "18" }, - ReplaceModifierKeyCombination = { type = "string", default = IsMacos and "17+18" or "17+18" }, - RepitchModifierKeyCombination = { type = "string", default = "none" }, - - HideEditModeMiniBar = { type = "bool", default = false }, - - Mode = { type = "int", default = InputMode.KeyboardRelease }, - EditMode = { type = "string", default = EditMode.Write }, - - PlaybackMeasureCount = { type = "int", default = -1 }, -- -1 is marker mode - NoteLenParamSource = { type = "int", default = NoteLenParamSource.OSS }, - NoteLenFactorDenominator = { type = "int", default = 1 }, - NoteLenFactorNumerator = { type = "int", default = 1 }, - TupletDivision = { type = "int", default = 4 }, - NoteLen = { type = "string", default = "1_4"}, - NoteLenModifier = { type = "int", default = NoteLenModifier.Straight }, - ------ - PlaybackMarkerPolicyWhenClosed = { type = "string", default = "Keep visible" }, - - AllowTargetingFocusedMidiEditors = { type = "bool", default = true }, - AllowTargetingNonSelectedItemsUnderCursor = { type = "bool", default = false }, - AllowCreateItem = { type = "bool", default = false }, - - DoNotRewindOnStepBackIfNothingErased = { type = "bool", default = true}, - CleanupJsfxAtClosing = { type = "bool", default = true}, - SelectInputNotes = { type = "bool", default = true}, - ------ - KeyPressModeAggregationTime = { type = "double", default = 0.05, min = 0, max = 0.1 }, - KeyPressModeInertiaTime = { type = "double", default = 0.5, min = 0.2, max = 1.0 }, - KeyPressModeInertiaEnabled = { type = "bool", default = true}, - - KeyReleaseModeForgetTime = { type = "double", default = 0.200, min = 0.05, max = 0.4}, - - RepitchModeAggregationTime = { type = "double", default = 0.05, min = 0, max = 0.1 }, - RepitchModeAffects = { type = "string", default = "Pitches Only", inclusion = { "Pitches only", "Velocities only", "Pitches + Velocities" } }, - - PedalRepeatEnabled = { type = "bool" , default = true }, - PedalRepeatTime = { type = "double", default = 0.200, min = 0.05, max = 0.5 }, - PedalRepeatFirstHitMultiplier = { type = "int", default = 4, min = 1, max = 10 }, - - Snap = { type = "bool", default = false }, - SnapNotes = { type = "bool", default = true }, - SnapProjectGrid = { type = "bool", default = true }, - SnapItemGrid = { type = "bool", default = true }, - SnapItemBounds = { type = "bool", default = true }, - - AutoScrollArrangeView = { type = "bool", default = true }, - - AllowKeyEventNavigation = { type = "bool", default = false } -}; - -local function unsafestr(str) - if str == "" then - return nil - end - return str -end - -local function getSetting(setting) - local spec = SettingDefs[setting]; - - if spec == nil then - error("Trying to get unknown setting " .. setting); - end - - local val = unsafestr(reaper.GetExtState("OneSmallStep", setting)); - - if val == nil then - val = spec.default; - else - if spec.type == 'bool' then - val = (val == "true"); - elseif spec.type == 'int' then - val = tonumber(val); - elseif spec.type == 'double' then - val = tonumber(val); - elseif spec.type == 'string' then - -- No conversion needed - end - end - return val; -end -local function setSetting(setting, val) - local spec = SettingDefs[setting]; - - if spec == nil then - error("Trying to set unknown setting " .. setting); - end - - if val == nil then - reaper.DeleteExtState("OneSmallStep", setting, true); - else - if spec.type == 'bool' then - val = (val == true) and "true" or "false"; - elseif spec.type == 'int' then - val = tostring(val); - elseif spec.type == 'double' then - val = tostring(val); - elseif spec.type == "string" then - -- No conversion needed - end - reaper.SetExtState("OneSmallStep", setting, val, true); - end -end -local function resetSetting(setting) - setSetting(setting, SettingDefs[setting].default) -end -local function getSettingSpec(setting) - return SettingDefs[setting] -end - -local function setPlaybackMeasureCount(c) return setSetting("PlaybackMeasureCount", c) end -local function getPlaybackMeasureCount() return getSetting("PlaybackMeasureCount") end - -local function setInputMode(m) return setSetting("Mode", m) end -local function getInputMode() return getSetting("Mode") end - -local function setNoteLenParamSource(m) return setSetting("NoteLenParamSource", m) end -local function getNoteLenParamSource() return getSetting("NoteLenParamSource") end - -local function setTupletDivision(m) return setSetting("TupletDivision", m) end -local function getTupletDivision() return getSetting("TupletDivision") end - -local function setNoteLen(nl) return setSetting("NoteLen", nl) end -local function getNoteLen() return getSetting("NoteLen") end - -local function setNoteLenModifier(nl) return setSetting("NoteLenModifier", nl) end -local function getNoteLenModifier() return getSetting("NoteLenModifier") end - -local function getNoteLenFactorNumerator() return getSetting("NoteLenFactorNumerator") end -local function getNoteLenFactorDenominator() return getSetting("NoteLenFactorDenominator") end - ---------------- --- FOCUS TOOLS - -local function IsActiveMidiEditorFocused() - local me = reaper.MIDIEditor_GetActive() - local f = reaper.JS_Window_GetFocus(); - while f do - if f == me then - return true - end - f = reaper.JS_Window_GetParent(f); - end - return false -end - -local function IsArrangeViewFocused() - return (reaper.GetCursorContext() >= 0); -end - -local lastKnownFocus = {}; -local function TrackFocus() - if IsActiveMidiEditorFocused() then - lastKnownFocus = { element = 'MIDIEditor' } - elseif IsArrangeViewFocused() then - lastKnownFocus = { element = 'ArrangeView', context = reaper.GetCursorContext() } - else - -- Simply ignore, we don't want to give back focus to this - end -end - -local function RestoreFocus() - - local hwnd = reaper.GetMainHwnd(); - reaper.JS_Window_SetFocus(hwnd); - - if lastKnownFocus.element == 'MIDIEditor' then - reaper.JS_Window_SetFocus(reaper.MIDIEditor_GetActive()); - elseif lastKnownFocus.element == 'ArrangeView' then - reaper.SetCursorContext(lastKnownFocus.context) - else - -- We don't know how to restore focus in a better way - end -end - ------------------------------------ --- Triggers for external actions - --- These functions are used to communicate between the independent actions and OSS - -local function validateActionTrigger(action_name) - if ActionTriggers[action_name] == nil then - error("Trying to use unknown action trigger :" .. action_name) - end -end - -local function setActionTrigger(action_name) - validateActionTrigger(action_name) - reaper.SetExtState("OneSmallStep", action_name .. "ActionTrigger", tostring(reaper.time_precise()), false); -end -local function getActionTrigger(action_name) - validateActionTrigger(action_name) - return tonumber(reaper.GetExtState("OneSmallStep", action_name .. "ActionTrigger")); -end -local function clearActionTrigger(action_name) - validateActionTrigger(action_name) - reaper.DeleteExtState("OneSmallStep", action_name .. "ActionTrigger", true); -end - -function clearAllActionTriggers() - for k,v in pairs(ActionTriggers) do - clearActionTrigger(k) - end -end - -local function hasActionTrigger(forward) - local res = false - for k,v in pairs(ActionTriggers) do - local cond = false - if forward then - cond = not v.back - else - cond = v.back - end - if cond then - res = (res or getActionTrigger(k)) - end - end - return res -end -local function hasForwardActionTrigger() - return hasActionTrigger(true) -end -local function hasBackwardActionTrigger() - return hasActionTrigger(false) -end - ------------- - --- Our manager for the Action/Pedal mode (use generic one) -local APActivityManager = KeyActivityManager:new(); --- Our manager for the Key Release input mode -local KRActivityManager = KeyReleaseActivityManager:new(); --- Our manager for the Key Press input mode -local KPActivityManager = KeyPressActivityManager:new(); - - -local function currentKeyEventManager() - local manager = nil - local mode = getInputMode(); - - -- We have different managers for all modes - -- But their architecture is identical and compliant - if mode == InputMode.KeyboardPress then - manager = KPActivityManager; - elseif mode == InputMode.KeyboardRelease then - manager = KRActivityManager; - else - manager = APActivityManager; - end - - return manager -end - -------------- --- Playback - -function findPlaybackMarker() - local mc = reaper.CountProjectMarkers(0); - for i=0, mc, 1 do - local retval, isrgn, pos, rgnend, name, markrgnindexnumber, color = reaper.EnumProjectMarkers3(0, i); - if name == "OSS Playback" then - return i, pos; - end - end - return nil; -end - -function setPlaybackMarkerAtPos(pos) - local id, mpos = findPlaybackMarker(); - - reaper.Undo_BeginBlock(); - if not (id == nil) then - reaper.DeleteProjectMarkerByIndex(0, id); - end - - if (mpos == nil) or math.abs(pos - mpos) > TIME_TOLERANCE then - reaper.AddProjectMarker2(0, false, pos, 0, "OSS Playback", -1, reaper.ColorToNative(0,200,255)|0x1000000); - end - reaper.Undo_EndBlock("One Small Step - Set playback marker", -1); -end - -function setPlaybackMarkerAtCurrentPos() - setPlaybackMarkerAtPos(reaper.GetCursorPosition()); -end - -function removePlaybackMarker() - reaper.Undo_BeginBlock(); - local id, mpos = findPlaybackMarker(); - if not (id == nil) then - reaper.DeleteProjectMarkerByIndex(0, id); - end - reaper.Undo_EndBlock("One Small Step - Remove playback marker", -1); -end - ------------------- - -local function validateModifierKeyCombination(id) - if ModifierKeyCombinationLookup[id] == nil then - error("Trying to use unknown modifier key combination with id " .. id) - end -end - --- Returns the state of the modifier key linked to the function "function_name" -local function IsModifierKeyCombinationPressed(id) - validateModifierKeyCombination(id) - - if id == "none" then - return false - end - - -- Avoid inconsistencies and only follow events during the lifetime of the plugin, so use launchTime - -- This will prevent bugs from a session to another (when for example the plugin crashes) - local keys = reaper.JS_VKeys_GetState(launchTime); - local combi = ModifierKeyCombinationLookup[id] - - for k, v in ipairs(combi.vkeys) do - local c1 = keys:byte(v); - if not (c1 ==1) then - return false - end - end - - return true -end - -local function IsStepBackModifierKeyPressed() - local keys = reaper.JS_VKeys_GetState(launchTime); - return (keys:byte(getSetting("StepBackModifierKey")) == 1) -end - ------------------ - -local function precpow2(n) - local b = 0 - local a = (n >> b) - while a > 0 do - b = b + 1 - a = (n >> b) - end - return (1 << (b-1)) -end - -local function tupletFactor(div) - return precpow2(div)/div -end - -local function getNoteLenModifierFactor() - - local m = getNoteLenModifier() - local nls = getNoteLenParamSource() - - if m == NoteLenModifier.Straight then - return 1.0; - elseif m == NoteLenModifier.Dotted then - return 1.5; - elseif m == NoteLenModifier.Triplet then - return 2/3.0; - elseif m == NoteLenModifier.Modified then - return getSetting("NoteLenFactorNumerator") / getSetting("NoteLenFactorDenominator"); - elseif m == NoteLenModifier.Tuplet then - local div = getTupletDivision(); - - if nls == NoteLenParamSource.OSS then - return tupletFactor(div) - else - -- In grid mode, we just return 1/n - return 1/div - end - end - - return 1.0; -end - -local function increaseNoteLen() - local l = getNoteLen(); - setNoteLen(NoteLenLookup[l].next); -end - -local function decreaseNoteLen() - local l = getNoteLen(); - setNoteLen(NoteLenLookup[l].prec); -end - -local function getNoteLenQN() - local nl = getNoteLen(); - - return NoteLenLookup[nl].qn; -end - - --------------------- - -local function swingNoteLenQN(measureStartQN, posQN, noteLenQN, swing) - local elapsedDoubleBeats = (posQN - measureStartQN)/(2*noteLenQN) - -- Hack, it may happen that the cursor is just before the measure start - if elapsedDoubleBeats < 0 then - elapsedDoubleBeats = 0 - end - - local eaten = elapsedDoubleBeats - math.floor(elapsedDoubleBeats) - if eaten > 1 - QN_TOLERANCE/noteLenQN then - -- Hack : cursor may be very close to next double beat. - eaten = 0 - end - - local onbeat = (1 + swing * 0.5) - local offbeat = (1 - swing * 0.5) - - if (2 * eaten) < onbeat - (QN_TOLERANCE / noteLenQN) then - return (noteLenQN * onbeat) - else - return (noteLenQN * offbeat) - end -end - -local function resolveNoteLenQN(take) - - local nlm = getNoteLenParamSource() - - local cursorTime = reaper.GetCursorPosition() - local cursorQN = reaper.TimeMap2_timeToQN(0, cursorTime) - local cursorMes = reaper.TimeMap_QNToMeasures(0, cursorQN) - local _, measureStartQN, measureEndQN = reaper.TimeMap_GetMeasureInfo(0, cursorMes - 1) - - if math.abs(cursorQN - measureEndQN) < QN_TOLERANCE then - -- We're on the measure end, advance 1 measure - cursorMes = cursorMes + 1 - _, measureStartQN, measureEndQN = reaper.TimeMap_GetMeasureInfo(0, cursorMes - 1) - end - - if nlm == NoteLenParamSource.OSS then - return getNoteLenQN() * getNoteLenModifierFactor() - elseif nlm == NoteLenParamSource.ProjectGrid then - - local _, division, swingmode, swing = reaper.GetSetProjectGrid(0, false) - local noteLenQN = division * 4 - local multFactor = getNoteLenQN() - - local baselen = 1 - if swingmode == 0 then - -- No swing - baselen = noteLenQN - elseif swingmode == 3 then - -- Project Grid is set to "measure" - baselen = (measureEndQN - measureStartQN) - else - -- Swing - if multFactor > 1 then - baselen = noteLenQN - else - baselen = swingNoteLenQN(measureStartQN, cursorQN, noteLenQN, swing) - end - end - - return baselen * getNoteLenQN() * getNoteLenModifierFactor() - - else - local gridLenQN, swing, noteLenQN = reaper.MIDI_GetGrid(take); - local multFactor = getNoteLenQN() - - if noteLenQN == 0 then - noteLenQN = gridLenQN - end - - local baselen = 1 - if swing == 0 then - baselen = noteLenQN - else - -- Swing - if multFactor > 1 then - baselen = noteLenQN - else - baselen = swingNoteLenQN(measureStartQN, cursorQN, noteLenQN, swing) - end - end - - return baselen * getNoteLenQN() * getNoteLenModifierFactor() - end -end - -local function resolveOperationMode(look_for_action_triggers) - local bk = IsStepBackModifierKeyPressed() - local mode = getSetting("EditMode") - local triggered_by_action = false - - local editModes = { - { name = "Write", prio = 4 }, - { name = "Navigate", prio = 3 }, - { name = "Insert", prio = 2 }, - { name = "Repitch", prio = 2.5}, - { name = "Replace", prio = 1 }, - } - - local activemodes = {} - for k, editmode in ipairs(editModes) do - local setting = getSetting(editmode.name .. "ModifierKeyCombination") - local combi = ModifierKeyCombinationLookup[setting] - local pressed = IsModifierKeyCombinationPressed(combi.id) - if pressed then - activemodes[#activemodes + 1] = { mode = editmode.name, combi = combi, prio = editmode.prio } - end - end - - table.sort(activemodes, function(e1,e2) - -- Priorize items that have their track selected - local l1 = #e1.combi.vkeys - local l2 = #e2.combi.vkeys - - if l1 == l2 then - return e1.prio < e2.prio - end - - return l1 > l2; - end) - - if #activemodes > 0 then - mode = activemodes[1].mode - end - - if look_for_action_triggers then - for k, v in pairs(ActionTriggers) do - local has_triggered = getActionTrigger(k) - if has_triggered then - if v.action ~= "Commit" then - -- If it's a commit, use current mode, else use the mode linked to the trigger - mode = v.action - end - triggered_by_action = true - break - end - end - end - - return { - mode = mode, - back = bk - } -end - -------------------- - - -local function KeepEditCursorOnScreen() - local start_time, end_time = reaper.GetSet_ArrangeView2(0, false, 0, 0, 0, 0) - local cursor_time = reaper.GetCursorPosition() - local diff_time = end_time - start_time - local bound = 0.05 - local alpha = 0.25 - - if cursor_time < start_time + bound * diff_time then - reaper.GetSet_ArrangeView2(0, true, 0, 0, cursor_time - diff_time * alpha, cursor_time + diff_time * (1 - alpha)) - end - - if cursor_time > end_time - bound * diff_time then - reaper.GetSet_ArrangeView2(0, true, 0, 0, cursor_time - diff_time * (1-alpha), cursor_time + diff_time * alpha) - end -end - - -local function MediaItemContainsCursor(mediaItem, CursorPos) - local pos = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") - local len = reaper.GetMediaItemInfo_Value(mediaItem, "D_LENGTH") - - local left = pos - TIME_TOLERANCE; - local right = pos + len + TIME_TOLERANCE; - - -- Only keep items that contain the cursor pos - return (CursorPos >= left) and (CursorPos <= right); -end - - -local function TryToGetTakeFromMidiEditor() - local midiEditor = reaper.MIDIEditor_GetActive(); - local midiEditorOk = not (reaper.MIDIEditor_GetMode(midiEditor) == -1); - - -- Prioritize the currently focused MIDI editor. - -- Use the last known focused element (between arrange view / midi editor) as it is more robust - -- When OSS window is focused for editing parameters - if midiEditorOk and lastKnownFocus.element == "MIDIEditor" then - -- -1 if ME not focused - take = reaper.MIDIEditor_GetTake(midiEditor); - if take then - local mediaItem = reaper.GetMediaItemTake_Item(take); - if MediaItemContainsCursor(mediaItem, reaper.GetCursorPosition()) then - return take - end - end - end - - return nil; -end - -local function TryToGetTakeFromArrangeViewAmongSelectedItems() - local mediaItemCount = reaper.CountSelectedMediaItems(0); - local cursorPos = reaper.GetCursorPosition(); - - local candidates = {}; - - for i = 0, mediaItemCount - 1 do - - local mediaItem = reaper.GetSelectedMediaItem(0, i) - local track = reaper.GetMediaItem_Track(mediaItem) - - -- Only keep items that contain the cursor pos - if MediaItemContainsCursor(mediaItem, cursorPos) then - local tk = reaper.GetActiveTake(mediaItem); - - candidates[#candidates + 1] = { - take = tk, - tsel = reaper.IsTrackSelected(track), - tname = reaper.GetTrackName(track), - name = reaper.GetTakeName(tk) - } - end - end - - table.sort(candidates, function(e1,e2) - -- Priorize items that have their track selected - local l1 = e1.tsel and 0 or 1; - local l2 = e2.tsel and 0 or 1; - - return l1 < l2; - end); - - if (#candidates) > 0 then - return candidates[1].take; - end - - return nil -end - -local function TryToGetTakeFromArrangeViewAmongSelectedTracks() - local cursorPos = reaper.GetCursorPosition(); - local trackCount = reaper.CountSelectedTracks(); - - local candidates = {}; - - for i = 0, trackCount - 1 do - local track = reaper.GetSelectedTrack(0, i); - local itemCount = reaper.CountTrackMediaItems(track); - for j = 0, itemCount - 1 do - local mediaItem = reaper.GetTrackMediaItem(track, j); - - if MediaItemContainsCursor(mediaItem, cursorPos) then - local tk = reaper.GetActiveTake(mediaItem); - - candidates[#candidates + 1] = { - take = tk, - tname = reaper.GetTrackName(track), - name = reaper.GetTakeName(tk) - } - end - end - end - - -- No sorting is possible - if (#candidates) > 0 then - return candidates[1].take; - end - - return nil -end - - --- This function returns the take that should be edited. --- Inspired by tenfour's scripts but modified --- It uses a strategy based on : --- - What component has focus (midi editor or arrange window) --- - What items are selected --- - What items contain the cursor --- - What tracks are selected - -local function TakeForEdition() - -- Try to get a take from the MIDI editor - local take = nil; - - if getSetting("AllowTargetingFocusedMidiEditors") then - take = TryToGetTakeFromMidiEditor(); - if take then - return take; - end - end - - -- Second heuristic, try to get a take from selected items - take = TryToGetTakeFromArrangeViewAmongSelectedItems(); - if take then - return take; - end - - if getSetting("AllowTargetingNonSelectedItemsUnderCursor") then - -- Third heuristic (if enabled), try to get a take from selected tracks - take = TryToGetTakeFromArrangeViewAmongSelectedTracks(); - if take then - return take; - end - end - - return nil; -end - -local function TrackForEditionIfNoItemFound() - local trackCount = reaper.CountSelectedTracks(); - if trackCount > 0 then - return reaper.GetSelectedTrack(0, 0); - end - return nil; -end - ---------------------------- - - -local function CreateItemIfMissing(track) - local newitem = reaper.CreateNewMIDIItemInProj(track, reaper.GetCursorPosition(), reaper.GetCursorPosition() + TIME_TOLERANCE, false); - take = reaper.GetMediaItemTake(newitem, 0); - local _, tname = reaper.GetTrackName(track); - reaper.GetSetMediaItemTakeInfo_String(take, "P_NAME", tname ..os.date(' - %Y%m%d%H%M%S'), true); - return take; -end - - -local function GetNote(take, ni, use_mu) - - local selected, muted, startPPQ, endPPQ, chan, pitch, vel, offvel - - if use_mu then - _, selected, muted, startPPQ, endPPQ, chan, pitch, vel, offvel = midi_utils.MIDI_GetNote(take, ni); - else - _, selected, muted, startPPQ, endPPQ, chan, pitch, vel = reaper.MIDI_GetNote(take, ni); - offvel = 0 - end - - return { - index = ni, - selected = selected, - muted = muted, - pitch = pitch, - chan = chan, - vel = vel, - startPPQ = startPPQ, - startQN = reaper.MIDI_GetProjQNFromPPQPos(take, startPPQ), - endPPQ = endPPQ, - endQN = reaper.MIDI_GetProjQNFromPPQPos(take, endPPQ), - offvel = offvel - }; -end - -local CrossApi = {} - -CrossApi.GetNote = GetNote - -CrossApi.MIDI_CountEvts = function(take, use_mu) - if use_mu then - return midi_utils.MIDI_CountEvts(take) - else - return reaper.MIDI_CountEvts(take) - end -end - - -local function SetNote(take, n, nosort) - reaper.MIDI_SetNote(take, n.index, n.selected, n.muted, n.startPPQ, n.endPPQ, n.chan, n.pitch, n.vel, nosort) -end - -local function bool2sign(b) - return ((b == true) and (1) or (-1)) -end - -local function noteStartsAfterPPQ(note, limit, strict) - return note.startPPQ > (limit + bool2sign(strict) * PPQ_TOLERANCE) -end -local function noteStartsBeforePPQ(note, limit, strict) - return note.startPPQ < (limit - bool2sign(strict) * PPQ_TOLERANCE) -end -local function noteEndsAfterPPQ(note, limit, strict) - return note.endPPQ > (limit + bool2sign(strict) * PPQ_TOLERANCE) -end -local function noteEndsBeforePPQ(note, limit, strict) - return note.endPPQ < (limit - bool2sign(strict) * PPQ_TOLERANCE) -end - -local function noteEndsOnPPQ(note, limit) - return math.abs(note.endPPQ - limit) < PPQ_TOLERANCE -end - -local function noteStartsOnPPQ(note, limit) - return math.abs(note.startPPQ - limit) < PPQ_TOLERANCE -end - -local function noteStartsInWindowPPQ(note, left, right, strict) - local a = noteStartsAfterPPQ(note, left, strict) - local e = noteStartsBeforePPQ(note, right, strict) - - return a and e -end - -local function setNewNoteBounds(note, take, startPPQ, endPPQ, startOffsetQN, endOffsetQN) - note.startQN = reaper.MIDI_GetProjQNFromPPQPos(take, startPPQ) + startOffsetQN - note.endQN = reaper.MIDI_GetProjQNFromPPQPos(take, endPPQ) + endOffsetQN - note.startPPQ = reaper.MIDI_GetPPQPosFromProjQN(take, note.startQN ) - note.endPPQ = reaper.MIDI_GetPPQPosFromProjQN(take, note.endQN ) -end - -local function moveComparatorHelper(v, cursor, best, direction, mode) - - local tolerance = TIME_TOLERANCE - if mode == "PPQ" then - tolerance = PPQ_TOLERANCE - end - - if direction > 0 then - if (v > cursor + tolerance) and ((best == nil) or (v < best)) then - best = v - end - else - if (v < cursor - tolerance) and ((best == nil) or (v > best)) then - best = v - end - end - - return best -end - - -local function gridSnapHelper(type, direction, cursorTime, cursorQN, bestJumpTime, take, itemStartTime, itemEndTime) - - local grid_len, swing = nil , nil - - if type == "ITEM" then - grid_len, swing, _ = reaper.MIDI_GetGrid(take) - else - _, grid_len, swingmode, swing = reaper.GetSetProjectGrid(0, false) - grid_len = grid_len * 4 -- put back in QN - if swingmode ~= 1 then - swing = 0 - end - end - - local cursorBars = reaper.TimeMap_QNToMeasures(0, cursorQN) - 1 - local _, measureStartQN, measureEndQN = reaper.TimeMap_GetMeasureInfo(0, cursorBars) - - if cursorBars > 0 and direction < 0 and math.abs(cursorQN - measureStartQN) < QN_TOLERANCE then - -- Cursor is aligned on the beginning of a measure but we're going back. - -- Work with the precedent measure. - cursorBars = cursorBars - 1 - _, measureStartQN, measureEndQN = reaper.TimeMap_GetMeasureInfo(0, cursorBars) - end - - -- Start with window, and slide - - local parity = 1 - - local oddOffset = grid_len * (1 + swing * 0.5) - local evenOffset = grid_len * (1 - swing * 0.5) - - local prec = measureStartQN - local next = prec + ((parity == 1) and (oddOffset) or (evenOffset)) - - while next < (cursorQN - QN_TOLERANCE) do - parity = parity ~ 1 - prec = next - next = prec + ((parity == 1) and (oddOffset) or (evenOffset)) - end - - if math.abs(cursorQN - next) < QN_TOLERANCE then - parity = parity ~ 1 - next = next + ((parity == 1) and (oddOffset) or (evenOffset)) - end - - local precTime = reaper.TimeMap2_QNToTime(0, prec) - local nextTime = reaper.TimeMap2_QNToTime(0, next) - local msTime = reaper.TimeMap2_QNToTime(0, measureStartQN) - local meTime = reaper.TimeMap2_QNToTime(0, measureEndQN) - - -- Only add these times if they belong to the item (outside, consider there's no grid) - if type == "PROJECT" or (precTime >= itemStartTime and precTime <= itemEndTime) then - bestJumpTime = moveComparatorHelper(precTime, cursorTime, bestJumpTime, direction, "TIME") - end - if type == "PROJECT" or (nextTime >= itemStartTime and nextTime <= itemEndTime) then - bestJumpTime = moveComparatorHelper(nextTime, cursorTime, bestJumpTime, direction, "TIME") - end - if type == "PROJECT" or (msTime >= itemStartTime and msTime <= itemEndTime) then - bestJumpTime = moveComparatorHelper(msTime, cursorTime, bestJumpTime, direction, "TIME") - end - if type == "PROJECT" or (meTime >= itemStartTime and meTime <= itemEndTime) then - bestJumpTime = moveComparatorHelper(meTime, cursorTime, bestJumpTime, direction, "TIME") - end - - return bestJumpTime -end - - --- Resolves the next snap point. --- Track can be nil (won't happen) -local function nextSnap(track, direction, reftime, options) - - local cursorTime = reftime -- I don't want to rename everything ... - local cursorQN = reaper.TimeMap2_timeToQN(0, cursorTime) - local bestJumpTime = nil - local maxTime = 0 - - if options.enabled and track then - - local itemCount = reaper.CountTrackMediaItems(track) - local ii = 0 - - -- For optimization, we should randomize the order of iteration over the items - while ii < itemCount do - local mediaItem = reaper.GetTrackMediaItem(track, ii) - - local itemStartTime = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") - local itemEndTime = itemStartTime + reaper.GetMediaItemInfo_Value(mediaItem, "D_LENGTH") - - if itemEndTime > maxTime then - maxTime = itemEndTime - end - - -- A few conditions to avoid exploring the item if not needed - if (direction > 0) then - if (itemEndTime < cursorTime) or ((bestJumpTime ~= nil) and (bestJumpTime < itemStartTime)) then - goto nextitem - end - else - if (itemStartTime > cursorTime) or ((bestJumpTime ~= nil) and (bestJumpTime > itemEndTime)) then - goto nextitem - end - end - - if options.itemBounds then - bestJumpTime = moveComparatorHelper(itemStartTime, cursorTime, bestJumpTime, direction, "TIME") - bestJumpTime = moveComparatorHelper(itemEndTime, cursorTime, bestJumpTime, direction, "TIME") - end - - if options.noteStart or options.noteEnd or options.itemGrid then - local takeCount = reaper.GetMediaItemNumTakes(mediaItem) - local ti = 0 - - while ti < takeCount do - - local take = reaper.GetMediaItemTake(mediaItem, ti) - - local itemStartPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemStartTime) - local itemEndPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemEndTime) - local cursorPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorTime) - local bestJumpPPQ = nil - - if options.noteStart or options.noteEnd then - local _, notecnt, _, _ = reaper.MIDI_CountEvts(take) - local ni = 0 - - while (ni < notecnt) do - local n = GetNote(take, ni) - - if options.noteStart then - bestJumpPPQ = moveComparatorHelper(n.startPPQ, cursorPPQ, bestJumpPPQ, direction, "PPQ") - end - - if options.noteEnd then - bestJumpPPQ = moveComparatorHelper(n.endPPQ, cursorPPQ, bestJumpPPQ, direction, "PPQ") - end - - ni = ni+1 - end -- end note iteration - - if bestJumpPPQ then - -- Found a snap note inside item, convert back to time and compare to already found bestJumpTime - local bjt = reaper.TimeMap2_QNToTime(0, reaper.MIDI_GetProjQNFromPPQPos(take, bestJumpPPQ)) - bestJumpTime = moveComparatorHelper(bjt, cursorTime, bestJumpTime, direction, "TIME") - end - end - - if options.itemGrid then - bestJumpTime = gridSnapHelper("ITEM", direction, cursorTime, cursorQN, bestJumpTime, take, itemStartTime, itemEndTime) - end - - ti = ti + 1 - end -- end take iteration - - end - - ::nextitem:: - ii = ii+1 - end -- end item iteration - end -- end options.enabled - - if options.projectGrid then - -- SWS version of BR_GetNextGrid has a bug, use my own implementation - bestJumpTime = gridSnapHelper("PROJECT", direction, cursorTime, cursorQN, bestJumpTime) - end - - -- Add track boundaries - bestJumpTime = moveComparatorHelper(0, cursorTime, bestJumpTime, direction, "TIME") - bestJumpTime = moveComparatorHelper(maxTime, cursorTime, bestJumpTime, direction, "TIME") - - -- Safety - if bestJumpTime == nil then - if maxTime == nil then - maxTime = cursorTime - end - bestJumpTime = (direction > 0) and (cursorTime) or 0 - end - - return { - time = bestJumpTime, - qn = reaper.TimeMap2_timeToQN(0, bestJumpTime) - } -end - -local function snapOptions() - return { - enabled = getSetting("Snap"), - itemBounds = getSetting("SnapItemBounds"), - noteStart = getSetting("SnapNotes"), - noteEnd = getSetting("SnapNotes"), - itemGrid = getSetting("SnapItemGrid"), - projectGrid = getSetting("SnapProjectGrid") - } -end - -local function nextSnapFromCursor(track, direction) - return nextSnap(track, direction, reaper.GetCursorPosition(), snapOptions()) -end - -local function nextSnapNote(track) - return nextSnap(track, direction, reaper.GetCursorPosition(), snapOptions()) -end - -local function navigate(track, direction) - local ns = nextSnapFromCursor(track, direction) - - reaper.Undo_BeginBlock(); - reaper.SetEditCurPos(ns.time, false, false); - if getSetting("AutoScrollArrangeView") then - KeepEditCursorOnScreen() - end - reaper.Undo_EndBlock("One Small Step: " .. ((direction > 0) and ("advanced") or ("stepped back")),-1); -end - - -local function navigateForward(track) - navigate(track, 1) -end - --- Commits the currently held notes into the take -local function navigateBack(track) - navigate(track, -1) -end - - -local function commitDescription(direction, addcount, remcount, shcount, extcount, mvcount) - local description = {} - - if shcount+addcount+remcount+shcount+extcount == 0 then - if direction > 0 then - description[#description+1] = "advanced" - else - description[#description+1] = "stepped back" - end - end - if addcount > 0 then - description[#description+1] = "added " .. addcount .. " notes" - end - if remcount > 0 then - description[#description+1] = "removed " .. remcount .. " notes" - end - if mvcount > 0 then - description[#description+1] = "moved " .. mvcount .. " notes" - end - if shcount > 0 then - description[#description+1] = "shortened " .. shcount .. " notes" - end - if extcount > 0 then - description[#description+1] = "extended " .. extcount .. " notes" - end - - return "One Small Step: " .. table.concat(description, ", ") -end - -local function AllowKeyEventNavigation() - return getSetting("AllowKeyEventNavigation") -end - -local autoOverlap = nil -local function PushAutoCorrectOverlapOption() - autoOverlap = reaper.GetToggleCommandStateEx(32060, 40681) - if autoOverlap == 1 then - reaper.MIDIEditor_LastFocused_OnCommand(40681, false) -- toggle off - end -end - -local function PopAutoCorrectOverlapOption() - if autoOverlap == 1 then - reaper.MIDIEditor_LastFocused_OnCommand(40681, false) -- toggle back on - end -end +local helper_lib = require "helper_lib" + +local KeyActivityManager = require "input_managers/KeyActivityManager" +local KeyReleaseActivityManager = require "input_managers/KeyReleaseActivityManager" +local KeyPressActivityManager = require "input_managers/KeyPressActivityManager" + +local S = require "modules/settings" +local D = require "modules/defines" +local AT = require "modules/action_triggers" +local MK = require "modules/markers" +local T = require "modules/time" +local SNP = require "modules/snap" +local N = require "modules/notes" +local TGT = require "modules/target" +local F = require "modules/focus" +local MOD = require "modules/modifiers" +local ED = require "modules/edition" + +local NAVIGATE = require "operations/navigate" +local REPITCH = require "operations/repitch" +local INSERT = require "operations/insert" +local WRITE = require "operations/write" +local REPLACE = require "operations/replace" +local STRETCH = require "operations/stretch" +local STUFF = require "operations/stuff" + +------------------------------------------ +-- Variables -local function repitch(track, take, notes_to_add, notes_to_extend, triggered_by_key_event) - - local shcount, remcount, mvcount, addcount, extcount = 0, 0, 0, 0, 0 - - local cursorTime = reaper.GetCursorPosition() - local cursorQN = reaper.TimeMap2_timeToQN(0, cursorTime) - - local aggregationTime = cursorTime + getSetting("RepitchModeAggregationTime") - - local useNewVelocities = string.find(getSetting("RepitchModeAffects"), "Velocities") - local useNewPitches = string.find(getSetting("RepitchModeAffects"), "Pitches") - - local shouldJump = true - - reaper.Undo_BeginBlock(); - - if take then - local mediaItem = reaper.GetMediaItemTake_Item(take) - - local cursorPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorTime) - local aggregationPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, aggregationTime) - - local itemStartTime = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") - local itemLength = reaper.GetMediaItemInfo_Value(mediaItem, "D_LENGTH") - local itemEndTime = itemStartTime + itemLength; - - local useMidiUtils = true - - local _, notecnt, _, _ = CrossApi.MIDI_CountEvts(take, useMidiUtils) - - - if useMidiUtils then - midi_utils.MIDI_InitializeTake(take) - end - - for _, v in pairs(notes_to_extend) do - notes_to_add[#notes_to_add+1] = v - end - - local tomod = {} - - local ni = 0 - while (ni < notecnt) do - local n = GetNote(take, ni, useMidiUtils) - - if noteStartsInWindowPPQ(n, cursorPPQ, aggregationPPQ, false) then - tomod[#tomod + 1] = n - end - - ni = ni + 1 - end - - if #tomod == 0 or #notes_to_add == 0 then - -- Just jump - else - if (#tomod ~= #notes_to_add) then - shouldJump = false - else - -- Apply mote modifications - - -- Sort notes to modify by pitch - table.sort(tomod, function(n1, n2) - return n1.pitch < n2.pitch - end) - - -- Sort notes to add by pitch - table.sort(notes_to_add, function(n1, n2) - return n1.note < n2.note - end) - - if useMidiUtils then - PushAutoCorrectOverlapOption() - midi_utils.MIDI_OpenWriteTransaction(take) - end - - for k, n in ipairs(tomod) do - local newvel = nil - local newpitch = nil - if useNewVelocities then - newvel = notes_to_add[k].velocity - end - if useNewPitches then - newpitch = notes_to_add[k].note - end - - if useMidiUtils then - midi_utils.MIDI_SetNote(take, n.index, nil, nil, nil, nil, nil, newpitch, newvel, nil) - else - reaper.MIDI_SetNote(take, n.index, nil, nil, nil, nil, nil, newpitch, newvel, false) - end - - if n.startPPQ > cursorPPQ then - cursorPPQ = n.startPPQ - end - end - - if useMidiUtils then - midi_utils.MIDI_CommitWriteTransaction(take) - PopAutoCorrectOverlapOption() - end - - cursorTime = reaper.MIDI_GetProjTimeFromPPQPos(take, cursorPPQ) - end - end - - if not useMidiUtils then - reaper.MIDI_Sort(take) - end - - reaper.UpdateItemInProject(mediaItem) - reaper.MarkTrackItemsDirty(track, mediaItem) - end +-- Our manager for the Action/Pedal mode (use generic one) +local APActivityManager = KeyActivityManager:new(); +-- Our manager for the Key Release input mode +local KRActivityManager = KeyReleaseActivityManager:new(); +-- Our manager for the Key Press input mode +local KPActivityManager = KeyPressActivityManager:new(); - if shouldJump then - local jumpTime = nextSnap(track, 1, cursorTime, {enabled = true, noteStart = true}) - jumpTime = (jumpTime and jumpTime.time) or itemEndTime - reaper.SetEditCurPos(jumpTime, false, false) +local function currentKeyEventManager() + local manager = nil + local mode = S.getInputMode(); - if getSetting("AutoScrollArrangeView") then - KeepEditCursorOnScreen() - end + -- We have different managers for all modes + -- But their architecture is identical and compliant + if mode == D.InputMode.KeyboardPress then + manager = KPActivityManager; + elseif mode == D.InputMode.KeyboardRelease then + manager = KRActivityManager; + else + manager = APActivityManager; end - reaper.Undo_EndBlock(commitDescription(1, addcount, remcount, shcount, extcount, mvcount), -1) + return manager end -local function repitchBack(track, take, notes_to_add, notes_to_extend, triggered_by_key_event) - - local shcount, remcount, mvcount, addcount, extcount = 0, 0, 0, 0, 0 - - local cursorTime = reaper.GetCursorPosition() - local cursorQN = reaper.TimeMap2_timeToQN(0, cursorTime) - local shouldJump = true - - reaper.Undo_BeginBlock(); - - local jumpTime = nextSnap(track, -1, cursorTime, {enabled = true, noteStart = true}) - jumpTime = (jumpTime and jumpTime.time) or itemStartTime - reaper.SetEditCurPos(jumpTime, false, false) - - if getSetting("AutoScrollArrangeView") then - KeepEditCursorOnScreen() - end - - reaper.Undo_EndBlock(commitDescription(1, addcount, remcount, shcount, extcount, mvcount), -1) -end +----------------- -- Commits the currently held notes into the take local function commit(track, take, notes_to_add, notes_to_extend, triggered_by_key_event) - local currentop = resolveOperationMode(true) + local currentop = ED.ResolveOperationMode(true) - local writeModeON = (currentop.mode == "Write") + local writeModeOn = (currentop.mode == "Write") local navigateModeOn = (currentop.mode == "Navigate") local insertModeOn = (currentop.mode == "Insert") local replaceModeOn = (currentop.mode == "Replace") local repitchModeOn = (currentop.mode == "Repitch") + local stretchModeOn = false + local stuffModeOn = false - if repitchModeOn then - return repitch(track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + if insertModeOn and currentop.use_alt then + insertModeOn = false + stretchModeOn = true + end + + if replaceModeOn and currentop.use_alt then + replaceModeOn = false + stuffModeOn = true end if navigateModeOn then - if (not triggered_by_key_event) or AllowKeyEventNavigation() then - return navigateForward(track) + if (not triggered_by_key_event) or S.AllowKeyEventNavigation() then + return NAVIGATE.NavigateForward(track) else -- Triggered by key event and not allowed ... do nothing return @@ -1404,554 +97,98 @@ local function commit(track, take, notes_to_add, notes_to_extend, triggered_by_k -- Other operations perform changes so go. if take == nil then - take = CreateItemIfMissing(track); + take = TGT.CreateItemIfMissing(track); end - local note_len = resolveNoteLenQN(take); - local mediaItem = reaper.GetMediaItemTake_Item(take) - - local cursorTime = reaper.GetCursorPosition() - local cursorQN = reaper.TimeMap2_timeToQN(0, cursorTime) - local cursorPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorTime) - - local advanceQN = cursorQN + note_len; - local advanceTime = reaper.TimeMap2_QNToTime(0, advanceQN) - local advancePPQ = reaper.MIDI_GetPPQPosFromProjTime(take, advanceTime) - - local newMaxQN = (justAdvancing and cursorQN) or advanceQN - - local shcount, remcount, mvcount, addcount, extcount = 0, 0, 0, 0, 0 - - local torem = {} - local tomod = {} - local toadd = {} - - local buildNewMidiNote = function(note_from_manager) - return { - index = nil, - selected = getSetting("SelectInputNotes"), - muted = false, - chan = note_from_manager.chan, - pitch = note_from_manager.note, - vel = note_from_manager.velocity, - startPPQ = cursorPPQ, - endPPQ = advancePPQ, - startQN = cursorQN, - endQN = advanceQN - } + if repitchModeOn then + return REPITCH.Repitch(currentKeyEventManager(), track, take, notes_to_add, notes_to_extend, triggered_by_key_event) end - reaper.Undo_BeginBlock(); - - -- First, move some notes if insert mode is on if insertModeOn then - local _, notecnt, _, _ = reaper.MIDI_CountEvts(take) - local ni = 0 - - while (ni < notecnt) do - local n = GetNote(take, ni); - - if noteStartsAfterPPQ(n, cursorPPQ, false) then - -- Move the note - setNewNoteBounds(n, take, n.startPPQ, n.endPPQ, note_len, note_len) - - if n.endQN > newMaxQN then - newMaxQN = n.endQN - end - - -- It should be removed and readded, because the start position changes - torem[#torem + 1] = n - toadd[#toadd + 1] = n - - mvcount = mvcount + 1 - end - - ni = ni + 1 - end - end - - if replaceModeOn then - -- Erase forward - local _, notecnt, _, _ = reaper.MIDI_CountEvts(take); - - local ni = 0; - while (ni < notecnt) do - - -- Examine each note in item - local n = GetNote(take, ni); - - if noteStartsAfterPPQ(n, advancePPQ, false) then - -- Note is not in the erasing window - -- - -- C A - -- | | - -- | | ==== - -- | | - -- - elseif noteStartsAfterPPQ(n, cursorPPQ, false) then - if noteEndsBeforePPQ(n, advancePPQ, false) then - -- Note should be suppressed - -- - -- C A - -- | | - -- | === | - -- | | - -- - torem[#torem+1] = n - remcount = remcount + 1 - else - -- The note should be shortened (removing tail). - -- Since its start will change, it should be removed and reinserted (see reaper's API doc) - -- - -- RC A - -- | | - -- | ==|=== - -- | | - -- - setNewNoteBounds(n, take, advancePPQ, n.endPPQ, 0, 0) - - torem[#torem+1] = n - toadd[#toadd+1] = n - - shcount = shcount + 1 - mvcount = mvcount + 1 - end - else - if noteEndsAfterPPQ(n, advancePPQ, false) then - -- We should make a hole. Shorten (or erase) left part. Shorten (or erase) right part - -- - -- C A - -- | | - -- ==|=====|=== - -- | | - -- - if noteStartsOnPPQ(n, cursorPPQ) then - -- The start changes, remove and reinsert - setNewNoteBounds(n, take, advancePPQ, n.endPPQ, 0, 0) - - torem[#torem+1] = n - toadd[#toadd+1] = n - - shcount = shcount + 1 - mvcount = mvcount + 1 - else - -- Copy note - local newn = {} - for k,v in pairs(n) do - newn[k] = v - end - - -- Shorten the note - setNewNoteBounds(n, take, n.startPPQ, cursorPPQ, 0, 0) - - tomod[#tomod+1] = n - shcount = shcount + 1 - - if not noteEndsOnPPQ(newn, advancePPQ) then - -- Add new note - setNewNoteBounds(newn, take, advancePPQ, newn.endPPQ, 0, 0); - toadd[#toadd+1] = newn - addcount = addcount + 1 - end - end - - elseif noteEndsAfterPPQ(n, cursorPPQ, true) then - -- Note ending should be erased - -- - -- C A - -- | | - -- ==|=== | - -- | | - -- - setNewNoteBounds(n, take, n.startPPQ, cursorPPQ, 0, 0); - tomod[#tomod+1] = n - shcount = shcount + 1 - else - -- Leave untouched - -- - -- C A - -- | | - -- === | | - -- | | - -- - end - - end - - ni = ni + 1; - end - - end - - -- All modes (Write / Insert / Replace) will insert new notes - for _, v in ipairs(notes_to_add) do - toadd[#toadd+1] = buildNewMidiNote(v) - addcount = addcount + 1 - end - - -- All modes (Write / Insert / Replace) will extend existing notes - if #notes_to_extend > 0 then - local _, notecnt, _, _ = reaper.MIDI_CountEvts(take); - - for _, exnote in pairs(notes_to_extend) do - - -- Search for a note that could be extended (matches all conditions) - local ni = 0; - local found = false; - - while (ni < notecnt) do - local n = GetNote(take, ni); - - -- Extend the note if found - if noteEndsOnPPQ(n, cursorPPQ) and (n.chan == exnote.chan) and (n.pitch == exnote.note) then - - tomod[#tomod + 1] = n - setNewNoteBounds(n, take, n.startPPQ, advancePPQ, 0, 0) - - extcount = extcount + 1; - found = true - end - - ni = ni + 1; - end - - if not found then - -- Could not find a note to extend... create one ! - toadd[#toadd+1] = buildNewMidiNote(exnote) - addcount = addcount + 1 - end - end - end - - -- Modify notes - for ri = 1, #tomod, 1 do - local n = tomod[ri] - reaper.MIDI_SetNote(take, n.index, nil, nil, n.startPPQ, n.endPPQ, nil, nil, nil, true ) + return INSERT.Insert(currentKeyEventManager(), track, take, notes_to_add, notes_to_extend, triggered_by_key_event) end - -- Delete notes that were shorten too much - -- Do this in reverse order to be sure that indices are descending - for ri = #torem, 1, -1 do - reaper.MIDI_DeleteNote(take, torem[ri].index) + if writeModeOn then + return WRITE.Write(currentKeyEventManager(), track, take, notes_to_add, notes_to_extend, triggered_by_key_event) end - -- Reinsert moved notes - for ri = 1, #toadd, 1 do - local n = toadd[ri] - reaper.MIDI_InsertNote(take, n.selected, n.muted, n.startPPQ, n.endPPQ, n.chan, n.pitch, n.vel, true ) + if replaceModeOn then + return REPLACE.Replace(currentKeyEventManager(), track, take, notes_to_add, notes_to_extend, triggered_by_key_event) end - -- Advance and mark dirty - reaper.MIDI_Sort(take) - reaper.UpdateItemInProject(mediaItem) - reaper.SetEditCurPos(advanceTime, false, false); - if getSetting("AutoScrollArrangeView") then - KeepEditCursorOnScreen() + if stretchModeOn then + return STRETCH.Stretch(currentKeyEventManager(), track, take, notes_to_add, notes_to_extend, triggered_by_key_event) end - -- Grow the midi item if needed - local itemStartTime = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") - local itemLength = reaper.GetMediaItemInfo_Value(mediaItem, "D_LENGTH") - local itemEndTime = itemStartTime + itemLength; - local newMaxTime = reaper.TimeMap2_QNToTime(0, newMaxQN) - - if(itemEndTime >= newMaxTime) then - -- Cool, the item is big enough - else - local itemStartQN = reaper.TimeMap2_timeToQN(0, itemStartTime) - local itemEndQN = reaper.TimeMap2_timeToQN(0, newMaxTime) - - reaper.MIDI_SetItemExtents(mediaItem, itemStartQN, itemEndQN) - reaper.UpdateItemInProject(mediaItem); + if stuffModeOn then + return STUFF.Stuff(currentKeyEventManager(), track, take, notes_to_add, notes_to_extend, triggered_by_key_event) end - - -- Mark item as dirty - reaper.MarkTrackItemsDirty(track, mediaItem) - - reaper.Undo_EndBlock(commitDescription(1, addcount, remcount, shcount, extcount, mvcount),-1); end -local blockRewindRef = nil - local function commitBack(track, take, notes_to_shorten, triggered_by_key_event) - local currentop = resolveOperationMode(true) + local currentop = ED.ResolveOperationMode(true) - local writeModeON = (currentop.mode == "Write") + local writeModeOn = (currentop.mode == "Write") local navigateModeOn = (currentop.mode == "Navigate") local insertModeOn = (currentop.mode == "Insert") local replaceModeOn = (currentop.mode == "Replace") local repitchModeOn = (currentop.mode == "Repitch") + local stretchModeOn = false + local stuffModeOn = false - if repitchModeOn then - return repitchBack(track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + if insertModeOn and currentop.use_alt then + insertModeOn = false + stretchModeOn = true end - local fullEraseMode = false + if replaceModeOn and currentop.use_alt then + replaceModeOn = false + stuffModeOn = true + end if navigateModeOn or not take then - if triggered_by_key_event and not AllowKeyEventNavigation() then + if triggered_by_key_event and not S.AllowKeyEventNavigation() then -- Do nothing, not allowed return else - return navigateBack(track) - end - end - - if insertModeOn then - - fullEraseMode = true - - if (#notes_to_shorten > 0) then - - -- Back + Insert + Selective ?? - -- This is a complicated behavior : we want to erase back, but only some notes - -- For the others what to we do ? move them ? if they're after the cursor ? - -- And if they contain the cursor ? - - -- For now, just force a full erase - notes_to_shorten = {} - end - - if triggered_by_key_event and not AllowKeyEventNavigation() then - -- Don't allow erasing when triggered by key - return - end - end - - if replaceModeOn then - fullEraseMode = true - if #notes_to_shorten > 0 then - -- Ignore selective erasing - notes_to_shorten = {} - end - - if triggered_by_key_event and not AllowKeyEventNavigation() then - -- Don't allow erasing when triggered by key - return + return NAVIGATE.NavigateBack(track) end end - local mediaItem = reaper.GetMediaItemTake_Item(take) - - local note_len = resolveNoteLenQN(take); - - local cursorTime = reaper.GetCursorPosition() - local cursorPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorTime) - local cursorQN = reaper.TimeMap2_timeToQN(0, cursorTime) - - local itemStartTime = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") - local itemStartPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemStartTime) - - local rewindTime = reaper.TimeMap2_QNToTime(0, cursorQN - note_len) - - if rewindTime < itemStartTime then - rewindTime = itemStartTime - end - - local rewindPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, rewindTime) - - -- If we're at the start of an item don't move things - if math.abs(itemStartPPQ - cursorPPQ) < PPQ_TOLERANCE then - return - end - - local shcount, remcount, mvcount, addcount, extcount = 0, 0, 0, 0, 0 - - reaper.Undo_BeginBlock(); - - -- Try to extend existing notes - local torem = {} - local tomod = {} - local toadd = {} - - local _, notecnt, _, _ = reaper.MIDI_CountEvts(take); - - local ni = 0; - while (ni < notecnt) do - - -- Examine each note in item - local n = GetNote(take, ni); - - local targetable = false - - if fullEraseMode then - -- steping back with insert mode on makes all notes potentially targetable - targetable = true - else - for _, shnote in pairs(notes_to_shorten) do - if n.chan == shnote.chan and n.pitch == shnote.note then - targetable = true - break - end - end - end - - if targetable then - if noteStartsAfterPPQ(n, cursorPPQ, false) then - -- Note should be moved back or left untouched (if in cursor mode or not) - -- - -- R C - -- | | - -- | | ==== - -- | | - -- - if insertModeOn then - -- Move the note back - setNewNoteBounds(n, take, n.startPPQ, n.endPPQ, -note_len, -note_len) - - torem[#torem+1] = n -- Remove - toadd[#toadd+1] = n -- And readd - - mvcount = mvcount + 1 - end - elseif noteStartsAfterPPQ(n, rewindPPQ, false) then - if noteEndsBeforePPQ(n, cursorPPQ, false) then - -- Note should be suppressed - -- - -- R C - -- | | - -- | === | - -- | | - -- - torem[#torem+1] = n - remcount = remcount + 1 - else - -- The note should be shortened (removing tail). - -- Since its start will change, it should be removed and reinserted (see reaper's API doc) - -- - -- R C - -- | | - -- | ==|=== - -- | | - -- - local offset = (insertModeOn) and (-note_len) or (0) - setNewNoteBounds(n, take, cursorPPQ, n.endPPQ, offset, offset) - - torem[#torem+1] = n - toadd[#toadd+1] = n - - shcount = shcount + 1 - mvcount = mvcount + 1 - end - else - if noteEndsAfterPPQ(n, cursorPPQ, false) then - -- Note should be cut. - -- - -- R C - -- | | - -- ==|=====|=== - -- | | - -- - if insertModeOn or noteEndsOnPPQ(n, cursorPPQ) then - setNewNoteBounds(n, take, n.startPPQ, n.endPPQ, 0, -note_len) - - tomod[#tomod+1] = n - shcount = shcount + 1 - else - -- Create a hole in the note. Copy note - local newn = {} - for k,v in pairs(n) do - newn[k] = v - end - - -- Shorted remaining note - setNewNoteBounds(n, take, n.startPPQ, rewindPPQ, 0, 0); - tomod[#tomod+1] = n - shcount = shcount + 1 - - -- Add new note - setNewNoteBounds(newn, take, cursorPPQ, newn.endPPQ, 0, 0); - toadd[#toadd+1] = newn - addcount = addcount + 1 - end - - elseif noteEndsAfterPPQ(n, rewindPPQ, true) then - -- Note ending should be erased - -- - -- R C - -- | | - -- ==|=== | - -- | | - -- - setNewNoteBounds(n, take, n.startPPQ, rewindPPQ, 0, 0); - tomod[#tomod+1] = n - shcount = shcount + 1 - else - -- Leave untouched - -- - -- R C - -- | | - -- === | | - -- | | - -- - end - end - end - - ni = ni + 1; + if repitchModeOn then + return REPITCH.RepitchBack(currentKeyEventManager(), track, take, notes_to_shorten, triggered_by_key_event) end - -- Modify notes - for ri = 1, #tomod, 1 do - local n = tomod[ri] - reaper.MIDI_SetNote(take, n.index, nil, nil, n.startPPQ, n.endPPQ, nil, nil, nil, true ) + if insertModeOn then + return INSERT.InsertBack(currentKeyEventManager(), track, take, notes_to_shorten, triggered_by_key_event) end - -- Delete notes that were shorten too much - -- Do this in reverse order to be sure that indices are descending - for ri = #torem, 1, -1 do - reaper.MIDI_DeleteNote(take, torem[ri].index) + if writeModeOn then + return WRITE.WriteBack(currentKeyEventManager(), track, take, notes_to_shorten, triggered_by_key_event) end - -- Reinsert moved notes - for ri = 1, #toadd, 1 do - local n = toadd[ri] - reaper.MIDI_InsertNote(take, n.selected, n.muted, n.startPPQ, n.endPPQ, n.chan, n.pitch, n.vel, true ) + if replaceModeOn then + return REPLACE.ReplaceBack(currentKeyEventManager(), track, take, notes_to_shorten, triggered_by_key_event) end - -- Rewind and mark dirty - reaper.MIDI_Sort(take); - reaper.UpdateItemInProject(mediaItem) - reaper.MarkTrackItemsDirty(track, mediaItem) - - local blockRewind = false - local triggeredByBackAction = hasBackwardActionTrigger() - local pedalStart = currentKeyEventManager():keyActivityForTrack(track).pedal.first_ts - - if writeModeON and (not triggeredByBackAction) then - -- We block the rewind in certain conditions (when erasing failed, and when during this pedal session, the erasing was blocked) - local hadCandidates = (#notes_to_shorten > 0) - local failedToErase = (hadCandidates and (shcount+remcount == 0)) - - local cond1 = getSetting("DoNotRewindOnStepBackIfNothingErased") and failedToErase - local cond2 = (not hadCandidates) and (pedalStart == blockRewindRef) - - blockRewind = cond1 or cond2 + if stretchModeOn then + return STRETCH.StretchBack(currentKeyEventManager(), track, take, notes_to_shorten, triggered_by_key_event) end - if blockRewind then - blockRewindRef = pedalStart - else - if fullEraseMode or (not hadCandidates) or (not nothingWasErased) then - reaper.SetEditCurPos(rewindTime, false, false); - if getSetting("AutoScrollArrangeView") then - KeepEditCursorOnScreen() - end - end + if stuffModeOn then + return STUFF.StuffBack(currentKeyEventManager(), track, take, notes_to_shorten, triggered_by_key_event) end - - reaper.Undo_EndBlock(commitDescription(-1, addcount, remcount, shcount, extcount, mvcount),-1); end -- Listen to events from instrumented tracks that have the JSFX companion effect installed (or install it if not present) local function listenToEvents() - local mode = getInputMode(); + local mode = S.getInputMode(); -- Input mode should be engaged - if mode == InputMode.None then + if mode == D.InputMode.None then return; end @@ -1961,11 +198,11 @@ local function listenToEvents() end local track = nil; - local take = TakeForEdition(); + local take = TGT.TakeForEdition(); if not take then - if getSetting("AllowCreateItem") then - track = TrackForEditionIfNoItemFound(); + if S.getSetting("AllowCreateItem") then + track = TGT.TrackForEditionIfNoItemFound(); if not track then return end @@ -1983,7 +220,6 @@ local function listenToEvents() return; end - -- Add helper FX if it is missing local helper_status = helper_lib.getOrInstallHelperFx(track); @@ -1999,7 +235,7 @@ local function listenToEvents() -- Update manager with new info from the helper JSFX manager:updateActivity(track, oss_state); - local spmod = IsStepBackModifierKeyPressed(); + local spmod = MOD.IsStepBackModifierKeyPressed(); local pedal = manager:pullPedalTriggerForTrack(track); manager:tryAdvancedCommitForTrack(track, @@ -2020,14 +256,14 @@ local function listenToEvents() ); -- Allow the use of the action or pedal - if (pedal and not spmod) or hasForwardActionTrigger() then + if (pedal and not spmod) or AT.hasForwardActionTrigger() then manager:simpleCommit(track, function(commit_candidates, held_candidates) commit(track, take, commit_candidates, held_candidates, false); end ); end - if (pedal and spmod) or hasBackwardActionTrigger() then + if (pedal and spmod) or AT.hasBackwardActionTrigger() then manager:simpleCommitBack(track, function(shorten_candidates) commitBack(track, take, shorten_candidates, false) end @@ -2036,57 +272,58 @@ local function listenToEvents() manager:clearOutdatedActivity() - clearAllActionTriggers() + AT.clearAllActionTriggers() - if getSetting("PedalRepeatEnabled") then - manager:forgetPedalTriggerForTrack(track, getSetting("PedalRepeatTime"), getSetting("PedalRepeatFirstHitMultiplier")) + if S.getSetting("PedalRepeatEnabled") then + manager:forgetPedalTriggerForTrack(track, S.getSetting("PedalRepeatTime"), S.getSetting("PedalRepeatFirstHitMultiplier")) end end -- To be called from companion action script -function reaperAction(action_name) - setActionTrigger(action_name) +local function reaperAction(action_name) + AT.setActionTrigger(action_name) end -function cleanupCompanionFXs() +local function cleanupCompanionFXs() reaper.Undo_BeginBlock() helper_lib.cleanupAllTrackFXs(); reaper.Undo_EndBlock("One Small Step - Cleanup companion JSFXs",-1); end -function handlePlaybackMarkerOnExit() - local setting = getSetting("PlaybackMarkerPolicyWhenClosed"); + +local function handleMarkerOnExit(policy_setting_name, marker_name) + local setting = S.getSetting(policy_setting_name); if setting == "Hide/Restore" then -- Need to backup the position -- Save on master track to be project dependent - local id, pos = findPlaybackMarker(); - local masterTrack = reaper.GetMasterTrack(); + local id, pos = MK.findMarker(marker_name) + local masterTrack = reaper.GetMasterTrack() local str = ""; if not (id == nil) then str = tostring(pos) end - reaper.GetSetMediaTrackInfo_String(masterTrack, "P_EXT:OneSmallStep:MarkerBackup", str, true); + reaper.GetSetMediaTrackInfo_String(masterTrack, "P_EXT:OneSmallStep:MarkerBackup:" .. marker_name, str, true) end if (setting == "Hide/Restore") or (setting == "Remove") then - removePlaybackMarker(); + MK.removeMarker(marker_name) end end -function mayRestorePlaybackMarkerOnStart() - local setting = getSetting("PlaybackMarkerPolicyWhenClosed"); +local function mayRestoreMarkerOnStart(policy_setting_name, marker_name) + local setting = S.getSetting("PlaybackMarkerPolicyWhenClosed"); if setting == "Hide/Restore" then - local masterTrack = reaper.GetMasterTrack(); - local succ, str = reaper.GetSetMediaTrackInfo_String(masterTrack, "P_EXT:OneSmallStep:MarkerBackup", '', false); + local masterTrack = reaper.GetMasterTrack() + local succ, str = reaper.GetSetMediaTrackInfo_String(masterTrack, "P_EXT:OneSmallStep:MarkerBackup:" .. marker_name, '', false); if succ and str ~= "" then - setPlaybackMarkerAtPos(tonumber(str)); + MK.setMarkerAtPos(marker_name, tonumber(str)) end end end -function atStart() +local function atStart() -- Do some cleanup at engine start -- But this adds an undo entry point ... -- So rely on the user instead to cleanup the JSFXs using the relevant action @@ -2094,84 +331,40 @@ function atStart() -- Then we can uncomment this automatic cleanup -- cleanupCompanionFXs(); - clearAllActionTriggers() - mayRestorePlaybackMarkerOnStart() + AT.clearAllActionTriggers() + mayRestoreMarkerOnStart("PlaybackMarkerPolicyWhenClosed", MK.PLAYBACK_MARKER) + mayRestoreMarkerOnStart("OperationMarkerPolicyWhenClosed", MK.OPERATION_MARKER) end -function atExit() +local function atExit() -- See comment in atStart - if getSetting("CleanupJsfxAtClosing") then + if S.getSetting("CleanupJsfxAtClosing") then cleanupCompanionFXs(); end - handlePlaybackMarkerOnExit(); + handleMarkerOnExit("PlaybackMarkerPolicyWhenClosed", MK.PLAYBACK_MARKER) + handleMarkerOnExit("OperationMarkerPolicyWhenClosed", MK.OPERATION_MARKER) end -function atLoop() +local function atLoop() return listenToEvents(); end -EngineLib = { - - IsStepBackModifierKeyPressed = IsStepBackModifierKeyPressed, - resolveOperationMode = resolveOperationMode, - - -- Enums - InputMode = InputMode, - EditMode = EditMode, - NoteLenParamSource = NoteLenParamSource, - NoteLenModifier = NoteLenModifier, - - ModifierKeys = ModifierKeys, - ModifierKeyLookup = ModifierKeyLookup, - ModifierKeyCombinations = ModifierKeyCombinations, - ModifierKeyCombinationLookup = ModifierKeyCombinationLookup, - - NoteLenDefs = NoteLenDefs, - - setInputMode = setInputMode, - getInputMode = getInputMode, - - setNoteLenParamSource = setNoteLenParamSource, - getNoteLenParamSource = getNoteLenParamSource, - - setPlaybackMeasureCount = setPlaybackMeasureCount, - getPlaybackMeasureCount = getPlaybackMeasureCount, - - setTupletDivision = setTupletDivision, - getTupletDivision = getTupletDivision, - - getNoteLenModifier = getNoteLenModifier, - setNoteLenModifier = setNoteLenModifier, - getNoteLenModifierFactor = getNoteLenModifierFactor, - - setNoteLen = setNoteLen, - getNoteLen = getNoteLen, - getNoteLenQN = getNoteLenQN, - - increaseNoteLen = increaseNoteLen, - decreaseNoteLen = decreaseNoteLen, - - findPlaybackMarker = findPlaybackMarker, - setPlaybackMarkerAtCurrentPos = setPlaybackMarkerAtCurrentPos, +return { + S = S, + D = D, + AT = AT, + MK = MK, + T = T, + TGT = TGT, + F = F, + MOD = MOD, + ED = ED, atStart = atStart, atExit = atExit, atLoop = atLoop, - getSetting = getSetting, - setSetting = setSetting, - resetSetting = resetSetting, - getSettingSpec = getSettingSpec, - reaperAction = reaperAction, - - TakeForEdition = TakeForEdition, - TrackForEditionIfNoItemFound = TrackForEditionIfNoItemFound, - - TrackFocus = TrackFocus, - RestoreFocus = RestoreFocus, } - -return EngineLib; \ No newline at end of file diff --git a/MIDI Editor/talagan_OneSmallStep/classes/helper_lib.lua b/MIDI Editor/talagan_OneSmallStep/classes/helper_lib.lua index 90335ddfe..9ffeadf08 100644 --- a/MIDI Editor/talagan_OneSmallStep/classes/helper_lib.lua +++ b/MIDI Editor/talagan_OneSmallStep/classes/helper_lib.lua @@ -55,7 +55,7 @@ local function removeInputFx(track, fx) -- Use 0x1000000 as flag for input fx chain idx = idx|0x1000000; - res = reaper.TrackFX_Delete(track, idx); + return reaper.TrackFX_Delete(track, idx); end @@ -95,7 +95,7 @@ local function oneSmallStepState(track) end -- Make sure helper is installed - local iHelper = getOrAddInputFx(track, jsfx, true) + local iHelper = getOrAddInputFx(track, jsfx) local pitches = {} local pedalActivity = reaper.TrackFX_GetParam(track, iHelper, jsfx.paramIndex_PedalActivity); @@ -104,9 +104,9 @@ local function oneSmallStepState(track) for i = 1, heldNoteCount, 1 do -- now the plugin updates its sliders to give us the values for this index. local evt = { - note = reaper.TrackFX_GetParam(track, iHelper, jsfx.paramIndex_NoteStart + 4*(i-1) + 0), + pitch = reaper.TrackFX_GetParam(track, iHelper, jsfx.paramIndex_NoteStart + 4*(i-1) + 0), chan = reaper.TrackFX_GetParam(track, iHelper, jsfx.paramIndex_NoteStart + 4*(i-1) + 1), - velocity = reaper.TrackFX_GetParam(track, iHelper, jsfx.paramIndex_NoteStart + 4*(i-1) + 2), + vel = reaper.TrackFX_GetParam(track, iHelper, jsfx.paramIndex_NoteStart + 4*(i-1) + 2), timestamp = reaper.TrackFX_GetParam(track, iHelper, jsfx.paramIndex_NoteStart + 4*(i-1) + 3) } @@ -124,4 +124,4 @@ return { removeHelperFx = removeHelperFx, cleanupAllTrackFXs = cleanupAllTrackFXs, oneSmallStepState = oneSmallStepState -}; +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/KeyActivityManager.lua b/MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyActivityManager.lua similarity index 95% rename from MIDI Editor/talagan_OneSmallStep/classes/KeyActivityManager.lua rename to MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyActivityManager.lua index 89ba698b8..0f7fc69ed 100644 --- a/MIDI Editor/talagan_OneSmallStep/classes/KeyActivityManager.lua +++ b/MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyActivityManager.lua @@ -9,7 +9,7 @@ KeyActivityManager = { activity = {} }; function KeyActivityManager:new() - local o = o or {} + local o = {} setmetatable(o, self) self.__index = self self.activity = {}; @@ -17,8 +17,8 @@ function KeyActivityManager:new() end function KeyActivityManager:dbgNote(v) - reaper.ShowConsoleMsg(" - " .. tostring(math.floor(v.chan+0.5)) .. "," .. tostring(math.floor(v.note+0.5)) .. " " .. - "(vel: " .. v.velocity .. ") " .. + reaper.ShowConsoleMsg(" - " .. tostring(math.floor(v.chan+0.5)) .. "," .. tostring(math.floor(v.pitch + 0.5)) .. " " .. + "(vel: " .. v.vel .. ") " .. "(committed : " .. ((v.committed == nil) and 'nil' or 'true') .. ") " .. "(released : " .. ((v.released == nil) and 'nil' or 'true') .. ") " .. "(ts : " .. (v.first_ts or 'nil') .. ") " .. @@ -117,7 +117,7 @@ function KeyActivityManager:updateActivity(track, oss_state) goto continue end - local k = tostring(math.floor(v.chan+0.5)) .. "," .. tostring(math.floor(v.note+0.5)) + local k = tostring(math.floor(v.chan + 0.5)) .. "," .. tostring(math.floor(v.pitch + 0.5)) note_activity[k] = note_activity[k] or {}; @@ -128,9 +128,9 @@ function KeyActivityManager:updateActivity(track, oss_state) note_activity[k].released = nil; end - note_activity[k].note = v.note; + note_activity[k].pitch = v.pitch; note_activity[k].chan = v.chan; - note_activity[k].velocity = v.velocity; + note_activity[k].vel = v.vel; note_activity[k].first_ts = v.timestamp; note_activity[k].latest_ts = t; diff --git a/MIDI Editor/talagan_OneSmallStep/classes/KeyPressActivityManager.lua b/MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyPressActivityManager.lua similarity index 84% rename from MIDI Editor/talagan_OneSmallStep/classes/KeyPressActivityManager.lua rename to MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyPressActivityManager.lua index e0928cd72..761dff48b 100644 --- a/MIDI Editor/talagan_OneSmallStep/classes/KeyPressActivityManager.lua +++ b/MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyPressActivityManager.lua @@ -3,17 +3,18 @@ -- @license MIT -- @description This is part of One Small Step -local KeyActivityManager = require "KeyActivityManager"; +local KeyActivityManager = require "input_managers/KeyActivityManager"; +local S = require "modules/settings" -- Inherit from generic KeyActivityManager KeyPressActivityManager = KeyActivityManager:new(); function KeyPressActivityManager:aggregation() - return EngineLib.getSetting("KeyPressModeAggregationTime"); + return S.getSetting("KeyPressModeAggregationTime"); end function KeyPressActivityManager:inertia() - return EngineLib.getSetting("KeyPressModeInertiaTime"); + return S.getSetting("KeyPressModeInertiaTime"); end function KeyPressActivityManager:tryAdvancedCommitForTrack(track, commit_callback) @@ -37,7 +38,7 @@ function KeyPressActivityManager:tryAdvancedCommitForTrack(track, commit_callbac end end - if EngineLib.getSetting("KeyPressModeInertiaEnabled") and (v.committed == true) and (time - v.first_ts > self:inertia()) then + if S.getSetting("KeyPressModeInertiaEnabled") and (v.committed == true) and (time - v.first_ts > self:inertia()) then held_candidates[#held_candidates+1] = v; end end diff --git a/MIDI Editor/talagan_OneSmallStep/classes/KeyReleaseActivityManager.lua b/MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyReleaseActivityManager.lua similarity index 90% rename from MIDI Editor/talagan_OneSmallStep/classes/KeyReleaseActivityManager.lua rename to MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyReleaseActivityManager.lua index ae196b586..e00ef99ca 100644 --- a/MIDI Editor/talagan_OneSmallStep/classes/KeyReleaseActivityManager.lua +++ b/MIDI Editor/talagan_OneSmallStep/classes/input_managers/KeyReleaseActivityManager.lua @@ -3,13 +3,14 @@ -- @license MIT -- @description This is part of One Small Step -local KeyActivityManager = require "KeyActivityManager"; +local KeyActivityManager = require "input_managers/KeyActivityManager"; +local S = require "modules/settings" -- Inherit from generic KeyActivityManager KeyReleaseActivityManager = KeyActivityManager:new(); function KeyReleaseActivityManager:inertia() - return EngineLib.getSetting("KeyReleaseModeForgetTime"); + return S.getSetting("KeyReleaseModeForgetTime"); end -- We need to override the default cleanup because we want to keep track of diff --git a/MIDI Editor/talagan_OneSmallStep/classes/lib/MIDIUtils.lua b/MIDI Editor/talagan_OneSmallStep/classes/lib/MIDIUtils.lua index 246a7960a..8c7a33608 100644 --- a/MIDI Editor/talagan_OneSmallStep/classes/lib/MIDIUtils.lua +++ b/MIDI Editor/talagan_OneSmallStep/classes/lib/MIDIUtils.lua @@ -1,7 +1,7 @@ -- @noindex -- @license MIT -- @description MIDI Utils API --- @version 0.1.13 +-- @version 0.1.18 -- @author sockmonkey72 -- @about -- # MIDI Utils API @@ -770,11 +770,18 @@ local function MIDI_CommitWriteTransaction(take, refresh, dirty) local newMIDIString = '' local lastPPQPos = 0 - local comparator = function(t, a, b) - return ( (t[a].ppqpos == t[b].ppqpos) and (t[a]:type() == NOTEOFF_TYPE) ) or (t[a].ppqpos < t[b].ppqpos) + -- iterate sorted to avoid (REAPER Inline MIDI Editor) problems with offset calculation + local comparator = function(t, a, b) -- thanks Talagan (Ben Babut) for this improvement + if (t[a].ppqpos == t[b].ppqpos) then + local aprio = (t[a]:type() == NOTEOFF_TYPE) and 0 or 1 + local bprio = (t[b]:type() == NOTEOFF_TYPE) and 0 or 1 + + return aprio < bprio + else + return (t[a].ppqpos < t[b].ppqpos) + end end - -- iterate sorted to avoid (REAPER Inline MIDI Editor) problems with offset calculation for _, event in spairs(MIDIEvents, comparator) do event.offset = math.floor(event.ppqpos - lastPPQPos) lastPPQPos = event.ppqpos @@ -1989,4 +1996,4 @@ end ----------------------------------------------------------------------------- ----------------------------------- EXPORT ---------------------------------- -return MIDIUtils +return MIDIUtils \ No newline at end of file diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/action_triggers.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/action_triggers.lua new file mode 100644 index 000000000..24a8b1d99 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/action_triggers.lua @@ -0,0 +1,64 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local D = require "modules/defines" +local S = require "modules/settings" + +local function validateActionTrigger(action_name) + if D.ActionTriggers[action_name] == nil then + error("Trying to use unknown action trigger :" .. action_name) + end +end + +local function setActionTrigger(action_name) + validateActionTrigger(action_name) + reaper.SetExtState("OneSmallStep", action_name .. "ActionTrigger", tostring(reaper.time_precise()), false) +end +local function getActionTrigger(action_name) + validateActionTrigger(action_name) + return tonumber(reaper.GetExtState("OneSmallStep", action_name .. "ActionTrigger")) +end +local function clearActionTrigger(action_name) + validateActionTrigger(action_name) + reaper.DeleteExtState("OneSmallStep", action_name .. "ActionTrigger", true) +end + +local function clearAllActionTriggers() + for k,v in pairs(D.ActionTriggers) do + clearActionTrigger(k) + end +end + +local function hasActionTrigger(forward) + local res = false + for k,v in pairs(D.ActionTriggers) do + local cond = false + if forward then + cond = not v.back + else + cond = v.back + end + if cond then + res = (res or (getActionTrigger(k) ~= nil)) + end + end + return res +end +local function hasForwardActionTrigger() + return hasActionTrigger(true) +end +local function hasBackwardActionTrigger() + return hasActionTrigger(false) +end + +return { + setActionTrigger = setActionTrigger, + getActionTrigger = getActionTrigger, + clearActionTrigger = clearActionTrigger, + clearAllActionTriggers = clearAllActionTriggers, + hasActionTrigger = hasActionTrigger, + hasForwardActionTrigger = hasForwardActionTrigger, + hasBackwardActionTrigger = hasBackwardActionTrigger +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/debugger.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/debugger.lua new file mode 100644 index 000000000..990f546bc --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/debugger.lua @@ -0,0 +1,47 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local S = require "modules/settings" + +local function LaunchDebugStubIfNeeded() + if not S.getSetting("UseDebugger") then + return + end + + local mav_repo = reaper.GetResourcePath() .. '/Scripts/Mavriq ReaScript Repository/Various/' + + package.cpath = package.cpath .. ';' .. mav_repo .. 'Mavriq-Lua-Sockets/?.dll' .. ';' .. mav_repo .. 'Mavriq-Lua-Sockets/?.so' + package.path = package.path .. ';' .. mav_repo .. 'Debugging/?.lua' .. ';' .. mav_repo .. 'Mavriq-Lua-Sockets/?.lua' + + -- Try to load mobedebug + local succ, mobdebug = pcall(require, "mobdebug") + + if not succ then + reaper.ShowConsoleMsg("Warning : Launched in Debugger mode, but the debug stub is not installed.\n\z + You need to install Mavriq Lua Sockets. And, to debug in Visual Studio Code, Lua MobDebug adapter.\n\z + Continuing without debugger.\n\z + To turn the Debugger mode off and remove this message, launch the 'OneSmallStep toggle debugger' action.\n\z") + return + end + + function ErrorHandler(err) + reaper.ShowConsoleMsg(err .. '\n' .. debug.traceback()) + mobdebug.pause() + end + + -- We override Reaper's defer method for two reasons : + -- We want the full trace on errors + -- We want the debugger to pause on errors + local rdefer = reaper.defer + reaper.defer = function(c) + return rdefer(function() xpcall(c, ErrorHandler) end) + end + + mobdebug.start() + end + + return { + LaunchDebugStubIfNeeded = LaunchDebugStubIfNeeded + } diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/defines.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/defines.lua new file mode 100644 index 000000000..f8f9a589f --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/defines.lua @@ -0,0 +1,142 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +-- Tolerance to detect if events match +local TIME_TOLERANCE = 0.001 +local QN_TOLERANCE = 0.01 +local PPQ_TOLERANCE = 1 + +local IsMacOs = (reaper.GetOS():find('OSX') ~= nil); + +local NoteLenDefs = { + { id = "1", next = "1", prec = "1_2", frac = "4" , qn = 4 }, + { id = "1_2", next = "1", prec = "1_4", frac = "2" , qn = 2 }, + { id = "1_4", next = "1_2", prec = "1_8", frac = "1" , qn = 1 }, + { id = "1_8", next = "1_4", prec = "1_16", frac = "1_2" , qn = 0.5 }, + { id = "1_16", next = "1_8", prec = "1_32", frac = "1_4" , qn = 0.25 }, + { id = "1_32", next = "1_16", prec = "1_64", frac = "1_8" , qn = 0.125 }, + { id = "1_64", next = "1_32", prec = "1_64", frac = "1_16", qn = 0.0625 } +}; + +local NoteLenParamSource = { + OSS = 0, + ProjectGrid = 1, + ItemConf = 2 +} + +local InputMode = { + None = 0, + Punch = 1, + KeyboardRelease = 2, + Action = 3, -- Removed, merged with pedal + KeyboardPress = 4 +} + +local NoteLenModifier = { + Straight = 0, + Dotted = 1, + Triplet = 2, + Tuplet = 3, + Modified = 4 +} + +local EditMode = { + Write = "Write", + Navigate = "Navigate", + Insert = "Insert", + Repitch = "Repitch", + Replace = "Replace", + Stretch = "Stretch", + Stuff = "Stuff" +} + +local ActionTriggers = { + Commit = { action = "Commit", back = false }, + CommitBack = { action = "Commit", back = true }, + + Write = { action = "Write", back = false }, + Navigate = { action = "Navigate", back = false }, + Insert = { action = "Insert", back = false, markerAlternate = "Stretch" }, + Replace = { action = "Replace", back = false, markerAlternate = "Stuff" }, + Repitch = { action = "Repitch", back = false }, + Stretch = { action = "Stretch", back = false }, + Stuff = { action = "Stuff", back = false }, + + WriteBack = { action = "Write", back = true }, + NavigateBack = { action = "Navigate", back = true }, + InsertBack = { action = "Insert", back = true, markerAlternate = "StretchBack" }, + ReplaceBack = { action = "Replace", back = true, markerAlternate = "StuffBack" }, + RepitchBack = { action = "Repitch", back = true }, + StretchBack = { action = "Stretch", back = true }, + StuffBack = { action = "Unstuff", back = true } +} + +local MacOSModifierKeys = { + { vkey = 16, name = 'Shift' }, + { vkey = 17, name = 'Cmd' }, + { vkey = 18, name = 'Opt' }, + { vkey = 91, name = 'Ctrl' } +}; + +local OtherOSModifierKeys = { + { vkey = 16, name = 'Shift' }, + { vkey = 17, name = 'Ctrl' }, + { vkey = 18, name = 'Alt' } +}; + +local ModifierKeys = IsMacOs and MacOSModifierKeys or OtherOSModifierKeys; + +local NoteLenLookup = {}; +for i,v in ipairs(NoteLenDefs) do + NoteLenLookup[v.id] = v; +end + +local ModifierKeyLookup = {}; +for i,v in ipairs(ModifierKeys) do + ModifierKeyLookup[v.vkey] = v; +end + +local ModifierKeyCombinations = {{ label = "None", id = "none", vkeys = {} }} +for i=1, #ModifierKeys do + local m1 = ModifierKeys[i] + ModifierKeyCombinations[#ModifierKeyCombinations+1] = { label = m1.name, id = "" .. m1.vkey, vkeys = { m1.vkey } } +end +for i=1, #ModifierKeys do + local m1 = ModifierKeys[i] + for j=i+1, #ModifierKeys do + local m2 = ModifierKeys[j] + ModifierKeyCombinations[#ModifierKeyCombinations+1] = { label = m1.name .. "+" .. m2.name, id = "" .. m1.vkey .. "+" .. m2.vkey, vkeys = { m1.vkey, m2.vkey } } + end +end + +local ModifierKeyCombinationLookup = {}; +for i,v in ipairs(ModifierKeyCombinations) do + ModifierKeyCombinationLookup[v.id] = v; +end + +return { + TIME_TOLERANCE = TIME_TOLERANCE, + PPQ_TOLERANCE = PPQ_TOLERANCE, + QN_TOLERANCE = QN_TOLERANCE, + + InputMode = InputMode, + EditMode = EditMode, + IsMacOs = IsMacOs, + + ActionTriggers = ActionTriggers, + + NoteLenDefs = NoteLenDefs, + NoteLenLookup = NoteLenLookup, + + NoteLenParamSource = NoteLenParamSource, + NoteLenModifier = NoteLenModifier, + + ModifierKeys = ModifierKeys, + ModifierKeyLookup = ModifierKeyLookup, + + ModifierKeyCombinations = ModifierKeyCombinations, + ModifierKeyCombinationLookup = ModifierKeyCombinationLookup +} + diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/edition.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/edition.lua new file mode 100644 index 000000000..53524ad95 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/edition.lua @@ -0,0 +1,82 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local D = require "modules/defines" +local S = require "modules/settings" +local MK = require "modules/markers" +local AT = require "modules/action_triggers" +local MOD = require "modules/modifiers" + +local function ResolveOperationMode(look_for_action_triggers) + local bk = MOD.IsStepBackModifierKeyPressed() + local mode = S.getSetting("EditMode") + local triggered_by_action = false + + local editModes = { + { name = "Write", prio = 4 }, + { name = "Navigate", prio = 3 }, + { name = "Insert", prio = 2 }, + { name = "Repitch", prio = 2.5 }, + { name = "Replace", prio = 1 }, + } + + -- List of modes that are active through modifier keys + local activemodes = {} + for k, editmode in ipairs(editModes) do + local setting = S.getSetting(editmode.name .. "ModifierKeyCombination") + local combi = D.ModifierKeyCombinationLookup[setting] + local pressed = MOD.IsModifierKeyCombinationPressed(combi.id) + if pressed then + activemodes[#activemodes + 1] = { mode = editmode.name, combi = combi, prio = editmode.prio } + end + end + + -- Sort modes by priority + table.sort(activemodes, function(e1,e2) + local l1 = #e1.combi.vkeys + local l2 = #e2.combi.vkeys + + if l1 == l2 then + return e1.prio < e2.prio + end + + return l1 > l2; + end) + + if #activemodes > 0 then + mode = activemodes[1].mode + end + + if look_for_action_triggers then + for k, v in pairs(D.ActionTriggers) do + local has_triggered = AT.getActionTrigger(k) + if has_triggered then + if v.action ~= "Commit" then + -- If it's a commit, use current mode, else use the mode linked to the trigger + mode = v.action + end + triggered_by_action = true + break + end + end + end + + -- Finally, if the op marker is set, override the mode if needed + local use_alt = nil + if D.ActionTriggers[mode].markerAlternate and MK.findOperationMarker() then + use_alt = true + end + + return { + mode = mode, + alternate = D.ActionTriggers[mode].markerAlternate, + back = bk, + use_alt = use_alt + } +end + +return { + ResolveOperationMode = ResolveOperationMode +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/focus.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/focus.lua new file mode 100644 index 000000000..727904250 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/focus.lua @@ -0,0 +1,59 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local lastKnownFocus = {}; + +local function IsActiveMidiEditorFocused() + local me = reaper.MIDIEditor_GetActive() + local f = reaper.JS_Window_GetFocus(); + while f do + if f == me then + return true + end + f = reaper.JS_Window_GetParent(f); + end + return false +end + +local function IsArrangeViewFocused() + return (reaper.GetCursorContext() >= 0); +end + + +local function TrackFocus() + if IsActiveMidiEditorFocused() then + lastKnownFocus = { element = 'MIDIEditor' } + elseif IsArrangeViewFocused() then + lastKnownFocus = { element = 'ArrangeView', context = reaper.GetCursorContext() } + else + -- Simply ignore, we don't want to give back focus to this + end +end + +local function RestoreFocus() + + local hwnd = reaper.GetMainHwnd(); + reaper.JS_Window_SetFocus(hwnd); + + if lastKnownFocus.element == 'MIDIEditor' then + reaper.JS_Window_SetFocus(reaper.MIDIEditor_GetActive()); + elseif lastKnownFocus.element == 'ArrangeView' then + reaper.SetCursorContext(lastKnownFocus.context) + else + -- We don't know how to restore focus in a better way + end +end + +local function LastKnownFocus() + return lastKnownFocus +end + +return { + IsActiveMidiEditorFocused = IsActiveMidiEditorFocused, + IsArrangeViewFocused = IsArrangeViewFocused, + TrackFocus = TrackFocus, + RestoreFocus = RestoreFocus, + LastKnownFocus = LastKnownFocus +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/markers.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/markers.lua new file mode 100644 index 000000000..e85a374fb --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/markers.lua @@ -0,0 +1,104 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local D = require "modules/defines" + +local PLAYBACK_MARKER = "OSS Playback" +local OPERATION_MARKER = "OSS OP Start" + +local function markerColor(marker_name) + if marker_name == PLAYBACK_MARKER then + return 0x00C000 + elseif marker_name == OPERATION_MARKER then + return 0x4080FF + end + + return 0xFFFFFF +end + +local function findMarker(marker_name) + local mc = reaper.CountProjectMarkers(0); + for i=0, mc, 1 do + local retval, isrgn, pos, rgnend, name, markrgnindexnumber, color = reaper.EnumProjectMarkers3(0, i); + if name == marker_name then + return i, pos + end + end + return nil +end + +local function setMarkerAtPos(marker_name, pos) + local id, mpos = findMarker(marker_name) + local color = markerColor(marker_name) + + reaper.Undo_BeginBlock() + if not (id == nil) then + reaper.DeleteProjectMarkerByIndex(0, id); + end + + if (mpos == nil) or math.abs(pos - mpos) > D.TIME_TOLERANCE then + reaper.AddProjectMarker2(0, false, pos, 0, marker_name, -1, reaper.ColorToNative( (color & 0xFF0000) >> 16, (color & 0x00FF00) >> 8, color & 0xFF)|0x1000000); + end + reaper.Undo_EndBlock("One Small Step - Set ".. marker_name .." marker", -1); +end + +local function setMarkerAtCurrentPos(marker_name) + setMarkerAtPos(marker_name, reaper.GetCursorPosition()) +end + +local function removeMarker(marker_name) + reaper.Undo_BeginBlock(); + local id, mpos = findMarker(marker_name) + if not (id == nil) then + reaper.DeleteProjectMarkerByIndex(0, id) + end + reaper.Undo_EndBlock("One Small Step - Remove " .. marker_name .. " marker", -1); +end + +local function findPlaybackMarker() + return findMarker(PLAYBACK_MARKER) +end + +local function setPlaybackMarkerAtPos(pos) + return setMarkerAtPos(PLAYBACK_MARKER, pos) +end +local function setPlaybackMarkerAtCurrentPos() + return setMarkerAtCurrentPos(PLAYBACK_MARKER) +end +local function removePlaybackMarker() + return removeMarker(PLAYBACK_MARKER) +end + +local function findOperationMarker() + return findMarker(OPERATION_MARKER) +end +local function setOperationMarkerAtPos(pos) + return setMarkerAtPos(OPERATION_MARKER, pos) +end +local function setOperationMarkerAtCurrentPos() + return setMarkerAtCurrentPos(OPERATION_MARKER) +end +local function removeOperationMarker() + return removeMarker(OPERATION_MARKER) +end + +return { + PLAYBACK_MARKER = PLAYBACK_MARKER, + OPERATION_MARKER = OPERATION_MARKER, + + findMarker = findMarker, + setMarkerAtPos = setMarkerAtPos, + removeMarker = removeMarker, + + findPlaybackMarker = findPlaybackMarker, + setPlaybackMarkerAtPos = setPlaybackMarkerAtPos, + setPlaybackMarkerAtCurrentPos = setPlaybackMarkerAtCurrentPos, + removePlaybackMarker = removePlaybackMarker, + + findOperationMarker = findOperationMarker, + setOperationMarkerAtPos = setOperationMarkerAtPos, + setOperationMarkerAtCurrentPos = setOperationMarkerAtCurrentPos, + removeOperationMarker = removeOperationMarker +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/modifiers.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/modifiers.lua new file mode 100644 index 000000000..7d862651d --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/modifiers.lua @@ -0,0 +1,48 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local D = require "modules/defines" +local S = require "modules/settings" + +local launchTime = reaper.time_precise() + +local function validateModifierKeyCombination(id) + if D.ModifierKeyCombinationLookup[id] == nil then + error("Trying to use unknown modifier key combination with id " .. id) + end +end + +-- Returns the state of the modifier key linked to the function "function_name" +local function IsModifierKeyCombinationPressed(id) + validateModifierKeyCombination(id) + + if id == "none" then + return false + end + + -- Avoid inconsistencies and only follow events during the lifetime of the plugin, so use launchTime + -- This will prevent bugs from a session to another (when for example the plugin crashes) + local keys = reaper.JS_VKeys_GetState(launchTime); + local combi = D.ModifierKeyCombinationLookup[id] + + for k, v in ipairs(combi.vkeys) do + local c1 = keys:byte(v); + if not (c1 ==1) then + return false + end + end + + return true +end + +local function IsStepBackModifierKeyPressed() + local keys = reaper.JS_VKeys_GetState(launchTime); + return (keys:byte(S.getSetting("StepBackModifierKey")) == 1) +end + +return { + IsStepBackModifierKeyPressed = IsStepBackModifierKeyPressed, + IsModifierKeyCombinationPressed = IsModifierKeyCombinationPressed +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/notes.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/notes.lua new file mode 100644 index 000000000..181579558 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/notes.lua @@ -0,0 +1,71 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local MU = require "lib/MIDIUtils" +local T = require "modules/time" +local S = require "modules/settings" + +-- Read/Manipulate notes as objects + +local function GetNote(take, ni, use_mu) + + local selected, muted, startPPQ, endPPQ, chan, pitch, vel, offvel + + if use_mu then + _, selected, muted, startPPQ, endPPQ, chan, pitch, vel, offvel = MU.MIDI_GetNote(take, ni) + else + _, selected, muted, startPPQ, endPPQ, chan, pitch, vel = reaper.MIDI_GetNote(take, ni) + offvel = 0 + end + + return { + index = ni, + selected = selected, + muted = muted, + pitch = pitch, + chan = chan, + vel = vel, + startPPQ = startPPQ, + startQN = reaper.MIDI_GetProjQNFromPPQPos(take, startPPQ), + endPPQ = endPPQ, + endQN = reaper.MIDI_GetProjQNFromPPQPos(take, endPPQ), + offvel = offvel + } +end + +local function SetNewNoteBounds(note, take, startPPQ, endPPQ) + note.startPPQ = T.PPQRound(startPPQ) + note.endPPQ = T.PPQRound(endPPQ) + note.startQN = reaper.MIDI_GetProjQNFromPPQPos(take, note.startPPQ) + note.endQN = reaper.MIDI_GetProjQNFromPPQPos(take, note.endPPQ) +end + +local function CountEvts(take, use_mu) + if use_mu then + return MU.MIDI_CountEvts(take) + else + return reaper.MIDI_CountEvts(take) + end +end + +local function BuildFromManager(note_from_manager, take, startPPQ, endPPQ) + local n = { + index = nil, + selected = S.getSetting("SelectInputNotes"), + muted = false, + chan = note_from_manager.chan, + pitch = note_from_manager.pitch, + vel = note_from_manager.vel + } + SetNewNoteBounds(n, take, startPPQ, endPPQ) + return n +end + +return { + GetNote = GetNote, + SetNewNoteBounds = SetNewNoteBounds, + CountEvts = CountEvts, + BuildFromManager = BuildFromManager +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/settings.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/settings.lua new file mode 100644 index 000000000..0a299fef1 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/settings.lua @@ -0,0 +1,244 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local D = require "modules/defines" + +local SettingDefs = { + StepBackModifierKey = { type = "int", default = D.IsMacOs and 16 or 16 }, + + WriteModifierKeyCombination = { type = "string", default = "none" }, + InsertModifierKeyCombination = { type = "string", default = D.IsMacos and "17" or "17" }, + NavigateModifierKeyCombination = { type = "string", default = D.IsMacos and "18" or "18" }, + ReplaceModifierKeyCombination = { type = "string", default = D.IsMacos and "17+18" or "17+18" }, + RepitchModifierKeyCombination = { type = "string", default = "none" }, + + HideEditModeMiniBar = { type = "bool", default = false }, + + Mode = { type = "int", default = D.InputMode.KeyboardRelease }, + EditMode = { type = "string", default = D.EditMode.Write }, + + PlaybackMeasureCount = { type = "int", default = -1 }, -- -1 is marker mode + NoteLenParamSource = { type = "int", default = D.NoteLenParamSource.OSS }, + NoteLenFactorDenominator = { type = "int", default = 1 }, + NoteLenFactorNumerator = { type = "int", default = 1 }, + TupletDivision = { type = "int", default = 4 }, + NoteLen = { type = "string", default = "1_4"}, + NoteLenModifier = { type = "int", default = D.NoteLenModifier.Straight }, + + PlaybackMarkerPolicyWhenClosed = { type = "string", default = "Keep visible" }, + OperationMarkerPolicyWhenClosed = { type = "string", default = "Keep visible" }, + + AllowTargetingFocusedMidiEditors = { type = "bool", default = true }, + AllowTargetingNonSelectedItemsUnderCursor = { type = "bool", default = false }, + AllowCreateItem = { type = "bool", default = false }, + + DoNotRewindOnStepBackIfNothingErased = { type = "bool", default = true}, + CleanupJsfxAtClosing = { type = "bool", default = true}, + SelectInputNotes = { type = "bool", default = true}, + + KeyPressModeAggregationTime = { type = "double", default = 0.05, min = 0, max = 0.1 }, + KeyPressModeInertiaTime = { type = "double", default = 0.5, min = 0.2, max = 1.0 }, + KeyPressModeInertiaEnabled = { type = "bool", default = true}, + + KeyReleaseModeForgetTime = { type = "double", default = 0.200, min = 0.05, max = 0.4}, + + RepitchModeAggregationTime = { type = "double", default = 0.05, min = 0, max = 0.1 }, + RepitchModeAffects = { type = "string", default = "Pitches Only", inclusion = { "Pitches only", "Velocities only", "Pitches + Velocities" } }, + + PedalRepeatEnabled = { type = "bool" , default = true }, + PedalRepeatTime = { type = "double", default = 0.200, min = 0.05, max = 0.5 }, + PedalRepeatFirstHitMultiplier = { type = "int", default = 4, min = 1, max = 10 }, + + Snap = { type = "bool", default = false }, + SnapNotes = { type = "bool", default = true }, + SnapProjectGrid = { type = "bool", default = true }, + SnapItemGrid = { type = "bool", default = true }, + SnapItemBounds = { type = "bool", default = true }, + + AutoScrollArrangeView = { type = "bool", default = true }, + + AllowKeyEventNavigation = { type = "bool", default = false }, + + UseDebugger = { type = "bool", default = false } +}; + + +local function unsafestr(str) + if str == "" then + return nil + end + return str +end + +local function getSetting(setting) + local spec = SettingDefs[setting]; + + if spec == nil then + error("Trying to get unknown setting " .. setting); + end + + local val = unsafestr(reaper.GetExtState("OneSmallStep", setting)); + + if val == nil then + val = spec.default; + else + if spec.type == 'bool' then + val = (val == "true"); + elseif spec.type == 'int' then + val = tonumber(val); + elseif spec.type == 'double' then + val = tonumber(val); + elseif spec.type == 'string' then + -- No conversion needed + end + end + return val; +end +local function setSetting(setting, val) + local spec = SettingDefs[setting]; + + if spec == nil then + error("Trying to set unknown setting " .. setting); + end + + if val == nil then + reaper.DeleteExtState("OneSmallStep", setting, true); + else + if spec.type == 'bool' then + val = (val == true) and "true" or "false"; + elseif spec.type == 'int' then + val = tostring(val); + elseif spec.type == 'double' then + val = tostring(val); + elseif spec.type == "string" then + -- No conversion needed + end + reaper.SetExtState("OneSmallStep", setting, val, true); + end +end +local function resetSetting(setting) + setSetting(setting, SettingDefs[setting].default) +end +local function getSettingSpec(setting) + return SettingDefs[setting] +end + +local function setPlaybackMeasureCount(c) return setSetting("PlaybackMeasureCount", c) end +local function getPlaybackMeasureCount() return getSetting("PlaybackMeasureCount") end + +local function setInputMode(m) return setSetting("Mode", m) end +local function getInputMode() return getSetting("Mode") end + +local function setNoteLenParamSource(m) return setSetting("NoteLenParamSource", m) end +local function getNoteLenParamSource() return getSetting("NoteLenParamSource") end + +local function setTupletDivision(m) return setSetting("TupletDivision", m) end +local function getTupletDivision() return getSetting("TupletDivision") end + +local function setNoteLen(nl) return setSetting("NoteLen", nl) end +local function getNoteLen() return getSetting("NoteLen") end + +local function setNoteLenModifier(nl) return setSetting("NoteLenModifier", nl) end +local function getNoteLenModifier() return getSetting("NoteLenModifier") end + +local function getNoteLenFactorNumerator() return getSetting("NoteLenFactorNumerator") end +local function getNoteLenFactorDenominator() return getSetting("NoteLenFactorDenominator") end + +local function AllowKeyEventNavigation() return getSetting("AllowKeyEventNavigation") end + +local function precpow2(n) + local b = 0 + local a = (n >> b) + while a > 0 do + b = b + 1 + a = (n >> b) + end + return (1 << (b-1)) +end + +local function getTupletFactor(div) + return precpow2(div)/div +end + +local function getNoteLenModifierFactor() + + local m = getNoteLenModifier() + local nls = getNoteLenParamSource() + + if m == D.NoteLenModifier.Straight then + return 1.0; + elseif m == D.NoteLenModifier.Dotted then + return 1.5; + elseif m == D.NoteLenModifier.Triplet then + return 2/3.0; + elseif m == D.NoteLenModifier.Modified then + return getSetting("NoteLenFactorNumerator") / getSetting("NoteLenFactorDenominator"); + elseif m == D.NoteLenModifier.Tuplet then + local div = getTupletDivision(); + + if nls == D.NoteLenParamSource.OSS then + return getTupletFactor(div) + else + -- In grid mode, we just return 1/n + return 1/div + end + end + + return 1.0; +end + +local function increaseNoteLen() + local l = getNoteLen(); + setNoteLen(D.NoteLenLookup[l].next); +end + +local function decreaseNoteLen() + local l = getNoteLen(); + setNoteLen(D.NoteLenLookup[l].prec); +end + +local function getNoteLenQN() + local nl = getNoteLen(); + + return D.NoteLenLookup[nl].qn; +end + +return { + SettingDefs = SettingDefs, + + getSetting = getSetting, + setSetting = setSetting, + resetSetting = resetSetting, + getSettingSpec = getSettingSpec, + + setPlaybackMeasureCount = setPlaybackMeasureCount, + getPlaybackMeasureCount = getPlaybackMeasureCount, + + setInputMode = setInputMode, + getInputMode = getInputMode, + + setNoteLenParamSource = setNoteLenParamSource, + getNoteLenParamSource = getNoteLenParamSource, + + setTupletDivision = setTupletDivision, + getTupletDivision = getTupletDivision, + + setNoteLen = setNoteLen, + getNoteLen = getNoteLen, + + setNoteLenModifier = setNoteLenModifier, + getNoteLenModifier = getNoteLenModifier, + + getNoteLenFactorNumerator = getNoteLenFactorNumerator, + getNoteLenFactorDenominator = getNoteLenFactorDenominator, + getNoteLenModifierFactor = getNoteLenModifierFactor, + + increaseNoteLen = increaseNoteLen, + decreaseNoteLen = decreaseNoteLen, + getNoteLenQN = getNoteLenQN, + + AllowKeyEventNavigation = AllowKeyEventNavigation +} + diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/snap.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/snap.lua new file mode 100644 index 000000000..ffcaf2505 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/snap.lua @@ -0,0 +1,236 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local S = require "modules/settings" +local N = require "modules/notes" +local D = require "modules/defines" + +local function moveComparatorHelper(v, cursor, best, direction, mode) + + -- mode could be "TIME", "PPQ", "QN" + + if direction > 0 then + if (v > cursor) and ((best == nil) or (v < best)) then + best = v + end + else + if (v < cursor) and ((best == nil) or (v > best)) then + best = v + end + end + + return best +end + +-- This gives a new value for bestjumptime, aligned on the given the grid (item or project) +local function gridSnapHelper(type, direction, cursorTime, cursorQN, bestJumpTime, take, itemStartTime, itemEndTime) + + local grid_len, swing, swingmode = nil , nil, nil + + if type == "ITEM" then + grid_len, swing, _ = reaper.MIDI_GetGrid(take) + else + _, grid_len, swingmode, swing = reaper.GetSetProjectGrid(0, false) + grid_len = grid_len * 4 -- put back in QN + if swingmode ~= 1 then + swing = 0 + end + end + + local cursorBars = reaper.TimeMap_QNToMeasures(0, cursorQN) - 1 + local _, measureStartQN, measureEndQN = reaper.TimeMap_GetMeasureInfo(0, cursorBars) + + if cursorBars > 0 and direction < 0 and math.abs(cursorQN - measureStartQN) < D.QN_TOLERANCE then + -- Cursor is aligned on the beginning of a measure but we're going back. + -- Work with the precedent measure. + cursorBars = cursorBars - 1 + _, measureStartQN, measureEndQN = reaper.TimeMap_GetMeasureInfo(0, cursorBars) + end + + -- Start with window, and slide + -- The odd/even logic is for handling the swing + + local parity = 1 + + local oddOffset = grid_len * (1 + swing * 0.5) + local evenOffset = grid_len * (1 - swing * 0.5) + + local prec = measureStartQN + local next = prec + ((parity == 1) and (oddOffset) or (evenOffset)) + + while next < (cursorQN - D.QN_TOLERANCE) do + parity = parity ~ 1 + prec = next + next = prec + ((parity == 1) and (oddOffset) or (evenOffset)) + end + + if math.abs(cursorQN - next) < D.QN_TOLERANCE then + parity = parity ~ 1 + next = next + ((parity == 1) and (oddOffset) or (evenOffset)) + end + + local precTime = reaper.TimeMap2_QNToTime(0, prec) + local nextTime = reaper.TimeMap2_QNToTime(0, next) + local msTime = reaper.TimeMap2_QNToTime(0, measureStartQN) + local meTime = reaper.TimeMap2_QNToTime(0, measureEndQN) + + -- Only add these times if they belong to the item (outside, consider there's no grid) + if type == "PROJECT" or (precTime >= itemStartTime and precTime <= itemEndTime) then + bestJumpTime = moveComparatorHelper(precTime, cursorTime, bestJumpTime, direction, "TIME") + end + if type == "PROJECT" or (nextTime >= itemStartTime and nextTime <= itemEndTime) then + bestJumpTime = moveComparatorHelper(nextTime, cursorTime, bestJumpTime, direction, "TIME") + end + if type == "PROJECT" or (msTime >= itemStartTime and msTime <= itemEndTime) then + bestJumpTime = moveComparatorHelper(msTime, cursorTime, bestJumpTime, direction, "TIME") + end + if type == "PROJECT" or (meTime >= itemStartTime and meTime <= itemEndTime) then + bestJumpTime = moveComparatorHelper(meTime, cursorTime, bestJumpTime, direction, "TIME") + end + + return bestJumpTime +end + + +-- Resolves the next snap point. +-- Track can be nil (won't happen) +local function nextSnap(track, direction, reftime, options) + + local cursorTime = reftime -- I don't want to rename everything ... + local cursorQN = reaper.TimeMap2_timeToQN(0, cursorTime) + local bestJumpTime = nil + local maxTime = 0 + + if options.enabled and track then + + -- Force Item Bounds when we have item grid on, that's usefule outside items + if options.itemGrid then + options.itemBounds = true + end + + local itemCount = reaper.CountTrackMediaItems(track) + local ii = 0 + + -- For optimization, we should randomize the order of iteration over the items + while ii < itemCount do + local mediaItem = reaper.GetTrackMediaItem(track, ii) + + local itemStartTime = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") + local itemEndTime = itemStartTime + reaper.GetMediaItemInfo_Value(mediaItem, "D_LENGTH") + + if itemEndTime > maxTime then + maxTime = itemEndTime + end + + -- A few conditions to avoid exploring the item if not needed + if (direction > 0) then + if (itemEndTime < cursorTime) or ((bestJumpTime ~= nil) and (bestJumpTime < itemStartTime)) then + goto nextitem + end + else + if (itemStartTime > cursorTime) or ((bestJumpTime ~= nil) and (bestJumpTime > itemEndTime)) then + goto nextitem + end + end + + if options.itemBounds then + bestJumpTime = moveComparatorHelper(itemStartTime, cursorTime, bestJumpTime, direction, "TIME") + bestJumpTime = moveComparatorHelper(itemEndTime, cursorTime, bestJumpTime, direction, "TIME") + end + + if options.noteStart or options.noteEnd or options.itemGrid then + local takeCount = reaper.GetMediaItemNumTakes(mediaItem) + local ti = 0 + + while ti < takeCount do + + local take = reaper.GetMediaItemTake(mediaItem, ti) + + local itemStartPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemStartTime) + local itemEndPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemEndTime) + local cursorPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorTime) + local bestJumpPPQ = nil + + if options.noteStart or options.noteEnd then + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take) + local ni = 0 + + while (ni < notecnt) do + local n = N.GetNote(take, ni) + + if options.noteStart then + bestJumpPPQ = moveComparatorHelper(n.startPPQ, cursorPPQ, bestJumpPPQ, direction, "PPQ") + end + + if options.noteEnd then + bestJumpPPQ = moveComparatorHelper(n.endPPQ, cursorPPQ, bestJumpPPQ, direction, "PPQ") + end + + ni = ni+1 + end -- end note iteration + + if bestJumpPPQ then + -- Found a snap note inside item, convert back to time and compare to already found bestJumpTime + local bjt = reaper.TimeMap2_QNToTime(0, reaper.MIDI_GetProjQNFromPPQPos(take, bestJumpPPQ)) + bestJumpTime = moveComparatorHelper(bjt, cursorTime, bestJumpTime, direction, "TIME") + end + end + + if options.itemGrid then + bestJumpTime = gridSnapHelper("ITEM", direction, cursorTime, cursorQN, bestJumpTime, take, itemStartTime, itemEndTime) + end + + ti = ti + 1 + end -- end take iteration + + end + + ::nextitem:: + ii = ii+1 + end -- end item iteration + end -- end options.enabled + + if options.projectGrid then + -- SWS version of BR_GetNextGrid has a bug, use my own implementation + bestJumpTime = gridSnapHelper("PROJECT", direction, cursorTime, cursorQN, bestJumpTime) + end + + -- Add track boundaries + if options.projectBounds then + bestJumpTime = moveComparatorHelper(0, cursorTime, bestJumpTime, direction, "TIME") + bestJumpTime = moveComparatorHelper(maxTime, cursorTime, bestJumpTime, direction, "TIME") + + if not bestJumpTime then + -- No boundaries worked? The cursor is one of them. + bestJumpTime = cursorTime + end + end + + return { + time = bestJumpTime, + qn = bestJumpTime and reaper.TimeMap2_timeToQN(0, bestJumpTime) + } +end + +local function snapOptions() + return { + enabled = S.getSetting("Snap"), + itemBounds = S.getSetting("SnapItemBounds"), + noteStart = S.getSetting("SnapNotes"), + noteEnd = S.getSetting("SnapNotes"), + itemGrid = S.getSetting("SnapItemGrid"), + projectGrid = S.getSetting("SnapProjectGrid"), + projectBounds = true + } +end + +local function nextSnapFromCursor(track, direction) + return nextSnap(track, direction, reaper.GetCursorPosition(), snapOptions()) +end + +return { + nextSnap = nextSnap, + nextSnapFromCursor = nextSnapFromCursor +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/target.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/target.lua new file mode 100644 index 000000000..34be40d8f --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/target.lua @@ -0,0 +1,160 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local D = require "modules/defines" +local S = require "modules/settings" +local T = require "modules/time" +local F = require "modules/focus" + +local function TryToGetTakeFromMidiEditor() + local midiEditor = reaper.MIDIEditor_GetActive(); + local midiEditorOk = not (reaper.MIDIEditor_GetMode(midiEditor) == -1); + + -- Prioritize the currently focused MIDI editor. + -- Use the last known focused element (between arrange view / midi editor) as it is more robust + -- When OSS window is focused for editing parameters + if midiEditorOk and F.LastKnownFocus().element == "MIDIEditor" then + -- -1 if ME not focused + local take = reaper.MIDIEditor_GetTake(midiEditor); + if take then + local mediaItem = reaper.GetMediaItemTake_Item(take); + if T.MediaItemContainsCursor(mediaItem, reaper.GetCursorPosition()) then + return take + end + end + end + + return nil +end + +local function TryToGetTakeFromArrangeViewAmongSelectedItems() + local mediaItemCount = reaper.CountSelectedMediaItems(0); + local cursorPos = reaper.GetCursorPosition(); + + local candidates = {}; + + for i = 0, mediaItemCount - 1 do + + local mediaItem = reaper.GetSelectedMediaItem(0, i) + local track = reaper.GetMediaItem_Track(mediaItem) + + -- Only keep items that contain the cursor pos + if T.MediaItemContainsCursor(mediaItem, cursorPos) then + local tk = reaper.GetActiveTake(mediaItem); + + candidates[#candidates + 1] = { + take = tk, + tsel = reaper.IsTrackSelected(track), + tname = reaper.GetTrackName(track), + name = reaper.GetTakeName(tk) + } + end + end + + table.sort(candidates, function(e1,e2) + -- Priorize items that have their track selected + local l1 = e1.tsel and 0 or 1; + local l2 = e2.tsel and 0 or 1; + + return l1 < l2; + end); + + if (#candidates) > 0 then + return candidates[1].take; + end + + return nil +end + +local function TryToGetTakeFromArrangeViewAmongSelectedTracks() + local cursorPos = reaper.GetCursorPosition(); + local trackCount = reaper.CountSelectedTracks(); + + local candidates = {}; + + for i = 0, trackCount - 1 do + local track = reaper.GetSelectedTrack(0, i); + local itemCount = reaper.CountTrackMediaItems(track); + for j = 0, itemCount - 1 do + local mediaItem = reaper.GetTrackMediaItem(track, j); + + if T.MediaItemContainsCursor(mediaItem, cursorPos) then + local tk = reaper.GetActiveTake(mediaItem); + + candidates[#candidates + 1] = { + take = tk, + tname = reaper.GetTrackName(track), + name = reaper.GetTakeName(tk) + } + end + end + end + + -- No sorting is possible + if (#candidates) > 0 then + return candidates[1].take; + end + + return nil +end + + +-- This function returns the take that should be edited +-- Inspired by tenfour's scripts but modified +-- It uses a strategy based on : +-- - What component has focus (midi editor or arrange window) +-- - What items are selected +-- - What items contain the cursor +-- - What tracks are selected + +local function TakeForEdition() + -- Try to get a take from the MIDI editor + local take = nil; + + if S.getSetting("AllowTargetingFocusedMidiEditors") then + take = TryToGetTakeFromMidiEditor(); + if take then + return take; + end + end + + -- Second heuristic, try to get a take from selected items + take = TryToGetTakeFromArrangeViewAmongSelectedItems(); + if take then + return take; + end + + if S.getSetting("AllowTargetingNonSelectedItemsUnderCursor") then + -- Third heuristic (if enabled), try to get a take from selected tracks + take = TryToGetTakeFromArrangeViewAmongSelectedTracks(); + if take then + return take; + end + end + + return nil; +end + +local function TrackForEditionIfNoItemFound() + local trackCount = reaper.CountSelectedTracks(); + if trackCount > 0 then + return reaper.GetSelectedTrack(0, 0); + end + return nil; +end + +local function CreateItemIfMissing(track) + local newitem = reaper.CreateNewMIDIItemInProj(track, reaper.GetCursorPosition(), reaper.GetCursorPosition() + D.TIME_TOLERANCE, false) + local take = reaper.GetMediaItemTake(newitem, 0) + local _, tname = reaper.GetTrackName(track) + reaper.GetSetMediaItemTakeInfo_String(take, "P_NAME", tname ..os.date(' - %Y%m%d%H%M%S'), true) + return take +end + +return { + TrackForEditionIfNoItemFound = TrackForEditionIfNoItemFound, + TakeForEdition = TakeForEdition, + CreateItemIfMissing = CreateItemIfMissing +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/modules/time.lua b/MIDI Editor/talagan_OneSmallStep/classes/modules/time.lua new file mode 100644 index 000000000..863f762f3 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/modules/time.lua @@ -0,0 +1,228 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local D = require "modules/defines" +local S = require "modules/settings" + +local function bool2sign(b) + return ((b == true) and (1) or (-1)) +end + +local function PPQIsAfterPPQ(ppq1, ppq2, strict) + return ppq1 > (ppq2 + bool2sign(strict) * D.PPQ_TOLERANCE) +end +local function PPQIsBeforePPQ(ppq1, ppq2, strict) + return ppq1 < (ppq2 - bool2sign(strict) * D.PPQ_TOLERANCE) +end +local function PPQIsOnPPQ(ppq1, ppq2) + return math.abs(ppq1 - ppq2) < D.PPQ_TOLERANCE +end + +local function noteStartsAfterPPQ(note, limit, strict) + return PPQIsAfterPPQ(note.startPPQ, limit, strict) +end +local function noteStartsBeforePPQ(note, limit, strict) + return PPQIsBeforePPQ(note.startPPQ, limit, strict) +end +local function noteStartsOnPPQ(note, limit) + return PPQIsOnPPQ(note.startPPQ, limit) +end + +local function noteEndsAfterPPQ(note, limit, strict) + return PPQIsAfterPPQ(note.endPPQ, limit, strict) +end +local function noteEndsBeforePPQ(note, limit, strict) + return PPQIsBeforePPQ(note.endPPQ, limit, strict) +end +local function noteEndsOnPPQ(note, limit) + return PPQIsOnPPQ(note.endPPQ, limit) +end + +local function noteStartsInWindowPPQ(note, left, right, strict) + local a = noteStartsAfterPPQ(note, left, strict) + local e = noteStartsBeforePPQ(note, right, strict) + + return a and e +end +local function noteEndsInWindowPPQ(note, left, right, strict) + local a = noteEndsAfterPPQ(note, left, strict) + local e = noteEndsBeforePPQ(note, right, strict) + + return a and e +end + +local function PPQRound(ppq) + return math.floor(ppq + 0.5) -- Compensate floating errors +end + +-- This converts the note length in PPQ from the QN value +-- For rounding reasons it's very important to work in PPQ +-- During the whole processes +local function NoteLenQN2PPQ(take, note_len_qn) + local mediaItem = reaper.GetMediaItemTake_Item(take) + local itemStartTime = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") + local itemStartPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemStartTime) + local itemStartQN = reaper.TimeMap2_timeToQN(0, itemStartTime) + local itemPlusNoteQN = itemStartQN + note_len_qn + local itemPlusNotePPQ = reaper.MIDI_GetPPQPosFromProjQN(take, itemPlusNoteQN) + local ret = itemPlusNotePPQ - itemStartPPQ + + return PPQRound(ret) +end + +local function TimeRoundBasedOnPPQ(take, time) + local ppq = reaper.MIDI_GetPPQPosFromProjTime(take, time) + return reaper.MIDI_GetProjTimeFromPPQPos(take, PPQRound(ppq)) +end + + +local function swingNoteLenQN(measureStartQN, posQN, noteLenQN, swing) + local elapsedDoubleBeats = (posQN - measureStartQN)/(2*noteLenQN) + -- Hack, it may happen that the cursor is just before the measure start + if elapsedDoubleBeats < 0 then + elapsedDoubleBeats = 0 + end + + local eaten = elapsedDoubleBeats - math.floor(elapsedDoubleBeats) + local qn_tolerance = 0.01 -- Beware, this should be a bit tolerant since the swing is not aligned on PPQs + + if eaten > 1 - qn_tolerance/noteLenQN then + -- Hack : cursor may be very close to next double beat. + eaten = 0 + end + + local onbeat = (1 + swing * 0.5) + local offbeat = (1 - swing * 0.5) + local onbeatlimit = onbeat - (qn_tolerance / noteLenQN) + + if (2 * eaten) < onbeatlimit then + return (noteLenQN * onbeat) + else + return (noteLenQN * offbeat) + end +end + +local function ResolveNoteLenQN(take) + + local nlm = S.getNoteLenParamSource() + + local cursorTime = reaper.GetCursorPosition() + local cursorQN = reaper.TimeMap2_timeToQN(0, cursorTime) + + local cursorMes = reaper.TimeMap_QNToMeasures(0, cursorQN) + local _, measureStartQN, measureEndQN = reaper.TimeMap_GetMeasureInfo(0, cursorMes - 1) + + if math.abs(cursorQN - measureEndQN) < D.QN_TOLERANCE then + -- We're on the measure end, advance 1 measure + cursorMes = cursorMes + 1 + _, measureStartQN, measureEndQN = reaper.TimeMap_GetMeasureInfo(0, cursorMes - 1) + end + + if nlm == D.NoteLenParamSource.OSS then + return S.getNoteLenQN() * S.getNoteLenModifierFactor() + elseif nlm == D.NoteLenParamSource.ProjectGrid then + + local _, division, swingmode, swing = reaper.GetSetProjectGrid(0, false) + local noteLenQN = division * 4 + local multFactor = S.getNoteLenQN() + + local baselen = 1 + if swingmode == 0 then + -- No swing + baselen = noteLenQN + elseif swingmode == 3 then + -- Project Grid is set to "measure" + baselen = (measureEndQN - measureStartQN) + else + -- Swing + if multFactor > 1 then + baselen = noteLenQN + else + baselen = swingNoteLenQN(measureStartQN, cursorQN, noteLenQN, swing) + end + end + + return baselen * S.getNoteLenQN() * S.getNoteLenModifierFactor() + + else + local gridLenQN, swing, noteLenQN = reaper.MIDI_GetGrid(take); + local multFactor = S.getNoteLenQN() + + if noteLenQN == 0 then + noteLenQN = gridLenQN + end + + local baselen = 1 + if swing == 0 then + baselen = noteLenQN + else + -- Swing + if multFactor > 1 then + baselen = noteLenQN + else + baselen = swingNoteLenQN(measureStartQN, cursorQN, noteLenQN, swing) + end + end + + return baselen * S.getNoteLenQN() * S.getNoteLenModifierFactor() + end +end + +local function ResolveNoteLenPPQ(take) + return NoteLenQN2PPQ(take, ResolveNoteLenQN(take)) +end + + +local function KeepEditCursorOnScreen() + local start_time, end_time = reaper.GetSet_ArrangeView2(0, false, 0, 0, 0, 0) + local cursor_time = reaper.GetCursorPosition() + local diff_time = end_time - start_time + local bound = 0.05 + local alpha = 0.25 + + if cursor_time < start_time + bound * diff_time then + reaper.GetSet_ArrangeView2(0, true, 0, 0, cursor_time - diff_time * alpha, cursor_time + diff_time * (1 - alpha)) + end + + if cursor_time > end_time - bound * diff_time then + reaper.GetSet_ArrangeView2(0, true, 0, 0, cursor_time - diff_time * (1-alpha), cursor_time + diff_time * alpha) + end +end + +local function MediaItemContainsCursor(mediaItem, CursorPos) + local pos = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") + local len = reaper.GetMediaItemInfo_Value(mediaItem, "D_LENGTH") + + local left = pos - D.TIME_TOLERANCE; + local right = pos + len + D.TIME_TOLERANCE; + + -- Only keep items that contain the cursor pos + return (CursorPos >= left) and (CursorPos <= right); +end + +return { + PPQRound = PPQRound, + + PPQIsAfterPPQ = PPQIsAfterPPQ, + PPQIsBeforePPQ = PPQIsBeforePPQ, + PPQIsOnPPQ = PPQIsOnPPQ, + + noteStartsAfterPPQ = noteStartsAfterPPQ, + noteStartsBeforePPQ = noteStartsBeforePPQ, + noteStartsOnPPQ = noteStartsOnPPQ, + noteStartsInWindowPPQ = noteStartsInWindowPPQ, + + noteEndsAfterPPQ = noteEndsAfterPPQ, + noteEndsBeforePPQ = noteEndsBeforePPQ, + noteEndsOnPPQ = noteEndsOnPPQ, + noteEndsInWindowPPQ = noteEndsInWindowPPQ, + + TimeRoundBasedOnPPQ = TimeRoundBasedOnPPQ, + + ResolveNoteLenPPQ = ResolveNoteLenPPQ, + + KeepEditCursorOnScreen = KeepEditCursorOnScreen, + MediaItemContainsCursor = MediaItemContainsCursor +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/operations/generic.lua b/MIDI Editor/talagan_OneSmallStep/classes/operations/generic.lua new file mode 100644 index 000000000..13a78d95e --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/operations/generic.lua @@ -0,0 +1,366 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local S = require "modules/settings" +local T = require "modules/time" +local N = require "modules/notes" +local MK = require "modules/markers" + +local function BuildContext(km, track, take) + local c = {} + + c.km = km + c.track = track + c.take = take + c.mediaItem = reaper.GetMediaItemTake_Item(take) + + c.counts = { sh = 0, ext = 0, rem = 0, mv = 0, add = 0 } + c.toadd, c.tomod, c.torem = {}, {}, {} + + c.noteLenPPQ = T.ResolveNoteLenPPQ(take) + + c.cursorTime = reaper.GetCursorPosition() + c.cursorQN = reaper.TimeMap2_timeToQN(0, c.cursorTime) + c.cursorPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, c.cursorTime) + + c.itemDuration = reaper.GetMediaItemInfo_Value(c.mediaItem, "D_LENGTH") + + c.itemStartTime = reaper.GetMediaItemInfo_Value(c.mediaItem, "D_POSITION") + c.imemStartQN = reaper.TimeMap2_timeToQN(0, c.itemStartTime) + c.itemStartPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, c.itemStartTime) + + c.itemEndTime = c.itemStartTime + c.itemDuration + c.itemEndQN = reaper.TimeMap2_timeToQN(0, c.itemEndTime) + c.itemEndPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, c.itemEndTime) + + local mkw = nil + mkw, c.markerTime = MK.findOperationMarker() + if mkw then + c.markerQN = reaper.TimeMap2_timeToQN(0, c.markerTime) + c.markerPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, c.markerTime) + end + + return c +end + +local function BuildForwardContext(km, track, take) + local c = BuildContext(km, track, take) + + c.advancePPQ = c.cursorPPQ + c.noteLenPPQ + c.advanceTime = reaper.MIDI_GetProjTimeFromPPQPos(take, c.advancePPQ) + c.advanceQN = reaper.TimeMap2_timeToQN(0, c.advanceTime) + + return c +end + +local function BuildBackwardContext(km, track, take, shouldClampRewindTime) + local c = BuildContext(km, track, take) + + c.rewindTime = reaper.MIDI_GetProjTimeFromPPQPos(take, c.cursorPPQ - c.noteLenPPQ) + + if shouldClampRewindTime and (c.rewindTime < c.itemStartTime) then + c.rewindTime = c.itemStartTime + end + + c.rewindQN = reaper.TimeMap2_timeToQN(0, c.rewindTime) + c.rewindPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, c.rewindTime) + + return c +end + +local function ForwardOperationFinish(c, jumpTime, newMaxQN) + + -- Modify notes + for ri = 1, #c.tomod, 1 do + local n = c.tomod[ri] + reaper.MIDI_SetNote(c.take, n.index, nil, nil, n.startPPQ, n.endPPQ, nil, nil, nil, true ) + end + + -- Delete notes that were shorten too much + -- Do this in reverse order to be sure that indices are descending + for ri = #c.torem, 1, -1 do + local n = c.torem[ri] + reaper.MIDI_DeleteNote(c.take, n.index) + end + + -- Reinsert moved notes + for ri = 1, #c.toadd, 1 do + local n = c.toadd[ri] + reaper.MIDI_InsertNote(c.take, n.selected, n.muted, n.startPPQ, n.endPPQ, n.chan, n.pitch, n.vel, true ) + end + + -- Advance and mark dirty + reaper.MIDI_Sort(c.take) + reaper.UpdateItemInProject(c.mediaItem) + + -- Grow the midi item if needed + local itemStartTime = reaper.GetMediaItemInfo_Value(c.mediaItem, "D_POSITION") + local itemLength = reaper.GetMediaItemInfo_Value(c.mediaItem, "D_LENGTH") + local itemEndTime = itemStartTime + itemLength; + local newMaxTime = reaper.TimeMap2_QNToTime(0, newMaxQN) + + if(itemEndTime >= newMaxTime) then + -- Cool, the item is big enough + else + local itemStartQN = reaper.TimeMap2_timeToQN(0, itemStartTime) + local itemEndQN = reaper.TimeMap2_timeToQN(0, newMaxTime) + + reaper.MIDI_SetItemExtents(c.mediaItem, itemStartQN, itemEndQN) + reaper.UpdateItemInProject(c.mediaItem) + end + + -- Mark item as dirty + reaper.MarkTrackItemsDirty(c.track, c.mediaItem) + + if jumpTime then + reaper.SetEditCurPos(T.TimeRoundBasedOnPPQ(c.take, jumpTime), false, false); + if S.getSetting("AutoScrollArrangeView") then + T.KeepEditCursorOnScreen() + end + end +end + +local function BackwardOperationFinish(c, jumpTime) + -- Modify notes + for ri = 1, #c.tomod, 1 do + local n = c.tomod[ri] + reaper.MIDI_SetNote(c.take, n.index, nil, nil, n.startPPQ, n.endPPQ, nil, nil, nil, true ) + end + + -- Delete notes that were shorten too much + -- Do this in reverse order to be sure that indices are descending + for ri = #c.torem, 1, -1 do + reaper.MIDI_DeleteNote(c.take, c.torem[ri].index) + end + + -- Reinsert moved notes + for ri = 1, #c.toadd, 1 do + local n = c.toadd[ri] + reaper.MIDI_InsertNote(c.take, n.selected, n.muted, n.startPPQ, n.endPPQ, n.chan, n.pitch, n.vel, true ) + end + + -- Rewind and mark dirty + reaper.MIDI_Sort(c.take) + reaper.UpdateItemInProject(c.mediaItem) + reaper.MarkTrackItemsDirty(c.track, c.mediaItem) + + if jumpTime then + reaper.SetEditCurPos(T.TimeRoundBasedOnPPQ(c.take, jumpTime), false, false) + if S.getSetting("AutoScrollArrangeView") then + T.KeepEditCursorOnScreen() + end + end +end + + +-- This function is used by the write/insert/replace modes +local function AddAndExtendNotes(c, notes_to_add, notes_to_extend) + -- Then add some other notes + for _, v in ipairs(notes_to_add) do + c.toadd[#c.toadd+1] = N.BuildFromManager(v, c.take, c.cursorPPQ, c.advancePPQ) + c.counts.add = c.counts.add + 1 + end + + -- Then extend notes + if #notes_to_extend > 0 then + local _, notecnt, _, _ = reaper.MIDI_CountEvts(c.take); + + for _, exnote in pairs(notes_to_extend) do + + -- Search for a note that could be extended (matches all conditions) + local ni = 0 + local found = false + + while (ni < notecnt) do + local n = N.GetNote(c.take, ni) + + -- Extend the note if found + if T.noteEndsOnPPQ(n, c.cursorPPQ) and (n.chan == exnote.chan) and (n.pitch == exnote.pitch) then + + c.tomod[#c.tomod + 1] = n + N.SetNewNoteBounds(n, c.take, n.startPPQ, c.advancePPQ) + + c.counts.ext = c.counts.ext + 1 + found = true + end + + ni = ni + 1 + end + + if not found then + -- Could not find a note to extend... create one ! + c.toadd[#c.toadd+1] = N.BuildFromManager(exnote, c.take, c.cursorPPQ, c.advancePPQ) + c.counts.add = c.counts.add + 1 + end + end + end +end + +local function GenericDelete(c, notes_to_shorten, selectiveErase, shiftMode) + + local _, notecnt, _, _ = reaper.MIDI_CountEvts(c.take); + local ni = 0; + while (ni < notecnt) do + + -- Examine each note in item + local n = N.GetNote(c.take, ni) + + local targetable = false + + if selectiveErase then + for _, shnote in pairs(notes_to_shorten) do + if n.chan == shnote.chan and n.pitch == shnote.pitch then + targetable = true + break + end + end + else + targetable = true + end + + if targetable then + if T.noteStartsAfterPPQ(n, c.cursorPPQ, false) then + -- Note should be moved back or left untouched (if in cursor mode or not) + -- + -- R C + -- | | + -- | | ==== + -- | | + -- + if shiftMode then + -- Move the note back + N.SetNewNoteBounds(n, c.take, n.startPPQ - c.noteLenPPQ, n.endPPQ - c.noteLenPPQ) + + c.torem[#c.torem+1] = n -- Remove + c.toadd[#c.toadd+1] = n -- And readd + + c.counts.mv = c.counts.mv + 1 + end + elseif T.noteStartsAfterPPQ(n, c.rewindPPQ, false) then + if T.noteEndsBeforePPQ(n, c.cursorPPQ, false) then + -- Note should be suppressed + -- + -- R C + -- | | + -- | === | + -- | | + -- + c.torem[#c.torem+1] = n + c.counts.rem = c.counts.rem + 1 + else + -- The note should be shortened (removing tail). + -- Since its start will change, it should be removed and reinserted (see reaper's API doc) + -- + -- R C + -- | | + -- | ==|=== + -- | | + -- + local offset = (shiftMode) and (- c.noteLenPPQ) or (0) + N.SetNewNoteBounds(n, c.take, c.cursorPPQ + offset, n.endPPQ + offset) + + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + + c.counts.sh = c.counts.sh + 1 + c.counts.mv = c.counts.mv + 1 + end + else + if T.noteEndsAfterPPQ(n, c.cursorPPQ, false) then + -- Note should be cut. + -- + -- R C + -- | | + -- ==|=====|=== + -- | | + -- + if shiftMode or T.noteEndsOnPPQ(n, c.cursorPPQ) then + N.SetNewNoteBounds(n, c.take, n.startPPQ, n.endPPQ - c.noteLenPPQ) + + c.tomod[#c.tomod+1] = n + c.counts.sh = c.counts.sh + 1 + else + -- Create a hole in the note. Copy note + local newn = {} + for k,v in pairs(n) do + newn[k] = v + end + + -- Shorted remaining note + N.SetNewNoteBounds(n, c.take, n.startPPQ, c.rewindPPQ); + c.tomod[#c.tomod+1] = n + c.counts.sh = c.counts.sh + 1 + + -- Add new note + N.SetNewNoteBounds(newn, c.take, c.cursorPPQ, newn.endPPQ); + c.toadd[#c.toadd+1] = newn + c.counts.add = c.counts.add + 1 + end + + elseif T.noteEndsAfterPPQ(n, c.rewindPPQ, true) then + -- Note ending should be erased + -- + -- R C + -- | | + -- ==|=== | + -- | | + -- + N.SetNewNoteBounds(n, c.take, n.startPPQ, c.rewindPPQ) + c.tomod[#c.tomod+1] = n + c.counts.sh = c.counts.sh + 1 + else + -- Leave untouched + -- + -- R C + -- | | + -- === | | + -- | | + -- + end + end + end + + ni = ni + 1; + end +end + +local function OperationSummary(direction, counts) + local description = {} + + if counts.sh + counts.add + counts.rem + counts.sh + counts.ext == 0 then + if direction > 0 then + description[#description+1] = "advanced" + else + description[#description+1] = "stepped back" + end + end + if counts.add > 0 then + description[#description+1] = "added " .. counts.add .. " notes" + end + if counts.rem > 0 then + description[#description+1] = "removed " .. counts.rem .. " notes" + end + if counts.mv > 0 then + description[#description+1] = "moved " .. counts.mv .. " notes" + end + if counts.sh > 0 then + description[#description+1] = "shortened " .. counts.sh .. " notes" + end + if counts.ext > 0 then + description[#description+1] = "extended " .. counts.ext .. " notes" + end + + return "One Small Step: " .. table.concat(description, ", ") +end + +return { + BuildForwardContext = BuildForwardContext, + BuildBackwardContext = BuildBackwardContext, + OperationSummary = OperationSummary, + ForwardOperationFinish = ForwardOperationFinish, + BackwardOperationFinish = BackwardOperationFinish, + AddAndExtendNotes = AddAndExtendNotes, + GenericDelete = GenericDelete +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/operations/insert.lua b/MIDI Editor/talagan_OneSmallStep/classes/operations/insert.lua new file mode 100644 index 000000000..4c98a5ef8 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/operations/insert.lua @@ -0,0 +1,84 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local T = require "modules/time" +local S = require "modules/settings" +local D = require "modules/defines" +local N = require "modules/notes" + +local GEN = require "operations/generic" + +local function Insert(km, track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + + local c = GEN.BuildForwardContext(km, track, take) + + local newMaxQN = c.advanceQN + + reaper.Undo_BeginBlock(); + + -- First, move some notes + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take) + local ni = 0 + + -- Shift notes + while (ni < notecnt) do + local n = N.GetNote(take, ni); + + if T.noteStartsAfterPPQ(n, c.cursorPPQ, false) then + -- Move the note + N.SetNewNoteBounds(n, take, n.startPPQ + c.noteLenPPQ, n.endPPQ + c.noteLenPPQ) + + if n.endQN > newMaxQN then + newMaxQN = n.endQN + end + + -- It should be removed and readded, because the start position changes + c.torem[#c.torem + 1] = n + c.toadd[#c.toadd + 1] = n + + c.counts.mv = c.counts.mv + 1 + end + + ni = ni + 1 + end + + GEN.AddAndExtendNotes(c, notes_to_add, notes_to_extend) + GEN.ForwardOperationFinish(c, c.advanceTime, newMaxQN) + + reaper.Undo_EndBlock(GEN.OperationSummary(1, c.counts), -1) +end + + +local function InsertBack(km, track, take, notes_to_shorten, triggered_by_key_event) + + -- For now, just force a full erase + notes_to_shorten = {} + + if triggered_by_key_event and not S.AllowKeyEventNavigation() then + -- Don't allow erasing when triggered by key + return + end + + local c = GEN.BuildBackwardContext(km, track, take, true) + + -- If we're at the start of an item don't move things + if math.abs(c.itemStartPPQ - c.cursorPPQ) < D.PPQ_TOLERANCE then + return + end + + reaper.Undo_BeginBlock(); + + GEN.GenericDelete(c,notes_to_shorten, false, true) + GEN.BackwardOperationFinish(c, c.rewindTime) + + reaper.Undo_EndBlock(GEN.OperationSummary(-1, c.counts), -1) +end + +return { + Insert = Insert, + InsertBack = InsertBack +} + + diff --git a/MIDI Editor/talagan_OneSmallStep/classes/operations/navigate.lua b/MIDI Editor/talagan_OneSmallStep/classes/operations/navigate.lua new file mode 100644 index 000000000..061a3a30c --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/operations/navigate.lua @@ -0,0 +1,34 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local SNP = require "modules/snap" +local T = require "modules/time" +local S = require "modules/settings" + +local function Navigate(track, direction) + local ns = SNP.nextSnapFromCursor(track, direction) + + reaper.Undo_BeginBlock(); + reaper.SetEditCurPos(ns.time, false, false); + if S.getSetting("AutoScrollArrangeView") then + T.KeepEditCursorOnScreen() + end + reaper.Undo_EndBlock("One Small Step: " .. ((direction > 0) and ("advanced") or ("stepped back")),-1); +end + +local function NavigateForward(track) + Navigate(track, 1) +end + +-- Commits the currently held notes into the take +local function NavigateBack(track) + Navigate(track, -1) +end + +return { + Navigate = Navigate, + NavigateForward = NavigateForward, + NavigateBack = NavigateBack +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/operations/repitch.lua b/MIDI Editor/talagan_OneSmallStep/classes/operations/repitch.lua new file mode 100644 index 000000000..4105fdd73 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/operations/repitch.lua @@ -0,0 +1,189 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local SNP = require "modules/snap" +local T = require "modules/time" +local S = require "modules/settings" +local N = require "modules/notes" + +local GEN = require "operations/generic" + +local MU = require "lib/MIDIUtils" + +-- Trying to disable overlapping note correction ... +-- Well it does not work. When the autocorrection is reenabled +-- It will destroy notes that overlap +-- We'll trigger it off and not reenable it +local autoOverlap = nil +local function PushAutoCorrectOverlapOption() + autoOverlap = reaper.GetToggleCommandStateEx(32060, 40681) + if autoOverlap == 1 then + local ret = reaper.MIDIEditor_LastFocused_OnCommand(40681, false) -- toggle off + end +end + +local function PopAutoCorrectOverlapOption() + if autoOverlap == 1 then + -- reaper.MIDIEditor_LastFocused_OnCommand(40681, false) -- toggle back on + end +end + +local function Repitch(km, track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + + local c = GEN.BuildForwardContext(km, track, take) + + c.aggregationTime = c.cursorTime + S.getSetting("RepitchModeAggregationTime") + c.aggregationPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, c.aggregationTime) + + local affects = S.getSetting("RepitchModeAffects") + local useNewVelocities = (affects == "Velocities only") or (affects == "Pitches + Velocities") + local useNewPitches = (affects == "Pitches only") or (affects == "Pitches + Velocities") + + local useMidiUtils = true + + local _, notecnt, _, _ = N.CountEvts(take, useMidiUtils) + + reaper.Undo_BeginBlock() + + if useMidiUtils then + MU.MIDI_InitializeTake(take) + end + + -- Since we repitch, there's no notion of holding notes + for _, v in pairs(notes_to_extend) do + notes_to_add[#notes_to_add+1] = v + end + + notes_to_extend = {} + + local ni = 0 + while (ni < notecnt) do + local n = N.GetNote(take, ni, useMidiUtils) + + if T.noteStartsInWindowPPQ(n, c.cursorPPQ, c.aggregationPPQ, false) then + c.tomod[#c.tomod + 1] = n + end + + ni = ni + 1 + end + + local shouldJump = true + local jumpRefPPQ = c.cursorPPQ + + if #c.tomod == 0 or #notes_to_add == 0 then + -- Just jump + else + if (#c.tomod ~= #notes_to_add) then + shouldJump = false + else + -- Apply mote modifications + + -- Sort notes to modify by pitch + table.sort(c.tomod, function(n1, n2) + return n1.pitch < n2.pitch + end) + + -- Sort notes to add by pitch + table.sort(notes_to_add, function(n1, n2) + return n1.pitch < n2.pitch + end) + + if useMidiUtils then + PushAutoCorrectOverlapOption() + MU.MIDI_OpenWriteTransaction(take) + end + + for k, n in ipairs(c.tomod) do + local newvel = nil + local newpitch = nil + + if useNewVelocities then + newvel = notes_to_add[k].vel + end + if useNewPitches then + newpitch = notes_to_add[k].pitch + end + + if useMidiUtils then + MU.MIDI_SetNote(take, n.index, nil, nil, nil, nil, nil, newpitch, newvel, nil) + else + reaper.MIDI_SetNote(take, n.index, nil, nil, nil, nil, nil, newpitch, newvel, false) + end + + if n.startPPQ > jumpRefPPQ then + jumpRefPPQ = n.startPPQ + end + end + + if useMidiUtils then + MU.MIDI_CommitWriteTransaction(take) + end + end + + if not useMidiUtils then + reaper.MIDI_Sort(take) + end + + reaper.UpdateItemInProject(c.mediaItem) + reaper.MarkTrackItemsDirty(track, c.mediaItem) + + if useMidiUtils then + PopAutoCorrectOverlapOption() + end + + end + + if shouldJump then + local jumpRefTime = reaper.MIDI_GetProjTimeFromPPQPos(take, jumpRefPPQ) + local jumpTime = SNP.nextSnap(track, 1, jumpRefTime, {enabled = true, noteStart = true}) + jumpTime = (jumpTime and jumpTime.time) + + if not jumpTime then + -- If it fails, try with not ends + jumpTime = SNP.nextSnap(track, 1, jumpRefTime, {enabled = true, noteEnd = true}) + jumpTime = (jumpTime and jumpTime.time) + end + + if not jumpTime then + -- If it still fails, use the item end time + jumpTime = c.itemEndTime + end + + reaper.SetEditCurPos(jumpTime, false, false) + + if S.getSetting("AutoScrollArrangeView") then + T.KeepEditCursorOnScreen() + end + end + + reaper.Undo_EndBlock(GEN.OperationSummary(1, c.counts), -1) +end + +local function RepitchBack(km, track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + + local c = GEN.BuildBackwardContext(km, track, take) + + reaper.Undo_BeginBlock(); + + local jumpTime = SNP.nextSnap(track, -1, c.cursorTime, {enabled = true, noteStart = true}) + jumpTime = (jumpTime and jumpTime.time) or c.itemStartTime + + if not jumpTime then + jumpTime = c.itemStartTime + end + + reaper.SetEditCurPos(jumpTime, false, false) + + if S.getSetting("AutoScrollArrangeView") then + T.KeepEditCursorOnScreen() + end + + reaper.Undo_EndBlock(GEN.OperationSummary(1, c.counts), -1) +end + +return { + Repitch = Repitch, + RepitchBack = RepitchBack +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/operations/replace.lua b/MIDI Editor/talagan_OneSmallStep/classes/operations/replace.lua new file mode 100644 index 000000000..b00e0b5fd --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/operations/replace.lua @@ -0,0 +1,168 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local T = require "modules/time" +local S = require "modules/settings" +local D = require "modules/defines" +local N = require "modules/notes" + +local GEN = require "operations/generic" + +-- Commits the currently held notes into the take +local function Replace(km, track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + + local c = GEN.BuildForwardContext(km, track, take) + + local newMaxQN = c.advanceQN + + reaper.Undo_BeginBlock(); + + -- Erase forward + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take) + + local ni = 0; + while (ni < notecnt) do + + -- Examine each note in item + local n = N.GetNote(take, ni) + + if T.noteStartsAfterPPQ(n, c.advancePPQ, false) then + -- Note is not in the erasing window + -- + -- C A + -- | | + -- | | ==== + -- | | + -- + elseif T.noteStartsAfterPPQ(n, c.cursorPPQ, false) then + if T.noteEndsBeforePPQ(n, c.advancePPQ, false) then + -- Note should be suppressed + -- + -- C A + -- | | + -- | === | + -- | | + -- + c.torem[#c.torem+1] = n + c.counts.rem = c.counts.rem + 1 + else + -- The note should be shortened (removing tail). + -- Since its start will change, it should be removed and reinserted (see reaper's API doc) + -- + -- RC A + -- | | + -- | ==|=== + -- | | + -- + N.SetNewNoteBounds(n, take, c.advancePPQ, n.endPPQ) + + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + + c.counts.sh = c.counts.sh + 1 + c.counts.mv = c.counts.mv + 1 + end + else + if T.noteEndsAfterPPQ(n, c.advancePPQ, false) then + -- We should make a hole. Shorten (or erase) left part. Shorten (or erase) right part + -- + -- C A + -- | | + -- ==|=====|=== + -- | | + -- + if T.noteStartsOnPPQ(n, c.cursorPPQ) then + -- The start changes, remove and reinsert + N.SetNewNoteBounds(n, take, c.advancePPQ, n.endPPQ) + + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + + c.counts.sh = c.counts.sh + 1 + c.counts.mv = c.counts.mv + 1 + else + -- Copy note + local newn = {} + for k,v in pairs(n) do + newn[k] = v + end + + -- Shorten the note + N.SetNewNoteBounds(n, take, n.startPPQ, c.cursorPPQ) + + c.tomod[#c.tomod+1] = n + c.counts.sh = c.counts.sh + 1 + + if not T.noteEndsOnPPQ(newn, c.advancePPQ) then + -- Add new note + N.SetNewNoteBounds(newn, take, c.advancePPQ, newn.endPPQ); + c.toadd[#c.toadd+1] = newn + c.counts.add = c.counts.add + 1 + end + end + + elseif T.noteEndsAfterPPQ(n, c.cursorPPQ, true) then + -- Note ending should be erased + -- + -- C A + -- | | + -- ==|=== | + -- | | + -- + N.SetNewNoteBounds(n, take, n.startPPQ, c.cursorPPQ); + c.tomod[#c.tomod+1] = n + c.counts.sh = c.counts.sh + 1 + else + -- Leave untouched + -- + -- C A + -- | | + -- === | | + -- | | + -- + end + + end + + ni = ni + 1; + end + + GEN.AddAndExtendNotes(c, notes_to_add, notes_to_extend) + GEN.ForwardOperationFinish(c, c.advanceTime, newMaxQN) + + reaper.Undo_EndBlock(GEN.OperationSummary(1, c.counts), -1) +end + + +local function ReplaceBack(km, track, take, notes_to_shorten, triggered_by_key_event) + + -- For now, just force a full erase + notes_to_shorten = {} + + if triggered_by_key_event and not S.AllowKeyEventNavigation() then + -- Don't allow erasing when triggered by key + return + end + + local c = GEN.BuildBackwardContext(km, track, take, true) + + -- If we're at the start of an item don't delete things + if math.abs(c.itemStartPPQ - c.cursorPPQ) < D.PPQ_TOLERANCE then + return + end + + reaper.Undo_BeginBlock(); + + GEN.GenericDelete(c, notes_to_shorten) + GEN.BackwardOperationFinish(c, c.rewindTime) + + reaper.Undo_EndBlock(GEN.OperationSummary(-1, c.counts), -1) +end + +return { + Replace = Replace, + ReplaceBack = ReplaceBack +} + diff --git a/MIDI Editor/talagan_OneSmallStep/classes/operations/stretch.lua b/MIDI Editor/talagan_OneSmallStep/classes/operations/stretch.lua new file mode 100644 index 000000000..e99835ba2 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/operations/stretch.lua @@ -0,0 +1,240 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local T = require "modules/time" +local D = require "modules/defines" +local N = require "modules/notes" +local MK = require "modules/markers" + +local GEN = require "operations/generic" + +local function Stretch(km, track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + + notes_to_add = {} + notes_to_extend = {} + + local c = GEN.BuildForwardContext(km, track, take) + + local newMaxQN = c.advanceQN + + if c.markerTime >= c.cursorTime then + -- Not possible + return + end + + local stretchFactor = (c.advanceTime - c.markerTime)/(c.cursorTime - c.markerTime) + + local _stretch = function(ppq) + return c.markerPPQ + stretchFactor * (ppq - c.markerPPQ) + end + + reaper.Undo_BeginBlock(); + + -- Erase forward + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take); + local ni = 0; + while (ni < notecnt) do + + -- Examine each note in item + local n = N.GetNote(take, ni) + + if T.noteStartsAfterPPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- | | ==== + -- | | + -- + N.SetNewNoteBounds(n, take, n.startPPQ + c.noteLenPPQ, n.endPPQ + c.noteLenPPQ) + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + c.counts.ext = c.counts.ext + 1 + elseif T.noteStartsAfterPPQ(n, c.markerPPQ, false) then + if T.noteEndsBeforePPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- | === | + -- | | + -- + N.SetNewNoteBounds(n, take, _stretch(n.startPPQ), _stretch(n.endPPQ)) + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + c.counts.ext = c.counts.ext + 1 + else + -- + -- M C + -- | | + -- | ==|=== + -- | | + -- + N.SetNewNoteBounds(n, take, _stretch(n.startPPQ), n.endPPQ + c.noteLenPPQ) + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + c.counts.ext = c.counts.ext + 1 + end + else + if T.noteEndsAfterPPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- ==|=====|=== + -- | | + -- + N.SetNewNoteBounds(n, take, n.startPPQ, n.endPPQ + c.noteLenPPQ) + c.tomod[#c.tomod+1] = n + c.counts.ext = c.counts.ext + 1 + elseif T.noteEndsAfterPPQ(n, c.markerPPQ, true) then + -- Note ending should be erased + -- + -- C A + -- | | + -- ==|=== | + -- | | + -- + N.SetNewNoteBounds(n, take, n.startPPQ, _stretch(n.endPPQ)) + c.tomod[#c.tomod+1] = n + c.counts.ext = c.counts.ext + 1 + else + -- Leave untouched + -- + -- C A + -- | | + -- === | | + -- | | + -- + end + end + + if n.endQN > newMaxQN then + newMaxQN = n.endQN + end + + ni = ni + 1; + end + + GEN.ForwardOperationFinish(c, c.advanceTime, newMaxQN) + + reaper.Undo_EndBlock(GEN.OperationSummary(-1, c.counts), -1) +end + +function StretchBack(km, track, take, notes_to_shorten, triggered_by_key_event) + + notes_to_shorten = {} + + local c = GEN.BuildBackwardContext(km, track, take, true) + + -- If we're at the start of an item don't move things + if math.abs(c.itemStartPPQ - c.cursorPPQ) < D.PPQ_TOLERANCE then + return + end + + if c.markerTime >= c.rewindTime then + -- Not possible + return + end + + reaper.Undo_BeginBlock(); + + -- Compress + local compFactor = (c.rewindTime - c.markerTime)/(c.cursorTime - c.markerTime) + + local _comp = function(ppq) + return c.markerPPQ + compFactor * (ppq - c.markerPPQ) + end + + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take); + local ni = 0; + while (ni < notecnt) do + + -- Examine each note in item + local n = N.GetNote(take, ni); + + if T.noteStartsAfterPPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- | | ==== + -- | | + -- + -- Move the note back + N.SetNewNoteBounds(n, take, n.startPPQ - c.noteLenPPQ, n.endPPQ - c.noteLenPPQ) + + c.torem[#c.torem+1] = n -- Remove + c.toadd[#c.toadd+1] = n -- And readd + c.counts.mv = c.counts.mv + 1 + elseif T.noteStartsAfterPPQ(n, c.markerPPQ, false) then + if T.noteEndsBeforePPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- | === | + -- | | + -- + N.SetNewNoteBounds(n, take, _comp(n.startPPQ), _comp(n.endPPQ)) + + c.torem[#c.torem+1] = n -- Remove + c.toadd[#c.toadd+1] = n -- And readd + c.counts.sh = c.counts.sh + 1 + c.counts.mv = c.counts.mv + 1 + else + -- + -- M C + -- | | + -- | ==|=== + -- | | + -- + N.SetNewNoteBounds(n, take, _comp(n.startPPQ), n.endPPQ - c.noteLenPPQ) + + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + c.counts.sh = c.counts.sh + 1 + c.counts.mv = c.counts.mv + 1 + end + else + if T.noteEndsAfterPPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- ==|=====|=== + -- | | + -- + N.SetNewNoteBounds(n, take, n.startPPQ, n.endPPQ - c.noteLenPPQ) + c.tomod[#c.tomod+1] = n + c.counts.sh = c.counts.sh + 1 + elseif T.noteEndsAfterPPQ(n, c.markerPPQ, true) then + -- + -- M C + -- | | + -- ==|=== | + -- | | + -- + N.SetNewNoteBounds(n, take, n.startPPQ, _comp(n.endPPQ)) + c.tomod[#c.tomod+1] = n + c.counts.sh = c.counts.sh + 1 + else + -- Leave untouched + -- + -- M C + -- | | + -- === | | + -- | | + -- + end + end + + ni = ni + 1; + end + + GEN.BackwardOperationFinish(c, c.rewindTime) + + reaper.Undo_EndBlock(GEN.OperationSummary(-1, c.counts), -1) +end + +return { + Stretch = Stretch, + StretchBack = StretchBack +} + diff --git a/MIDI Editor/talagan_OneSmallStep/classes/operations/stuff.lua b/MIDI Editor/talagan_OneSmallStep/classes/operations/stuff.lua new file mode 100644 index 000000000..3980e95e1 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/operations/stuff.lua @@ -0,0 +1,350 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local T = require "modules/time" +local S = require "modules/settings" +local D = require "modules/defines" +local N = require "modules/notes" +local MK = require "modules/markers" + +local GEN = require "operations/generic" + +local function getStuffingBaseLength(take, markerPPQ, cursorPPQ) + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take) + + local ret = nil + + -- First, examine in priority notes beginning before the region or at start of the region + local ni = 0 + while ni < notecnt do + local n = N.GetNote(take, ni) + + if T.noteStartsBeforePPQ(n, markerPPQ, false) and T.noteEndsBeforePPQ(n, cursorPPQ, false) then + local reflen = n.endPPQ - markerPPQ + if not ret or reflen > ret then + ret = reflen + end + end + + ni = ni + 1 + end + + if ret then + return ret + end + + -- Then examine notes starting and ending in the window + local firstone = nil + local ni = 0 + while ni < notecnt do + local n = N.GetNote(take, ni) + + if T.noteStartsAfterPPQ(n, markerPPQ, false) and T.noteEndsBeforePPQ(n, cursorPPQ, false) then + if not firstone or n.startPPQ < firstone.startPPQ then + firstone = n + end + end + + ni = ni + 1 + end + + if firstone then + return firstone.endPPQ - firstone.startPPQ + end + + return 0 +end + +local function Stuff(km, track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + + local c = GEN.BuildForwardContext(km, track, take) + local newMaxQN = c.advanceQN + + if c.markerTime >= c.cursorTime then + -- Not possible + return + end + + reaper.Undo_BeginBlock(); + + local sbl = getStuffingBaseLength(take, c.markerPPQ, c.cursorPPQ) * S.getNoteLenQN() * S.getNoteLenModifierFactor() + local compFactor = (c.cursorPPQ - c.markerPPQ) / (sbl + (c.cursorPPQ - c.markerPPQ)) + local extPPQ = sbl * compFactor + + local _comp = function(ppq) + if sbl == 0 then + return c.markerPPQ + else + return T.PPQRound(c.markerPPQ + compFactor * (ppq - c.markerPPQ)) + end + end + + local _isNoteHeld = function(pitch, chan) + for nhi = 1, #notes_to_extend do + local nex = notes_to_extend[nhi] + if nex.chan == chan and nex.pitch == pitch then + return true, nhi + end + end + return false, -1 + end + + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take) + local ni = 0 + while (ni < notecnt) do + + -- Examine each note in item + local n = N.GetNote(take, ni) + + if T.noteStartsAfterPPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- | | ==== + -- | | + -- + elseif T.noteStartsAfterPPQ(n, c.markerPPQ, false) then + if T.noteEndsBeforePPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- | === | + -- | | + -- + local isheld, hidx = _isNoteHeld(n.pitch, n.chan) + local offset = 0 + if isheld and T.noteEndsOnPPQ(n, c.cursorPPQ) then + table.remove(notes_to_extend, hidx) + offset = extPPQ + end + + N.SetNewNoteBounds(n, take, _comp(n.startPPQ), _comp(n.endPPQ) + offset) + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + c.counts.ext = c.counts.ext + 1 + else + -- + -- M C + -- | | + -- | ==|=== + -- | | + -- + N.SetNewNoteBounds(n, take, _comp(n.startPPQ), n.endPPQ) + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + c.counts.ext = c.counts.ext + 1 + end + else + if T.noteEndsAfterPPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- ==|=====|=== + -- | | + -- + elseif T.noteEndsAfterPPQ(n, c.markerPPQ, true) then + -- + -- C A + -- | | + -- ==|=== | + -- | | + -- + N.SetNewNoteBounds(n, take, n.startPPQ, _comp(n.endPPQ)) + c.tomod[#c.tomod+1] = n + c.counts.ext = c.counts.ext + 1 + else + -- + -- C A + -- | | + -- === | | + -- | | + -- + end + end + + ni = ni + 1; + end + + -- We've extended what we could, other candidates should be added + -- Concat notes_to_extend that haven't been used to notes_to_add + for i=1,#notes_to_extend do + notes_to_add[#notes_to_add+1] = notes_to_extend[i] + end + + for i=1,#notes_to_add do + c.toadd[#c.toadd+1] = N.BuildFromManager(notes_to_add[i], c.take, _comp(c.cursorPPQ), c.cursorPPQ) + c.counts.add = c.counts.add + 1 + end + + -- Pass nil as jump time, we don't want to jump in stuff mode + GEN.ForwardOperationFinish(c, nil, newMaxQN) + + reaper.Undo_EndBlock(GEN.OperationSummary(1, c.counts),-1); +end + + +local function getStuffingBackLength(take, markerPPQ, cursorPPQ) + local ret = cursorPPQ - markerPPQ + local found = false + + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take) + local ni = 0 + + -- Look for notes ending on the and starting in the window + while ni < notecnt do + local n = N.GetNote(take, ni) + + if T.noteStartsAfterPPQ(n, markerPPQ, false) and T.noteEndsBeforePPQ(n, cursorPPQ, false) then + if T.noteEndsOnPPQ(n, cursorPPQ) then + found = true + local nl = n.endPPQ - n.startPPQ + if nl < ret then + ret = nl + end + end + end + + ni = ni + 1 + end + + if found then + return ret + end + + ni = 0 + while ni < notecnt do + local n = N.GetNote(take, ni) + + if T.noteStartsAfterPPQ(n, markerPPQ, false) and T.noteEndsBeforePPQ(n, cursorPPQ, false) then + found = true + local nl = cursorPPQ - n.endPPQ + if nl < ret then + ret = nl + end + end + ni = ni + 1 + end + + if found then + return ret + end + + return 0 +end + +local function StuffBack(km, track, take, notes_to_shorten, triggered_by_key_event) + + notes_to_shorten = {} + + local c = GEN.BuildBackwardContext(km, track, take, true) + + -- If we're at the start of an item don't move things + if math.abs(c.itemStartPPQ - c.cursorPPQ) < D.PPQ_TOLERANCE then + return + end + + if c.markerTime >= c.rewindTime then + -- Not possible + return + end + + local sbl = getStuffingBackLength(take, c.markerPPQ, c.cursorPPQ) + local strFactor = (c.cursorPPQ - c.markerPPQ) / (c.cursorPPQ - c.markerPPQ - sbl) + + local _stretch = function(ppq) + if sbl == 0 then + return c.cursorPPQ + else + return T.PPQRound(c.markerPPQ + strFactor * (ppq - c.markerPPQ)) + end + end + + reaper.Undo_BeginBlock(); + + local _, notecnt, _, _ = reaper.MIDI_CountEvts(take); + local ni = 0; + while (ni < notecnt) do + + -- Examine each note in item + local n = N.GetNote(take, ni); + + if T.noteStartsAfterPPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- | | ==== + -- | | + -- + -- Move the note back + elseif T.noteStartsAfterPPQ(n, c.markerPPQ, false) then + if T.noteEndsBeforePPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- | === | + -- | | + -- + if T.PPQIsAfterPPQ(_stretch(n.startPPQ), c.cursorPPQ, false) then + c.torem[#c.torem+1] = n + c.counts.rem = c.counts.rem + 1 + else + N.SetNewNoteBounds(n, take, _stretch(n.startPPQ), _stretch(n.endPPQ)) + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + c.counts.ext = c.counts.ext + 1 + end + else + -- + -- M C + -- | | + -- | ==|=== + -- | | + -- + N.SetNewNoteBounds(n, take, _stretch(n.startPPQ), n.endPPQ) + c.torem[#c.torem+1] = n + c.toadd[#c.toadd+1] = n + c.counts.ext = c.counts.ext + 1 + end + else + if T.noteEndsAfterPPQ(n, c.cursorPPQ, false) then + -- + -- M C + -- | | + -- ==|=====|=== + -- | | + -- + elseif T.noteEndsAfterPPQ(n, c.markerPPQ, true) then + -- + -- M C + -- | | + -- ==|=== | + -- | | + -- + N.SetNewNoteBounds(n, take, n.startPPQ, _stretch(n.endPPQ)) + c.tomod[#c.tomod+1] = n + c.counts.ext = c.counts.ext + 1 + else + -- Leave untouched + -- + -- M C + -- | | + -- === | | + -- | | + -- + end + end + + ni = ni + 1; + end + + GEN.BackwardOperationFinish(c, nil) + + reaper.Undo_EndBlock(GEN.OperationSummary(-1, c.counts),-1); +end + +return { + Stuff = Stuff, + StuffBack = StuffBack +} diff --git a/MIDI Editor/talagan_OneSmallStep/classes/operations/write.lua b/MIDI Editor/talagan_OneSmallStep/classes/operations/write.lua new file mode 100644 index 000000000..3305b5f69 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/operations/write.lua @@ -0,0 +1,80 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +local T = require "modules/time" +local S = require "modules/settings" +local D = require "modules/defines" +local AT = require "modules/action_triggers" + +local GEN = require "operations/generic" + +-- Commits the currently held notes into the take +local function Write(km, track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + local c = GEN.BuildForwardContext(km, track, take) + + local newMaxQN = c.advanceQN + + reaper.Undo_BeginBlock(); + + GEN.AddAndExtendNotes(c, notes_to_add, notes_to_extend) + GEN.ForwardOperationFinish(c, c.advanceTime, newMaxQN) + + reaper.Undo_EndBlock(GEN.OperationSummary(1, c.counts), -1); +end + + +local blockRewindRef = nil + +local function WriteBack(km, track, take, notes_to_shorten, triggered_by_key_event) + + local c = GEN.BuildBackwardContext(km, track, take) + + if c.rewindTime < c.itemStartTime then + c.rewindTime = c.itemStartTime + end + + -- If we're at the start of an item don't move things + if math.abs(c.itemStartPPQ - c.cursorPPQ) < D.PPQ_TOLERANCE then + return + end + + reaper.Undo_BeginBlock(); + + GEN.GenericDelete(c, notes_to_shorten, true, false) + + local blockRewind = false + local triggeredByBackAction = AT.hasBackwardActionTrigger() + local pedalStart = km:keyActivityForTrack(track).pedal.first_ts + local hadCandidates = (#notes_to_shorten > 0) + local failedToErase = (hadCandidates and (c.counts.sh + c.counts.rem == 0)) + + if (not triggeredByBackAction) then + -- We block the rewind in certain conditions (when erasing failed, and when during this pedal session, the erasing was blocked) + + local cond1 = S.getSetting("DoNotRewindOnStepBackIfNothingErased") and failedToErase + local cond2 = (not hadCandidates) and (pedalStart == blockRewindRef) + + blockRewind = cond1 or cond2 + end + + local jumpTime = nil + if blockRewind then + blockRewindRef = pedalStart + else + if (not hadCandidates) or (not failedToErase) then + jumpTime = c.rewindTime + end + end + + GEN.BackwardOperationFinish(c, jumpTime) + + reaper.Undo_EndBlock(GEN.OperationSummary(-1, c.counts), -1) +end + +return { + Write = Write, + WriteBack = WriteBack +} + diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_compress.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_compress.lua new file mode 100644 index 000000000..aafb4c1a1 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_compress.lua @@ -0,0 +1,17 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +return "\z +\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x14\x00\x00\x00\x14\x08\x06\x00\x00\x00\x8D\x89\x1D\x0D\x00\x00\x00\x09\x70\x48\x59\z +\x73\x00\x00\x0B\x13\x00\x00\x0B\x13\x01\x00\x9A\x9C\x18\x00\x00\x01\x38\x49\x44\x41\x54\x38\x8D\x9D\xD4\x3D\x4B\x03\x31\x1C\x06\xF0\x9F\xD7\x83\x22\x74\xD3\x49\z +\x5C\x4A\xC1\xA1\x20\x74\x76\x50\x5C\x5D\xFB\x55\x9C\x74\x28\xF7\x81\x5C\xDD\xF5\x13\x38\x39\x08\xD2\xC5\x51\x37\x41\x04\x8B\x0E\x97\xC3\x5C\x4C\x4E\xE8\x03\xC7\z +\x85\x3C\x2F\x79\xFB\x27\x3B\x57\xDF\xD7\x0A\x98\xE1\x02\x7B\x49\xFF\x1B\x6E\xF1\x9C\x33\x55\xA5\x34\x2C\xA2\xB0\x55\xF8\x84\xBE\x45\xC9\x34\x14\x38\x8D\xC2\x24\z +\xED\xA9\x02\x86\x02\x77\xB7\xE1\xEA\xA8\x3D\xC3\x21\xF6\xF1\x8A\xD1\x40\xE0\x08\x67\x91\xF6\x45\xD8\xD3\x38\x70\x81\x23\x8C\x07\x82\x62\x9C\x87\xFF\x27\x9E\xBA\z +\xC0\x0A\x4B\x5C\xE2\x38\x84\xAD\x12\x63\x53\x68\x0B\xDA\x71\xF0\x5E\x62\x59\x61\x8E\x49\x46\xF8\x1F\x52\xCD\x04\xF3\x5A\xBB\x1F\x31\xD9\x44\x86\x74\x46\x32\x5C\z +\xCF\x1B\x9F\x72\xE3\xEF\xF2\x72\x33\x4D\x07\xEA\xF9\xAA\x44\x98\xD6\x5C\x6E\x86\xE9\x40\x3D\x5F\x8D\x4D\x62\xEC\xC8\xD2\x72\xD3\xD0\x58\xB7\xA9\xF0\x88\xF7\x8C\z +\xE1\x3F\xA4\x9A\x77\x3C\xD6\xB8\x09\x1D\x4B\x6D\x1D\x76\xC2\x8D\x72\x71\x77\x5C\xE3\xB7\x0E\x6F\xE8\x17\xF6\x83\xB6\xEA\xBB\xEA\x3F\xF1\xB7\x02\xBA\x7D\xFD\xC2\z +\xBD\xFE\x4D\x91\x06\x3E\xEB\x3F\x49\xA7\x85\xD9\x75\xBE\xBB\x1C\x31\xF4\x38\x6C\x85\xA1\xC0\x8F\x6D\xB8\xA1\xC0\x75\xF8\xE7\xEE\xF2\x5A\x01\x75\x89\xD0\x1E\xD2\z +\x81\xF6\x85\x8E\x43\xDF\x02\x97\xC5\x0F\x7D\xD9\x43\xF2\x55\xBB\x37\x49\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" +; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_stretch.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_stretch.lua new file mode 100644 index 000000000..eb554eeca --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_stretch.lua @@ -0,0 +1,15 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +return "\z +\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x14\x00\x00\x00\x14\x08\x06\x00\x00\x00\x8D\x89\x1D\x0D\x00\x00\x00\x09\x70\x48\x59\z +\x73\x00\x00\x0B\x13\x00\x00\x0B\x13\x01\x00\x9A\x9C\x18\x00\x00\x00\xEE\x49\x44\x41\x54\x38\x8D\xAD\x94\x3D\x12\x82\x40\x0C\x46\x9F\xC0\x0D\xB0\xB2\xA5\xF7\x08\z +\x1E\xC0\x23\x69\xC1\x70\x29\x0E\xE0\x11\xB0\xB6\x73\xAC\xE4\x08\x8E\x16\x64\x35\xAC\xD9\xE0\x0F\x5F\x15\x36\xC9\x63\x33\xE4\x63\xB1\xBB\xEF\x2B\x60\x0B\x94\xFC\z +\xA7\x1E\x68\x8B\x08\x56\xFF\x08\x6B\x84\xB1\xCD\x66\x80\xE9\xDE\x32\x9B\x01\x36\x82\x66\x53\x55\x0C\xE3\x58\xB1\xA9\x29\xA0\x05\x70\xA1\x1E\xD0\x6B\x4C\xE6\x52\z +\xC0\xD0\x70\x53\xCF\xD6\xD9\x47\xC0\x50\xD8\x03\x07\xE0\x28\x71\x2F\xF1\x41\x62\x13\x5A\x38\xB0\x16\x38\x25\x26\x38\xF3\xDA\xDF\x06\xB5\x25\xFA\x86\xFA\x6D\x9D\z +\x03\x43\x72\x9D\xD5\xAB\x81\x7A\x17\xD7\x40\xE5\x00\x2B\xA9\x79\xEB\x8D\x47\xAE\x51\x36\x92\x5B\x2C\x81\x95\xE4\x2F\xC0\x55\x60\xA6\xC3\x62\x60\x0C\xDD\x00\xB9\z +\xCA\x95\x0C\x5F\x39\x57\xB5\x23\xA5\xD6\x26\x14\xEA\x46\xEB\xEC\x4D\xDE\x62\x7B\xFE\x4E\xE6\xA6\xAC\x67\x35\xBA\x3F\x92\x00\xF4\x6C\x56\x27\xE2\x58\x4D\x00\x26\z +\xB7\xFE\x0B\x3D\x0D\x51\x30\x38\x42\x6F\xFD\xAF\xEA\x81\xF6\x01\xC4\x49\x36\x31\x8F\xD8\x18\xC3\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" +; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_stuff.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_stuff.lua new file mode 100644 index 000000000..e002acd00 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_stuff.lua @@ -0,0 +1,18 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +return "\z +\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x14\x00\x00\x00\x14\x08\x06\x00\x00\x00\x8D\x89\x1D\x0D\x00\x00\x00\x09\x70\x48\x59\z +\x73\x00\x00\x0B\x13\x00\x00\x0B\x13\x01\x00\x9A\x9C\x18\x00\x00\x01\x47\x49\x44\x41\x54\x38\x8D\x95\x95\xC1\x4A\xC4\x30\x10\x40\x9F\x6D\xA4\x08\x82\x0B\x0A\xC2\z +\x22\x0B\x52\xF0\xD0\x83\xEC\xD9\x8B\x7F\xE1\xCF\x78\x08\xFD\x29\xBF\x63\x4F\x1E\x84\xC5\x8B\x78\xD2\x83\xA7\x65\x0F\x45\x0F\xC9\xE0\x64\x48\xB2\xDD\x81\x85\x26\z +\x99\xBE\xBC\x66\x3A\xDD\x93\xDF\xE7\x1F\x8E\x88\x1E\x58\x03\xB7\xC0\x19\xD0\xC6\xF9\x09\xD8\x01\xEF\xEE\x18\x5A\x84\x0D\x0A\x24\xD1\x02\xE7\xC0\x60\x81\x62\xB0\z +\x8C\xE3\x4F\x60\x03\x6C\xE3\x78\x19\x6F\xF6\x85\x0D\x47\x0D\xEC\x81\x47\x60\xA5\xE6\x2E\x81\x8B\x78\xBD\x25\x8D\xD1\x8C\x3D\x40\xA3\x26\xD6\x0A\xE6\x95\xC5\x2A\z +\xAE\x89\xB1\xC0\xB2\x96\xDA\x50\x1E\xD3\xAB\xDD\xE5\x7A\x00\xBE\xE2\x6F\x0F\x74\x39\x98\x35\x2C\x26\x11\xCE\xED\x01\xB8\x02\xDE\x6A\x96\x02\xEC\x81\x53\x93\x68\z +\x4D\x3B\xE0\x8E\xFF\x33\x45\xAD\x25\x40\x29\x46\x97\x49\xF0\x66\xDC\x91\x16\xCD\x16\x06\x07\xDC\x00\xD7\x0A\x30\x92\xDA\xD9\xA8\xAD\xD1\x10\xCE\xC5\x1D\x4A\x9C\z +\x1B\x8E\x50\xB9\x5D\x06\x56\x7A\x79\x0F\x02\x3F\x80\x7B\x42\xEB\x68\xD0\x1C\xDB\x62\x95\x17\x33\x61\x53\x66\x2E\xC9\x75\x84\x2E\x68\x33\x09\xDF\xC0\x0B\x69\xCB\z +\xF5\xC0\x13\xA1\xDA\xD9\x33\x6F\x08\x5D\xA0\xED\xC4\xC4\xC2\x88\x9B\xD7\x1A\x80\xA6\x60\xF7\x9A\x81\x41\xBE\x3D\x93\x70\x2A\x41\xDB\x6D\x6A\x16\x99\x7B\x12\xE0\z +\x5E\xED\x36\x55\xEC\x20\x7C\x6D\x16\x25\x3B\x60\x72\x84\x66\xB7\x1F\xD4\x52\xC8\x5A\xF1\x2F\xE0\x0F\x86\xEF\x45\x9B\xE4\x3C\x10\x8C\x00\x00\x00\x00\x49\x45\x4E\z +\x44\xAE\x42\x60\x82" +; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_unstuff.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_unstuff.lua new file mode 100644 index 000000000..0ddfbbe45 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_unstuff.lua @@ -0,0 +1,18 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @license MIT +-- @description This is part of One Small Step + +return "\z +\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52\x00\x00\x00\x14\x00\x00\x00\x14\x08\x06\x00\x00\x00\x8D\x89\x1D\x0D\x00\x00\x00\x09\x70\x48\x59\z +\x73\x00\x00\x0B\x13\x00\x00\x0B\x13\x01\x00\x9A\x9C\x18\x00\x00\x01\x49\x49\x44\x41\x54\x38\x8D\x9D\x95\xB1\x6A\x02\x41\x10\x40\x9F\xF1\x40\x42\x0E\x12\x62\x20\z +\x20\x41\x90\xEB\xAC\xFC\x83\x7C\x84\x75\x3E\x21\x5D\x3A\x21\xC7\x7D\x90\x1F\x91\x3A\x4D\x48\x61\x77\xD8\x88\x95\x88\x42\x42\x2A\x31\xC5\xCD\xC6\xB9\xC9\xEE\xDD\z +\xE2\xC0\xB2\x33\xB3\xBB\xEF\x66\x66\x87\xBD\xCE\x71\xB6\xE7\x0C\xC9\x80\x09\x30\x02\x2E\xC5\xF7\x03\x2C\x93\x73\x68\x02\x1B\x03\x5D\xE5\x4B\x81\x71\x08\xE8\x22\z +\x18\x88\xBD\x06\x3E\x80\x52\xEC\x91\xC0\x72\x73\xAE\xF0\x01\x33\xE0\x11\x18\x2A\x5F\x1F\xB8\x16\xBD\xE4\x94\x26\x40\x21\x73\x0E\x70\xE1\x01\x4E\x14\x2C\x57\x51\z +\x0C\x65\x0D\xEA\xA9\xD6\xC4\x07\x74\x69\xE6\xF2\xF5\x42\x41\x07\x9E\xFD\xAD\xC0\x26\xB9\xA1\x2A\x49\x50\x6C\x0D\x33\xA0\x27\xBA\x8E\xCC\xD5\xA9\x0B\x3C\x35\x01\z +\x75\x84\xEE\x32\x52\xCF\x3E\x5D\xCB\x46\x71\x11\xDA\x9B\xD5\x87\x0B\xA5\x87\xFC\xFF\x80\x0F\xC0\xBD\x3A\x54\x6B\x85\x36\x88\x16\x97\xF2\x9D\xC0\x35\xCC\x17\x15\z +\xC0\x21\x06\xB8\x01\x76\xC0\x6B\x00\xA4\xF5\x5D\x0C\x70\x05\x7C\x02\x0B\xB3\xEE\xFA\x50\x43\xFB\x31\xC0\x12\x78\x03\xE6\xC0\x97\x67\x5F\xA8\xA6\x5E\xE0\x14\x78\z +\x96\x31\x05\xB6\x0A\xD2\x7A\xAB\x56\x12\xAA\x97\xC3\xF5\x5E\x0F\x58\xC6\x1C\x6C\x8A\x30\xE5\xD4\xB8\x0E\xAC\xD3\xB6\x69\xFA\xEC\x3F\x5F\xE7\x38\xDB\xBF\x18\xD0\z +\x3B\x55\x1B\xD9\x07\x34\x46\x0E\x09\x55\x8A\xFA\x21\x5D\xC9\x40\xF9\xBF\x65\xBE\x32\xF6\x2D\xE6\x17\xF0\x0B\x6C\x59\x3E\x34\xAF\x4F\x3F\x20\x00\x00\x00\x00\x49\z +\x45\x4E\x44\xAE\x42\x60\x82" +;