使用AudioRecord采集音频pcm数据,使用MediaCodec转化为aac数据
package com.yuanxuzhen.yuanrecord;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RecordAudio {
private int mSampleRate = 44100;
private int mInChannel = AudioFormat.CHANNEL_IN_STEREO;
private int mSampleFormat = AudioFormat.ENCODING_PCM_16BIT;
public int getmChannelCount() {
return mChannelCount;
}
private int mChannelCount = 2;
private String mFilePath = null;
private AudioRecord mRecorder = null;
ExecutorService mExecutorService;
private int mBufferSize;
public enum AUDIO_STAUS {
START,
PAUSE,
STOP
}
AUDIO_STAUS mStatus;
public void init(Context context, int sampleRate, int inChannel, int sampleFormat, String filePath) {
mSampleRate = sampleRate;
mInChannel = inChannel;
mSampleFormat = sampleFormat;
mFilePath = filePath;
mBufferSize = AudioRecord.getMinBufferSize(mSampleRate,
mInChannel, mSampleFormat);
mRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC,
mSampleRate, mInChannel,
mSampleFormat,
mBufferSize);
mChannelCount = mRecorder.getChannelCount();
mExecutorService = Executors.newCachedThreadPool();
}
public void startRecord() {
if(mStatus == AUDIO_STAUS.START){
return;
}
if(mStatus == AUDIO_STAUS.STOP){
File file = new File(mFilePath);
if(file.exists()){
file.delete();
}
}
mStatus = AUDIO_STAUS.START;
mRecorder.startRecording();
mExecutorService.execute(new Runnable() {
@Override
public void run() {
realStartRecord();
}
});
}
private void realStartRecord() {
// Write the output audio in byte
short sData[] = new short[mBufferSize / 2];
byte byteData[] = new byte[mBufferSize];
FileOutputStream os = null;
try {
os = new FileOutputStream(mFilePath, true);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
while (mStatus == AUDIO_STAUS.START) {
// gets the voice output from microphone to byte format
if(mSampleFormat == AudioFormat.ENCODING_PCM_16BIT){
int size = mRecorder.read(sData, 0, mBufferSize / 2);
System.out.println("Short wirting to file size=" + size);
try {
// // writes the data to file from buffer
// // stores the voice buffer
byte bData[] = short2byte(sData);
os.write(bData, 0, mBufferSize);
} catch (IOException e) {
e.printStackTrace();
}
}else{
int size = mRecorder.read(byteData, 0, mBufferSize);
System.out.println("Short wirting to file size=" + size);
try {
// // writes the data to file from buffer
// // stores the voice buffer
os.write(byteData, 0, mBufferSize);
} catch (IOException e) {
e.printStackTrace();
}
}
}
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//convert short to byte
private byte[] short2byte(short[] sData) {
int shortArrsize = sData.length;
byte[] bytes = new byte[shortArrsize * 2];
for (int i = 0; i < shortArrsize; i++) {
bytes[i * 2] = (byte) (sData[i] & 0x00FF);
bytes[(i * 2) + 1] = (byte) (sData[i] >> 8);
sData[i] = 0;
}
return bytes;
}
public void pauseRecord(){
mStatus = AUDIO_STAUS.PAUSE;
if(mRecorder != null)
mRecorder.stop();
}
public void stopRecord(){
mStatus = AUDIO_STAUS.STOP;
if(mRecorder != null)
mRecorder.stop();
release();
}
public void release(){
if(mRecorder == null){
return;
}
mRecorder.release();
mRecorder = null;
}
}
下面是aac编码的操作,难点在于从pcm文件每次都少数据给编码器。如果听到的声音不对,就调整这个参数。 byte[] byteArray = new byte[2048];我这里设置了2048.在ffmpeg中,这个参数来自于编码器(ffmpeg计算方法是size= nb_sample * channel * bytePerSample).
package com.yuanxuzhen.yuanrecord;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
import androidx.annotation.RequiresApi;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class EncodeAudioAAC {
private int mSampleRate = 44100;
private int mInChannel = AudioFormat.CHANNEL_IN_STEREO;
private int mSampleFormat = AudioFormat.ENCODING_PCM_16BIT;
private int mChannelCount = 2;
private String mDstFilePath = null;
private String mSrcFilePath = null;
MediaCodec mEncorder;
ExecutorService mExecutorService;
private int mBufferSize;
private IHandlerCallback mCallback;
public void init(Context context, int sampleRate,
int channelCount, int sampleFormat,
String srcPath, String dstPath,
IHandlerCallback callback) {
mSampleRate = sampleRate;
mChannelCount = channelCount;
mSampleFormat = sampleFormat;
mSrcFilePath = srcPath;
mDstFilePath = dstPath;
mExecutorService = Executors.newCachedThreadPool();
mCallback = callback;
mBufferSize = AudioRecord.getMinBufferSize(mSampleRate,
mInChannel, mSampleFormat);
try {
mEncorder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
} catch (Exception e) {
e.printStackTrace();
}
MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
mSampleRate,
mChannelCount);
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AAC);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, 100000);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, mBufferSize);
mEncorder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
/**
* 开始编码
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void startEncording() {
if (mEncorder == null) {
if(mCallback != null){
mCallback.onFail();
}
return;
}
mEncorder.start();
try {
FileInputStream inputStream = new FileInputStream(mSrcFilePath);
FileOutputStream mFileStream = new FileOutputStream(mDstFilePath);
MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
while(true) {
long a = System.currentTimeMillis();
// 从队列中取出录音的一帧音频数据
byte[] byteArray = new byte[2048];
int readSize = inputStream.read(byteArray);
if(readSize <= 0){
break;
}
ByteBuffer buf = ByteBuffer.wrap(byteArray);
// 取出InputBuffer,填充音频数据,然后输送到编码器进行编码
int inputBufferIndex = mEncorder.dequeueInputBuffer(0);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = mEncorder.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(buf);
mEncorder.queueInputBuffer(inputBufferIndex, 0, readSize, System.nanoTime(), 0);
}
// 取出编码好的一帧音频数据,然后给这一帧添加ADTS头
int outputBufferIndex = mEncorder.dequeueOutputBuffer(mBufferInfo, 0);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mEncorder.getOutputBuffer(outputBufferIndex);
int outBufferSize = outputBuffer.limit() + 7;
byte[] aacBytes = new byte[outBufferSize];
addADTStoPacket(aacBytes, outBufferSize);
outputBuffer.get(aacBytes, 7, outputBuffer.limit());
mFileStream.write(aacBytes);
mEncorder.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mEncorder.dequeueOutputBuffer(mBufferInfo, 0);
}
long b = System.currentTimeMillis() - a;
Log.i("AudioEncode", "编码耗时-毫秒==" + b);
}
if (mFileStream != null) {
try {
mFileStream.flush();
mFileStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream != null) {
try {
mFileStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (mEncorder != null) {
mEncorder.stop();
}
if(mCallback != null){
mCallback.onSuccess();
}
} catch (FileNotFoundException e) {
if(mCallback != null){
mCallback.onFail();
}
e.printStackTrace();
} catch (IOException e) {
if(mCallback != null){
mCallback.onFail();
}
e.printStackTrace();
}
}
/**
* 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.
* <p>
* Note the packetLen must count in the ADTS header itself !!! .
* 注意,这里的packetLen参数为raw aac Packet Len + 7; 7 bytes adts header
**/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC,MediaCodecInfo.CodecProfileLevel.AACObjectLC;
int freqIdx = 4; //见后面注释avpriv_mpeg4audio_sample_rates中32000对应的数组下标,来自ffmpeg源码
int chanCfg = 1; //见后面注释channel_configuration,AudioFormat.CHANNEL_IN_MONO 单声道(声道数量)
/*int avpriv_mpeg4audio_sample_rates[] = {96000, 88200, 64000, 48000, 44100, 32000,24000, 22050, 16000, 12000, 11025, 8000, 7350};
channel_configuration: 表示声道数chanCfg
0: Defined in AOT Specifc Config
1: 1 channel: front-center
2: 2 channels: front-left, front-right
3: 3 channels: front-center, front-left, front-right
4: 4 channels: front-center, front-left, front-right, back-center
5: 5 channels: front-center, front-left, front-right, back-left, back-right
6: 6 channels: front-center, front-left, front-right, back-left, back-right, LFE-channel
7: 8 channels: front-center, front-left, front-right, side-left, side-right, back-left, back-right, LFE-channel
8-15: Reserved
*/
// fill in ADTS data
packet[0] = (byte) 0xFF;
//packet[1] = (byte)0xF9;
packet[1] = (byte) 0xF1;//解决ios 不能播放问题
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;
}
public void release(){
if(mEncorder == null){
return;
}
mEncorder.release();
mEncorder = null;
}
}
package com.yuanxuzhen.yuanrecord;
import android.content.Context;
import android.os.Environment;
import java.io.File;
public class DirUtil {
public static final String WEBVIEW_CACHE = ".webviewCache";
public static final String IMAGE_PATH = "image";
public static final String DOWNLOAD_PATH = "download";
public static final String VIDEO_PATH = ".video";
public static final String NET_PATH = ".net";
//image
public static String getImageDir(Context context) {
return getCacheDir(context) + File.separator + IMAGE_PATH;
}
//webview
public static String getWebviewCache(Context context) {
return getCacheDir(context) + File.separator + WEBVIEW_CACHE;
}
//download
public static String getDownloadDir(Context context) {
return getCacheDir(context) + File.separator + DOWNLOAD_PATH;
}
//video
public static String getVideoPath(Context context) {
return getCacheDir(context) + File.separator + VIDEO_PATH;
}
//net
public static String getNetPath(Context context) {
return getCacheDir(context) + File.separator + NET_PATH;
}
public static String getCacheDir(Context context) {
if (context == null) {
return "";
}
String path = null;
if (context.getExternalCacheDir() != null
&& (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable())) {
//外部存储可用
path = context.getExternalCacheDir().getPath();
} else {
//内部存储不可用
path = context.getCacheDir().getPath();
}
return path;
}
}
package com.yuanxuzhen.yuanrecord;
public interface IHandlerCallback {
void onSuccess(Object... object);
void onFail(Object... object);
}
调用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
android:paddingTop="100dp">
<Button
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start!"
android:layout_gravity="center_horizontal"/>
<Button
android:id="@+id/btnPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="pause!"
android:layout_gravity="center_horizontal"/>
<Button
android:id="@+id/btnStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="stop!"
android:layout_gravity="center_horizontal"/>
<Button
android:id="@+id/encode_aac"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="encode pcm to aac "
android:layout_gravity="center_horizontal"/>
</LinearLayout>
package com.yuanxuzhen.yuanrecord;
import android.app.Activity;
import android.media.AudioFormat;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import java.io.File;
public class Audio_Record extends Activity {
private static final int RECORDER_SAMPLERATE = 44100;
private static final int RECORDER_CHANNELS = AudioFormat.CHANNEL_IN_STEREO;
private static final int RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
private RecordAudio mRecorder;
private EncodeAudioAAC mEncoder;
private String pcmPath;
private String aacPath;
private Button btnStart;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_record);
setButtonHandlers();
btnStart = findViewById(R.id.btnStart);
enableButton(R.id.btnStart, true);
enableButton(R.id.btnPause, false);
enableButton(R.id.btnStop, false);
pcmPath = DirUtil.getCacheDir(this) + File.separator + "123.pcm";
aacPath = DirUtil.getCacheDir(this) + File.separator + "123.aac";
mRecorder = new RecordAudio();
mRecorder.init(this, RECORDER_SAMPLERATE, RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING, pcmPath);
mEncoder = new EncodeAudioAAC();
mEncoder.init(this,
RECORDER_SAMPLERATE,
mRecorder.getmChannelCount(),
RECORDER_AUDIO_ENCODING,
pcmPath,
aacPath,
new IHandlerCallback() {
@Override
public void onSuccess(Object... object) {
btnStart.post(new Runnable() {
@Override
public void run() {
Toast.makeText(Audio_Record.this, "解码成功", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onFail(Object... object) {
btnStart.post(new Runnable() {
@Override
public void run() {
Toast.makeText(Audio_Record.this, "解码失败", Toast.LENGTH_SHORT).show();
}
});
}
});
}
private void setButtonHandlers() {
((Button) findViewById(R.id.btnStart)).setOnClickListener(btnClick);
((Button) findViewById(R.id.btnStop)).setOnClickListener(btnClick);
((Button) findViewById(R.id.btnPause)).setOnClickListener(btnClick);
((Button) findViewById(R.id.encode_aac)).setOnClickListener(btnClick);
}
private void enableButton(int id, boolean isEnable) {
((Button) findViewById(id)).setEnabled(isEnable);
}
private void enableButtons(RecordAudio.AUDIO_STAUS status) {
if (status == RecordAudio.AUDIO_STAUS.START) {
enableButton(R.id.btnStart, false);
enableButton(R.id.btnPause, true);
enableButton(R.id.btnStop, true);
} else if (status == RecordAudio.AUDIO_STAUS.PAUSE) {
enableButton(R.id.btnStart, true);
enableButton(R.id.btnPause, false);
enableButton(R.id.btnStop, true);
} else if (status == RecordAudio.AUDIO_STAUS.STOP) {
enableButton(R.id.btnStart, true);
enableButton(R.id.btnPause, false);
enableButton(R.id.btnStop, false);
}
}
private View.OnClickListener btnClick = new View.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnStart: {
Toast.makeText(Audio_Record.this, "开始录制", Toast.LENGTH_SHORT).show();
mRecorder.startRecord();
enableButtons(mRecorder.mStatus);
break;
}
case R.id.btnPause: {
Toast.makeText(Audio_Record.this, "暂停录制", Toast.LENGTH_SHORT).show();
mRecorder.pauseRecord();
enableButtons(mRecorder.mStatus);
break;
}
case R.id.btnStop: {
Toast.makeText(Audio_Record.this, "停止录制", Toast.LENGTH_SHORT).show();
mRecorder.stopRecord();
enableButtons(mRecorder.mStatus);
break;
}
case R.id.encode_aac: {
Toast.makeText(Audio_Record.this, "开始转码", Toast.LENGTH_SHORT).show();
mEncoder.startEncording();
break;
}
}
}
};
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
finish();
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onDestroy() {
mRecorder.release();
mEncoder.release();
super.onDestroy();
}
}