mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-06 06:44:57 +01:00
Merge 4717b68b3d into c08531f96e
This commit is contained in:
commit
b506496012
1 changed files with 395 additions and 0 deletions
395
share/scripts/midi_pc_and_cc.lua
Normal file
395
share/scripts/midi_pc_and_cc.lua
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
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"
|
||||
|
||||
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.
|
||||
|
||||
In such 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)
|
||||
|
||||
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")
|
||||
|
||||
A convenience function to select a preset:
|
||||
MIDI CC 93: load_preset(route, "plugin", "preset")
|
||||
|
||||
Remember that a "MIDI Program #" (or "MIDI Bank # 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 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)
|
||||
local start_sample
|
||||
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
|
||||
|
||||
-- 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 = { }
|
||||
|
||||
-- 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 = { }
|
||||
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)
|
||||
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 #: 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
|
||||
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, 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"
|
||||
-- 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] = { code = "return function(route, value)\n", bank_list = nil, program_list = nil, CC_num = CC_num }
|
||||
end
|
||||
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, 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
|
||||
local cc_num = tonumber(entry.CC_num)
|
||||
if not CC_functions[cc_num] then
|
||||
CC_functions[cc_num] = { }
|
||||
end
|
||||
-- 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
|
||||
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 ]
|
||||
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
|
||||
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[num]
|
||||
if lst then
|
||||
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
|
||||
if (event_type == 12) then
|
||||
-- Program Change message
|
||||
program = d[2]
|
||||
program_change()
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue