【Unity】Microsoft GS Wavetable SW Synth で音を出したい！

#  はじめに

この記事は [法政大学情報科学部 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/cis_hosei) 15 日目の記事です。

こんにちは、情報科学部の waigoma です。気がついたらもう 12 月。1 年間あっという間でしたね。  
今年はついに登校が当たり前になって、やっと1年生の気分でしたw。

そんなこんなで、今回は  
「Unity で Midi キーボードを使って、 Microsoft GS Wavetable SW Synth (MSGS) を通してリアルタイムで音を出したい」  
という、少しコアな話題 (？) について書いていこうと思う。  

今回作成したプロジェクトは、この `Github` にあげておく。

https://github.com/waigoma/TutorialUnityMidi

# 目的
Midi キーボードを使用する音ゲーを作ろうとしたときに、入力された鍵盤の音を出したいとなった。
その時、Audio Clip を再生、停止すれば、リアルタイムで音が鳴っているように聞こえるだろうが、  
鍵盤全てにやろうとすれば `88 鍵盤 x 音色数 x 音の大きさ` の手間がかかるので限りなく不可能に近い。(容量的にも)

なので、Windows に標準で用意されているソフトシンセ MSGS を使って音を出そうとしたわけである。  
これは、Midi 信号を送ればそれに対応した音を出してくれるものだと思っておけば良い。  

なぜわざわざ書こうかと思ったかと言えば、文献が少なくこの結論に至るまで時間がかかったためである。  
ここでは、自分的失敗を交えて説明していこうと思っている。  
単語単語の意味は説明を省く部分があると思うので、各自調べてもらえると嬉しい。

# 環境
- Unity 2021.3.15f1
- Rider 2022.3
- RtMidi 1.0.4
- FANTOM-06 (Midi キーボードとして使用)

今回は、Midi を取り扱うために RtMidi というラッパーライブラリをお借りする。

https://github.com/keijiro/jp.keijiro.rtmidi

# RtMidi を依存関係に追加する
Unity のプロジェクトフォルダから、`Packages/manifest.json` を開く。  
こんな感じになるように、記述を追加する。
```json
{
  "scopedRegistries": [
    {
      "name": "Keijiro",
      "url": "https://registry.npmjs.com",
      "scopes": [ "jp.keijiro" ]
    }
  ],
  "dependencies": {
    "jp.keijiro.rtmidi": "1.0.4",
    (最初からあるやつ)... 
  }
}
```
[全文はこんな感じ](https://github.com/waigoma/TutorialUnityMidi/blob/master/Packages/manifest.json)
Unity 内の Packages フォルダに RtMidi が追加されていれば成功！

# RtMidi のラッパーを作る
今回使用させていただく、[RtMidi](https://github.com/keijiro/jp.keijiro.rtmidi) は、生のドライバーをそのまま提供しているような感じなので、これのラッパーをさらに自分で作成する。  
作成する方法は、[GitHub/Assets](https://github.com/keijiro/jp.keijiro.rtmidi/tree/master/Assets) に用意されている例を使用する。

## Allow 'unsafe' Code
ラッパーを作るにあたって、unsafe なクラスを作成する必要がある。  
そのため、Unity 上で unsafe なクラスを作れるように `Project Settings > Player` の設定を変更する。

`Project Settings > Player` の `Windows, Mac, Linux settings` タブの、  
`Other Settings` を開き、`Script Complilation` の欄の、`Allow 'unsafe' Code` にチェックを入れる。

画面右上の検索欄に `Allow unsafe Code` と入れると探しやすいと思う。
画像で見るとこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348896/0358d963-8a70-59eb-3d7a-030da6bd66da.png)

ここまでくれば、後はクラスを作成していく。

### 設定したのに Rider で赤波線が出る人向け
- そのファイルを消して、もう一度作成し直す。
- Unity の画面へ行き、もう一度コンパイルする。
自分の場合はこれでなんとかなった。

## スクリプト作成
全てのスクリプトを出すと長くなってしまうので、詳しいコードは [私のGitHub](https://github.com/waigoma/TutorialUnityMidi/tree/master/Assets/TutorialUnityMidi/Scripts) を見て欲しい。  
基本的に先ほど挙げた、[RtMidiの例](https://github.com/keijiro/jp.keijiro.rtmidi/tree/master/Assets) を参考にした。  

これに加えて、入力されたものを出力にそのまま流せるように、
`MidiInTest` と `MidiOutTest` を合体させた、`MidiInOutTest` を作成した。

`ScanPorts` メソッドの `_inPorts` を追加するタイミングで、`_outPorts` の指定ポートに信号を送る形である。
ポートの指定は、`[Serialize Field]` で指定できるようにしてみた。
今回は音を出すだけが目的なので、Out ポートはスキャン時に出力される番号で判断する。

一応それぞれのメソッドが何をしているかだけ書き残しておく。
```MidiInOutTest.cs
 // Midi 信号を Out するポート指定用
[SerializeField] private int outPort;

// Midi In Out を使うための変数
private MidiProbe _inProbe;
private MidiProbe _outProbe;
private readonly List<MidiInPort> _inPorts = new ();
private readonly List<MidiOutPort> _outPorts = new ();

// Unity で実行したとき、Start より前に Midi が使えるように変数に格納しておく
private void Awake() 
{
    ...
}

// 接続されている Midi デバイスをすべて取得し、Out ポートのサウンドを全て Off にする
private void Start()
{
    ...
}

// Midi ポートに変化があれば、デバイスの取得を再度行う。
// Midi In ポートにたまっている処理を行う。
private void Update()
{
    ...
}

// Unity を終えたときに、Midi を使用するために生成したインスタンスを破棄する
private void OnDestroy()
{
    ...
}

// そのポートが実在するかどうかを判定する
private bool IsRealPort(string nm) => !nm.Contains("Through") && !nm.Contains("RtMidi");

// 接続されている Midi デバイスをすべて取得する
private void ScanPorts()
{
    for (var i = 0; i < _outProbe.PortCount; i++)
    {
        var nm = _outProbe.GetPortName(i);
        // Out ポートの番号とデバイス名を出力
        Debug.Log($"MIDI-out No.{i} port found: " + nm);
        _outPorts.Add(IsRealPort(nm) ? new MidiOutPort(i) : null);
    }
    
    for (var i = 0; i < _inProbe.PortCount; i++)
    {
        var nm = _inProbe.GetPortName(i);
        // In ポートの番号とデバイス名を出力
        Debug.Log($"MIDI-in No.{i} port found: " + nm);

        _inPorts.Add(IsRealPort(nm) ? new MidiInPort(i)
            {
                OnNoteOn = (channel, note, velocity) =>
                {
                    Debug.Log($"{nm} [{channel}] On {note} ({velocity})");
                    // Out ポートに NoteOn 信号を送る
                    _outPorts[outPort]?.SendNoteOn(channel, note, velocity);
                },

                OnNoteOff = (channel, note) =>
                {
                    Debug.Log($"{nm} [{channel}] Off {note})");
                    // Out ポートに NoteOff 信号を送る
                    _outPorts[outPort]?.SendNoteOff(channel, note);
                },

                OnControlChange = (channel, number, value) =>
                    Debug.Log($"{nm} [{channel}] CC {number} ({value})")
            } : null
        );
    }
}

// 取得していたデバイスを全て破棄する
private void DisposePorts()
{
    ...
}
```

# Unity
ここからは、Unity の話。
## 失敗談
先に失敗談？から話す。(ちょっと脱線します)
実際書いてみるとかなり遠回りしてるなぁと思ってしまった、、、（笑）

### Unity で音を鳴らす方法
Unity で音を鳴らす方法と言えば、`Audio Source` をオブジェクトにアタッチして、  
`Audio Clip` を再生/停止、または一回だけ再生する方法が普通だと思う。
しかし、リアルタイムで再生/停止？どうすればできるんだ？となると思う。

### C# で使えるソフトシンセ
最初に話したように全ての音声ファイルを用意すればできるかもしれないがあまりにも無謀。  
でも、Unity では `Audio Source` 通さないと音だせないよな、、、
そこで調べ付いた先が、[MeltySynth](https://github.com/sinshu/meltysynth) だった。

これは、SoundFont synthesizer と言って、音のフォントを使って音を生成するソフトシンセである。
`Audio Source` は、`float[]` の波形データなら再生できるので、試しに使ってみた。
しかし、音があまりきれいではない上にメモリ使用量もかなりやばかった。
しかも、波形を生成してるので動作ももたついていた。 

### Microsoft GS Wavetable SW Synth で波形生成してみる
Windows には標準で Microsoft GS Wavetable SW Synth というソフトシンセが入っている。
MeltySynth じゃなくて、これ使えばできるのでは？

鳴らす方法はあっても、波形生成する記事が何もない。
詰んだ。とここで私は思っていた。

### じゃあどう鳴らすん？
Unity で音を鳴らす方法と言えば、`Audio Source` だけど、、
他の Midi 再生するのはどうしてるんだろう？

ということで、[Domino](https://takabosoft.com/domino) という Midi が再生できるソフトの環境設定を覗いてみた。
そこには、MIDI OUT の項目があり、Microsoft GS Wavetable SW Synth と書いてあった。
Unity で Microsoft GS Wavetable SW Synth 通せば音なるのでは？という結論に至った。

## Midi Out で鳴らしちゃう
ここから、本題に戻る。先ほど作成したスクリプトを使って音が鳴るか試してみる。
Midi のシーンに空のオブジェクトを追加。そのオブジェクトに、`MidiInOutTest` をアタッチする。
そして実行してみると、、、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348896/e1bf654a-ad26-c09a-5c50-5de46e659259.png)
やった！ MIDI-OUT と MIDI-IN のデバイスが一覧で表示できた！
しかも、MIDI-OUT の 0 番ポートに Microsoft GS Wavetable SW Synth ある！

`Out Port` が 0 になっているのを確認して、Midi キーボードから信号を送ってみると、、、
鳴ったぞ！！！！ (∩´∀`)∩わ～い♪

そんなこんなで、Microsoft GS Wavetable SW Synth で音を出すことに成功した。

一応動画。

<iframe width="560" height="315" src="https://www.youtube.com/embed/aY9Km185nf8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 未解決だけど問題点
音が鳴った！これはかなりうれしい。が、音が出るまで結構遅延してるっぽい...
解決策は今のところ見つかっていないので、何とも言えないが、

Microsoft GS Wavetable SW Synth ではなく、Midi キーボードに Midi Out すると遅延がほぼない。
Windows 以外だとここまで遅延しない... らしい。

ので、もしかしたら Windows？Microsoft GS Wavetable SW Synth？が遅延しやすいのかもしれない。

# 最後に
ここまでたどり着いてくださった方々。ここまで読んでくださってありがとうございます。  
多分コアな話題だったんじゃないかなとは思うが、いかがだったでしょうか。

今年もなんとか advent calender に参加できてよかった。
また気分で他にも記事を書くかもしれないけれど、その時はまたよろしくお願いする。

ここまでお付き合いいただき、ありがとうございました！
それでは良き Unity ライフを！

# 参考文献
・[RtMidi for Unity](https://github.com/keijiro/jp.keijiro.rtmidi)
・[MeltySynth](https://github.com/sinshu/meltysynth)
・[Unity Audio Source](https://docs.unity3d.com/ja/2020.3/ScriptReference/AudioSource.html)