delphi - Directsound - 播放充满网络数据的流缓冲区时出现问题!为 Delphi 使用移植的 DirectX 头文件

标签 delphi directx delphi-2010 voip directsound

再次回到另一个 DirectSound 问题,这个问题是关于 DirectSound Buffers 的使用方式:

我有数据包以大约 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 事件中运行的代码(由我们的专有组件定义,该组件解码使用我们的协议(protocol)发送的消息。)
基本上,每当我收到带有音频数据的 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() 要求停止缓冲区)对播放本身有显着影响,因此音频播放的声音比以前更 splinter 。

我将此添加到我编写代码的末尾以获取通知。我有点期望每次将数据写入缓冲区时设置一个新的通知可能不会很好地工作......但是,嘿,我认为尝试不会有什么坏处:
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 找出最近写入的字节何时播放的想法是徒劳的吗?你会认为有一些方法可以用流缓冲区来做这种缓冲。

最佳答案

我再次发现自己在回答我自己的问题 ^^;

出现了三个问题:
首先,我查看播放光标位置以确定播放是否已达到最后写入的字节。
这不太正确,因为虽然播放光标显示当前正在播放的内容,但它并没有告诉您要播放的数据集的最后一个字节是什么。
写光标是 总是 位于播放光标之前一点。播放光标和写入光标之间的区域被锁定以进行播放。

换句话说,我看错了光标。

其次,这里是东西不能正常工作的主要原因,是我正在使用 DSBLOCK_FROMWRITECURSOR 标志写入数据。
这样做是允许我从写游标开始写入数据。
每次。

这意味着当我认为它正在缓冲数据时,它实际上只是一次又一次地覆盖相同的数据。
与我假设的相反,写入游标在您将数据写入缓冲区时不会向前移动。
所以我现在要做的是手动跟踪上次写入的字节,然后在该偏移量上执行 DSBLOCK_ENTIREBUFFER。
我只是通过跟踪缓冲区大小和我的 lastwrittenbyte 变量来处理包装,并确认它没有包装。这有助于我在任何时候只需要将 512 字节的数据写入缓冲区。使我的缓冲区大小为 512 的倍数意味着我只需要确保如果 lastwrittenbyte >= buffersize 然后 lastwrittenbyte := 0(我将 lastwrittenbyte 更改为 nextbyte,因为它现在显示要写入的下一个字节。那样我不必担心不均匀的字节,用 + 1 和所有这些来解决它。)

因此,将数据流式传输到缓冲区的假设方法让您一次又一次地写入自己的数据。
他们打算如何正确地使用它来将数据流式传输到缓冲区中……我真的不知道。也许他们认为您可以将通知放在中间并将其视为拆分缓冲区?

这让我想到了第三个问题:
我试图使用通知。事实证明,通知是不可靠的,人们总体上建议您避免使用它们,如果必须这样做,则不应使用多个。

所以我取消了通知并投入了一个每 30 毫秒触发一次的高分辨率计时器(根据我们的协议(protocol),我正在接收和提取要播放的音频的数据包总是以 32 毫秒的间隔发送,所以任何低于 32ms 真的适用于这个计时器。)

我用一个简单的 BytesInBuffer 变量跟踪我还有多少数据可以播放。
当我的计时器触发时,我会检查自上次计时器触发以来写入游标(正如我之前所说的本质上是真正的播放游标)前进了多少,并从我的 BytesInBuffer 变量中删除了这个数字。

从那里我可以查看是否已用完字节,或者我可以查看 writecursor 位置并检查它是否超过了我的 lastwrittenbyte(了解自上次计时器事件以来我播放了多少字节有帮助。

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

所以在那里。

找到和你问同样问题的人总是很糟糕,但最终却说“没关系我解决了它”而不留下答案。

这是希望这可能会在 future 启发其他人。

关于delphi - Directsound - 播放充满网络数据的流缓冲区时出现问题!为 Delphi 使用移植的 DirectX 头文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2148681/

相关文章:

multithreading - 无法终止线程

c++读取(.cso)编译的着色器对象返回\0

c++ - C++ 复制保护 API

delphi - 在Delphi 2010迁移项目中,Rave BE的“代码报告”替代方案是什么?

delphi - 如何在 DataSnap 方法中返回记录

delphi - Delphi 应用程序问题中的帧行为

delphi - 调用 TsaveDialog 时,我的详细表单隐藏在主表单后面

Delphi 7 企业版或 Delphi 2010 专业版

delphi - 参数化类型的方法在制作通用接口(interface)工厂时不能使用局部符号错误

c++ - 使用 C++ AMP 读取缓冲区数据