AudioSourceの簡単なサンプル - マイク入力をメータにして表示する
About
ここではKinectのマイクから入る音情報をオーディオストリームから取得し、その音量(大きさ)を算出してメータとして表示する方法について解説します。簡単のため音量としていますが正確には音圧で、デシベル(dB)を算出します。2013年10月現在最新の公式サンプルではBitmapに波形を描画していますが、処理内容が非常にわかりにくいです。ここで紹介する内容は、公式のサンプルの読解を助ける意味でも役に立つと思います。
ここで扱うサンプルではKinectから取得できるオーディオストリームを用いて処理を行っていますが、オーディオストリームから得られるデータの処理については一般的な音声情報処理とまったく同じですので、必要に応じてWebに公開されているサンプルなどを参照するのも良いと思います。ソースコードに限らず様々な計算方法を取り入れることもできるでしょう。※正直に言うと数学・物理が苦手なので自信がない。
サンプルプログラムの実行結果
目指すところはWindowsのマイクミキサーに表示されるメータと同じような挙動をするメータなのですが、ここで紹介するサンプルプログラムは大体同じような挙動をしてくれます。
オーディオの仕様
Kinectから得られる音声データの仕様を確認しておく必要があります。Kinect第1世代から得られる音声データの仕様は [16KHz、16bit、モノラル] です。つまり対象の音声情報は、1秒あたりに16000のデータから構成され、データ1つ当たりが16bitで、モノラルである、ということになります。モノラルとステレオについてはここでは解説しませんが、端的にステレオならばデータ数が2倍になる、ということです。
- Kinect for Windows Sensor Components and Specifications - msdn
- MSDNで公開されているKinectの公式資料です。
2013年10月現在公式のドキュメントではマイクの入力は24bitと記されています。一方で同時期最新のSDKとそれに付属する公式のサンプルでは16bitとして処理されている点に注意してください。どちらが正しいか確認していないので定かではないですが16bitで処理できるハズです。
オーディオストリームからのデータの取得
サンプルの解説に入ります。まずオーディオストリームからデータを獲得する準備をします。まずオーディオストリームからどれだけの情報を取得するのかを決定し、それに応じたバッファ領域を用意します。先に開設した通り、Kinect第一世代では16KHz、16bit(=2byte)のデータが送られてきますから、まずはこれを定数として定義しておきます。ここでは変数SamplesPerMsecとBytesPerSampleとして定義しています。
次いで音声を取得する時間を定義すると、取得する音声データのバッファサイズが算出できます。サンプルでは50に設定しました。単位はmsecです。1sec = 1000msec ですから、100を設定するときに0.1秒、50を設定するときに0.05秒間の音声データを取得することになります。
これで取得する音声データの(バッファ)サイズが求められるようになりました。サイズは、[取得する時間(msec) * 時間あたりのサンプリング数 * 単位当たりのデータ大きさ]で求まります。
/// <summary> /// 1msecあたりに取り入れるデータの数(サンプリング数)です。 /// Kinectでは1秒あたり16000のデータが入るため、16で0.1秒分のデータになります。 /// </summary> const int SamplesPerMsec = 16; /// <summary> /// オーディオストリームから取得されるデータの単位当たりのbyte数です。 /// Kinectのオーディオストリームから取得されるデータは単位あたり2byteです。 /// </summary> const int BytesPerSample = 2; /// <summary> /// 単位処理あたりに何ミリ秒分のデータを処理するかを指定します。 /// 1秒(sec)は1000ミリ秒(msec)です。100を指定するとき、0.1秒分のデータを処理します。 /// </summary> const int AudioPollingInterval = 50; /// <summary> /// オーディオストリームから取得したデータのバッファです。 /// </summary> readonly byte[] audioBuffer = new byte[AudioPollingInterval * SamplesPerMillisecond * BytesPerSample];
オーディオストリームの開始とスレッドの開始
準備ができたのでKinectからオーディオストリームを受取り、定期的にデータを参照してUIを更新します。ここではスレッド(Threadクラス)を用いて定期的な処理を実現します。
Kinect.AudioSource.Startメソッドは音の発生を検知したりする機能を有効にするために利用しますが、マイクから得られるデータのストリームを返すためにも利用します。このストリームへの参照は、スレッド内での処理で必要になるので維持しておきます。
ここでストリームから定期的にデータを受取り、処理を行うためのスレッド(audioReadingThread)を用意しています。audioReadingメソッドを定期実行するように設定していますが、このメソッドの内容については後に続く項目で解説します。
/// <summary> /// 対象となるKinectセンサーへの参照です。 /// </summary> KinectSensor kinect; /// <summary> /// オーディオ入力のストリームです。 /// </summary> Stream audioStream; /// <summary> /// オーディオ入力を処理するスレッドです。 /// </summary> Thread audioReadingThread; … public MainWindow() { InitializeComponent(); this.kinect = KinectSensor.KinectSensors[0]; this.kinect.Start(); this.kinect.AudioSource.SoundSourceAngleChanged += AudioSource_SoundSourceAngleChanged; this.audioStream = this.kinect.AudioSource.Start(); //音量の判定を行うスレッドを開始する。 this.audioIsReading = true; this.audioReadingThread = new Thread(AudioReading); this.audioReadingThread.Start(); }
オーディオストリームの開始とスレッドの開始
まずbool型の変数audioIsReadingが真の間は処理を繰り返すようにしていますが、audioIsReadingは全ての処理を終えるときにfalseにして、スレッドを終了させるために利用します。
ストリームからデータを読み込む方法は、音声データに限らず、例えばネットワーク越しにデータを受け取ったりテキストを受け渡すためのストリームの処理と変わりません。Stream.Readメソッドを利用して、用意したバッファにバッファの大きさだけデータを読み込みます。後は読み込んだデータを走査して処理するだけです。ここで注意しなければならないのは、Kinectのオーディオストリームから受け取るデータは16bit(≒2byte)で1つのデータとなる点だけです。後は受け取ったデータを自由に活用します。
音声処理の内容については簡単に解説します(詳細について分からないし、長くなるので)。まず音声データは一般に波形ですので正負の値がある点に注意します。さらにここで入力されてくるデータは2byteですから、最大でも32,767(=short型の最大値)までの値しか取りません。これを元に矩形の大きさなどを制御すると、伸び縮みするメータを実装することができます。得られた値の絶対値をとって、32,767で除算して0~1の値に正規化します。値が1のときにゲージが最大、0のときに最小となる様な処理を施せばメータが完成します。後に続く処理は後述します。
このサンプルの値の変化はWindowsのマイク入力の変化とほとんど同じような挙動を見せますが、値の変動が極端であるため、WindowsのUIと同じように滑らかな処理を実現するには多少の工夫が必要だと思います。
private void AudioReading() { while (audioIsReading) { //累積二乗和です。 double accumulatedSquareSum = 0; //ストリームから用意したバッファにデータを読み込みます。 int readCount = audioStream.Read(audioBuffer, 0, audioBuffer.Length); //読み込んだバッファを処理します。 //2byteで1組となるデータを読み込んで、累積二乗和を算出します。 for (int i = 0; i < readCount; i += BytesPerSample) { short audioSample = BitConverter.ToInt16(audioBuffer, i); double tempVolume = Math.Abs((double)audioSample / short.MaxValue); //メータのUIを更新します。 this.Dispatcher.Invoke(new Action(() => { this.Rectangle_VolumeMeter.Width = tempVolume * this.Grid_VolumeMeter.ActualWidth; })); accumulatedSquareSum += audioSample * audioSample; } //平均平方を求めます。 double meanSquare = accumulatedSquareSum / (readCount / 2); //二乗平均平方(RMS = √ meanSquare)を求めます。 //一般的に音処理においては入出力信号レベルを示します。 double rms = Math.Sqrt(meanSquare); //デシベル(dB)を求めます。デシベルはパスカル(Pa)で示される音圧を、 //分かりやすさのために常用対数(Log10)で表したものです。 double decibel = 20 * Math.Log10(rms); //UIを更新します。 this.Dispatcher.Invoke(new Action(() => { this.Label_Decibel.Content = "dB : " + decibel; })); } }
RMSとデシベル(dB)
残りの波形処理については正確な情報であることを確信できないので参考にしてください。RMS(root mean square)は一般に、波形から入出力信号の大きさを算出するために利用されます。これは実効値とも呼ばれます。"mean square"は平方平均(平均平方)でその√(root)をとるのでRMSです。まずある区間から得られる各データ(=サンプル)の値を2乗して足していきます(累積二乗和)。その後にデータ数で割ると"mean squre"が求まり、あとは平方根をとるだけです。
RMSだけで入力信号の大きさは取れるのですが、いくつかの情報を元にデシベル(dB)単位にしています。20*Log10(RMS)とすることで変換します(※果たしてこれが音の大きさを示す意味で正しい変換かどうかは定かではありません)。大体40~60dBで一般的な会話の範囲になるようですが、一応その範囲には収まります。ただしマイクの入力レベルの設定に影響をうけますので、注意してください。何も話さない状態で20dB程度の値が出ているとき、発声すると70dBを超える値を示しますが、20dB分だけ引けば50dB程度に収まりますね。公式のサンプルでも、一定値以下の値をノイズとして扱う、といった処理が含まれています。これらの値に関連するような処理が必要であれば、工夫して実装することになります。