2008年7月15日火曜日

Managed DirectXでサウンドをキャプチャする

とりあえず、VC++ 2008 Express Editionでサウンドをキャプチャするコードを書いてみることになったのだが、折角なのでManaged DirectX(MDX)のDirectSoundを使ってみることにした。
コードは、VC++ではあるが、CLRで動作するので、C#などの他言語でもほぼ同じコードになるはずである。
MDXのDirectSoundを利用するには、「Microsoft.DirectX.DirectSound」という、.NETの参照をプロジェクトに追加しなければならない。もしこれが「参照の追加」の一覧で見つからなければ、DX9以降のランタイムかSDKをインストールする。

MDXとはいえ、所詮DirectXをラップしたようなものであるがゆえに、基本的な概念はDirectSoundに由来する。もし、DirectSound関連のドキュメントを読んだことがなければ、一読することをおすすめする。
というわけで、ManagedなDirectSoundを利用するに当たり、知っていたほうがよいと思われること(アンマネージドのDirectSoundとの違いも含む)は、以下のことであると思われる。

CaptureはIDirectSound8
サウンドデバイス関連を司っていると考えてよい。
CaptureBufferはIDirectSoundCaptureBuffer8に相当
ただし、使い方はManaged風になっているため多少違う。
非同期用のイベント(IDirectSoundNotify8)はNotify
イベントの処理は、System.Threadingにあるものを使う。

まずは、Captureクラスだが、IDirectSound8とほとんど同じ様なものなので、特に説明はしない。単純にデフォルトキャプチャデバイス(コントロールパネルのサウンドで定義されている)を使って、キャプチャするには以下のようなコードをになる。

Capture^ capture = gcnew Capture();

次にCaptureBufferクラスだが、これもやっていることは同じで、「CaptureBufferDescription」という構造体(DSCBUFFERDESCに相当)に必要な情報を入れて、インスタンスを作成すればよい。エフェクトを利用しない場合のバッファの作成は以下のようになる。

CaptureBufferDescription desc;
desc.ControlEffects = false;
desc.WaveMapped = true;
//GetWaveFormat()はWaveFormat構造体を返す関数
desc.Format = GetWaveFormat(44100, 2, 16);
//BufferSecondsはバッファの秒数
desc.BufferBytes = desc.Format.AverageBytesPerSecond * BufferSeconds;

CaptureBuffer^ captureBuffer = gcnew CaptureBuffer(desc, capture);

ちなみに上記のGetWaveFormat()は、誰が書いても同じようなコードになると思われるが、いちおうこんな感じ。MDXの「WaveFormat」は、DXの「WAVEFORMATEX」と同じ。

WaveFormat GetWaveFormat(int rate, int channel, int rez)
{
 WaveFormat format;
 format.FormatTag = WaveFormatTag::Pcm;
 format.SamplesPerSecond = rate;
 format.Channels = channel;
 format.BitsPerSample = rez;
 format.BlockAlign = format.Channels * format.BitsPerSample / 8;
 format.AverageBytesPerSecond = format.SamplesPerSecond * format.BlockAlign;
 return format;
}

バッファの更新通知の設定については、DXの「IDirectSoundNotify8::SetNotificationPositions()」を使って設定する方法とイメージは同じだが、登録するためのイベントハンドルは、Managedなものを使う。基本的には、「AutoResetEvent」を使うようなのでここでもこれを使って登録する。

AutoResetEvent^ autoResetEvent = gcnew AutoResetEvent(false);
// BufferSecondsはバッファの秒数
// CapturePerSecondは1秒間に通知してほしい回数
int notifications = BufferSeconds * CapturePerSecond;
array^ bufferPositionNotifies = gcnew array(notifications);
for (int i=0; i<notifications; i++) {
 bufferPositionNotifies[i].EventNotifyHandle = autoResetEvent->Handle;
 bufferPositionNotifies[i].Offset = 
  format.AverageBytesPerSecond * (i+1) / CapturePerSecond - 1;
}
Notify^ notify = gcnew Notify(captureBuffer);
notify->SetNotificationPositions(bufferPositionNotifies);

通知を登録したので、別スレッドでイベントを待って、キャプチャされたデータを取得する。以下の関数は、スレッド用の関数となる。
ちなみにDXの場合、「IDirectSoundCaptureBuffer8::Lock()」を使ってデータを得るのであるが、この関数は露骨にリングバッファから読み取る仕様になっている。MDXの場合は、単純にArrayまたはSystem::IO::Streamに読み込むことができる。以下の例は、shortが格納されているArrayに読み込むようにしている。

void CaptureThreadProc()
{
 int bufferSize = captureBuffer->Caps.BufferBytes;
 int capturePos, lastReadPos;
 captureBuffer->GetCurrentPosition(capturePos, lastReadPos);
 do {
  autoResetEvent->WaitOne();
  int readPos;
  captureBuffer->GetCurrentPosition(capturePos, readPos);
  int size = readPos - lastReadPos;
  if (size == 0) continue;
  if (size < 0) size += bufferSize;
  // shortで読む場合、読み込むサイズをsizeof(short)で割らないとエラーになる
  Array^ captureData = 
   captureBuffer->Read(lastReadPos, Type::GetType("System.Int16"), LockFlag::None, size / 2);
  //ここでcaptureDataにあるデータを処理する
  lastReadPos = readPos;
 } while(captureBuffer->Capturing);
}

ちなみにこの関数では、キャプチャが停止するとループを抜けてスレッドが終了するようにしている。そのため、キャプチャ開始前に(CaptureBuffer::Start()を呼ぶ前)にスレッドを作成して走らせておく必要がある。
この場合、Arrayにデータが入ってくるのであるが、実際のデータはGC内にあるため、ネイティブ(アンマネージド)で処理をする場合には、ArrayのGetEnumerator()でEnumeratorを得てからひとつずつ取り出すか、array<>にしてから、pin_ptr<>を使ってポインタを得て処理するという方法が考えられる。

あと、キャプチャの開始、停止は、CaptureBuffer::Start()、CaptureBuffer::Stop()を呼び出せばよい。この場合にはスレッドで読み込み続けるのでStart(true)となる。

1 件のコメント:

Finky さんのコメント...

別の記事でpin_ptrを使ってアンマネージドでキャプチャデータを取り込む方法を書いています。