-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add support for loading controller scripts as JS modules #2868
Conversation
Here is the proof-of-concept JS file I am testing with:
|
I have stumbled upon an ugly limitation of using the JS
Thoughts on how to proceed? |
e66ae18
to
ce22c15
Compare
Is there the possibility of a hybrid approach? |
The Qt bug has been marked as priority 2, important. So I hope we can expect it to be solved soonish. I am not sure if that will not help with legacy Qt versions. I have asked in the bug report if a fix would be backported to Qt 5.12, since that is still a supported LTS release. @Swiftb0y your proposal could work, however, it still requires the mapping developer to make some changes every time they upload to the forum. I am concerned any step like that which requires manual intervention will get skipped and forum users will get confused. Perhaps we could recommend starting development using the |
I didn't take into account when somebody develops a mapping in the repository but then decides to publish it via the forum. We could provide an awk or python script to run on a directory to transform all imports automatically. That would still involve an extra step but make the "export" process easier. Obviously the other way around works as well. |
I think we should tell mapping developers to use the QRC paths until https://bugreports.qt.io/browse/QTBUG-85025 is fixed upstream and widely available in Linux distros. Most mapping developers will not need to edit the libraries shipped with Mixxx, so this requires no extra work and is therefore the least error prone. While we develop the libraries, we can use the relative paths to import in the controller script we are working with to develop the library. When the fix is made upstream, it will be easy to change all the import statements in the Git repository to relative paths. |
Unfortunately using QDir::setPrefixPaths with JS module imports is targeted for Qt 6.2, so we won't be able to use it anytime soon. So I added the MidiDispatcher module to the QRC system and it can be loaded with The next step will be rewriting Components as JS modules using this new system. But that's for another PR. |
} | ||
} | ||
receiveData(data, timestamp) { | ||
const key = JSON.stringify([data[0], data[1]]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm this assumes that MIDI data is at least 2 bytes, so it can't be used to map things like FA
(sent by my controller when pressing the play button of the built-in drum computer).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class doesn't need to handle every single possibility for esoteric MIDI messages. The controller module's receiveData
function could check for exceptional messages and return before calling MidiDispatcher.receiveData
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to this list, all messages are 3 bytes, except:
- Messages starting with C or D are 2 bytes long where byte 2 is the value
- Messages starting with F are 1 byte long or "special messages" like SysEx
I think we only need 2 cases:
- First midi nibble is C, D or F: only use first byte as key
- Use first 2 bytes in all other cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added support for 0xC (program change) and 0xD (aftertouch) messages, but I don't think it would be helpful to add logic for handling 0xF messages which are highly variable. I suggest implementing controller-specific logic for handling those messages in the controller module's receiveData
function and returning before calling MidiDispatcher.receiveData
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW there is a single controller mapping in Mixxx (Akai LPD8) that uses 0xC (program change) MIDI messages and zero that use 0xD (aftertouch).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a couple of nitpicks/questions about the mididispatcher, but as far as I can tell with my limited C++ knowledge, the C++ part looks good.
@@ -0,0 +1,26 @@ | |||
export class MidiDispatcher { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you document the class and it’s members using JSdoc comments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please review the documentation: 98a3d82
@Holzhaus do you have any comments about the C++ parts? |
@@ -57,7 +57,7 @@ void PresetInfoEnumerator::loadSupportedPresets() { | |||
m_bulkPresets.clear(); | |||
|
|||
for (const QString& dirPath : m_controllerDirPaths) { | |||
QDirIterator it(dirPath); | |||
QDirIterator it(dirPath, QDirIterator::Subdirectories); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can is unrelated, right? I think enabling support for controller scripts in subdirectories is a different matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was very simple to implement and I don't think there's any reason to remove it now.
Just a small comment from a conceptional standpoint, you are probably already aware of: |
If we implement support for interacting with multiple controllers through one script, I think the required JS changes would be minimal, if any. But that would require rewriting the whole controller system so I'm in no hurry to do it. |
What’s the plan with the other libraries like common-controller-scripts.js? Since the old way we implemented modules by polluting the global namespace is incompatible with ES6 modules (which run in strict mode and thus disallow modification of the global namespace), I think the only possible way would be to rewrite them as well. That shouldn’t be too difficult though as we’re not trying to make breaking changes feature-wise or otherwise improve their design much. While that’s obviously out-of-scope for this PR, it’s definitely a requirement for creating ES6 mappings IMO. |
I think we should redesign common controller scripts completely. I feel like a some of the functions are unneeded. I also think we should remove stuff like |
No changes. There is no need to rewrite them as modules. They are imported into the global namespace just as they always have been.
Yes, there is some junk in common-controller-scripts.js. But we can't remove it without breaking backwards compatibility. |
Ideally I'd like to move all of |
Looking at the contents of common-controller-script.js, yeah I think we should deprecate the whole file. I think the only code in that worth recycling are the regexes, |
I think this would require some changes to how script files are located using the legacy |
But won't that be an additional context switch? C++ -> JS (module.handleInput) -> C++ -> JS callback? Probably doesn't make much of a difference, but just wanted to make sure I understand the proposal. |
The idea was to eliminate that context switch by having the logic for registering and calling callbacks for specific midi messages purely in C++. This logic would run before module.handleInput so there is only a context switch as part of executing the callback (or module.handleInput as a fallback). So simply C++ -> callback / module.handleInput. |
I want to keep the C++ API to the bare minimum required for controller scripts to interact with Mixxx. IMO everything else should be in JS to leave us maximum flexibility. We could implement a class with the same API as MidiDispatcher in C++, but then we would be stuck with it in the JS environment indefinitely. We have lots of junk that has built up over time in the legacy scripting environment. With JS modules, scripts only need to load what they use. We are free to implement alternate ways of approaching the same task in different modules. Perhaps we end up not liking this MidiDispatcher API in 2 years. No problem, write a new JS module and leave the old scripts using the old module. ColorMapper is an exception because it was implemented for 2.3 before we had
I don't think this is a realistic idea. We need the full flexibility of JavaScript to handle jog wheels because there is no standardization of the signals used for jog wheels. My idea is to expose a C++ class to the JS environment which passes the data to the audio thread via lock free queue. We still need JS to interpret the controller's signals into data Mixxx can process. Maybe we could hack something together in C++ that would work for many MIDI use cases, but then we'd need a separate API for HID. As @Swiftb0y mentioned, one of the design goals is to make the APIs as similar as practical regardless of the wire protocol used by the controller.
I grow increasingly annoyed with GitHub on long PRs. I hope we can move to Gitea eventually. Maybe it has a nicer UX. |
Thank you for reviewing the JSDoc @Swiftb0y. Could you take another look at the new updates? |
This is not reasonable. With the proposed C++ dispatcher we are able to release the JS Engine from a lot of rubber band work.
There is no difference from the user perspective of compiling JS modul into the mixxx binary or a c++ object.
That is not entirely true, if you look into existing mapping. While many are individual switching states, all have in common a Midi message that give the position (wheelTurn) This is the only one that is updated with a high rate and real-time sensitive and the one that we directly need to pass into the position scratch controller. The rest will be remain in the JS domain to control the position scratch controller or the other jog wheel related COs.
Unless you are planning the re-implement the position scratch controller for every mapping, we will have separate APIs for separate protocols anyway. The mapping writer will barely notice the difference if it is an integrated JS module or a C++ object.
This is still there, If you don't use the build in dispatcher. This is no different if the map of callbacks live in the C++ or JS domain. |
I am still in doubt this proposed optimization to reduce the use of the JS runtime is necessary with JIT compilation and efficient JS (no string usage in JS). Maybe we can revisit this when implementing the new jog wheel API. For now I would like to merge this and move on. I already have #2920 based on this and I'd like to not have to keep rebasing that. Originally I did not intend the discussion on this PR to focus on the MidiDispatcher module anyway. I included it in this PR as a proof of concept to test how a controller module would load another module as a library. |
I think we either need a conclusion here ore remove the dispatcher part.
Normally every CPU cycle counts, think to the Raspberry use case and to battery drain. We normally optimize everywhere even in not real time contexts. Here we are in a hot path and real time relevant. |
How about we merge this now with the understanding that we will try your approach later before releasing 2.4? The MidiDispatcher JS module is already useful for developing proof-of-concept JS code with this. This JS module feature is still just a proof of concept with this PR anyway. In #2920 I have already removed the hacks which made JS modules usable with the legacy XML files. |
unfortunately, the C++ Mididispatcher API would be slightly different than the JS version so we have to keep that in mind when developing PoCs using it. Otherwise, I'd be ok with merging the PR as is and swapping the MidiDispatcher out later. |
Let's get it right here. |
Okay, I have removed the MidiDispatcher JS module. If anyone wants to use it, here it is for future reference. |
Ready for merge? |
LGTM, Thank you. @Holzhaus anything missing? AppVeyor failure is a timeout. |
Hey, we have the rule not to merge own PRs. In addition there was a Pending review from @Holzhaus. |
I don't think @Holzhaus had any pending concerns? |
Not really. I'm still not super-convinced that it's a good idea to allow loading JS files from subdirectories without further planning the layout of the controller script directory, but we can easily disable that before the 2.4 release. |
Considering we'll be making a new metadata format after all, I agree. #2936 |
This fixes two bugs that were caused by mixxxdj#2868: 1. Mixxx didn't check if the JS module path was really a file. If you pass in a directory, QtJSEngine::importModule makes Mixxx crash (at least on Qt 5.15.0) 2. If a mapping does not have JS module associated with it, then the moduleFileInfo always pointed to the controller directory and hence triggered bug 1. This fixes both of these issues and adds some debug assertions to make sure it stays like that.
This fixes two bugs that were caused by mixxxdj#2868: 1. Mixxx didn't check if the JS module path was really a file. If you pass in a directory, QtJSEngine::importModule makes Mixxx crash (at least on Qt 5.15.0) 2. If a mapping does not have JS module associated with it, then the moduleFileInfo always pointed to the controller directory and hence triggered bug 1. This fixes both of these issues and adds some debug assertions to make sure it stays like that.
This bypasses the old code path used for the
scriptfiles
/file
XML elements. Instead, the root JS module file is specified by a new XML element:MyController.mjs can use the standard JS
import
keyword to import other modules.The module must export 3 functions:
All incoming data is passed to
receiveData
. Functions defined in the JS module cannot be used in the legacy XML system and this will not be supported.The standard JS "import" keyword only works if the script is loaded with QJSEngine::importModule. JS modules cannot be used with the legacy system of reading JS files into a QString and calling QJSEngine::evaluate.
This makes Qt 5.12 a strict requirement to build Mixxx.
TODO:
cleanup redundancies with legacy codeThe legacy code is a mess. I will not touch it.