网上有很多关于 MediaCodec 硬编解 AAC 格式音频的文章,但良莠不齐,以下时自己学习 MediaCodec 硬编 AAC 格式音频过程的记录和一些参考的文章。
介绍
AAC
一种音频压缩格式,区别于无损的 PCM 数据格式,其中分成了两种:ADTS 和 ADIF 格式
- ADTS:Audio Data Interchange Format 音频数据交换格式,有一个统一的头部再带上数据流,如果想解析,必须得到所有的数据后,先寻找同步头,再解码。
- ADIF:Audio Data Transport Stream 音频数据传输流,每一个帧都有一个头部,所以可以在任意帧上进行解码。
AudioRecord
Android 中录制无损音频 PCM 格式的工具
关于 AudioRecord 录音的流程和参数可以参考之前的文章 Android AudioRecord、AudioTrack 录制播放音频
MediaCodec
MediaCodec 是 Android 用于音视频编解码的一套偏底层的 API,直接利用硬件加速进行编解码。
我们所需要做的处理流程:
- dequeueInputBuffer:从 input 缓存区申请 buffer 编号 Index
- getInputBuffer:用编号 Index 取得输入的缓冲区,将需要编码的数据写入 buffer
- queueInputBuffer:将 buffer 入 MediaCodec 的队列,MediaCodec 会从 buffer 中取数据处理
- dequeueOutputBuffer:从 output 缓冲区申请 buffer 编号
- getOutputBuffer:用编号 Index 取得输出的缓冲区,buffer 中就是处理后的数据
- releaseOutputBuffer:将该 buffer 放回 output 缓冲区队列
流程
1. AudioRecord 录制 PCM 格式的音频
(1)创建 AudioRecord:
// 算出所需的最小的缓冲区大小
int minBufferSize = AudioRecord.getMinBufferSize(
44100, // 采样率
AudioFormat.CHANNEL_IN_MONO, // 通道数
AudioFormat.ENCODING_PCM_16BIT
);
mRecorder = new AudioRecord(
MediaRecorder.AudioSource.DEFAULT, // 音频源
44100, // 采样率
AudioFormat.CHANNEL_IN_MONO, // 声道
AudioFormat.ENCODING_PCM_16BIT, // 采样大小
minBufferSize
);
(2)开始录音
mRecorder.startRecording();
(3)读取 mRecorder 的一帧帧数据,实时传送给 MediaCodec 后续处理
while (isRecording && (len = mRecorder.read(bytesBuffer, 0, minBufferSize)) > 0) {
// 后续转码处理
}
2. 通过 MediaCodec 硬编成 AAC 格式,并保存为外部文件
(1)MediaCodec 创建流程
mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
//指定创建的MediaCodec类型
MediaFormat format = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
44100, //采样率
1 // 声道数量,一开始写成了AudioFormat.CHANNEL_IN_MONO
);
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AAC);
// 封装可用于编解码器组件的配置文件
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
//码率:声音中的比特率是指将模拟声音信号转换成数字声音信号后,单位时间内的二进制数据量,是间接衡量音频质量的一个指标
format.setInteger(MediaFormat.KEY_BIT_RATE, 96000);//传入的数据最大值,可以修改
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, minBufferSize * 2);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mEncoder.start();
(2)MediaCodec 具体转码流程
while (isRecording && (len = mRecorder.read(bytesBuffer, 0, minBufferSize)) > 0) {
// 不断从Recorder读取数据
int inputBufferIndex = mEncoder.dequeueInputBuffer(100000);
// 申请输入的buffer, 等待时间,0:不等待,-1:一直而等待
if (inputBufferIndex >=0 ) {
ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex);
inputBuffer.clear(); // 清除数据,否则可能是上次写入的数据
inputBuffer.put(bytesBuffer);
inputBuffer.limit(len);// 设定上限值
mEncoder.queueInputBuffer(inputBufferIndex, 0, len, System.nanoTime(), 0); // 第三个参数为时间戳,这里是使用当前使劲按
} else {
isRecording = false;
}
// 申请处理完成后输出的buffer
int outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 0);
// 可能不能一次性处理完,所以循环读取
while (outputBufferIndex >= 0) {
int outBitsSize = mBufferInfo.size;
int outPacketSize = outBitsSize + 7; // ADTS头部是7个字节
ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + outBitsSize);
byte[] outData = new byte[outPacketSize];
// 需要为adts帧加上头部
addADTStoPacket(outData, outPacketSize);
outputBuffer.get(outData, 7, outBitsSize);
outputBuffer.position(mBufferInfo.offset);
// 写入文件
mFileStream.write(outData);
// 释放输出的buffer
mEncoder.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 0);
}
}
源码:
public class AacRecordActivity extends AppCompatActivity{
private final static String TAG = AacRecordActivity.class.getSimpleName();
public static String[] MICROPHONE = {Manifest.permission.RECORD_AUDIO};
public static String[] STORAGE = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
private Button mRecordBtn;
private MyAudioRecorder mAudioRecorder;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_aac);
mRecordBtn = findViewById(R.id.aac_recording_btn);
mAudioRecorder = new MyAudioRecorder();
mRecordBtn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
int action = motionEvent.getAction();
if (action == MotionEvent.ACTION_DOWN) {
checkRecordPermission();
mRecordBtn.setText("正在录制");
startRecord();
} else if (action == MotionEvent.ACTION_UP) {
mRecordBtn.setText("录音");
stopRecord();
}
return false;
}
});
}
private void startRecord() {
new Thread(new Runnable() {
@Override
public void run() {
mAudioRecorder.start();
}
}).start();
}
private void stopRecord() {
mAudioRecorder.stop();
}
private void checkRecordPermission() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, MICROPHONE, 1);
return;
}
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, STORAGE, 1);
return;
}
}
private class MyAudioRecorder {
private AudioRecord mRecorder;
private MediaCodec mEncoder;
private OutputStream mFileStream;
private boolean isRecording;
public void start() {
try {
isRecording = true;
int minBufferSize = AudioRecord.getMinBufferSize(44100, 1, AudioFormat.ENCODING_PCM_16BIT);
mRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, 44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);
if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "initRecord: mAudioRecord init failed");
isRecording = false;
return;
}
mRecorder.startRecording();
mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1);
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AAC);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, minBufferSize * 2);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mEncoder.start();
mFileStream = new FileOutputStream(getSDPath() + "/acc_encode.mp4");
MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
byte[] bytesBuffer = new byte[minBufferSize];
int len = 0;
while (isRecording && (len = mRecorder.read(bytesBuffer, 0, minBufferSize)) > 0) {
int inputBufferIndex = mEncoder.dequeueInputBuffer(100000);
if (inputBufferIndex >=0 ) {
ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(bytesBuffer);
inputBuffer.limit(len);
mEncoder.queueInputBuffer(inputBufferIndex, 0, len, System.nanoTime(), 0);
} else {
isRecording = false;
}
int outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 0);
while (outputBufferIndex >= 0) {
int outBitsSize = mBufferInfo.size;
int outPacketSize = outBitsSize + 7; // ADTS头部是7个字节
ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + outBitsSize);
byte[] outData = new byte[outPacketSize];
addADTStoPacket(outData, outPacketSize);
outputBuffer.get(outData, 7, outBitsSize);
outputBuffer.position(mBufferInfo.offset);
mFileStream.write(outData);
mEncoder.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 0);
}
}
release();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private void stop() {
isRecording = false;
}
private void release() throws IOException {
if (mFileStream != null) {
mFileStream.flush();
mFileStream.close();
}
if (mRecorder != null && mRecorder.getState() == AudioRecord.STATE_INITIALIZED) {
mRecorder.stop();
mRecorder.release();
mRecorder = null;
}
if (mEncoder != null) {
mEncoder.stop();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AacRecordActivity.this, "录制完毕", Toast.LENGTH_SHORT).show();
}
});
}
}
public String getSDPath() {
// 判断是否挂载
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
return Environment.getRootDirectory().getAbsolutePath();
}
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC
int freqIdx = 4; //44100
int chanCfg = 2; //CPE
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;
}
参考文章:
《音频数据的编解码》 云图网
《MediaCodec 解码 aac》 云图网
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/6281.html