ブログ@kaorun55

HoloLensやKinectなどのDepthセンサーを中心に書いています。

OpenNI で音声データを扱う( C# + WPF ) #openni_ac

このエントリはOpenNI Advent Calendar 2011 : ATNDの12月11日分です!!
Advent Calendarでの、僕の全プロジェクトはこちらです


Xtionについたマイクから入力された音を、PCのスピーカーから出力してみましょう
Xtionから入力される音声データはWAVE形式なのですが、C#でWAVEデータをストリーミング出力する方法がわからなかったので、C++でやった方法を使っています。CのAPIC++/CLIでラップしC#で使えるようにしています。

StreamingWavePlayer

// StreamingWavePlayer.h

#pragma once

#include <Windows.h>
#include <mmsystem.h>
#pragma comment( lib, "winmm.lib" )

using namespace System;
using namespace System::Runtime::InteropServices;

namespace Win32 {

    public ref class StreamingWavePlayer
    {
    public:

        StreamingWavePlayer( int nSampleRate, int nBitsPerSample, Byte nChannels, int audioBufferCount )
        {
            audioBufferCount_ = audioBufferCount;
            audioBufferNextIndex_ = 0;

            // WAVEデータの設定
            WAVEFORMATEX wf;
            wf.wFormatTag = 0x0001; // PCM
            wf.nChannels = nChannels;
            wf.nSamplesPerSec = nSampleRate;
            wf.wBitsPerSample = nBitsPerSample;
            wf.nBlockAlign = wf.wBitsPerSample * wf.nChannels / 8;
            wf.nAvgBytesPerSec = wf.nBlockAlign * wf.nSamplesPerSec;

            handle_ = new HWAVEOUT;
            MMRESULT mmRes = ::waveOutOpen( handle_, WAVE_MAPPER, &wf, NULL, NULL, CALLBACK_NULL );
            if ( mmRes != MMSYSERR_NOERROR ) {
                throw gcnew Exception( "waveOutOpen failed\n" );
            }

            // 音声データ用のバッファの作成と初期化
            waveHeaders_ = new WAVEHDR[audioBufferCount_];
            memset( &waveHeaders_[0], 0, sizeof(WAVEHDR) * audioBufferCount_ );

            for ( int i = 0; i < audioBufferCount_; ++i ) {
                waveHeaders_[i].lpData = new char[MAX_BUFFER_SIZE];
                waveHeaders_[i].dwUser = i;
                waveHeaders_[i].dwFlags = WHDR_DONE;
            }
        }

        !StreamingWavePlayer()
        {
            ::waveOutPause( *handle_ );

            for ( int i = 0; i < audioBufferCount_; ++i ) {
                delete[] waveHeaders_[i].lpData;
            }

            delete waveHeaders_;
            delete handle_;
        }

        ~StreamingWavePlayer()
        {
            this->!StreamingWavePlayer();
        }

        void Output( IntPtr buffer, int length )
        {
            // バッファの取得
            WAVEHDR* pHeader = &waveHeaders_[audioBufferNextIndex_];
            if ( (pHeader->dwFlags & WHDR_DONE) == 0 ) {
                return;
            }

            // WAVEデータの取得
            pHeader->dwBufferLength = length;
            pHeader->dwFlags = 0;
            CopyMemory( pHeader->lpData, (void*)buffer, length );

            Output( pHeader );
        }

        void Output( array<Byte>^ buffer )
        {
            WAVEHDR* pHeader = GetBuffer();
            if ( pHeader == 0 ) {
                return;
            }

            // WAVEデータの取得
            pHeader->dwBufferLength = buffer->Length;
            pHeader->dwFlags = 0;
            Marshal::Copy( buffer, 0, (IntPtr)pHeader->lpData, buffer->Length );

            Output( pHeader );
        }

    private:

        WAVEHDR* GetBuffer()
        {
            // バッファの取得
            WAVEHDR* pHeader = &waveHeaders_[audioBufferNextIndex_];
            if ( (pHeader->dwFlags & WHDR_DONE) == 0 ) {
                return 0;
            }

            // WAVEヘッダのクリーンアップ
            MMRESULT mmRes = ::waveOutUnprepareHeader( *handle_, pHeader, sizeof(WAVEHDR) );
            if ( mmRes != MMSYSERR_NOERROR ) {
                return 0;
            }

            return pHeader;
        }

        void Output( WAVEHDR* pHeader ) {
            // WAVEヘッダの初期化
            MMRESULT mmRes = ::waveOutPrepareHeader( *handle_, pHeader, sizeof(WAVEHDR) );
            if ( mmRes != MMSYSERR_NOERROR ) {
                return;
            }

            // WAVEデータを出力キューに入れる
            mmRes = ::waveOutWrite( *handle_, pHeader, sizeof(WAVEHDR) );
            if ( mmRes != MMSYSERR_NOERROR ) {
                return;
            }

            // 次のバッファインデックス
            audioBufferNextIndex_ = (audioBufferNextIndex_ + 1) % audioBufferCount_;
        }

    private:

        static const int MAX_BUFFER_SIZE = 2 * 1024 * 1024;

        HWAVEOUT* handle_;
        WAVEHDR* waveHeaders_;

        int audioBufferCount_;
        int audioBufferNextIndex_;
    };
}
概要

C++でやってたことを、そのままC++/CLIに書き直しました。
OpenNIに依存しないようにしながらも、AudioGneratorがIntPtrでバッファを返すので、出力方法を2種類用意しています

// IntPtrから出力する
void Output( IntPtr buffer, int length )
// マネージドのバイト列から出力する
void Output( array<Byte>^ buffer )

使う側

見た目はいつもどおりなので割愛します

using System;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using OpenNI;
using Win32;

namespace AudioWPF
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        Context context;
        ImageGenerator image;
        AudioGenerator audio;

        StreamingWavePlayer wavePlayer;

        private Thread readerThread;
        private bool shouldRun;

        public MainWindow()
        {
            InitializeComponent();

            try {
                // ContextとImageGeneratorの作成
                ScriptNode node;
                context = Context.CreateFromXmlFile( "../../SamplesConfig.xml", out node );
                context.GlobalMirror = false;
                image = context.FindExistingNode( NodeType.Image ) as ImageGenerator;
                audio = context.FindExistingNode( NodeType.Audio ) as AudioGenerator;

                wavePlayer = new StreamingWavePlayer( audio.WaveOutputMode.SampleRate,
                    audio.WaveOutputMode.BitsPerSample, audio.WaveOutputMode.Channels, 100 );

                // 画像更新のためのスレッドを作成
                shouldRun = true;
                readerThread = new Thread( new ThreadStart( () =>
                {
                    while ( shouldRun ) {
                        context.WaitAndUpdateAll();
                        ImageMetaData imageMD = image.GetMetaData();

                        // WAVEデータの出力
                        wavePlayer.Output( audio.AudioBufferPtr, audio.DataSize );

                        // ImageMetaDataをBitmapSourceに変換する(unsafeにしなくてもOK!!)
                        this.Dispatcher.BeginInvoke( DispatcherPriority.Background, new Action( () =>
                        {
                            image1.Source = BitmapSource.Create( imageMD.XRes, imageMD.YRes,
                                96, 96, PixelFormats.Rgb24, null, imageMD.ImageMapPtr,
                                imageMD.DataSize, imageMD.XRes * imageMD.BytesPerPixel );
                        } ) );
                    }
                } ) );
                readerThread.Start();
            }
            catch ( Exception ex ) {
                MessageBox.Show( ex.Message );
            }
        }

        private void Window_Closing( object sender, System.ComponentModel.CancelEventArgs e )
        {
            shouldRun = false;
        }
    }
}
初期化

通常通りに初期化します。AudioGenerator初期化後にStreamingWavePlayerを生成します。音声に関する設定はAudioGenerator.WaveOutputModeから取得できるので、それを渡します。

// ContextとImageGeneratorの作成
ScriptNode node;
context = Context.CreateFromXmlFile( "../../SamplesConfig.xml", out node );
context.GlobalMirror = false;
image = context.FindExistingNode( NodeType.Image ) as ImageGenerator;
audio = context.FindExistingNode( NodeType.Audio ) as AudioGenerator;

wavePlayer = new StreamingWavePlayer( audio.WaveOutputMode.SampleRate,
    audio.WaveOutputMode.BitsPerSample, audio.WaveOutputMode.Channels, 100 );
音声の出力

StreamingWavePlayer.Outputにバッファのアドレスとサイズを渡します。音声の場合はAudioGeneratorから直接データを取得しています。

// WAVEデータの出力
wavePlayer.Output( audio.AudioBufferPtr, audio.DataSize );

まとめ

音声の出力方法だけわかれば、特に難しいことはないですね。これで、WAVEデータが取得できるようになったので、録音や音声処理などにも使えるでしょう。