如何避免 DirectSound 中的失真和卡顿?

Posted

技术标签:

【中文标题】如何避免 DirectSound 中的失真和卡顿?【英文标题】:How can I avoid distortion and stuttering in DirectSound? 【发布时间】:2015-01-20 06:54:43 【问题描述】:

我有一个使用 C 语言编写的 DirectSound 应用程序,在 Windows 7 上运行。该应用程序只是捕获一些声音帧,然后播放它们。为了对捕获结果进行完整性检查,我将 PCM 数据写入一个文件,我可以使用 aplay 在 Linux 中播放该文件。

不幸的是,声音断断续续,有时包含口吃(并且在 Linux 中播放速度不正确)。奇怪的是,如果 PCM 数据在捕获时没有在播放缓冲区中播放,则在播放捕获文件时观察到的失真量会更少。

这是我的 WAVEFORMATEX 的初始化:

memset(&wfx, 0, sizeof(WAVEFORMATEX)); 
wfx.cbSize = 0;
wfx.wFormatTag = WAVE_FORMAT_PCM; 
wfx.nChannels = 1; 
wfx.nSamplesPerSec = sampleRate; 
wfx.wBitsPerSample = sampleBitWidth;
wfx.nBlockAlign = (wfx.nChannels * wfx.wBitsPerSample) / 8; 
wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign code here

sampleRate 为 8000,sampleBitWidth 为 16。

我使用相同的结构创建了一个捕获和播放缓冲区,捕获缓冲区有 3 个通知位置。我开始捕捉:

lpDsCaptureBuffer->Start(DSCBSTART_LOOPING);

然后我启动一个播放线程,该线程在与通知点关联的事件上调用 WaitForMultipleObjects。收到通知后,我重置所有事件,并将 1 或 2 块捕获缓冲区复制到本地缓冲区,然后将它们传递给播放例程:

void playFromBuff(LPVOID captureBuff,DWORD captureLen) 
  LPVOID playBuff;
  DWORD playLen;
  HRESULT hr;

  hr = lpDsPlaybackBuffer->Lock(0L,captureLen,&playBuff,&playLen,NULL,0L,0L);

  memcpy(playBuff,captureBuff,playLen);
  hr = lpDsPlaybackBuffer->Unlock(playBuff,playLen,NULL,0L);
  hr = lpDsPlaybackBuffer->SetCurrentPosition(0L);
  hr = lpDsPlaybackBuffer->Play(0L,0L,0L);

(省略了一些错误检查)。

请注意,播放缓冲区没有通知位置。每次从捕获缓冲区中获取块时,我都会从位置 0 开始锁定播放缓冲区。

由 WaitForMultipleObjects 保护的捕获代码如下所示:

    lpDsCaptureBuffer->GetCurrentPosition(NULL,&readPos);

    hr = lpDsCaptureBuffer->Lock(...,...,&captureBuff1,&captureLen1,&captureBuff2,&captureLen2,0L);

其中省略号包含涉及当前和先前看到的读取位置的计算。我忽略了那些可能错误的计算——我怀疑这就是问题所在。

我的通知位置是 1024 的倍数。但报告的读取位置是 1500、2500 和 3500。所以如果我看到读取位置为 1500,这是否意味着我可以从字节 0 读取到 1500。当接下来我看到 2500,这是否意味着我应该从 1501 读取到 2500?为什么这些阅读位置与我的通知位置不完全对应?这里的正确算法是什么?

我尝试了一种更简单的替代方法,即在捕获缓冲区已满时停止捕获,而无需其他通知位置。但这意味着,我认为,允许一些声音逃脱捕获。

【问题讨论】:

【参考方案1】:

我的通知位置是 1024 的倍数。但报告的读取位置是 1500、2500 和 3500。所以如果我看到读取位置为 1500,这是否意味着我可以从字节 0 读取到 1500。当接下来我看到 2500,这是否意味着我应该从 1501 读取到 2500?为什么这些阅读位置与我的通知位置不完全对应?这里的正确算法是什么?

DirectSound API 现在是其他“真实”音频捕获 API 之上的兼容层。这意味着内部音频捕获会填充一些缓冲区(尤其是 500 的倍数),然后将填充的缓冲区传递给 DirectSound 捕获,然后再将它们报告给您。这就解释了为什么您会看到读取位置是 500 的倍数,因为 DirectSound 本身就可以通过这种方式获得数据。

由于您对获取捕获的数据感兴趣,因此您的假设是正确的,即您主要对读取位置感兴趣。您收到通知,并且知道可以安全读取的偏移量。由于捕获 API 是分层的,因此会涉及一些延迟,因为层需要在彼此之间传递大量数据,然后才能将它们提供给您。

【讨论】:

我重新计算了我的捕获缓冲区计算,所以现在,当我将捕获数据保存到一个文件时,在使用 Linux aplay 播放时声音是完美的——如果我不使用Windows 中的播放缓冲区。不知何故,在单独的缓冲区中播放数据的行为会影响捕获的内容。 一些提示:您应该能够使用调试器找到一些东西并跟踪调用和操作的顺序。也许您的线程同步会使您以某种方式丢失某些循环。出于调试目的,您需要从超长捕获缓冲区开始,以确保您没有上溢、下溢等问题。一旦在这个简单的场景中进行了调整,您就可以将其更改为接近您最终想要的。 我将一个 WAV 文件加载到缓冲区中,并尝试以 2 种方式播放它。首先,我尝试了老式的调用 sndPlaySound,听起来不错。接下来,我尝试使用我的 DirectSound 播放缓冲区。尽管音高和速度合适,但它听起来波涛汹涌且失真。会不会是因为仿真层效果不好? 我是这样看的:如果您有一个标准/外部方法可以流畅地播放捕获的数据,那么捕获本身就可以了。如果捕获受到并发播放的影响,那么我会研究导致捕获延迟的线程问题和死锁。如果只是播放有问题,那么那里应该有一个错误。请注意,在回放时,您再次处理分层 API,您必须提前准备好数据并远离缓冲数据(20-30-50 毫秒),否则数据在实际回放时到达较晚,您会听到断断续续的声音。 请参阅相关 Q What is the smallest audio buffer needed to produce Tone sound without distotions with WaveOUT API 以获取另一个旧 API,这同样适用于 DirectSound 播放 - 数据预加载时间最短,并且做得少于此时间会导致由于迟到而播放静音片段数据到达。

以上是关于如何避免 DirectSound 中的失真和卡顿?的主要内容,如果未能解决你的问题,请参考以下文章

DirectSound:将音量设置为百分比

将 pcm 样本链接在一起时避免静态和失真

WPF 绘图效率的问题 如何速度快而且又避免卡顿

COM 和 DirectSound

Android中的相机,如何获得最佳尺寸,预览尺寸,图片尺寸,视图尺寸,图像失真

使用 DirectSound 进行音高转换