再次回到另一个 DirectSound 问题,这个关于 DirectSound 缓冲区可以使用的方式:
我有大约 30 毫秒间隔通过网络传入的数据包,其中包含由应用程序的其他部分解码为原始 wav 数据的音频数据。
当 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);
到目前为止一切都很好,虽然音频播放有点断断续续。不幸的是,这段代码还不够。
我还需要停止播放并等待缓冲区在数据用完时再次填满。这是我遇到问题的地方。
基本上,我需要能够找出音频播放何时到达我上次写入缓冲区的位置,并在它到达时停止它。否则,我收到的音频数据的任何延迟都会弄乱音频,当然。不幸的是,我不能停止写入缓冲区,因为如果我让缓冲区继续播放,它只会播放一秒钟前留下的旧数据(什么是循环的等等。)所以我需要知道 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;
当我的通知被触发并且我在我正在使用的辅助缓冲区上调用 Stop 时,音频仍然继续循环主缓冲区中似乎是剩余数据的内容......
即便如此,通知也没有正确触发。如果我停止从发送这些音频消息的其他应用程序广播音频数据,它只会不断循环缓冲区中的剩余部分。所以基本上,不管怎样,它都会超过我设置的最新通知(lastwrittenbyte)。在播放时,它只是偶尔停止,填充缓冲区然后开始播放......并跳过它刚刚缓冲的半秒数据,继续播放缓冲区填充后传入的数据(所以它填满缓冲区,但显然不关心在开始填充新数据之前播放其内容。是的。我也不知道。)
我在这里似乎缺少什么?使用 DirectSound Notificatiosn 找出最近写入的字节何时播放的想法是徒劳的吗?你会认为有一些方法可以使用流缓冲区进行这种缓冲。