Android中Pcm文件转Amr文件
Android
最经在做一个聊天的模块,聊天的模块很简单:录音-上传-接收-播放
录音
录音部分可以采用MediaRecord和AudioRecord两个类进行录音.但是各有优缺点.
MediaRecord已经封装了很多方法,方便使用.
AudioRecord能获取录音的原生数据,以便对录音二次加工.
在录音过程我采用的是AudioRecord.但是AudioRecord录音数据是PCM格式,数据占用存储空间很大.必须压缩后才能传输.项目中的压缩是项目其他成员写的一个so库进行压缩的,有点不太方便.在网上翻阅了下,其实Android系统内部已经携带有压缩的库文件了.
压缩库
Android自带的有一个Pcm转amr的库:media_jni.so.
但是由于是Android系统内部的库,无法直接使用.根据网上的说明,最终终于弄明白如何使用了.
AmrInputStream
在要使用压缩库的项目中新建包:
com.android.media
在此包中新建AmrInputStream类,代码如下:
publicfinalclassAmrInputStreamextendsInputStream{
static{
System.loadLibrary("media_jni");
}
privatefinalstaticStringTAG="AmrInputStream";
// frame is 20 msec at 8.000 khz
privatefinalstaticintSAMPLES_PER_FRAME=8000*20/1000;
// pcm input stream
privateInputStreammInputStream;
// native handle
privateintmGae;
// result amr stream
privatebyte[]mBuf=newbyte[SAMPLES_PER_FRAME*2];
privateintmBufIn=0;
privateintmBufOut=0;
// helper for bytewise read()
privatebyte[]mOneByte=newbyte[1];
/**
* Create a new AmrInputStream, which converts 16 bit PCM to AMR
* @param inputStream InputStream containing 16 bit PCM.
*/
publicAmrInputStream(InputStreaminputStream){
mInputStream=inputStream;
mGae=GsmAmrEncoderNew();
GsmAmrEncoderInitialize(mGae);
}
@Override
publicintread()throwsIOException{
intrtn=read(mOneByte,0,1);
returnrtn==1?(0xff&mOneByte[0]):-1;
}
@Override
publicintread(byte[]b)throwsIOException{
returnread(b,0,b.length);
}
@Override
publicintread(byte[]b,intoffset,intlength)throwsIOException{
if(mGae==0)thrownewIllegalStateException("not open");
// local buffer of amr encoded audio empty
if(mBufOut>=mBufIn){
// reset the buffer
mBufOut=0;
mBufIn=0;
// fetch a 20 msec frame of pcm
for(inti=0;i
intn=mInputStream.read(mBuf,i,SAMPLES_PER_FRAME*2-i);
if(n==-1)return-1;
i+=n;
}
// encode it
mBufIn=GsmAmrEncoderEncode(mGae,mBuf,0,mBuf,0);
}
// return encoded audio to user
if(length>mBufIn-mBufOut)length=mBufIn-mBufOut;
System.arraycopy(mBuf,mBufOut,b,offset,length);
mBufOut+=length;
returnlength;
}
@Override
publicvoidclose()throwsIOException{
try{
if(mInputStream!=null)mInputStream.close();
}finally{
mInputStream=null;
try{
if(mGae!=0)GsmAmrEncoderCleanup(mGae);
}finally{
try{
if(mGae!=0)GsmAmrEncoderDelete(mGae);
}finally{
mGae=0;
}
}
}
}
@Override
protectedvoidfinalize()throwsThrowable{
if(mGae!=0){
close();
thrownewIllegalStateException("someone forgot to close AmrInputStream");
}
}
//
// AudioRecord JNI interface
//
privatestaticnativeintGsmAmrEncoderNew();
privatestaticnativevoidGsmAmrEncoderInitialize(intgae);
privatestaticnativeintGsmAmrEncoderEncode(intgae,
byte[]pcm,intpcmOffset,byte[]amr,intamrOffset)throwsIOException;
privatestaticnativevoidGsmAmrEncoderCleanup(intgae);
privatestaticnativevoidGsmAmrEncoderDelete(intgae);
}
AmrEmcoder
只有AmrInputStream类是不够,还需要一个转码的类AmrEncoder,代码如下:
publicclassAmrEncoder{
publicstaticvoidpcm2Amr(StringpcmPath,StringamrPath){
FileInputStreamfis;
try{
fis=newFileInputStream(pcmPath);
pcm2Amr(fis,amrPath);
fis.close();
}catch(FileNotFoundExceptione1){
e1.printStackTrace();
}catch(IOExceptione){
e.printStackTrace();
}
}
publicstaticvoidpcm2Amr(InputStreampcmStream,StringamrPath){
try{
AmrInputStreamais=newAmrInputStream(pcmStream);
OutputStreamout=newFileOutputStream(amrPath);
byte[]buf=newbyte[4096];
intlen=-1;
/*
* 下面的AMR的文件头,缺少这几个字节是不行的
*/
out.write(0x23);
out.write(0x21);
out.write(0x41);
out.write(0x4D);
out.write(0x52);
out.write(0x0A);
while((len=ais.read(buf))>0){
out.write(buf,0,len);
}
out.close();
ais.close();
}catch(FileNotFoundExceptione){
e.printStackTrace();
}catch(IOExceptione){
e.printStackTrace();
}
}
}
这里有两个方法:
1. pcm2Amr(String pcmPath , String amrPath): 将pcm文件转为amr文件
2. pcm2Amr(InputStream pcmStream, String amrPath): 将pcm数据流转为amr文件
测试
测试的界面
测试的界面很简单,就一个按钮一个文本显示,布局界面就不再给出,下面是MainActivity代码:
publicclassMainActivityextendsActivityimplementsOnClickListener{
@Override
protectedvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
privateTextViewhintView;
privateButtonstartButton;
privatevoidinitView(){
startButton=(Button)findViewById(R.id.btn_start);
startButton.setOnClickListener(this);
hintView=(TextView)findViewById(R.id.hint);
}
@Override
publicvoidonClick(Viewv){
if(v.getId()==R.id.btn_start){
transferButtonClicked();
}
}
privatevoidtransferButtonClicked(){
showWaitDialog();
startTransfer();
}
privateProgressDialogwaitDialog;
privatevoidshowWaitDialog(){
waitDialog=newProgressDialog(this);
waitDialog.setTitle(getResources().getString(R.string.transfer_wait_title));
waitDialog.setMessage(getResources().getString(R.string.transfer_wait_message));
waitDialog.show();
}
privatevoidstartTransfer(){
newTransferThread(this,newTransferCallback(){
@Override
publicvoidonSuccess(){
transferSuccess();
}
@Override
publicvoidonFailed(){
}
}).start();
}
privatevoidtransferSuccess(){
runOnUiThread(newRunnable(){
@Override
publicvoidrun(){
waitDialog.dismiss();
hintView.setText(getResources().getString(R.string.transfer_result));
ToastUtil.showShort(MainActivity.this,R.string.success_hint);
}
});
}
}
转换线程
由于文件转换是耗时操作,所以需要一个转换线程来实现文件转换.
publicclassTransferThreadextendsThread{
privateTransferCallbackcallback;
privateContextcontext;
publicTransferThread(Contextcontext,TransferCallbackcallback){
this.callback=callback;
this.context=context;
}
@Override
publicvoidrun(){
transfer();
}
privatevoidtransfer(){
StringrootPath=Environment.getExternalStorageDirectory().getPath();
StringamrPath=rootPath+"/test.amr";
try{
InputStreampcmStream=context.getAssets().open("test.pcm");
AmrEncoder.pcm2Amr(pcmStream,amrPath);
callback.onSuccess();
}catch(IOExceptione){
callback.onFailed();
e.printStackTrace();
}
}
publicstaticinterfaceTransferCallback{
voidonSuccess();
voidonFailed();
}
}
测试结果
经过测试,160KB的test.pcm压缩后的amr文件大小为15KB,且可以正常播放.
本文的项目文件在此:Pcm2Amr