PC端音视频相关应用往往会涉及到系统音频采集相关需求。例如音视频通信应用在屏幕共享场景下,用户除了共享屏幕内容之外,往往需要共享音频数据。目前macOS没有系统API可以直接获取输出到播放设备的音频数据,这就需要通过别的办法来采集系统音频。由于macOS开发相关资料较少,完成这个需求也花了点时间,在这里将思路做个分享,希望能帮助到有这方面需求的朋友。

技术背景

macOS音频设备管理

macOS支持管理多个音频输入输出设备,可以使用“音频 MIDI 设置”来设置如麦克风和多声道音频接口。
image.png

音频 MIDI 设置”界面如上图所示,左侧列表展示了所有音频输入输出设备,例如内建麦克风、扬声器、AirPods、通过数据线连接端iPhone设备,右侧界面支持调节设备端具体参数,例如音量等。

我们可以通过右键选择设备,将其设为系统默认的输入输出设备。设为默认设备后,设备列表上会有一个小图标显示,同时,系统会默认在对应的设备上采集、播放音频数据。当然,应用也依然可以选择特定设备采集、播放音频,例如Abode公司的Audition中可以通过设置选择输入输出设备。

switchaudio-osx

switchaudio-osx是一款开源项目,支持命令行设置macOS默认输入输出音频设备。这个开源项目为我们通过代码切换系统音频设备提供了极大帮助。

项目地址:https://github.com/deweller/switchaudio-osx

KEXT内核扩展

KEXT是macOS的内核扩展程序,通常用于系统驱动程序。内核扩展运行于内核态,支持动态加载。

我们可以在Xcode中选择“general kernel extension”创建内核扩展,基于IOKit接口实现驱动程序,生成的产物以kext作为后缀名。

加载驱动需要首先将kext文件拷贝到系统目录(需要管理员权限):

1
/Library/Extensions 

加载驱动:

1
sudo kextload /path/to/kext.kext

卸载驱动:

1
sudo kextunload /path/to/kext.kext

Soundflower

Soundflower是mac端开源的kext内核扩展程序,它虚拟了一套音频采集、播放设备驱动,直接将播放数据传给采集设备,实现音频数据环路。通过将系统默认输入输出设备设置为Soundflower,就可以采集到系统播放的音频数据。

项目地址:https://github.com/RogueAmoeba/Soundflower-Original

Update(2021.6.9):

新发布的macOS11安全管理策略变得更为严格,由于kext运行在内核态,系统对其做了更多限制,不再建议使用。官方说明如下:macOS 中的系统和内核扩展

作为替代,建议使用基于CoreAudio框架实现的虚拟设备插件。CoreAudio插件运行在用户态,插件加载后,作用于Soundflower基本一致。

相关资料可以参考:Building an Audio Server Plug-in and Driver Extension

方案实现

介绍完以上的技术背景,其实就可以设计出macOS系统音频数据采集方案:

image.png

如上图所示,用户App通过mac系统播放音频。

我们可以在启动系统音频采集功能时将系统音频播放设备设置为Soundflower虚拟扬声器,同时在App中将输入设为Soundflower虚拟麦克风和硬件麦克风,将混音后的音频数据输出到硬件扬声器。关闭系统音频采集功能将系统设置还原。理论上,这就已经可以满足我们的需求了。

关键调用流程

为了实现以上方案,有以下关键调用流程:

  1. 第一次调用内录接口时,请求管理员权限,将Soundflower.kext内核扩展拷贝到系统目录
  2. 调用kextload命令加载Soundflower.kext
  3. 将系统默认音频播放设备设置为Soundflower虚拟扬声器
  4. App中将音频采集设备设置为Soundflower虚拟麦克风
  5. App中如果有硬件麦克风采集需求,则另起一路硬件麦克风采集
  6. App中将音频播放设备设置为硬件扬声器

需要关闭该功能时,需要执行如下调用流程:

  1. 将系统默认输入输出设备还原
  2. 卸载Soundflower.kext

以上调用流程并不复杂,关键代码都可以从以上介绍的开源项目中找到参考,需要注意一下几点:

加载/卸载kext需要系统提权,macOS提权可以选择AuthorizationRef或者苹果官方推荐的SMJobBless

切换系统默认音频设备代码实现可以参考switchaudio-osx项目中audio_switch.c相关代码

至于mac端音频采集播放的代码实现就相对比较复杂了,不过幸运的是,谷歌WebRTC项目中audio_device_mac.cc已经有了一套实现

总结

文本提供了macOS采集/录制系统音频数据的一种实现思路。具体代码实现过程中,还是有很多工作需要做的,例如系统提权如果要做的比较友好就需要用到SMJobBless启动守护进程来实现。当然,这属于另外一个话题了,在此不再赘述。