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