このエントリはKINECT SDK Advent Calendar 2011 : ATNDの12月24日分です!!
Advent Calendarでの、僕の全プロジェクトはこちらです
今回は、C++で音声と音源方向の取得をやってみます。
Kinect SDK のC++での音声系はとてもややこしいです。どのくらいややこしいかは、サンプルのMicArrayEchoCancellationを見てもらえればわかると思います。
ただ、これだと自分でも使いにくいので、C#っぽくしてみました。
全体のソースはこちらにあります。
main
C#と同じ名前の「KinectAudioSource」というクラスを作成し、そこに処理をまとめました。使い方はC#とほとんど同じです。
void main() { try { ::CoInitialize( NULL ); KinectAudioSource source; source.Initialize(); source.SetSystemMode( 4 ); source.Start(); WaveAudio wave; wave.Initialize( source.GetWaveFormat().nSamplesPerSec, source.GetWaveFormat().wBitsPerSample, source.GetWaveFormat().nChannels ); puts("\nAEC-MicArray is running ... Press any key to stop\n"); while ( !_kbhit() ) { std::vector< BYTE > buffer = source.Read(); if ( buffer.size() != 0 ) { wave.Output( &buffer[0], buffer.size() ); } if( source.GetSoundSourcePositionConfidence() > 0.9 ) { printf( "Position: %f\t\tBeam Angle = %f\r", source.GetSoundSourcePosition(), source.GetMicArrayBeamAngle() ); } } std::cout << std::endl; std::cout << "success!!" << std::endl; } catch ( std::exception& ex ) { std::cout << ex.what() << std::endl; } }
KinectAudioSource
KinectAudioSource.h
全体的にC#の形に合わせてあります。ビーム方向の取得はGetMicArrayBeamAngle()、音源方向の取得はGetSoundSourcePosition()、音源方向の信頼性はGetSoundSourcePositionConfidence()というのも同じです。
また、マイクのモードもSetSystemMode()で設定でき、パラメーターもC#と同じです。以下に値とその意味を記述します。音源方向はマイクアレイの機能なので、シングルチャネルのマイク時には利用できません。
- 0:シングルチャネルのマイクで、エコーキャンセルを使用する
- 2:マルチチャネルのマイクのみを使用する(エコーキャンセルを使用しない)
- 4:マルチチャネルのマイクで、エコーキャンセルを使用する
- 5:シングルチャネルのマイクのみを使用する(エコーキャンセルを使用しない)
実装の話をすると、COMを直に扱ってたので、すべてCComPtrにしました(書いてて思ったけど、ExpressにATLってあったっけか...)
class KinectAudioSource { public: void Initialize(); void SetSystemMode( LONG mode ); void Start(); std::vector< BYTE > Read(); const WAVEFORMATEX& GetWaveFormat() { static const WAVEFORMATEX wfxOut = {WAVE_FORMAT_PCM, 1, 16000, 32000, 2, 16, 0}; return wfxOut; } double GetSoundSourcePositionConfidence() const { return soundSourcePositionConfidence_; } double GetSoundSourcePosition() const { return soundSourcePosition_; } double GetMicArrayBeamAngle() const { return beamAngle_; } private: GUID GetJackSubtypeForEndpoint( IMMDevice* pEndpoint ); int GetMicArrayDeviceIndex(); private: CComPtr<IMediaObject> mediaObject_; CComPtr<IPropertyStore> propertyStore_; CComPtr<ISoundSourceLocalizer> soundSource_; CStaticmediaBuffer mediaBuffer_; DMO_OUTPUT_DATA_BUFFER outputBufferStruct_; double beamAngle_; double soundSourcePosition_; double soundSourcePositionConfidence_; };
KinectAudioSource.cpp
初期化です。オーディオのためのインタフェースであるCLSID_CMSRKinectAudioを作成し、そこからプロパティのためのオブジェクト、音源のためのオブジェクトを生成します。
C++の場合は、マイクのインデックスを自分で取得する必要があるようなので、GetMicArrayDeviceIndex()を使ってデバイスのインデックスを取得し、そのインデックスのデバイスから音声データを取得しています。
void KinectAudioSource::Initialize() { CHECKHR( mediaObject_.CoCreateInstance(CLSID_CMSRKinectAudio, NULL, CLSCTX_INPROC_SERVER ) ); CHECKHR( mediaObject_.QueryInterface( &propertyStore_ ) ); CHECKHR( mediaObject_->QueryInterface( IID_ISoundSourceLocalizer, (void**)&soundSource_ ) ); // Tell DMO which capture device to use (we're using whichever device is a microphone array). // Default rendering device (speaker) will be used. int iMicDevIdx = GetMicArrayDeviceIndex(); PROPVARIANT pvDeviceId; PropVariantInit(&pvDeviceId); pvDeviceId.vt = VT_I4; //Speaker index is the two high order bytes and the mic index the two low order ones int iSpkDevIdx = 0; //Asume default speakers pvDeviceId.lVal = (unsigned long)(iSpkDevIdx<<16) | (unsigned long)(0x0000ffff & iMicDevIdx); CHECKHR(propertyStore_->SetValue(MFPKEY_WMAAECMA_DEVICE_INDEXES, pvDeviceId)); PropVariantClear(&pvDeviceId); }
次に、マイクモードの設定です。モードはプロパティに設定します。設定できるモードは次の4つです。
- 0:シングルチャネルのマイクで、エコーキャンセルを使用する
- 2:マルチチャネルのマイクのみを使用する(エコーキャンセルを使用しない)
- 4:マルチチャネルのマイクで、エコーキャンセルを使用する
- 5:シングルチャネルのマイクのみを使用する(エコーキャンセルを使用しない)
// SINGLE_CHANNEL_AEC = 0 // OPTIBEAM_ARRAY_ONLY = 2 // OPTIBEAM_ARRAY_AND_AEC = 4 // SINGLE_CHANNEL_NSAGC = 5 void KinectAudioSource::SetSystemMode( LONG mode ) { // Set AEC-MicArray DMO system mode. // This must be set for the DMO to work properly PROPVARIANT pvSysMode; PropVariantInit(&pvSysMode); pvSysMode.vt = VT_I4; pvSysMode.lVal = mode; CHECKHR(propertyStore_->SetValue(MFPKEY_WMAAECMA_SYSTEM_MODE, pvSysMode)); PropVariantClear(&pvSysMode); }
音声取得および、音源方向の取得を開始します。
音声データはWAVEフォーマットで取得でき、その設定はGetWaveFormat()で固定値が取得できるようにしています。実際のデータは、後述するIMediaBufferから派生したクラスに格納され、そのバッファを指定するOutputBufferStructの設定をおこないます。
ほかに出力のフォーマットや、出力バッファのサイズなどを指定して、音声の取得を開始します。
void KinectAudioSource::Start() { DMO_MEDIA_TYPE mt = {0}; ULONG cbProduced = 0; memset( &outputBufferStruct_, 0, sizeof(outputBufferStruct_) ); outputBufferStruct_.pBuffer = &mediaBuffer_; // Set DMO output format CHECKHR( MoInitMediaType(&mt, sizeof(WAVEFORMATEX)) ); mt.majortype = MEDIATYPE_Audio; mt.subtype = MEDIASUBTYPE_PCM; mt.lSampleSize = 0; mt.bFixedSizeSamples = TRUE; mt.bTemporalCompression = FALSE; mt.formattype = FORMAT_WaveFormatEx; memcpy(mt.pbFormat, &GetWaveFormat(), sizeof(WAVEFORMATEX)); CHECKHR( mediaObject_->SetOutputType(0, &mt, 0) ); MoFreeMediaType(&mt); // Allocate streaming resources. This step is optional. If it is not called here, it // will be called when first time ProcessInput() is called. However, if you want to // get the actual frame size being used, it should be called explicitly here. CHECKHR( mediaObject_->AllocateStreamingResources() ); // Get actually frame size being used in the DMO. (optional, do as you need) int iFrameSize; PROPVARIANT pvFrameSize; PropVariantInit(&pvFrameSize); CHECKHR(propertyStore_->GetValue(MFPKEY_WMAAECMA_FEATR_FRAME_SIZE, &pvFrameSize)); iFrameSize = pvFrameSize.lVal; PropVariantClear(&pvFrameSize); // allocate output buffer mediaBuffer_.SetBufferLength( GetWaveFormat().nSamplesPerSec * GetWaveFormat().nBlockAlign ); }
音声データおよび、音源方向の取得です。
IMediaObject::ProcessOutput()で音声データが取得できます。データの取得はOutputBufferStructを通じてIMediaBufferに格納されるので、戻り値として格納したバッファをクローンして返します。
ビーム方向はISoundSourceLocalizer::GetBeam()、音源方向および信頼性はISoundSourceLocalizer::GetPosition()で取得することができます。
std::vector< BYTE > KinectAudioSource::Read() { mediaBuffer_.Clear(); do{ // 音声データを取得する DWORD dwStatus; CHECKHR( mediaObject_->ProcessOutput(0, 1, &outputBufferStruct_, &dwStatus) ); // ビームと音声の方向を取得する CHECKHR( soundSource_->GetBeam(&beamAngle_) ); CHECKHR( soundSource_->GetPosition(&soundSourcePosition_, &soundSourcePositionConfidence_) ); } while ( outputBufferStruct_.dwStatus & DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE ); return mediaBuffer_.Clone(); }
Initialize()から呼ばれる、マイクアレイのデバイスインデックスを取得する関数です。IMMDeviceEnumeratorでデバイスを列挙し、そのなかからKSNODETYPE_MICROPHONE_ARRAY を探してインデックスとして返します。
int KinectAudioSource::GetMicArrayDeviceIndex() { CComPtr<IMMDeviceEnumerator> spEnumerator; CHECKHR( spEnumerator.CoCreateInstance( __uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL ) ); CComPtr<IMMDeviceCollection> spEndpoints; CHECKHR( spEnumerator->EnumAudioEndpoints( eCapture, DEVICE_STATE_ACTIVE, &spEndpoints ) ); UINT dwCount = 0; CHECKHR(spEndpoints->GetCount(&dwCount)); // Iterate over all capture devices until finding one that is a microphone array for ( UINT index = 0; index < dwCount; index++) { IMMDevice* spDevice; CHECKHR( spEndpoints->Item( index, &spDevice ) ); GUID subType = GetJackSubtypeForEndpoint( spDevice ); if ( subType == KSNODETYPE_MICROPHONE_ARRAY ) { return index; } } throw std::runtime_error( "デバイスが見つかりません" ); }
GetMicArrayDeviceIndex()から呼ばれ、デバイスのGUIDを返します。取得されたGUIDを利用して、デバイスを判別します。
GUID KinectAudioSource::GetJackSubtypeForEndpoint( IMMDevice* pEndpoint ) { if ( pEndpoint == 0 ) { throw std::invalid_argument( "生成されていないインスタンスが渡されました" ); } CComPtr<IDeviceTopology> spEndpointTopology; CComPtr<IConnector> spPlug; CComPtr<IConnector> spJack; CComPtr<IPart> spJackAsPart; // Get the Device Topology interface CHECKHR( pEndpoint->Activate(__uuidof(IDeviceTopology), CLSCTX_INPROC_SERVER, NULL, (void**)&spEndpointTopology) ); CHECKHR( spEndpointTopology->GetConnector(0, &spPlug) ); CHECKHR( spPlug->GetConnectedTo( &spJack ) ); CHECKHR( spJack.QueryInterface( &spJackAsPart ) ); GUID subtype; CHECKHR( spJackAsPart->GetSubType( &subtype ) ); return subtype; }
CStaticmediaBuffer(IMediaBufferの派生クラス)
Kinect SDKのサンプルからもってきています。音声データの取得のためのインタフェースであるIMediaBufferを実装し、データを格納します。内部でバッファを持つように、少し変更しています。
キモとなるインタフェースは次の3つです
STDMETHODIMP SetLength( DWORD ulLength ) | バッファに書き込んだデータ長(有効なデータ数)を設定する | ||
STDMETHODIMP GetMaxLength(DWORD *pcbMaxLength) | バッファの最大数を取得する | ||
STDMETHODIMP GetBufferAndLength(BYTE **ppBuffer, DWORD *pcbLength) | バッファと、有効なデータ数を取得する |
バッファとバッファのサイズはvector、有効なデータ数はm_ulDataを使っています。vectorにデータが書き込まれた後に、SetLength()を通してm_ulDataが更新されます。データの取得には、Clone()を使用してvectorに格納されたデータをm_ulDataに切りつめたもの(実際の有効データ)を使用するようにしています。
// Kinect SDKのサンプルから class CStaticmediaBuffer : public IMediaBuffer { public: CStaticmediaBuffer() {} STDMETHODIMP_(ULONG) AddRef() { return 2; } STDMETHODIMP_(ULONG) Release() { return 1; } STDMETHODIMP QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) { AddRef(); *ppv = (IUnknown*)this; return NOERROR; } else if (riid == IID_IMediaBuffer) { AddRef(); *ppv = (IMediaBuffer*)this; return NOERROR; } else return E_NOINTERFACE; } STDMETHODIMP SetLength( DWORD ulLength ) { m_ulData = ulLength; return NOERROR; } STDMETHODIMP GetMaxLength(DWORD *pcbMaxLength) { *pcbMaxLength = buffer_.size(); return NOERROR; } STDMETHODIMP GetBufferAndLength(BYTE **ppBuffer, DWORD *pcbLength) { if ( ppBuffer ) *ppBuffer = &buffer_[0]; if ( pcbLength ) *pcbLength = m_ulData; return NOERROR; } void Clear() { m_ulData = 0; } ULONG GetDataLength() const { return m_ulData; } void SetBufferLength( ULONG length ) { buffer_.resize( length ); } std::vector< BYTE > Clone() const { return std::vector< BYTE >( buffer_.begin(), buffer_.begin() + GetDataLength() ); } protected: std::vector< BYTE > buffer_; ULONG m_ulData; };