|
1 | 1 | -- @description Quad Cortex MIDI control |
2 | 2 | -- @author Bertrand C |
3 | | --- @version 1.0 |
4 | | --- @changelog Initial release |
| 3 | +-- @version 2.3.2 |
| 4 | +-- @changelog |
| 5 | +-- - Fix reapack-index linked to wrong git commit |
| 6 | +-- - README update with region combination name examples |
| 7 | +-- - Add info after setup on how to add a toolbar button for easy access, or re-execute the setup wizard if needed. |
| 8 | +-- - Add MIDI output ID validation in the setup wizard to prevent invalid entries. |
| 9 | +-- - README update with clearer setup instructions and troubleshooting tips. |
| 10 | +-- - Add info after first config on how to add a toolbar button for easy access, or re-execute the setup wizard if needed. |
| 11 | +-- - Add setup to list action list (for easier access and reconfiguration) |
| 12 | +-- - Fix folders to fix category detection with reapack-index |
| 13 | +-- - Split from original Quad Cortex MIDI Control script to create a more modular and maintainable codebase. |
| 14 | +-- - Added a setup wizard for first-time configuration. |
| 15 | +-- - Improved logging and error handling. |
| 16 | +-- - Automatically creates the MIDI output track if it doesn't exist. |
| 17 | +-- @provides |
| 18 | +-- [main] bertrandc_Quad Cortex MIDI control/bertrandc_Quad Cortex MIDI control (setup).lua |
| 19 | +-- [nomain] bertrandc_Quad Cortex MIDI control/lib.lua |
5 | 20 | -- @about |
6 | | --- # Quad Cortex MIDI Control |
| 21 | +-- # Quad Cortex MIDI control |
7 | 22 | -- Real-time MIDI control for Neural DSP Quad Cortex via Reaper Regions. |
8 | 23 | -- - Automates Presets using "#BankLetter" (e.g., #1A). |
9 | 24 | -- - Automates Scenes using "!Sx" (e.g., !S1 or !SA). |
10 | 25 | -- - Auto-activates Gig View on Play and Tuner on Stop (both can be toggled). |
| 26 | +-- - **MIDI Clock Support**: Reaper will send tempo/clock to the Quad Cortex |
| 27 | +-- if "Send clock" is enabled for your MIDI output in Reaper's Preferences. |
11 | 28 | -- |
12 | 29 | -- ## USAGE INSTRUCTIONS |
13 | 30 | -- |
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. |
| 31 | +-- 1. AUTOMATIC INSTALLATION & SETUP |
| 32 | +-- - Run this script. If it's the first time, the SETUP WIZARD will open. |
| 33 | +-- - The script AUTOMATICALLY creates a dedicated MIDI track (Default: "Quad Cortex MIDI control"). |
| 34 | +-- - This track is pre-configured: Armed, Monitoring ON, and Record DISABLED (Mode 2). |
| 35 | +-- - In the Wizard, select the correct MIDI Hardware Output ID for your QC. |
19 | 36 | -- --- |
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). |
| 37 | +-- 2. TEMPO & MIDI CLOCK SYNC |
| 38 | +-- - To sync Tempo (BPM) to your QC: |
| 39 | +-- - Preferences > MIDI Devices > Double-click Output > Check "Send clock to this device". |
25 | 40 | -- --- |
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". |
| 41 | +-- 3. SYNC YOUR PRESETS AND SCENES |
| 42 | +-- - PRESETS: Use "#BankLetter" format (e.g., "#1A", "#12C"). |
| 43 | +-- - SCENES: Use "!SA" to "!SH" (Matches QC Footswitches A to H). |
29 | 44 | -- --- |
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") |
| 45 | +-- 4. LOG LEVELS |
| 46 | +-- - Log Level 1 [INFO]: Preset/Scene changes and Transport status. |
| 47 | +-- - Log Level 2 [DEBUG]: Full configuration details and file operations. |
| 48 | + |
| 49 | +-- Main synchronization engine |
| 50 | + |
| 51 | +local base_path = debug.getinfo(1).source:match("@?(.*[\\/])") |
| 52 | +local lib = dofile(base_path .. "bertrandc_Quad Cortex MIDI control/lib.lua") |
| 53 | + |
| 54 | +-- --- INITIALIZATION --- |
| 55 | +reaper.ClearConsole() |
| 56 | + |
| 57 | +if not lib.LoadSettings() then |
| 58 | + lib.Log("First run or missing config. Launching Setup Wizard...", 1) |
| 59 | + local setup_success = dofile(base_path .. "bertrandc_Quad Cortex MIDI control/bertrandc_Quad Cortex MIDI control (setup).lua") |
| 60 | + if not setup_success then |
| 61 | + lib.SetToolbarButtonState(0) |
| 62 | + return |
60 | 63 | end |
| 64 | + lib.LoadSettings() |
61 | 65 | end |
62 | 66 |
|
63 | | -function SendMIDI(status, data1, data2) |
64 | | - local ch = MIDI_CHANNEL - 1 |
65 | | - reaper.StuffMIDIMessage(MIDI_OUTPUT_ID, status + ch, data1, data2) |
| 67 | +if not lib.EnsureControlTrack() then |
| 68 | + lib.SetToolbarButtonState(0) |
| 69 | + return |
66 | 70 | end |
67 | 71 |
|
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 | +lib.Log("Hardware Check: OK", 1) |
72 | 73 |
|
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 |
| 74 | +local lastPlayState, lastPc, lastCc = -1, -1, -1 |
117 | 75 |
|
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 |
| 76 | +function MainLoop() |
| 77 | + local playState = reaper.GetPlayState() |
| 78 | + local playPos = (playState == 0) and reaper.GetCursorPosition() or reaper.GetPlayPosition() |
139 | 79 |
|
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 |
| 80 | + -- --- TRANSPORT HANDLING (Log Level 1) --- |
| 81 | + if playState == 1 and lastPlayState ~= 1 then |
| 82 | + local statusMsg = "Play" |
| 83 | + if lib.Config.AUTO_TUNER == "true" then |
| 84 | + lib.SendMidi(0xB0, 45, 0) -- Tuner OFF |
| 85 | + statusMsg = statusMsg .. " | Tuner: OFF" |
| 86 | + end |
| 87 | + if lib.Config.AUTO_GIGVIEW == "true" then |
| 88 | + lib.SendMidi(0xB0, 46, 127) -- GigView ON |
| 89 | + statusMsg = statusMsg .. " | GigView: ON" |
| 90 | + end |
| 91 | + lib.Log(statusMsg, 1) |
| 92 | + lastPc, lastCc = -1, -1 |
| 93 | + |
| 94 | + elseif (playState == 0 or playState == 2) and lastPlayState == 1 then |
| 95 | + local statusMsg = (playState == 0) and "Stop" or "Pause" |
| 96 | + if lib.Config.AUTO_TUNER == "true" then |
| 97 | + lib.SendMidi(0xB0, 45, 127) -- Tuner ON |
| 98 | + statusMsg = statusMsg .. " | Tuner: ON" |
| 99 | + end |
| 100 | + lib.Log(statusMsg, 1) |
144 | 101 | end |
145 | 102 |
|
146 | | - local current = GetQCStateAtPos(play_pos) |
| 103 | + -- --- REGION PROCESSING --- |
| 104 | + local current = lib.GetProjectState(playPos) |
147 | 105 |
|
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 |
| 106 | + if current.pc and current.pc ~= lastPc then |
| 107 | + lib.SendMidi(0xB0, 32, 1) |
| 108 | + lib.SendMidi(0xC0, current.pc, 0) |
| 109 | + lastPc, lastCc = current.pc, -1 |
| 110 | + lib.Log("Preset Change -> " .. current.pc_name, 1) |
154 | 111 | end |
155 | 112 |
|
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 |
| 113 | + if current.cc and current.cc ~= lastCc then |
| 114 | + lib.SendMidi(0xB0, 43, current.cc) |
| 115 | + lastCc = current.cc |
| 116 | + lib.Log("Scene Change -> Scene " .. current.cc_name, 1) |
160 | 117 | end |
161 | 118 |
|
162 | | - last_play_state = play_state |
163 | | - reaper.defer(Main) |
| 119 | + lastPlayState = playState |
| 120 | + reaper.defer(MainLoop) |
164 | 121 | end |
165 | 122 |
|
166 | | -function OnExit() |
167 | | - reaper.SetToggleCommandState(sectionID, cmdID, 0) |
168 | | - reaper.RefreshToolbar2(sectionID, cmdID) |
169 | | - Log("QC MIDI Control: DEACTIVATED", 1) |
170 | | -end |
| 123 | +-- --- EXECUTION --- |
| 124 | +lib.SetToolbarButtonState(1) |
| 125 | +reaper.atexit(lib.HandleExit) |
171 | 126 |
|
172 | | -reaper.atexit(OnExit) |
173 | | -reaper.ClearConsole() |
174 | | -Log("QC MIDI Control 1.0 - Ready", 1) |
175 | | -Main() |
| 127 | +lib.Log("Engine Active (MIDI Track: " .. lib.Config.TRACK_NAME .. ")", 1) |
| 128 | +lib.Log("Note: Enable 'Send clock' in MIDI Prefs for Tempo Sync.", 1) |
| 129 | + |
| 130 | +MainLoop() |
0 commit comments