Android ilbc 语音对话示范(一)开篇

来源:http://blog.csdn.net/ranxiedao/article/details/7783015

----------------------------------------------------------------------------------------------------------------------

近刚刚做成了Android ilbc的项目,实现了语音对话功能,效果不错,ilbc将音频数据编码压缩为AMR格式,这种格式的音频压缩率很高,

960B的数据经过编码后长度仅仅为100B ,如此小的数据非常适合移动网络下的语音传输,节省大量的带宽,当然,高压缩比就意味着语

音质量损失高,不过实际使用中,AMR格式完全能够满足语音对话的要求。

之前使用别人给的一个现成的demo,可是发现仅仅是从GitHub扣下来的,源地址如下https://github.com/bjdodson/iLBC-Android,

这个我没有真正试过到底如何,不过首先不爽的就是使用人家的现成的代码后,你会发现连里面的包名都不能改,一改就报错,因为编

译的 .so 库无法对应到该包名下面,并且,经过查看里面的Android.mk发现,他使用的是webrtc,这个是google 推出的一个开源项目,

里面带有 iLBC的库,我把这个 webrtc下载下来,整整有190多M!并且用这个编译,时间非常漫长,还容易出错,一狠心,干脆自己编

译一份.so 库出来。

最终,自己编写了一个简易的DEMO,包括一个服务端,项目最终源代码在这里:

http://download.csdn.net/detail/ranxiedao/4923759

接下来的后续文章中,将在Ubuntu系统上演示如何完整地实现语音对话的基本功能。

其二:Android ilbc 语音对话示范(二)代码搭建

基于上一篇中提到的google网站的一份代码,这个需要git下载,我上传了一份在CSDN,进行了修改:下载链接: (http://download.csdn.net/detail/ranxiedao/4450917)。

现在开始讲解代码结构搭建环节:

  1. 要求:
  2. 环境:Ubuntu12.04(其他Linux环境皆可),Android2.2及以上系统
  3. 工具:Elicpse3.7,AndroidNDKr7,AndroidSDK

1.新建工程:

打开Eclipse,新建一个Android程序,名称为 AndroidILBC

2.添加底层代码:

将下载的源码中的 jni 文件夹复制到新建的工程的根目录下,此时,代码结构如下:

android 网络语音电话合集 此文为备份_第1张图片

3.Android.mk编写:

  1. LOCAL_PATH:=$(callmy-dir)
  2. include$(CLEAR_VARS)
  3. LOCAL_MODULE:=libilbc
  4. codec_dir:=iLBC_RFC3951#ilbc源代码的目录
  5. LOCAL_SRC_FILES:=\
  6. $(codec_dir)/anaFilter.c\
  7. $(codec_dir)/constants.c\
  8. $(codec_dir)/createCB.c\
  9. $(codec_dir)/doCPLC.c\
  10. $(codec_dir)/enhancer.c\
  11. $(codec_dir)/filter.c\
  12. $(codec_dir)/FrameClassify.c\
  13. $(codec_dir)/gainquant.c\
  14. $(codec_dir)/getCBvec.c\
  15. $(codec_dir)/helpfun.c\
  16. $(codec_dir)/hpInput.c\
  17. $(codec_dir)/hpOutput.c\
  18. $(codec_dir)/iCBConstruct.c\
  19. $(codec_dir)/iCBSearch.c\
  20. $(codec_dir)/iLBC_decode.c\
  21. $(codec_dir)/iLBC_encode.c\
  22. $(codec_dir)/LPCdecode.c\
  23. $(codec_dir)/LPCencode.c\
  24. $(codec_dir)/lsf.c\
  25. $(codec_dir)/packing.c\
  26. $(codec_dir)/StateConstructW.c\
  27. $(codec_dir)/StateSearchW.c\
  28. $(codec_dir)/syntFilter.c
  29. LOCAL_C_INCLUDES+=$(common_C_INCLUDES)
  30. LOCAL_PRELINK_MODULE:=false
  31. include$(BUILD_STATIC_LIBRARY)
  32. #BuildJNIwrapper
  33. include$(CLEAR_VARS)
  34. LOCAL_MODULE:=libilbc-codec#生成的.so库名,可以自行修改
  35. LOCAL_C_INCLUDES+=\
  36. $(JNI_H_INCLUDE)\
  37. $(codec_dir)
  38. LOCAL_SRC_FILES:=ilbc-codec.c
  39. LOCAL_LDLIBS:=-L$(SYSROOT)/usr/lib-llog
  40. LOCAL_STATIC_LIBRARIES:=libilbc
  41. LOCAL_PRELINK_MODULE:=false
  42. include$(BUILD_SHARED_LIBRARY)


4.代码分析:

打开jni 文件夹下的ilbc-codec.c文件,里面总共只有五个函数,负责音频编解码器的初始化,以及音频的编码和解码。其中的三个方法:

  1. jintJava_com_googlecode_androidilbc_Codec_init(
  2. JNIEnv*env,jobjectthis,jintmode)
  3. jintJava_com_googlecode_androidilbc_Codec_encode(
  4. JNIEnv*env,jobjectthis,
  5. jbyteArraysampleArray,jintsampleOffset,jintsampleLength,
  6. jbyteArraydataArray,jintdataOffset)
  7. jintJava_com_googlecode_androidilbc_Codec_decode(
  8. JNIEnv*env,jobjectthis,
  9. jbyteArraydataArray,jintdataOffset,jintdataLength,
  10. jbyteArraysampleArray,jintsampleOffset)

根据这三个函数的名称就可以知道,使用来让 Java层代码调用的三个函数,现在我们对这三个函数进行改造(仅仅是换个函数名称而已)

5.写Java层native 方法:

在程序的Java层代码中,建立一个包,用于放 NDK 的java层代码,比如我建一个名为xmu.swordbearer.audio的包,里面新建一个类:

AudioCodec.java,在这个类中只负责对底层C函数进行调用,相当于一个工具类。新建三个public staic native int方法:

  1. packagexmu.swordbearer.audio;
  2. publicclassAudioCodec{
  3. //initializedecoderandencoder
  4. publicstaticnativeintaudio_codec_init(intmode);
  5. //encode
  6. publicstaticnativeintaudio_encode(byte[]sample,intsampleOffset,
  7. intsampleLength,byte[]data,intdataOffset);
  8. //decode
  9. publicstaticnativeintaudio_decode(byte[]data,intdataOffset,
  10. intdataLength,byte[]sample,intsampleLength);
  11. }


三个方法分别用于初始化,音频编码,音频解码,在这里只需声明为native方法,不用写任何代码;

6.编译.h 头文件:

(如果不会,请参考之前的文章)
打开终端,定位到第 5步建立的AudioCodec.java目录下,如下:

这一步很关键,进入到src目录后,就要带上AudioCodec这个类的包名,此例中的包名为:xmu.swordbearer.audio如果上述步

骤正确,就会在该包下生成一个xmu_swordbearer_audio_AudioCodec.h的头文件,内容如下:

  1. /*
  2. *Class:xmu_swordbearer_audio_AudioCodec
  3. *Method:audio_codec_init
  4. *Signature:(I)I
  5. */
  6. JNIEXPORTjintJNICALLJava_xmu_swordbearer_audio_AudioCodec_audio_1codec_1init
  7. (JNIEnv*,jclass,jint);
  8. /*
  9. *Class:xmu_swordbearer_audio_AudioCodec
  10. *Method:audio_encode
  11. *Signature:([BII[BI)I
  12. */
  13. JNIEXPORTjintJNICALLJava_xmu_swordbearer_audio_AudioCodec_audio_1encode
  14. (JNIEnv*,jclass,jbyteArray,jint,jint,jbyteArray,jint);
  15. /*
  16. *Class:xmu_swordbearer_audio_AudioCodec
  17. *Method:audio_decode
  18. *Signature:([BII[BI)I
  19. */
  20. JNIEXPORTjintJNICALLJava_xmu_swordbearer_audio_AudioCodec_audio_1decode
  21. (JNIEnv*,jclass,jbyteArray,jint,jint,jbyteArray,jint);

第4步中分析的三个方法修改,打开jni 下的ilbc-codec.c文件,,把那三个名称分别用刚刚生成的这三个方法名替换,具体对应如下:

  1. Java_com_googlecode_androidilbc_Codec_init
  2. 改为:
  3. Java_xmu_swordbearer_audio_AudioCodec_audio_1codec_1init
  4. Java_com_googlecode_androidilbc_Codec_encode
  5. 改为:
  6. Java_xmu_swordbearer_audio_AudioCodec_audio_1encode
  7. Java_com_googlecode_androidilbc_Codec_decode
  8. 改为:
  9. Java_xmu_swordbearer_audio_AudioCodec_audio_1decode

仅是一个 复制,粘贴的过程!!!

当然,如果你写的JAVA代码的包名或者方法名不一样,那生成的 .h 文件中的方法也就不一样,这就是为什么编译好一个.so库后,不能随

便修改 native方法所在类的包名,因为方法名会也就改变了.

7. 编译 .so 库

下来就是要编译生成.so库了,正如上面Android.mk文件中写的,最终编译生成的库是libilbc-codec.so,编译方法如下:

打开终端,定位到jni文件夹下面,输入ndk-build回车,会看到如下情景:

android 网络语音电话合集 此文为备份_第2张图片

看到倒数第二行了吗?libs/armeabi/libilbc-codec.so,说明已经生成了我们需要的动态库,这时你会发现在工程的根目录下多了一个libs

文件夹,里面有个armeabi目录,打开后就有一个libilbc-codec.so的文件

android 网络语音电话合集 此文为备份_第3张图片

得到这个库之后,我们所有与底层有关的工作全部完成,被Linux 虐了的人可以马上转战Windows下,后续工作已经不需要在Liunx下进行了。

OK ,库编译完成了,后续将会示范音频的采集以及如何通过Java来调用 底层编解码 函数。


8.总结:

光这个编译过程我研究了两天才跑通,就像上一篇所提到的,开始使用同学给的demo,尽管可以运行,但是程序的扩展性不好,你不可

写个程序后,里面还夹杂一个诡异的包名,而这个包名你连碰都不敢碰,再加上各种代码的嵌套,总结了一句话----自己动手,丰衣足食!

几番研究后,对Android.mk 文件的编写积累了一些经验,NDK 编译过程逐渐顺手,现在可以任意修改自己的代码,如果哪里需要改

进,就可以直接该代码,重新编译一个 .so 库出来,甚至这个库随便用在其他程序中都可以。

Android NDK 的确提供了一个非常好的平台,从做视频时的FFMPEG移植,到现在的ILBC库的移植,用的都是C代码,以后如果转战

游戏开发,说不定会有更广阔的天地.

整整花了三个小时来写这片文章,因为自己也是刚刚学会,一边写代码,一边又去看 ilbc源码,从头到尾顺了一遍,ilbc真正的代码就是几

千行,不多,但是精。要想掌握得花一定的时间。目前这系列教程只是完成了底层的开发,接下来将完善整个系统,文章中有写的不好的,希

望多多指教,互相学习!

Android ilbc 语音对话示范(三)程序流程

上一篇文章中详细讲述了 ilbc 在Android平台的移植和编译,现转到Java层,实现音频的采集和处理操作,本示范中的程序流程图如下:

顺便提一下:因为是在Ubuntu下写的博客,所以没有一个现成的工具来绘制流程图,后来网上找到一个在线绘图网站:

http://creately.com/感觉非常不错,绘制功能强大,推荐给大家,需要注册才能使用

android 网络语音电话合集 此文为备份_第4张图片

图解:

1.发送端有三个主要的类:AudioRecorder(负责音频采集),AudioEncoder(负责音频编码),AudioSender(负责将编码后的数

发送出去);这三个类中各有一个线程,录制开始后,这三个线程一起运行,分别执行各自的任务,AudioRecorder采集音频后,添加到

AudioEncoder 的音频数据的List中,而AudioEncoder 的编码线程不断从List头部取出数据,调用 ilbc的底层\函数进行编码,编码

后的数据则又添加到下一级的AudioSender的 List中,AudioSender又不断从头部取出数据,然后发送出去;

2.使用Android 系统自带的AudioRecord这个类来实现音频数据的采集,注意要在AndroidManifest.xml文件中加上权限

android.permission.RECORD_AUDIO ,使用AudioRecord时,一定要配置好一些音频参数,比如采样频率,采样格式等,具体将

续代码中详细写出;采集方法是 AudioRecord 中的 read(samples, 0, bufferSize);

3.AudioEncoder 对数据编码一次后,交付给AudioSender 让其发送到服务器,发送方采用UDP协议,采集一次数据长度为960B,

码后长为100B ,所以一个UDP包就非常小,既节省带宽,又减少丢包率;

4.接收端有三个主要的类:AudioReciever(负责接收UDP 包),AudioDecoder(负责解码音频),AudioPlayer(负责音频播放),

致流在上图中已经详细给出,这里不做说明了,只不过就是 发送方流程的逆序。播放音频使用的是Android中的AudioTrack 这个类,

使用write(byte[] data , int sampleOffset ,int sampleLength) 方法能够直接播放音频数据流

5.发送方有三个线程,接受方也有三个线程,一一对应,下一篇正式开始贴代码 ;

Android ilbc 语音对话示范(四)发送方代码

上一文章中提到:

发送端有三个主要的类:AudioRecorder(负责音频采集),AudioEncoder(负责音频编码),AudioSender(负责将编码后的数

发送出去);这三个类中各有一个线程,录制开始后,这三个线程一起运行,分别执行各自的任务,AudioRecorder采集音频后,添加到

AudioEncoder 的音频数据的List中,而AudioEncoder 的编码线程不断从List头部取出数据,调用 ilbc的底层\函数进行编码,编码

后的数据则又添加到下一级的AudioSender的 List中,AudioSender又不断从头部取出数据,然后发送出去;

1.先建立一个 AudioData的类,代表一段音频数据:

  1. publicclassAudioData{
  2. intsize;
  3. byte[]realData;
  4. //longtimestamp;
  5. publicintgetSize(){
  6. returnsize;
  7. }
  8. publicvoidsetSize(intsize){
  9. this.size=size;
  10. }
  11. publicbyte[]getRealData(){
  12. returnrealData;
  13. }
  14. publicvoidsetRealData(byte[]realData){
  15. this.realData=realData;
  16. }
  17. //publiclonggetTimestamp(){
  18. //returntimestamp;
  19. //}
  20. //
  21. //publicvoidsetTimestamp(longtimestamp){
  22. //this.timestamp=timestamp;
  23. //}
  24. }


2.AudioRecorder 类,使用Android系统自带的AudioRecord来采集音频,每采集一次,就交给编码器编码。

  1. publicclassAudioRecorderimplementsRunnable{
  2. StringLOG="Recorder";
  3. privatebooleanisRecording=false;
  4. privateAudioRecordaudioRecord;
  5. privatestaticfinalintaudioSource=MediaRecorder.AudioSource.MIC;
  6. privatestaticfinalintsampleRate=8000;
  7. privatestaticfinalintchannelConfig=AudioFormat.CHANNEL_IN_MONO;
  8. privatestaticfinalintaudioFormat=AudioFormat.ENCODING_PCM_16BIT;
  9. privatestaticfinalintBUFFER_FRAME_SIZE=960;
  10. privateintaudioBufSize=0;
  11. //
  12. privatebyte[]samples;//缓冲区
  13. privateintbufferRead=0;//从recorder中读取的samples的大小
  14. privateintbufferSize=0;//samples的大小
  15. //开始录制
  16. publicvoidstartRecording(){
  17. bufferSize=BUFFER_FRAME_SIZE;
  18. audioBufSize=AudioRecord.getMinBufferSize(sampleRate,channelConfig,
  19. audioFormat);
  20. if(audioBufSize==AudioRecord.ERROR_BAD_VALUE){
  21. Log.e(LOG,"audioBufSizeerror");
  22. return;
  23. }
  24. samples=newbyte[audioBufSize];
  25. //初始化recorder
  26. if(null==audioRecord){
  27. audioRecord=newAudioRecord(audioSource,sampleRate,
  28. channelConfig,audioFormat,audioBufSize);
  29. }
  30. newThread(this).start();
  31. }
  32. //停止录制
  33. publicvoidstopRecording(){
  34. this.isRecording=false;
  35. }
  36. publicbooleanisRecording(){
  37. returnisRecording;
  38. }
  39. //run
  40. publicvoidrun(){
  41. //录制前,先启动解码器
  42. AudioEncoderencoder=AudioEncoder.getInstance();
  43. encoder.startEncoding();
  44. System.out.println(LOG+"audioRecordstartRecording()");
  45. audioRecord.startRecording();
  46. this.isRecording=true;
  47. while(isRecording){
  48. bufferRead=audioRecord.read(samples,0,bufferSize);
  49. if(bufferRead>0){
  50. //将数据添加给解码器
  51. encoder.addData(samples,bufferRead);
  52. }
  53. try{
  54. Thread.sleep(20);
  55. }catch(InterruptedExceptione){
  56. e.printStackTrace();
  57. }
  58. }
  59. System.out.println(LOG+"录制结束");
  60. audioRecord.stop();
  61. encoder.stopEncoding();
  62. }
  63. }


3.AudioEncoder,负责调用NDK 方法实现音频的编码,每编码一次,就交给AudioSender 去发送:

  1. publicclassAudioEncoderimplementsRunnable{
  2. StringLOG="AudioEncoder";
  3. privatestaticAudioEncoderencoder;
  4. privatebooleanisEncoding=false;
  5. privateList<AudioData>dataList=null;//存放数据
  6. publicstaticAudioEncodergetInstance(){
  7. if(encoder==null){
  8. encoder=newAudioEncoder();
  9. }
  10. returnencoder;
  11. }
  12. privateAudioEncoder(){
  13. dataList=Collections.synchronizedList(newLinkedList<AudioData>());
  14. }
  15. publicvoidaddData(byte[]data,intsize){
  16. AudioDatarawData=newAudioData();
  17. rawData.setSize(size);
  18. byte[]tempData=newbyte[size];
  19. System.arraycopy(data,0,tempData,0,size);
  20. rawData.setRealData(tempData);
  21. dataList.add(rawData);
  22. }
  23. //开始编码
  24. publicvoidstartEncoding(){
  25. System.out.println(LOG+"解码线程启动");
  26. if(isEncoding){
  27. Log.e(LOG,"编码器已经启动,不能再次启动");
  28. return;
  29. }
  30. newThread(this).start();
  31. }
  32. //结束
  33. publicvoidstopEncoding(){
  34. this.isEncoding=false;
  35. }
  36. publicvoidrun(){
  37. //先启动发送端
  38. AudioSendersender=newAudioSender();
  39. sender.startSending();
  40. intencodeSize=0;
  41. byte[]encodedData=newbyte[256];
  42. //初始化编码器
  43. AudioCodec.audio_codec_init(30);
  44. isEncoding=true;
  45. while(isEncoding){
  46. if(dataList.size()==0){
  47. try{
  48. Thread.sleep(20);
  49. }catch(InterruptedExceptione){
  50. e.printStackTrace();
  51. }
  52. continue;
  53. }
  54. if(isEncoding){
  55. AudioDatarawData=dataList.remove(0);
  56. encodedData=newbyte[rawData.getSize()];
  57. //
  58. encodeSize=AudioCodec.audio_encode(rawData.getRealData(),0,
  59. rawData.getSize(),encodedData,0);
  60. System.out.println();
  61. if(encodeSize>0){
  62. sender.addData(encodedData,encodeSize);
  63. //清空数据
  64. encodedData=newbyte[encodedData.length];
  65. }
  66. }
  67. }
  68. System.out.println(LOG+"编码结束");
  69. sender.stopSending();
  70. }
  71. }


4.AudioSender类,负责音频数据的发送,使用UDP协议将编码后的AMR音频数据发送到服务器端,这个类功能简单:

  1. publicclassAudioSenderimplementsRunnable{
  2. StringLOG="AudioSender";
  3. privatebooleanisSendering=false;
  4. privateList<AudioData>dataList;
  5. DatagramSocketsocket;
  6. DatagramPacketdataPacket;
  7. privateInetAddressip;
  8. privateintport;
  9. publicAudioSender(){
  10. dataList=Collections.synchronizedList(newLinkedList<AudioData>());
  11. try{
  12. try{
  13. ip=InetAddress.getByName(MyConfig.SERVER_HOST);
  14. this.port=MyConfig.SERVER_PORT;
  15. socket=newDatagramSocket();
  16. }catch(UnknownHostExceptione){
  17. e.printStackTrace();
  18. }
  19. }catch(SocketExceptione){
  20. e.printStackTrace();
  21. }
  22. }
  23. //添加数据
  24. publicvoidaddData(byte[]data,intsize){
  25. AudioDataencodedData=newAudioData();
  26. encodedData.setSize(size);
  27. byte[]tempData=newbyte[size];
  28. System.arraycopy(data,0,tempData,0,size);
  29. encodedData.setRealData(tempData);
  30. dataList.add(encodedData);
  31. }
  32. //发送数据
  33. privatevoidsendData(byte[]data,intsize){
  34. try{
  35. dataPacket=newDatagramPacket(data,size,ip,port);
  36. dataPacket.setData(data);
  37. socket.send(dataPacket);
  38. }catch(IOExceptione){
  39. e.printStackTrace();
  40. }
  41. }
  42. //开始发送
  43. publicvoidstartSending(){
  44. System.out.println(LOG+"发送线程启动");
  45. newThread(this).start();
  46. }
  47. //停止发送
  48. publicvoidstopSending(){
  49. this.isSendering=false;
  50. }
  51. //run
  52. publicvoidrun(){
  53. this.isSendering=true;
  54. System.out.println(LOG+"开始发送数据");
  55. while(isSendering){
  56. if(dataList.size()>0){
  57. AudioDataencodedData=dataList.remove(0);
  58. sendData(encodedData.getRealData(),encodedData.getSize());
  59. }
  60. }
  61. System.out.println(LOG+"发送结束");
  62. }
  63. }


5.另外,上述类中有一个 MyConfig 类,主要放一些 配置参数:

  1. publicclassMyConfig{
  2. publicstaticStringSERVER_HOST="192.168.1.130";//服务器的IP
  3. publicstaticfinalintSERVER_PORT=5656;//服务器的监听端口
  4. publicstaticfinalintCLIENT_PORT=5757;//
  5. publicstaticfinalintAUDIO_STATUS_RECORDING=0;//手机端的状态:录音or播放
  6. publicstaticfinalintAUDIO_STATUS_LISTENING=1;
  7. publicstaticvoidsetServerHost(Stringip){
  8. System.out.println("修改后的服务器网址为"+ip);
  9. SERVER_HOST=ip;
  10. }
  11. }


上述代码实现了发送端的功能,现在代码结构如下:

android 网络语音电话合集 此文为备份_第5张图片

本实例中对音频没有添加时间戳处理,实际测试中没有太大的影响,可以听的清楚双方的语音对话,如果想要添加

时间戳的话,就在音频录制 AudioRecord的 read方法出得到时间戳,然后附加给UDP包。

接收端的原理已经在本系列文章的第三篇中讲述清楚,下一篇将贴出代码实现过程,有写的不好的地方,欢迎各位指点!

Android ilbc 语音对话示范(五)接收端处理

此系列文章拖了N久,有好多人发邮件来询问我第五次的文章为什么没有写,其实非常抱歉,本人学生一个,暑假一直

去公司实习,最近又忙着各种招聘找工作,没有时间好好写,现在抽空把最后一篇补上,水平有限,如过有不对的,请

各位指正~

前四篇文章分别介绍了 “代码结构”,“程序流程”,以及”发送方的处理”,现在就把接收方的处理流程做个介绍;

android 网络语音电话合集 此文为备份_第6张图片

如上图所示,接收方的操作有三个类:AudioDecoder(负责解码),AudioPlayer(负责播放解码后的音频),

AudioReceiver(负责从服务器接收音频数据包),这三个类的流程在第三篇中有详细的介绍。

1.AudioReceiver代码:

AudioReceiver使用UDP方式从服务端接收音频数据,其过程比较简单,直接上代码:

  1. packagexmu.swordbearer.audio.receiver;
  2. importjava.io.IOException;
  3. importjava.net.DatagramPacket;
  4. importjava.net.DatagramSocket;
  5. importjava.net.SocketException;
  6. importxmu.swordbearer.audio.MyConfig;
  7. importandroid.util.Log;
  8. publicclassAudioReceiverimplementsRunnable{
  9. StringLOG="NETReciever";
  10. intport=MyConfig.CLIENT_PORT;//接收的端口
  11. DatagramSocketsocket;
  12. DatagramPacketpacket;
  13. booleanisRunning=false;
  14. privatebyte[]packetBuf=newbyte[1024];
  15. privateintpacketSize=1024;
  16. /*
  17. *开始接收数据
  18. */
  19. publicvoidstartRecieving(){
  20. if(socket==null){
  21. try{
  22. socket=newDatagramSocket(port);
  23. packet=newDatagramPacket(packetBuf,packetSize);
  24. }catch(SocketExceptione){
  25. }
  26. }
  27. newThread(this).start();
  28. }
  29. /*
  30. *停止接收数据
  31. */
  32. publicvoidstopRecieving(){
  33. isRunning=false;
  34. }
  35. /*
  36. *释放资源
  37. */
  38. privatevoidrelease(){
  39. if(packet!=null){
  40. packet=null;
  41. }
  42. if(socket!=null){
  43. socket.close();
  44. socket=null;
  45. }
  46. }
  47. publicvoidrun(){
  48. //在接收前,要先启动解码器
  49. AudioDecoderdecoder=AudioDecoder.getInstance();
  50. decoder.startDecoding();
  51. isRunning=true;
  52. try{
  53. while(isRunning){
  54. socket.receive(packet);
  55. //每接收一个UDP包,就交给解码器,等待解码
  56. decoder.addData(packet.getData(),packet.getLength());
  57. }
  58. }catch(IOExceptione){
  59. Log.e(LOG,LOG+"RECIEVEERROR!");
  60. }
  61. //接收完成,停止解码器,释放资源
  62. decoder.stopDecoding();
  63. release();
  64. Log.e(LOG,LOG+"stoprecieving");
  65. }
  66. }

2.AudioDecoder代码:

解码的过程也很简单,由于接收端接收到了音频数据,然后就把数据交给解码器,所以解码的主要工作就是把接收端的数

据取出来进行解码,如果解码正确,就将解码后的数据再转交给AudioPlayer去播放,这三个类之间是依次传递的 :

AudioReceiver---->AudioDecoder--->AudioPlayer

下面代码中有个List变量private List<AudioData> dataList = null;这个就是用来存放数据的,每次解码时,dataList.remove(0),

从最前端取出数据进行解码:

  1. packagexmu.swordbearer.audio.receiver;
  2. importjava.util.Collections;
  3. importjava.util.LinkedList;
  4. importjava.util.List;
  5. importxmu.swordbearer.audio.AudioCodec;
  6. importxmu.swordbearer.audio.data.AudioData;
  7. importandroid.util.Log;
  8. publicclassAudioDecoderimplementsRunnable{
  9. StringLOG="CODECDecoder";
  10. privatestaticAudioDecoderdecoder;
  11. privatestaticfinalintMAX_BUFFER_SIZE=2048;
  12. privatebyte[]decodedData=newbyte[1024];//dataofdecoded
  13. privatebooleanisDecoding=false;
  14. privateList<AudioData>dataList=null;
  15. publicstaticAudioDecodergetInstance(){
  16. if(decoder==null){
  17. decoder=newAudioDecoder();
  18. }
  19. returndecoder;
  20. }
  21. privateAudioDecoder(){
  22. this.dataList=Collections
  23. .synchronizedList(newLinkedList<AudioData>());
  24. }
  25. /*
  26. *addDatatobedecoded
  27. *
  28. *@data:thedatarecievedfromserver
  29. *
  30. *@size:datasize
  31. */
  32. publicvoidaddData(byte[]data,intsize){
  33. AudioDataadata=newAudioData();
  34. adata.setSize(size);
  35. byte[]tempData=newbyte[size];
  36. System.arraycopy(data,0,tempData,0,size);
  37. adata.setRealData(tempData);
  38. dataList.add(adata);
  39. System.out.println(LOG+"adddataonce");
  40. }
  41. /*
  42. *startdecodeAMRdata
  43. */
  44. publicvoidstartDecoding(){
  45. System.out.println(LOG+"startdecoder");
  46. if(isDecoding){
  47. return;
  48. }
  49. newThread(this).start();
  50. }
  51. publicvoidrun(){
  52. //startplayerfirst
  53. AudioPlayerplayer=AudioPlayer.getInstance();
  54. player.startPlaying();
  55. //
  56. this.isDecoding=true;
  57. //initILBCparameter:30,20,15
  58. AudioCodec.audio_codec_init(30);
  59. Log.d(LOG,LOG+"initializeddecoder");
  60. intdecodeSize=0;
  61. while(isDecoding){
  62. while(dataList.size()>0){
  63. AudioDataencodedData=dataList.remove(0);
  64. decodedData=newbyte[MAX_BUFFER_SIZE];
  65. byte[]data=encodedData.getRealData();
  66. //
  67. decodeSize=AudioCodec.audio_decode(data,0,
  68. encodedData.getSize(),decodedData,0);
  69. if(decodeSize>0){
  70. //adddecodedaudiotoplayer
  71. player.addData(decodedData,decodeSize);
  72. //cleardata
  73. decodedData=newbyte[decodedData.length];
  74. }
  75. }
  76. }
  77. System.out.println(LOG+"stopdecoder");
  78. //stopplaybackaudio
  79. player.stopPlaying();
  80. }
  81. publicvoidstopDecoding(){
  82. this.isDecoding=false;
  83. }
  84. }


3.AudioPlayer代码:

播放器的工作流程其实和解码器一模一样,都是启动一个线程,然后不断从自己的 dataList中提取数据。

不过要注意,播放器的一些参数配置非常的关键;

播放声音时,使用了Android自带的 AudioTrack 这个类,它有这个方法:

public int write(byte[] audioData,int offsetInBytes, int sizeInBytes)可以直接播放;

所有播放器的代码如下:

  1. packagexmu.swordbearer.audio.receiver;
  2. importjava.util.Collections;
  3. importjava.util.LinkedList;
  4. importjava.util.List;
  5. importxmu.swordbearer.audio.data.AudioData;
  6. importandroid.media.AudioFormat;
  7. importandroid.media.AudioManager;
  8. importandroid.media.AudioRecord;
  9. importandroid.media.AudioTrack;
  10. importandroid.util.Log;
  11. publicclassAudioPlayerimplementsRunnable{
  12. StringLOG="AudioPlayer";
  13. privatestaticAudioPlayerplayer;
  14. privateList<AudioData>dataList=null;
  15. privateAudioDataplayData;
  16. privatebooleanisPlaying=false;
  17. privateAudioTrackaudioTrack;
  18. privatestaticfinalintsampleRate=8000;
  19. //注意:参数配置
  20. privatestaticfinalintchannelConfig=AudioFormat.CHANNEL_IN_MONO;
  21. privatestaticfinalintaudioFormat=AudioFormat.ENCODING_PCM_16BIT;
  22. privateAudioPlayer(){
  23. dataList=Collections.synchronizedList(newLinkedList<AudioData>());
  24. }
  25. publicstaticAudioPlayergetInstance(){
  26. if(player==null){
  27. player=newAudioPlayer();
  28. }
  29. returnplayer;
  30. }
  31. publicvoidaddData(byte[]rawData,intsize){
  32. AudioDatadecodedData=newAudioData();
  33. decodedData.setSize(size);
  34. byte[]tempData=newbyte[size];
  35. System.arraycopy(rawData,0,tempData,0,size);
  36. decodedData.setRealData(tempData);
  37. dataList.add(decodedData);
  38. }
  39. /*
  40. *initPlayerparameters
  41. */
  42. privatebooleaninitAudioTrack(){
  43. intbufferSize=AudioRecord.getMinBufferSize(sampleRate,
  44. channelConfig,audioFormat);
  45. if(bufferSize<0){
  46. Log.e(LOG,LOG+"initializeerror!");
  47. returnfalse;
  48. }
  49. audioTrack=newAudioTrack(AudioManager.STREAM_MUSIC,sampleRate,
  50. channelConfig,audioFormat,bufferSize,AudioTrack.MODE_STREAM);
  51. //setvolume:设置播放音量
  52. audioTrack.setStereoVolume(1.0f,1.0f);
  53. audioTrack.play();
  54. returntrue;
  55. }
  56. privatevoidplayFromList(){
  57. while(dataList.size()>0&&isPlaying){
  58. playData=dataList.remove(0);
  59. audioTrack.write(playData.getRealData(),0,playData.getSize());
  60. }
  61. }
  62. publicvoidstartPlaying(){
  63. if(isPlaying){
  64. return;
  65. }
  66. newThread(this).start();
  67. }
  68. publicvoidrun(){
  69. this.isPlaying=true;
  70. if(!initAudioTrack()){
  71. Log.e(LOG,LOG+"initializedplayererror!");
  72. return;
  73. }
  74. while(isPlaying){
  75. if(dataList.size()>0){
  76. playFromList();
  77. }else{
  78. try{
  79. Thread.sleep(20);
  80. }catch(InterruptedExceptione){
  81. }
  82. }
  83. }
  84. if(this.audioTrack!=null){
  85. if(this.audioTrack.getPlayState()==AudioTrack.PLAYSTATE_PLAYING){
  86. this.audioTrack.stop();
  87. this.audioTrack.release();
  88. }
  89. }
  90. Log.d(LOG,LOG+"endplaying");
  91. }
  92. publicvoidstopPlaying(){
  93. this.isPlaying=false;
  94. }
  95. }

4.简易服务端:

为了方便测试,我自己用Java 写了一个UDP的服务器,其功能非常的弱,就是接收,然后转发给另一方:

  1. importjava.io.IOException;
  2. importjava.net.DatagramPacket;
  3. importjava.net.DatagramSocket;
  4. importjava.net.InetAddress;
  5. importjava.net.SocketException;
  6. importjava.net.UnknownHostException;
  7. publicclassAudioServerimplementsRunnable{
  8. DatagramSocketsocket;
  9. DatagramPacketpacket;//从客户端接收到的UDP包
  10. DatagramPacketsendPkt;//转发给另一个客户端的UDP包
  11. byte[]pktBuffer=newbyte[1024];
  12. intbufferSize=1024;
  13. booleanisRunning=false;
  14. intmyport=5656;
  15. /////////////
  16. StringclientIpStr="192.168.1.104";
  17. InetAddressclientIp;
  18. intclientPort=5757;
  19. publicAudioServer(){
  20. try{
  21. clientIp=InetAddress.getByName(clientIpStr);
  22. }catch(UnknownHostExceptione1){
  23. e1.printStackTrace();
  24. }
  25. try{
  26. socket=newDatagramSocket(myport);
  27. packet=newDatagramPacket(pktBuffer,bufferSize);
  28. }catch(SocketExceptione){
  29. e.printStackTrace();
  30. }
  31. System.out.println("服务器初始化完成");
  32. }
  33. publicvoidstartServer(){
  34. this.isRunning=true;
  35. newThread(this).start();
  36. }
  37. publicvoidrun(){
  38. try{
  39. while(isRunning){
  40. socket.receive(packet);
  41. sendPkt=newDatagramPacket(packet.getData(),
  42. packet.getLength(),packet.getAddress(),clientPort);
  43. socket.send(sendPkt);
  44. try{
  45. Thread.sleep(20);
  46. }catch(InterruptedExceptione){
  47. e.printStackTrace();
  48. }
  49. }
  50. }catch(IOExceptione){
  51. }
  52. }
  53. //main
  54. publicstaticvoidmain(String[]args){
  55. newAudioServer().startServer();
  56. }
  57. }

5.结语:

Android使用 ILBC 进行语音通话的大致过程就讲述完了,此系列只是做一个ILBC 使用原理的介绍,距离真正的语音

通话还有很多工作要做,缺点还是很多的:

1. 文章中介绍的只是单方通话,如果要做成双方互相通话或者一对多的通话,就需要增加更多的流程处理,其服务端

也要做很多工作;

2. 实时性:本程序在局域网中使用时,实时性还是较高的,但是再广域网中,效果可能会有所下降,除此之外,本

程序还缺少时间戳的处理,如果网络状况不理想,或者数据延迟,就会导致语音播放前后混乱;

3. 服务器很弱:真正的流媒体服务器,需要很强的功能,来对数据进行处理,我是为了方便,就写了一个简单的,

最近打算移植live555,用来做专门的流媒体服务器,用RTP协议对数据进行封装,这样效果应该会好很多。

更多相关文章

  1. Android 继承SQLiteOpenHelper自定义DBHelper存取数据与图像
  2. android 网络异步加载数据进度条
  3. Android--遍历SQLite数据库下的所有表名
  4. android带图片的AlertDialog和文件管理器(代码)
  5. android 用代码画虚线边框背景
  6. 在eclipse中查看Android SDK源代码
  7. Android 对话框【Dialog】去除白色边框代码
  8. Android 音频播放
  9. Android 版本分布数据:Android 2.3 估超过 50%

随机推荐

  1. Google:所有含 Android(安卓)Market 的 An
  2. android 使用handler更新ui,使用与原理分
  3. Android(安卓)之 Window、WindowManager
  4. Android中AsyncTask的简单用法
  5. Android隐藏状态栏 全屏
  6. [Android官方API阅读]___
  7. 看网易和腾讯如何用一套 H5 通杀Android(
  8. Android(安卓)开发者从0到1发布一个微信
  9. Android和iPhone应用程序界面布局示例
  10. Android(安卓)studio Gradle home can no