将低延迟音频从一个 CoreAudio 设备路由到另一个

Routing low-latency audio from one CoreAudio device to another

首先,一些背景信息:我正在编写一个 MacOS/X 应用程序,它使用 CoreAudio 从 CoreAudio 设备的输入流接收音频信号,对音频进行一些实时处理,然后发送它返回到 CoreAudio 设备的输出流供用户收听。

此应用程序使用较低级别的 CoreAudio API(即 AudioDeviceAddIOProcAudioDeviceStart 等 -- 而不是 AudioUnits)来获取对用户指定的 CoreAudio 设备的独占访问权,将其设置为所需的采样率(96kHz),并做它的事情。效果很好,我对它的性能很满意。

但是,我的程序目前有一个限制 -- 它一次只能使用一个 CoreAudio 设备。我想做的是扩展我的应用程序,以便用户可以彼此独立地选择他的 "input CoreAudio device" 和他的 "output CoreAudio device",而不是仅限于使用同时提供两者的单个 CoreAudio 设备输入音频源和输出音频接收器。

我的问题是,执行此操作的推荐技术是什么?我可以要求两个 CoreAudio 设备都可设置为相同的采样率,但即使我这样做了,我想我也必须处理各种问题,例如:

其他 MacOS/X 程序如何处理这些问题?

前段时间我用 C 语言玩过 playground audiomixer 的概念验证。这一切都没有完成,但事情确实有效。该库使用可用的最低核心音频 API,因此确实有 AudioDeviceCreateIOProcIDAudioObjectAddPropertyListener.

之类的东西

简而言之,这个游乐场允许我使用 MacOS 已知的多个音频设备,并在它们之间路由一个或多个音频流,同时沿途通过不同类型的 "nodes"(想想矩阵混音器节点例如)。

首先回答大家的问题

AudioDeviceStart() 发起的回调将从不同的(随机)线程触发。此外,回调不会以确定的顺序调用。我还发现回调之间的差异可能会有很大差异(似乎取决于数据的音频设备 providing/asking)。 为了解决这个问题,我使用了 lock-free(即使用原子计数器)ringbuffer。

您对不同时钟域的关注是非常真实的。以 96KHz 频率 运行 连接的两个设备将以不同的速度 运行。这可能会持续很长时间,但最终其中之一会 运行 数据不足并开始出现故障。如果外部设备没有一起外部同步,例如使用 word 或 ptp,它们将 运行 在它们自己的时域中。 要在不同时域之间传递音频,您必须 async-sample-rate-convert 音频数据。而且 SRC 需要有可能以非常小的比率进行转换并在此过程中进行调整。 Soxr 就是其中一位做得很好的人。在 Core Audio 的世界里有一个 VarispeedNode,它允许你做基本相同的事情。 async-src 解决方案的一大缺点是它引入了延迟,但是也许您可以指定 "low-latency".

在您的情况下,不同音频设备的同步将是最大的挑战。在我的例子中,我发现不同音频设备的回调差异太大 select 一个 "clock-master" 所以我最终通过仔细计时处理周期的执行来创建一个独立的时域。为此,我使用了低级计时机制,如 mach_wait_until()mach_absolute_time()(相关文档不多)。

聚合设备

但是,可能还有其他解决方案。查看来自 CoreAudio 框架的 AudioHardware.h 中的文档,似乎有一种方法可以使用 AudioHardwareCreateAggregateDevice() 以编程方式创建聚合设备。这允许您让 MacOS 处理不同音频设备的同步。另请注意 kAudioAggregateDeviceIsPrivateKey 键,它允许您创建聚合设备而无需将其发布到整个系统。因此,该设备不会出现在音频 MIDI 设置中(我认为)。另请注意,当创建聚合的进程停止 运行ning 时,此键会使聚合消失。它可能是也可能不是您所需要的,但这将是一种使用多个音频设备的非常可靠的实现方式。 如果我再写这个软件,我肯定会研究这种同步方式。

其他问题和提示

通常,在处理 low-latency 音频时,您希望实现尽可能确定的行为。但我相信你知道这一点。

另一个问题是 Core Audio api 的文档在 Apple 的开发者网站 (https://developer.apple.com/documentation/coreaudio/core_audio_functions?language=objc) 上不可用。为此,您必须深入研究 Core Audio 框架的 headers,在那里您会找到很多关于使用 API.

的有用文档

在我的机器上 headers 位于:/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/CoreAudio.framework/Versions/A/Headers

进一步阅读:

http://atastypixel.com/blog/four-common-mistakes-in-audio-development http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing https://developer.apple.com/library/archive/qa/qa1467/_index.html

"leaky bucket" 算法与分数插值重采样器相结合,可用于动态调整非常微小(和非常数!)的采样率差异。速率的较大跳跃或跳跃通常需要更复杂的错误隐藏策略。无锁 circular/ring 缓冲区的许多变体使用原子原语在异步音频线程之间传递数据。我使用 mach 计时器或 CADisplay link 计时器来驱动 UI 轮询线程(用于控制、显示等)。我通常尝试先开始输出,然后用静音填充它直到输入开始提供样本,然后交叉淡入。然后在输入停止后交叉淡出再次静音。