🎵

MuseScore Studio に強弱記号ショートカットを実装する

2024/11/19に公開

はじめに

この記事は、東京大学工学部電子情報工学科・電気電子工学科の学生実験「大規模ソフトウェアを手探る」のレポートの一部として書かれたものです。
レポートの全記事をご覧になりたい方は下のリンクよりご覧ください。
https://zenn.dev/cmakescore/articles/6ca173cd570216

この記事について

この記事では、楽譜作成ソフト MuseScore Studio で強弱記号のテキスト要素をキーボードショートカット経由で追加できるようにするために行ったことを紹介します。

新しいショートカットを定義する

まずは強弱記号ショートカットを新しく定義して、環境設定のショートカットの一覧画面に表示させることを目標にしてみます。機能の実装はその後です。

関連するファイルを特定する

ショートカットに関するコードはどこに、どのように書かれているのでしょうか。まずは関連するファイルを探してみましょう。
他のアクションに対するキーボードショートカットはすでに実装されているので、対応するプルリクエストが存在するはずです。そこで、まずはそのプルリクエストを探してみることにします。

MuseScore の GitHub リポジトリ のプルリクエストで、 "shortcut" と検索してみたところ、直近でショートカットの追加が行われた以下のプルリクエストが見つかりました。
https://github.com/musescore/MuseScore/pull/23488

Commits タブから allow shortcuts for more ornaments というコミットを参照すると、 src/notation/internal/notationactioncontroller.cpp、src/notation/internal/notationuiactions.cpp の2つのファイルをいじれば良さそうなことがわかります。

既存の実装を模倣してみる

上の2つのファイルに、強弱記号以外のテキストを追加するショートカットが定義されていそうな部分を参考にしつつ、まずは見様見真似で新しいショートカットの定義を追記してみます。これで上手く行けばラッキー、上手くいかなかったら後で手直ししてみれば良い話です。

src/notation/internal/notationactioncontroller.cpp

このファイルでは、ショートカットのキーとして使う文字列と、そのショートカットが実行されたときに呼び出すべき関数を登録しているようです。
addText といういかにもテキストを追加してくれそうな関数が書かれている部分があったので、これが関係していそうです。引数には、追加するべきテキストの種類が渡されています。強弱記号は英語で dynamics なので、ここでは TextStyleType::DYNAMICS を渡します。

TextStyleType について

この列挙型は、以下のファイルで定義されています。 MuseScore Studio では多くの要素がテキスト要素によって実現されていますが、要素ごとのフォントやスタイル、振る舞いなどを決定づけるために使われています。
https://github.com/musescore/MuseScore/blob/1b50c5be8715b8a234bf9eb01200aa8393762493/src/engraving/types/types.h#L749-L833

新しいショートカットのキーは "dynamics" とし、以下のように追記します。

 void NotationActionController::init()
 {
     ...
     registerAction("system-text", [this]() { addText(TextStyleType::SYSTEM); });
     registerAction("staff-text", [this]() { addText(TextStyleType::STAFF); });
     registerAction("expression-text", [this]() { addText(TextStyleType::EXPRESSION); });
     registerAction("rehearsalmark-text", [this]() { addText(TextStyleType::REHEARSAL_MARK); });
     registerAction("instrument-change-text", [this]() { addText(TextStyleType::INSTRUMENT_CHANGE); });
     registerAction("fingering-text", [this]() { addText(TextStyleType::FINGERING); });
     registerAction("sticking-text", [this]() { addText(TextStyleType::STICKING); });
     registerAction("chord-text", [this]() { addText(TextStyleType::HARMONY_A); });
     registerAction("roman-numeral-text", [this]() { addText(TextStyleType::HARMONY_ROMAN); });
     registerAction("nashville-number-text", [this]() { addText(TextStyleType::HARMONY_NASHVILLE); });
     registerAction("lyrics", [this]() { addText(TextStyleType::LYRICS_ODD); });
     registerAction("tempo", [this]() { addText(TextStyleType::TEMPO); });
+    registerAction("dynamics", [this]() { addText(TextStyleType::DYNAMICS); });
     ...
 }

src/notation/internal/notationuiactions.cpp

このファイルでは、ショートカットのキーの文字列に対して、ショートカットが実行できる状況や、ショートカットの編集画面、メニューなどで表示する文字列を定義しているようです。
先程見つけたテキスト追加系のショートカットのキーについて書かれている部分を参考にして、以下のように追記してみます。

 const UiActionList NotationUiActions::m_actions = {
     ...
     UiAction("tempo",
              mu::context::UiCtxProjectOpened,
              mu::context::CTX_ANY,
              TranslatableString("action", "Tempo &marking"),
              TranslatableString("action", "Add text: tempo marking")
              ),
+    UiAction("dynamics",
+             mu::context::UiCtxProjectOpened,
+             mu::context::CTX_ANY,
+             TranslatableString("action", "Dynamics"),
+             TranslatableString("action", "Add text: dynamics")
+             ),
     ...
 };

動作を確認してみよう

一旦ここまでの結果を確認してみましょう。UIの実装を目標にしていましたが、 addText 関数がうまいこと処理してくれれば、機能の実装もできているかもしれないという期待を抱きながら、ビルドして実行してみます。
環境設定のショートカット一覧を確認してみると、今回追加したショートカットが表示されていました!
追加されたショートカット

しかし、この画面からショートカットを割り当てて、実際にショートカットを入力してみても、何も起こりません。機能の実現にはまだ実装が足りていないようです。

ショートカットの機能を実装する

ということで、ショートカットを実行したときに実際に強弱記号のテキスト要素が追加されるように、機能を実装してみます。テキストが追加されない原因は、addText 関数に渡している TextStyleType により場合分けが行われていることだと予想されます。早速手探っていきましょう。

デバッガで挙動を観察

まずは addText 関数にブレークポイントを設定して、ショートカット経由でテキストを追加し、どこで場合分けが行われているか突き止めましょう。

デバッガの使い方の基本

Qt Creator では行数の左の部分をクリックすると、ブレークポイント(赤い丸印)を追加することができます。ブレークポイントを設定した行のプログラムが実行されようとすると、デバッガはそこで実行を一時停止します。
ブレークポイント
ブレークポイントで実行を止めてデバッグしている様子。黄色の矢印は、次に実行される行を示しています。
一時停止したあとは、動作を確かめながら1行ずつ実行したり(ステップオーバー)、呼び出している関数の中に入ったり(ステップイン)、黄色の矢印をドラッグして無理矢理次に実行する行を変更したりすることができます。
コードの実行中どこの分岐に入っているのか、どの関数を呼び出すのか、どのタイミングでどの処理が行われているのかなど確認するために使うことができます。
操作ボタン
デバッガの操作ボタン。左から順に一時停止の解除、強制終了、ステップオーバー、ステップイン、ステップアウト。

addText の動作をデバッガで追いかけて、実際にテキスト要素を追加している動作が書かれている場所を調べてみたところ、いくつかの関数を経由して、 src/engraving/dom/edit.cpp の Score::addText にたどり着きました。

再び模倣

Score::addText では、 TextStyleType に応じて分岐が起き、それぞれについてテキストを追加する際の処理が書かれているようです。
普段 MuseScore Studio で楽譜を書いている方はご存知だと思いますが、強弱記号は発想標語(Expression Text)とその動作がよく似ています。そこで、発想標語を追加する動作について書かれている部分を参考にして、強弱記号の追加動作を追記してみましょう。

src/engraving/dom/edit.cpp

 TextBase* Score::addText(TextStyleType type, EngravingItem* destinationElement)
 {
     ...
     switch (type) {
     ...
     case TextStyleType::EXPRESSION: {
         ChordRest* chordRest = chordOrRest(destinationElement);
         if (!chordRest) {
             break;
         }
         textBox = Factory::createExpression(dummy()->segment());
         chordRest->undoAddAnnotation(textBox);
         break;
     }
     ...
+    case TextStyleType::DYNAMICS: {
+        ChordRest* chordRest = chordOrRest(destinationElement);
+        if (!chordRest) {
+            break;
+        }
+        textBox = Factory::createDynamic(dummy()->segment());
+        chordRest->undoAddAnnotation(textBox);
+        break;
+    }
     ...
     }
 }

動作確認

ビルドして実行、確認してみると、ショートカットが実際に機能し、強弱記号のテキスト要素が追加されるようになっていました。やはり模倣は最強ですね。

デフォルトのショートカットを設定する

強弱記号はよく用いられるので、デフォルトのショートカットを設定しておくほうが親切でしょう。他のテキスト要素の追加コマンドの多くが Ctrl + ◯ のショートカットを使っている傾向にあることを踏まえて、 Ctrl + D をデフォルトのショートカットとして設定してみます。

デフォルトショートカットの登録ファイルを編集する

デフォルトのショートカットを設定しているであろうファイルはプラットフォームやキーボードレイアウトごとに分かれているようで、 src/app/configs/data/shortcuts.xml 、 src/app/configs/data/shortcuts_azerty.xml 、 src/app/configs/data/shortcuts_mac.xml の3個ありました。それぞれに以下を追記します。

 <?xml version="1.0" encoding="UTF-8"?>
 <Shortcuts>
   ...
+  <SC>
+    <key>dynamics</key>
+    <seq>Ctrl+D</seq>
+    </SC>
   ...
   </Shortcuts>

動作確認

動作確認のときにはひと工夫必要でした。ユーザーの上書きしたショートカットが保存されている場合はそれがリセットされてしまわないようにデフォルトのショートカットが読み込まれないようになっているようなので、ユーザー設定ファイルを削除してあげる必要があります。
筆者は Ubuntu で作業していましたので、 /home/(ユーザー名)/.local/share/MuseScore/MuseScore4Development/shortcuts.xml を削除して実行し直すと、デフォルトのショートカットが設定されていることが確認できました。
Windows の場合は %localappdata%\MuseScore\MuseScore4Development\shortcuts.xml を消せばよいです。
Mac の方は、ごめんなさい。この記事の筆者は Mac ユーザーではないのでわかりません。

入力した強弱記号を解釈する

ここまでで、ショートカットを経由して強弱記号を入力するテキストボックスをスコアに追加することができるようになりましたが、まだ入力した強弱記号は演奏に反映されません。また、強弱記号のシンボルは Ctrl+Shift+アルファベット でも入力することができますが、普通に入力した文字列も強弱記号として有効であれば強弱記号シンボルに置き換えるほうが親切でしょう。
ということでここからは、入力した文字列を処理して、有効な強弱記号として認識されるようにしていきます。

テンポテキストの編集が終わった瞬間にテンポプロパティが書き換わる動作を観察

入力した文字列に対して処理を行う方法を探るため、既存機能の観察です。テンポ記号のテキストを編集すると、入力した数値のとおりにテンポが設定されるという動作がどのように起こっているのか、観察してみます。

まず野生の直感で src/engraving/dom/tempotext.cpp のファイルを見てみると、 updateTempo という関数があり、テンポテキストをパースしてテンポのプロパティを書き換えている処理がこの中に書かれていることがわかりました。

この関数はどこから呼び出されているのでしょうか?
ここでデバッガの出番です。 updateTempo の中にブレークポイントを設定して動作を止め、スタックを眺めてみます。

スタック

スタックとは

関数の呼び出し順序や実行状態を管理するために使われるデータのことで、デバッガを使うと上の画像のように眺めることができます。イメージとしては積んである本のようなデータ構造で、関数が呼び出されると上に積まれ、関数が終了すると上から取り除かれます。
このデータでは下の関数から上の関数を呼び出したという関係が成り立つので、これを見ることで、関数がどこから・どのような順番で呼び出されているのかを簡単に調べることができます。

ご覧の通り、テキストの編集を終了した瞬間には src/engraving/dom/textedit.cpp の endEdit → src/engraving/dom/tempotext.cpp の commitText → src/engraving/dom/tempotext.cpp の updateTempo と呼ばれていることがわかりました。

これらの情報から、テキストの編集が終了した瞬間に強弱記号を解釈するには、 src/engraving/dom/dynamic.cpp に commitText と updateDynamic 相当の関数を実装すれば良さそうな気がします。

強弱記号のパースをどうするか?

では、 updateDynamic を実装するにはどのようにすればいいのでしょうか。まずは同じファイルに書かれている内容を読んで雰囲気を掴んでみようと思います。

すると、 DYN_LIST という定義を発見しました。テキスト xml の情報と強弱記号の値を関連付けている。
そしてなんと、 DYN_LIST と照合して、強弱記号の種類を設定している関数 setDynamicType も存在しているではありませんか。
自分で updateDynamic なる関数を実装するより、既存のこの関数をありがたく使わせていただくことにすればとても楽に実装できてしまいそうです。

いざ実装

方針は固まったのでさっさと作ってしまいましょう。コードの変更量はとても少なく簡単です。

src/engraving/dom/dynamic.h

 ...
 class Dynamic final : public TextBase
 {
 ...
+ protected:
+    void commitText() override;
 };
 ...

src/engraving/dom/dynamic.cpp

 ...
 namespace mu::engraving {
 ...
+void Dynamic::commitText()
+{
+   setDynamicType(xmlText());
+   TextBase::commitText();
+}
 }

動作確認

実現したい機能に対してとても少ないコード変更量でしたが、実際に試してみると、テキスト編集が終了したタイミングで強弱記号が解釈され、再生時に反映されるようになりました!
既存の関数を使ったため圧倒的コスパ・タイパで機能実装ができてしまいました。

メニューの UI をつくる

他のテキスト要素を追加するのショートカットは、メニューにも同じコマンドが実行できる項目が存在します。強弱記号を追加するコマンドについても同様の項目を追加しましょう。
具体的には、上部のメニューの Add > Text > Dynamics と、音符入力バーの + > Text > Dynamics の項目を増やしてみます。

コードのどこをいじればよいのかを探すにあたって、ショートカットを登録した際のキー文字列と関係があるのではないかと考え、コードの他の部分に出てきそうにないキー文字列("nashville-number-text")でコードを全文検索してみたところ、関係する2ファイルを特定することができました。

またまた模倣

他のコマンドと同様に書いていきます。

src/appshell/view/appmenumodel.cpp

このファイルは画面上部のメニューに対応しています。

 ...
 MenuItemList AppMenuModel::makeTextItems()
 {
     MenuItemList items {
         ...,
         makeMenuItem("staff-text"),
+        makeMenuItem("dynamics"),
         makeMenuItem("expression-text"),
         ...
     };
     ...
 }
 ...

src/notation/view/noteinputbarmodel.cpp

このファイルは音符入力バーのメニューに対応しています。

 ...
 MenuItemList NoteInputBarModel::makeTextItems()
 {
     MenuItemList items {
         ...,
         makeMenuItem("staff-text"),
+        makeMenuItem("dynamics"),
         makeMenuItem("expression-text"),
     };
     ...
 }
 ...

動作確認

ビルドし直して確認すると、正しくメニューアイテムが追加されていることを確認できました!

追加できないときにエラーメッセージを表示する

実際にショートカットを使いながら他のショートカットと比較していると、今回作成したショートカットは音符や休符以外を選択している場合に強弱記号が追加できないにもかかわらず、なんのエラーメッセージも出さないということが判明しました。どうやらエラーの判定はまた別に実装する必要があるようです。

関連箇所の特定

ひとまず、他のショートカットを使用したときに出てくるエラーメッセージのダイアログに表示されている文字列でコードを全文検索したところ、 src/notation/notationerrors.h と src/notation/internal/mscoreerrorscontroller.cpp の2つが引っかかりました。デバッガでそれぞれにブレークポイントを設定してもう一度エラーを発生させてみると、前者が動いていることがわかりました。
スタックを眺めてこれを呼び出している関数を追跡していくと、 src/notation/internal/notationinteraction.cpp の canAddTextToItem という関数でエラーの判定をしていることがわかりました。

実装

ここに、強弱記号を追加しようとしたときに音符や休符が選択されていなかった場合にエラーを起こすように追記しましょう。

src/notation/internal/notationinteraction.cpp

 ...
 Ret NotationInteraction::canAddTextToItem(TextStyleType type, const EngravingItem* item) const
 {
     ...
     static const std::set<TextStyleType> needSelectNoteOrRestTypes {
         ...
         TextStyleType::STAFF,
+        TextStyleType::DYNAMICS,
         TextStyleType::EXPRESSION,
         ...
     };
     ...
 }
 ...

動作確認

動作を確認すると、強弱記号が追加できない場合にエラーを表示するようになった。
ここまでで、強弱記号ショートカットの追加は一段落です。

おわりに

このような手順で、強弱記号をショートカットで追加する機能を完成させることができました。
この新機能を実際に MuseScore Studio にマージしてもらうには、まだいくつかの作業が残っています。これらの詳細については下の記事で紹介していますので、こちらもぜひご覧ください。
https://zenn.dev/cmakescore/articles/04ffecc8af518c

Discussion