现在的短视频、音视频通话都离不开编码和解码,今天就来聊一下音频的编解码。
1. 音频的基本概念
在音频开发中,有些基本概念是需要了解的。
- 采样率(SampleRate):每秒采集声音的数量,它用赫兹(Hz)来表示。采样频率越高,音频质量越好。常用的音频采样频率有:8kHz、16kHz、44.1kHz、48kHz 等。
- 声道数(Channel):一般表示声音录制时的音源数量或回放时相应的扬声器数量。常用的是单声道(Mono)和双声道(Stereo)。
- 采样精度(BitDepth):每个采样点用多少数据量表示,它以位(Bit)为单位。位数越多,表示得就越精细,声音质量自然就越好,当然数据量也越大。常见的位宽是:8bit 或者 16bit。
- 比特率(BitRate):每秒音频占用的比特数量,单位是 bps(Bit Per Second),比特率越高,压缩比越小,声音质量越好,音频体积也越大。
AAC 是应用非常广泛的音频压缩格式,Android 硬件编码天生支持 AAC。我们采集的原始 PCM 音频,一般不直接用来网络传输,而是经过编码器压缩成 AAC,这样就提高了传输效率,节省了网络带宽。
简言之,编码就是压缩,解码就是解压。编码的目的是减小数据的体积,方便网络传输和本地存储。编码后的数据是不能直接使用的,必须先解码成原来的样子。就像 zip 压缩文件里面有张图片,我们用图片查看器是无法打开的,必须先解压文件,恢复图片原来的数据,这样才能查看。音视频编解码也是同样的道理。
2. MediaCodec 介绍
Android 在 API 16 后引入的音视频编解码 API,Android 应用层统一由 MediaCodec API 提供音视频编解码的功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等。由于使用硬件编解码,兼容性有不少问题,据说 MediaCodec 坑比较多。
MediaCodec 采用了基于环形缓冲区的「生产者-消费者」模型,异步处理数据。在 input 端,Client 是这个环形缓冲区「生产者」,MediaCodec 是「消费者」。在 output 端,MediaCodec 是这个环形缓冲区「生产者」,而 Client 则变成了「消费者」。
工作流程是这样的:
(1)Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]
(2)Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer]
(3)MediaCodec 从 input 缓冲区队列取一帧数据进行编解码处理
(4)处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列
(5)Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]
(6)Client 对编解码后的 buffer 进行渲染/播放
(7)渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]
MediaCodec 基本使用流程:
- createEncoderByType/createDecoderByType
- configure
- start
- while(true) {
- dequeueInputBuffer
- queueInputBuffer
- dequeueOutputBuffer
- releaseOutputBuffer
}
- stop
- release
3. 实时采集音频并编码
我们将使用 AudioRecord 和 MediaCodec 实现这个功能,关于 AudioRecord 的使用可以参考之前的文章:Android音视频之使用AudioRecord采集音频。
为了保证兼容性,推荐的配置是 44.1kHz、单通道、16 位精度。首先创建并配置 AudioRecord 和 MediaCodec。
// 输入源 麦克风
private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
// 采样率 44.1kHz,所有设备都支持
private final static int SAMPLE_RATE = 44100;
// 通道 单声道,所有设备都支持
private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
// 精度 16 位,所有设备都支持
private final static int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
// 通道数 单声道
private static final int CHANNEL_COUNT = 1;
// 比特率
private static final int BIT_RATE = 96000;
public void createAudio() {
mBufferSizeInBytes = AudioRecord.getMinBufferSize(AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT);
if (mBufferSizeInBytes <= 0) {
throw new RuntimeException("AudioRecord is not available, minBufferSize: " + mBufferSizeInBytes);
}
Log.i(TAG, "createAudioRecord minBufferSize: " + mBufferSizeInBytes);
mAudioRecord = new AudioRecord(AudioEncoder.AUDIO_SOURCE, AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT, mBufferSizeInBytes);
int state = mAudioRecord.getState();
Log.i(TAG, "createAudio state: " + state + ", initialized: " + (state == AudioRecord.STATE_INITIALIZED));
}
public void createMediaCodec() throws IOException {
MediaCodecInfo mediaCodecInfo = CodecUtils.selectCodec(MIMETYPE_AUDIO_AAC);
if (mediaCodecInfo == null) {
throw new RuntimeException(MIMETYPE_AUDIO_AAC + " encoder is not available");
}
Log.i(TAG, "createMediaCodec: mediaCodecInfo " + mediaCodecInfo.getName());
MediaFormat format = MediaFormat.createAudioFormat(MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNEL_COUNT);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_AUDIO_AAC);
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
然后开始录音,得到原始音频数据,再编码为 AAC 格式。这个地方会阻塞调用的线程,而且编码比较耗时,一定要在主线程之外调用。
public void start(File outFile) throws IOException {
Log.d(TAG, "start() called with: outFile = [" + outFile + "]");
mStopped = false;
FileOutputStream fos = new FileOutputStream(outFile);
mMediaCodec.start();
mAudioRecord.startRecording();
byte[] buffer = new byte[mBufferSizeInBytes];
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
try {
while (!mStopped) {
int readSize = mAudioRecord.read(buffer, 0, mBufferSizeInBytes);
if (readSize > 0) {
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(buffer);
inputBuffer.limit(buffer.length);
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, readSize, 0, 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
byte[] chunkAudio = new byte[bufferInfo.size + 7];// 7 is ADTS size
addADTStoPacket(chunkAudio, chunkAudio.length);
outputBuffer.get(chunkAudio, 7, bufferInfo.size);
outputBuffer.position(bufferInfo.offset);
fos.write(chunkAudio);
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
} else {
Log.w(TAG, "read audio buffer error:" + readSize);
break;
}
}
} finally {
Log.i(TAG, "released");
mAudioRecord.stop();
mAudioRecord.release();
mMediaCodec.stop();
mMediaCodec.release();
fos.close();
}
}
AAC 是一种压缩格式,可以直接使用播放器播放。为了实现流式播放,也就是做到边下边播,我们采用 ADTS 格式。给每帧加上 7 个字节的头信息。加上头信息就是为了告诉解码器,这帧音频长度、采样率、通道是多少,每帧都携带头信息,解码器随时都可以解码播放。我们这里采用单通道、44.1KHz 采样率的头信息配置。
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC
int freqIdx = 4; //44.1KHz
int chanCfg = 1; //CPE
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
4. AAC 解码
我们可以利用 MediaExtractor 和 MediaCodec 来提取编码后的音频数据,并解压成音频源数据。
/**
* AAC 格式解码成 PCM 数据
*
* @param aacFile
* @param pcmFile
* @throws IOException
*/
public static void decodeAacToPcm(File aacFile, File pcmFile) throws IOException {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(aacFile.getAbsolutePath());
MediaFormat mediaFormat = null;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
extractor.selectTrack(i);
mediaFormat = format;
break;
}
}
if (mediaFormat == null) {
Log.e(TAG, "Invalid file with audio track.");
extractor.release();
return;
}
FileOutputStream fosDecoder = new FileOutputStream(pcmFile);
String mediaMime = mediaFormat.getString(MediaFormat.KEY_MIME);
Log.i(TAG, "decodeAacToPcm: mimeType: " + mediaMime);
MediaCodec codec = MediaCodec.createDecoderByType(mediaMime);
codec.configure(mediaFormat, null, null, 0);
codec.start();
ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
final long kTimeOutUs = 10_000;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
try {
while (!sawOutputEOS) {
if (!sawInputEOS) {
int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
if (inputBufIndex >= 0) {
ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
int sampleSize = extractor.readSampleData(dstBuf, 0);
if (sampleSize < 0) {
Log.i(TAG, "saw input EOS.");
sawInputEOS = true;
codec.queueInputBuffer(inputBufIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
codec.queueInputBuffer(inputBufIndex, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance();
}
}
}
int outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
if (outputBufferIndex >= 0) {
// Simply ignore codec config buffers.
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
Log.i(TAG, "audio encoder: codec config buffer");
codec.releaseOutputBuffer(outputBufferIndex, false);
continue;
}
if (info.size != 0) {
ByteBuffer outBuf = codecOutputBuffers[outputBufferIndex];
outBuf.position(info.offset);
outBuf.limit(info.offset + info.size);
byte[] data = new byte[info.size];
outBuf.get(data);
fosDecoder.write(data);
}
codec.releaseOutputBuffer(outputBufferIndex, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(TAG, "saw output EOS.");
sawOutputEOS = true;
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
Log.i(TAG, "output buffers have changed.");
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat oformat = codec.getOutputFormat();
Log.i(TAG, "output format has changed to " + oformat);
}
}
} finally {
Log.i(TAG, "decodeAacToPcm finish");
codec.stop();
codec.release();
extractor.release();
fosDecoder.close();
}
}
源码在 GitHub上,欢迎交流。
参考资料: