Posted at

MuseScoreにショートカットキーを追加してみた話

※この記事は、東京大学工学部電子情報工学科・電気電子工学科の実験「大規模ソフトウェアを手探る」の報告書として書かれたものです。


はじめに

MuseScoreとは、楽譜を作成することができるクロスプラットホームのGUIソフトウェアです。

オープンソースソフトウェアであり、GitHub上にソースコードが公開されています。

GitHub: https://github.com/musescore/MuseScore

大学での実験の一環として、私たちの班はこの「MuseScore」のソースコードを手探り、新たなショートカットを追加してみることを試みました。

結論から言うと、一部の機能については新しいショートカットを追加する方法を発見、実装することに成功しました。


環境

この記事では、以下の環境を想定しています。


  • Ubuntu 18.04.3 LTS

  • gcc 7.4.0

  • GNU gdb 8.1.0

  • Qt 5.13.1


ビルド

とりあえずソースをコンパイルしてみる。


Qtの入手

公式サイトを参考にインストール。

最初apt-get installでQtをインストールしていたのですが、なぜかパレットがうまく表示されないなどのバグが発生してしまいました(おそらく足りないパッケージがある等の問題だと思われる)。

そこで、Qt公式サイトの方からパッケージをダウンロードすることにしました。ダウンロード後、

sudo chmod +x qt-unified-linux-x64-3.1.1-online.run

sudo ./qt-unified-linux-x64-3.1.1-online.run

でインストーラを実行し、

echo 'export PATH=/opt/Qt/5.13.1/gcc_64/bin:$PATH' >> ~/.bashrc

source ~/.bashrc

でパスを通してQtのインストールは完了です。


ソースコードの入手

GitHub上のリポジトリからcloneします。

git clone https://github.com/musescore/MuseScore.git

cd MuseScore


コンパイル

こちらも公式サイトを参考にコンパイル。

以下のようにしてデバッグ用のビルドができます。

make revision

make installdebug PREFIX=install SUFFIX=-qt LABEL="Qt Creator Build" UPDATE_CACHE=FALSE
./build.debug/install/bin/mscore-qt

これでめでたくMuseScoreを自前でコンパイル・実行することができました!


ショートカットの追加をしてみる

さてめでたくビルドができたところで、いよいよMuseScoreの中身をいじってみましょう。

今回は例として、「ヤマハ音楽教室のCMの曲を一瞬で入力してくれる」という世にも便利なショートカットを実装します。

いやー、便利すぎて使いどころが全く思いつかないですね。

さてではまずは結論から。

ショートカットの追加方法としては、以下の3箇所のコードをいじればOKであることがわかりました。

以下、ソースコードのソースファイルがMuseScoreというディレクトリに保存されているものとし、そこからの相対パスにてコードの位置を表記しています。


①MuseScore/mscore/shortcut.cpp : 30行目以降


MuseScore/mscore/shortcut.cpp


Shortcut Shortcut::_sc[] = {
{
MsWidget::MAIN_WINDOW,
STATE_ALL,
"help",
QT_TRANSLATE_NOOP("action","Online Handbook…"), // Appears in menu
QT_TRANSLATE_NOOP("action","Online handbook"), // Appears in Edit > Preferences > Shortcuts
QT_TRANSLATE_NOOP("action","Show online handbook"), // Appears if you use Help > What's This?
Icons::Invalid_ICON,
Qt::ApplicationShortcut,
ShortcutFlags::NONE | ShortcutFlags::A_CHECKABLE
},
{
MsWidget::MAIN_WINDOW,
STATE_DISABLED | STATE_NORMAL | STATE_NOTE_ENTRY | STATE_EDIT | STATE_PLAY,
"file-open",
...

ここでは各ショートカットの宣言が行われています。

Shortcutクラスの配列_sc[]の中に一つ一つのショートカットを定義して格納していくわけです。

ショートカットを追加したい場合、ここに宣言を追加します。

宣言の内容の詳細項目については調べきっていませんが、さしあたり前後のものをコピーして必要な部分だけ変えて用いれば十分でしょう。

今回の例では、音符入力機能の一つであるinsert-gの宣言を真似して、その下にショートカットyamahaを追加します。


MuseScore/mscore/shortcut.cpp

      {

MsWidget::SCORE_TAB,
STATE_NORMAL | STATE_NOTE_ENTRY_STAFF_PITCHED | STATE_NOTE_ENTRY_STAFF_DRUM,
"insert-g",
QT_TRANSLATE_NOOP("action","Insert G"),
QT_TRANSLATE_NOOP("action","Insert note G")
},

//ここから追加した内容
{
MsWidget::SCORE_TAB,
STATE_NORMAL | STATE_NOTE_ENTRY_STAFF_PITCHED | STATE_NOTE_ENTRY_STAFF_DRUM,
"yamaha",
QT_TRANSLATE_NOOP("action","Yamaha"),
QT_TRANSLATE_NOOP("action","Yamaha Ongaku Kyoshitsu")
},


これにより、ショートカットyamahaが宣言され追加されました。


②MuseScore/mscore/data/shortcuts.xml : 1行目以降


MuseScore/mscore/data/shortcuts.xml


<?xml version="1.0" encoding="UTF-8"?>
<Shortcuts>
<SC>
<key>plugin-creator</key>
<seq>Ctrl+Shift+P</seq>
</SC>
<SC>
<key>help</key>
<seq>F1</seq>
</SC>
<SC>
<key>file-open</key>
<std>3</std>
</SC>
...

ここではショートカットのキー割り当てをリスト化しています。

先程の宣言とはkeyの名前によって対応付が行われているため、このkeyを間違えないように注意しつつ新しいショートカットのキーを追加していきましょう。

なお、キーとの種類によってseqやstdなどいくつかタグがあるので注意。Ctrlと組み合わせて用いるキーか、などといったあたりで差があるようなので前後の例に準じましょう。

まあ基本的にはseqを用いれば問題ないようには思われます。


MuseScore/mscore/data/shortcuts.xml

  <SC>

<key>insert-g</key>
<seq>Ctrl+Shift+G</seq>
</SC>

//ここから追加した内容
<SC>
<key>yamaha</key>
<seq>Alt+Y</seq>
</SC>


ここではひとまず使われていないキーAlt+Yをショートカットyamahaに割り当ててみました。


注意点

開発を勧めていく上ではおそらく何度もビルドをし直すことになりますが、そのときにmake cleanを行っても、このxmlファイルの更新は反映されません。

理由としては、一度コンパイルしたあとは、ubuntuの場合

/home/ユーザ名/.local/share/MuseScore/MuseScore3Development/shortcuts.xml

などという場所にファイルが移され、ここを参照するようになるためです(ここでない場合についてその調べ方は後述します)。

このファイルはmake cleanを行っても消えてくれませんし、再度ビルドしてもこのファイルがあるおかげで、編集したxmlファイルはスルーされてしまうわけです。

そのため、再コンパイルする際にはこれを直接編集するか、もしくはこのファイルを消してから再度コンパイルすることで編集内容を反映することができます。


③MuseScore/libmscore/cmd.cpp : 3609行目以降


MuseScore/libmscore/cmd.cpp

void Score::cmd(const QAction* a, EditData& ed)

{
QString cmd(a ? a->data().toString() : "");
if (MScore::debugMode)
qDebug("<%s>", qPrintable(cmd));

struct ScoreCmd {
const char* name;
std::function<void()> cmd;
};
const std::vector<ScoreCmd> cmdList {
{ "note-c", [this,ed]{ cmdAddPitch(ed, 0, false, false); }},
{ "note-d", [this,ed]{ cmdAddPitch(ed, 1, false, false); }},
{ "note-e", [this,ed]{ cmdAddPitch(ed, 2, false, false); }},
{ "note-f", [this,ed]{ cmdAddPitch(ed, 3, false, false); }},
{ "note-g", [this,ed]{ cmdAddPitch(ed, 4, false, false); }},
{ "note-a", [this,ed]{ cmdAddPitch(ed, 5, false, false); }},
{ "note-b", [this,ed]{ cmdAddPitch(ed, 6, false, false); }},
{ "chord-c", [this,ed]{ cmdAddPitch(ed, 0, true, false); }},
{ "chord-d", [this,ed]{ cmdAddPitch(ed, 1, true, false); }},
...


はい。とてもそれっぽいコードが出てきましたね。

ここでショートカットひとつひとつの実際の機能が定義されています。

いわばショートカットの核です。

これまで同様、前後のものを参考に追加してみましょう。

ラムダ関数の中身には、例えばcmdAddPitch()なら音符を追加、padToggle()なら音の長さを変更、などと行った具合に既に用意されている関数を並べます。

cmd.cpp内に書かれている関数なら自由に使えるので、これを見ながら組み合わせることでいろいろな機能を組み合わせましょう。

上下を見たりkeyを調べたりすればそれぞれがどんな関数かは想像がつきやすいかと思うのでここではその解説は割愛します。


MuseScore/libmscore/cmd.cpp


{ "insert-a", [this,ed]{ cmdAddPitch(ed, 5, false, true); }},
{ "insert-b", [this,ed]{ cmdAddPitch(ed, 6, false, true); }},

//ここから追加した内容
{ "yamaha", [this,ed]{
padToggle(Pad::NOTE8); cmdAddPitch(ed, 0, false, false);
cmdAddPitch(ed, 1, false, false);
cmdAddPitch(ed, 2, false, false);
cmdAddPitch(ed, 3, false, false);
padToggle(Pad::NOTE4); cmdAddPitch(ed, 4, false, false);
padToggle(Pad::NOTE8); cmdAddPitch(ed, 5, false, false);
cmdAddPitch(ed, 3, false, false);
padToggle(Pad::NOTE4); cmdAddPitch(ed, 2, false, false);
cmdAddPitch(ed, 1, false, false);
padToggle(Pad::NOTE2); cmdAddPitch(ed, 0, false, false);
padToggle(Pad::NOTE8); padToggle(Pad::DOT); cmdAddPitch(ed, 4, false, false);
cmdPitchUpOctave();
padToggle(Pad::NOTE16); cmdAddPitch(ed, 3, false, false);
padToggle(Pad::NOTE8); cmdAddPitch(ed, 2, false, false);
cmdAddPitch(ed, 4, false, false);
cmdAddPitch(ed, 3, false, false);
cmdAddPitch(ed, 2, false, false);
padToggle(Pad::NOTE4); cmdAddPitch(ed, 1, false, false);
padToggle(Pad::NOTE8); padToggle(Pad::DOT); cmdAddPitch(ed, 4, false, false);
padToggle(Pad::NOTE16); cmdAddPitch(ed, 3, false, false);
padToggle(Pad::NOTE8); cmdAddPitch(ed, 2, false, false);
cmdAddPitch(ed, 4, false, false);
cmdAddPitch(ed, 3, false, false);
cmdAddPitch(ed, 2, false, false);
padToggle(Pad::NOTE4); cmdAddPitch(ed, 1, false, false);
padToggle(Pad::NOTE8); cmdAddPitch(ed, 0, false, false);
cmdAddPitch(ed, 1, false, false);
cmdAddPitch(ed, 2, false, false);
cmdAddPitch(ed, 3, false, false);
padToggle(Pad::NOTE4); cmdAddPitch(ed, 4, false, false);
padToggle(Pad::NOTE8); cmdAddPitch(ed, 5, false, false);
cmdAddPitch(ed, 3, false, false);
padToggle(Pad::NOTE4); cmdAddPitch(ed, 2, false, false);
cmdAddPitch(ed, 1, false, false);
padToggle(Pad::NOTE2); cmdAddPitch(ed, 0, false, false);
}},


少々雑な部分はありますが、これでyamahaの機能が定義されました。

なお、ここに存在しないショートカットについてはMuseScore/mscore/scoreview.cppで機能定義がされているようです。

代表的なものとしては休符の追加やスラーの追加などでしょうか。

特に今回休符はできれば使いたかったので、scoreview.cppで定義されている関数を用いる方法も色々検討したのですが、バグの温床となってしまう様子だったのでやむなく断念しました。

こちらの機能を使いたい方は、まあ頑張りましょう。

以上の3箇所を変更すれば、ショートカットyamahaは実装されます。

まあぶっちゃけこれ自体はなんの役にも立たない機能ですが、自分の追加したいショートカットがある場合同様の手順で頑張って実装してみてください。


変更方法をどうやって見つけたか

さて、ここまでは結論として「どうすればショートカットを追加できるか」を書いてきましたが、他のいろいろな機能をいじりたい人向けに、ここまでに至った経緯も書いておきます。

まず最初にやったのは、デバッガGNU gdb 8.1.0を用いて、ソフトウェア起動前の処理の構造を探っていく方法です。

main()にブレイクポイントを設定し、地道にコードの中身を見ていきました。

GNU gdbの使い方については、僕らが受講している講義のテキストを参照していただくととてもわかり易いのではないかと思います。デバッガはいいぞ。

ただMuseScoreのソースコードは膨大であること、また要所要所にQtという外部ライブラリを用いておりそのソースコードの中にデバッガで立ち入るのはやや難しいことから、なかなか核心部分に触れることは敵いません。

そのため、非常に原始的な方法ながら、まずはファイル名の中から「shortcut」という名前のつくものを検索してみました。

するとshortcut.cppshortcuts.xmlなどといったそれらしきファイルを発見できました。

また同時に、MuseScoreの開発者向けissueの中に、ショートカットを編集しようとしている人のissueを発見し、そこから偶然にもcmd.cppの存在を知ることができました。

これらのファイル内のそれらしき場所を検索したりしていくことで、上記の該当3箇所を見つけることは割合とたやすくできました。

また、ショートカットの追加に当たっては「ショートカットの宣言」「キーの割当」「機能の設定」の3つができていればおそらく動くのではないかという見立てもできたため、これらを編集することを試みました。

しかしなぜかshortcuts.xmlへの変更が反映されず苦戦することとなります。

まず第一に考えた原因が、「このファイルは既に使われておらず別のファイルでキー割り当てが決められている」ということでした。

それを探るために、田浦教授が公開してくださったlibitraceというソフトを用いて、MuseScoreの「ショートカットキーの割当変更機能」を使ったときの前後で呼び出されている関数を調べてみました(その結果はこちら)。

この結果、やはりどうもshortcut.cpp内の関数Shortcut::setKeysが呼び出されていることがわかり、またこの関数にはやはりshortcutの配列_shortcutsが渡されていることがわかり、また_shortcutsの初期化に使われている以下のコードを見つけました。


shortcut.cpp

void Shortcut::init()

{
//
// initialize shortcut hash table
//
_shortcuts.clear();
for (Shortcut& i : _sc)
_shortcuts.insert(i._key, &i);
if (!MScore::noGui)
load();
}

この中で特に'load()'という関数が怪しいことがわかります。これをさらに見てみると、

void Shortcut::load()

{
QFile f(dataPath + "/shortcuts.xml");
if (!f.exists())
f.setFileName(defaultFileName);
if (!f.open(QIODevice::ReadOnly)) {
qDebug("Cannot open shortcuts <%s>", qPrintable(f.fileName()));
return;
}
if (MScore::debugMode)
qDebug("read shortcuts from <%s>", qPrintable(f.fileName()));

XmlReader e(&f);
...

とあります。3行目でやはりshortcuts.xmlを参照していることがわかりました。

となると、一体原因は何だろうと考えたときに、この3行目の変数dataPathが怪しいことに気が付きます。

実はこの'dataPath'が全然違う場所を表しているとしたら…………

それを考え、再びGNU gdbを用いてこの変数の中身を探りました。

(ブレイクポイントをこの直前に設定し、変数の中身のプリント機能を用いました(参考としたサイト))

するとこれがQtのQStringというオブジェクトであることがわかったので、先人のブログを参考にしつつこの中身を表示してみたら、このdataPathが

/home/ユーザ名/.local/share/MuseScore/MuseScore3Development/

という内容であることがわかりました。

つまり、我々が見ていたxmlファイルとは全く別の場所にこの'shortcuts.xml'はあったわけです。

MuseScoreにおけるmake cleanはこれを削除まではしてくれないためいくらビルドし直しても変更が反映されないのだ、ということがわかったためこれを削除して再びビルドしてみたところ、無事にショートカットの追加が成功しました。

なお、他の便利だったコマンドとしてgrepコマンドがあります。これはシェルのファイル内の検索コマンドです。

要所要所で、「shortcut」などと検索してみる手法は用いました。

grepの使い方はこちらのサイトなどに詳しく載っているため参考にするとよいかと思います。


感想

これほど大規模なソースコードを読み解くのは実験班のメンバー2人とも初めての経験でした。ビルドやデバッグを重ねる中で、これだけ大きいコードの片鱗をどうやったら読み解くことができるのかを学ぶことができ、大変有意義な実験だったと思います。

今回の実験を通して、MuseScoreのショートカットの仕組みを紐解くことはできましたが、実際になにか有用な機能を追加する、というところまではできませんでした(本当は調号や反復記号を入力するショートカットとかを実装したかった…)。今後もし余力があれば、そういった機能を追加していけたらいいな、と思っています。