将低延迟音频从一个 CoreAudio 设备路由到另一个
Routing low-latency audio from one CoreAudio device to another
首先,一些背景信息:我正在编写一个 MacOS/X 应用程序,它使用 CoreAudio 从 CoreAudio 设备的输入流接收音频信号,对音频进行一些实时处理,然后发送它返回到 CoreAudio 设备的输出流供用户收听。
此应用程序使用较低级别的 CoreAudio API(即 AudioDeviceAddIOProc
、AudioDeviceStart
等 -- 而不是 AudioUnits)来获取对用户指定的 CoreAudio 设备的独占访问权,将其设置为所需的采样率(96kHz),并做它的事情。效果很好,我对它的性能很满意。
但是,我的程序目前有一个限制 -- 它一次只能使用一个 CoreAudio 设备。我想做的是扩展我的应用程序,以便用户可以彼此独立地选择他的 "input CoreAudio device" 和他的 "output CoreAudio device",而不是仅限于使用同时提供两者的单个 CoreAudio 设备输入音频源和输出音频接收器。
我的问题是,执行此操作的推荐技术是什么?我可以要求两个 CoreAudio 设备都可设置为相同的采样率,但即使我这样做了,我想我也必须处理各种问题,例如:
集成来自两个设备的单独 AudioDeviceStart()
发起的回调,我怀疑它们不会以任何明确定义的顺序调用,甚至可能相互同时调用(?)。我需要以某种方式将音频从一个回调传递到另一个回调,理想情况下不会显着增加音频延迟。
处理两个设备采样时钟速率的差异。例如。即使两个设备名义上都设置为 96kHz 采样率,我怀疑实际上可能是这种情况,例如上游设备以 95.99999kHz 的频率产生样本,而下游设备以 96.000001kHz 的频率消耗它们(反之亦然),这最终会导致我得到 "not enough" 或 "too many" 样本在给定的渲染回调期间馈送下游设备,导致故障。
我还没有考虑的任何其他陷阱
其他 MacOS/X 程序如何处理这些问题?
前段时间我用 C 语言玩过 playground audiomixer 的概念验证。这一切都没有完成,但事情确实有效。该库使用可用的最低核心音频 API,因此确实有 AudioDeviceCreateIOProcID
和 AudioObjectAddPropertyListener
.
之类的东西
简而言之,这个游乐场允许我使用 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 轮询线程(用于控制、显示等)。我通常尝试先开始输出,然后用静音填充它直到输入开始提供样本,然后交叉淡入。然后在输入停止后交叉淡出再次静音。
首先,一些背景信息:我正在编写一个 MacOS/X 应用程序,它使用 CoreAudio 从 CoreAudio 设备的输入流接收音频信号,对音频进行一些实时处理,然后发送它返回到 CoreAudio 设备的输出流供用户收听。
此应用程序使用较低级别的 CoreAudio API(即 AudioDeviceAddIOProc
、AudioDeviceStart
等 -- 而不是 AudioUnits)来获取对用户指定的 CoreAudio 设备的独占访问权,将其设置为所需的采样率(96kHz),并做它的事情。效果很好,我对它的性能很满意。
但是,我的程序目前有一个限制 -- 它一次只能使用一个 CoreAudio 设备。我想做的是扩展我的应用程序,以便用户可以彼此独立地选择他的 "input CoreAudio device" 和他的 "output CoreAudio device",而不是仅限于使用同时提供两者的单个 CoreAudio 设备输入音频源和输出音频接收器。
我的问题是,执行此操作的推荐技术是什么?我可以要求两个 CoreAudio 设备都可设置为相同的采样率,但即使我这样做了,我想我也必须处理各种问题,例如:
集成来自两个设备的单独
AudioDeviceStart()
发起的回调,我怀疑它们不会以任何明确定义的顺序调用,甚至可能相互同时调用(?)。我需要以某种方式将音频从一个回调传递到另一个回调,理想情况下不会显着增加音频延迟。处理两个设备采样时钟速率的差异。例如。即使两个设备名义上都设置为 96kHz 采样率,我怀疑实际上可能是这种情况,例如上游设备以 95.99999kHz 的频率产生样本,而下游设备以 96.000001kHz 的频率消耗它们(反之亦然),这最终会导致我得到 "not enough" 或 "too many" 样本在给定的渲染回调期间馈送下游设备,导致故障。
我还没有考虑的任何其他陷阱
其他 MacOS/X 程序如何处理这些问题?
前段时间我用 C 语言玩过 playground audiomixer 的概念验证。这一切都没有完成,但事情确实有效。该库使用可用的最低核心音频 API,因此确实有 AudioDeviceCreateIOProcID
和 AudioObjectAddPropertyListener
.
简而言之,这个游乐场允许我使用 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 轮询线程(用于控制、显示等)。我通常尝试先开始输出,然后用静音填充它直到输入开始提供样本,然后交叉淡入。然后在输入停止后交叉淡出再次静音。