ブログ@kaorun55

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

KINECT SDK Beta2 で音声と音源方向を取得する(C++) #kinectsdk_ac

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


今回は、C++で音声と音源方向の取得をやってみます。
Kinect SDKC++での音声系はとてもややこしいです。どのくらいややこしいかは、サンプルの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;
};

まとめ

長くなりましたが、Kinect SDKを使ってC++で音声と音源方向を取得する方法は以上です。OpenNI + Kinectでは今のところ音声が取得できないので、Kinectで音声を利用する場合はKinect SDK一択ですね。