Running plugins in converter mode.

• Jan 10, 2020 - 14:28

I'm trying to get musescore to batch-transpose some sheet music (my use case is writing lead sheets - I'd like to write them in concert pitch, then transpose for Bb/Eb instruments in a script), but I'm having trouble getting the "transposer_0" script (found here: https://musescore.org/en/project/transposer) to run in converter mode, or at the command line in general.

Specifically, I'm copying the qml file to ~/MuseScore/Plugins, then running the following command:


mscore3 Concert_Pitch.mscx -p transposer_0.qml -o B_flat.mscx

I am sure the plugin is being run, as various logging output is printed, but the output .mscx file is identical to the input file.

The command line output that I get is as follows:

Jack appears to be installed on this system, so we'll use it.
QApplication: invalid style override passed, ignoring it.
qml: Hello Transposer
qml: Score title: undefined
qml: Original key: undefined
qml: Transposed key: undefined
Signal QQmlEngine::quit() emitted, but no receivers connected to handle it.
convert  to 

For ease of reference, the entire qml script is as follows:

  import QtQuick 2.0
  import MuseScore 3.0

  MuseScore {
        menuPath: "Plugins.Transposer"
        description: "Description goes here"
        version: "1.0"

        onRun: {
              console.log("Hello Transposer")

              console.log("Score title: " + curScore.title);

              if (!curScore)
                    Qt.quit();

              // transpose up a whole step for Bb instrument
              // note: to get the key right, go up 3 half-steps and down 1
              console.log("Original key: " + curScore.keysig);
              for (var i = 3; i-- > 0; ) {
                    cmd("transpose-up");
              }
              cmd("transpose-down");
              console.log("Transposed key: " + curScore.keysig);

              // insert instrument name on first note
              var cursor = curScore.newCursor();
              cursor.staffIdx = 0;
              cursor.voice = 0;
              cursor.rewind(Cursor.SCORE_START);

              var text = newElement(Element.INSTRUMENT_CHANGE);
              text.autoplace = false;
              text.placement = Placement.ABOVE;
              text.text = "Bb Instrument";
              text.fontSize = 18;
              text.sizeSpatiumDependent = false;
              cursor.add(text);
              // offsets must be done AFTER adding
              text.offsetX = -8;
              text.offsetY = -8;

              Qt.quit();
        }
  }

My assumption is that certain Qt elements are not initialised correctly when running from the command line, but I can't see why that would be a good design decision, so it seems like a bug. If anyone has any insight, or anything I could do that would fix the issue I'd appreciate the help.

Thanks!


Comments

So, on some experimentation, it seems that the error output was only for musescore 3.0.5. With the new beta (3.4.0), I get output that seems more reasonable:

/usr/lib/x86_64-linux-gnu/libjack.so.0
qml: Hello Transposer
qml: Score title: Flying Home (That Solo)
qml: Original key: -4
qml: Transposed key: -2
Signal QQmlEngine::quit() emitted, but no receivers connected to handle it.
convert ...
    to 
... success!

However, on inspection, Bb.mscx is no different to Concert.mscx. By that I mean running diff shows no difference between the two files.

EDIT:

Trying other output formats, exporting to PDF doesn't work either - it almost seems like the script is being run at the wrong time in the lifecycle of the command, so that it doesn't end up actually touching the score.

In reply to by harries.adam

Right! Solved (or at least, I know what's wrong with it, I think).

So: Musescore can run in one of two modes - GUI, or Non-GUI. When run in Non-GUI mode, the following logic is followed (essentially):

if (pluginMode) {
    loadScores(input_filename); 
    loadPlugin(...);
    // run plugins
}

if (converterMode) {
    convert(input_filename, output_filename);
}

The issue with this is that in plugin mode, a file is loaded, and then modified. In converter mode, another file is then loaded, converted, and written out. Unfortunately, this means that the file loaded and processed by the plugin is entirely disregarded and another fresh file is loaded and converted.

I'm going to try and fix this when I have some time, but that might take a while.

In reply to by harries.adam

No, I was incorrect - we do run plugins when converting, but not the one specified at the command line! We run a blank-name plugin (which will obviously do nothing).

More problematically, we also don't properly append the score when converting, so that would seem to be the first port of call for a fix...

In reply to by harries.adam

So I've managed to fix this, and have a successful build of musescore that will correctly run plugins when converting. I'll open a PR when I get some time, but in the meantime if anyone stumbles across this topic and needs a similar fix, then this patch will give you what you need (albeit, in a rather hacky way):

  diff --git a/mscore/musescore.cpp b/mscore/musescore.cpp
  index 4432b86c6..6175c0279 100644
  --- a/mscore/musescore.cpp
  +++ b/mscore/musescore.cpp
  @@ -495,7 +495,7 @@ void MuseScore::preferencesChanged(bool fromWorkspace)
        // change workspace
        if (!fromWorkspace && preferences.getString(PREF_APP_WORKSPACE) != WorkspacesManager::currentWorkspace()->name()) {
              Workspace* workspace = WorkspacesManager::findByName(preferences.getString(PREF_APP_WORKSPACE));
  -            
  +
              if (workspace)
                    mscore->changeWorkspace(workspace);
              }
  @@ -753,7 +753,7 @@ bool MuseScore::importExtension(QString path)

                    if (!sfzs.isEmpty())
                          synti->storeState();
  -                  
  +
                    s->gui()->synthesizerChanged();
                    }

  @@ -776,7 +776,7 @@ bool MuseScore::importExtension(QString path)
                          }
                    if (!sfs.isEmpty())
                          synti->storeState();
  -                  
  +
                    s->gui()->synthesizerChanged();
                    }
              };
  @@ -843,7 +843,7 @@ bool MuseScore::uninstallExtension(QString extensionId)
                    }
              if (found)
                    synti->storeState();
  -            
  +
              s->gui()->synthesizerChanged();
              }
        bool refreshWorkspaces = false;
  @@ -873,7 +873,7 @@ bool MuseScore::uninstallExtension(QString extensionId)
              bool curWorkspaceDisappeared = false;
              if (WorkspacesManager::findByName(curWorkspaceName))
                    curWorkspaceDisappeared = true;
  -            
  +
              if (curWorkspaceDisappeared)
                    changeWorkspace(WorkspacesManager::workspaces().last());
              }
  @@ -2036,7 +2036,7 @@ void MuseScore::retranslate()
        if (paletteWorkspace)
        paletteWorkspace->retranslate();
        }
  -      
  +
  //---------------------------------------------------------
  //   setMenuTitles
  //---------------------------------------------------------
  @@ -2136,7 +2136,7 @@ void MuseScore::updateMenus()
        addPluginMenuEntries();
  #endif
        }
  -      
  +
  //---------------------------------------------------------
  //   resizeEvent
  //---------------------------------------------------------
  @@ -3485,18 +3485,20 @@ static bool doConvert(Score *cs, const QString& fn);

  static bool doConvert(Score* cs, const QJsonArray& outFiles, QString plugin)
        {
  -      LayoutMode layoutMode = cs->layoutMode();
  -      cs->setLayoutMode(LayoutMode::PAGE);
  -      if (cs->layoutMode() != layoutMode)
  -            cs->doLayout();
  -
  -      if (!styleFile.isEmpty()) {
  +       if (!styleFile.isEmpty()) {
              QFile f(styleFile);
              if (f.open(QIODevice::ReadOnly)) {
                    fprintf(stderr, "\tusing style <%s>\n", qPrintable(styleFile));
                    cs->style().load(&f);
                    }
              }
  +
  +      LayoutMode layoutMode = cs->layoutMode();
  +      if (layoutMode != LayoutMode::PAGE) {
  +                        cs->setLayoutMode(LayoutMode::PAGE);
  +                        cs->doLayout();
  +                        }
  +
        if (!plugin.isEmpty()) {
              if (mscore->loadPlugin(plugin)) {
                    fprintf(stderr, "\tusing plugin <%s>\n", qPrintable(plugin));
  @@ -3649,7 +3651,10 @@ static bool convert(const QString& inFile, const QJsonArray& outFiles, const QSt
        MasterScore* score = mscore->readScore(inFile);
        if (!score)
              return false;
  +      mscore->appendScore(score);
        mscore->setCurrentScore(score);
  +      mscore->setCurrentView(0, 0);
  +      mscore->setCurrentView(1, 0);
        bool success = doConvert(score, outFiles, plugin);
        fprintf(stderr, success ? "... success!\n" : "... failed!\n");
        mscore->setCurrentScore(nullptr);
  @@ -3659,7 +3664,11 @@ static bool convert(const QString& inFile, const QJsonArray& outFiles, const QSt

  static bool convert(const QString& inFile, const QString& outFile)
        {
  -      return convert(inFile, QJsonArray{ outFile });
  +            if(pluginMode) {
  +                  return convert(inFile, QJsonArray{ outFile }, pluginName);
  +            }else{
  +                  return convert(inFile, QJsonArray{ outFile });
  +            }
        }

  //---------------------------------------------------------
  @@ -3731,7 +3740,7 @@ static bool processNonGui(const QStringList& argv)
        else if (exportScorePartsPdf)
              return mscore->exportPartsPdfsToJSON(argv[0]);

  -      if (pluginMode) {
  +      if (0) {
              loadScores(argv);
              QString pn(pluginName);
              bool res = false;
  @@ -3752,12 +3761,18 @@ static bool processNonGui(const QStringList& argv)
                          cs->setLayoutMode(layoutMode);
                          cs->doLayout();
                          }
  +                  QString newfile = "newfile_name.mscx";
  +                  QFileInfo fi(newfile);
  +                  return cs->saveFile(fi);
  +
                    res = true;
                    }
              if (!converterMode)
                    return res;
              }
  +
        if (converterMode) {
  +            qDebug() << "Running in converter mode";
              if (processJob)
                    return doProcessJob(jsonFileName);
              else
  @@ -7746,7 +7761,7 @@ void MuseScore::init(QStringList& argv)
                    Workspace* targetWorkspace = WorkspacesManager::visibleWorkspaces()[0];
                    if (targetWorkspace)
                          mscore->changeWorkspace(targetWorkspace, true);
  -                  
  +
                    preferences.setPreference(PREF_UI_APP_STARTUP_SHOWTOURS, sw->showTours());
                    delete sw;

  diff --git a/mscore/revision.h b/mscore/revision.h
  index 09277a11d..9cab92671 100644
  --- a/mscore/revision.h
  +++ b/mscore/revision.h
  @@ -1 +1 @@
  -3543170
  +87c14c3

In reply to by harries.adam

That patch is certainly not ready for inclusion, but please submit a (clean) PR for this. Do not include revision.h (changes to it should get ignored, there's a hint about how to avoid that in the developers handbook, albeit pretty well hidden, see https://musescore.org/en/handbook/developers-handbook/finding-your-way-…).
I've reconstructed and attached your patch (your's had formatting issues here)...

Attachment Size
plugin-converter-mode.patch 5.57 KB

In reply to by Jojo-Schmitz

I'm not surprised - it was just the result of me hacking on it to try and get something actually working. I've never worked with the musescore source code before, so the patch is just the result of my frustration - not an attempt to creat something beautiful.

Would it be worth me expanding the scope of the PR to tran and encompass some cleanup to command line behaviour in general? It's quite shockingly bad right now, and some cleaner handling of the various options might be good.

In reply to by Jojo-Schmitz

The command-line behaviour is /complicated/, and somewhat confusing. It's not entirely clear which options work together, and which options should work together. For example, the code to run plugins doesn't actually save anything, so if it's run without anything else it does absolutely nothing. Similarly, the converterMode re-loads the input file from scratch, so ignores anything that the plugin mode does beforehand.

I could go on - I think this is either an issue with documentation or with the overall design (or lack thereof) of the command line interface. I'm minded to say it's the latter (and happy to design something new), but I'm not sure where to start discussing this with the community. Is there a good place for formal RFC-like discussions? Or should I open an issue or something?

Hi - So 3 years later, I still can't figure out how to get musescore to batch-transpose - did you ever solve the problem ? Thanks !

In reply to by Jojo-Schmitz

Evidently not - running the example in his original post still fails. The output score has new text "Bb Instrument" but is otherwise identical to the input; it has not been transposed. Also the transpose commands cause the "not valid in current state" error pop-ups reported in my other post (musescore.org/en/comment/1190347). MS v3.6.2 on Windows 11. Thanks for any suggestions ...

Do you still have an unanswered question? Please log in first to post your question.