|
| 1 | +-- @description Quad Cortex MIDI control |
| 2 | +-- @author Bertrand C |
| 3 | +-- @version 1.0 |
| 4 | +-- @changelog Initial release |
| 5 | +-- @about |
| 6 | +-- # Quad Cortex MIDI Control |
| 7 | +-- Real-time MIDI control for Neural DSP Quad Cortex via Reaper Regions. |
| 8 | +-- - Automates Presets using "#BankLetter" (e.g., #1A). |
| 9 | +-- - Automates Scenes using "!Sx" (e.g., !S1 or !SA). |
| 10 | +-- - Auto-activates Gig View on Play and Tuner on Stop (both can be toggled). |
| 11 | +-- |
| 12 | +-- ## USAGE INSTRUCTIONS |
| 13 | +-- |
| 14 | +-- 1. REAPER CONFIGURATION |
| 15 | +-- - Create a track (e.g., "MIDI QC CTRL"). |
| 16 | +-- - Set Track Input to "MIDI > Virtual MIDI Keyboard > All Channels". |
| 17 | +-- - Add a "MIDI Hardware Output" on this track to your MIDI device (WIDI / Interface). |
| 18 | +-- - Record Arm and Input Monitoring must be ENABLED for the script to reach the QC. |
| 19 | +-- --- |
| 20 | +-- 2. SYNC YOUR PRESETS AND SCENES |
| 21 | +-- - Create Regions in Reaper named with the following tags: |
| 22 | +-- - PRESETS: Use "#BankLetter" format (e.g., "#1A", "#12C"). |
| 23 | +-- - SCENES: Use "!Sx" where x is 1-8 or A-H (e.g., "!S1", "!SA"). |
| 24 | +-- Note: Small "Scene" regions inside larger "Preset" regions are supported (Inheritance). |
| 25 | +-- --- |
| 26 | +-- 3. TOOLBAR INTEGRATION |
| 27 | +-- - Assign this script to a toolbar button. The button will light up when |
| 28 | +-- active. To stop, click again and select "Terminate Instance". |
| 29 | +-- --- |
| 30 | +-- 4. ADDITIONAL FEATURES |
| 31 | +-- - PLAY: Automatically switches QC to "Gig View" and hides the Tuner. |
| 32 | +-- Set AUTO_GIGVIEW to false to disable this behavior. |
| 33 | +-- - STOP: Automatically activates the "Tuner" on the QC for silent breaks. |
| 34 | +-- Set AUTO_TUNER to false to disable this behavior. |
| 35 | + |
| 36 | +-- ============================================================================ |
| 37 | +-- CONFIGURATION & OPTIONS |
| 38 | +-- ============================================================================ |
| 39 | +local MIDI_CHANNEL = 1 |
| 40 | +local MIDI_OUTPUT_ID = 0 |
| 41 | + |
| 42 | +-- AUTOMATION OPTIONS |
| 43 | +local AUTO_GIGVIEW = true |
| 44 | +local AUTO_TUNER = true |
| 45 | + |
| 46 | +-- LOG LEVEL (0: Silence, 1: Essential, 2: Full Debug) |
| 47 | +local DEBUG_LEVEL = 1 |
| 48 | + |
| 49 | +-- Initialize Toggle Button State |
| 50 | +local _, _, sectionID, cmdID = reaper.get_action_context() |
| 51 | +reaper.SetToggleCommandState(sectionID, cmdID, 1) |
| 52 | +reaper.RefreshToolbar2(sectionID, cmdID) |
| 53 | + |
| 54 | +-- ============================================================================ |
| 55 | +-- MIDI & LOGGING FUNCTIONS |
| 56 | +-- ============================================================================ |
| 57 | +function Log(msg, level) |
| 58 | + if (level or 1) <= DEBUG_LEVEL then |
| 59 | + reaper.ShowConsoleMsg(tostring(msg) .. "\n") |
| 60 | + end |
| 61 | +end |
| 62 | + |
| 63 | +function SendMIDI(status, data1, data2) |
| 64 | + local ch = MIDI_CHANNEL - 1 |
| 65 | + reaper.StuffMIDIMessage(MIDI_OUTPUT_ID, status + ch, data1, data2) |
| 66 | +end |
| 67 | + |
| 68 | +function ConvertQCtoPC(bank, letter) |
| 69 | + local letters = {A=0, B=1, C=2, D=3, E=4, F=5, G=6, H=7} |
| 70 | + return ((tonumber(bank) - 1) * 8) + letters[letter:upper()] |
| 71 | +end |
| 72 | + |
| 73 | +-- ============================================================================ |
| 74 | +-- REGION ANALYSIS |
| 75 | +-- ============================================================================ |
| 76 | +function GetQCStateAtPos(pos) |
| 77 | + local num_markers = reaper.CountProjectMarkers(0) |
| 78 | + local state = { preset = nil, scene = nil, scene_dur = 9999999 } |
| 79 | + |
| 80 | + for i = 0, num_markers - 1 do |
| 81 | + local _, isrgn, r_pos, r_end, r_name, _ = reaper.EnumProjectMarkers3(0, i) |
| 82 | + |
| 83 | + if isrgn and pos >= r_pos and pos <= r_end then |
| 84 | + local match_found = false |
| 85 | + |
| 86 | + local bank, letter = r_name:match("#(%d+)([A-Ha-h])") |
| 87 | + if bank and letter then |
| 88 | + state.preset = ConvertQCtoPC(bank, letter) |
| 89 | + state.preset_name = bank .. letter |
| 90 | + match_found = true |
| 91 | + end |
| 92 | + |
| 93 | + local scene_map = {A=0, B=1, C=2, D=3, E=4, F=5, G=6, H=7} |
| 94 | + local s_match = r_name:match("!S([A-H1-8a-h])") |
| 95 | + |
| 96 | + if s_match then |
| 97 | + local dur = r_end - r_pos |
| 98 | + if dur < state.scene_dur then |
| 99 | + state.scene_dur = dur |
| 100 | + if s_match:match("%d") then |
| 101 | + state.scene = tonumber(s_match) - 1 |
| 102 | + else |
| 103 | + state.scene = scene_map[s_match:upper()] |
| 104 | + end |
| 105 | + state.scene_name = s_match:upper() |
| 106 | + match_found = true |
| 107 | + end |
| 108 | + end |
| 109 | + |
| 110 | + if not match_found then |
| 111 | + Log(" [DEBUG] Region ignored: " .. r_name, 2) |
| 112 | + end |
| 113 | + end |
| 114 | + end |
| 115 | + return state |
| 116 | +end |
| 117 | + |
| 118 | +-- ============================================================================ |
| 119 | +-- MAIN LOOP |
| 120 | +-- ============================================================================ |
| 121 | +local last_play_state = -1 |
| 122 | +local last_sent_preset = -1 |
| 123 | +local last_sent_scene = -1 |
| 124 | +local last_debug_time = 0 |
| 125 | + |
| 126 | +function Main() |
| 127 | + local play_state = reaper.GetPlayState() |
| 128 | + local play_pos = (play_state == 0) and reaper.GetCursorPosition() or reaper.GetPlayPosition() |
| 129 | + |
| 130 | + if play_state == 1 and (last_play_state == 0 or last_play_state == 2 or last_play_state == -1) then |
| 131 | + Log("► PLAYING: GigView ON / Tuner OFF", 1) |
| 132 | + if AUTO_TUNER then SendMIDI(0xB0, 45, 0) end |
| 133 | + if AUTO_GIGVIEW then SendMIDI(0xB0, 46, 127) end |
| 134 | + last_sent_preset, last_sent_scene = -1, -1 |
| 135 | + elseif (play_state == 0 or play_state == 2) and last_play_state == 1 then |
| 136 | + Log("■ STOPPED: Tuner ON", 1) |
| 137 | + if AUTO_TUNER then SendMIDI(0xB0, 45, 127) end |
| 138 | + end |
| 139 | + |
| 140 | + local current_time = reaper.time_precise() |
| 141 | + if DEBUG_LEVEL >= 2 and (current_time - last_debug_time > 1.0) then |
| 142 | + Log("[HEARTBEAT] Monitoring at " .. play_pos, 2) |
| 143 | + last_debug_time = current_time |
| 144 | + end |
| 145 | + |
| 146 | + local current = GetQCStateAtPos(play_pos) |
| 147 | + |
| 148 | + if current.preset and current.preset ~= last_sent_preset then |
| 149 | + Log(" >>> Target Preset: #" .. current.preset_name, 1) |
| 150 | + SendMIDI(0xB0, 32, 1) |
| 151 | + SendMIDI(0xC0, current.preset, 0) |
| 152 | + last_sent_preset = current.preset |
| 153 | + last_sent_scene = -1 |
| 154 | + end |
| 155 | + |
| 156 | + if current.scene and current.scene ~= last_sent_scene then |
| 157 | + Log(" >>> Target Scene: !S" .. current.scene_name, 1) |
| 158 | + SendMIDI(0xB0, 43, current.scene) |
| 159 | + last_sent_scene = current.scene |
| 160 | + end |
| 161 | + |
| 162 | + last_play_state = play_state |
| 163 | + reaper.defer(Main) |
| 164 | +end |
| 165 | + |
| 166 | +function OnExit() |
| 167 | + reaper.SetToggleCommandState(sectionID, cmdID, 0) |
| 168 | + reaper.RefreshToolbar2(sectionID, cmdID) |
| 169 | + Log("QC MIDI Control: DEACTIVATED", 1) |
| 170 | +end |
| 171 | + |
| 172 | +reaper.atexit(OnExit) |
| 173 | +reaper.ClearConsole() |
| 174 | +Log("QC MIDI Control 1.0 - Ready", 1) |
| 175 | +Main() |
0 commit comments