算法 - 使用外部编解码器/调制解调器处理抖动和漂移

Posted

技术标签:

【中文标题】算法 - 使用外部编解码器/调制解调器处理抖动和漂移【英文标题】:Algorithm - Handling Jitter and Drift with External Codec/Modem 【发布时间】:2014-09-13 21:43:00 【问题描述】:

我正在用 C 语言编写一个小模块来处理全双工音频系统的抖动和漂移。它作为一个非常原始的语音聊天模块,连接到一个使用独立时钟的外部调制解调器,独立于我的主系统时钟(即:它不从系统主时钟的奴隶)。

源代码基于此处在线提供的现有示例:http://svn.xiph.org/trunk/speex/libspeex/jitter.c

我有 4 个音频流:

网络上行(我的声音,经过处理,送到远端扬声器) 网络下行(远端的声音,在处理之前,来到我这里) 扬声器输出(远端的声音,经过处理后,发送到本地扬声器) 麦克风输入(我的声音,在处理之前,来自本地麦克风)

我有两个独立的执行线程。一个处理本地设备和缓冲区(即:将处理后的音频播放到扬声器,并从麦克风捕获数据并将其传递给 DSP 处理库以消除背景噪声、回声等)。另一个线程负责拉取网络下行信号并将其传递给处理库,并从库中取出处理后的数据并通过上行连接推送。

两个线程使用互斥锁和一组共享的循环/环形缓冲区。我正在寻找一种方法来实现可靠(安全可靠)的抖动和漂移校正机制。抖动是指具有可变占空比的时钟,但与理想时钟的频率相同。

我需要纠正的另一个潜在问题是漂移,假设两个时钟都使用理想的 50% 占空比,但它们的基本频率偏离了 ±5%。

最后,这两个问题可以同时发生。理想的方法是什么?我目前的方法是使用一种抖动缓冲器。它们只是实现移动平均值以计算其平均“填充”水平的数据缓冲区。如果一个线程试图从缓冲区读取,但没有足够的数据可用并且存在缓冲区下溢,我只是通过提供一个备用的清零数据包或复制一个数据包即时为其生成数据(即:丢包隐藏)。如果数据来得太快,我会丢弃整个数据包,然后继续。这会处理抖动部分。

问题的后半部分是漂移校正。这就是平均填充水平指标派上用场的地方。对于所有缓冲区,我可以计算各种缓冲区中的相对增长/减少水平,并每隔一段时间添加或减去少量样本,以使所有缓冲区水平徘徊在一个共同的平均“填充”水平附近。

这种方法是否有意义,是否有更好的或“行业标准”的方法来处理这个问题?

谢谢。

参考文献


    字时钟 - 抖动和频率漂移有什么区别?,2014 年 9 月 13 日访问,<http://www.apogeedigital.com/knowledgebase/fundamentals-of-digital-audio/word-clock-whats-the-difference-between-jitter-and-frequency-stability/> Jitter.c,于 2014 年 9 月 13 日访问,<http://svn.xiph.org/trunk/speex/libspeex/jitter.c>

【问题讨论】:

【参考方案1】:

我遇到了一个类似的问题,虽然公认更简单。我无法完全回答你的问题,但我希望分享我对我遇到的一些实际问题的解决方案对你有所帮助。

去年我正在开发一个系统,该系统应该同时从多个音频设备录制和渲染到多个音频设备,每个设备都可能在不同的时钟上打勾。最明显的例子是 2 个设备上的双工流,但它也只处理多个输入/输出。总而言之,比您的情况要简单一些(单线程且无网络 i/o)。最后,我不相信处理超过 2 个设备比 2 个更难,任何具有多个时钟的系统都必须处理相同的问题。

我学到的一些东西:

选择一个流并将其时钟指定为“真实”(即,将所有其他流同步到一个公共主时钟)。如果您不这样做,您将没有明确定义的“当前样本位置”概念,没有它就没有什么可同步的。这还有一个好处,即系统中至少有一个流始终是干净的(没有丢弃/填充样本)。 您使用附加缓冲区处理抖动的方法是正确的。如果没有它,即使在具有相同标称采样率的流上,您也会不断地丢弃/填充。 考虑您是否也想为“主”流引入这样的抖动缓冲区。这样做意味着在主流中引入人为延迟,不这样做意味着您的其余流将落后。 我不确定丢弃整个数据包是否是个好主意。为什么不尝试用尽尽可能多的样本呢?尤其是对于大数据包大小,这远不那么明显。 为了详细说明上述内容,我被以下情况严重咬伤:假设 s1 (master) 每秒产生 48000 帧,s2 每 2 秒产生 96000 帧。第 1 轮:从 s1 读取 48000,从 s2 读取 0。第 2 轮:从 s1 读取 48000,从 s2 读取 96000 -> 溢出。丢弃整个数据包。第 3 轮:从 s1 读取 48000,从 s2 读取 0。等等。显然这是一个人为的例子,但我遇到了使用这种方案平均丢弃 50% 的辅助流数据的情况。抖动缓冲器的引入确实有帮助,但并没有完全解决这个问题。请注意,这与时钟抖动/偏差并不严格相关,只是某些驱动程序喜欢定期更新其填充值,它们不会准确地向您报告硬件缓冲区中的实际内容。 当您确实有时钟抖动但您选择的 API 不允许您控制数据包大小(例如,允许您请求的帧少于实际可用的帧)时,会发生此问题的另一种变化。假设 s1(主)记录 @1000 Hz 和 s2 每秒交替 @1000 和 1001hz。第 1 轮,从两者中读取 1000 帧。第 2 轮,从 s1 读取 1000 帧,从 s2 读取 1001 -> 溢出。等等,平均而言,您将在 s2 上转储大约 50% 的帧。请注意,如果您的 API 允许您说“给我 1000 个样本,即使我知道您有更多样本”,这并不是什么大问题。但是,这样做最终会溢出硬件输入缓冲区。 为了最大程度地控制何时放置/填充,我发现始终保持输入缓冲区为空和输出缓冲区为满是最简单的方法。这样,所有丢弃/填充都发生在抖动缓冲区中,您至少可以了解并控制正在发生的事情。 如果可能,请尝试分离您的程序逻辑:困难的部分是找出填充/删除样本的位置。设置好之后,就可以轻松尝试不同的 pad/drop、sample-and-hold、插值等变化。

总而言之,我会说您的解决方案看起来非常合理,尽管我不确定“丢弃整个数据包的事情”,而且我肯定会选择一个流作为要同步的主流。为了完整起见,这是我最终提出的解决方案:

1 假设每个流上有一个大小为 J 的抖动缓冲区。 2:等待大小为 M 的数据包在主流上可用(M 通常源自流延迟)。我们将向应用程序提供 M 帧的输入/输出。我没有在主流上实现额外的缓冲区。 3:对于所有输入流:设 H 是硬件缓冲区中记录的帧数,B 是当前在抖动缓冲区中记录的帧数,A 是应用程序可用的帧数:A 等于H + B。 3a:如果 A 3b:如果 A == M,则向应用程序提供 A 帧。抖动缓冲区现在为空。 3c:如果 A > M 但 (A - M) 3d:如果 A > M 且 (A - M) > J,则输入溢出。向应用程序提供 M 个记录的帧,剩余的帧将 J/2 放回抖动缓冲区,我们用完 M + J/2 帧,我们丢弃 A - (M + J/2) 帧作为溢出。不要试图让抖动缓冲区保持满,因为设备可能很快,我们不想在下一轮再次溢出。 4:3 的倒数排序:对于输出,快速设备将下溢,慢速设备将溢出。 A、H 和 B 是同一个东西,但这次它们不代表可用的帧,而是可用的填充(例如,我可以为应用程序提供多少帧来写入)。 不惜一切代价尽量保持硬件缓冲区已满。

这个方案对我来说效果很好,尽管有几点需要考虑:

它涉及大量的簿记。确保对于输入缓冲区,数据始终来自硬件->抖动缓冲区->应用程序,而输出始终来自应用程序->抖动缓冲区->硬件。如果有足够的样本可从硬件直接提供给应用程序,则很容易误以为您可以“跳过”抖动缓冲区中的帧。这实际上会打乱音频流中帧的时间顺序。 该方案在辅助流上引入了可变延迟,因为我尝试尽可能长时间地推迟填充/删除的时刻。这可能是也可能不是问题。我发现在实践中推迟这些操作会产生更好的结果,这可能是因为只有少数样本的许多“小”故障比偶尔的较大打嗝更烦人。

此外,PortAudio(一个开源音频项目)也实施了类似的方案,请参阅http://www.portaudio.com/docs/proposals/001-UnderflowOverflowHandling.html。浏览邮件列表并查看那里出现的问题/解决方案可能是值得的。

请注意,到目前为止我所说的一切都只是关于与音频硬件的交互,我不知道这是否同样适用于网络流,但我看不出有什么明显的原因。只需选择 1 个音频流作为主音频流并将另一个音频流同步到它,并对网络流执行相同的操作。这样,您最终将得到两个或多或少独立的系统,它们仅通过环形缓冲区连接,每个系统都有一个内部一致的时钟,每个系统都在自己的线程上运行。如果您的目标是低音频延迟,您还需要放弃互斥锁并选择某种无锁 fifo。

【讨论】:

非常彻底。非常感谢!【参考方案2】:

我很想知道这是否可能。不过,我会投入我的两点。

我是一名新手程序员,但学习音频工程/交互式音频。

我的第一个假设是这是不可能的。至少不是在一个样本到一个样本的基础上。尤其不适用于复杂的音频数据和波形,例如人类语音。该程序可能无法预期波形“应该”是什么样子。

这就是为什么会有带有温度控制内部时钟的高端音频接口的原因。

另一方面,也许有一个库可以检测抖动的症状,不知何故...... 在这种情况下,我会很想知道。

至于漂移校正,也许我不了解编程方面的某些内容,但是您不应该以特定的采样率提取音频吗?我相信采样率/漂移是在硬件级别处理的。

我真的希望这会有所帮助。你可能需要把我引到离家更近的地方。

【讨论】:

是的!出于某种原因,它被称为保真度或数据的“损失”。为了将数据“恢复”到其原始形式,您需要更多数据——以前的样本、大样本或重复数据或其他东西来解释损失。此类数据可能包括有关先前用于压缩数据的编解码器效果的知识。

以上是关于算法 - 使用外部编解码器/调制解调器处理抖动和漂移的主要内容,如果未能解决你的问题,请参考以下文章

2022-04-25

让 WebRTC 使用外部的音视频编解码器

使用 windows 编解码器解码音频文件

直播常见面试题

Android音频开发(三)——音频编解码

视频的编解码-编码篇