HoloLensのマイクで拾った音声からピッチを検出して表示したかったので、やり方を調べてみました。
- マイクから音声入力して音声データを取得する
- 音声データの周波数成分を解析する
- 周波数成分の中から最も大きくなっている周波数を計算して代表値とする
- 代表値の周波数から音名(C,D,E or ド,レ,ミ)を計算する
計算した結果を画面に表示するのはまた別の話になるので今回は割愛します。
マイクから音声入力して音声データを取得する
Unityでマイクからの音声を扱うためにはMicrophoneクラスを使います。これを使うとマイクからの音声入力をAudioClipとして扱うことができ、このAudioClipをAudioSourceに渡すことでマイクからの音声入力をUnityシーン上で再生することができます。
シーン上に空のGameObjectを作成してAudioSourceコンポーネントと以下のカスタムコンポーネントをアタッチするだけで、マイクからの音声入力がシーン上の音源として再生されます。もちろん、GameObjectを動かすことで音源の位置を変えることもできます。
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です。これを呼び出すだけで、AudioSourceが今持っている音声データの周波数成分を取得できます。
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なの?という方はナイキスト周波数を調べてください。
周波数の最大値と最小値は決まっていてその間を等分するわけですから、spectrum
の配列の長さを長くする方が周波数成分の解析精度が上がることになります。音声出力のサンプリングレートが48000HzでGetSpectrumData
に渡す配列の長さが1024だとすると、結果の各周波数成分は23.4Hzごとに取得されます。2048の配列では11.7Hzです。
参考: Unity forum - Audio Analysis: isolating frequency values
周波数成分の中から最も大きくなっている周波数を計算して代表値とする
周波数成分が取得できてもそれだけではピッチはわかりません。ピッチの調べ方はいろいろあると思いますが、最も簡単なのは周波数成分の中で最も大きいところを見つけることです。
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の周波数成分が含まれています。
したがって、周波数の計算は以下のようになります。
var freq = maxIndex * AudioSettings.outputSampleRate / 2 / spectrum.Length;
代表値の周波数から音名(C,D,E or ド,レ,ミ)を計算する
周波数が計算できたので、最後にこれを音名に変換します。周波数と音名の対応はMIDI tuning standardによると以下のようにして計算できます。
public static int CalculateNoteNumberFromFrequency(float freq)
{
return Mathf.FloorToInt(69 + 12 * Mathf.Log(freq / 440, 2));
}
ここで計算しているのは440Hzのラ(A4)を69に割り当てた時の各音名の番号です。各番号がどの音に対応しているのかはこちらのサイトにまとめられています。
ここまで計算できればあとはこの音名番号をどのように表示するかを考えるだけです。
私の場合は「ドレミ」という音名で表示したかったので、次のようにして音名を文字列に変換しました。
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));
}
}
作例
ここまでのものをベースにして、あとはいろいろと最適化したり工夫した末にできたのがこちらです。
誰でも絶対音感持った人の気分が味わえるアプリを作りました。だいぶ精度が甘いけど。 pic.twitter.com/FJ3NcKI22T
— niu(にぅ) (@niusounds) 2017年7月17日