ラベル Managed の投稿を表示しています。 すべての投稿を表示
ラベル Managed の投稿を表示しています。 すべての投稿を表示

2008年8月4日月曜日

Managed DirectXでのサウンドキャプチャ(2)

前記事でVC++でManaged DirectSoundを使ってサウンドをキャプチャする方法について書いたのだが、アンマネージドでキャプチャされたデータを取得するところが曖昧だったので、ちゃんと書いてみる。

キャプチャバッファからデータを取得する場合、読み取り用のメソッド(CaptureBuffer::Read)でArrayかStreamのいずれかを得ることができるが、最終的にアンマネージドなコードで処理するために、キャプチャされたデータが格納されているバッファへのポインタ(const unsigned char*)を取得したいとする。

マネージドなデータをアンマネージドなコードから参照する手段は、pin_ptr<>を使用するとして、その場合、array<>が必要になるわけだが、メソッドとして、ArrayかStreamのいずれかの選択肢がある中、どれを使ってpin_ptr<>を得るのがよいのかということを試行した結果、MemoryStreamを使って、CaptureBuffer::Readを呼び出す方法が無難であるという結論になった。

以下のコードは前回の最後のコードの続きとなるが、最終的には、16bitデータ(const short*)へのポインタを得ている。

int size;//キャプチャされたデータのサイズ[byte]
array<unsigned char>^ dataBuffer = gcnew array<unsigned char>(size);
MemoryStream^ dataStream = gcnew MemoryStream(dataBuffer);
captureBuffer->Read(lastReadPos, dataStream, size, LockFlag::None);
pin_ptr<unsigned char> ptr = &dataBuffer[0];
const short* sampleBuffer = (const short*) ptr;

ここで注意することとしては、const short*にキャストしているので、実際のデータはsizeの半分(sizeof(short)分の1)になっていることと、キャプチャしているスレッドで時間がかかる処理をすると、データを取りこぼす可能性があるということである。
キャプチャの取りこぼしを避けるための無難な手段としては、取り込んだデータ(array<unsigned char>^)をキュー(Queue)に入れて、別のスレッドで取り込む方法が考えられる。ただし、CLIで用意されているキューは同期化はサポートされているものの、空の状態のブロッキングはしてくれないので、なんだかの同期オブジェクト(AutoResetEventなど)を利用したほうがよいかもしれない。

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)となる。