Directsound - 播放来自网络数据的流媒体缓冲区的问题!为Delphi使用移植的DirectX头文件

Mic*_*hre 6 delphi directx voip directsound delphi-2010

再回到另一个DirectSound问题,可以使用关于DirectSound缓冲区的方法:

我通过网络以大约30ms的间隔进入包含音频数据的数据包,该数据被应用程序的其他部分解码为原始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;
Run Code Online (Sandbox Code Playgroud)

我将此数据写入我的辅助缓冲区,如下所示:

var
FirstPart, SecondPart: Pointer;
FirstLength, SecondLength: DWORD;
AudioData: Array [0 .. 511] of Byte;
I, K: Integer;
Status: Cardinal;
begin
Run Code Online (Sandbox Code Playgroud)

输入数据在此处转换为音频数据,与问题本身无关.

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;
Run Code Online (Sandbox Code Playgroud)

以上是我在OnAudioData事件中运行的代码(由我们的专有组件定义,解码使用我们的协议发送的消息.)基本上,每当我收到带有音频数据的UDP消息时,代码就会运行.

写入缓冲区后,如果缓冲区中有足够的数据,我会执行以下操作来开始播放:顺便说一下,BufferSize设置为相当于1秒.

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

到目前为止这么好,虽然音频播放略有波动.不幸的是,这段代码还不够.

我还需要停止播放并等待缓冲区在数据耗尽时再次填满.这是我遇到问题的地方.

基本上,我需要能够找出音频播放何时达到我上次写入缓冲区的时间点,并在此时将其停止.否则,我收到的音频数据中的任何延迟都会使音频混乱.遗憾的是,我不能只是停止写入缓冲区,因为如果我让缓冲区继续播放,它将只播放一秒钟之前留下的旧数据(圆形和所有内容).所以我需要知道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;
Run Code Online (Sandbox Code Playgroud)

NotificationThread是一个执行WaitForSingleObject的线程.CreateEvent创建一个新的事件句柄并使其成为WaitForSingleObject将开始等待而不是前一个.ReachedLastWrittenByte是我的应用程序中定义的过程.线程启动一个关键部分,并在触发通知时调用它.(调用WaitForSingleObject时超时为20ms,这样我可以在调用CreateEvent时更新句柄.)

ReachedLastWrittenByte()执行以下操作:

DSCurrentBuffer.Stop;
LastWrittenByte := 0;
Run Code Online (Sandbox Code Playgroud)

当我的通知被触发并且我在我正在使用的辅助缓冲区上调用Stop时,音频仍然会循环显示主缓冲区中的剩余数据......

即使这样,通知也没有被正确触发.如果我停止从发送这些音频消息的其他应用程序广播音频数据,它只会循环遍历缓冲区中的剩余部分.所以基本上,无论如何,它都会超过我设置的最新通知(lastwrittenbyte).在播放时,它偶尔会停止,填充缓冲区,然后开始播放...并跳过它刚刚缓冲的数据的半秒,然后继续播放缓冲区填满后进入的数据(所以它填充缓冲区,但显然不关心在它开始填充新数据之前播放它的内容.是的.我也不知道.)

我在这里似乎缺少什么?是否使用DirectSound Notificatiosn来查明最近写入的字节何时播出无效?你认为有一些方法可以使用流式缓冲区进行这种缓冲.

Mic*_*hre 10

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

有三个问题:首先,我正在查看播放光标位置以确定播放是否已达到最后写入的字节.这不太正常,因为当播放光标显示当前正在播放的内容时,它不会告诉您要播放的数据集的最后一个字节是什么.写入光标始终位于播放光标的前面.播放光标和写入光标之间的区域被锁定以进行播放.

换句话说,我正在查看错误的光标.

其次,这里的主要原因是内容不能正常工作,就是我用DSBLOCK_FROMWRITECURSOR标志写数据.这样做是允许我从写入光标和向前写入数据.每次.

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

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

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

所以我取消了通知并扔进了一个每30ms触发一次的高分辨率计时器(我收到的数据包和根据我们的协议提取要播放的音频将始终以32 ms的间隔发送,所以任何事情都是如此低于32毫秒真的适用于这个计时器.)

我用一个简单的BytesInBuffer变量跟踪剩下多少数据.当我的计时器触发时,我检查自上次定时器触发后写入光标(我之前所说的基本上是真正的播放光标)已经提前了多少,并从我的BytesInBuffer var中删除了这个数字.

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

if (BytesInBuffer <= 0) or ((WritePos >= LastWrittenByte) and (WritePos-PosDiff <= LastWrittenByte)) then
  DSBuffer.Stop;
Run Code Online (Sandbox Code Playgroud)

所以那里.

它总是很难找到人们问同样的问题,因为你最终只是"永远不要解决它",而不是留下答案.

这是希望将来可能启发其他人.

  • “发现人们问与你相同的问题,最终却说‘没关系,我解决了它’,而不留下答案,这总是很糟糕的。” &lt;---- +1。 (2认同)