initial version of "MIDI PC/CC" plugin script

This commit is contained in:
Brent Baccala 2025-06-30 20:45:13 -04:00
parent d9fda4ce7a
commit 970d3f0f56

View file

@ -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