Re-engineering Cantabile's Bindings Framework

For the last week or so, I’ve been focusing on the last major piece of technical work for the new bindings:

Some background: in the current bindings implementation most MIDI to MIDI bindings are executed on the audio thread. All other bindings are processed on the UI thread. This works well for the current capabilities, but I want to setup the groundwork for more advanced automation like “binding animations” (ie: progressively updating a binding over time) which should also be run on the audio thread.

While designing the new bindings I’ve simplified things by having everything run on the UI thread. Now it’s time to push some of that logic down to the audio thread.

A major piece of this work was to get all the binding mappers running in native code (ie: C++, not C#/.NET). Remember a binding consists of three main parts - a source binding point, a target binding point and a “mapper” which handles mapping of values between the different kinds of binding points. While only some binding points will run on the audio thread all mappers need to be able to work on the audio thread.

I’d already extracted all the mapper code from the old bindings into separate C# mapper objects, but now I’ve ported the logic of all those mappers to native C++ objects and written unit tests:

The next step is to figure out if a binding can run on the audio thread (not all can) and if so, wiring it up to run there.

If you’re wondering about the longer time to run the SwitchToCommand tests in the above screen shot, it’s because that mapper has some time-based functionality (auto-repeat invocation of the target command) so the test needs to pause and check it fires. That’s the only mapper that has this, and I’ve not figured out yet how that’s going to work on the audio engine side, so that’s another job for today.

8 Likes

Progress: basic MIDI to MIDI bindings now running on audio thread. Not finished, but a big step.

11 Likes

Done! The new binding system can now push down a binding to run on the audio thread if both the source and target binding points support it. At the moment, only the MIDI source and MIDI target bindings points support this, but the framework is now in-place to more easily support moving whole classes of other bindings to the audio thread too (but that’s for later).

In case you’re wondering, bindings that run on the audio thread have much more precise timing - in fact it’s sample accurate. eg: if you delay a binding by n milliseconds it will be delayed by exactly that amount. Bindings on the audio UI thread aren’t that precise, have a slight bit of latency and can also be subject to other UI stalls.

The only MIDI binding point not supported on the audio thread right now is a MIDI to user SysEx binding - the code that processes the sys-ex scripts is written in C# and can’t be called from the audio thread without risking audio processing stalls so they still run on the UI thread. This is the same as the currently binding system.

That’s the last majorly technical piece of work on the new bindings. There’s still a fair bit to go but pleased to have this one done.

11 Likes

I guess it should be the “UI thread”.
Gabriel

P.S. Sorry for being picky! :laughing:

1 Like

Indeed.

6 posts were split to a new topic: Batch editing bindings

Another task checked off. For this one I’ve made some small improvements too. In the old bindings system, you could set a binding’s routing mode as:

  • Continue
  • Suppress, or
  • Block and then Suppress.

That last option let you make sure a song or state load has completed before processing any more incoming MIDI bindings. The idea being that if you’re trying to send a song/state load followed by some MIDI commands to configure the song then you didn’t have to wait an arbitrary period of time before sending the events after the load - you could just send them all at once and the subsequent events would be queued and processed after the load finishes.

Since there’s only a couple of cases where this actually makes a difference I’ve changed things so if you create a binding to a non-delayed song/state load action and set the routing mode to suppress it will automatically block subsequent events until the load finishes… and removed that third routing mode option.

7 Likes

This deals with the settings that control if a song is modified when changes are the result of a binding invocation. Basically the “mark modified” logic checks if a binding is currently being dispatched and ignores change notifications if so when appropriate.

In the same area of code, I’ve also updated the way bindings are logged if Options → Diagnostics → Log Bindings are enabled. It now gives a more precise and cleaner description of the source and target binding point, the source and target values and an indicator if a binding was invoked due to a change, but the target wasn’t invoked for some reason.

4 Likes

Some small changes for this:

  • The old bindings used to have three modes (disabled, half and full). The new binding object now only has a simple enabled/disabled toggle and by default bindings run in half mode (ie: reverse binding is suppressed when the forward binding is invoked and vice versa).

  • The only place where full mode makes sense is for MIDI bindings, so this option is now available on the source MIDI binding point, and made more explicit:

With that I think the bindings themselves are complete and functionally include everything the old binding system could do. What remains is various things around the bindings, like upgrading old bindings, updating the network API’s, verification etc…

And testing, lots and lots of testing.

5 Likes

Just finished implementing the replacement network API for talking to bindings. This is a new API that is not backwards compatible with the old API, but is cleaner, more self-documenting and simpler.

Since I’m reluctant to remove the old network API I’ll also need to build a backwards compatible API that maps to the new binding system - that’ll let old client applications (including the current WebUI and the Stream Deck plugin) to continue to work without change. However…

To get that working I need a way to map/convert old bindings to the new system and so I’ll leave that until I’ve tackled the “Upgrade Old Bindings” task since that’ll need something similar.

How that’s going to work I have no idea yet and it’s a bit daunting - but something to think about over the next couple of days (I’m taking a few days off to prepare Christine and all her parts to be shipped off to be put back together).

11 Likes

Phew! I never want to be without the Stream Deck integration. It’s been a life-changer here!

1 Like

Can you share some documentation on the new API? Probably best to also prepare LivePrompter’s CantabileConnect capabilities for the “new world”. I guess with your plan to keep the old network API, things will still work, but maybe I can do things more easily with the new API…

2 Likes

Yep, I’ll definitely be updating the documentation for this. The main difference is that instead of all the binding properties being lumped into one json object, they’re separated into bindable object properties (this is typically the song/rack indicies for “song by name/index” type bindings) and binding point properties (eg: MIDI binding points have props for event, channel, controller etc…).

There’s also a new API that lets you retrieve a list of the property names and types for a particular binding point on a particular bindable object.

Wish List: Could you add method for opening song by Name. And also the possibility to select Song by name from the ‘Songs’ folder, not just the current Set List?
My song ‘notes’ are html pages and I’ve managed to host them in the Cantabile Web Server. I would like a way to select a Song from my web page and issue the command to have Cantabile either move to that Song in the current set list or, if not found, open the song from the Songs folder.
Thank you - David

This will definitely be simple to add once the new bindings framework is done. Might even include it in the first pass. Thanks for suggestion.

1 Like

Very jealous of Christine :heart_eyes: Drum breaks on the rear though eek.

1 Like

Phew, another big tick item.

I’ve been dreading this task ever since I started on this new bindings framework and while the final code is only about 1500 lines of code, it took a couple of weeks because I wanted to do everything I could to make sure it’s correct:

  • generating lists of bindings and mapping types in the old and new systems and checking everything maps over (and implementing a couple of binding points that I neglected).
  • various approaches for converting the bindings (mapping tables didn’t work, straight code was cleaner)
  • mapping binding point and mapper properties from the old to new binding objects.
  • additional code to also upgrade all the binding states.
  • creating songs with every possible source and target binding point and mapping type
  • testing that everything upgrades correctly - which it now seems to.

After all that, it seems there’s just one thing that’s supported in the old system and not in the new - the “Control Curve” state behaviour. This has been removed in the new system since those are now properties on the mapper object and not individually controllable via states. When upgrading bindings that use this the upgraded binding will be generated correctly but the option to explicitly control (or not) the curve via a state behaviour has been dropped. There’s also a very weird edge case to do with the control curve state behaviour and the exported states - but I’d be shocked if anyone actually uses that.

The other requirement for all this was that it could be re-used to map a backwards compatible network API to the new binding system so existing network clients will continue to work. That’s the next job.

Getting closer…

12 Likes

Web UI and StreamDeck plugin are working again without changes (due to net api compatibility endpoint).

Almost there… biggest job now is a bit of UI rework as I’m not happy with it embedded in the main window. I’m going to try moving all the binding properties to a popup window - kind of like when editing MIDI route settings.

8 Likes

Happy to see point 3 above!

Well done, Brad. It sounds like light is now streaming in from the end of this tunnel.

2 Likes

Much happier with the UI now:

  • moved the binding editor to a separate window rather than embedded at the bottom of the binding panel as it took up too much room there.
  • duplicated the enabled/bidi/schedule/test and comments fields from the main slot into the binding editor window. Now everything to do with a binding can be edited in one place.
  • changed from single click to double click to bring up the binding editor. It was a bit hard to mouse navigate in the binding list with single click (basically clicking anywhere showed the editor).
  • re-ordered the columns from “Source->Mapping->Target” to “Source->Target->Mapping”. Even though logically the first order makes more sense, you need to configure the target before the mapping so the new order works better in practice.
  • made the curve display a popup while editing the value as is was taking too much room when shown inline.
  • more prominent display of binding errors

10 Likes