写点什么

Android 音频架构

用户头像
轻口味
关注
发布于: 20 小时前
Android音频架构

前面《Android 音频 API》介绍了 Android 系统提供的四个层面的音频 API:


  1. Java 层 MediaRecorder&MediaPlayer 系列;

  2. Java 层 AudioTrack&AudioRecorder 系列;

  3. Jni 层 opensles;

  4. JNI 层 AAudio(Android O 引入)


本文基于这些 API 介绍 Android 系统的音频架构。


下面先上这张经典的 Android 系统架构图:



从图上看 Andorid 整个系统层面从下到上分以下四层:


  1. Linux Kernel

  2. 硬件适配层

  3. Framework 层(可分为 Java 层与 C++层)

  4. APP 层


我们上面介绍的四个层面的音频 API 实现均在 Framework 层,其他各层音频相关有哪些功能?当我们调用某一 API 时最终是怎么驱动硬件工作的呢?下面我们先看看系统各层音频相关模块及功能。

1. 各层音频模块

1.1 Java 层

Java 层提供了 android.media API 与音频硬件进行交互。在内部,此代码会调用相应的 JNI 类,以访问与音频硬件交互的原生代码。


  • 源代码目录:frameworks/base/media/java/android/media/

  • AudioManager:音频管理器,包括音量管理、AudioFocus 管理、音频设备管理、模式管理;

  • 录音:AudioRecord、MediaRecorder;

  • 播放:AudioTrack、MedaiPlayer、SoundPool、ToneGenerator;

  • 编解码:MediaCodec,音视频数据 编解码接口。

1.2 JNI 层

与 android.media 关联的 JNI 代码可调用较低级别的原生代码,以访问音频硬件。JNI 位于 frameworks/base/core/jni/ 和 frameworks/base/media/jni 中。


在这里可以调用我们上篇文章介绍的 AAudio 和 OpenSLES 接口。

1.3 Native framework 原生框架层

不管是 Java 层还是 JNI 层都只是对外提供的接口,真正的实现在原生框架层。原生框架可提供相当于 android.media 软件包的原生软件包,从而调用 Binder IPC 代理以访问媒体服务器的特定于音频的服务。原生框架代码位于 frameworks/av/media/libmediaframeworks/av/media/libaudioclient中(不同版本,位置有所改变)。

1.4 Binder IPC

Binder IPC 代理用于促进跨越进程边界的通信。代理位于 frameworks/av/media/libmediaframeworks/av/media/libaudioclient 中,并以字母“I”开头。

1.5 Audio Server

Audio 系统在 Android 中负责音频方面的数据流传输和控制功能,也负责音频设备的管理。这个部分作为 Android 的 Audio 系统的输入/输出层次,一般负责播放 PCM 声音输出和从外部获取 PCM 声音,以及管理声音设备和设置(注意:解码功能不在这里实现,在 android 系统里音频视频的解码是 opencore 或 stagefright 完成的,在解码之后才调用音频系统的接口,创建音频流并播放)。Audio 服务在 Android N(7.0)之前存在于 mediaserver 中,Android N 开始以 audioserver 形式存在,这些音频服务是与 HAL 实现进行交互的实际代码。媒体服务器位于 frameworks/av/services/audioflingerframeworks/av/services/audiopolicy中。


Audio 服务包含 AudioFlinger 和 AudioPolicyService:


  • AudioFlinger:主要负责音频流设备的管理以及音频流数据的处理传输,⾳量计算,重采样、混⾳、⾳效等。

  • AudioPolicyService:主要负责⾳频策略相关,⾳量调节⽣效,设备选择,⾳频通路选择等。

1.6 HAL 层

HAL 定义了由音频服务调用且手机必须实现以确保音频硬件功能正常运行的标准接口。音频 HAL 接口位于 hardware/libhardware/include/hardware 中。详情可参阅 audio.h。

1.7 内核驱动层

音频驱动程序可与硬件和 HAL 实现进行交互。我们可以使用高级 Linux 音频架构 (ALSA)、开放声音系统 (OSS) 或自定义驱动程序(HAL 与驱动程序无关)。


注意:如果使用的是 ALSA,建议将 external/tinyalsa 用于驱动程序的用户部分,因为它具有兼容的许可(标准的用户模式库已获得 GPL 许可)。

2. 音频系统架构的演进

一个好的系统架构,需要尽可能地降低上层与具体硬件的耦合,这既是操作系统的设计目的,对于音频系统也是如此。音频系统的雏形框架可以简单的用下图来表示:

在这个图中,除去 Linux 本身的 Audio 驱动外,整个 Android 音频实现都被看成了 User。因而我们可以认为 Audio Driver 就是上层与硬件间的“隔离板”。但是如果单纯采用上图所示的框架来设计音频系统,对上层应用使用音频功能是不小的负担,显然 Android 开发团队还会根据自身的实际情况来进一步细化“User”部分。具体该怎么细化呢?如果是让我们去细化我们该怎么做呢?


首先作为一个操作系统要对外提供可用的 API,供应用开发者调用。APP 开发者开发的应用我们称 APP,我们提供的 API 姑且叫 Framework。如果 Framework 直接和驱动交互有什么问题呢?


  1. 首先是耦合问题,接口和实现耦合,硬件层有任何变动都需要接口层适配,我们增加一层硬件适配层;

  2. 资源统一管理的问题,如果多个 APP 调用相同 API 使用硬件资源,改怎么分配?增加统一资源管理器,其实就是对应 Android 系统的 Audio Lib 层。


细化后我们发现,整个结构对应的就就是 Android 的几个层次结构,包括应用层、framework 层、库层以及 HAL 层,如下图所示:


我们可以结合目前已有的知识,我们分析 Lib 层和 HAL 层架构主要设计思路。

2.1 Lib 层

framework 层的大多数类,其实只是应用程序使用 Android 库文件的“中介”,它只是个壳子。因为 Android 应用采用 java 语言编写,它们需要最直接的 java 接口的支持,如果我们的 Android 系统支持另一种语言的运行时,那么可以提供另一种语言的接口支持(比如 Go),这就是 framework 层存在的意义之一。但是作为“中介”,它们并不会真正去实现具体的功能,或者只实现其中的一部分功能,而把主要重心放在核心库中来完成。比如上面的 AudioTrack、AudioRecorder、MediaPlayer 和 MediaRecorder 等等在库中都能找到相对应的类,这些多数是 C++语言编写的。


我们再从另一个线索来思考这个问题:我们提供的 API 供应用层调用,那么这个 API 最终运行在应用的进程中。如果多个应用同时使用这个功能就会冲突;再一个允许任何一个进程操作硬件也是个危险的行为。那么真相就浮出了水面:我们需要一个有权限管理和硬件交互的进程,需要调用某个硬件服务必须和我这个服务打交道。这就是 Android 系统的很常用的 C/S 结构以及 Binder 存在的主要原因。Android 系统中的 Server 就是一个个系统服务,比如 ServiceManager、LocationManagerService、ActivityManagerService 等等,以及管理图像合成的 SurfaceFlinger,和今天我们今天介绍的音频服务 AudioFlinger 和 AudioPolicyService。它们的代码放置在frameworks/av/services/audioflinger,生成的最主要的库叫做 libaudioflinger。


这里也提到了分析源码除以模块为线索外的另一种线索以进程为线索。库并不代表一个进程,但是进程则依赖于库来运行。虽然有的类是在同一个库中实现的,但并不代表它们会在同一个进程中被调用。比如 AudioFlinger 和 AudioPolicyService 都驻留于名为 mediaserver 的系统进程中;而 AudioTrack/AudioRecorder 和 MediaPlayer/MediaRecorder 只是应用进程的一部分,它们通过 binder 服务来与其它 audioflinger 等系统进程通信。

2.2 HAL 层

硬件抽象层顾名思义为适配不同硬件而独立封装的一层,音频硬件抽象层的任务是将 AudioFlinger/AudioPolicyService 真正地与硬件设备关联起来,但又必须提供灵活的结构来应对变化。


从设计上来看,硬件抽象层是 AudioFlinger 直接访问的对象。这里体现了两方面的考虑:


  • 一方面 AudioFlinger 并不直接调用底层的驱动程序;

  • 另一方面,AudioFlinger 上层(包括和它同一层的 MediaPlayerService)的模块只需要与它进行交互就可以实现音频相关的功能了。


AudioFlinger 和 HAL 是整个架构解耦的核心层,通过 HAL 层的 audio.primary 等库抹平音频设备间的差异,无论硬件如何变化,不需要大规模地修改上层实现,保证系统对外暴露的上层 API 不需要修改,达成高内聚低耦合。而对厂商而言,在定制时的重点就是如何在这部分库中进行高效实现了。


举个例子,以前 Android 系统中的 Audio 系统依赖于 ALSA-lib,但后期就变为了 tinyalsa,这样的转变不应该对上层造成破坏。因而 Audio HAL 提供了统一的接口来定义它与 AudioFlinger/AudioPolicyService 之间的通信方式,这就是 audio_hw_device、audio_stream_in 及 audio_stream_out 等等存在的目的,这些 Struct 数据类型内部大多只是函数指针的定义,是一个个句柄。当 AudioFlinger/AudioPolicyService 初始化时,它们会去寻找系统中最匹配的实现(这些实现驻留在以 audio.primary.*,audio.a2dp.*为名的各种库中)来填充这些“壳”,可以理解成是一种“多态”的实现。

3. Linux 平台下的两种主要的音频驱动架构介绍

上面我们的示例提到了 ALSA,这个其实是 Linux 平台的一种音频驱动架构。下面介绍两种常见的 Linux 音频驱动架构。

3.1 OSS (Open Sound System)

早期 Linux 版本采用的是 OSS 框架,它也是 Unix 及类 Unix 系统中广泛使用的一种音频体系。OSS 既可以指 OSS 接口本身,也可以用来表示接口的实现。OSS 的作者是 Hannu Savolainen,就职于 4Front Technologies 公司。由于涉及到知识产权问题,OSS 后期的支持与改善不是很好,这也是 Linux 内核最终放弃 OSS 的一个原因。


另外,OSS 在某些方面也遭到了人们的质疑,比如:


  • 对新音频特性的支持不足;

  • 缺乏对最新内核特性的支持等等。


当然,OSS 做为 Unix 下统一音频处理操作的早期实现,本身算是比较成功的。它符合“一切都是文件”的设计理念,而且做为一种体系框架,其更多地只是规定了应用程序与操作系统音频驱动间的交互,因而各个系统可以根据实际的需求进行定制开发。总的来说,OSS 使用了如下表所示的设备节点:

更多详情,可以参考 OSS 的官方说明:http://www.opensound.com/

3.2 ALSA(Advanced Linux Sound Architecture)

ALSA 是 Linux 社区为了取代 OSS 而提出的一种框架,是一个源代码完全开放的系统(遵循 GNU GPL 和 GNU LGPL)。ALSA 在 Kernel 2.5 版本中被正式引入后,OSS 就逐步被排除在内核之外。当然,OSS 本身还是在不断维护的,只是不再为 Kernel 所采用而已。


ALSA 相对于 OSS 提供了更多,也更为复杂的 API 接口,因而开发难度相对来讲加大了一些。为此,ALSA 专门提供了一个供开发者使用的工具库,以帮助他们更好地使用 ALSA 的 API。根据官方文档的介绍,ALSA 有如下特性:


  • 高效支持大多数类型的 audio interface(不论是消费型或者是专业型的多声道声卡)

  • 高度模块化的声音驱动

  • SMP 及线程安全(thread-safe)设计

  • 在用户空间提供了 alsa-lib 来简化应用程序的编写

  • 与 OSS API 保持兼容,这样子可以保证老的 OSS 程序在系统中正确运行


ALSA 主要由下表所示的几个部分组成:


Alsa 主要的文件节点如下:


  1. Information Interface (/proc/asound)

  2. Control Interface (/dev/snd/controlCX)

  3. Mixer Interface (/dev/snd/mixerCXDX)

  4. PCM Interface (/dev/snd/pcmCXDX)

  5. Raw MIDI Interface (/dev/snd/midiCXDX)

  6. Sequencer Interface (/dev/snd/seq)

  7. Timer Interface (/dev/snd/timer)


Android 的 TinyALSA 是基于 Linux ALSA 基础改造而来。一看“Tiny”这个词,我们应该能猜到这是一个 ALSA 的缩减版本。实际上在 Android 系统的其它地方也可以看到类似的做法——既想用开源项目,又嫌工程太大太繁琐,怎么办?那就只能瘦身了,于是很多 Tiny-XXX 就出现了。


在早期版本中,Android 系统的音频架构主要是基于 ALSA 的,其上层实现可以看做是 ALSA 的一种“应用”。后来可能是由于 ALSA 所存在的一些不足,Android 后期版本开始不再依赖于 ALSA 提供的用户空间层的实现。HAL 层最终依赖 alsa-lib 库与驱动层交互。

4. 一种新的录音方式实现

除了之前提到的系统 API,我们还有其他的录音方式吗?答案是肯定的。上面我们提到 HAL 层依赖 alsa-lib 库与驱动层交互,我们直接使用 alsa-lib,绕开 HAL 层和 Framework 层不也可以做到吗(当然前提是要有系统权限)?


为什么会有这种述求呢?在做家居和车载产品时,会有四麦、六麦、甚至八麦的场景。录制大于 2 麦的设备时需要在 HAL 层以及 Framework 层做适配,基于 AOSP 的修改会显得特别重,特别是一些像回声抑制,声源定位等信号处理算法,如果集成在操作系统,会有更新升级麻烦的问题,我们可以基于 alsa-lib 在应用层拿到多路数据调用信号处理算法,这样算法模块升级只需要升级 APP 即可,不需要升级整个系统。


我们先来看看 Android 系统自带的 tinyX 系列工具。

4.1 tinymix 混响器

在 root 用户下调用 tinymix 可以查看硬件驱动支持的混响配置


root@android:/ # tinymixNumber of controls: 7ctl  type  num  name                                     value0  ENUM  1  Playback Path                            OFF1  ENUM  1  Capture MIC Path                         MIC OFF2  ENUM  1  Voice Call Path                          OFF3  ENUM  1  Voip Path                                OFF4  INT  2  Speaker Playback Volume                  0 05  INT  2  Headphone Playback Volume                0 06  ENUM  1  Modem Input Enable                       ONroot@android:/ #
复制代码


那么它里面的内容是什么意思呢?


  • 首先我们要知道,一个 mixer 通常有多个 controler,像这个,里面有 7 个,然后就分别列出每一个 controller 的信息;

  • 首先看第一个:它的编号为 0,类型是 ENUM 型,它目前的值是 OFF,它是用来控制音频输出通道;

  • 同理,第二个也控制音频输入通道;

  • 第三个,通话音频通道;

  • 第四个 IP 电话音频通道;

  • 第五个扬声器音量,和上层音量值无关;

  • 第六个耳机音量,和上层音量值无关;


一般 Playback Path 对应的枚举值有:


  1. OFF:关闭

  2. RCV

  3. SPK:扬声器

  4. HP:耳机带麦

  5. HP_NO_MIC:耳机无麦

  6. BT:蓝牙


那么我如果像改变某一项的时候,要怎么设置呢?方法是 tinymix ctl value;如果 tinymix 只跟上控制器的编号,就会把控制器的当前状态显示出来:


# tinymix 7Audio linein in: On# tinymix 7 0root@dolphin-fvd-p1:/ # **tinymix 7**Audio linein in: Off
复制代码

4.2 tinycap 采集器

使用下面命令即可实现录制并保存到 sd 卡:


 tinycap Usage: tinycap file.wav [-D card] [-d device] [-c channels] [-r rate] [-b bits] [-p period_size] [-n n_periods]  tinycap /sdcard/rec.wav -D 0 -d 0 –c 4 –r 16000 –b 16 –p 1024 –n 3
复制代码

4.3 tinyplay 播放

tinyplayUsage: tinyplay file.wav [-D card] [-d device] [-p period_size] [-n n_periods]tinyplay /sdcard/test44.wav -D 0 -d 0 -p 1024 -n 3
复制代码

4.4 程序中集成

现在我们已经通过命令的方式实现了绕开 framework 的音频采集,我们在自己的 app 中怎么使用呢?如果还是通过命令的方式只能录制到文件,无法实现流式录制。


解决办法是我们的 app 依赖 tinyalsa 库https://android.googlesource.com/platform/external/tinyalsa/,调用 asoundlib.h 中的 read 方法模拟 tinycap 不断读取音频数据。


    struct pcm_config config;    config.channels = 4;    config.rate = 16000;    config.period_size = 1024;    config.period_count = 4;    config.start_threshold = 0;    config.stop_threshold = 0;    config.silence_threshold = 0;
if (bitDepth == 32) config.format = PCM_FORMAT_S32_LE; else if (bitDepth == 16) config.format = PCM_FORMAT_S16_LE; pcm = pcm_open(0, device, PCM_IN, &config); if (!pcm || !pcm_is_ready(pcm)) { return -1; } int bufferSize = pcm_get_buffer_size(pcm); char *buffer = (char*)malloc(bufferSize); int i = pcm_read(pcm, buffer, bufferSize); if(i ==0){ //success }
复制代码

5. 总结

本文介绍了 Andorid 系统的整套音频架构,以及架构各层级的功能及作用。并介绍了一种绕开 framework 层的新的音频采集方式。其实 Andorid 的音频架构实现是更复杂的一个过程,本文只是简略的对各个模块做了一些介绍,以助于更深入理解上一篇提到的各个 API 的实现。其实 API 提供出来的音频接口,都是属于接口层,不论是 Java 接口还是 C++接口,都隶属于应用进程。以采集为例,不论我们调用哪个 API,我们都会发现启动后应用进程会多出一个 AudioRecord 的线程:


我们启动的录制线程调用 API 只是从 AudioRecord 线程写入到 Buffer 的数据的读取。


发布于: 20 小时前阅读数: 18
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017.10.17 加入

Android音视频、AI相关领域从业者

评论

发布
暂无评论
Android音频架构