さらに別の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を使用して、最後に書き込まれたバイトがいつ再生されたかを確認するというアイデアは、無駄な努力ですか?ストリーミングバッファを使用してこの種のバッファリングを行う方法があると思います。