写作计划
学习入门 Android
平台下的音视频技术, 我打算按照下图所示的步骤去学习:
从上图可以看出, 学习的步骤大概有三大部分(可以穿插学习):
- 音视频的录制
- 音视频在网络传输的流媒体协议
- 音视频的播放
音视频的录制
音频和视频的录制流程为:音频和视频录制 -> 将录制得到的音频和视频数据进行编码 -> 将编码后的音频和视频数据按照一定的格式进行合成得到视频文件。
流媒体协议
将得到的视频文件根据一定的协议进行网络传输,不同的协议适用不同的场景。
音视频的播放
从网络得到一个视频文件后, 我们需要处理的流程有:将视频文件按照一定的格式分离出其中的音频和视频 -> 对音频和视频进行解码 -> 音视频同步播放。
作为本专栏的开篇,我们先学习音频的基础知识和 Android
平台下音频的录制。
音频基础知识
自然界的声音经过麦克风采集后,可以得到模拟信号。接着,我们可以编写程序采集麦克风发出的模拟信号, 最后得到数字信号。在这个过程中,含有三种信号的转变:
关于声信号到模拟信号的转换,我们一般无需关心,手机的麦克风都帮我们转换好了。 我们只需关心的是从麦克风得到的模拟信号,程序如何去采集得到数字信号,最后保存为音频文件。
模拟信号到数字信号的转换
模拟信号一般通过脉冲编码调制(Pulse Code Modulation,PCM)方法转换为数字信号,这种方法包含三个步骤:
-
采样:模拟信号本身是一种连续信号,它在一定的时间范围内可以有无限多个不同的取值。而数字信号是指在取值上是离散的、不连续的信号。所谓的采样,就是将一段时间内的连续信号转为离散信号。在上图中,按照一定的频率, 对连续的模拟信号进行采集,然后记录下来。根据采样定理,按比声音最高频率的二倍进行采样,声音就能被完整地恢复。由于人耳能听到的频率范围是在 20~20KHz,所以采样率一般为 44.1kHz,这样才能保证声音达到 20KHz 时,也能够被完整地恢复。
-
量化:指采样得到后的数据,我们用多少位的二进制数字来表示声音的振幅。例如使用 16 比特的二进制信号来表示一个声音的采样,而 16bit 能表示的范围是: [-32768, 32767],一共有 65536 个取值。
-
编码:将采样量化后的数据按照一定的格式进行记录,比如顺序储存或者压缩储存。
音频开发中的重要参数
-
采样率
-
量化精度
-
声道数
-
帧间隔
采样率
将模拟信号转为数字信号时,需要隔一定的时间对模拟信号进行一个采样,然后将这个采样用 01 来表示,也就是数字化的过程。 采样率表示,1S 内,对模拟信号进行多少次采样。采样频率越高,说明采样点之间越密集,记录这段音频所用的数据量就越大, 因此音质也就越好。
为了保证声音不失效,我们采样率通常设置为 44100Hz。常用的音频采样频率有:8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz、96kHz、192kHz 等。
量化精度
对于一个采样点,需要用二进制数字来表示,这个二进制的精度可以是:4bit、8bit、16bit、32bit。 位数越多,表示的声音就越精细,声音的质量就越好。不过数据量也会变大。常见的位宽:8bit,16bit。
声道数
声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。常用的有:单通道和双通道。
帧间隔
音频不像视频那样,有一帧一帧的概念。它是约定一个时间为单位,然后这个时间内的数据为一帧,这个时间被称为采样时间。这个时间没有特别的标准,要看具体的编解码器。
计算一帧音频的大小
假设某通道的音频信号是采样率为8kHz,位宽为16bit,20ms一帧,双通道,则一帧音频数据的大小为:
int size = 8000 x 16bit x 0.02s x 2 = 5120 bit = 640 byte
Android 音频录制
了解音频的基础知识后,我们可以开始来熟悉 Android
平台下音频录制的 API
了。Android
提供了两套音频采集的 API
,分别是:
-
MediaRecorder
:比较上层的API
,它可以直接把手机麦克风的音频数据进行编码然后储存成文件。使用简单,但是支持的格式有限,并且不支持对音频进行进一步的处理,例如变声、混音等。 -
AudioRecord
:比较底层的一个API
,能够得到原始的PCM
音频数据。由于我们得到的是原始的PCM
数据,我们可以对音频进行进一步的处理,例如编码、混音和变声等。
关于 MediaRecorder
的使用比较简单,这里不做介绍。接下来,主要介绍 AudioRecord
的使用套路。
AudioRecord 工作流程
AudioRecord
的使用套路大体可以分为以下四个步骤:
- 根据配置参数,初始化音频内部的缓冲区
- 开始采集原始的音频数据
- 开辟一个
worker
线程, 不断地从AudioRecord
中的缓冲区将音频数据读出来 - 停止采集,及时释放资源
根据配置参数,初始化音频内部的缓冲区
//默认采样率,44100Hz 可以保证兼容所有 Android 手机的采样率。
private static final int DEFAULT_SAMPLE_RATE = 44100;
//通道数:单通道,AudioFormat.CHANNEL_IN_STEREO 表示双通道
private static final int DEFAULT_CHANNEL = AudioFormat.CHANNEL_IN_MONO;
//16 位量化位宽
private static final int SIMPLE_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
//声音从麦克风采集而来,可选的值以常量的形式定义在 MediaRecorder.AudioSource 类中,常用的值包括:DEFAULT(默认),
//VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),
//VOICE_COMMUNICATION(用于VoIP应用)等等。
private static final int DEFAULT_SOURCE_MIC = MediaRecorder.AudioSource.MIC;
private AudioRecord createAudioRecord(int audioSource, int simpleRate, int channels, int audioFormat) {
//获取一帧音频帧的大小
int minBufferSize = AudioRecord.getMinBufferSize(simpleRate, channels, audioFormat);
if (minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
Log.d(TAG, "获取音频帧大小失败!");
return null;
}
int audioRecordBufferSize = minBufferSize * 4; //AudioRecord内部缓冲设置为4帧音频帧的大小
AudioRecord audioRecord = new AudioRecord(audioSource, simpleRate,
channels, audioFormat, audioRecordBufferSize);
if (audioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
Log.d(TAG, "初始化AudioRecord失败!");
return null;
}
return audioRecord;
}
初始化AudioRecord
时,我们需要先确定一帧音频占用的大小,Android
提供了AudioRecord.getMinBufferSize
函数给我们确定。不建议手动算音频帧的大小。
audioRecordBufferSize
配置的是 AudioRecord
内部的音频缓冲区的大小,该缓冲区的值不能低于一帧音频帧的大小,我这里配置为4帧的大小。
开始采集原始的音频数据
mAudioRecord.startRecording(); //开始采集数据
开辟一个 worker 线程,不断地从 AudioRecord 中的缓冲区将音频数据读出来
当我们调用 AudioRecord
的startRecording()
方法后,AudioRecord
就会开始帮我们采集数据,然后存放在内部的缓冲区,等待客户端去拿走数据。这时候,我们应该开辟一个 worker
线程,不断从 AudioRecord
内部缓冲区将音频数据读出来。来个比较生动的图:
拿到后的PCM
数据,我们一般通过接口回调给外部使用。外部使用者可以使用 AudioTrick
进行实时播放,或者进行再一次编码,然后保存成一个音频文件。
private OnAudioCaptureListener listener;
public interface OnAudioCaptureListener {
void onAudioFrameCaptured(byte[] audioData);
}
public void setOnAudioCaptureListener(OnAudioCaptureListener listener) {
this.listener = listener;
}
private class AudioCaptureRunnable implements Runnable {
@Override
public void run() {
while (!isExit) {
byte[] buffer = new byte[1024 * 2]; //每次拿2k
int result = mAudioRecord.read(buffer, 0, buffer.length);
if (result == AudioRecord.ERROR_BAD_VALUE) {
Log.d(TAG, "run: ERROR_BAD_VALUE");
} else if (result == AudioRecord.ERROR_INVALID_OPERATION) {
Log.d(TAG, "run: ERROR_INVALID_OPERATION");
} else {
if (listener != null) {
Log.d(TAG, "run: capture buffer length is " + result);
listener.onAudioFrameCaptured(buffer);
}
}
}
}
}
停止采集, 及时释放资源
当我们录制完时,需要停止采集然后及时释放掉 native
层的资源。
public boolean stop() {
if (!isStart) {
return false;
}
isExit = true;
try {
captureThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (mAudioRecord.getState() == AudioRecord.RECORDSTATE_RECORDING) {
mAudioRecord.stop(); // 不是必须调用的, 因为release内部也会调用
}
mAudioRecord.release(); //释放掉资源
mAudioRecord = null;
isStart = false;
Log.d(TAG, "stop: stop successfully");
return true;
}
最后,附上完整代码地址。欢迎 start
和 follow
总结
这篇文章主要总结了自己学习音视频的一个大体计划、音频的一些基础知识和 Android
平台下的 AudioRecord
的使用姿势。
下一步
-
使用
AudioTrick
来播放采集得到的PCM
数据 -
将采集得到的
PCM
数据编码成AAC
、WAV
这两种格式的文件进行保存
参考资料
-
《音视频开发进阶指南》
由于本人能力水平限制,文章中难免会有错误。如果大家发现文章有不足之处,欢迎指出。
网友评论