From 22e3d58a4503492c434a726bed640cd4ccaf12ff Mon Sep 17 00:00:00 2001 From: BenTalagan Date: Tue, 12 Mar 2024 12:07:28 +0100 Subject: [PATCH] Update OneSmallStep v0.9.6 > v0.9.7 --- MIDI Editor/talagan_OneSmallStep.lua | 357 +-- .../talagan_OneSmallStep Change edit mode.lua | 11 + .../classes/engine_lib.lua | 203 +- .../classes/lib/MIDIUtils.lua | 1987 +++++++++++++++++ .../images/edit_mode_repitch.lua | 13 + .../images/indicator_insert_back.lua | 12 +- .../images/indicator_insert_forward.lua | 14 +- .../images/indicator_repitch_back.lua | 16 + .../images/indicator_repitch_forward.lua | 16 + .../images/indicator_write_back.lua | 14 +- .../images/indicator_write_forward.lua | 12 +- 11 files changed, 2469 insertions(+), 186 deletions(-) create mode 100644 MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua create mode 100644 MIDI Editor/talagan_OneSmallStep/classes/lib/MIDIUtils.lua create mode 100644 MIDI Editor/talagan_OneSmallStep/images/edit_mode_repitch.lua create mode 100644 MIDI Editor/talagan_OneSmallStep/images/indicator_repitch_back.lua create mode 100644 MIDI Editor/talagan_OneSmallStep/images/indicator_repitch_forward.lua diff --git a/MIDI Editor/talagan_OneSmallStep.lua b/MIDI Editor/talagan_OneSmallStep.lua index ae268f48d..b97ad1077 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.6 +@version 0.9.7 @author Ben 'Talagan' Babut @license MIT @metapackage @@ -9,6 +9,11 @@ [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode - KeyboardPress.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode - KeyboardRelease.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change input mode - Punch.lua + [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode - Write.lua + [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode - Navigate.lua + [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode - Replace.lua + [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode - Insert.lua + [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode - Repitch.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source - OSS.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source - ItemConf.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Change note len param source - ProjectGrid.lua @@ -34,6 +39,8 @@ [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action - ReplaceBack.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action - Navigate.lua [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action - NavigateBack.lua + [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action - Repitch.lua + [main=main,midi_editor] talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action.lua > talagan_OneSmallStep/actions/talagan_OneSmallStep Edit Action - RepitchBack.lua [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 @@ -47,27 +54,12 @@ @screenshot https://stash.reaper.fm/48269/oss_094.png @changelog - - [Feature] Added Replace mode - - [Feature] Added Navigate mode - - [Feature] Added auto-scroll arrange view option - - [Feature] [All Input Modes] Handle grid size for note length with modifier factor - - [Feature] [All Input Modes] Handle swing for grid size note length - - [Feature] [Navigate] Snap on project grid (with swing) - - [Feature] [Navigate] Snap on item grid (with swing) - - [Feature] [Navigate] Snap on note start/ends - - [Feature] [Navigate] Snap on item bounds - - [Feature] [Navigate] Added option to allow navigation on key events (does not input notes) - - [Feature] [Write] Step back delete/shortening now happens on every key press/release event (notes should match keys) - - [Feature] [Write] Added option to prevent the cursor from being moved back if step back delete fails (notes don't match keys, the user missed) - - [Feature] [Insert] Step back delete can now make holes - - [Feature] Added system to engage modes with buttons or with customizable modifiers - - [Rework] [Write] Reworked Delete/Step back logic - - [Rework] [Insert] Reworked Delete/Step back logic - - [Rework] Removed option "do not add notes if step back modifier key is pressed", not pertinent anymore - - [Rework] Removed option "erase note ends even if they do not align on cursor", since the eraser does more complex things, it does not fit in the new flow - - [Bug Fix] n-tuplets always used a value of 2/n, now using precpow2(n)/n - - [Bug Fix] Create new items when advancing only if insert mode is on - - [Bug fix] Icons/Images coould be randomly wrong + - [Feature] Added repitch (+revel) mode (thanks @smandrap !) + - [Fix] Added missing "Change Edit Mode" actions + - [Doc] Added help button that redirects to current documentation + - [Rework] Re arranged settings panel + - [Rework] Reworked some icons and colors + - [Rework] Started to use MIDI Utils API by sockmonkey72 instead of default MIDI API @about # Purpose @@ -83,33 +75,24 @@ # Documentation - Since the documentation is growing, it is now centralized on the forum thread. + The documentation for all versions is located here : https://bentalagan.github.io/onesmallstep-doc/ # Credits This tool takes a lot of inspiration in tenfour's "tenfour-step" scripts. Epic hail to tenfour for opening the way ! + One Small Step uses Jeremy Bernstein (sockmonkey72)'s MIDIUtils library . Thanks for the precious work ! + Thanks to @cfillion for the precious pieces of advice when reviewing this source ! A lot of thanks to all donators, and forum members that help this tool to get better ! @stevie, @hipox, @MartinTL, @henu, @Thonex, @smandrap, @SoaSchas, @daodan, @inthevoid, @dahya, @User41, @Spookye, @R.Cato --]] --------------------------------- - ---[[ -# Ruby script to convert from png > lua to load binary img for ReaImGui - -def png_to_lua(fname) - buf = File.open(fname,"rb").read.unpack("C*").map{ |c| "\\x%02X" % c }.each_slice(40).map{ |s| s.join }.join("\\z\n") - buf = "return \"\\z\n" + buf + "\"\n;\n" - outname = File.basename(fname,".png") + ".lua" - File.open(outname, "wb") { |f| f << buf } -end - -png_to_lua("triplet.png") +VERSION = "0.9.7" +DOC_URL = "https://bentalagan.github.io/onesmallstep-doc/index.html?ver=" .. VERSION ---]] +-------------------------------- -- Tell the script to be terminated if relaunched. -- Check the existence of the function for sanity (added in v 7.03) @@ -121,23 +104,53 @@ end -- Path and modules package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])[^\/]-$]] .."?.lua;".. package.path -local engine_lib = require "talagan_OneSmallStep/classes/engine_lib"; ------------------------------- -- Check dependencies -if not reaper.APIExists("JS_ReaScriptAPI_Version") then - local answer = reaper.MB( "You have to install JS_ReaScriptAPI for this script to work. Right-click the entry in the next window and choose to install.", "JS_ReaScriptAPI not installed", 0 ) - reaper.ReaPack_BrowsePackages( "js_ReaScriptAPI" ) - return +local function CheckReapack(func_name, api_name, search_string) + if not reaper.APIExists(func_name) then + local answer = reaper.MB( api_name .. " is required and you need to install it.\z + Right-click the entry in the next window and choose to install.", + api_name .. " not installed", 0 ) + reaper.ReaPack_BrowsePackages( search_string ) + exit(66) + end end -if not reaper.APIExists("ImGui_CreateContext") then - local answer = reaper.MB( "You have to install ReaImGui for this script to work. Right-click the entry in the next window and choose to install.", "ReaImGUI not installed", 0 ) - reaper.ReaPack_BrowsePackages( "ReaImGui:" ) +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 + +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 + +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 engine_lib = require "talagan_OneSmallStep/classes/engine_lib"; + ------------------------------- -- ImGui Backward compatibility @@ -151,7 +164,7 @@ local ctx = reaper.ImGui_CreateContext('One Small Step'); local images = {}; -function getImage(image_name) +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) images[image_name] = reaper.ImGui_CreateImageFromMem(bin) @@ -172,6 +185,12 @@ function SL() reaper.ImGui_SameLine(ctx); end +function SEP(txt) + reaper.ImGui_PushStyleColor(ctx, reaper.ImGui_Col_Text(), 0xA0A0A0FF); + reaper.ImGui_SeparatorText(ctx, txt) + reaper.ImGui_PopStyleColor(ctx); +end + function XSeparator() reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx) + 2) ; reaper.ImGui_Text(ctx, "x "); end @@ -360,13 +379,10 @@ function ButtonGroupImageButton(image_name, is_on, options) end - function ButtonGroupTextButton(text, is_on, callback) - if reaper.ImGui_Button(ctx, text) then callback(); - end; - + end end function ImGui_NoteLenImg(context, image_name, triplet, divider) @@ -608,7 +624,6 @@ function NoteLenModifierMiniBar(with_fracs) 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); @@ -708,7 +723,7 @@ function PlayBackMeasureCountComboBox() reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_FramePadding(), 5, 3.5); local curm = engine_lib.getPlaybackMeasureCount(); - local function label(mnum) + local label = function(mnum) return ((mnum == -1) and "Mk" or mnum); end @@ -740,7 +755,7 @@ function PlayBackMeasureCountComboBox() end -function PlaybackButton() +local function PlaybackButton() reaper.ImGui_PushID(ctx, "playback"); if ButtonGroupImageButton("playback", false, { colorset = "Green" } ) then local id = reaper.NamedCommandLookup("_RS0bbcbcb0cb7174a2406403352d006c0573c4c8b4"); @@ -751,7 +766,7 @@ function PlaybackButton() TT("Playback"); end -function PlaybackSetMarkerButton() +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 @@ -764,7 +779,7 @@ function PlaybackSetMarkerButton() end -function MagnetMiniBar() +local function MagnetMiniBar() local label = "##snap" @@ -805,6 +820,7 @@ function EditModeMiniBar() { 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)" }, } @@ -901,17 +917,7 @@ function SettingSlider(setting, in_label, out_label, tooltip, use_help_interroga end end -function TargetLine(take) - - reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_ItemSpacing(), 2, 4); - reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_ItemInnerSpacing(), 0, 0); - - PlaybackWidget(); SL() - MiniBarSeparator(); SL() - - - reaper.ImGui_PopStyleVar(ctx,2); - +function TargetModeInfo() local currentop = engine_lib.resolveOperationMode() if currentop.mode == "Insert" then @@ -932,6 +938,12 @@ function TargetLine(take) else reaper.ImGui_Image(ctx, getImage("indicator_navigate_forward"),20,20); SL(); TT("Navigate forward") SL(); end + elseif currentop.mode == "Repitch" then + if currentop.back then + reaper.ImGui_Image(ctx, getImage("indicator_repitch_back"),20,20); SL(); TT("Write back (selective delete") SL(); + else + reaper.ImGui_Image(ctx, getImage("indicator_repitch_forward"),20,20); SL(); TT("Write (add notes)") SL(); + end else if currentop.back then reaper.ImGui_Image(ctx, getImage("indicator_write_back"),20,20); SL(); TT("Write back (selective delete") SL(); @@ -939,7 +951,18 @@ function TargetLine(take) reaper.ImGui_Image(ctx, getImage("indicator_write_forward"),20,20); SL(); TT("Write (add notes)") SL(); end end +end + + +function TargetLine(take) + + reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_ItemSpacing(), 2, 4); + reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_ItemInnerSpacing(), 0, 0); + + PlaybackWidget(); SL() + MiniBarSeparator(); SL() + reaper.ImGui_PopStyleVar(ctx,2); SL(); @@ -947,6 +970,7 @@ function TargetLine(take) if engine_lib.getSetting("AllowCreateItem") then local track = engine_lib.TrackForEditionIfNoItemFound(); if track then + TargetModeInfo() TrackInfo(track); else reaper.ImGui_TextColored(ctx, 0xA0A0A0FF, "No target item or track."); @@ -956,6 +980,7 @@ function TargetLine(take) end ImGui_VerticalSpacer(ctx,0); else + TargetModeInfo() TakeInfo(take); end end @@ -1014,7 +1039,7 @@ end function ClearConflictingModifierKeys() local sbmk = engine_lib.getSetting("StepBackModifierKey") - local editmodes = { "Navigate", "Replace", "Insert" } + local editmodes = { "Navigate", "Replace", "Insert", "Repitch" } for k,v in ipairs(editmodes) do local setting = v.."ModifierKeyCombination" @@ -1107,12 +1132,45 @@ function EditModeComboBox(editModeName, callback) reaper.ImGui_Text(ctx, "edit mode") end +function RepitchModeComboBox() + + local setting = "RepitchModeAffects" + local combo_items = engine_lib.getSettingSpec("RepitchModeAffects").inclusion + local curval = engine_lib.getSetting("RepitchModeAffects") + + reaper.ImGui_PushStyleVar(ctx, reaper.ImGui_StyleVar_FramePadding(), 5, 3.5) + reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx) + 3) + + reaper.ImGui_Text(ctx, "Repitch mode affects"); + SL(); + reaper.ImGui_SetNextItemWidth(ctx, 180); + reaper.ImGui_SetCursorPosY(ctx, reaper.ImGui_GetCursorPosY(ctx) - 3) + + reaper.ImGui_PushID(ctx, setting .. "_combo") + 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(setting, v); + end + if is_selected then + reaper.ImGui_SetItemDefaultFocus(ctx) + end + end + reaper.ImGui_EndCombo(ctx) + 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() if reaper.ImGui_BeginTabBar(ctx, 'settings_tab_bar', reaper.ImGui_TabBarFlags_None()) then reaper.ImGui_PushStyleColor(ctx, reaper.ImGui_Col_Tab(), 0x00000000); reaper.ImGui_PushStyleColor(ctx, reaper.ImGui_Col_TabHovered(), 0x00000000); - if reaper.ImGui_TabItemButton(ctx, 'Settings', reaper.ImGui_TabItemFlags_Leading() | reaper.ImGui_TabItemFlags_NoTooltip()) then + if reaper.ImGui_TabItemButton(ctx, 'Settings##settings_tab', reaper.ImGui_TabItemFlags_Leading() | reaper.ImGui_TabItemFlags_NoTooltip()) then end reaper.ImGui_PopStyleColor(ctx, 2); @@ -1150,6 +1208,58 @@ function SettingsPanel() reaper.ImGui_EndTabItem(ctx) end + if reaper.ImGui_BeginTabItem(ctx, 'Input') then + ImGui_VerticalSpacer(ctx,5); + SEP("Key Press input mode") + + SettingSlider("KeyPressModeAggregationTime", + "%.3f seconds", + "Chord Aggregation", + "Key press events happening within this time\nwindow are aggregated as a chord", + true, nil) + + SettingSlider("KeyPressModeInertiaTime", + "%.3f seconds", + "Sustain Inertia", + "If key A is pressed, and then key B is pressed but\n\z + key A was still held for more than this time,\n\z + then A is considered sustained and not released.\n\n\z + This setting allows to enter new notes overlapping sustained notes.", + true, nil) + SL() + curval = engine_lib.getSetting("KeyPressModeInertiaEnabled"); + if reaper.ImGui_Checkbox(ctx, "Enabled##kp_inertia", curval) then + engine_lib.setSetting("KeyPressModeInertiaEnabled", not curval); + end + + ImGui_VerticalSpacer(ctx,5); + SEP("Key Release input mode") + + SettingSlider("KeyReleaseModeForgetTime", + "%.3f seconds", + "Forget time", + "How long a key should be remembered after release,\n\z + if other keys are still pressed.\n\n\z + This is used to know if a note should be forgotten/trashed\n\z + or used as part of the input chord.", + true, nil) + + ImGui_VerticalSpacer(ctx,5); + SEP("Sustain Pedal") + + curval = engine_lib.getSetting("PedalRepeatEnabled"); + if reaper.ImGui_Checkbox(ctx, "Pedal repeat every", curval) then + engine_lib.setSetting("PedalRepeatEnabled", not curval); + end + SL(); + SettingSlider("PedalRepeatTime", "%.3f seconds", "and", "Repeat time for the pedal event when pressed", false, 120) + SL(); + SettingSlider("PedalRepeatFirstHitMultiplier", "x %.d", "on first hit", "Multiplication factor for first hit", false, 50) + + + reaper.ImGui_EndTabItem(ctx) + end + if reaper.ImGui_BeginTabItem(ctx, 'Controls') then ImGui_VerticalSpacer(ctx,5); @@ -1158,6 +1268,7 @@ function SettingsPanel() EditModeComboBox("Navigate") EditModeComboBox("Insert") EditModeComboBox("Replace") + EditModeComboBox("Repitch") curval = engine_lib.getSetting("HideEditModeMiniBar"); if reaper.ImGui_Checkbox(ctx, "Hide edit mode mini bar", curval) then @@ -1171,79 +1282,39 @@ function SettingsPanel() also want to keep the ability to toggle a mode with a\n\z mouse click). Up to you!") - - curval = engine_lib.getSetting("PedalRepeatEnabled"); - if reaper.ImGui_Checkbox(ctx, "Pedal repeat every", curval) then - engine_lib.setSetting("PedalRepeatEnabled", not curval); - end - SL(); - SettingSlider("PedalRepeatTime", "%.3f seconds", "and", "Repeat time for the pedal event when pressed", false, 120) - SL(); - SettingSlider("PedalRepeatFirstHitMultiplier", "x %.d", "on first hit", "Multiplication factor for first hit", false, 50) - reaper.ImGui_EndTabItem(ctx) end - if reaper.ImGui_BeginTabItem(ctx, 'Stepping') then + if reaper.ImGui_BeginTabItem(ctx, 'Editing') then ImGui_VerticalSpacer(ctx,5); - - curval = engine_lib.getSetting("DoNotRewindOnStepBackIfNothingErased"); - if reaper.ImGui_Checkbox(ctx, "Do not rewind when trying to erase with some pressed keys but nothing was erased", curval) then - engine_lib.setSetting("DoNotRewindOnStepBackIfNothingErased", not curval); - end - - curval = engine_lib.getSetting("AllowKeyEventNavigation"); - if reaper.ImGui_Checkbox(ctx, "Allow navigating on key press/release events", curval) then - engine_lib.setSetting("AllowKeyEventNavigation", not curval); - end + SEP("All edit modes") curval = engine_lib.getSetting("AutoScrollArrangeView"); if reaper.ImGui_Checkbox(ctx, "Auto-scroll arrange view after editing/navigating", curval) then engine_lib.setSetting("AutoScrollArrangeView", not curval); end + curval = engine_lib.getSetting("AllowKeyEventNavigation"); + if reaper.ImGui_Checkbox(ctx, "Allow navigating on controller key press/release", curval) then + engine_lib.setSetting("AllowKeyEventNavigation", not curval); + end - reaper.ImGui_EndTabItem(ctx) - end - - if reaper.ImGui_BeginTabItem(ctx, 'KP Mode') then ImGui_VerticalSpacer(ctx,5); + SEP("Write mode") - SettingSlider("KeyPressModeAggregationTime", - "%.3f seconds", - "Chord Aggregation", - "Key press events happening within this time\nwindow are aggregated as a chord", - true, nil) - - SettingSlider("KeyPressModeInertiaTime", - "%.3f seconds", - "Sustain Inertia", - "If key A is pressed, and then key B is pressed but\n\z - key A was still held for more than this time,\n\z - then A is considered sustained and not released.\n\n\z - This setting allows to enter new notes overlapping sustained notes.", - true, nil) - - SL(); - - curval = engine_lib.getSetting("KeyPressModeInertiaEnabled"); - if reaper.ImGui_Checkbox(ctx, "Enabled##kp_inertia", curval) then - engine_lib.setSetting("KeyPressModeInertiaEnabled", not curval); + curval = engine_lib.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); end - reaper.ImGui_EndTabItem(ctx) - end - - if reaper.ImGui_BeginTabItem(ctx, 'KR Mode') then ImGui_VerticalSpacer(ctx,5); + SEP("Repitch mode") - SettingSlider("KeyReleaseModeForgetTime", + RepitchModeComboBox() + SettingSlider("RepitchModeAggregationTime", "%.3f seconds", - "Forget time", - "How long a key should be remembered after release,\n\z - if other keys are still pressed.\n\n\z - This is used to know if a note should be forgotten/trashed\n\z - or used as part of the input chord.", + "Repitch chord aggregation", + "Notes that fit in that time window are aggregated as a chord", true, nil) reaper.ImGui_EndTabItem(ctx) @@ -1256,6 +1327,10 @@ function SettingsPanel() reaper.ImGui_EndTabItem(ctx) end + if reaper.ImGui_TabItemButton(ctx, "?##go_to_help") then + reaper.CF_ShellExecute(DOC_URL) + end + reaper.ImGui_EndTabBar(ctx) end end @@ -1295,7 +1370,7 @@ function ui_loop() -- 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); - local visible, open = reaper.ImGui_Begin(ctx, 'One Small Step v0.9.6', true, flags); + local visible, open = reaper.ImGui_Begin(ctx, 'One Small Step v' .. VERSION, true, flags); reaper.ImGui_PopStyleColor(ctx,1); if visible then @@ -1331,28 +1406,20 @@ function ui_loop() MagnetMiniBar(); SL() MiniBarSeparator(); SL(); - ConfSourceMiniBar(); SL(); - MiniBarSeparator(); SL(); - - if nlm == engine_lib.NoteLenParamSource.OSS then - - NoteLenOptions(false) - - elseif nlm == engine_lib.NoteLenParamSource.ProjectGrid then - - ProjectGridLabel(ctx); SL() - XSeparator(); SL(); - NoteLenOptions(true) - - - elseif nlm == engine_lib.NoteLenParamSource.ItemConf then - - ItemGridLabel(ctx,take); SL() - XSeparator(); SL(); - NoteLenOptions(true) - - end + ConfSourceMiniBar(); SL(); + MiniBarSeparator(); SL(); + if nlm == engine_lib.NoteLenParamSource.OSS then + NoteLenOptions(false) + elseif nlm == engine_lib.NoteLenParamSource.ProjectGrid then + ProjectGridLabel(ctx); SL() + XSeparator(); SL(); + NoteLenOptions(true) + elseif nlm == engine_lib.NoteLenParamSource.ItemConf then + ItemGridLabel(ctx,take); SL() + XSeparator(); SL(); + NoteLenOptions(true) + end reaper.ImGui_PopStyleVar(ctx,3); 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 new file mode 100644 index 000000000..7fb8881cb --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/actions/talagan_OneSmallStep Change edit mode.lua @@ -0,0 +1,11 @@ +-- @noindex +-- @author Ben 'Talagan' Babut +-- @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$"); + + +engine_lib.setSetting("EditMode", param) diff --git a/MIDI Editor/talagan_OneSmallStep/classes/engine_lib.lua b/MIDI Editor/talagan_OneSmallStep/classes/engine_lib.lua index c90c263e0..86faaa0f0 100644 --- a/MIDI Editor/talagan_OneSmallStep/classes/engine_lib.lua +++ b/MIDI Editor/talagan_OneSmallStep/classes/engine_lib.lua @@ -8,14 +8,17 @@ local upperDir = scriptDir:match( "((.*)[\\/](.+)[\\/])(.+)$" ); package.path = scriptDir .."?.lua;".. package.path -local helper_lib = require "helper_lib"; +-- 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 @@ -60,6 +63,7 @@ local EditMode = { Write = "Write", Navigate = "Navigate", Insert = "Insert", + Repitch = "Repitch", Replace = "Replace" } @@ -71,11 +75,13 @@ local ActionTriggers = { 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 = { @@ -111,7 +117,7 @@ end for i=1, #ModifierKeys do local m1 = ModifierKeys[i] for j=i+1, #ModifierKeys do - m2 = ModifierKeys[j] + local m2 = ModifierKeys[j] ModifierKeyCombinations[#ModifierKeyCombinations+1] = { label = m1.name .. "+" .. m2.name, id = "" .. m1.vkey .. "+" .. m2.vkey, vkeys = { m1.vkey, m2.vkey } } end end @@ -130,6 +136,7 @@ local SettingDefs = { 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 }, @@ -160,6 +167,9 @@ local SettingDefs = { 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 }, @@ -594,6 +604,7 @@ local function resolveOperationMode(look_for_action_triggers) { name = "Write", prio = 4 }, { name = "Navigate", prio = 3 }, { name = "Insert", prio = 2 }, + { name = "Repitch", prio = 2.5}, { name = "Replace", prio = 1 }, } @@ -824,8 +835,17 @@ local function CreateItemIfMissing(track) end -local function GetNote(take, ni) - local _, selected, muted, startPPQ, endPPQ, chan, pitch, vel = reaper.MIDI_GetNote(take, ni); +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, @@ -836,7 +856,8 @@ local function GetNote(take, ni) startPPQ = startPPQ, startQN = reaper.MIDI_GetProjQNFromPPQPos(take, startPPQ), endPPQ = endPPQ, - endQN = reaper.MIDI_GetProjQNFromPPQPos(take, endPPQ) + endQN = reaper.MIDI_GetProjQNFromPPQPos(take, endPPQ), + offvel = offvel }; end @@ -849,16 +870,16 @@ local function bool2sign(b) end local function noteStartsAfterPPQ(note, limit, strict) - return note.startPPQ > limit + bool2sign(strict) * PPQ_TOLERANCE + return note.startPPQ > (limit + bool2sign(strict) * PPQ_TOLERANCE) end local function noteStartsBeforePPQ(note, limit, strict) - return note.startPPQ < limit - bool2sign(strict) * PPQ_TOLERANCE + return note.startPPQ < (limit - bool2sign(strict) * PPQ_TOLERANCE) end local function noteEndsAfterPPQ(note, limit, strict) - return note.endPPQ > limit + bool2sign(strict) * PPQ_TOLERANCE + return note.endPPQ > (limit + bool2sign(strict) * PPQ_TOLERANCE) end local function noteEndsBeforePPQ(note, limit, strict) - return note.endPPQ < limit - bool2sign(strict) * PPQ_TOLERANCE + return note.endPPQ < (limit - bool2sign(strict) * PPQ_TOLERANCE) end local function noteEndsOnPPQ(note, limit) @@ -869,6 +890,13 @@ 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 @@ -986,7 +1014,7 @@ local function nextSnap(track, direction, reftime, options) local itemStartTime = reaper.GetMediaItemInfo_Value(mediaItem, "D_POSITION") local itemEndTime = itemStartTime + reaper.GetMediaItemInfo_Value(mediaItem, "D_LENGTH") - if (maxTime ~= nil) or (itemEndTime > maxTime) then + if itemEndTime > maxTime then maxTime = itemEndTime end @@ -1006,7 +1034,7 @@ local function nextSnap(track, direction, reftime, options) bestJumpTime = moveComparatorHelper(itemEndTime, cursorTime, bestJumpTime, direction, "TIME") end - if options.notes or options.itemGrid then + if options.noteStart or options.noteEnd or options.itemGrid then local takeCount = reaper.GetMediaItemNumTakes(mediaItem) local ti = 0 @@ -1019,15 +1047,20 @@ local function nextSnap(track, direction, reftime, options) local cursorPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, cursorTime) local bestJumpPPQ = nil - if options.notes then + 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) - bestJumpPPQ = moveComparatorHelper(n.startPPQ, cursorPPQ, bestJumpPPQ, direction, "PPQ") - bestJumpPPQ = moveComparatorHelper(n.endPPQ, cursorPPQ, bestJumpPPQ, direction, "PPQ") + 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 @@ -1080,7 +1113,8 @@ local function snapOptions() return { enabled = getSetting("Snap"), itemBounds = getSetting("SnapItemBounds"), - notes = getSetting("SnapNotes"), + noteStart = getSetting("SnapNotes"), + noteEnd = getSetting("SnapNotes"), itemGrid = getSetting("SnapItemGrid"), projectGrid = getSetting("SnapProjectGrid") } @@ -1090,6 +1124,10 @@ 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) @@ -1145,6 +1183,131 @@ local function AllowKeyEventNavigation() return getSetting("AllowKeyEventNavigation") end +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 _, notecnt, _, _ = midi_utils.MIDI_CountEvts(take) + local ni = 0 + + midi_utils.MIDI_InitializeTake(take) + + for _, v in pairs(notes_to_extend) do + notes_to_add[#notes_to_add+1] = v + end + + local tomod = {} + + while (ni < notecnt) do + local n = GetNote(take, ni, true) + + 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) + + midi_utils.MIDI_OpenWriteTransaction(take) + 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 + + midi_utils.MIDI_SetNote(take, n.index, nil, nil, nil, nil, nil, newpitch, newvel, nil) + if n.startPPQ > cursorPPQ then + cursorPPQ = n.startPPQ + end + end + midi_utils.MIDI_CommitWriteTransaction(take) + + cursorTime = reaper.MIDI_GetProjTimeFromPPQPos(take, cursorPPQ) + end + end + + reaper.MIDI_Sort(take) + reaper.UpdateItemInProject(mediaItem) + reaper.MarkTrackItemsDirty(track, mediaItem) + end + + 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) + + if getSetting("AutoScrollArrangeView") then + KeepEditCursorOnScreen() + end + end + + reaper.Undo_EndBlock(commitDescription(1, addcount, remcount, shcount, extcount, mvcount), -1) +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) @@ -1155,6 +1318,11 @@ local function commit(track, take, notes_to_add, notes_to_extend, triggered_by_k local navigateModeOn = (currentop.mode == "Navigate") local insertModeOn = (currentop.mode == "Insert") local replaceModeOn = (currentop.mode == "Replace") + local repitchModeOn = (currentop.mode == "Repitch") + + if repitchModeOn then + return repitch(track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + end if navigateModeOn then if (not triggered_by_key_event) or AllowKeyEventNavigation() then @@ -1444,6 +1612,11 @@ local function commitBack(track, take, notes_to_shorten, triggered_by_key_event) local navigateModeOn = (currentop.mode == "Navigate") local insertModeOn = (currentop.mode == "Insert") local replaceModeOn = (currentop.mode == "Replace") + local repitchModeOn = (currentop.mode == "Repitch") + + if repitchModeOn then + return repitchBack(track, take, notes_to_add, notes_to_extend, triggered_by_key_event) + end local fullEraseMode = false diff --git a/MIDI Editor/talagan_OneSmallStep/classes/lib/MIDIUtils.lua b/MIDI Editor/talagan_OneSmallStep/classes/lib/MIDIUtils.lua new file mode 100644 index 000000000..eff26d8e3 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/classes/lib/MIDIUtils.lua @@ -0,0 +1,1987 @@ +-- @noindex +-- @license MIT +-- @description MIDI Utils API +-- @version 0.1.13 +-- @author sockmonkey72 +-- @about +-- # MIDI Utils API +-- Drop-in replacement for REAPER's high-level MIDI API +-- @about2 +-- Third party library by sockmonkey72, used by One Small Step +-- Only this header was modified for reapack logic + +--[[ + + See the Readme.txt document in the MIDIUtils/ subdirectory for full documentation, + or view the latest version online: https://raw.githubusercontent.com/jeremybernstein/ReaScripts/main/MIDI/MIDIUtils/Readme.txt + +--]] + +local r = reaper +local MIDIUtils = {} + +MIDIUtils.ENFORCE_ARGS = true -- turn off for efficiency +MIDIUtils.CORRECT_OVERLAPS = false +MIDIUtils.CORRECT_OVERLAPS_FAVOR_SELECTION = false +MIDIUtils.ALLNOTESOFF_SNAPS_TO_ITEM_END = true + +local NOTE_TYPE = 0 +local NOTEOFF_TYPE = 1 +local CC_TYPE = 2 +local SYSEX_TYPE = 3 +local META_TYPE = 4 +local BEZIER_TYPE = 5 +local TAIL_TYPE = 6 +local OTHER_TYPE = 7 + +local MIDIEvents = {} +local bezTable = {} +local tailEvent + +local noteEvents = {} +local ccEvents = {} +local syxEvents = {} + +local enumNoteIdx = 0 +local enumCCIdx = 0 +local enumSyxIdx = 0 +local enumAllIdx = 0 +local enumAllLastCt = -1 + +local activeTake +local openTransaction +local configVarCache = {} + +local OnError = function (err) + r.ShowConsoleMsg(err .. '\n' .. debug.traceback() .. '\n') +end + +local function CheckDependencies(scriptName) + -- no dependencies at the moment + return true +end + +----------------------------------------------------------------------------- + +MIDIUtils.SetOnError = function(fn) + OnError = fn +end + +MIDIUtils.CheckDependencies = function(scriptName) + return select(2, xpcall(CheckDependencies, OnError, scriptName)) +end + +----------------------------------------------------------------------------- +-------------------------------- UTILITIES ---------------------------------- + +local function post(...) + local args = {...} + local str = '' + for i = 1, #args do + local v = args[i] + str = str .. (i ~= 1 and ', ' or '') .. (v ~= nil and tostring(v) or '') + end + str = str .. '\n' + r.ShowConsoleMsg(str) +end + +local function spairs(t, order) -- sorted iterator (https://stackoverflow.com/questions/15706270/sort-a-table-in-lua) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + +local function ReadREAPERConfigVar_Int(name) + if configVarCache[name] then return configVarCache[name] end + for line in io.lines(r.GetResourcePath()..'/reaper.ini') do + local match = string.match(line, name..'='..'(%d+)$') + if match then + match = math.floor(match) + configVarCache[name] = match + return match + end + end + return nil +end + +----------------------------------------------------------------------------- + +MIDIUtils.post = function(...) + return select(2, xpcall(post, OnError, ...)) +end + +MIDIUtils.p = MIDIUtils.post + +----------------------------------------------------------------------------- +------------------------------- ARG CHECKING -------------------------------- + +-- Print contents of `tbl`, with indentation. +-- `indent` sets the initial level of indentation. +local function tprint (tbl, indent) + if not indent then indent = 0 end + for k, v in pairs(tbl) do + local formatting = string.rep(" ", indent) .. k .. ": " + if type(v) == "table" then + post(formatting) + tprint(v, indent+1) + elseif type(v) == 'boolean' then + post(formatting .. tostring(v)) + else + post(formatting .. v) + end + end +end + +function IsValidNumber(val) + if type(val) == 'number' then + if val == math.huge + or val == -math.huge + or val ~= val + or not (val > -math.huge and val < math.huge) + then + return false + end + return true + end + return false +end + +function EnforceArgs(...) + if not MIDIUtils.ENFORCE_ARGS then return true end + local fnName = debug.getinfo(2).name + local args = table.pack(...) + for i = 1, args.n do + if args[i].val == nil and not args[i].optional then + error(fnName..': invalid or missing argument #'..i, 3) + return false + elseif type(args[i].val) ~= args[i].type and not args[i].optional then + error(fnName..': bad type for argument #'..i.. + ', expected \''..args[i].type..'\', got \''..type(args[i].val)..'\'', 3) + return false + elseif args[i].reapertype and not r.ValidatePtr(args[i].val, args[i].reapertype) then + error(fnName..': bad type for argument #'..i.. + ', expected \''..args[i].reapertype..'\'', 3) + return false + elseif args[i].type == 'number' and not ((args[i].optional and args[i].val == nil) or IsValidNumber(args[i].val)) then + error(fnName..': invalid number #'..i, 3) + return false + end + end + return true +end + +function MakeTypedArg(val, type, optional, reapertype) + if not MIDIUtils.ENFORCE_ARGS then return {} end + local typedArg = { + type = type, + val = val, + optional = optional + } + if reapertype then typedArg.reapertype = reapertype end + return typedArg +end + +local function TypeFromBytes(b1, b2, b3) + if b1 == 0xFF then return META_TYPE, 0xFF + elseif b1 == 0xF0 then return SYSEX_TYPE, 0xF0 + elseif (b1 >= 0x90 and b1 < 0xA0 and b3 ~= 0) then return NOTE_TYPE, 0x90 + elseif (b1 >= 0x80 and b1 < 0xA0) then return NOTEOFF_TYPE, 0x80 + elseif (b1 >= 0xA0 and b1 < 0xF0) then return CC_TYPE, b1 & 0xF0 + else return OTHER_TYPE, 0 + end +end + +local function FlagsFromSelMute(selected, muted) + return selected and muted and 3 or selected and 1 or muted and 2 or 0 +end + +----------------------------------------------------------------------------- +----------------------------------- OOP ------------------------------------- + +local DEBUG_CLASS = false -- enable to check whether we're using known object properties + +local function class(base, setup, init) -- http://lua-users.org/wiki/SimpleLuaClasses + local c = {} -- a new class instance + if not init and type(base) == 'function' then + init = base + base = nil + elseif type(base) == 'table' then + -- our new class is a shallow copy of the base class! + for i, v in pairs(base) do + c[i] = v + end + c._base = base + end + if DEBUG_CLASS then + c._names = {} + if setup then + for i, v in pairs(setup) do + c._names[i] = true + end + end + + c.__newindex = function(table, key, value) + local found = false + if table._names and table._names[key] then found = true + else + local m = getmetatable(table) + while (m) do + if m._names[key] then found = true break end + m = m._base + end + end + if not found then + error("unknown property: "..key, 3) + else rawset(table, key, value) + end + end + end + + -- the class will be the metatable for all its objects, + -- and they will look up their methods in it. + c.__index = c + + -- expose a constructor which can be called by () + local mt = {} + mt.__call = function(class_tbl, ...) + local obj = {} + setmetatable(obj, c) + if class_tbl.init then + class_tbl.init(obj,...) + else + -- make sure that any stuff from the base class is initialized! + if base and base.init then + base.init(obj, ...) + end + end + return obj + end + c.init = init + c.is_a = function(self, klass) + local m = getmetatable(self) + while m do + if m == klass then return true end + m = m._base + end + return false + end + setmetatable(c, mt) + return c +end + +----------------------------------------------------------------------------- +----------------------------------- EVENT ----------------------------------- + +local Event = class(nil, { ppqpos = 0, offset = 0, flags = 0, msg1 = 0, msg2 = 0, msg3 = 0, + chanmsg = 0, chan = 0, msg = '', MIDI = '', recalcMIDI = false, MIDIidx = 0, delete = false }) +function Event:init(ppqpos, offset, flags, msg, MIDI) + self.ppqpos = math.floor(ppqpos + 0.5) + self.offset = math.floor(offset + 0.5) + self.flags = flags + + self.msg1 = (msg and msg:byte(1)) or 0 + self.msg2 = (msg and msg:byte(2)) or 0 + self.msg3 = (msg and msg:byte(3)) or 0 + + _, self.chanmsg = TypeFromBytes(self.msg1, self.msg2, self.msg3) + self.chan = self:IsChannelEvt() and self.msg1 & 0x0F or 0 + + self.msg = self:PurifyMsg(msg) + if self:IsChannelEvt() then msg = self.msg end + + self.MIDI = MIDI + if not self.MIDI and msg and self.offset and self.flags then + self.MIDI = string.pack('i4Bs4', self.offset, self.flags, msg) + end + if not self.MIDI then self.recalcMIDI = true end +end + +function Event:PurifyMsg(msg) + return msg +end + +function Event:IsChannelEvt() return false end +function Event:IsAllEvt() return false end +function Event:IsSelected() return self.flags & 1 ~= 0 end +function Event:IsMuted() return self.flags & 2 ~= 0 end + +function Event:SetMIDIString(msg) + self.msg = self:PurifyMsg(msg) + return self:GetMIDIString() +end + +function Event:GetMIDIString() + return self.msg +end + +function Event:type() return OTHER_TYPE end + +local TailEvent = class(Event) +function TailEvent:init(ppqpos, offset, flags, msg, MIDI) + Event.init(self, ppqpos, offset, flags, msg, MIDI) +end +function TailEvent:type() return TAIL_TYPE end + +local UnknownEvent = class(Event) +function UnknownEvent:init(ppqpos, offset, flags, msg, MIDI) + Event.init(self, ppqpos, offset, flags, msg, MIDI) + self.chanmsg = 0 + self.chan = 0 + self.msg2 = 0 + self.msg3 = 0 +end + +----------------------------------------------------------------------------- +------------------------------- CHANNEL EVENT ------------------------------- + +local ChannelEvent = class(Event) +function ChannelEvent:init(ppqpos, offset, flags, msg, MIDI) + Event.init(self, ppqpos, offset, flags, msg, MIDI) +end +-- function ChannelEvent:init(ppqpos, offset, flags, msg, MIDI) +-- Event.init(self, ppqpos, offset, flags, msg, MIDI) +-- end +function ChannelEvent:IsChannelEvt() return true end +function ChannelEvent:IsAllEvt() return true end + +----------------------------------------------------------------------------- +-------------------------------- NOTEON EVENT ------------------------------- + +local NoteOnEvent = class(ChannelEvent, { endppqpos = 0, noteOffIdx = 0, idx = 0 }) +function NoteOnEvent:init(ppqpos, offset, flags, msg, MIDI, count) + ChannelEvent.init(self, ppqpos, offset, flags, msg, MIDI) + self.endppqpos = -1 + self.noteOffIdx = -1 + if count == nil or count then + self.idx = #noteEvents + table.insert(noteEvents, self) + end +end + +function NoteOnEvent:type() return NOTE_TYPE end + +----------------------------------------------------------------------------- +-------------------------------- NOTEOFF EVENT ------------------------------ + +local NoteOffEvent = class(ChannelEvent, { noteOnIdx = 0 }) +function NoteOffEvent:init(ppqpos, offset, flags, msg, MIDI) + ChannelEvent.init(self, ppqpos, offset, flags, msg, MIDI) + self.noteOnIdx = -1 +end + +function NoteOffEvent:type() return NOTEOFF_TYPE end + +----------------------------------------------------------------------------- +----------------------------------- CC EVENT -------------------------------- + +local CCEvent = class(ChannelEvent, { hasBezier = false, idx = 0 }) +function CCEvent:init(ppqpos, offset, flags, msg, MIDI, count) + ChannelEvent.init(self, ppqpos, offset, flags, msg, MIDI) + self.hasBezier = false + if count == nil or count then + self.idx = #ccEvents + table.insert(ccEvents, self) + end +end + +function CCEvent:GetMIDIString() + if self.msg:len() == 2 then + return self.msg..string.char(0) + end + return self.msg +end + +function CCEvent:PurifyMsg(msg) + local msglen = msg:len() + if (self.chanmsg == 0xC0 or self.chanmsg == 0xD0) and msglen > 2 then + self.msg3 = 0 + msg = msg:sub(1, 2) -- truncate 3rd byte + elseif (self.chanmsg ~= 0xC0 and self.chanmsg ~= 0xD0) and msglen < 3 then + for i = msglen, 2 do + msg = msg..string.char(0) -- if it's a 3-byte message with a 2-byte payload, just stick a 0 on the end + end + end + return msg +end + +function CCEvent:type() return CC_TYPE end + +----------------------------------------------------------------------------- +------------------------------- ALLEVT EVENT ------------------------------- + +local AllEvtEvent = class(Event) +function AllEvtEvent:init(ppqpos, offset, flags, msg, MIDI, count) + Event.init(self, ppqpos, offset, flags, msg, MIDI) +end +function AllEvtEvent:IsAllEvt() return true end + +----------------------------------------------------------------------------- +------------------------------- TEXTSYX EVENT ------------------------------- + +local TextSysexEvent = class(AllEvtEvent, { idx = 0 }) +function TextSysexEvent:init(ppqpos, offset, flags, msg, MIDI, count) + AllEvtEvent.init(self, ppqpos, offset, flags, msg, MIDI) + if count == nil or count then + self.idx = #syxEvents + table.insert(syxEvents, self) + end +end + +----------------------------------------------------------------------------- +--------------------------------- SYSEX EVENT ------------------------------- + +local SysexEvent = class(TextSysexEvent) +function SysexEvent:init(ppqpos, offset, flags, msg, MIDI, count) + TextSysexEvent.init(self, ppqpos, offset, flags, msg, MIDI, count) + self.msg2 = 0 + self.msg3 = 0 +end + +function SysexEvent:GetMIDIString() + return self.msg == '' and '' or string.char(0xF0)..self.msg..string.char(0xF7) +end + +function SysexEvent:PurifyMsg(msg) + if msg:byte(1) == 0xF0 then msg = string.sub(msg, 2) end + if msg:byte(msg:len()) == 0xF7 then msg = string.sub(msg, 1, -2) end + return msg +end + +function SysexEvent:type() return SYSEX_TYPE end + +----------------------------------------------------------------------------- +--------------------------------- META EVENT -------------------------------- + +local MetaEvent = class(TextSysexEvent) +function MetaEvent:init(ppqpos, offset, flags, msg, MIDI, count) + TextSysexEvent.init(self, ppqpos, offset, flags, msg, MIDI, count) + self.msg3 = 0 +end + +function MetaEvent:GetMIDIString() + return self.msg == '' and '' or string.char(0xFF)..string.char(self.msg2)..self.msg +end + +function MetaEvent:PurifyMsg(msg) + if msg:byte(1) == 0xFF then msg = string.sub(msg, 3) end -- just going to assume that this message conforms w b2 == type + return msg +end + +function MetaEvent:type() return META_TYPE end + +----------------------------------------------------------------------------- +-------------------------------- BEZIER EVENT ------------------------------- + +local BezierEvent = class(Event, { ccIdx = 0 }) +function BezierEvent:init(ppqpos, offset, flags, msg, MIDI) + Event.init(self, ppqpos, offset, flags, msg, MIDI) + self.ccIdx = #ccEvents - 1 -- previous event, ignore if -1 +end + +function BezierEvent:type() return BEZIER_TYPE end + +----------------------------------------------------------------------------- +-------------------------------- EVENT FACTORY ------------------------------ + +local function MakeEvent(ppqpos, offset, flags, msg, MIDI, count) + if msg then + local b1 = msg:byte(1) + local b2 = msg:byte(2) + local b3 = msg:byte(3) + local type = TypeFromBytes(b1, b2, b3) + if type == NOTE_TYPE then + return NoteOnEvent(ppqpos, offset, flags, msg, MIDI, count) + elseif type == NOTEOFF_TYPE then + return NoteOffEvent(ppqpos, offset, flags, msg, MIDI) + elseif type == CC_TYPE then + return CCEvent(ppqpos, offset, flags, msg, MIDI, count) + elseif type == SYSEX_TYPE then + return SysexEvent(ppqpos, offset, flags, msg, MIDI, count) + elseif type == META_TYPE then + if b2 == 15 and string.sub(msg, 3, 7) == 'CCBZ ' then + return BezierEvent(ppqpos, offset, flags, msg, MIDI) + else + return MetaEvent(ppqpos, offset, flags, msg, MIDI, count) + end + else + return UnknownEvent(ppqpos, offset, flags, msg, MIDI) + end + end +end + +----------------------------------------------------------------------------- +----------------------------------- PARSE ----------------------------------- + +local function Reset() + MIDIEvents = {} + bezTable = {} + enumNoteIdx = 0 + enumCCIdx = 0 + enumSyxIdx = 0 + enumAllIdx = 0 + enumAllLastCt = -1 + tailEvent = nil + activeTake = nil + openTransaction = nil + + noteEvents = {} + ccEvents = {} + syxEvents = {} +end + +local function InsertMIDIEvent(event) + if event:is_a(BezierEvent) then -- special case for BezierEvents + bezTable[event.ccIdx + 1] = event + ccEvents[event.ccIdx + 1].hasBezier = true + return nil, #MIDIEvents + else + table.insert(MIDIEvents, event) + event.MIDIidx = #MIDIEvents + return event, #MIDIEvents + end +end + +local function ReplaceMIDIEvent(event, newEvent) + newEvent.idx = event.idx + newEvent.MIDIidx = event.MIDIidx + MIDIEvents[event.MIDIidx] = newEvent + if newEvent.idx then + if newEvent:is_a(NoteOnEvent) then noteEvents[newEvent.idx + 1] = newEvent + elseif newEvent:is_a(CCEvent) then ccEvents[newEvent.idx + 1] = newEvent + elseif newEvent:is_a(TextSysexEvent) then syxEvents[newEvent.idx + 1] = newEvent + end + end + return newEvent +end + +local function GetItemEndPPQPos(take) + local item = r.GetMediaItemTake_Item(take) + local itempos = r.GetMediaItemInfo_Value(item, 'D_POSITION') + local itemlen = r.GetMediaItemInfo_Value(item, 'D_LENGTH') + return r.MIDI_GetPPQPosFromProjTime(take, itempos + itemlen) +end + +local function GetEvents(take) + local ppqTime = 0 + local stringPos = 1 + local noteOns = {} + + local rv, MIDIString = r.MIDI_GetAllEvts(take) + if rv and MIDIString then + Reset() + activeTake = take + end + + while stringPos < MIDIString:len() - 12 do -- -12 to exclude final All-Notes-Off message + local offset, flags, msg, newStringPos = string.unpack('i4Bs4', MIDIString, stringPos) + if not (msg and newStringPos) then return false end + + ppqTime = ppqTime + offset -- current PPQ time for this event + + -- TODO: don't add bezier events to the main table, put all in the aux table for simplicity + local event = InsertMIDIEvent(MakeEvent(ppqTime, offset, flags, msg, MIDIString:sub(stringPos, newStringPos - 1))) + if event then + if event:is_a(NoteOnEvent) then + table.insert(noteOns, { chan = event.chan, pitch = event.msg2, flags = event.flags, ppqpos = event.ppqpos, index = #MIDIEvents }) + elseif event:is_a(NoteOffEvent) then + for k, v in spairs(noteOns, function(t, a, b) return t[a].ppqpos < t[b].ppqpos end) do + if v.chan == event.chan and v.pitch == event.msg2 and v.flags == event.flags then + local noteon = MIDIEvents[v.index] + event.noteOnIdx = k + noteon.noteOffIdx = #MIDIEvents + noteon.endppqpos = event.ppqpos + noteOns[k] = nil -- remove it + break + end + end + end + end + stringPos = newStringPos + end + local TailMsg = MIDIString:sub(-12) + local offset, flags, msg = string.unpack('i4Bs4', TailMsg) + tailEvent = TailEvent(ppqTime + offset, offset, flags, msg, TailMsg) + -- this is a battle for another day + -- local itemEndPPQPos = GetItemEndPPQPos(take) + -- tailEvent.allnotesoff_delta = itemEndPPQPos - tailEvent.ppqpos -- distance from clip end + return true +end + +----------------------------------------------------------------------------- +---------------------------------- BASICS ----------------------------------- + +function EnsureTake(take) + if take ~= activeTake then + GetEvents(take) + activeTake = take + end +end + +function EnsureTransaction(take) + if openTransaction ~= take then + post('MIDIUtils: cannot modify MIDI stream without an open WRITE transaction for this take') + return false + end + return true +end + +----------------------------------------------------------------------------- +------------------------------------ API ------------------------------------ + +local function MIDI_InitializeTake(take) + GetEvents(take) +end + +local function MIDI_CountEvts(take) + EnsureTake(take) + return true, #noteEvents, #ccEvents, #syxEvents +end + +-- cache this, or store it in the event for faster lookup? +local function MIDI_CountAllEvts(take) + EnsureTake(take) + local allcnt = 0 + for _, event in ipairs(MIDIEvents) do + if event:IsAllEvt() then allcnt = allcnt + 1 end + end + return allcnt +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_InitializeTake = function(take, enforceargs) + if enforceargs ~= nil then MIDIUtils.ENFORCE_ARGS = enforceargs end + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(enforceargs, 'boolean', true) + ) + return select(2, xpcall(MIDI_InitializeTake, OnError, take)) +end + +MIDIUtils.MIDI_CountEvts = function(take) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*') + ) + return select(2, xpcall(MIDI_CountEvts, OnError, take)) +end + +MIDIUtils.MIDI_CountAllEvts = function(take) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*') + ) + return select(2, xpcall(MIDI_CountAllEvts, OnError, take)) +end + +----------------------------------------------------------------------------- +------------------------------ OVERLAP CORRECT ------------------------------ + +local function CorrectOverlapForEvent(take, testEvent, selectedEvent, favorSelection) + local modified = false + if testEvent.chan == selectedEvent.chan + and testEvent.msg2 == selectedEvent.msg2 + then + -- quick test for equality, in which case we should prioritize a selected event over an unselected one + -- regardless of the overlap selection setting + if testEvent.ppqpos == selectedEvent.ppqpos and testEvent.endppqpos == selectedEvent.endppqpos then + local testSel = testEvent:IsSelected() + local selSel = selectedEvent:IsSelected() + if testSel ~= selSel and testSel then selectedEvent.delete = true + else testEvent.delete = true + end + return true + elseif testEvent.endppqpos >= selectedEvent.ppqpos and testEvent.endppqpos <= selectedEvent.endppqpos then + MIDIUtils.MIDI_SetNote(take, testEvent.idx, nil, nil, nil, selectedEvent.ppqpos, nil, nil, nil) + modified = true + elseif testEvent.ppqpos >= selectedEvent.ppqpos and testEvent.ppqpos <= selectedEvent.endppqpos then + if favorSelection then + MIDIUtils.MIDI_SetNote(take, testEvent.idx, nil, nil, selectedEvent.endppqpos, nil, nil, nil, nil) + else + MIDIUtils.MIDI_SetNote(take, selectedEvent.idx, nil, nil, nil, testEvent.ppqpos, nil, nil, nil) + end + modified = true + end + + if testEvent.endppqpos - testEvent.ppqpos < 1 then + testEvent.delete = true + end + end + return modified +end + +local function DoCorrectOverlaps(take, event, favorSelection) +-- look backward + local idx = event.idx + 1 + for i = idx - 1, 1, -1 do + if CorrectOverlapForEvent(take, noteEvents[i], event, favorSelection) then break end + end + -- look forward + for i = idx + 1, #noteEvents do + if CorrectOverlapForEvent(take, noteEvents[i], event, favorSelection) then break end + end +end + +local function CorrectOverlaps(take, favorSelection) + if not EnsureTransaction(take) then return false end + for _, event in ipairs(noteEvents) do + DoCorrectOverlaps(take, event, event:IsSelected() and favorSelection or false) + end + return true +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_CorrectOverlaps = function (take, favorSelection) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(favorSelection, 'boolean', true) + ) + return select(2, xpcall(CorrectOverlaps, OnError, take, favorSelection or false)) +end + +----------------------------------------------------------------------------- +------------------------------- TRANSACTIONS -------------------------------- + +local function MIDI_OpenWriteTransaction(take) + EnsureTake(take) + openTransaction = take +end + +local function MIDI_CommitWriteTransaction(take, refresh, dirty) + if not EnsureTransaction(take) then return false end + + if MIDIUtils.CORRECT_OVERLAPS then + CorrectOverlaps(take, MIDIUtils.CORRECT_OVERLAPS_FAVOR_SELECTION) + end + local newMIDIString = '' + local lastPPQPos = 0 + -- iterate sorted to avoid (REAPER Inline MIDI Editor) problems with offset calculation + for _, event in spairs(MIDIEvents, function(t, a, b) return t[a].ppqpos < t[b].ppqpos end) do + event.offset = math.floor(event.ppqpos - lastPPQPos) + lastPPQPos = event.ppqpos + local MIDIStr = event:GetMIDIString() + if event.delete then + event.flags = 0 + MIDIStr = event:SetMIDIString('') + elseif event.recalcMIDI then + if event:IsChannelEvt() then + local b1 = string.char(event.chanmsg | event.chan) + local b2 = string.char(event.msg2) + local b3 = string.char(event.msg3) + MIDIStr = event:SetMIDIString(table.concat({ b1, b2, b3 })) + end + end + event.MIDI = string.pack('i4Bs4', event.offset, event.flags, MIDIStr) + newMIDIString = newMIDIString .. event.MIDI + + -- handle any BezierEvents + if event:is_a(CCEvent) and event.hasBezier then + local bezEvent = bezTable[event.idx + 1] + if bezEvent and bezEvent.ccIdx == event.idx then + local bezString = bezEvent.MIDI --string.pack('i4Bs4', bezEvent.offset, bezEvent.flags, bezEvent.msg) + newMIDIString = newMIDIString .. bezString + end + end + end + + r.MIDI_DisableSort(take) + if MIDIUtils.ALLNOTESOFF_SNAPS_TO_ITEM_END then + local itemEndPPQPos = GetItemEndPPQPos(take) -- in case it changed + -- local ASOPPQPos = itemEndPPQPos - tailEvent.allnotesoff_delta + tailEvent.offset = math.floor(itemEndPPQPos - lastPPQPos) + else + tailEvent.offset = math.floor(tailEvent.ppqpos - lastPPQPos) + end + local TailMsg = string.pack('i4Bs4', tailEvent.offset, tailEvent.flags, tailEvent.msg) + r.MIDI_SetAllEvts(take, newMIDIString .. TailMsg) + r.MIDI_Sort(take) + openTransaction = nil + + if refresh then MIDIUtils.MIDI_InitializeTake(take) end -- update the tables based on the new data + if dirty then r.MarkTrackItemsDirty(r.GetMediaItemTake_Track(take), r.GetMediaItemTake_Item(take)) end + + return true +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_OpenWriteTransaction = function(take) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*') + ) + return select(2, xpcall(MIDI_OpenWriteTransaction, OnError, take)) +end + +MIDIUtils.MIDI_CommitWriteTransaction = function(take, refresh, dirty) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(refresh, 'boolean', true), + MakeTypedArg(dirty, 'boolean', true) + ) + return select(2, xpcall(MIDI_CommitWriteTransaction, OnError, take, refresh, dirty)) +end + +----------------------------------------------------------------------------- +----------------------------------- NOTES ----------------------------------- + +local function MIDI_GetNote(take, idx) + EnsureTake(take) + local event = noteEvents[idx + 1] + if event and event:is_a(NoteOnEvent) and event.idx == idx and not event.delete then + local noteoff = MIDIEvents[event.noteOffIdx] + return true, event.flags & 1 ~= 0 and true or false, event.flags & 2 ~= 0 and true or false, + event.ppqpos, event.endppqpos, event.chan, event.msg2, event.msg3, noteoff and noteoff.msg3 or 0 + end + return false, false, false, 0, 0, 0, 0, 0 +end + +local function AdjustNoteOff(noteoff, param, val) + noteoff[param] = val + noteoff.recalcMIDI = true +end + +local function MIDI_SetNote(take, idx, selected, muted, ppqpos, endppqpos, chan, pitch, vel, relvel) + if not EnsureTransaction(take) then return false end + local rv = false + local event = noteEvents[idx + 1] + if event and event:is_a(NoteOnEvent) and event.idx == idx and not event.delete then + local noteoff = MIDIEvents[event.noteOffIdx] + -- if not noteoff then post('missing noteoff in setnote') end + + if selected ~= nil then + if selected then event.flags = event.flags | 1 + else event.flags = event.flags & ~1 end + AdjustNoteOff(noteoff, 'flags', event.flags) + end + if muted ~= nil then + if muted then event.flags = event.flags | 2 + else event.flags = event.flags & ~2 end + AdjustNoteOff(noteoff, 'flags', event.flags) + end + if ppqpos then + ppqpos = math.floor(ppqpos + 0.5) + local diff = ppqpos - event.ppqpos + event.ppqpos = ppqpos + end + if endppqpos then + endppqpos = math.floor(endppqpos + 0.5) + AdjustNoteOff(noteoff, 'ppqpos', endppqpos) + event.endppqpos = noteoff.ppqpos + end + if chan then + event.chan = chan & 0x0F + AdjustNoteOff(noteoff, 'chan', event.chan) + end + if pitch then + event.msg2 = pitch & 0x7F + AdjustNoteOff(noteoff, 'msg2', event.msg2) + end + if vel then + event.msg3 = vel & 0x7F + if event.msg3 < 1 then event.msg3 = 1 end + end + if relvel then + AdjustNoteOff(noteoff, 'msg3', relvel & 0x7F) + end + event.recalcMIDI = true + rv = true + end + return rv +end + +local function MIDI_InsertNote(take, selected, muted, ppqpos, endppqpos, chan, pitch, vel, relvel) + if not EnsureTransaction(take) then return false end + local lastEventPPQ = #MIDIEvents ~= 0 and MIDIEvents[#MIDIEvents].ppqpos or 0 + local newNoteOn = NoteOnEvent(ppqpos, + ppqpos - lastEventPPQ, + FlagsFromSelMute(selected, muted), + table.concat({ + string.char(0x90 | (chan & 0xF)), + string.char(pitch & 0x7F), + string.char(vel & 0x7F) + })) + newNoteOn.noteOffIdx = -1 + InsertMIDIEvent(newNoteOn) + + local newNoteOff = NoteOffEvent(endppqpos, + endppqpos - ppqpos, + newNoteOn.flags, + table.concat({ + string.char(0x80 | newNoteOn.chan), + string.char(newNoteOn.msg2), + string.char(relvel and (relvel & 0x7F) or 0) + })) + newNoteOn.endppqpos = newNoteOff.ppqpos + newNoteOff.noteOnIdx = #MIDIEvents + _, newNoteOn.noteOffIdx = InsertMIDIEvent(newNoteOff) + return true, newNoteOn.idx +end + +local function MIDI_DeleteNote(take, idx) + if not EnsureTransaction(take) then return false end + local event = noteEvents[idx + 1] + if event and event:is_a(NoteOnEvent) and event.idx == idx then + event.delete = true + MIDIEvents[event.noteOffIdx].delete = true + return true + end + return false +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_GetNote = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_GetNote, OnError, take, idx)) +end + +MIDIUtils.MIDI_SetNote = function(take, idx, selected, muted, ppqpos, endppqpos, chan, pitch, vel, relvel) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number'), + MakeTypedArg(selected, 'boolean', true), + MakeTypedArg(muted, 'boolean', true), + MakeTypedArg(ppqpos, 'number', true), + MakeTypedArg(endppqpos, 'number', true), + MakeTypedArg(chan, 'number', true), + MakeTypedArg(pitch, 'number', true), + MakeTypedArg(vel, 'number', true), + MakeTypedArg(relvel, 'number', true) + ) + return select(2, xpcall(MIDI_SetNote, OnError, take, idx, selected, muted, ppqpos, endppqpos, chan, pitch, vel, relvel)) +end + +MIDIUtils.MIDI_InsertNote = function(take, selected, muted, ppqpos, endppqpos, chan, pitch, vel, relvel) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(selected, 'boolean'), + MakeTypedArg(muted, 'boolean'), + MakeTypedArg(ppqpos, 'number'), + MakeTypedArg(endppqpos, 'number'), + MakeTypedArg(chan, 'number'), + MakeTypedArg(pitch, 'number'), + MakeTypedArg(vel, 'number'), + MakeTypedArg(relvel, 'number', true) + ) + return select(2, xpcall(MIDI_InsertNote, OnError, take, selected, muted, ppqpos, endppqpos, chan, pitch, vel, relvel)) +end + +MIDIUtils.MIDI_DeleteNote = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_DeleteNote, OnError, take, idx)) +end + +----------------------------------------------------------------------------- +---------------------------------- BEZIER ----------------------------------- + +local function FindBezierData(idx, event) + local bezEvent + if event.hasBezier then + bezEvent = bezTable[event.idx + 1] + -- if not bezEvent then + -- -- this would be catastrophic, but just for debugging + -- for k, v in pairs(bezTable) do -- use pairs, indices may be non-contiguous + -- if v.ccIdx == idx then + -- bezEvent = v + -- event.hasBezier = true + -- break + -- end + -- end + end + if bezEvent then return true, bezEvent end + return false +end + +local function GetBezierData(idx, event) + local rv, bezEvent = FindBezierData(idx, event) + if rv and bezEvent then + local metadata = string.sub(bezEvent.msg, 3) + if string.sub(metadata, 1, 5) == 'CCBZ ' then + local beztype = metadata:byte(6) + local beztension = string.unpack('f', string.sub(metadata, 7)) + return true, beztype, beztension + end + end + return false +end + +local function SetBezierData(idx, event, beztype, beztension) + local bezMsg = table.concat({ + string.char(0xFF), + string.char(0xF), + 'CCBZ ', + string.char(beztype), -- should be 0 + string.pack('f', beztension) + }) + local rv, bezEvent = FindBezierData(idx, event) + if rv and bezEvent then + bezEvent.msg = bezMsg -- update in place + bezEvent.MIDI = string.pack('i4Bs4', bezEvent.offset, bezEvent.flags, bezEvent.msg) + bezEvent.ccIdx = idx + event.hasBezier = true + return true + else + bezEvent = BezierEvent(event.ppqpos, 0, 0, bezMsg) + bezEvent.ccIdx = idx + bezTable[idx + 1] = bezEvent + event.hasBezier = true + return true + end + return false +end + +local function DeleteBezierData(idx, event) + local rv, bezEvent = FindBezierData(idx, event) + if rv and bezEvent then + rv = false + local ev = ccEvents[bezEvent.ccIdx + 1] + if ev:is_a(CCEvent) and ev.idx == bezEvent.ccIdx and ev.hasBezier then + bezTable[ev.idx + 1] = nil + ev.hasBezier = false + rv = true + end + end + return rv +end + +----------------------------------------------------------------------------- +------------------------------------ CCS ------------------------------------ + +local function MIDI_GetCC(take, idx) + EnsureTake(take) + local event = ccEvents[idx + 1] + if event and event:is_a(CCEvent) and event.idx == idx and not event.delete then + return true, event.flags & 1 ~= 0 and true or false, event.flags & 2 ~= 0 and true or false, + event.ppqpos, event.chanmsg, event.chan, event.msg2, event.msg3 + end + return false, false, false, 0, 0, 0, 0, 0 +end + +local function MIDI_SetCC(take, idx, selected, muted, ppqpos, chanmsg, chan, msg2, msg3) + if not EnsureTransaction(take) then return false end + local rv = false + local event = ccEvents[idx + 1] + if event and event:is_a(CCEvent) and event.idx == idx and not event.delete then + if selected ~= nil then + if selected then event.flags = event.flags | 1 + else event.flags = event.flags & ~1 end + end + if muted ~= nil then + if muted then event.flags = event.flags | 2 + else event.flags = event.flags & ~2 end + end + if ppqpos then + ppqpos = math.floor(ppqpos + 0.5) + event.ppqpos = ppqpos -- bounds checking? + end + if chanmsg then + event.chanmsg = chanmsg < 0xA0 or chanmsg >= 0xF0 and 0xB0 or chanmsg & 0xF0 + end + if chan then + event.chan = chan & 0x0F + end + if msg2 then + event.msg2 = msg2 & 0x7F + end + if msg3 then + event.msg3 = msg3 & 0x7F + if chanmsg == 0xC0 or chanmsg == 0xD0 then event.msg3 = 0 end + end + event.recalcMIDI = true + rv = true + end + return rv +end + +local function MIDI_GetCCShape(take, idx) + EnsureTake(take) + local event = ccEvents[idx + 1] + if event and event:is_a(CCEvent) and event.idx == idx and not event.delete then + local rv, _, bztension = GetBezierData(idx, event) + return true, ((event.flags & 0xF0) >> 4) & 7, rv and bztension or 0. + end + return false, 0, 0. +end + +local function MIDI_SetCCShape(take, idx, shape, beztension) + EnsureTransaction(take) + local event = ccEvents[idx + 1] + if event and event:is_a(CCEvent) and event.idx == idx and not event.delete then + event.flags = event.flags & ~0xF0 + -- flag high 4 bits for CC shape: &16=linear, &32=slow start/end, &16|32=fast start, &64=fast end, &64|16=bezier + event.flags = event.flags | ((shape & 0x7) << 4) + event.recalcMIDI = true + if shape == 5 and beztension then + return SetBezierData(idx, event, 0, beztension) + else + DeleteBezierData(idx, event) + end + return true + end + return false +end + +local function MIDI_InsertCC(take, selected, muted, ppqpos, chanmsg, chan, msg2, msg3) + if not EnsureTransaction(take) then return false end + local lastEventPPQ = #MIDIEvents ~= 0 and MIDIEvents[#MIDIEvents].ppqpos or 0 + chanmsg = chanmsg < 0xA0 or chanmsg >= 0xF0 and 0xB0 or chanmsg + local newFlags = FlagsFromSelMute(selected, muted) + local defaultCCShape = ReadREAPERConfigVar_Int('midiccenv') or 0 + if defaultCCShape ~= -1 then + defaultCCShape = defaultCCShape & 7 + if defaultCCShape >= 0 and defaultCCShape <= 5 then + newFlags = newFlags | (defaultCCShape << 4) + end + end + + local newCC = CCEvent(ppqpos, + ppqpos - lastEventPPQ, + newFlags, + table.concat({ + string.char((chanmsg & 0xF0) | (chan & 0xF)), + string.char(msg2 & 0x7F), + string.char(msg3 & 0x7F) + })) + InsertMIDIEvent(newCC) + return true, newCC.idx +end + +local function MIDI_DeleteCC(take, idx) + if not EnsureTransaction(take) then return false end + local event = ccEvents[idx + 1] + if event and event:is_a(CCEvent) and event.idx == idx then + event.delete = true + DeleteBezierData(idx, event) + return true + end + return false +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_GetCC = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_GetCC, OnError, take, idx)) +end + +MIDIUtils.MIDI_SetCC = function(take, idx, selected, muted, ppqpos, chanmsg, chan, msg2, msg3) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number'), + MakeTypedArg(selected, 'boolean', true), + MakeTypedArg(muted, 'boolean', true), + MakeTypedArg(ppqpos, 'number', true), + MakeTypedArg(chanmsg, 'number', true), + MakeTypedArg(chan, 'number', true), + MakeTypedArg(msg2, 'number', true), + MakeTypedArg(msg3, 'number', true) + ) + return select(2, xpcall(MIDI_SetCC, OnError, take, idx, selected, muted, ppqpos, chanmsg, chan, msg2, msg3)) +end + +MIDIUtils.MIDI_GetCCShape = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_GetCCShape, OnError, take, idx)) +end + +MIDIUtils.MIDI_SetCCShape = function(take, idx, shape, beztension) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number'), + MakeTypedArg(shape, 'number'), + MakeTypedArg(beztension, 'number', true) + ) + return select(2, xpcall(MIDI_SetCCShape, OnError, take, idx, shape, beztension)) +end + +MIDIUtils.MIDI_InsertCC = function(take, selected, muted, ppqpos, chanmsg, chan, msg2, msg3) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(selected, 'boolean'), + MakeTypedArg(muted, 'boolean'), + MakeTypedArg(ppqpos, 'number'), + MakeTypedArg(chanmsg, 'number'), + MakeTypedArg(chan, 'number'), + MakeTypedArg(msg2, 'number'), + MakeTypedArg(msg3, 'number') + ) + return select(2, xpcall(MIDI_InsertCC, OnError, take, selected, muted, ppqpos, chanmsg, chan, msg2, msg3)) +end + +MIDIUtils.MIDI_DeleteCC = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_DeleteCC, OnError, take, idx)) +end + +----------------------------------------------------------------------------- +-------------------------------- TEXT / SYSEX ------------------------------- + +local function GetMIDIStringForTextSysex(type, msg) + local newMsg = msg + if type == -1 then + newMsg = string.char(0xF0)..msg..string.char(0xF7) + elseif type >= 1 and type <= 15 then + newMsg = string.char(0xFF)..string.char(type)..msg + end + return newMsg +end + +local function MIDI_GetTextSysexEvt(take, idx) + EnsureTake(take) + local event = syxEvents[idx + 1] + if event and (event:is_a(SysexEvent) or event:is_a(MetaEvent)) and event.idx == idx and not event.delete then + return true, event.flags & 1 ~= 0 and true or false, event.flags & 2 ~= 0 and true or false, event.ppqpos, + event.chanmsg == 0xF0 and -1 or event.chanmsg == 0xFF and event.msg2 or 0, event.msg + end + return false, false, false, 0, 0, '' +end + +local function MIDI_SetTextSysexEvt(take, idx, selected, muted, ppqpos, type, msg) + if not EnsureTransaction(take) then return false end + local rv = false + local event = syxEvents[idx + 1] + if event and (event:is_a(SysexEvent) or event:is_a(MetaEvent)) and event.idx == idx and not event.delete then + if selected ~= nil then + if selected then event.flags = event.flags | 1 + else event.flags = event.flags & ~1 end + end + if muted ~= nil then + if muted then event.flags = event.flags | 2 + else event.flags = event.flags & ~2 end + end + if ppqpos then + ppqpos = math.floor(ppqpos + 0.5) + event.ppqpos = ppqpos + end + if type and msg then + local newEvt = MakeEvent(event.ppqpos, event.offset, event.flags, GetMIDIStringForTextSysex(type, msg), nil, false) + event = ReplaceMIDIEvent(event, newEvt) + end + event.recalcMIDI = true + rv = true + end + return rv +end + +local function MIDI_InsertTextSysexEvt(take, selected, muted, ppqpos, type, bytestr) + if not EnsureTransaction(take) then return false end + local lastEventPPQ = #MIDIEvents ~= 0 and MIDIEvents[#MIDIEvents].ppqpos or 0 + local newTextSysex = MakeEvent(ppqpos, + ppqpos - lastEventPPQ, + FlagsFromSelMute(selected, muted), + GetMIDIStringForTextSysex(type, bytestr)) + InsertMIDIEvent(newTextSysex) + return true, newTextSysex.idx +end + +local function MIDI_DeleteTextSysexEvt(take, idx) + if not EnsureTransaction(take) then return false end + local event = syxEvents[idx + 1] + if event and (event:is_a(SysexEvent) or event:is_a(MetaEvent)) and event.idx == idx then + event.delete = true + return true + end + return false +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_GetTextSysexEvt = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_GetTextSysexEvt, OnError, take, idx)) +end + +MIDIUtils.MIDI_SetTextSysexEvt = function(take, idx, selected, muted, ppqpos, type, msg) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number'), + MakeTypedArg(selected, 'boolean', true), + MakeTypedArg(muted, 'boolean', true), + MakeTypedArg(ppqpos, 'number', true), + MakeTypedArg(type, 'number', true), + MakeTypedArg(msg, 'string', true) + ) + return select(2, xpcall(MIDI_SetTextSysexEvt, OnError, take, idx, selected, muted, ppqpos, type, msg)) +end + +MIDIUtils.MIDI_InsertTextSysexEvt = function(take, selected, muted, ppqpos, type, bytestr) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(selected, 'boolean'), + MakeTypedArg(muted, 'boolean'), + MakeTypedArg(ppqpos, 'number'), + MakeTypedArg(type, 'number'), + MakeTypedArg(bytestr, 'string') + ) + return select(2, xpcall(MIDI_InsertTextSysexEvt, OnError, take, selected, muted, ppqpos, type, bytestr)) +end + +MIDIUtils.MIDI_DeleteTextSysexEvt = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_DeleteTextSysexEvt, OnError, take, idx)) +end + +----------------------------------------------------------------------------- +------------------------------------ EVTS ----------------------------------- + +-- these operate just on the raw index into the array, not based on type +local function MIDI_GetEvt(take, idx) + EnsureTake(take) + local allcnt = 0 + for _, event in ipairs(MIDIEvents) do + if event:IsAllEvt() then + if idx == allcnt then + return true, event.flags & 1 ~= 0 and true or false, event.flags & 2 ~= 0 and true or false, event.ppqpos, event:GetMIDIString() + end + allcnt = allcnt + 1 + end + end + return false, false, false, 0, '' +end + +local function MIDI_SetEvt(take, idx, selected, muted, ppqpos, msg) + if not EnsureTransaction(take) then return false end + + local allcnt = 0 + for _, event in ipairs(MIDIEvents) do + if event:IsAllEvt() then + if idx == allcnt then + if selected ~= nil then + if selected then event.flags = event.flags | 1 + else event.flags = event.flags & ~1 end + end + if muted ~= nil then + if muted then event.flags = event.flags | 2 + else event.flags = event.flags & ~2 end + end + if ppqpos then + ppqpos = math.floor(ppqpos + 0.5) + event.ppqpos = ppqpos + end + if msg then -- the problem here is that we could mess up the numbering + local newEvt = MakeEvent(event.ppqpos, event.offset, event.flags, msg, nil, false) + event = ReplaceMIDIEvent(event, newEvt) + end + event.recalcMIDI = true + return true + end + allcnt = allcnt + 1 + end + end + return false +end + +-- TODO: this is not 100% complete, in that it doesn't hook stuff up (noteoffs for noteons, bezier curves for CC events) +-- OTOH, ... WTFC -- if you're using this function, you know what you're doing +local function MIDI_InsertEvt(take, selected, muted, ppqpos, bytestr) + if not EnsureTransaction(take) then return false end + local newFlags = FlagsFromSelMute(selected, muted) + local lastEventPPQ = #MIDIEvents ~= 0 and MIDIEvents[#MIDIEvents].ppqpos or 0 + local newOffset = ppqpos - lastEventPPQ + local newEvt = MakeEvent(ppqpos, newOffset, newFlags, bytestr) + InsertMIDIEvent(newEvt) + + local allcnt = 0 + for _, event in ipairs(MIDIEvents) do + if event:IsAllEvt() then + if event == newEvt then + return true, allcnt + end + allcnt = allcnt + 1 + end + end + return false +end + +local function MIDI_DeleteEvt(take, idx) + if not EnsureTransaction(take) then return false end + local allcnt = 0 + for _, event in ipairs(MIDIEvents) do + if event:IsAllEvt() then + if idx == allcnt then + event.delete = true + return true + end + allcnt = allcnt + 1 + end + end + return false +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_GetEvt = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_GetEvt, OnError, take, idx)) +end + +MIDIUtils.MIDI_SetEvt = function(take, idx, selected, muted, ppqpos, msg) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number'), + MakeTypedArg(selected, 'boolean', true), + MakeTypedArg(muted, 'boolean', true), + MakeTypedArg(ppqpos, 'number', true), + MakeTypedArg(msg, 'string', true) + ) + return select(2, xpcall(MIDI_SetEvt, OnError, take, idx, selected, muted, ppqpos, msg)) +end + +MIDIUtils.MIDI_InsertEvt = function(take, selected, muted, ppqpos, bytestr) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(selected, 'boolean'), + MakeTypedArg(muted, 'muted'), + MakeTypedArg(ppqpos, 'number'), + MakeTypedArg(bytestr, 'str') + ) + return select(2, xpcall(MIDI_InsertEvt, OnError, take, selected, muted, ppqpos, bytestr)) +end + +MIDIUtils.MIDI_DeleteEvt = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_DeleteEvt, OnError, take, idx)) +end + +----------------------------------------------------------------------------- +------------------------------------ ENUM ----------------------------------- + +local function EnumNotesImpl(take, idx, selectedOnly) + if idx < 0 then enumNoteIdx = 0 end + for i = enumNoteIdx > 0 and enumNoteIdx + 1 or 1, #MIDIEvents do + local event = MIDIEvents[i] + if event and event:is_a(NoteOnEvent) and (not selectedOnly or event.flags & 1 ~= 0) then + enumNoteIdx = i + return event.idx + end + end + enumNoteIdx = 0 + return -1 +end + +local function MIDI_EnumNotes(take, idx) + EnsureTake(take) + return EnumNotesImpl(take, idx, false) +end + +local function MIDI_EnumSelNotes(take, idx) + EnsureTake(take) + return EnumNotesImpl(take, idx, true) +end + +local function EnumCCImpl(take, idx, selectedOnly) + if idx == -1 then enumCCIdx = 0 end + for i = enumCCIdx > 0 and enumCCIdx + 1 or 1, #MIDIEvents do + local event = MIDIEvents[i] + if event:is_a(CCEvent) and (not selectedOnly or event.flags & 1 ~= 0) then + enumCCIdx = i + return event.idx + end + end + enumCCIdx = 0 + return -1 +end + +local function MIDI_EnumCC(take, idx) + EnsureTake(take) + return EnumCCImpl(take, idx, false) +end + +local function MIDI_EnumSelCC(take, idx) + EnsureTake(take) + return EnumCCImpl(take, idx, true) +end + +local function EnumTextSysexImpl(take, idx, selectedOnly) + if idx < 0 then enumSyxIdx = 0 end + for i = enumSyxIdx > 0 and enumSyxIdx + 1 or 1, #MIDIEvents do + local event = MIDIEvents[i] + if (event:is_a(SysexEvent) or event:is_a(MetaEvent)) and (not selectedOnly or event.flags & 1 ~= 0) then + enumSyxIdx = i + return event.idx + end + end + enumSyxIdx = 0 + return -1 +end + +local function MIDI_EnumTextSysexEvts(take, idx) + EnsureTake(take) + return EnumTextSysexImpl(take, idx, false) +end + +local function MIDI_EnumSelTextSysexEvts(take, idx) + EnsureTake(take) + return EnumTextSysexImpl(take, idx, true) +end + +local function EnumEvtsImpl(take, idx, selectedOnly) + if idx < 0 then + enumAllIdx = 0 + enumAllLastCt = -1 + end + enumAllIdx = enumAllIdx > 0 and enumAllIdx + 1 or 1 + + local allcnt = enumAllLastCt < 0 and 0 or enumAllLastCt + for k = enumAllIdx, #MIDIEvents do + local event = MIDIEvents[k] + if event and event:IsAllEvt() then + if not selectedOnly or event.flags & 1 ~= 0 then + enumAllIdx = k + enumAllLastCt = allcnt + 1 + return allcnt + end + allcnt = allcnt + 1 + end + end + enumAllIdx = 0 + enumAllLastCt = -1 + return -1 +end + +local function MIDI_EnumEvts(take, idx) + EnsureTake(take) + return EnumEvtsImpl(take, idx, false) +end + +local function MIDI_EnumSelEvts(take, idx) + EnsureTake(take) + return EnumEvtsImpl(take, idx, true) +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_EnumNotes = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_EnumNotes, OnError, take, idx)) +end + +MIDIUtils.MIDI_EnumSelNotes = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_EnumSelNotes, OnError, take, idx)) +end + +MIDIUtils.MIDI_EnumCC = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_EnumCC, OnError, take, idx)) +end + +MIDIUtils.MIDI_EnumSelCC = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_EnumSelCC, OnError, take, idx)) +end + +MIDIUtils.MIDI_EnumTextSysexEvts = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_EnumTextSysexEvts, OnError, take, idx)) +end + +MIDIUtils.MIDI_EnumSelTextSysexEvts = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_EnumSelTextSysexEvts, OnError, take, idx)) +end + +MIDIUtils.MIDI_EnumEvts = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_EnumEvts, OnError, take, idx)) +end + +MIDIUtils.MIDI_EnumSelEvts = function(take, idx) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(idx, 'number') + ) + return select(2, xpcall(MIDI_EnumSelEvts, OnError, take, idx)) +end + +----------------------------------------------------------------------------- +--------------------------------- CURVEINTERP ------------------------------- + +--[[ ported from lice_bezier.h ]] + +--[[ + -- https://forum.cockos.com/showpost.php?p=1690470&postcount=7 + Given end points at (0,1) (1,-1), tension maps to control points as: + + tension 0 => (0.25,0.5) (0.75,-0.5) + + This is somewhat arbitrary and everything else is based off it. + + tension -1 => (0,-1) (0,-1) + tension 1 => (1,1) (1,1) + + All other tension values just interpolate the control points between those 3 states. + + tension -0.5 => (0.125,-0.25) (0.375,-0.75) + tension 0.5 => (0.625,0.75) (0.875,0.25) + etc. +--]] + +--[[ + -- https://github.com/reaper-oss/sws/blob/98b91a9495bf90c1ae4d31a7bec483e69fbb897b/Breeder/BR_EnvelopeUtil.cpp#L754-L807 + int id0 = (m_sorted) ? (id-1) : (this->FindPrevious(t1, 0)); + int id3 = (m_sorted) ? (nextId+1) : (this->FindNext(t2, 0)); + double t0 = (!this->ValidateId(id0)) ? (t1) : (m_points[id0].position); + double v0 = (!this->ValidateId(id0)) ? (v1) : (m_points[id0].value); + double t3 = (!this->ValidateId(id3)) ? (t2) : (m_points[id3].position); + double v3 = (!this->ValidateId(id3)) ? (v2) : (m_points[id3].value); + + double x1, x2, y1, y2, empty; + LICE_Bezier_FindCardinalCtlPts(0.25, t0, t1, t2, v0, v1, v2, &empty, &x1, &empty, &y1); + LICE_Bezier_FindCardinalCtlPts(0.25, t1, t2, t3, v1, v2, v3, &x2, &empty, &y2, &empty); + + double tension = m_points[id].bezier; + x1 += tension * ((tension > 0) ? (t2-x1) : (x1-t1)); + x2 += tension * ((tension > 0) ? (t2-x2) : (x2-t1)); + y1 -= tension * ((tension > 0) ? (y1-v1) : (v2-y1)); + y2 -= tension * ((tension > 0) ? (y2-v1) : (v2-y2)); + + x1 = SetToBounds(x1, t1, t2); + x2 = SetToBounds(x2, t1, t2); + y1 = SetToBounds(y1, this->MinValue(), this->MaxValue()); + y2 = SetToBounds(y2, this->MinValue(), this->MaxValue()); + return LICE_CBezier_GetY(t1, x1, x2, t2, v1, y1, y2, v2, position); +--]] + +local CBEZ_ITERS = 8 + +local function EVAL_CBEZ(a,b,c,d,t) + local _t2=t*t + local tx=(a*t*_t2+b*_t2+c*t+d) + return tx +end + +local function LICE_CBezier_GetCoeffs(ctrl_x1, ctrl_x2, ctrl_x3, ctrl_x4, ctrl_y1, ctrl_y2, ctrl_y3, ctrl_y4) + local pAX, pBX, pCX + local pAY, pBY, pCY + + pCX = 3.0 * (ctrl_x2 - ctrl_x1) + local cx = pCX + pBX = 3.0 * (ctrl_x3 - ctrl_x2) - cx + local bx = pBX + pAX = (ctrl_x4 - ctrl_x1) - cx - bx + pCY = 3.0 * (ctrl_y2 - ctrl_y1) + local cy = pCY + pBY = 3.0 * (ctrl_y3 - ctrl_y2) - cy + local by = pBY + pAY = (ctrl_y4 - ctrl_y1) - cy - by + return pAX, pBX, pCX, pAY, pBY, pCY +end + +local function LICE_CBezier_GetY(ctrl_x1, ctrl_x2, ctrl_x3, ctrl_x4, ctrl_y1, ctrl_y2, ctrl_y3, ctrl_y4, x) + local pNextX = 0 + local pdYdX = 0 + local ptLo = 0 + local ptHi = 0 + + if x < ctrl_x1 then + pNextX = ctrl_x1 + pdYdX = 0 + return ctrl_y1, pNextX, pdYdX, ptLo, ptHi + end + + if x >= ctrl_x4 then + pNextX = ctrl_x4 + pdYdX = 0 + return ctrl_y4, pNextX, pdYdX, ptLo, ptHi + end + + local ax, bx, cx, ay, by, cy = + LICE_CBezier_GetCoeffs(ctrl_x1, ctrl_x2, ctrl_x3, ctrl_x4, ctrl_y1, ctrl_y2, ctrl_y3, ctrl_y4) + + local tx, t + local tLo = 0.0 + local tHi = 1.0 + local xLo=0.0 + local xHi=0.0 + local yLo, yHi + + for i = 1, CBEZ_ITERS do + t = 0.5 * (tLo + tHi) + tx = EVAL_CBEZ(ax, bx, cx, ctrl_x1, t) + if tx < x then + tLo = t + xLo = tx + elseif tx > x then + tHi = t + xHi = tx + else + tLo = t + xLo = tx + tHi = t + 1.0 / (2.0 ^ CBEZ_ITERS) + if tHi > 1.0 then tHi = 1.0 end -- floating point error + xHi = EVAL_CBEZ(ax, bx, cx, ctrl_x1, tHi) + break + end + end + + if tLo == 0. then xLo = EVAL_CBEZ(ax, bx, cx, ctrl_x1, 0.) end + if tHi == 1. then xHi = EVAL_CBEZ(ax, bx, cx, ctrl_x1, 1.) end + + yLo = EVAL_CBEZ(ay, by, cy, ctrl_y1, tLo) + yHi = EVAL_CBEZ(ay, by, cy, ctrl_y1, tHi) + + local dYdX = (xLo == xHi and 0.0 or (yHi - yLo) / (xHi - xLo)) + local y = yLo + (x - xLo) * dYdX + + pNextX = xHi + pdYdX = dYdX + + ptLo = tLo + ptHi = tHi + + return y, pNextX, pdYdX, ptLo, ptHi +end + +local function CalculateCCValueAtTime(val1, val2, pos, shape, beztension) + if shape == 0 then -- square + return val1 + elseif shape == 1 then -- linear + return val1 + ((val2 - val1) * pos) + elseif shape == 2 then -- slow start/end + return val1 + ((val2 - val1) * (pos ^ 2) * (3 - 2 * pos)) + elseif shape == 3 then -- fast start + return val1 + ((val2 - val1) * (1. - ((1. - pos) ^ 3))) + elseif shape == 4 then -- fast end + return val1 + ((val2 - val1) * (pos ^ 3)) + elseif shape == 5 then -- bezier TODO + local x0, x1, x2, x3, y0, y1, y2, y3 + x0, y0 = 0., val1 + x3, y3 = 1., val2 + x1, y1 = 0.25, val1 + ((val2 - val1) * 0.25) + x2, y2 = 0.75, val1 + ((val2 - val1) * 0.75) + + x1 = x1 + beztension * (beztension > 0 and 1 - x1 or x1 - 0) + y1 = y1 - beztension * (beztension > 0 and y1 - val1 or val2 - y1) + x2 = x2 + beztension * (beztension > 0 and 1 - x2 or x2 - 0) + y2 = y2 - beztension * (beztension > 0 and y2 - val1 or val2 - y2) + + local bezy = LICE_CBezier_GetY(x0, x1, x2, x3, y0, y1, y2, y3, pos) + return bezy + end + return val1 +end + +local function MIDI_GetCCValueAtTime(take, chanmsg, chan, msg2, time) + EnsureTake(take) + local rv = false + local val = 0 + local ppqpos = 0 + chanmsg = chanmsg & 0xF0 + chan = chan & 0xF + local b3 = chanmsg == 0xA0 or chanmsg == 0xB0 + local b2 = chanmsg == 0xC0 or chanmsg == 0xD0 + local pb = chanmsg == 0xE0 + local msg2out = 0 + local msg3out = 0 + + if chanmsg >= 0xA0 and chanmsg < 0xF0 then + ppqpos = r.MIDI_GetPPQPosFromProjTime(take, time) + local event_start + local event_end + if b3 and not msg2 then return false, 0 end + for _, event in ipairs(MIDIEvents) do + if event:is_a(CCEvent) + and event.chanmsg == chanmsg + and event.chan == chan + and not event:IsMuted() + then + if not b3 or event.msg2 == msg2 then + if event.ppqpos <= ppqpos and (not event_start or event.ppqpos > event_start.ppqpos) then + event_start = event + elseif event.ppqpos >= ppqpos and (not event_end or event.ppqpos < event_end.ppqpos) then + event_end = event + end + end + end + end + if event_start and event_end then + local ret, shape, beztension = MIDI_GetCCShape(take, event_start.idx) + local val_start, val_end + if b3 then + val_start = event_start.msg3 + val_end = event_end.msg3 + elseif chanmsg == 0xC0 or chanmsg == 0xD0 then + val_start = event_start.msg2 + val_end = event_end.msg2 + else + val_start = (event_start.msg3 << 7 | event_start.msg2) - (1 << 13) + val_end = (event_end.msg3 << 7 | event_end.msg2) - (1 << 13) + end + if val_start and val_end then + local range = event_end.ppqpos - event_start.ppqpos + local pos = range ~= 0 and (ppqpos - event_start.ppqpos) / range or 0 + val = CalculateCCValueAtTime(val_start, val_end, pos, shape, beztension) + rv = true + end + elseif event_start then + val = b3 and event_start.msg3 or b2 and event_start.msg2 or pb and (event_start.msg3 << 7 | event_start.msg2) - (1 << 13) or 0 + rv = true + end + if rv then + if pb then + val = (math.floor(val + 0.5) + (1 << 13)) & 0x3FFF + msg2out = val & 0x7F + msg3out = (val >> 7) & 0x7F + else + msg2out = b3 and event_start.msg2 or b2 and math.floor(val + 0.5) or 0 + msg3out = b3 and math.floor(val + 0.5) or 0 + end + end + end + return rv, val, ppqpos, chanmsg, chan, msg2out, msg3out +end + +----------------------------------------------------------------------------- + +MIDIUtils.MIDI_GetCCValueAtTime = function(take, chanmsg, chan, msg2, time) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*'), + MakeTypedArg(chanmsg, 'number'), + MakeTypedArg(chan, 'number'), + MakeTypedArg(time, 'number') + ) + return select(2, xpcall(MIDI_GetCCValueAtTime, OnError, take, chanmsg, chan, msg2, time)) +end + +----------------------------------------------------------------------------- +------------------------------------ MISC ----------------------------------- + +local noteNames = { 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B' } + +local function MIDI_NoteNumberToNoteName(notenum, names) + notenum = math.abs(notenum) % 128 + names = names or noteNames + local notename = names[notenum % 12 + 1] + local octave = math.floor((notenum / 12)) - 1 + local noteOffset = ReadREAPERConfigVar_Int('midioctoffs') or -0xFF + if noteOffset ~= -0xFF then + noteOffset = noteOffset - 1 -- 1 == 0 in the interface (C4) + octave = octave + noteOffset + end + return notename..octave +end + +local function MIDI_DebugInfo(take) + EnsureTake(take) + local noteon = 0 + local noteoff = 0 + local cc = 0 + local sysex = 0 + local text = 0 + local bezier = 0 + local unknown = 0 + for _, event in ipairs(MIDIEvents) do + if event:is_a(NoteOnEvent) then noteon = noteon + 1 + elseif event:is_a(NoteOffEvent) then noteoff = noteoff + 1 + elseif event:is_a(CCEvent) then + cc = cc + 1 + local _, bezEvt = FindBezierData(event.idx, event) + if bezEvt then + bezier = bezier + 1 + end + elseif event:is_a(SysexEvent) then sysex = sysex + 1 + elseif event:is_a(MetaEvent) then text = text + 1 + elseif event:is_a(BezierEvent) then bezier = bezier + 1 + else unknown = unknown + 1 + end + end + return noteon, noteoff, cc, sysex, text, bezier, unknown +end + +local function MIDI_GetPPQ(take) + EnsureTake(take) + local qn1 = r.MIDI_GetProjQNFromPPQPos(take, 0) + local qn2 = qn1 + 1 + return math.floor(r.MIDI_GetPPQPosFromProjQN(take, qn2) - r.MIDI_GetPPQPosFromProjQN(take, qn1)) +end + + +MIDIUtils.MIDI_NoteNumberToNoteName = function(notenum, names) + EnforceArgs( + MakeTypedArg(notenum, 'number'), + MakeTypedArg(names, 'table', true) + ) + return select(2, xpcall(MIDI_NoteNumberToNoteName, OnError, notenum, names)) +end + +MIDIUtils.MIDI_DebugInfo = function(take) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*') + ) + return select(2, xpcall(MIDI_DebugInfo, OnError, take)) +end + +MIDIUtils.MIDI_GetPPQ = function(take) + EnforceArgs( + MakeTypedArg(take, 'userdata', false, 'MediaItem_Take*') + ) + return select(2, xpcall(MIDI_GetPPQ, OnError, take)) +end + +----------------------------------------------------------------------------- +----------------------------------- EXPORT ---------------------------------- + +return MIDIUtils diff --git a/MIDI Editor/talagan_OneSmallStep/images/edit_mode_repitch.lua b/MIDI Editor/talagan_OneSmallStep/images/edit_mode_repitch.lua new file mode 100644 index 000000000..c8c708e5e --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/images/edit_mode_repitch.lua @@ -0,0 +1,13 @@ +-- @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\x7E\x49\x44\x41\x54\x38\x8D\xC5\x94\x4B\x0E\xC0\x20\x08\x44\x4B\xD3\xFB\x5F\x79\xBA\x6B\x10\z +\x18\x15\x30\x29\x1B\x62\xC4\xA7\xC3\x47\x01\x70\x9D\xB4\xA7\x78\x2E\x7A\x85\x74\x80\xC2\x36\xEE\x22\x90\x5A\x17\xE8\xA4\x77\x81\x4E\xBA\xCE\xE1\xAC\xDC\x2C\x67\z +\xB0\x7B\x1A\x48\x13\x9D\xB1\xAA\x64\xAB\xE6\x5B\x33\xE0\xAA\xDB\x45\xC5\x0C\xB2\x23\x20\x8C\x9F\x41\xB5\x0F\x81\x30\x81\xBB\x50\x0A\xB4\x01\xE9\x42\xB1\xD1\x5B\z +\x81\xD2\xB3\xEC\xFA\x6B\xF7\x42\x56\xE5\x72\x4F\x1E\xFF\x1C\xBA\xA3\x37\x05\xFE\x3A\x7A\xD4\x5E\x21\x6A\x13\x3B\x1F\x74\x61\xC3\x00\x00\x00\x00\x49\x45\x4E\x44\z +\xAE\x42\x60\x82" +; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_insert_back.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_insert_back.lua index 7731cce17..21125cab1 100644 --- a/MIDI Editor/talagan_OneSmallStep/images/indicator_insert_back.lua +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_insert_back.lua @@ -5,10 +5,10 @@ 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\xC5\x49\x44\x41\x54\x38\x8D\xAD\xD5\xC1\x0D\x83\x30\x0C\x40\xD1\x4F\xE0\x52\xA9\x37\xBA\x40\z -\xC5\xB9\x1D\xA1\x03\xB0\x52\x0F\x94\xA5\x18\x84\x7B\x47\xC8\xA1\x03\x54\xED\xA1\x04\x99\x28\x22\x4E\x1A\xDF\x40\xF1\x93\x1D\x6C\x51\x7D\xEE\xAF\x0E\xE8\x81\x96\z -\xFF\xC2\x02\x53\xE3\x61\x43\x26\x36\x2E\x46\x6F\x0A\x60\x32\xB7\x35\x05\xB0\x0D\x6A\x62\xA7\xF8\xB5\xA3\x8E\x18\x98\x84\xC5\xC0\x64\x0C\xA0\x89\x60\x6F\xA0\x56\z -\xE0\xEB\x37\x08\x81\x2E\xD9\x02\x33\x70\x02\xCE\xC0\x61\xC1\x93\x2A\x94\xD8\x04\x3C\x23\xF9\x0F\xFF\x85\xBC\x43\xD9\xD6\xAC\xC0\x82\x21\x41\x39\x8B\x57\xA0\xCB\z -\x01\xFD\x96\x07\xC4\x1A\x91\x71\x87\xA1\xB1\x59\xD7\x08\xB8\x01\x17\xE0\xA8\xC1\x42\x15\xFA\x95\xD6\xE2\x59\x15\x7B\x83\x9D\xB5\xDF\xB1\xD5\x4B\x46\x1D\xB8\xB7\z -\x09\x5A\x74\x74\xA0\x55\xA0\x2A\x0C\xB0\x55\xE9\x5F\xC0\x17\xFE\xF2\x24\x8A\xAC\xF9\xB7\xB2\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" +\x73\x00\x00\x0B\x13\x00\x00\x0B\x13\x01\x00\x9A\x9C\x18\x00\x00\x00\xCA\x49\x44\x41\x54\x38\x8D\xAD\xD4\xC1\x0D\x83\x30\x0C\x40\xD1\x5F\xC8\xA5\x52\x6F\x74\x81\z +\x8A\x73\x3B\x42\x07\x60\xA4\xF6\x40\x59\x2A\x83\x70\xEF\x08\x19\xA1\x6A\x0F\x10\xE4\x46\x11\x71\x02\xBE\x81\xE2\x27\x3B\xD8\x1C\x1E\xDF\x67\x0B\x74\x40\xC3\xB6\z +\x70\x80\x35\x01\xD6\x17\x62\xC3\x6C\x74\xD5\x0E\x98\xCC\x6D\xAA\x1D\xB0\x3F\xB4\x4A\x9D\x62\x6A\x47\x1D\x29\x30\x0B\x4B\x81\xD9\x18\x80\x49\x60\x1F\xA0\x56\xE0\z +\xCB\x37\x88\x81\x3E\xD9\x01\x23\x70\x06\x2E\xC0\x71\xC6\xB3\x2A\x94\x98\x05\xDE\x89\xFC\x57\xF8\x42\xDE\xA1\x6C\x6B\x54\x60\xD1\x90\xA0\x9C\xC5\x1B\xD0\x96\x80\z +\x61\xCB\x3D\x62\x8D\x28\xB8\xC3\xD8\xD8\x2C\x6B\x04\xDC\x81\x2B\x70\xD2\x60\xB1\x0A\xC3\x4A\x6B\xF1\xAC\x8A\xB5\xC1\x2E\xDA\xEF\xD4\xEA\x65\xA3\x1E\x5C\xDB\x04\z +\x2D\x3A\x78\xD0\x29\x50\x15\x06\x38\xC3\xB4\x11\xFE\xAF\xBD\x05\x75\x80\xFD\x01\x9D\x70\x22\xBF\xE6\xF6\xA4\x1F\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" ; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_insert_forward.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_insert_forward.lua index 440b013c6..810fdc581 100644 --- a/MIDI Editor/talagan_OneSmallStep/images/indicator_insert_forward.lua +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_insert_forward.lua @@ -5,11 +5,11 @@ 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\xCE\x49\x44\x41\x54\x38\x8D\xAD\x95\x31\x12\x82\x30\x10\x45\x1F\x81\xD2\x8E\x23\x50\xEB\x41\z -\xB8\x92\x85\xE6\x52\x1C\xC0\x23\x68\xED\xD8\x58\x4A\xE1\x8C\x33\x56\x8C\x16\x26\x10\x42\x42\x42\xE4\x77\x99\xD9\xBC\xFD\xBB\xD9\x9D\x64\x9F\xFD\xB3\x02\x6A\xA0\z -\xE4\x3F\xB5\x40\x53\x58\xB0\x43\x22\x4C\x2A\x46\x2D\x56\x80\x99\x77\x4B\xB1\x02\x6C\x04\x15\xA1\x28\x4B\x32\x14\xB0\x14\x18\x84\xA6\x00\x67\xA1\xC5\x92\xEC\x4A\z -\x1D\x90\xAB\xD8\x49\xEF\x63\x1D\x76\xC0\x0B\xB8\x00\x27\x7E\x33\xE7\x34\x60\x3B\xD4\x3A\x06\x12\xDC\x19\xE6\x77\xE4\x34\xB5\x87\x57\xE0\x6C\x9C\x7B\xA7\xA9\xC0\z -\x0A\xD8\x19\xE7\xDE\x61\x6C\xC9\x1D\xF0\x06\x6E\xC0\x43\xC1\x9C\x1B\xE6\x03\xDA\xCA\x81\x0D\xB0\x65\x78\xE5\x09\xCC\x05\x0C\xAD\xA0\x9C\x83\x41\x7A\x0F\xBD\x89\z -\x53\x80\xB3\x55\x68\x60\xCC\x86\x84\x60\x52\x03\xBD\x53\xBF\x40\xFA\x6E\x9B\xAD\xFD\x05\x7C\x01\x09\xC0\x29\x7B\xE8\x6E\x0D\x79\x00\x00\x00\x00\x49\x45\x4E\x44\z -\xAE\x42\x60\x82" +\x73\x00\x00\x0B\x13\x00\x00\x0B\x13\x01\x00\x9A\x9C\x18\x00\x00\x00\xD1\x49\x44\x41\x54\x38\x8D\xAD\x93\x39\x12\xC2\x30\x0C\x45\x1F\x4E\x4A\xBA\x1C\x21\x35\x1C\z +\x24\x47\x82\x02\x7C\x29\x1F\x20\x47\x80\x9A\xA1\xA1\x24\x1D\x33\x54\x1E\x28\xB0\xB3\x38\x8B\x13\x91\xDF\x79\x46\x7A\xFA\x92\xA5\xCD\xE1\x73\xCC\x81\x02\xC8\xF8\z +\x4F\x15\x60\xD2\x00\x76\x12\xC2\xB4\x63\x14\x6A\x05\x58\x3B\x37\x53\x2B\xC0\x3A\x50\x15\x8B\x0A\xA4\x63\x01\x4B\x81\x51\xA8\x04\x38\x09\x4D\x97\x54\x77\xB2\x40\z +\xE2\x62\x7B\xB3\x9F\xEB\xD0\x02\x2F\xE0\x0A\x94\xFC\x76\x6E\xD0\x40\xE8\xD0\xEB\x1C\x29\xF0\xA0\xD9\xDF\x8E\x53\xE9\x0C\x6F\xC0\xA5\xF5\xAE\x9D\x4A\x81\x39\xB0\z +\x6F\xBD\x6B\x87\x73\x5B\xB6\xC0\x1B\xB8\x03\x4F\x07\x1B\xBC\xB0\x31\x60\xA8\x04\xD8\x02\x3B\x9A\x5F\xEE\xC1\x86\x80\xB1\x13\xD4\x53\x30\x90\xCF\x70\xB4\xB0\x04\z +\x38\xD9\x85\x07\xCE\xB9\x90\x18\x4C\x7B\xE0\xE8\xD6\x2F\x90\xCF\xAD\x52\xC0\xD0\xDD\x7A\xA9\x2A\xC0\x7C\x01\xA8\x2F\x27\xB0\xC5\x0D\x32\xEA\x00\x00\x00\x00\x49\z +\x45\x4E\x44\xAE\x42\x60\x82" ; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_repitch_back.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_repitch_back.lua new file mode 100644 index 000000000..ce9203ab0 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_repitch_back.lua @@ -0,0 +1,16 @@ +-- @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\x02\x49\x44\x41\x54\x38\x8D\xAD\xD5\xB1\x4A\x03\x41\x10\xC6\xF1\xDF\xA9\x20\x82\x9D\x29\xD3\z +\x88\x9D\x8F\xE2\x4B\xC5\x3C\x50\x5A\x1F\xC1\x3A\x95\x85\x10\xD2\x5C\x19\xBB\x40\x48\x11\xCE\x22\x37\xB8\xAC\xB7\x7B\x51\xEE\x83\xE5\x58\x76\xF6\x7F\xDF\x30\x3B\z +\xBB\x4D\xD7\x75\xA6\xD4\xD5\xA4\x34\xDC\xC0\xA2\xBC\xFE\x84\x39\x66\xD8\xA1\xC5\x66\x14\x58\x81\xBD\xE0\xA1\x9F\x1F\xF1\x39\x06\x2C\xA5\x9C\xC3\x16\xB8\xC5\x63\z +\x0D\x56\x72\x38\x04\x0B\xDD\xE3\x35\x8B\x3F\xE1\x80\x2D\xD6\x39\xB0\x06\x2B\xE9\xBA\xFF\xD1\x33\x34\x5D\xD7\xC5\xAE\x12\x6C\x79\x21\x78\x89\x7D\x38\xAC\xC1\xD2\z +\x6F\x49\x11\x7F\x17\x45\x99\x57\x60\x7F\x52\x38\x9C\x8D\xC0\x2E\x49\x19\x0E\x01\xDC\x25\xA0\x45\x3F\x72\xE8\x98\xE3\x13\xB6\x91\x72\x8B\xAF\x6C\xE3\xA5\xAE\x38\z +\x1F\xFA\x0F\xAC\x03\xB8\xC1\xDB\x3F\xA1\x7B\xBC\x63\x85\x4D\x7A\x0E\x03\x1A\xD5\x4E\xD3\x4F\x95\xCF\x97\x7E\x6A\xF0\xAB\x53\x4A\xD0\x1C\x90\x3B\x8C\x1A\x0C\xF6\z +\x72\x29\xFD\x14\x70\xEA\xC7\xDE\xB9\xE5\xDA\x58\x4C\x3B\x25\x57\xE9\xB6\x59\x0D\x87\x9F\x55\xBB\xBE\xC2\x69\x7E\x1F\x56\xD5\x4C\xFD\x04\x7C\x03\x6B\xAA\x4F\xA7\z +\xED\xC3\xC2\x89\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" +; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_repitch_forward.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_repitch_forward.lua new file mode 100644 index 000000000..b09512955 --- /dev/null +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_repitch_forward.lua @@ -0,0 +1,16 @@ +-- @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\x07\x49\x44\x41\x54\x38\x8D\xAD\xD5\x3D\x6A\xC3\x40\x10\x86\xE1\x47\x49\x20\x18\xD2\x45\xA5\z +\x1B\x61\x48\x91\xCA\xE7\x48\x9B\x03\x19\x1F\x28\x6D\x8E\x90\xDA\x17\x30\x69\x54\xCA\x9D\x21\x95\x51\x0A\xED\x82\x2C\xAF\xB2\x8B\xE3\x01\x21\x16\x66\xDF\xFD\xE6\z +\x9B\xFD\xA9\xFA\xBE\x77\xCB\xB8\xBB\x29\x0D\x0F\xB0\xC9\xE7\xAD\xB0\x44\x8D\x0E\x2D\xF6\xB3\xC0\x82\x58\xE3\x05\x8F\x61\x7C\xC0\x67\x0A\x5A\x5A\x72\x13\x60\xB1\z +\x98\x67\xBC\x19\x94\x27\x15\xAE\x82\x8A\x06\x0B\xDC\xFF\x01\xDF\x60\x3B\x82\x9E\x29\x8D\x0A\xD7\x78\xC5\x53\x06\x36\x86\x26\x95\x46\x85\x4D\x00\x15\xF4\xC7\x36\z +\xE4\x25\x95\x46\xE0\x62\x32\xE1\x6A\xE8\x7F\xF6\x61\x5C\x78\x5C\xFE\x32\x2A\xFC\x31\xF8\x37\x4E\xC8\x81\x52\x4A\xEB\x08\xFC\x36\x34\x25\x57\xEE\x74\xB1\x38\x8E\z +\xF3\xBA\x08\xDC\x85\xFF\x78\xF3\xE6\x62\x0A\x3B\xA0\x8D\x1E\xEE\xF1\x81\x2F\x1C\xAF\x84\x9D\x75\x39\x46\x6D\xF0\x72\x5A\xDA\xD8\x8A\x59\x18\x97\x67\xB9\x0B\x0A\z +\x73\x5E\x26\x61\x5C\x9E\xE5\xD6\xD0\xA0\x23\x4E\xE1\x9B\x5A\x30\x0B\x83\xAA\xEF\xFB\x92\xE3\xF1\xAE\xF0\xB6\x29\xBD\xBE\x76\x06\x3B\xB2\xF7\x61\x75\xEB\x27\xE0\z +\x17\xED\xA0\x4C\x2F\xC2\x7F\x5F\xBC\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" +; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_write_back.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_write_back.lua index 537a56ae6..e9e147c40 100644 --- a/MIDI Editor/talagan_OneSmallStep/images/indicator_write_back.lua +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_write_back.lua @@ -5,11 +5,11 @@ 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\xEA\x49\x44\x41\x54\x38\x8D\xAD\xD5\xB1\x4A\x03\x41\x10\xC6\xF1\x5F\xCE\x80\x08\x62\xA3\x65\z -\x1A\xB1\xF6\x45\x7C\x26\xC1\x04\x2C\x7D\x1E\x9F\x43\xB0\x13\x8E\x34\x29\xB5\x09\x81\x90\xE2\xB8\x14\xB7\x1B\xF5\xB2\x77\x2B\x71\xBF\x6E\xD9\xE5\x3F\xDF\xCC\xCE\z -\xCE\x4E\xDA\xB6\x55\x52\x55\x51\x1A\xA6\x1E\xD7\x43\x7B\x77\x98\xE1\x06\x9F\x58\xA1\x1E\xA5\x3D\x5F\x99\x8E\xC0\x1E\x70\x1D\xD6\x3B\x7C\x64\x81\xD2\x29\xF7\x61\z -\x4F\x38\xC7\x6D\x0E\x86\x23\x87\x29\x58\xD4\x25\xE6\xBD\xF3\x0D\xB6\x58\xE2\x0D\xF5\x4F\xE0\x18\x6C\x48\x67\x21\xD0\x7D\x58\x1F\x80\x39\x58\x0E\xBE\x10\x4A\x52\z -\x9D\xE8\x2C\xA5\x8B\x08\x9C\x15\x80\xD1\xA5\xAF\xD2\xF5\xD9\x7F\x61\x07\x55\xBA\xA6\xA5\xAB\x43\x11\xE0\x0A\x5F\x05\xA0\x4D\x04\xD6\x78\x2D\x00\xDD\xF2\xDD\xD8\z -\x11\x1A\x6F\x7B\xE1\x77\x4D\xFF\x12\x64\x19\x1D\x46\x9D\xE2\xB4\xC1\x06\xEF\xBA\x97\x72\xF4\xF4\xC6\x9C\x6E\xF0\x92\x8B\x90\x1A\x0E\x29\xA7\x3B\x21\xA5\x9C\x86\z -\xC6\x57\x84\xF6\xE7\x61\x56\x93\xD2\x5F\xC0\x1E\x5A\x34\x3C\x8C\xFB\x89\xD6\x81\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" +\x73\x00\x00\x0B\x13\x00\x00\x0B\x13\x01\x00\x9A\x9C\x18\x00\x00\x00\xEA\x49\x44\x41\x54\x38\x8D\xAD\xD5\x31\x4E\x03\x31\x10\x85\xE1\x2F\x4B\x24\x84\x44\x17\xCA\z +\x34\x88\x9A\x8B\x70\x89\x1C\x25\x49\xCF\x25\x72\x09\xCE\x81\x44\x87\xB4\x4A\x93\x12\xBA\x48\x51\x8A\xD5\x52\xAC\x1D\x60\xE3\x5D\xA3\xE0\xD7\x59\xB6\xFE\x79\x33\z +\x1E\x8F\x27\x6D\xDB\x2A\xA9\xAA\x28\x0D\xD3\xCD\x66\x70\xEF\x01\x73\xDC\xE1\x03\x3B\xD4\x63\xB0\xC5\x82\xE9\x08\xEC\x09\xB3\xB0\x3E\xE2\x3D\x07\x24\x9D\x72\x1F\z +\xB6\xC4\x35\xEE\x73\x30\xCE\x1D\xA6\x60\x51\xB7\x58\xF5\xCE\x37\x38\x60\x8B\x57\xD4\x3F\x81\x63\xB0\x21\x5D\x85\x40\x8F\x61\x7D\x02\xE6\x60\x39\xF8\x5A\x28\x49\z +\x75\xA1\xB3\x94\x6E\x22\x70\x5E\x00\x46\x97\xBE\x4A\xD7\x67\xFF\x85\x9D\x54\xE9\x9A\x96\xAE\x0E\x45\x80\x3B\x7C\x16\x80\x36\x11\x58\xE3\xA5\x00\xF4\xC0\x77\x63\z +\x47\x68\xBC\xED\xB5\xDF\x35\xFD\x4B\x90\x6D\x74\x18\x75\x89\xD3\x06\x7B\xBC\xE9\x5E\xCA\xD9\xD3\x1B\x73\xBA\xC7\x73\x2E\x42\x6A\x38\xA4\x9C\x1E\x85\x94\x72\x1A\z +\x1A\x5F\x11\xDA\x9F\x87\x59\x4D\x4A\x7F\x01\x5F\x4F\x24\x3C\x8C\x99\x9A\x4C\x40\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" ; diff --git a/MIDI Editor/talagan_OneSmallStep/images/indicator_write_forward.lua b/MIDI Editor/talagan_OneSmallStep/images/indicator_write_forward.lua index 87f381423..9159a7c5b 100644 --- a/MIDI Editor/talagan_OneSmallStep/images/indicator_write_forward.lua +++ b/MIDI Editor/talagan_OneSmallStep/images/indicator_write_forward.lua @@ -6,10 +6,10 @@ 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\xE8\x49\x44\x41\x54\x38\x8D\xAD\xD5\x31\x6A\xC3\x30\x18\x86\xE1\x27\x6D\xA1\x04\xBA\x35\x63\z -\x16\x13\xE8\x56\xC8\x39\x7A\x9E\x8E\x85\xE0\x4B\xE5\x1C\xB9\x80\xC9\x92\x31\x59\xBA\x94\x0E\xC1\x1D\x2C\x51\xB5\xB5\x2D\x15\xF4\x2D\x42\x20\xBF\xBC\x82\x4F\xBF\z -\x17\x7D\xDF\xAB\x99\x9B\xAA\x34\xDC\x81\xB7\xF7\xDC\xB9\x0D\xD6\x58\xE1\x8C\x13\xBA\x69\x60\x3E\x5B\x3C\xE1\x3E\xEC\x2F\xD8\x8F\x41\x4B\xAF\xDC\x04\xD8\x2E\xEC\z -\x1F\xF1\x62\x30\x1F\x35\xDC\x04\x8B\x06\x4B\xDC\xCE\xC0\x77\x68\x13\xE8\x0F\xD3\x68\xB8\xC5\x33\x1E\x32\xB0\x14\x3A\x6A\x1A\x0D\x9B\x5F\x07\x4B\x32\x6A\x1A\x0D\z -\x97\xFF\x00\xCD\x9A\x46\x60\xC9\x35\x4B\xA0\xEB\x5A\xC5\x8E\xD0\x55\x2D\x60\x1B\xD6\x73\x04\x5E\x2B\xC0\x2E\x38\x45\xE0\x47\x05\xD8\x1E\x5D\xAC\xCD\xD1\xD0\xC3\z -\x76\xEC\xAB\x24\x69\xAD\xFE\xC0\xF8\xEE\xE1\x21\xAC\x25\x2F\x65\x12\x96\x02\x3B\x13\xD3\x23\xE4\xD5\xF0\x8A\x66\x61\x94\x0F\x87\x23\x3E\x73\xB0\xD4\x30\x97\x83\z -\x61\x0E\x66\xE7\xE1\xA2\xF6\x2F\xE0\x0B\xF7\x38\x39\xAA\xB7\xDA\x59\x68\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" +\x16\x13\xE8\x56\xC8\x39\x7A\x9E\xEE\xC1\x47\xC9\x25\x72\x8E\x5C\xC0\x64\xC9\x98\xCC\xA5\x43\x71\x06\x4B\x54\x6D\x65\x5B\x05\x7D\x8B\x10\xC8\x2F\xAF\xE0\xD3\xEF\z +\x45\xDF\xF7\x6A\xE6\xAE\x2A\x0D\x0F\xB0\xDF\xCF\x9E\xDB\x60\x8D\x15\x2E\x38\xA3\x1B\x05\x16\x64\x8B\x17\x3C\x86\xFD\x15\x87\x1C\xB4\xF4\xCA\x4D\x80\xED\xC2\xFE\z +\x19\x6F\x06\xF3\xAC\xE1\x26\x58\x34\x58\xE2\x7E\x02\xBE\x43\x9B\x40\x7F\x98\x46\xC3\x2D\x5E\xF1\x34\x03\x4B\xA1\x59\xD3\x68\xD8\xFC\x3A\x58\x92\xAC\x69\x34\x5C\z +\xFE\x03\x34\x69\x1A\x81\x25\xD7\x2C\x81\xAE\x6B\x15\x3B\x42\x57\xB5\x80\x6D\x58\x2F\x11\xF8\x55\x01\x76\xC5\x39\x02\x3F\x2A\xC0\x0E\xE8\x62\x6D\x4E\x86\x1E\xB6\z +\xB9\xAF\x92\xA4\xB5\xFA\x03\xE3\xBB\x87\xC7\xB0\x96\xBC\x94\x51\x58\x0A\xEC\x8C\x4C\x8F\x90\x77\xC3\x2B\x9A\x84\x51\x3E\x1C\x4E\xF8\x9C\x83\xA5\x86\x73\x39\x1A\z +\xE6\xE0\xEC\x3C\x5C\xD4\xFE\x05\xDC\x00\xFF\x2C\x39\x7E\x7B\xBB\x38\xE5\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82" ;