Skip to content

New controller system

Be edited this page Jul 11, 2020 · 41 revisions

The current controller system is clunky to program with. Now that we are using QJSEngine (PR #2682) which supports ES7 as of Qt 5.12 in master, we can build a whole new controller mapping system that will be nicer to work with and implement features we have wanted for years. This wiki page explains plans for making this new controller mapping system.

C++ refactoring

Currently, the C++ classes that handle controllers and controller mappings are arranged in this inheritance hierarchy:

                           Controller
           MidiController              HidController BulkController
PortMidiController Hss1394Controller

These Controller classes couple hardware I/O with the implementation of the legacy mapping system. Scripting is handled by the ControllerEngine class, which is a private member of Controller. ControllerEngine both runs the JavaScript interpreter (QJSEngine in master, QScriptEngine in Mixxx <= 2.3) and provides the API for scripts to interact with Mixxx via the engine object in the JS environment.

In PR #2868 we have started working towards a new mapping system in which mapping is done entirely by JavaScript modules. Although this worked as a proof-of-concept hack in the legacy controller system, it became apparent that bringing the new system to its full potential requires major reorganization of the C++ code architecture.

The first step is to decouple the engine API in the script environment from the ControllerEngine class which manages the JS interpreter. This facilitates decoupling the JS handling code for the legacy and new controller systems so we can have a fresh start in the new system with only the code necessary for the new features. This has already been implemented in PR #2920.

The next step is to split mapping handling code from the Controller classes so the Controller classes will only handle hardware I/O. Mappings will be handled by the new ControllerMappingProcessor classes. The new class hierarchy will be:

                  ControllerMappingProcessor
ModularControllerMappingProcessor   LegacyControllerMappingProcessor
                                    LegacyMidiControllerMappingProcessor

                                   Controller
              MidiController  HidController  BulkController
PortMidiController Hss1394Controller

                      ScriptEngine
                ControllerScriptEngine
ModularControllerScriptEngine LegacyControllerScriptEngine

Each ControllerMappingProcessor will be given at least one Controller* pointer upon construction by ControllerManager. From the controller polling thread, ControllerManager will call a poll method on each ControllerMappingProcessor. ControllerMappingProcessor::poll will call the poll method of each Controller* that it is using, then process the polled data as appropriate. For the ModularControllerMappingProcessor, this will pass the polled data to a JS callback as a QByteArray (turned into a Uint8Array in JS) plus a timestamp. LegacyControllerMapping will implement the legacy XML+JS system. Currently MidiController implements handling the XML mappings. This will be moved to LegacyMidiControllerMappingProcessor.

Zulip discussion

Connecting controllers to JS environment

The Controller objects will be exposed to the JS environment via ControllerJSProxy wrappers like is currently done in master. By decoupling hardware I/O from handling the mapping, the new system allows multiple controllers to be mapped within one script. The script would be responsible for registering a callback function with each ControllerJSProxy to handle all incoming data from that controller. This will allow the scripts for different controllers to communicate with each other without requiring manipulating the state of Mixxx. For example, pressing a button on one controller could switch another controller to a different layer.

The JSON metadata file would specify unique identifying information for each controller so Mixxx could automatically load mappings for controllers. Like in Bitwig Studio, multiple identifiers can be used to match a controller. This can accommodate for MIDI port name differences between different OSes. It could also be used to match one mapping to multiple controllers, for example the Allen & Heath Xone K2 and K1 can share a mapping, and many Pioneer DDJ controllers share the same MIDI commands. The manufacturer and model strings would be shown in the controller preferences GUI.

controllers: {
  midi: [
     xoneK2: {
      midiPort: ["XONE:K2", "XONE:K1"]
      manufacturer: "Allen & Heath"
      model: "Xone K2 or K1"
     },
     launchpad: {
      midiPort: ["LAUNCHPAD"],
      manufacturer: "Novation"
      model: "Launchpad"
     }
  ]
}

The JSON object name for the controller would be used as a unique identifier to retrieve the ControllerJSProxy object in the script module:

export function init() {
  const xoneK2 = mixxx.getMidiController("xoneK2");
  xoneK2.registerInputCallback(...);
  const launchpad = mixxx.getMidiController("launchpad");
  launchpad.registerInputCallback(...);
}

The USB bulk endpoints for the screens on Native Instruments controllers could be exposed as JS objects. The C++ code could share this same object in a QML scripting environment to render for the screens. By setting properties on this JS object, the HID controller mapping script could communicate with the QML code. For example, in the JSON metadata file:

// The numbers are just examples.
controllers: {
  hid: [
    kontrols4mk3: {
      vendor_id: 0x17cc,
      product_id: 0x1310,
      usage_page: 0xff01,
      usage: 0x1,
      interface_number: 0x4
    }
  ],
  bulk: [
    kontrols4mk3screen: {
      vendor_id: 0x17cc,
      product_id: 0x1310
      ep_out: 0x3
    }
  ]

In the JS module:

export function init() {
   controller = mixxx.getHidController("kontrols4mk3");
   controller.registerInputHandler(...);
   screen = mixxx.getBulkController("kontrols4mk3screen");
   screen.shiftButtonPressed = true;
   // The QML script could then read the shiftButtonPressed property.
}

Zulip discussion

New ControlObject JS API

The old engine.getValue/engine.setValue/engine.getParameter/engine.setParameter API will be replaced by a new C++ class with a constructor inserted into the JS environment as mixxx.Control:

const play = new mixxx.Control('[Channel1]', 'play');
play.setValue(1);
play.toggle();
console.log(play.getValue()); // 0
// connect callback
play.setCallback(control => console.log(control.getValue()));
// disconnect callback
play.setCallback(null);
// manually invoke callback
play.trigger();
// Change group or key -- callback is automatically reconnected to new CO and triggered.
// A second boolean parameter can be added to avoid automatically triggering the callback,
// but most of the time automatically triggering the callback is helpful.
play.setGroup('[Channel3]');
play.setKey('cue_default');

The callbacks would be passed the mixxx.Control as their first parameter. If access to other context (for example, a surrounding Component from the Components JS library) is required inside the callback, use an arrow function or Function.prototype.bind to bind this for the callback.

Zulip discussion

New jog wheel scratching API

https://mixxx.zulipchat.com/#narrow/stream/113295-controller-mapping/topic/new.20jog.20wheel.20API

New JSON metadata format

https://mixxx.zulipchat.com/#narrow/stream/113295-controller-mapping/topic/new.20jog.20wheel.20API

Show documentation in the application

to be discussed

Hotplug

https://mixxx.zulipchat.com/#narrow/stream/109171-development/topic/Hotplugging

Persistent state & preferences for mappings

https://mixxx.zulipchat.com/#narrow/stream/109171-development/topic/controller.20preferences.20design/

New Components JS library

https://mixxx.zulipchat.com/#narrow/stream/113295-controller-mapping/topic/ComponentsJS.20intercomponent.20communication

Clone this wiki locally