【Android 音频】“声音”从何而来

2020-08-05 10:40:07 浏览数 (1)

| 导语 透过本文,全面了解 Android 系统音频录制技能,深入理解王者时刻为什么没有把环境音或者人声录制下来

一、音频量化

音频基础的文章很多,想要了解更多,请自行百度。这里重点关注 PCM 和采样率,因为目前遇到的音频问题都跟这两个有关。 接下来看一张经典的音频采样流程图:

以上就是计算机系统中的音频文件的生成过程:采样、量化、编码。 人耳所能听到的声音,最低的频率是 20Hz ~ 20KHZ,因此音频文件格式的最大带宽是 20KHZ。

根据奈奎斯特的理论,只有采样频率高于声音信号最高频率的两倍时,才能把数字信号表示的声音还原成为原来的声音,所以音频文件的采样率一般在 40~50KHZ,比如最常见的 CD 音质采样率 44.1KHZ。

对声音进行采样、量化过程被称为脉冲编码调制(Pulse Code Modulation),简称PCM。PCM 数据是最原始的音频数据完全无损,所以 PCM 数据虽然音质优秀但体积庞大,为了解决这个问题先后诞生了一系列的音频格式,这些音频格式运用不同的方法对音频数据进行压缩,其中有无损压缩(ALAC、APE、FLAC)和有损压缩(MP3、AAC、OGG、WMA)两种。

二、录音方式

1、系统内录

直接上 Google 官方的文档,大意就通过该接口可以进行实时字幕和游戏内录音。这里不准备深入,因为硬性要求 Android Q 及以上系统版本(文末有 AudioPlaybackCapture 使用实例可以参考):

2、麦克风录制

麦克风录制要特别注意的两个情况:

  • 同时只能有一个实例存在,比如有 GVoice 在录音,那么其他都要歇菜了。官方文档 “共享音频输入” 有详细的说明。
  • 另一种情况就是不可控(可能会录制到 ”黄、爆” 音频)。

2.1、基本概念

Android 系统提供的录制麦克风方式就两种:MediaRecorder 和 AudioRecord:

  • MediaRecorder:简易模式,调用简单,只有开始、结束,录音之后的文件也是指定编码格式,系统播放器可以直接播放。
  • AudioRecord:原始模式,可以暂停、继续,可以实时获取到 PCM 数据然后进行音视频的混合,也是录屏软件经常使用的接口。

2.2、麦克风内录的另类实现

通过上面的 AudioRecord 的代码可以发现,输入源是可以配置的,并且通过接口文档很快就发现 REMOTE_SUBMIX 这个输入源也可以实现内录功能,但是有两个必要条件:

  • 需要系统权限,就是需要在 Android 源码中进行编译的工程才可以获取系统签名权限
  • 会截走扬声器和耳机的声音,也就是说在录音时本地无法播放声音

3、Hook 内录

通过以下 Android 的音频系统架构图可以看出,Android 提供的唯二接口:MediaPlayer 和 AudioTrack,最终都是经过 AudioFlinger 的 AudioStreamOut::write(const void *buffer, size_t numBytes) 把数据传递到硬件层:

通过 inline hook 的形式可以导出音频数据,目前 KM 上已经有同事实践成功,具体可以参考链接: http://km.oa.com/group/22117/articles/show/223181

但是,inline hook 需要操作寄存器并且方案通用性欠佳,所以不建议使用 inline hook。不过,在音频数据流转的过程中截取数据的形式是通用做法,引擎内录就是借鉴的这种做法。在更高的层级进行的获取行为,并且由于层级比较高,所以无法做到一个方案覆盖全的情况。几乎都是跟引擎密切相关,但是也有好的方面,就是稳定性得到了保证并且可以统一多端的获取逻辑。

4、引擎内录

引擎内录可以统一 Android、iOS、PC 多端音频获取逻辑

从上图可以看出,整个获取内音的逻辑很简单,但是,引擎这么多,引擎插件怎么写就成了一个难题

我们先从最简单的 Unity FMOD 开始。

4.1、Unity FMOD 内录

Unity 內置的 Audio 內部使用的是 FMOD,但是没有导出接口,所以无法使用 FMOD 插件的方式。

通过查找 ,Unity 给出类似的接口,简单实现如下,具体的请查看附件 ExportAudioRes.cs 和 FmodSupport.cs

集成的话,只需要把 ExportAudioRes.cs 挂载到 AudioListener 所在的主 Camera 就可以(保存成 PCM 文件是常用的调试技巧):

代码语言:javascript复制
public class ExportAudioRes : MonoBehaviour
{
    ...
    void OnAudioFilterRead(float[] data, int channels)
    {
        FmodSupport.FmodWritePCM(data, data.Length, (uint)channels);
        
        if (DebugInGame)
        {
            // 把音频数据直接保存成 PCM 文件,这样就可以定位是否是音频本身的问题
            this.Write(data);
        }
    }
}

4.2、 FMOD Studio 内录

感觉这是废话:安装 FMOD Studio ,打开工程导入 FMOD for Unity

如果只是测试,可以直接使用 FMOD Studio 自带的样例的 bank 文件,FMOD 在 Unity 里面的配置也比较简单,如下动图所示,设置 bank 路径然后添加事件响应就可以:

接下来是 FMOD 插件实现的关键代码:

4.3、 Wwise 内录

安装 wwise Launcher 之后可以在本地路径下找到 AkDelay 的源码,通过改造源码添加转发器就可以实现自定义插件:

代码语言:javascript复制
D:Program Files (x86)AudiokineticWwise 2017.1.9.6501SDKsamplesPluginsAkDelay
//通过修改上面的工程名,就可以得到一个自定义插件,有需要可以找我要工程源码

这里的关键就变成,如何在自定义插件里面把 “音频转发器” 这个库给拉起来? 为了减少工程之间的依赖,Unity 里面可以通过 PInvoke 的形式动态拉起,但是在 wwise 插件里面就需要另辟蹊径,但是肯定还是想使用动态打开库的形式。直接上代码,大体如下:

另外需要注意的就是 wwise 插件需要挂载到 Master Audio Bus 上,并且不能同时挂载多个,否则录制出来的声音会叠加在一起:

前面只是抛砖引玉。 想要实现一个插件远比上面说的复杂。因为没有考虑版本兼容问题、插件的编译环境、动态获取库代码健壮性、不同平台差异兼容、插件集成等等问题。

5、思考扩展

既然引擎层面都有插件特效,那么 Android 原生应该也有对应的功能才对?是否可以利用呢?

代码语言:javascript复制
//https://developer.android.com/reference/android/media/audiofx/Visualizer
//下面是摘抄于 Google 的文档,Google 早就料到会有人像我这样思考, 还特别提醒:这不是音频录制接口,别瞎用

三、联调案例

两个音频相关的实例:CFM 上出现的音频加速问题和 AOV 上出现的音频叠加问题

如果出现类似的情况,你会如何思考定位问题呢?答案就在上面的内容里,如果有好的思路请留言回复

无处不在的辛普森悖论

走近鹅厂专家 | Ta们靠什么成为专家?

如何通过画像洞察用户价值点

0 人点赞