6

さらに別のDirectSoundの質問に戻ります。これは、DirectSoundバッファの使用方法に関するものです。

アプリケーションの他の部分によって生のwavデータにデコードされるオーディオデータを含むパケットが約30ms間隔でネットワーク経由で着信しています。

Indataイベントがこれらの他のコードによってトリガーされると、基本的に、オーディオデータをパラメーターとして使用するプロシージャにドロップされます。

DSCurrentBufferは次のように初期化されました。

ZeroMemory(@BufferDesc, SizeOf(DSBUFFERDESC));
wfx.wFormatTag := WAVE_FORMAT_PCM;
wfx.nChannels := 1;
wfx.nSamplesPerSec := fFrequency;
wfx.wBitsPerSample := 16;
wfx.nBlockAlign := 2; // Channels * (BitsPerSample/8)
wfx.nAvgBytesPerSec := fFrequency * 2; // SamplesPerSec * BlockAlign

BufferDesc.dwSize := SizeOf(DSBUFFERDESC);
BufferDesc.dwFlags := (DSBCAPS_GLOBALFOCUS or DSBCAPS_GETCURRENTPOSITION2 or
    DSBCAPS_CTRLPOSITIONNOTIFY);
BufferDesc.dwBufferBytes := BufferSize;
BufferDesc.lpwfxFormat := @wfx;

case DSInterface.CreateSoundBuffer(BufferDesc, DSCurrentBuffer, nil) of
  DS_OK:
    ;
  DSERR_BADFORMAT:
    ShowMessage('DSERR_BADFORMAT');
  DSERR_INVALIDPARAM:
    ShowMessage('DSERR_INVALIDPARAM');
end;

このデータを次のようにセカンダリバッファに書き込みます。

var
FirstPart, SecondPart: Pointer;
FirstLength, SecondLength: DWORD;
AudioData: Array [0 .. 511] of Byte;
I, K: Integer;
Status: Cardinal;
begin

ここでは、入力データは音声データに変換されますが、質問自体には関係ありません。

DSCurrentBuffer.GetStatus(Status);

if (Status and DSBSTATUS_PLAYING) = DSBSTATUS_PLAYING then // If it is playing, request the next segment of the buffer for writing
  begin
    DSCurrentBuffer.Lock(LastWrittenByte, 512, @FirstPart, @FirstLength,
      @SecondPart, @SecondLength, DSBLOCK_FROMWRITECURSOR);

    move(AudioData, FirstPart^, FirstLength);
    LastWrittenByte := LastWrittenByte + FirstLength;
    if SecondLength > 0 then
      begin
        move(AudioData[FirstLength], SecondPart^, SecondLength);
        LastWrittenByte := SecondLength;
      end;
    DSCurrentBuffer.GetCurrentPosition(@PlayCursorPosition,
      @WriteCursorPosition);
    DSCurrentBuffer.Unlock(FirstPart, FirstLength, SecondPart, SecondLength);
  end
else // If it isn't playing, set play cursor position to the start of buffer and lock the entire buffer
  begin
    if LastWrittenByte = 0 then
      DSCurrentBuffer.SetCurrentPosition(0);

    LockResult := DSCurrentBuffer.Lock(LastWrittenByte, 512, @FirstPart, @FirstLength,
      @SecondPart, @SecondLength, DSBLOCK_ENTIREBUFFER);
    move(AudioData, FirstPart^, 512);
    LastWrittenByte := LastWrittenByte + 512;

    DSCurrentBuffer.Unlock(FirstPart, 512, SecondPart, 0);
  end;

上記は、OnAudioDataイベントで実行するコードです(プロトコルで送信されたメッセージをデコードする独自のコンポーネントによって定義されます)。基本的に、コードは、オーディオデータを含むUDPメッセージを取得するたびに実行されます。

バッファに書き込んだ後、バッファに十分なデータが入ったら再生を開始するために次のことを行います。ちなみに、BufferSizeは現時点で1秒に相当するように設定されています。

if ((Status and DSBSTATUS_PLAYING) <> DSBSTATUS_PLAYING) and
   (LastWrittenByte >= BufferSize div 2) then
  DSCurrentBuffer.Play(0, 0, DSCBSTART_LOOPING);

これまでのところ、オーディオの再生は少し途切れがちですが、これまでのところ良好です。残念ながら、このコードでは十分ではありません。

また、再生を停止し、データが不足したときにバッファが再びいっぱいになるのを待つ必要があります。これは私が問題にぶつかっているところです。

基本的に、オーディオの再生が最後にバッファに書き込んだ時点に到達したことを確認し、書き込みが終了したときに停止できるようにする必要があります。そうしないと、もちろん、受信しているオーディオデータの遅延によってオーディオが台無しになります。残念ながら、バッファへの書き込みを停止することはできません。バッファを再生し続けると、1秒前に残った古いデータが再生されるだけです(循環している場合など)。したがって、PlayCursorPositionかどうかを知る必要があります。バッファ書き込みコードで追跡しているLastWrittenByte値に達しました。

DirectSound Notificationsは私にとってこれを行うことができるように見えましたが、データを書き込むたびにバッファを停止してから新たに開始すると(SetNotificationPositions()ではバッファを停止する必要があります)、再生自体に顕著な影響があるため、オーディオそれは以前よりもさらに壊れた音を再生します。

通知を受け取るために、これを記述コードの最後に追加しました。バッファにデータを書き込むたびに新しい通知を設定することは、おそらくうまく機能しないだろうと思っていました...しかし、ちょっと、試してみても害はないと思いました:

if (Status and DSBStatus_PLAYING) = DSBSTATUS_PLAYING then //If it's playing, create a notification
  begin
    DSCurrentBuffer.QueryInterface(IID_IDirectSoundNotify, DSNotify);
    NotifyDesc.dwOffset := LastWrittenByte;
    NotifyDesc.hEventNotify := NotificationThread.CreateEvent(ReachedLastWrittenByte);
    DSCurrentBuffer.Stop;
    LockResult := DSNotify.SetNotificationPositions(1, @NotifyDesc);
    DSCurrentBuffer.Play(0, 0, DSBPLAY_LOOPING);
  end;

NotificationThreadは、WaitForSingleObjectを実行するスレッドです。CreateEventは、新しいイベントハンドルを作成し、WaitForSingleObjectが前のイベントハンドルの代わりにそれを待機し始めるようにします。ReachedLastWrittenByteは、私のアプリケーションで定義されているプロシージャです。スレッドはクリティカルセクションを開始し、通知がトリガーされたときにそれを呼び出します。(もちろん、WaitForSingleObjectは20ミリ秒のタイムアウトで呼び出されるため、CreateEventが呼び出されるたびにハンドルを更新できます。)

ReachedLastWrittenByte()は次のことを行います。

DSCurrentBuffer.Stop;
LastWrittenByte := 0;

通知がトリガーされ、使用しているセカンダリバッファで停止を呼び出すと、オーディオはプライマリバッファに残っているデータのように見えるものをループし続けます...

それでも、通知は適切にトリガーされません。これらのオーディオメッセージを送信している他のアプリケーションからのオーディオデータのブロードキャストを停止すると、バッファ内の残りの部分をループし続けます。つまり、基本的には、設定した最新の通知(lastwrittenbyte)を超えています。再生中は、ときどき停止し、バッファがいっぱいになってから再生を開始します...バッファがいっぱいになった後、入ってくるデータを再生するために、バッファリングしたばかりのデータの0.5秒をスキップします(つまり、バッファがいっぱいになりますが、新しいデータの入力を開始する前にその内容を再生する必要はないようです。ええ、私にもわかりません。)

ここで何が欠けているように見えますか?DirectSound Notificatiosnを使用して、最後に書き込まれたバイトがいつ再生されたかを確認するというアイデアは、無駄な努力ですか?ストリーミングバッファを使用してこの種のバッファリングを行う方法があると思います。

4

1 に答える 1

10

そしてまた、私は自分自身の質問に答えていることに気づきました^^;

3つの問題がありました。最初に、再生が最後に書き込まれたバイトに到達したかどうかを判断するために、再生カーソルの位置を調べていました。再生カーソルには現在再生されているものが表示されますが、再生されるデータセットの最後のバイトが何であるかは表示されないため、これは正しく機能しません。書き込みカーソルは常に再生カーソルの少し前に配置されます。再生カーソルと書き込みカーソルの間の領域は、再生のためにロックされています。

言い換えれば、私は間違ったカーソルを見ていました。

次に、これが正常に機能しなかった主な理由です。これは、DSBLOCK_FROMWRITECURSORフラグを使用してデータを書き込んでいたためです。これにより、書き込みカーソル以降からデータを書き込むことができます。毎回。

つまり、データをバッファリングしていると思ったとき、実際には同じデータを何度も何度も上書きしているだけでした。私が想定していたのとは逆に、書き込みカーソルは、バッファにデータを書き込むときに前方に移動しません。したがって、私が今していることは、最後に書き込まれたバイトを手動で追跡し、そのオフセットに対してDSBLOCK_ENTIREBUFFERを実行することです。バッファサイズとlastwrittenbyte変数を追跡するだけでラッピングを処理し、ラッピングしないことを確認します。いつでも正確に512バイトのデータをバッファに書き込むだけでよいのです。バッファサイズを512の倍数にすると、lastwrittenbyte> = buffersize then lastwrittenbyte:= 0(lastwrittenbyteをnextbyteに変更しました。これは、書き込む次のバイトが表示されるためです。そのようにして、不均一なバイトについて心配する必要はありません、

したがって、データをバッファにストリーミングするための想定される方法では、自分のデータを何度も上書きする必要があります。データをバッファにストリーミングするためにこれを適切に使用することを彼らがどのように意図したのか...私は本当に知りません。おそらく彼らはあなたが通知を真ん中に置いてそれを分割バッファのように扱うことができると考えていますか?

それは私に3番目の問題をもたらします:私は通知を使おうとしていました。通知は信頼できないことが判明し、人々は全体的に通知の使用を避けることを推奨しています。通知が必要な場合は、少数を超えて使用しないでください。

だから私は通知を廃止し、30msごとにトリガーする高解像度タイマーを投入しました(私が受信して再生するオーディオを抽出するデータパケットは、プロトコルに従って常に32ms間隔で送信されるので、何でも32ms未満は、このタイマーで実際に機能します。)

単純なBytesInBuffer変数で再生するために残されたデータの量を追跡します。タイマーがトリガーされたら、最後にタイマーがトリガーされてから書き込みカーソル(基本的には真の再生カーソル)がどれだけ進んだかを確認し、BytesInBuffer変数からこの数値を削除します。

そこから、バイトが不足していないかどうかを確認するか、writecursorの位置を確認して、lastwrittenbyteを超えているかどうかを確認できます(前回のタイマーイベント以降に再生したバイト数がわかります)。

if (BytesInBuffer <= 0) or ((WritePos >= LastWrittenByte) and (WritePos-PosDiff <= LastWrittenByte)) then
  DSBuffer.Stop;

だからそこに。

あなたと同じ質問をしている人を見つけるのはいつもひどいことですが、最終的には「私がそれを解決したことを気にしないで」行き、答えを残さないでください。

これが将来誰か他の人を啓発するかもしれないことを願っています。

于 2010-02-04T10:23:12.050 に答える