【征文 】Android音频架构

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. 音频系统架构的演进

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

image-20210923115024009-2369025

在这个图中,除去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使用了如下表所示的设备节点:

设备节点 说明
/dev/dsp 向此文件写数据à输出到外放Speaker向此文件读数据à从Microphone进行录音
/dev/mixer 混音器,用于对音频设备进行相关设置,比如音量调节
/dev/midi00 第一个MIDI端口,还有midi01,midi02等等
/dev/sequencer 用于访问合成器(synthesizer),常用于游戏等效果的产生

更多详情,可以参考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主要由下表所示的几个部分组成:

Element Description
alsa-driver 内核驱动包
alsa-lib 用户空间的函数库
alsa-utils 包含了很多实用的小程序,比如alsactl:用于保存设备设置amixer:是一个命令行程序,用于声量和其它声音控制alsamixer:amixer的ncurses版acconnect和aseqview:制作MIDI连接,以及检查已连接的端口列表aplay和arecord:两个命令行程序,分别用于播放和录制多种格式的音频
alsa-tools 包含一系列工具程序
alsa-firmware 音频固件支持包
alsa-plugins 插件包,比如jack,pulse,maemo
alsa-oss 用于兼容OSS的模拟包
pyalsa 用于编译Python版本的alsa lib

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:/ # tinymix
Number of controls: 7
ctl	type	num	name                                     value
0	ENUM	1	Playback Path                            OFF
1	ENUM	1	Capture MIC Path                         MIC OFF
2	ENUM	1	Voice Call Path                          OFF
3	ENUM	1	Voip Path                                OFF
4	INT	2	Speaker Playback Volume                  0 0
5	INT	2	Headphone Playback Volume                0 0
6	ENUM	1	Modem Input Enable                       ON
root@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 7
Audio linein in: On
# tinymix 7 0
root@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播放

tinyplay
Usage: 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的数据的读取。

推荐阅读
相关专栏
开发者实践
186 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。