Skip to content

Commit ab380cb

Browse files
authored
Release Quad Cortex MIDI control v1.0 (#1709)
1 parent f988cbc commit ab380cb

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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

Comments
 (0)