ardour icon indicating copy to clipboard operation
ardour copied to clipboard

"MIDI PC/CC" plugin script

Open BrentBaccala opened this issue 6 months ago • 24 comments

Initial version of a Lua script that handles MIDI Program Change (PC) and Continuous Controller (CC) messages.

Put this plugin on a track that can hear the PC and CC messages. It will activate and deactivate tracks when PC messages are seen, and run arbitrary Lua code when CC messages are seen.

Configuration is done using statements in each track's comment block.

See the plugin description for more documentation.

BrentBaccala avatar Jul 01 '25 00:07 BrentBaccala

So, this plugin isn't ready to be committed yet, but I'd like to hear any discussion about it in its present form.

The idea is different from automation, which records onto a track. The idea here is to handle messages coming in over MIDI during live performance.

The simplest way to use it is to have Program Change messages activate and deactivate tracks. Just put a statement like "MIDI Program 0" in a track's comment block.

Some open issues that I know about:

  • it needs to handle Bank Change messages, too
  • people might want tracks muted, not deactivated
  • it all runs on a realtime thread, so we get lots of messages like "programming error: Session RT event queued from thread without a UI - cleanup in RT thread"
  • it can't trigger action events

We need some way to trigger a Lua function from a realtime thread, and queue its execution on a normal thread.

We need some way to trigger action events (buttons) from a Lua script.

Or maybe there's just a better way of doing it, which is why I'm soliciting feedback now, even though it's not done.

BrentBaccala avatar Jul 01 '25 00:07 BrentBaccala

So, this plugin isn't ready to be committed yet, but I'd like to hear any discussion about it in its present form.

Perhaps mark it as "Draft" then until you it's ready to be merged?

Note that load_preset may not be realtime-safe. It's expected to be called from the UI thread (plugin GUI or Ardour GUI). It should be fine for simple plugins where presets just change port values.

Remote control like this should usually happen asynchronously in a dedicated event-loop (see lib/surfaces/) and not from realtime context. I am not conformable to merge this as-is, since others may think it's acceptable to perform this from realtime thread.

x42 avatar Jul 03 '25 18:07 x42

@x42, you just taught me about github's "draft" feature.

Any suggestions for how to defer processing off the DSP thread? Maybe somehow trigger a Lua function to be called from a normal thread, and avoid all of the "programming error: Session RT event queued from thread without a UI - cleanup in RT thread" messages?

BrentBaccala avatar Jul 04 '25 14:07 BrentBaccala

I guess the problem here is that we don't have a function to do what we want, which is to defer some action off the realtime thread.

How about adding "Session::dcall(func)" (deferred call), which puts the Lua function "func" onto a queue to be run in a standard thread?

Is there a better namespace to put it in?

@x42, can you suggest where the queue might get emptied and run?

with some guidance like that, i could take a crack at writing it

BrentBaccala avatar Jul 06 '25 21:07 BrentBaccala

We have extensively used mechanisms for invoking code in another thread. But this all exists at the C++ level, which has a global memory model. Crossing between threads in Lua is massively more complex, because the interpreters in different threads are not the same.

pauldavisthefirst avatar Jul 06 '25 22:07 pauldavisthefirst

@pauldavisthefirst, is there any way we can create two threads with the same Lua interpreter, one real time and one not?

I know you said they're all in different threads, but is there some way to create two threads with that special property?

BrentBaccala avatar Jul 06 '25 22:07 BrentBaccala

I don't there's any model for starting threads from Lua in ardour, and in general, you likely need the non-RT thread to be "the right one", rather than just some thread you create.

pauldavisthefirst avatar Jul 06 '25 22:07 pauldavisthefirst

Any suggestions for how to defer processing off the DSP thread?

What you really want to do is not possible with a Lua script and requires C++ code.

As opposed to closed source DAWs we do not need to rely on Lua for control-surfaces, and async code with realtime constraints is really nicer and easier in C++, particularly since Lua interpreter is single threaded.

That being said.. for a quick/dirty hack what you're doing is OKish (just don't rely on it live on stage :)

x42 avatar Jul 06 '25 22:07 x42

The consensus seems to be that a plugin like this needs to be written in C++. OK, we can do that.

I would still like to provide Lua scripting ability, for flexibility.

We're going to get MIDI messages on a realtime thread. How (in C++) can a realtime thread trigger Lua code in a standard thread? Can you guys provide suggestions, or a pointer to some existing code?

BrentBaccala avatar Jul 07 '25 16:07 BrentBaccala

@x42 is on vacation this week, and he's by far the best person to answer this.

pauldavisthefirst avatar Jul 07 '25 17:07 pauldavisthefirst

As opposed to closed source DAWs we do not need to rely on Lua for control-surfaces...

So, how do people control it, especially on stage? Stream Decks? Am I being old school for expecting it to respond to Program Change messages?

Paul says you're on vacation this week. That's good to know; I won't expect an immediate response.

BrentBaccala avatar Jul 07 '25 20:07 BrentBaccala

That being said.. for a quick/dirty hack what you're doing is OKish (just don't rely on it live on stage :)

What I saw yesterday was a track was active (and I could hear it), but it was grayed out in the GUI. I guess that's the kind of flaky behavior I can expect.

I'm thinking I want the RT thread to trigger something on the GUI's main event loop, just like a mouse click on a script button.

BrentBaccala avatar Jul 09 '25 15:07 BrentBaccala

The documentation for g_timeout_add() says "It is safe to call this function from any thread".

I'm thinking that the RT thread should call g_timeout_add() with an interval of zero, and the function passed in should always return FALSE, so it will only be called once.

That will move the processing from the RT thread to the GUI thread, and then it should be safe to invoke Lua, right?

BrentBaccala avatar Jul 11 '25 16:07 BrentBaccala

We do not/have not used the g_timeout_add() mechanism for x-thread invocation (as mentioned, we have our own mechanism that is used all over the place, but not exposed as a Lua API. It might work, it might work but hide issues, it might not work ... unsure.

pauldavisthefirst avatar Jul 11 '25 16:07 pauldavisthefirst

@pauldavisthefirst, what is the mechanism we use in lieu of g_timeout_add()?

BrentBaccala avatar Jul 13 '25 23:07 BrentBaccala

We rarely do it directly, but any thread can connect a handler to a PBD::Signal; then when the signal is emitted, the thread in which that happens has a lock-free FIFO queue that connects it to the thread in which the handler is supposed to run, and queues the handler there. That is tied into the glib event loop via an additional file descriptor that wakes up the GUI thread (in the case we're talking about), and then the handler is invoked in that thread.

See libs/pbd/crossthread*.cc for details, which implement the CrossThreadChannel object.

pauldavisthefirst avatar Jul 14 '25 02:07 pauldavisthefirst

Note that this mechanism requires a thread that wants code to be called in its own context must register with the thread that initiates the event that would cause the code to be run. This allows us to do this is a lock-free manner, ignoring kernel side locking on the underlying communication mechanism.

This is fairly important when its an RT thread that needs to start the ball rolling - it cannot take locks as part of that process.

pauldavisthefirst avatar Jul 14 '25 02:07 pauldavisthefirst

For the case at hand, I'd probably use Mixer Scenes. and then recall a given scene using custom MIDI bindings for (Preferences > Control Surfaces) Generic MIDI surface.

e.g. ~/.config/ardour8/midi_maps/pgm_scene.map

<?xml version="1.0" encoding="UTF-8"?>
<ArdourMIDIBindings version="1.0.0" name="MIDI Patch to Mixer Scene">
  <Binding channel="1" pgm="1" action="Mixer/recall-mixer-scene-1"/>
  <Binding channel="1" pgm="2" action="Mixer/recall-mixer-scene-2"/>
  <Binding channel="1" pgm="3" action="Mixer/recall-mixer-scene-3"/>
</ArdourMIDIBindings>

x42 avatar Jul 14 '25 11:07 x42

@x42, the MIDI Bindings seem close to what I want (I didn't know about that feature). A Program Change or CC message can invoke an arbitrary GUI action, which could be a shortcut to a Lua action script, but there's no way to pass in the program number or CC value. We can also trigger functions. Perhaps allow user-defined Lua functions here that receive the program number or CC value? If the existing functions can do things like "roll_transport", they're probably on a thread that can do things like adjust the active state of tracks.

What do you think?

BrentBaccala avatar Jul 15 '25 02:07 BrentBaccala

but there's no way to pass in the program number or CC value

Correct. Those are Actions which operate on the selection, and hence cannot take any arguments.

x42 avatar Jul 15 '25 13:07 x42

I've been working to resolve these issues by creating a new type of XML MIDI binding that runs Lua code.

My current implementation uses a new PBD::Signal with a string argument that is Lua code. It's called BasicUI::AccessLuaScript and it gets connected and fired like the existing BasicUI::AccessAction:

BasicUI::AccessLuaScript.connect (*this, invalidator (*this), std::bind (&Editor::access_lua_script, this, _1, _2), gui_context());

Editor::access_lua_script() starts like this:

Editor::access_lua_script (const std::string& script, int midi_value)
{
        if (!_session) return;

        ENSURE_GUI_THREAD (*this, &Editor::access_lua_script, script, midi_value)

        LuaInstance* lua_instance = LuaInstance::instance();
        if (!lua_instance) {
                error << "LuaInstance not available" << endmsg;
                return;
        }

Does this seem reasonable?

BrentBaccala avatar Sep 10 '25 14:09 BrentBaccala

Personally I would pass a const char const * and a size_t so that you can pass in multiple bytes of MIDI data (i.e. the whole message if appropriate)

pauldavisthefirst avatar Sep 10 '25 15:09 pauldavisthefirst

@pauldavisthefirst I haven't decided exactly what the API should look like, which is why I haven't pushed an actual commit. Passing the whole MIDI message makes sense. How then should it be presented to Lua? I'm thinking about using a global variable midiin, mimicking the DSP code, but only passing a single message and not the entire buffer. Judging by the comment on that code, Robin wasn't crazy about putting midiin in Lua's global namespace, but after ten years I guess we're somewhat stuck with it, so probably do the MIDI binding code the same way.

BrentBaccala avatar Sep 10 '25 21:09 BrentBaccala

I'm slowly working towards an RT thread safe version of this script.

I'm now thinking that a plugin really is the way to go, instead of an XML MIDI binding. The nice thing about a plugin is that it will respond to recorded MIDI messages as well as messages from Jack inputs.

So, for example, my latest push, which enhances the roll_transport() function to take a string to identify a marker, can be used to skip a region on playback, something previously requested.

You create a clip with a single CC message (for some reason, it also has to have a single note as well), drop it on a track, and configure the MIDI PC/CC plugin like this:

MIDI CC 100: roll_transport(value, "skip to")`

Then when we hit that point during playback, the playhead jumps to the "skip to" marker.

BrentBaccala avatar Nov 13 '25 16:11 BrentBaccala