涉及硬件的音视频能力,比如采集、渲染、硬件编码、硬件解码,通常是与客户端操作系统强相关的,就算是跨平台的多媒体框架也必须使用平台原生语言的模块来支持这些功能
本系列文章将详细讲述移动端音视频的采集、渲染、硬件编码、硬件解码这些涉及硬件的能力该如何实现。本文为该系列文章的第 3 篇,也是有关音频采集的最后一篇,将详细讲述在 Android 平台如何实现高性能音频采集。往期精彩内容,可参考:
音视频基础能力之 Android 音频篇 (一):音频采集
音视频基础能力之 Andoid 音频篇(二):音频录制
一、前言
在之前的文章里面,我们详细的介绍了使用 Java 相关的 API 来实现音频的采集和录制。但是在低延迟音视频或者跨平台的项目中,还是会优先考虑 Android 平台提供的 c/c++ 接口,因为不仅可以提升程序性能,还能最大限度上来缩短音频延迟。
Android 平台提供了三种 c/c++ 接口来实现音频采集,分别是:
- Opensl es 嵌入式、跨平台的免费音频处理库,为嵌入式设备提供标准化、高性能、低延迟的 API。NDK 中包含的 Opensl es 1.0.1 API 是 Khronos Group 为 Android 平台量身打造的一个版本。
- AAudio Android O(Android 8) 版本中引入的全新的 Android C API,此 API 专为需要低延迟的高性能音频应用而设计。
- Oboe Android 团队打造的一个 c++ 库,可以在 android 上构建高性能应用应用,它的主要目的是让开发者能够使用简化的 API,该 API 最低可以在 Android API 16 (Jelly Bean) 上运行。
你可能比较好奇,为什么给开发者提供这么多的 c/c++ 的音频采集接口出来?其实还是有点原因的,简单的来说,使用 Opensl es 的接口来实现音频采集比较繁琐,实现起来要写大几百行代码(可以参考下文章底部的 sample code 链接)。
所以,Android 团队又设计了一套 AAudio 的接口给开发者使用,但是 AAudio 又无法在 Android 8 以下的设备上运行,然后又搞了一套 Oboe 库,它内部自动帮你完成回退,Android 8 以下使用 opensl es接口,Android 8 以上使用 AAudio 库,就这么回事。
下面,详细介绍下三种库的接入和使用姿势。如果不想听笔者废话,想直接看代码的话,可以直接跳转到文章底部。
二、申请权限
任何使用到音频采集的 app 都需要申请音频录制权限,无论开发者使用的是 Java 接口还是 c/c++ 接口。配置方式,Android 项目的清单文件中添加:
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
在 API 23 (Android 6.0) 之后,为了保护用户隐私,对于一些敏感权限(比如录音权限),应用需要在运行时动态申请。示例代码如下:
private static final int PERMISSION_REQUEST_CODE = 1;
// step1: 检查是否有录音权限
private boolean checkPermission() {
int result = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
return result == PackageManager.PERMISSION_GRANTED;
}
// step2: 请求录音权限
private void requestPermission() {
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.RECORD_AUDIO}, PERMISSION_REQUEST_CODE);
}
// step3: 处理权限请求结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限被授予,可以进行录音
startRecording();
} else {
// 权限被拒绝,无法进行录音
Toast.makeText(this, "录音权限被拒绝", Toast.LENGTH_SHORT).show();
}
}
}
三、Opensl es
上一章节曾说过,通过 Opensl es 来实现音频采集会比较繁琐,繁琐的原因是因为 Opensl es 能做的事情比较多,所以在接口层面上设计会更优先考虑到拓展性和模块化。所以,在接入 Opensl es 之前还是有必要先了解下它的编程模型。
Opensl es 使用 c 接口实现了一套类似于 Java/c++ 面向对象的概念,所有对象的初始接口叫做 SLObjectItf
,它的声明以及简单实用如下:
struct SLObjectItf_ {
SLresult (*Realize) (
SLObjectItf self,
SLboolean async
);
SLresult (*GetInterface) (
SLObjectItf self,
const SLInterfaceID iid,
void * pInterface
);
//... 此处省略其他接口
};
//1. 创建对象
SLObjectItf sl_object_;
SLresult result = slCreateEngine(&sl_object, args...);
//2. 示例化
(*sl_object)->Realize(sl_object, args..);
首先通过初始接口创建出这个对象,然后实现 (realize),这个和我们常见的编程模式差不多。唯一的区别是,你不能通过这个对象来调用其内部的函数,而是调用它内部的函数指针,将其句柄传递进去。
SLObjectItf 对象的生命周期时序图如上,下面开始讲解下如何在项目中集成和使用。
3.1 项目集成
引入头文件:
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h> //Android相关拓展
引入库文件:
target_link_libraries(${
CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log
OpenSLES)
3.2 调用流程
初始化流程
step1 : 首先通过 SLObjectItf 创建出 SLEngineItf 引擎对象 sl_engine_
出来。
// Create the engine object in thread safe mode.
const SLEngineOption option[] = {
{
SL_ENGINEOPTION_THREADSAFE, static_cast