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

Posted

技术标签:

【中文标题】将低延迟音频从一个 CoreAudio 设备路由到另一个【英文标题】:Routing low-latency audio from one CoreAudio device to another 【发布时间】:2020-02-19 02:36:21 【问题描述】:

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

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

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

我的问题是,这样做的推荐技术是什么?我可以要求两个 CoreAudio 设备都设置为相同的采样率,但即使我这样做了,我认为我将不得不处理各种问题,例如:

集成来自两个设备的单独的AudioDeviceStart()-initiated 回调,我怀疑它们不会以任何明确定义的顺序调用,甚至可能相互同时调用(?)。我需要以某种方式将音频从一个回调传递到另一个回调,理想情况下不会显着增加音频延迟。

处理两个设备的采样时钟速率差异。例如。即使两个设备名义上都设置为 96kHz 采样率,我怀疑实际上可能是这种情况,例如上游设备以 95.99999kHz 生成样本,而下游设备以 96.000001kHz 消耗它们(反之亦然),这最终会导致我最终得到“不够”或“太多”的样本来馈送在给定的渲染回调期间的下游设备,导致故障。

我还没有考虑过的任何其他问题

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

【问题讨论】:

【参考方案1】:

前段时间,我用 C 语言玩了一个概念验证的 Playground 混音器。这一切都还没有完成,但事情确实有效。该库使用可用的最低核心音频 API,因此确实使用了诸如 AudioDeviceCreateIOProcIDAudioObjectAddPropertyListener 之类的东西。

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

首先,回答您的问题

AudioDeviceStart() 发起的回调将从不同的(随机)线程触发。此外,不会以确定的顺序调用回调。我还发现回调之间的差异可能会有很大差异(似乎取决于提供/请求数据的音频设备)。 为了解决这个问题,我使用了无锁(即使用原子计数器)环形缓冲区。

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

在您的情况下,不同音频设备的同步将是最大的挑战。就我而言,我发现不同音频设备的回调变化太大,无法选择一个作为“时钟主控”,所以我最终通过仔细计时处理周期的执行来创建一个独立的时域。为此,我使用了 mach_wait_until()mach_absolute_time() 之类的低级计时机制(关于此的文档不​​多)。

聚合设备

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

其他问题和提示

通常,在处理低延迟音频时,您希望获得尽可能确定性的行为。但我相信你知道这一点。

另一个问题是,Apple 的开发者网站 (https://developer.apple.com/documentation/coreaudio/core_audio_functions?language=objc) 上没有 Core Audio api 的文档。为此,您必须深入了解 Core Audio 框架的标头,您会在其中找到许多有关使用 API 的有用文档。

在我的机器上,标题位于:/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

【讨论】:

我还发现了这篇关于聚合设备的文章:flyaga.info/… 不错的一个!我猜私人聚合不起作用的问题可能是一个问题?或许同时解决了…… 另一个有用的链接,供参考:web.archive.org/web/20140716012404/http://daveaddey.com/?p=51【参考方案2】:

“漏桶”算法与分数插值重采样器相结合,可用于动态调整非常微小(且非常数!)的采样率差异。更大的速率跳跃或跳跃通常需要更复杂的错误隐藏策略。使用原子原语在异步音频线程之间传递数据的无锁循环/环形缓冲区有很多变化。我使用马赫计时器或 CADisplay 链接计时器来驱动 UI 轮询线程(用于控件、显示等)。我通常尝试先开始输出,然后用静音填充它,直到输入开始提供样本,然后交叉淡入。然后在输入停止后再次交叉淡出以静音。

【讨论】:

以上是关于将低延迟音频从一个 CoreAudio 设备路由到另一个的主要内容,如果未能解决你的问题,请参考以下文章

使用 ffmpeg 将低延迟 RTSP 视频流式传输到 android

CoreAudio基础概念

使用 CoreAudio 以编程方式在 Swift 中创建聚合音频设备

CoreAudio/OpenAL:如何获取传出的音频信号

如何从外部音频接口访问带有核心音频的各个通道

Core Audio(音频单元)音频会话和 MPVolumeView