From 970d3f0f5651c028f30703ff1c36f86d8ec1f4e0 Mon Sep 17 00:00:00 2001 From: Brent Baccala Date: Mon, 30 Jun 2025 20:45:13 -0400 Subject: [PATCH 1/6] initial version of "MIDI PC/CC" plugin script --- share/scripts/midi_pc_and_cc.lua | 221 +++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 share/scripts/midi_pc_and_cc.lua diff --git a/share/scripts/midi_pc_and_cc.lua b/share/scripts/midi_pc_and_cc.lua new file mode 100644 index 0000000000..8bf976e4f1 --- /dev/null +++ b/share/scripts/midi_pc_and_cc.lua @@ -0,0 +1,221 @@ +ardour { + ["type"] = "dsp", + name = "MIDI PC/CC", + category = "Utility", + author = "Brent Baccala", + description = [[Handle MIDI PC and CC messages + +It only needs to be on one track to hear the MIDI Program Change (PC) and Continuous Controller (CC) messages. + +It operates on all of the tracks based on commands set in their comment blocks. + +Every track with "MIDI Program #" in its comments in activated when that PC is sent, and is deactivated when any other PC is sent + +The program number can be comma-separated ranges, as in "MIDI Program 0-7,9" + +Each such track can have MIDI CC lines with arbitrary Lua code. + +Variables route (the current route) and value (the CC value) are defined. + +The following are working MIDI CC lines. + +Mute or unmute the current track: +MIDI CC 93: Session:set_control(route:mute_control(), (value == 0) and 1 or 0, PBD.GroupControlDisposition.NoGroup) + +Program blocks are supported. This makes CC 93 role transport: +MIDI CC 93: if value<40 then +MIDI CC 93: Session:request_stop (false, false, ARDOUR.TransportRequestSource.TRS_UI) +MIDI CC 93: else +MIDI CC 93: Session:request_locate(0, false, ARDOUR.LocateTransportDisposition.MustStop, ARDOUR.TransportRequestSource.TRS_UI) +MIDI CC 93: Session:request_roll (ARDOUR.TransportRequestSource.TRS_UI) +MIDI CC 93: end + +There's a convenience function to roll transport at a start time in seconds: +MIDI CC 93: roll_transport(value, 0) + +Another convenience function to activate or deactivate a plugin: +MIDI CC 91: activate_processor_by_name(route, value, "Gigaverb") + +A convenience function to select a preset: +MIDI CC 93: load_preset(route, "plugin", "preset") + +Remember that a "MIDI Program #" line is required for MIDI CC lines to be processed +]] +} + +function dsp_ioconfig () + return { { midi_in = 1, midi_out = 1, audio_in = 0, audio_out = 0}, } +end + +-- +-- activate_processor_by_name(route, name) +-- +-- a convenience function that enables or disables a plugin by name (a string) +-- route should be an Ardour Route object (not a string) + +function get_processor_by_name(route, name) + local i = 0; + repeat + local proc = route:nth_plugin (i) -- get Nth Ardour::Processor + if (not proc:isnil()) and proc:display_name() == name then + return proc + end + i = i + 1 + until proc:isnil() + return nil +end + +function activate_processor_by_name(route, value, name) + local proc = get_processor_by_name(route, name) + if not proc:isnil() then + if value == 0 then + proc:deactivate() + else + proc:activate() + end + end +end + +-- +-- load_preset(route, plugin, preset) +-- +-- a convenience function to load a plugin's preset by name +-- route is an Ardour Route object +-- plugin and preset are strings + +function load_preset(route, plugin, preset) + local proc = get_processor_by_name(route, plugin) + if not proc:isnil() then + local pp = proc:to_insert():plugin(0) + pp:load_preset(pp:preset_by_label(preset)) + end +end + +-- roll_transport(value, location) +-- +-- a convenience function to start or stop the transport +-- value is 0 to stop transport and not 0 to start transport +-- location is a value in seconds + +function roll_transport(value, location) + if value==0 then + Session:request_stop (false, false, ARDOUR.TransportRequestSource.TRS_UI) + else + -- default value of location is 0 (start of session) + if not location then location = 0 end + start_sample = Session:nominal_sample_rate() * location; + Session:request_locate(location, false, ARDOUR.LocateTransportDisposition.MustStop, ARDOUR.TransportRequestSource.TRS_UI) + Session:request_roll (ARDOUR.TransportRequestSource.TRS_UI) + end +end + +-- the currently active functions on the Continuous Controllers +-- it's a list of pairs, indexed by CC number, each pair a Route object and a Lua function +CC_functions = { } + +function process_midi_messages() + for _,b in pairs (midiin) do + local t = b["time"] -- t = [ 1 .. n_samples ] + local d = b["data"] -- midi-event data + local event_type + local channel + if #d == 0 then + event_type = -1 + channel = -1 + else + event_type = d[1] >> 4 + channel = d[1] & 15 + end + if (event_type == 11) then + -- Continuous Controller message + -- if any functions are registered for this CC, call them with their respective Route objects and the value of the controller + local lst = CC_functions[d[2]] + if lst then + for _, tbl in ipairs(lst) do + local route = tbl[1] + local func = tbl[2] + func(route, d[3]) + end + end + end + if (event_type == 12) then + -- Program Change message + -- Parse/reparse the comment blocks + local patch = d[2]; + -- Clear the global table of CC functions; it'll be recreated during the parse + CC_functions = { } + -- Run through all comment blocks on all routes looking for certain strings + for route in Session:get_routes():iter() do + local nextchar = 1 + local MIDI_Program_seen = false + local route_comment = route:comment() + local route_active = false + local local_CC_functions = { } + -- Look for "MIDI Program" statements that match the patch number in the Program Change message + local MIDI_Program_start,MIDI_Program_end,program_list = string.find(route_comment, "MIDI Program (%d[-%d,]*)", nextchar) + if MIDI_Program_start then + local fieldstart = 1 + program_list = program_list .. ',' + repeat + local r,_,start,stop = string.find(program_list, "^(%d+)-(%d+)", fieldstart) + if r and patch >= tonumber(start) and patch <= tonumber(stop) then + route_active = true + end + r,_,program = string.find(program_list, "^(%d+)", fieldstart) + if r and patch == tonumber(program) then + route_active = true + end + local nexti = string.find(program_list, ",", fieldstart) + fieldstart = nexti + 1 + until fieldstart > string.len(program_list) + -- all tracks with a "MIDI Program" line present are set either active or inactive, depending on if they matched + -- all other tracks (no "MIDI Progam" line) are not affected (they never get to this point) + route:set_active(route_active, nil); + -- if this route is active, parse any CC functions in the route's comment block + if route_active then + local nextchar = 1 + local local_CC_functions = { } + -- we wrap the code inside "return function (route, value)" and "end" + -- so that when we pcall it with no argument it returns a function that takes two arguments + while true do + MIDI_CC_start, MIDI_CC_end, CC_num, CC_program = string.find(route_comment, "MIDI CC (%d+): ([^\n]+)", nextchar) + if MIDI_CC_start == nil then break end + if (local_CC_functions[CC_num] == nil) then + local_CC_functions[CC_num] = "return function(route, value)\n" + end + local_CC_functions[CC_num] = local_CC_functions[CC_num] .. CC_program .. "\n" + nextchar = MIDI_CC_end + 1 + end + -- done parsing (or at least gathering all of the lines together) + -- now compile the CC functions and insert them in the global table + for key, val in pairs(local_CC_functions) do + val = val .. "\nend" + local generator, err = load(val) + if generator then + local ok, func = pcall(generator) + if ok then + if not CC_functions[tonumber(key)] then + CC_functions[tonumber(key)] = { } + end + table.insert(CC_functions[tonumber(key)], { route, func }) + else + print("Execution error:", func) + end + else + print("Compilation error:", err) + end + end + end + end + end + end + end +end + +function dsp_run (_, _, n_samples) + -- without pcall, any errors would crash Ardour; instead, print errors + status, error = pcall(process_midi_messages) + if not status then + print(error) + end +end From 01d221170f79d40bc9b639d9558c55b099909c13 Mon Sep 17 00:00:00 2001 From: Brent Baccala Date: Mon, 7 Jul 2025 16:53:45 -0400 Subject: [PATCH 2/6] MIDI PC/CC script: handle Bank Select messages --- share/scripts/midi_pc_and_cc.lua | 201 ++++++++++++++++++++----------- 1 file changed, 129 insertions(+), 72 deletions(-) diff --git a/share/scripts/midi_pc_and_cc.lua b/share/scripts/midi_pc_and_cc.lua index 8bf976e4f1..5976092685 100644 --- a/share/scripts/midi_pc_and_cc.lua +++ b/share/scripts/midi_pc_and_cc.lua @@ -13,9 +13,11 @@ Every track with "MIDI Program #" in its comments in activated when that PC is s The program number can be comma-separated ranges, as in "MIDI Program 0-7,9" +Tracks can also be labeled "MIDI Bank # Program #", in which case both bank and program numbers have to match. + Each such track can have MIDI CC lines with arbitrary Lua code. -Variables route (the current route) and value (the CC value) are defined. +In such code, variables route (the current route) and value (the CC value) are defined. The following are working MIDI CC lines. @@ -39,7 +41,7 @@ MIDI CC 91: activate_processor_by_name(route, value, "Gigaverb") A convenience function to select a preset: MIDI CC 93: load_preset(route, "plugin", "preset") -Remember that a "MIDI Program #" line is required for MIDI CC lines to be processed +Remember that a "MIDI Program #" (or "MIDI Bank # Program #") line is required for MIDI CC lines to be processed ]] } @@ -113,6 +115,115 @@ end -- it's a list of pairs, indexed by CC number, each pair a Route object and a Lua function CC_functions = { } +-- global variables to track current bank and program +bank_msb = 0 +bank_lsb = 0 +bank = 0 +program = 0 + +-- return true/false if number (an integer) is in range (a string) +-- format of range is a comma-separated list of items, each either START-STOP or VALUE +function match_number_range(number, range) + local fieldstart = 1 + local match = false + range = range .. ',' + repeat + local r,_,start,stop = string.find(range, "^(%d+)-(%d+)", fieldstart) + if r and number >= tonumber(start) and number <= tonumber(stop) then + match = true + end + r,_,value = string.find(range, "^(%d+)", fieldstart) + if r and number == tonumber(value) then + match = true + end + local nexti = string.find(range, ",", fieldstart) + fieldstart = nexti + 1 + until fieldstart > string.len(range) + return match +end + +function program_change() + -- Program Change or Bank Change message + -- program and bank are global variables + -- Parse/reparse the comment blocks + -- Clear the global table of CC functions; it'll be recreated during the parse + CC_functions = { } + -- Run through all comment blocks on all routes looking for certain strings + for route in Session:get_routes():iter() do + local nextchar = 1 + local MIDI_Program_seen = false + local route_comment = route:comment() + local route_active = false + local local_CC_functions = { } + -- Look for "MIDI Bank # Program #" statements that match both the program number and the bank number + while true do + local MIDI_Program_start, MIDI_Program_end, bank_list, program_list + MIDI_Program_start,MIDI_Program_end,bank_list,program_list = string.find(route_comment, "MIDI Bank (%d[-%d,]*) Program (%d[-%d,]*)", nextchar) + if bank_list then + route_active = route_active or (match_number_range(program, program_list) and match_number_range(bank, bank_list)) + MIDI_Program_seen = true + nextchar = MIDI_Program_end + 1 + else + break + end + end + -- Look for "MIDI Program #" statements that match just the program number + nextchar = 1 + while true do + local MIDI_Program_start, MIDI_Program_end, program_list + MIDI_Program_start,MIDI_Program_end,program_list = string.find(route_comment, "MIDI Program (%d[-%d,]*)", nextchar) + if program_list then + route_active = route_active or match_number_range(program, program_list) + MIDI_Program_seen = true + nextchar = MIDI_Program_end + 1 + else + break + end + end + -- if at least one of either kind of line was seen, there is further handling of this track + if MIDI_Program_seen then + -- all tracks with a "MIDI Program" line present are set either active or inactive, depending on if they matched + -- all other tracks (no "MIDI Progam" line) are not affected (they never get to this point) + route:set_active(route_active, nil); + -- if this route is active, parse any CC functions in the route's comment block + if route_active then + local nextchar = 1 + local local_CC_functions = { } + -- we wrap the code inside "return function (route, value)" and "end" + -- so that when we pcall it with no argument it returns a function that takes two arguments + while true do + MIDI_CC_start, MIDI_CC_end, CC_num, CC_program = string.find(route_comment, "MIDI CC (%d+): ([^\n]+)", nextchar) + if MIDI_CC_start == nil then break end + if (local_CC_functions[CC_num] == nil) then + local_CC_functions[CC_num] = "return function(route, value)\n" + end + local_CC_functions[CC_num] = local_CC_functions[CC_num] .. CC_program .. "\n" + nextchar = MIDI_CC_end + 1 + end + -- done parsing (or at least gathering all of the lines together) + -- now compile the CC functions and insert them in the global table + for key, val in pairs(local_CC_functions) do + val = val .. "\nend" + local generator, err = load(val) + if generator then + local ok, func = pcall(generator) + if ok then + if not CC_functions[tonumber(key)] then + CC_functions[tonumber(key)] = { } + end + table.insert(CC_functions[tonumber(key)], { route, func }) + else + print("Execution error:", func) + end + else + print("Compilation error:", err) + end + end + end + end + end +end + function process_midi_messages() for _,b in pairs (midiin) do local t = b["time"] -- t = [ 1 .. n_samples ] @@ -128,86 +239,32 @@ function process_midi_messages() end if (event_type == 11) then -- Continuous Controller message + local num = d[2] + local val = d[3] + -- handle Bank Select messages + if num == 0 or num == 32 then + if num == 0 then + bank_msb = val + else + bank_lsb = val + end + bank = 128 * bank_msb + bank_lsb + program_change() + end -- if any functions are registered for this CC, call them with their respective Route objects and the value of the controller - local lst = CC_functions[d[2]] + local lst = CC_functions[num] if lst then for _, tbl in ipairs(lst) do local route = tbl[1] local func = tbl[2] - func(route, d[3]) + func(route, val) end end end if (event_type == 12) then -- Program Change message - -- Parse/reparse the comment blocks - local patch = d[2]; - -- Clear the global table of CC functions; it'll be recreated during the parse - CC_functions = { } - -- Run through all comment blocks on all routes looking for certain strings - for route in Session:get_routes():iter() do - local nextchar = 1 - local MIDI_Program_seen = false - local route_comment = route:comment() - local route_active = false - local local_CC_functions = { } - -- Look for "MIDI Program" statements that match the patch number in the Program Change message - local MIDI_Program_start,MIDI_Program_end,program_list = string.find(route_comment, "MIDI Program (%d[-%d,]*)", nextchar) - if MIDI_Program_start then - local fieldstart = 1 - program_list = program_list .. ',' - repeat - local r,_,start,stop = string.find(program_list, "^(%d+)-(%d+)", fieldstart) - if r and patch >= tonumber(start) and patch <= tonumber(stop) then - route_active = true - end - r,_,program = string.find(program_list, "^(%d+)", fieldstart) - if r and patch == tonumber(program) then - route_active = true - end - local nexti = string.find(program_list, ",", fieldstart) - fieldstart = nexti + 1 - until fieldstart > string.len(program_list) - -- all tracks with a "MIDI Program" line present are set either active or inactive, depending on if they matched - -- all other tracks (no "MIDI Progam" line) are not affected (they never get to this point) - route:set_active(route_active, nil); - -- if this route is active, parse any CC functions in the route's comment block - if route_active then - local nextchar = 1 - local local_CC_functions = { } - -- we wrap the code inside "return function (route, value)" and "end" - -- so that when we pcall it with no argument it returns a function that takes two arguments - while true do - MIDI_CC_start, MIDI_CC_end, CC_num, CC_program = string.find(route_comment, "MIDI CC (%d+): ([^\n]+)", nextchar) - if MIDI_CC_start == nil then break end - if (local_CC_functions[CC_num] == nil) then - local_CC_functions[CC_num] = "return function(route, value)\n" - end - local_CC_functions[CC_num] = local_CC_functions[CC_num] .. CC_program .. "\n" - nextchar = MIDI_CC_end + 1 - end - -- done parsing (or at least gathering all of the lines together) - -- now compile the CC functions and insert them in the global table - for key, val in pairs(local_CC_functions) do - val = val .. "\nend" - local generator, err = load(val) - if generator then - local ok, func = pcall(generator) - if ok then - if not CC_functions[tonumber(key)] then - CC_functions[tonumber(key)] = { } - end - table.insert(CC_functions[tonumber(key)], { route, func }) - else - print("Execution error:", func) - end - else - print("Compilation error:", err) - end - end - end - end - end + program = d[2] + program_change() end end end From 3a61a355eeac2a0013c3f89ff7b28e0840879a90 Mon Sep 17 00:00:00 2001 From: Brent Baccala Date: Wed, 12 Nov 2025 19:07:32 -0500 Subject: [PATCH 3/6] MIDI PC/CC script: allow roll_transport() to start playback at a marker --- share/scripts/midi_pc_and_cc.lua | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/share/scripts/midi_pc_and_cc.lua b/share/scripts/midi_pc_and_cc.lua index 5976092685..3e72427b00 100644 --- a/share/scripts/midi_pc_and_cc.lua +++ b/share/scripts/midi_pc_and_cc.lua @@ -35,6 +35,9 @@ MIDI CC 93: end There's a convenience function to roll transport at a start time in seconds: MIDI CC 93: roll_transport(value, 0) +You can also roll transport at a marker: +MIDI CC 93: roll_transport(value, "01") + Another convenience function to activate or deactivate a plugin: MIDI CC 91: activate_processor_by_name(route, value, "Gigaverb") @@ -97,17 +100,34 @@ end -- -- a convenience function to start or stop the transport -- value is 0 to stop transport and not 0 to start transport --- location is a value in seconds +-- location is either a value in seconds or a string matching a marker + +function string.starts(String,Start) + return string.sub(String,1,string.len(Start))==Start +end function roll_transport(value, location) if value==0 then Session:request_stop (false, false, ARDOUR.TransportRequestSource.TRS_UI) else -- default value of location is 0 (start of session) - if not location then location = 0 end - start_sample = Session:nominal_sample_rate() * location; - Session:request_locate(location, false, ARDOUR.LocateTransportDisposition.MustStop, ARDOUR.TransportRequestSource.TRS_UI) - Session:request_roll (ARDOUR.TransportRequestSource.TRS_UI) + if not location then + start_sample = 0 + elseif type(location) == "string" then + for loc in Session:locations():list():iter() do + if loc:is_mark() then + if string.starts(loc:name(), location) then + start_sample = loc:start():samples(); + end + end + end + else + start_sample = Session:nominal_sample_rate() * location; + end + if start_sample then + Session:request_locate(start_sample, false, ARDOUR.LocateTransportDisposition.MustStop, ARDOUR.TransportRequestSource.TRS_UI) + Session:request_roll (ARDOUR.TransportRequestSource.TRS_UI) + end end end From d8f01e1eb8e5614b477d49a19fec0375f5b7259e Mon Sep 17 00:00:00 2001 From: Brent Baccala Date: Thu, 13 Nov 2025 12:55:57 -0500 Subject: [PATCH 4/6] MIDI PC/CC script: missed a 'local' declaration --- share/scripts/midi_pc_and_cc.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/share/scripts/midi_pc_and_cc.lua b/share/scripts/midi_pc_and_cc.lua index 3e72427b00..64d4e9fa2c 100644 --- a/share/scripts/midi_pc_and_cc.lua +++ b/share/scripts/midi_pc_and_cc.lua @@ -111,6 +111,7 @@ function roll_transport(value, location) Session:request_stop (false, false, ARDOUR.TransportRequestSource.TRS_UI) else -- default value of location is 0 (start of session) + local start_sample if not location then start_sample = 0 elseif type(location) == "string" then From b2a980b0321deafe380662e6a8eb55bd4ed6af88 Mon Sep 17 00:00:00 2001 From: Brent Baccala Date: Thu, 13 Nov 2025 22:57:24 -0500 Subject: [PATCH 5/6] MIDI PC/CC: add "MIDI Program" function lines to run Lua functions when program change occurs --- share/scripts/midi_pc_and_cc.lua | 54 +++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/share/scripts/midi_pc_and_cc.lua b/share/scripts/midi_pc_and_cc.lua index 64d4e9fa2c..c76e059713 100644 --- a/share/scripts/midi_pc_and_cc.lua +++ b/share/scripts/midi_pc_and_cc.lua @@ -176,7 +176,25 @@ function program_change() local route_comment = route:comment() local route_active = false local local_CC_functions = { } + local program_functions = { } -- to store functions to execute after track activation + -- Look for "MIDI Bank # Program #: code" statements with function calls + while true do + local MIDI_Program_start, MIDI_Program_end, bank_list, program_list, code + MIDI_Program_start,MIDI_Program_end,bank_list,program_list,code = string.find(route_comment, "MIDI Bank (%d[-%d,]*) Program (%d[-%d,]*): ([^\n]+)", nextchar) + if bank_list then + local matches = match_number_range(program, program_list) and match_number_range(bank, bank_list) + route_active = route_active or matches + MIDI_Program_seen = true + if matches and code then + table.insert(program_functions, code) + end + nextchar = MIDI_Program_end + 1 + else + break + end + end -- Look for "MIDI Bank # Program #" statements that match both the program number and the bank number + nextchar = 1 while true do local MIDI_Program_start, MIDI_Program_end, bank_list, program_list MIDI_Program_start,MIDI_Program_end,bank_list,program_list = string.find(route_comment, "MIDI Bank (%d[-%d,]*) Program (%d[-%d,]*)", nextchar) @@ -188,6 +206,23 @@ function program_change() break end end + -- Look for "MIDI Program #: code" statements with function calls + nextchar = 1 + while true do + local MIDI_Program_start, MIDI_Program_end, program_list, code + MIDI_Program_start,MIDI_Program_end,program_list,code = string.find(route_comment, "MIDI Program (%d[-%d,]*): ([^\n]+)", nextchar) + if program_list then + local matches = match_number_range(program, program_list) + route_active = route_active or matches + MIDI_Program_seen = true + if matches and code then + table.insert(program_functions, code) + end + nextchar = MIDI_Program_end + 1 + else + break + end + end -- Look for "MIDI Program #" statements that match just the program number nextchar = 1 while true do @@ -206,8 +241,25 @@ function program_change() -- all tracks with a "MIDI Program" line present are set either active or inactive, depending on if they matched -- all other tracks (no "MIDI Progam" line) are not affected (they never get to this point) route:set_active(route_active, nil); - -- if this route is active, parse any CC functions in the route's comment block + -- if this route is active, execute any program change functions and parse any CC functions in the route's comment block if route_active then + -- execute program change functions after track activation + for _, code in ipairs(program_functions) do + local func, err = load("return function(route) " .. code .. " end") + if func then + local ok, generated_func = pcall(func) + if ok then + local success, exec_err = pcall(generated_func, route) + if not success then + print("Program function execution error:", exec_err) + end + else + print("Program function generation error:", generated_func) + end + else + print("Program function compilation error:", err) + end + end local nextchar = 1 local local_CC_functions = { } -- we wrap the code inside "return function (route, value)" and "end" From 4717b68b3d883e3e0ee6f467811d0f3044badfe0 Mon Sep 17 00:00:00 2001 From: Brent Baccala Date: Thu, 13 Nov 2025 23:09:04 -0500 Subject: [PATCH 6/6] MIDI PC/CC script: allow CC functions to be restricted to specific programs or banks --- share/scripts/midi_pc_and_cc.lua | 68 ++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/share/scripts/midi_pc_and_cc.lua b/share/scripts/midi_pc_and_cc.lua index c76e059713..5476470f58 100644 --- a/share/scripts/midi_pc_and_cc.lua +++ b/share/scripts/midi_pc_and_cc.lua @@ -264,27 +264,59 @@ function program_change() local local_CC_functions = { } -- we wrap the code inside "return function (route, value)" and "end" -- so that when we pcall it with no argument it returns a function that takes two arguments + -- First parse "MIDI Bank # Program # CC #: code" patterns with bank and program restrictions + while true do + MIDI_CC_start, MIDI_CC_end, bank_list, program_list, CC_num, CC_program = string.find(route_comment, "MIDI Bank (%d[-%d,]*) Program (%d[-%d,]*) CC (%d+): ([^\n]+)", nextchar) + if MIDI_CC_start == nil then break end + local key = CC_num .. "|" .. bank_list .. "|" .. program_list + if (local_CC_functions[key] == nil) then + local_CC_functions[key] = { code = "return function(route, value)\n", bank_list = bank_list, program_list = program_list, CC_num = CC_num } + end + local_CC_functions[key].code = local_CC_functions[key].code .. CC_program .. "\n" + nextchar = MIDI_CC_end + 1 + end + -- Then parse "MIDI Program # CC #: code" patterns with only program restrictions + nextchar = 1 + while true do + MIDI_CC_start, MIDI_CC_end, program_list, CC_num, CC_program = string.find(route_comment, "MIDI Program (%d[-%d,]*) CC (%d+): ([^\n]+)", nextchar) + if MIDI_CC_start == nil then break end + local key = CC_num .. "||" .. program_list + if (local_CC_functions[key] == nil) then + local_CC_functions[key] = { code = "return function(route, value)\n", bank_list = nil, program_list = program_list, CC_num = CC_num } + end + local_CC_functions[key].code = local_CC_functions[key].code .. CC_program .. "\n" + nextchar = MIDI_CC_end + 1 + end + -- Finally parse "MIDI CC #: code" patterns without restrictions + nextchar = 1 while true do MIDI_CC_start, MIDI_CC_end, CC_num, CC_program = string.find(route_comment, "MIDI CC (%d+): ([^\n]+)", nextchar) if MIDI_CC_start == nil then break end if (local_CC_functions[CC_num] == nil) then - local_CC_functions[CC_num] = "return function(route, value)\n" + local_CC_functions[CC_num] = { code = "return function(route, value)\n", bank_list = nil, program_list = nil, CC_num = CC_num } end - local_CC_functions[CC_num] = local_CC_functions[CC_num] .. CC_program .. "\n" + local_CC_functions[CC_num].code = local_CC_functions[CC_num].code .. CC_program .. "\n" nextchar = MIDI_CC_end + 1 end -- done parsing (or at least gathering all of the lines together) -- now compile the CC functions and insert them in the global table - for key, val in pairs(local_CC_functions) do - val = val .. "\nend" - local generator, err = load(val) + for key, entry in pairs(local_CC_functions) do + local code = entry.code .. "\nend" + local generator, err = load(code) if generator then local ok, func = pcall(generator) if ok then - if not CC_functions[tonumber(key)] then - CC_functions[tonumber(key)] = { } + local cc_num = tonumber(entry.CC_num) + if not CC_functions[cc_num] then + CC_functions[cc_num] = { } end - table.insert(CC_functions[tonumber(key)], { route, func }) + -- Store route, function, and program/bank restrictions + table.insert(CC_functions[cc_num], { + route = route, + func = func, + program_list = entry.program_list, + bank_list = entry.bank_list + }) else print("Execution error:", func) end @@ -327,10 +359,22 @@ function process_midi_messages() -- if any functions are registered for this CC, call them with their respective Route objects and the value of the controller local lst = CC_functions[num] if lst then - for _, tbl in ipairs(lst) do - local route = tbl[1] - local func = tbl[2] - func(route, val) + for _, entry in ipairs(lst) do + -- Check if program/bank restrictions match current program/bank + local program_match = true + local bank_match = true + + if entry.program_list then + program_match = match_number_range(program, entry.program_list) + end + if entry.bank_list then + bank_match = match_number_range(bank, entry.bank_list) + end + + -- Only execute if restrictions match (or no restrictions) + if program_match and bank_match then + entry.func(entry.route, val) + end end end end