流程大致分为三部分:1、获取从麦克风得到的pcm数据;2、对拿到的pcm数据进行硬编码;3、把编码后的数据进行处理;
一、获取从麦克风得到的pcm数据
1、使用AudioRecord 进行音频数据获取时,初始化AudioRecord之后,调用startRecording()方法进行开始录音;
/**
* 第一步初始化音频采集
*/
public void initAudioRecord() {
//获取最小音频buffer,给音频设置缓存区不能小于这个大小
int min_buffer_size = AudioRecord.getMinBufferSize(Constants.AUDIO_CHANNEL, Constants.AUDIO_SAMPLERATE,
AudioFormat.ENCODING_PCM_16BIT);
//用来获取最终的缓存区大小 SAMPLES_PER_FRAME 每帧的采样率 FRAMES_PER_BUFFER 每次缓存的帧数
int buffer_size = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER;
if (buffer_size < min_buffer_size)
buffer_size = ((min_buffer_size / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2;// TODO: 18-3-29 未理解为何这里要乘2?
//用来缓存音频数据的buffer
inBuffer = new byte[SAMPLES_PER_FRAME];
//根据音频的来源进行初始化:麦克风,或者其他
for (final int source : AUDIO_SOURCES) {
try {
//初始化AudioRecord,音频来源,采样率,声道,返回的音频格式,缓存区的大小
audioRecord = new AudioRecord(source, this.sampleRate, this.nInputChannels,
AudioFormat.ENCODING_PCM_16BIT, buffer_size);
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED)
audioRecord = null;
} catch (final Exception e) {
audioRecord = null;
}
if (audioRecord != null) break;
}
}
2、使用for循环进行 pcm数据的获取;
/**
* 开始对采集到的音频进行硬编码
*/
public void startEncodec() {
//开始进行
if (audioRecord != null && audioRecord.getState() != AudioRecord.STATE_UNINITIALIZED) {
audioRecord.startRecording();//开始进行录音
while (true && !isStop) {
try {
Arrays.fill(inBuffer, (byte) 0);//把原来的数据清空
int err = audioRecord.read(inBuffer, 0, inBuffer.length);//开始从audioRecord中读pcm输出出来
if (err <= 0) {
if (err == AudioRecord.ERROR_INVALID_OPERATION) {
MyLog.e("OF", "AudioRecord error ERROR_INVALID_OPERATION");
return;
}
if (err == AudioRecord.ERROR_BAD_VALUE) {
MyLog.e("OF", "AudioRecord error ERROR_BAD_VALUE");
return;
}
if (err == 0) {
MyLog.e("OF", "AudioRecord error 0 samples read");
return;
}
} else {
hardEncoder(inBuffer, inBuffer.length);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
二、pcm数据进行硬编码
硬编码的解释已经有很多了,这里就说自己对硬编码的理解:
假设整个编码的过程就是去寄存式洗衣店的过程,规则是:
1、收拾好你的臭袜子,写好一个清单:有几双,需要用什么方法洗,用什么样的清洗剂;
2、拿一个店里的空箱子用来存放你的臭袜子,拿一张带有序号的卡片,并且写上你想什么时候取洗干净的袜子;
3、清空箱子里面的灰尘并把臭袜子放进去;
4、把箱子给柜员,并且把你写的卡片给他,他便会把你的东西放到洗衣机中;
5、洗衣机洗好之后,会放到箱子中,并贴上序号;
6、你根据你设置的取货的时间根据你的序号把你的袜子拿走;
对应的硬编码MediaCodec需要做的事情是:
1、对编码器进行初始化操作,告诉它你想要什么类型的数据:音频/视频、声道、采样率、码率等等;
/**
* 初始化硬编码器
*/
private void initMediaCode() {
try {
//尝试是否能够进行初始化成功
mMediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
} catch (Exception e) {
e.printStackTrace();
return;
}
// AAC 硬编码器
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");//音频编码
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); //声道数(这里是数字)
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, Constants.AUDIO_SAMPLERATE); //采样率
format.setInteger(MediaFormat.KEY_BIT_RATE, Constants.BIT_RATE); //码率
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
bufferInfo = new MediaCodec.BufferInfo();//记录编码完成的buffer的信息
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);// MediaCodec.CONFIGURE_FLAG_ENCODE 标识为编码器
mMediaCodec.start();//开始工作
}
2、获取需要存放未编码数据的盒子buffer
int index=mMediaCodec.dequeueInputBuffer(-1);//拿空盒子,index 拿到的盒子序号
3、清空原来的数据,把数据放进去
final ByteBuffer buffer=mMediaCodec.getInputBuffer(index);
buffer.clear();
buffer.put(buffer1);
4、把数据交给编码器进行编码
mMediaCodec.queueInputBuffer(index,0,length,System.nanoTime()/1000,0);//往空盒子里塞要编码的数据
5、编码的过程不需要你关注;
6、获取编码完成的数据
outIndex=mMediaCodec.dequeueOutputBuffer(mInfo,0);//取出已经编码好的数据,outIndex 表示盒子的位置
ByteBuffer buffer=mMediaCodec.getOutputBuffer(outIndex);
其中2-6是一个完整的过程,完整的代码如下:
/**
* 进行实际编码
* @param buffer1
* @param length
*/
public void hardEncoder(final byte[] buffer1, final int length) {
//这里的处理就是和之前传送带取盒子放原料的流程一样了,注意一般在子线程中循环处理
int index=mMediaCodec.dequeueInputBuffer(-1);//拿空盒子,index 拿到的盒子序号
if(index>=0){
final ByteBuffer buffer=mMediaCodec.getInputBuffer(index);
buffer.clear();
buffer.put(buffer1);
if(length>0){
mMediaCodec.queueInputBuffer(index,0,length,System.nanoTime()/1000,0);//往空盒子里塞要编码的数据
}
}
MediaCodec.BufferInfo mInfo=new MediaCodec.BufferInfo();
int outIndex;
//每次取出的时候,把所有加工好的都循环取出来
do{
outIndex=mMediaCodec.dequeueOutputBuffer(mInfo,0);//取出已经编码好的数据,outIndex 表示盒子的位置
if(outIndex>=0){
ByteBuffer buffer=mMediaCodec.getOutputBuffer(outIndex);
buffer.position(mInfo.offset);
//AAC编码,需要加数据头,AAC编码数据头固定为7个字节
byte[] temp=new byte[mInfo.size+7];
buffer.get(temp,7,mInfo.size);
addADTStoPacket(temp,temp.length, Constants.AUDIO_SAMPLERATE, 1);//temp是处理后的acc数据,你可以把temp存储成一个文件就是一个acc的音频文件
mMediaCodec.releaseOutputBuffer(outIndex,false);
}else if(outIndex ==MediaCodec.INFO_TRY_AGAIN_LATER){
//TODO something
}else if(outIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
//TODO something
}
}while (outIndex>=0);
}
三、编码后的数据进行处理
上面有个方法是addADTStoPacket()这个方法就是用来给编码之后的音频数据进行头数据的添加,否则不能够进行播放;具体如下:
/**
* 添加头部信息
* Add ADTS header at the beginning of each and every AAC packet. This is
* needed as MediaCodec encoder generates a packet of raw AAC data.
* Note the packetLen must count in the ADTS header itself.
* packet 数据
* packetLen 数据长度
* sampleInHz 采样率
* chanCfgCounts 通道数
**/
private void addADTStoPacket(byte[] packet, int packetLen, int sampleInHz, int chanCfgCounts) {
int profile = 2; // AAC LC
int freqIdx = 8; // 16KHz 39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
switch (sampleInHz) {
case 8000: {
freqIdx = 11;
break;
}
case 16000: {
freqIdx = 8;
break;
}
default:
break;
}
int chanCfg = chanCfgCounts; // 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;
}
对于添加之后的数据可以进行保存了!
完整的代码:
package com.gufeilong.media;
import android.annotation.TargetApi;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Environment;
import com.gufeilong.utils.Constants;
import com.gufeilong.utils.MyLog;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static com.gufeilong.Activitys.TestActivity.isStop;
/**
* Created by Gu Feilong on 18-3-20.
* <p>
* 音频硬编码步骤思路:
* 1、获取到音频数据;
* 2、使用硬编码对数据进行编码;
* 3、保存编码后的数据为acc文件(需要对编码后的数据添加acc头文件);
* <p>
* 4、当需要进行与图像合成视频时则需要使用合成类?
*/
public class MyTestAudioEncorder {
private AudioRecord audioRecord;
private final int nInputChannels, sampleRate;
private byte[] inBuffer;
public static final int SAMPLES_PER_FRAME = 2048; // AAC, bytes/frame/channel
public static final int FRAMES_PER_BUFFER = 25; // AAC, frame/buffer/sec
private MediaCodec.BufferInfo bufferInfo = null;//存储的信息
protected MediaCodec mMediaCodec; // API >= 16(Android4.1.2)
private String TAG = MyTestAudioEncorder.class.getSimpleName();
/**
* 录音设备来源
*/
private static final int[] AUDIO_SOURCES = new int[]{
MediaRecorder.AudioSource.MIC,//麦克风
MediaRecorder.AudioSource.DEFAULT,//默认
MediaRecorder.AudioSource.CAMCORDER,
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
MediaRecorder.AudioSource.VOICE_RECOGNITION,
};
public MyTestAudioEncorder() {
this.nInputChannels = Constants.AUDIO_CHANNEL;//声道
this.sampleRate = Constants.AUDIO_SAMPLERATE;// 采样率
initAudioRecord();
initMediaCode();
}
/**
* 第一步初始化音频采集
*/
public void initAudioRecord() {
//获取最小音频buffer,给音频设置缓存区不能小于这个大小
int min_buffer_size = AudioRecord.getMinBufferSize(Constants.AUDIO_CHANNEL, Constants.AUDIO_SAMPLERATE,
AudioFormat.ENCODING_PCM_16BIT);
//用来获取最终的缓存区大小 SAMPLES_PER_FRAME 每帧的采样率 FRAMES_PER_BUFFER 每次缓存的帧数
int buffer_size = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER;
if (buffer_size < min_buffer_size)
buffer_size = ((min_buffer_size / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2;// TODO: 18-3-29 未理解为何这里要乘2?
//用来缓存音频数据的buffer
inBuffer = new byte[SAMPLES_PER_FRAME];
//根据音频的来源进行初始化:麦克风,或者其他
for (final int source : AUDIO_SOURCES) {
try {
//初始化AudioRecord,音频来源,采样率,声道,返回的音频格式,缓存区的大小
audioRecord = new AudioRecord(source, this.sampleRate, this.nInputChannels,
AudioFormat.ENCODING_PCM_16BIT, buffer_size);
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED)
audioRecord = null;
} catch (final Exception e) {
audioRecord = null;
}
if (audioRecord != null) break;
}
}
/**
* 初始化硬编码器
*/
private void initMediaCode() {
try {
//尝试是否能够进行初始化成功
mMediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
} catch (Exception e) {
e.printStackTrace();
return;
}
// AAC 硬编码器
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");//音频编码
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); //声道数(这里是数字)
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, Constants.AUDIO_SAMPLERATE); //采样率
format.setInteger(MediaFormat.KEY_BIT_RATE, Constants.BIT_RATE); //码率
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
bufferInfo = new MediaCodec.BufferInfo();//记录编码完成的buffer的信息
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);// MediaCodec.CONFIGURE_FLAG_ENCODE 标识为编码器
mMediaCodec.start();//开始工作
}
/**
* 开始对采集到的音频进行硬编码
*/
public void startEncodec() {
//开始进行
if (audioRecord != null && audioRecord.getState() != AudioRecord.STATE_UNINITIALIZED) {
audioRecord.startRecording();//开始进行录音
while (true && !isStop) {
try {
Arrays.fill(inBuffer, (byte) 0);//把原来的数据清空
int err = audioRecord.read(inBuffer, 0, inBuffer.length);//开始从audioRecord中读pcm输出出来
if (err <= 0) {
if (err == AudioRecord.ERROR_INVALID_OPERATION) {
MyLog.e("OF", "AudioRecord error ERROR_INVALID_OPERATION");
return;
}
if (err == AudioRecord.ERROR_BAD_VALUE) {
MyLog.e("OF", "AudioRecord error ERROR_BAD_VALUE");
return;
}
if (err == 0) {
MyLog.e("OF", "AudioRecord error 0 samples read");
return;
}
} else {
hardEncoder(inBuffer, inBuffer.length);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private ByteBuffer[] inputBuffers; //可用编码的bytebuffer的数组
private ByteBuffer[] outputBuffers; //编码完成的bytebuffer的数组
private ByteBuffer inputBuffer;
private ByteBuffer outputBuffer;
private int inputBufferIndex; //可用的编码的bytebuffer的数组的索引
private int outputBufferIndex; //可用的编码完成的bytebuffer的数组的索引
protected static final int TIMEOUT_USEC = 1000; // 10[msec]
public static final String MIME_TYPE = "audio/mp4a-latm";
/**
* 进行实际编码
* @param buffer1
* @param length
*/
public void hardEncoder(final byte[] buffer1, final int length) {
//之前的音频录制是直接循环读取,然后写入文件,这里需要做编码处理再写入文件
//这里的处理就是和之前传送带取盒子放原料的流程一样了,注意一般在子线程中循环处理
int index=mMediaCodec.dequeueInputBuffer(-1);//拿空盒子,index 拿到的盒子序号
if(index>=0){
final ByteBuffer buffer=mMediaCodec.getInputBuffer(index);
buffer.clear();
buffer.put(buffer1);
if(length>0){
mMediaCodec.queueInputBuffer(index,0,length,System.nanoTime()/1000,0);//往空盒子里塞要编码的数据
}
}
MediaCodec.BufferInfo mInfo=new MediaCodec.BufferInfo();
int outIndex;
//每次取出的时候,把所有加工好的都循环取出来
do{
outIndex=mMediaCodec.dequeueOutputBuffer(mInfo,0);//取出已经编码好的数据,outIndex 表示盒子的位置
if(outIndex>=0){
ByteBuffer buffer=mMediaCodec.getOutputBuffer(outIndex);
buffer.position(mInfo.offset);
//AAC编码,需要加数据头,AAC编码数据头固定为7个字节
byte[] temp=new byte[mInfo.size+7];
buffer.get(temp,7,mInfo.size);
addADTStoPacket(temp,temp.length, Constants.AUDIO_SAMPLERATE, 1);
saveDataWithAppend(temp, temp.length, "encorded.aac");
/
mMediaCodec.releaseOutputBuffer(outIndex,false);
}else if(outIndex ==MediaCodec.INFO_TRY_AGAIN_LATER){
//TODO something
}else if(outIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
//TODO something
}
}while (outIndex>=0);
}
public static void saveDataWithAppend(byte[] data, int len, String fileName) {
String filePath = Environment.getExternalStorageDirectory() + File.separator + fileName;
try {
FileOutputStream outputStream = new FileOutputStream(new File(filePath), true);
outputStream.write(data, 0, len);
} catch (FileNotFoundException e) {
} catch (Exception e) {
}
}
/**
* 添加头部信息
* Add ADTS header at the beginning of each and every AAC packet. This is
* needed as MediaCodec encoder generates a packet of raw AAC data.
* Note the packetLen must count in the ADTS header itself.
* packet 数据
* packetLen 数据长度
* sampleInHz 采样率
* chanCfgCounts 通道数
**/
private void addADTStoPacket(byte[] packet, int packetLen, int sampleInHz, int chanCfgCounts) {
int profile = 2; // AAC LC
int freqIdx = 8; // 16KHz 39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
switch (sampleInHz) {
case 8000: {
freqIdx = 11;
break;
}
case 16000: {
freqIdx = 8;
break;
}
default:
break;
}
int chanCfg = chanCfgCounts; // 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;
}
/**
* select the first codec that match a specific MIME type
*
* @param mimeType
* @return
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static final MediaCodecInfo selectAudioCodec(final String mimeType) {
MediaCodecInfo result = null;
// get the list of available codecs
final int numCodecs = MediaCodecList.getCodecCount();
LOOP:
for (int i = 0; i < numCodecs; i++) {
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder()) { // skipp decoder
continue;
}
final String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
if (result == null) {
result = codecInfo;
break LOOP;
}
}
}
}
return result;
}
}