Unityでマイク入力からピッチ検出をするまで

HoloLensのマイクで拾った音声からピッチを検出して表示したかったので、やり方を調べてみました。

1. マイクから音声入力して音声データを取得する
2. 音声データの周波数成分を解析する
3. 周波数成分の中から最も大きくなっている周波数を計算して代表値とする
4. 代表値の周波数から音名（C,D,E or ド,レ,ミ）を計算する

計算した結果を画面に表示するのはまた別の話になるので今回は割愛します。

## マイクから音声入力して音声データを取得する

Unityでマイクからの音声を扱うためには[Microphone](https://docs.unity3d.com/ja/540/ScriptReference/Microphone.html)クラスを使います。これを使うとマイクからの音声入力を[AudioClip](https://docs.unity3d.com/ja/540/ScriptReference/AudioClip.html)として扱うことができ、このAudioClipを[AudioSource](https://docs.unity3d.com/jp/540/ScriptReference/AudioSource.html)に渡すことでマイクからの音声入力をUnityシーン上で再生することができます。
シーン上に空のGameObjectを作成してAudioSourceコンポーネントと以下のカスタムコンポーネントをアタッチするだけで、マイクからの音声入力がシーン上の音源として再生されます。もちろん、GameObjectを動かすことで音源の位置を変えることもできます。

```csharp
using UnityEngine;
using System.Collections;

public class ExampleClass : MonoBehaviour {
    void Start() {
        AudioSource aud = GetComponent<AudioSource>();
        // マイク名、ループするかどうか、AudioClipの秒数、サンプリングレート を指定する
        aud.clip = Microphone.Start(null, true, 10, 44100);
        aud.Play();
    }
}
```

再生開始するためにはAudioSourceコンポーネントの方もPlayする必要があります。エディター上で*Play On Awake*にチェックが入っていれば、シーンの開始と共に勝手に再生開始するので何もする必要はありません。
AudioSourceコンポーネントにLoopのチェックが入っていない場合、`Microphone.Start`の第三引数で指定した秒数分しか再生されません。Loopにチェックを入れると延々と再生することができます。


## 音声データの周波数成分を解析する

次に、マイクの音声データの周波数成分を解析します。周波数成分の解析をすると、マイクの音のなかに100Hzの音がこれくらい、200Hzの音がこれくらい、300Hzの音がこれくらい、……というのがわかります。詳しくはFFTとか調べてください。

周波数成分の解析をするためのメソッドが[AudioSource.GetSpectrumData](https://docs.unity3d.com/ScriptReference/AudioSource.GetSpectrumData.html)です。これを呼び出すだけで、AudioSourceが今持っている音声データの周波数成分を取得できます。

```csharp
void Update()
{
    float[] spectrum = new float[256];
    AudioListener.GetSpectrumData(spectrum, 0, FFTWindow.Rectangular);
}    
```

`spectrum`に周波数成分の値が入ります。`spectrum`は**2のべき乗の長さ(256,512,1024,2048,4096,8192等)の配列でなければいけないことに注意してください。**
spectrumには低い周波数成分から高い周波数成分までが順番に入っています。具体的には、音声出力のサンプリングレートをF,`spectrum`の長さをQとすると**`spectrum[N]`には`N * F/2 / Q`Hzの周波数成分が含まれています。**要は最も低い周波数(0Hz)から最も高い周波数(F/2Hz)までをQ等分した各周波数成分の値が配列の各要素になるのです。**なんでF/2なの？という方は[ナイキスト周波数](https://ja.wikipedia.org/wiki/ナイキスト周波数)を調べてください。

周波数の最大値と最小値は決まっていてその間を等分するわけですから、`spectrum`の配列の長さを長くする方が周波数成分の解析精度が上がることになります。音声出力のサンプリングレートが48000Hzで`GetSpectrumData`に渡す配列の長さが1024だとすると、結果の各周波数成分は23.4Hzごとに取得されます。2048の配列では11.7Hzです。

参考: [Unity forum - Audio Analysis: isolating frequency values](http://answers.unity3d.com/questions/175173/audio-analysis-pass-filter.html)


## 周波数成分の中から最も大きくなっている周波数を計算して代表値とする

周波数成分が取得できてもそれだけではピッチはわかりません。ピッチの調べ方はいろいろあると思いますが、最も簡単なのは**周波数成分の中で最も大きいところを見つける**ことです。

```csharp
var maxIndex = 0;
var maxValue = 0.0f;
for (int i = 0; i < spectrum.Length; i++)
{
  var val = spectrum[i];
  if (val > maxValue)
  {
    maxValue = val;
    maxIndex = i;
  }
}
// maxValue が最も大きい周波数成分の値で、
// maxIndex がそのインデックス。欲しいのはこっち。
```

これで欲しいspectrumのインデックス番号がわかりました。このインデックス番号が実際に何Hzの周波数に対応しているのかは、先程の説明をもう一度読めばわかります。

    音声出力のサンプリングレートをF,`spectrum`の長さをQとすると**`spectrum[N]`には`N * F/2 / Q`Hzの周波数成分が含まれています。

したがって、周波数の計算は以下のようになります。

```csharp
var freq = maxIndex * AudioSettings.outputSampleRate / 2 / spectrum.Length;
```


## 代表値の周波数から音名（C,D,E or ド,レ,ミ）を計算する

周波数が計算できたので、最後にこれを音名に変換します。周波数と音名の対応は[MIDI tuning standard](https://en.wikipedia.org/wiki/MIDI_tuning_standard)によると以下のようにして計算できます。

```csharp
public static int CalculateNoteNumberFromFrequency(float freq)
{
  return Mathf.FloorToInt(69 + 12 * Mathf.Log(freq / 440, 2));
}
```

ここで計算しているのは440Hzのラ(A4)を69に割り当てた時の各音名の番号です。各番号がどの音に対応しているのかは[こちらのサイト](http://www.asahi-net.or.jp/~HB9T-KTD/music/Japan/Research/DTM/freq_map.html)にまとめられています。

ここまで計算できればあとはこの音名番号をどのように表示するかを考えるだけです。
私の場合は「ドレミ」という音名で表示したかったので、次のようにして音名を文字列に変換しました。

```csharp
public class NoteNameDetector
{
  private string[] noteNames = { "ド", "ド♯", "レ", "レ♯", "ミ", "ファ", "ファ♯", "ソ", "ソ♯", "ラ", "ラ♯", "シ" };

  public string GetNoteName(float freq)
  {
    // 周波数からMIDIノートナンバーを計算
    var noteNumber = calculateNoteNumberFromFrequency(freq);
    // 0:C - 11:B に収める
    var note = noteNumber % 12;
    // 0:C～11:Bに該当する音名を返す
    return noteNames[note];
  }

  // See https://en.wikipedia.org/wiki/MIDI_tuning_standard
  private int calculateNoteNumberFromFrequency(float freq)
  {
    return Mathf.FloorToInt(69 + 12 * Mathf.Log(freq / 440, 2));
  }
}
```


## 作例

ここまでのものをベースにして、あとはいろいろと最適化したり工夫した末にできたのがこちらです。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">誰でも絶対音感持った人の気分が味わえるアプリを作りました。だいぶ精度が甘いけど。 <a href="https://t.co/FJ3NcKI22T">pic.twitter.com/FJ3NcKI22T</a></p>&mdash; niu（にぅ） (@niusounds) <a href="https://twitter.com/niusounds/status/887008677260963840">2017年7月17日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>