🐈

MuseScoreに様々な機能を追加してみた。

に公開

この記事について

この記事は、東京大学工学部電気系の学生実験である「大規模ソフトウェアを手探る(大規模ソフトウェアを手探る)」における取り組みの1つとして執筆しています。
レポートの全記事をご覧になりたい方は下のリンクよりご覧ください。

https://zenn.dev/yamamotomutsumi/articles/bb3ea47ad36e45

※全体の流れは非常に長くなるため、ここでは要所のみの解説としています。全体のコードを見たい方はこちらからご確認ください。解説の部分に載せているコード以外にも変更しないとおそらく機能は実装できないと思います。

https://github.com/doss-eeic/2025-04-MuseScore

動作環境

以下の環境で動作確認を行いました。

  • OS: Ubuntu Linux 24.04 LTS
  • Compiler: GCC 13.3.0
  • Framework: Qt 6.9.3

デバッグをしやすくする変更(プロファイラの改善)

MuseScore のデバッグ作業中、アプリケーション終了時にターミナルへ出力されるデバッグ情報(各関数の実行時間など)がありました。当初は gdb の機能かとも思いましたが、調べてみるとどうやら MuseScore 固有の機能である可能性が浮上しました。

そこで、この出力機能がどこで実装されているのかを調査し、よりデバッグしやすくなるように改良を試みました。

調査と特定

この機能は GUI ではなく標準出力(printf)で実現されていると仮定し、アプリケーションのエントリーポイントである main.cpp から調査を開始しました。

以前 main.cpp を調査した際に、ConsoleApp::finish() 関数でアプリが終了することを知っていたため、その中身を確認したところ、PROFILER_PRINT; という記述を発見しました。

この PROFILER_PRINT といういかにもそれらしいマクロの定義元を探したところ、./src/framework/global/thirdparty/kors_profiler/profiler/profiler.cpp というファイルに行き着きました。

このファイル内には、デバッグ時に出力されていた文字列(関数名や時間)が多数含まれており、ここでプロファイリング情報の集計と出力を行っていると特定できました。

機能の追加:呼び出し回数でのソート

デフォルトのプロファイラ出力は、メインスレッドが呼び出した合計時間順でソートされていました。これはボトルネックを探すのには便利ですが、デバッグ時にはどの関数が何回呼ばれているかを知りたい場合もあります。

そこで、この出力順序を呼び出し回数順でもソートできるよう、コマンドライン引数で切り替えられる機能を追加することにしました。

profiler.cpp 内のソート処理を行う関数を特定し、次にコマンドライン引数を処理していそうなファイルを探しました。

commandline という名前でファイルを検索すると、./src/app/internal/commandlineparser.cpp を発見しました。このファイル内でコマンドラインオプションを定義していそうな箇所を見つけ、新しいオプション(--sort-by-call-count)を追加し、それが渡された場合に profiler.cpp 側のソート処理が切り替わるように変更しました。

実行方法

この機能は gdb 経由で実行する際に便利です。
gdb を起動した後、run コマンドの後にオプションを渡します。

gdb ./src/app/mscore

▼ 呼び出し回数順でソートする場合
--sort-by-call-count オプションを付けて実行します。

(gdb) run --sort-by-call-count

この時、次のように表示されます。

▼ 合計時間順でソートする場合 (デフォルト)
オプションを付けずに実行します。

これにより、MuseScore 終了時に出力されるプロファイラ情報が、指定した順序でソートされるようになりました。

この時、次のように表示されます。

注意点

なお、この実装には注意点があります。
一度 gdb のセッション内で --sort-by-call-count を付けて実行すると、その情報が保持されてしまいます。

そのため、次に同じセッションでオプション無しで実行しても、呼び出し回数順でソートされてしまいます。

もしソート順をデフォルトの「合計時間順」に戻したい場合は、一度 gdb を q コマンドで終了し、再度 gdb を立ち上げ直す必要があります。

オープンハイハットの譜面表記の変更

MuseScore のデフォルトのドラム譜面では、オープンハイハットの表記が、一般的な(クローズドハイハット「×」の上に「〇」がつく)形ではなく、「xcircle」(×印が〇で囲まれた記号)で表示されます。

個人の設定でこれを変更することは可能ですが、非常に手間がかかります。そこで、デフォルトの表記自体を変更するため、ソースコードの解析と変更を行いました。

調査1:楽器定義ファイル (instruments.xml) の特定

まず、ミューズスコア公式の開発者ハンドブックを参考に、楽器の定義が instruments.xml で行われていることを突き止めました。

このファイル内を「Open Hi-hat」で検索すると、複数の定義が見つかりました。その一部は次のようになっていました。

 <Drum pitch="46"> <!--Open Hi-hat-->
        <head>xcircle</head>
        <line>-1</line>
        <voice>0</voice>
        <name>Open Hi-Hat</name>
        <stem>1</stem>
        <shortcut>D</shortcut>
        <panelRow>0</panelRow>
        <panelColumn>3</panelColumn>
  </Drum>

これは、MuseScore が「大きめのドラム」や「普通のドラム」といったセットごとに楽器定義を分けているためです。

その中で、ハイハットの見た目(符頭)に関係ありそうな記述として、符頭(head)が xcircle と指定されている箇所を発見しました。

実装1:「cross-circle」の仮説

試しに、この xcircle の部分をクローズドハイハットと同じ cross に置き換えてビルドしたところ、オープンハイハットの見た目がクローズドハイハットと全く同じ「×」印に変わりました。

この結果から、この符頭(head)の部分を、一般的な表記である「cross-circle」という(まだ存在しない)定義名に変更し、その「cross-circle」の定義を自分で追加すれば問題を解決できる、という仮説を立てました。

そこで、既存の xcircle の定義を参考に、関係しそうなファイル全てに「cross-circle」に関する項目を追加しました。ビルドエラー(誤植)を修正しながら作業を進め、ビルドには成功しましたが、まだハイハットの見た目に変化はありませんでした。これは、cross-circle が具体的にどのような形を描画するのかを、まだ一切定義していないためです。

調査2:描画処理の特定 (tdraw.cpp)

次に、具体的な描画処理を定義している場所を探しました。
公式のコード構成の解説ページ(Notion)や過去のプルリクエスト(変更履歴)を確認すると、見た目に関する変更は ./src/engraving ディレクトリに集中していることがわかりました。

その中の rendering という、いかにも描画に関係ありそうなディレクトリを探すと paint.cpp というファイルがあり、そこから最終的に tdraw.cpp というファイルがスコアの実際の描画を行っていると突き止めました。

実装2 と新たな問題:二重描画

計画通り tdraw.cpp を解析し、cross-circle が指定された場合に特定の模様(「〇」印)を描くようにコードを追加して実行しました。

すると、「〇」は表示されるようになったものの、意図していた cross(×印)の上ではなく、元の xcircle の記号の上に「〇」が描画されてしまう、という二重描画の問題が発生しました。

調査3 と解決:二段階描画の実装

この原因は、tdraw.cpp 側ではなく、ベースとなる符頭の定義側にあると考え、再度調査しました。

./src/engraving/dom/drumset.cpp の中に noteHeads というドラムに関係しそうな定義があり、さらにその定義元をたどると note.cpp というファイルが見つかりました。この note.cpp が、各ドラム楽器の「ベースとなる符頭(どの記号を描画するか)」を具体的に定義しているファイルでした。

問題の原因は、私自身の実装ミスにありました。
cross-circle という定義は元々存在しなかったため、note.cpp にこの定義を新しく作成しました。その際、既存の xcircle の定義を参考にして(コピーして)作成したため、ベースとなる符頭として xcircle を指定する設定のままになっていたのです。

このため、note.cpp が(意図せず)xcircle を描き、tdraw.cpp が(意図して)さらに「〇」を描く、という二重描画になっていました。

この問題を解決するため、実装の方針として、note.cpp にはベースとなる「×」印(cross)を描画させ、tdraw.cpp には「〇」印だけを追加させる、という二段階の描画処理を(cross-circle 専用の処理として)組み込むことにしました。

そこで、note.cpp 内の cross-circle 用の定義を修正し、ベースとなる符頭は cross(クローズドハイハット)と同じ設定になるように書き直しました。

これにより、note.cpp が「×」を描画し、tdraw.cpp がその上に「〇」を追加描画するという、私たちが意図した通りの処理が実現でき、オープンハイハットのデフォルト表記を一般的なものに変更することに成功しました。「〇」を追加描画するという機能は次のようなコードで表現することができます。

        item->drawSymbol(ldata->cachedNoteheadSym.value(), painter);        
        // judge if we need to draw circle around cross notehead
        if (item->headGroup() == NoteHeadGroup::HEAD_CROSS_CIRCLE) {
            painter->save();
            painter->setPen(Pen(item->curColor(opt), 1.5, PenStyle::SolidLine));
            painter->setBrush(BrushStyle::NoBrush);            
            // calculate circle parameters
            double noteheadWidth = item->symWidth(ldata->cachedNoteheadSym.value());
            double circleRadius = noteheadWidth * 0.25; 
            double xOffset = noteheadWidth * 0.5;
            double yOffset = -circleRadius * 12.5;
            // draw circle above cross notehead if needed
            painter->drawEllipse(PointF(xOffset, yOffset), circleRadius, circleRadius);
            painter->restore();

現在のMuseScoreで実装されているオープンハイハットと今回実装したオープンハイハットを見比べると次のようになります。

現在のMuseScoreで実装されているオープンハイハット

今回実装したオープンハイハット

ロールの再生速度の変更機能

問題点:トレモロ記号の再生速度

小太鼓などの打楽器奏法である「ロール」は、楽譜上では特定の記号(ここでは「トレモロ」と呼びます)で表現されます。

しかし、MuseScore はこの記号を音楽的に厳密な定義(例:32分音符の連続)で再生してしまいます。これにより、特にテンポが遅い曲の場合、ロールが機械的で遅い音の連続に聞こえてしまい、非常に違和感がありました。

目標:再生速度を任意に設定する

そこで、このトレモロ記号で再生されるロールのスピードを、ユーザーが「プロパティ」インスペクタで自由に調整できる機能を追加することにしました。

調査1:プロパティ(GUI)側の特定

まず、プロパティ画面に設定項目を追加するため、トレモロ記号に関するデータを変更した際に呼び出される関数を gdb で調査しました。

デバッグの結果、InspectorListModel といった関数群が怪しいと判断し、そこから名前を推測しつつデバッグを続けることで、buildModelsForSelectedElements → createModelsBySectionType → ... → TremoloSettingsModel という呼び出し階層を経て、./src/inspector/notation/tremols/tremolosettingsmodel.cpp というファイルがトレモロの設定を扱っていると特定しました。

実装1:GUI への設定項目の追加

特定した tremolosettingsmodel.cpp に、「ロールのスピード」を定義するための構造体のようなものを追加しました。

このファイルでは32分音符以外のトレモロも定義されていたため、32分音符のロール(=今回問題にしているロール)の場合に限り、スピード調整の項目が表示されるように変更しました。次のようなコードで実装しています。


    SpinBoxPropertyView {
        id: rollSpeedSection
        titleText: qsTrc("inspector", "Roll speed (%)")
        propertyItem: root.model ? root.model.rollSpeed : null
        minValue: 1
        maxValue: 400
        step: 5

        navigationPanel: root.navigationPanel
        navigationRowStart: root.navigationRowStart + 3
    }

調査2:再生処理側の特定

しかし、これだけでは GUI 上で設定を定義しただけで、実際の再生音には反映されません。次に、音を流す処理を行っている部分を特定する必要がありました。

再生と停止を繰り返すことで呼び出し回数が増える関数を調査したところ、PlaybackModel::updateEvents という関数に当たりをつけました。「Playback」という名前や「updateEvents」という用語から、楽譜の変更を再生イベントに反映している部分だと仮説を立てました。

そこからさらに調査を進め、processSegment → render → ... → ChordArticulationsRenderer を経て、その中にある TremoloRenderer というクラスが、トレモロの実際の再生処理を担当していると特定しました。

実装2:再生への反映

特定した TremoloRenderer にも変更を加え、TremoloSettingsModel で定義した「ロールのスピード」の値を読み込み、それが実際の再生音に反映されるようにしました。

最後に、プロパティの GUI にスピードを調整するための UI(スライダーなど)を追加することで、目的の機能を達成しました。次のようなコードで実装しています。

   bool hasTwoChord = false;
    bool hasSingleR32 = false;
    
    for (EngravingItem* it : m_elementList) {
        if (it->type() == ElementType::TREMOLO_TWOCHORD) {
            hasTwoChord = true;
        } else if (it->type() == ElementType::TREMOLO_SINGLECHORD) {
            const auto sc = static_cast<TremoloSingleChord*>(it);
            if (sc->lines() == 3) {
                hasSingleR32 = true;
            }
        }
        if (hasTwoChord && hasSingleR32) break;
    }
    
    if (hasTwoChord && muse::contains(propertyIdSet, Pid::TREMOLO_STYLE)) {
        loadPropertyItem(m_style);
    }
    if (muse::contains(propertyIdSet, Pid::STEM_DIRECTION)) {
        loadPropertyItem(m_direction);
    }
    if (hasSingleR32 && muse::contains(propertyIdSet, Pid::TREMOLO_ROLL_SPEED_PERCENT)) {
        loadPropertyItem(m_rollSpeed);
    }
    
    if (m_rollSpeed) {
        m_rollSpeed->setIsVisible(hasSingleR32);
        m_rollSpeed->setIsEnabled(hasSingleR32);
    }

補足:音量調整機能の追加

上記と全く同様の手順(GUI 側の定義追加と、再生処理側への反映)を踏むことで、「ロールの音量調整」機能も併せて実装しました。

MuseScore上では次のような形で表示されました。

音の追加(Body Percussion とスネア)

今回の機能拡張の一環として、新しい音色の追加も試みました。
具体的には、以下の2つです。

  1. Body Percussion に「Thigh」(太もも)の音を追加する
  2. スネアに「Off」(リムショットやゴーストノートのようなニュアンス)の音を追加する

調査:独自の録音ファイルを使いたかったが…

当初は、自分で録音した音声ファイル(.wav や .mp3 など)を MuseScore に取り込み、新しい音として出力できるようにしたいと考えていました。

しかし、ソースコードを手探りで調査した結果、MuseScore は(少なくとも標準状態では)音声ファイルを直接再生しているわけではなく、「MIDI」という規格を使ってシンセサイザー(サウンドフォント)を鳴らしていることが判明しました。

実装:MIDI の音を工夫して対応

gdb などでデバッグを試みても、MIDI 関連の出力しか得られず、独自の音声ファイルを追加する場所を特定するのは非常に困難であると判断しました。

そこで方針を変更し、既存の MIDI の音色を工夫して(あるいは別の音色を割り当てるなどして)、擬似的に「Thigh」やスネアの「Off」に近い音を表現する形で機能を実装しました。

「Thigh」を実装するためのコードは次のようになっています。スネアのオフについても同様の操作をするとうまくいきました。

            <Instrument id="thigh">
                  <family>body-percussion</family>
                  <trackName>Thigh</trackName>
                  <longName>Thigh</longName>
                  <shortName>Thi.</shortName>
                  <description>Thigh slap.</description>
                  <clef>PERC</clef>
                  <stafftype staffTypePreset="perc1Line">percussion</stafftype>
                  <barlineSpan>1</barlineSpan>
                  <drumset>1</drumset>
                  <singleNoteDynamics>0</singleNoteDynamics>
                  <Drum pitch="45"> <!--Low Tom (snare-off)-->
                        <head>normal</head>
                        <line>0</line>
                        <voice>0</voice>
                        <name>Thigh</name>
                        <stem>1</stem>
                        <shortcut>A</shortcut>
                        <panelRow>0</panelRow>
                        <panelColumn>0</panelColumn>
                  </Drum>
                  <Channel>
                        <!--MIDI: Bank 128, Prog 48; MS General: Orchestra Kit (closer to snare snares-off) -->
                        <controller ctrl="0" value="1"/> <!--Bank MSB-->
                        <program value="48"/> <!--Orchestra Kit-->
                  </Channel>
                  <genre>common</genre>
                  <genre>popular</genre>
                  <genre>choral</genre>
                  <genre>classroom</genre>
            </Instrument>

MuseScore上では次のように表示されました。

Discussion