android 网络语音电话合集 此文为备份
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)。
现在开始讲解代码结构搭建环节:
- 要求:
- 环境:Ubuntu12.04(其他Linux环境皆可),Android2.2及以上系统
- 工具:Elicpse3.7,AndroidNDKr7,AndroidSDK
1.新建工程:
打开Eclipse,新建一个Android程序,名称为 AndroidILBC。
2.添加底层代码:
将下载的源码中的 jni 文件夹复制到新建的工程的根目录下,此时,代码结构如下:
3.Android.mk编写:
- LOCAL_PATH:=$(callmy-dir)
- include$(CLEAR_VARS)
- LOCAL_MODULE:=libilbc
- codec_dir:=iLBC_RFC3951#ilbc源代码的目录
- LOCAL_SRC_FILES:=\
- $(codec_dir)/anaFilter.c\
- $(codec_dir)/constants.c\
- $(codec_dir)/createCB.c\
- $(codec_dir)/doCPLC.c\
- $(codec_dir)/enhancer.c\
- $(codec_dir)/filter.c\
- $(codec_dir)/FrameClassify.c\
- $(codec_dir)/gainquant.c\
- $(codec_dir)/getCBvec.c\
- $(codec_dir)/helpfun.c\
- $(codec_dir)/hpInput.c\
- $(codec_dir)/hpOutput.c\
- $(codec_dir)/iCBConstruct.c\
- $(codec_dir)/iCBSearch.c\
- $(codec_dir)/iLBC_decode.c\
- $(codec_dir)/iLBC_encode.c\
- $(codec_dir)/LPCdecode.c\
- $(codec_dir)/LPCencode.c\
- $(codec_dir)/lsf.c\
- $(codec_dir)/packing.c\
- $(codec_dir)/StateConstructW.c\
- $(codec_dir)/StateSearchW.c\
- $(codec_dir)/syntFilter.c
- LOCAL_C_INCLUDES+=$(common_C_INCLUDES)
- LOCAL_PRELINK_MODULE:=false
- include$(BUILD_STATIC_LIBRARY)
- #BuildJNIwrapper
- include$(CLEAR_VARS)
- LOCAL_MODULE:=libilbc-codec#生成的.so库名,可以自行修改
- LOCAL_C_INCLUDES+=\
- $(JNI_H_INCLUDE)\
- $(codec_dir)
- LOCAL_SRC_FILES:=ilbc-codec.c
- LOCAL_LDLIBS:=-L$(SYSROOT)/usr/lib-llog
- LOCAL_STATIC_LIBRARIES:=libilbc
- LOCAL_PRELINK_MODULE:=false
- include$(BUILD_SHARED_LIBRARY)
4.代码分析:
打开jni 文件夹下的ilbc-codec.c文件,里面总共只有五个函数,负责音频编解码器的初始化,以及音频的编码和解码。其中的三个方法:
- jintJava_com_googlecode_androidilbc_Codec_init(
- JNIEnv*env,jobjectthis,jintmode)
- 和
- jintJava_com_googlecode_androidilbc_Codec_encode(
- JNIEnv*env,jobjectthis,
- jbyteArraysampleArray,jintsampleOffset,jintsampleLength,
- jbyteArraydataArray,jintdataOffset)
- 和
- jintJava_com_googlecode_androidilbc_Codec_decode(
- JNIEnv*env,jobjectthis,
- jbyteArraydataArray,jintdataOffset,jintdataLength,
- jbyteArraysampleArray,jintsampleOffset)
根据这三个函数的名称就可以知道,使用来让 Java层代码调用的三个函数,现在我们对这三个函数进行改造(仅仅是换个函数名称而已)
5.写Java层native 方法:
在程序的Java层代码中,建立一个包,用于放 NDK 的java层代码,比如我建一个名为xmu.swordbearer.audio的包,里面新建一个类:
AudioCodec.java,在这个类中只负责对底层C函数进行调用,相当于一个工具类。新建三个public staic native int方法:
- packagexmu.swordbearer.audio;
- publicclassAudioCodec{
- //initializedecoderandencoder
- publicstaticnativeintaudio_codec_init(intmode);
- //encode
- publicstaticnativeintaudio_encode(byte[]sample,intsampleOffset,
- intsampleLength,byte[]data,intdataOffset);
- //decode
- publicstaticnativeintaudio_decode(byte[]data,intdataOffset,
- intdataLength,byte[]sample,intsampleLength);
- }
三个方法分别用于初始化,音频编码,音频解码,在这里只需声明为native方法,不用写任何代码;
6.编译.h 头文件:
(如果不会,请参考之前的文章)
打开终端,定位到第 5步建立的AudioCodec.java目录下,如下:
这一步很关键,进入到src目录后,就要带上AudioCodec这个类的包名,此例中的包名为:xmu.swordbearer.audio如果上述步
骤正确,就会在该包下生成一个xmu_swordbearer_audio_AudioCodec.h的头文件,内容如下:
- /*
- *Class:xmu_swordbearer_audio_AudioCodec
- *Method:audio_codec_init
- *Signature:(I)I
- */
- JNIEXPORTjintJNICALLJava_xmu_swordbearer_audio_AudioCodec_audio_1codec_1init
- (JNIEnv*,jclass,jint);
- /*
- *Class:xmu_swordbearer_audio_AudioCodec
- *Method:audio_encode
- *Signature:([BII[BI)I
- */
- JNIEXPORTjintJNICALLJava_xmu_swordbearer_audio_AudioCodec_audio_1encode
- (JNIEnv*,jclass,jbyteArray,jint,jint,jbyteArray,jint);
- /*
- *Class:xmu_swordbearer_audio_AudioCodec
- *Method:audio_decode
- *Signature:([BII[BI)I
- */
- JNIEXPORTjintJNICALLJava_xmu_swordbearer_audio_AudioCodec_audio_1decode
- (JNIEnv*,jclass,jbyteArray,jint,jint,jbyteArray,jint);
第4步中分析的三个方法修改,打开jni 下的ilbc-codec.c文件,,把那三个名称分别用刚刚生成的这三个方法名替换,具体对应如下:
- Java_com_googlecode_androidilbc_Codec_init
- 改为:
- Java_xmu_swordbearer_audio_AudioCodec_audio_1codec_1init
- Java_com_googlecode_androidilbc_Codec_encode
- 改为:
- Java_xmu_swordbearer_audio_AudioCodec_audio_1encode
- Java_com_googlecode_androidilbc_Codec_decode
- 改为:
- Java_xmu_swordbearer_audio_AudioCodec_audio_1decode
仅是一个 复制,粘贴的过程!!!
当然,如果你写的JAVA代码的包名或者方法名不一样,那生成的 .h 文件中的方法也就不一样,这就是为什么编译好一个.so库后,不能随
便修改 native方法所在类的包名,因为方法名会也就改变了.
7. 编译 .so 库
下来就是要编译生成.so库了,正如上面Android.mk文件中写的,最终编译生成的库是libilbc-codec.so,编译方法如下:
打开终端,定位到jni文件夹下面,输入ndk-build,回车,会看到如下情景:
看到倒数第二行了吗?libs/armeabi/libilbc-codec.so,说明已经生成了我们需要的动态库,这时你会发现在工程的根目录下多了一个libs的
文件夹,里面有个armeabi目录,打开后就有一个libilbc-codec.so的文件:
得到这个库之后,我们所有与底层有关的工作全部完成,被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/感觉非常不错,绘制功能强大,推荐给大家,需要注册才能使用。
图解:
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的类,代表一段音频数据:
- publicclassAudioData{
- intsize;
- byte[]realData;
- //longtimestamp;
- publicintgetSize(){
- returnsize;
- }
- publicvoidsetSize(intsize){
- this.size=size;
- }
- publicbyte[]getRealData(){
- returnrealData;
- }
- publicvoidsetRealData(byte[]realData){
- this.realData=realData;
- }
- //publiclonggetTimestamp(){
- //returntimestamp;
- //}
- //
- //publicvoidsetTimestamp(longtimestamp){
- //this.timestamp=timestamp;
- //}
- }
2.AudioRecorder 类,使用Android系统自带的AudioRecord来采集音频,每采集一次,就交给编码器编码。
- publicclassAudioRecorderimplementsRunnable{
- StringLOG="Recorder";
- privatebooleanisRecording=false;
- privateAudioRecordaudioRecord;
- privatestaticfinalintaudioSource=MediaRecorder.AudioSource.MIC;
- privatestaticfinalintsampleRate=8000;
- privatestaticfinalintchannelConfig=AudioFormat.CHANNEL_IN_MONO;
- privatestaticfinalintaudioFormat=AudioFormat.ENCODING_PCM_16BIT;
- privatestaticfinalintBUFFER_FRAME_SIZE=960;
- privateintaudioBufSize=0;
- //
- privatebyte[]samples;//缓冲区
- privateintbufferRead=0;//从recorder中读取的samples的大小
- privateintbufferSize=0;//samples的大小
- //开始录制
- publicvoidstartRecording(){
- bufferSize=BUFFER_FRAME_SIZE;
- audioBufSize=AudioRecord.getMinBufferSize(sampleRate,channelConfig,
- audioFormat);
- if(audioBufSize==AudioRecord.ERROR_BAD_VALUE){
- Log.e(LOG,"audioBufSizeerror");
- return;
- }
- samples=newbyte[audioBufSize];
- //初始化recorder
- if(null==audioRecord){
- audioRecord=newAudioRecord(audioSource,sampleRate,
- channelConfig,audioFormat,audioBufSize);
- }
- newThread(this).start();
- }
- //停止录制
- publicvoidstopRecording(){
- this.isRecording=false;
- }
- publicbooleanisRecording(){
- returnisRecording;
- }
- //run
- publicvoidrun(){
- //录制前,先启动解码器
- AudioEncoderencoder=AudioEncoder.getInstance();
- encoder.startEncoding();
- System.out.println(LOG+"audioRecordstartRecording()");
- audioRecord.startRecording();
- this.isRecording=true;
- while(isRecording){
- bufferRead=audioRecord.read(samples,0,bufferSize);
- if(bufferRead>0){
- //将数据添加给解码器
- encoder.addData(samples,bufferRead);
- }
- try{
- Thread.sleep(20);
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }
- }
- System.out.println(LOG+"录制结束");
- audioRecord.stop();
- encoder.stopEncoding();
- }
- }
3.AudioEncoder,负责调用NDK 方法实现音频的编码,每编码一次,就交给AudioSender 去发送:
- publicclassAudioEncoderimplementsRunnable{
- StringLOG="AudioEncoder";
- privatestaticAudioEncoderencoder;
- privatebooleanisEncoding=false;
- privateList<AudioData>dataList=null;//存放数据
- publicstaticAudioEncodergetInstance(){
- if(encoder==null){
- encoder=newAudioEncoder();
- }
- returnencoder;
- }
- privateAudioEncoder(){
- dataList=Collections.synchronizedList(newLinkedList<AudioData>());
- }
- publicvoidaddData(byte[]data,intsize){
- AudioDatarawData=newAudioData();
- rawData.setSize(size);
- byte[]tempData=newbyte[size];
- System.arraycopy(data,0,tempData,0,size);
- rawData.setRealData(tempData);
- dataList.add(rawData);
- }
- //开始编码
- publicvoidstartEncoding(){
- System.out.println(LOG+"解码线程启动");
- if(isEncoding){
- Log.e(LOG,"编码器已经启动,不能再次启动");
- return;
- }
- newThread(this).start();
- }
- //结束
- publicvoidstopEncoding(){
- this.isEncoding=false;
- }
- publicvoidrun(){
- //先启动发送端
- AudioSendersender=newAudioSender();
- sender.startSending();
- intencodeSize=0;
- byte[]encodedData=newbyte[256];
- //初始化编码器
- AudioCodec.audio_codec_init(30);
- isEncoding=true;
- while(isEncoding){
- if(dataList.size()==0){
- try{
- Thread.sleep(20);
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }
- continue;
- }
- if(isEncoding){
- AudioDatarawData=dataList.remove(0);
- encodedData=newbyte[rawData.getSize()];
- //
- encodeSize=AudioCodec.audio_encode(rawData.getRealData(),0,
- rawData.getSize(),encodedData,0);
- System.out.println();
- if(encodeSize>0){
- sender.addData(encodedData,encodeSize);
- //清空数据
- encodedData=newbyte[encodedData.length];
- }
- }
- }
- System.out.println(LOG+"编码结束");
- sender.stopSending();
- }
- }
4.AudioSender类,负责音频数据的发送,使用UDP协议将编码后的AMR音频数据发送到服务器端,这个类功能简单:
- publicclassAudioSenderimplementsRunnable{
- StringLOG="AudioSender";
- privatebooleanisSendering=false;
- privateList<AudioData>dataList;
- DatagramSocketsocket;
- DatagramPacketdataPacket;
- privateInetAddressip;
- privateintport;
- publicAudioSender(){
- dataList=Collections.synchronizedList(newLinkedList<AudioData>());
- try{
- try{
- ip=InetAddress.getByName(MyConfig.SERVER_HOST);
- this.port=MyConfig.SERVER_PORT;
- socket=newDatagramSocket();
- }catch(UnknownHostExceptione){
- e.printStackTrace();
- }
- }catch(SocketExceptione){
- e.printStackTrace();
- }
- }
- //添加数据
- publicvoidaddData(byte[]data,intsize){
- AudioDataencodedData=newAudioData();
- encodedData.setSize(size);
- byte[]tempData=newbyte[size];
- System.arraycopy(data,0,tempData,0,size);
- encodedData.setRealData(tempData);
- dataList.add(encodedData);
- }
- //发送数据
- privatevoidsendData(byte[]data,intsize){
- try{
- dataPacket=newDatagramPacket(data,size,ip,port);
- dataPacket.setData(data);
- socket.send(dataPacket);
- }catch(IOExceptione){
- e.printStackTrace();
- }
- }
- //开始发送
- publicvoidstartSending(){
- System.out.println(LOG+"发送线程启动");
- newThread(this).start();
- }
- //停止发送
- publicvoidstopSending(){
- this.isSendering=false;
- }
- //run
- publicvoidrun(){
- this.isSendering=true;
- System.out.println(LOG+"开始发送数据");
- while(isSendering){
- if(dataList.size()>0){
- AudioDataencodedData=dataList.remove(0);
- sendData(encodedData.getRealData(),encodedData.getSize());
- }
- }
- System.out.println(LOG+"发送结束");
- }
- }
5.另外,上述类中有一个 MyConfig 类,主要放一些 配置参数:
- publicclassMyConfig{
- publicstaticStringSERVER_HOST="192.168.1.130";//服务器的IP
- publicstaticfinalintSERVER_PORT=5656;//服务器的监听端口
- publicstaticfinalintCLIENT_PORT=5757;//
- publicstaticfinalintAUDIO_STATUS_RECORDING=0;//手机端的状态:录音or播放
- publicstaticfinalintAUDIO_STATUS_LISTENING=1;
- publicstaticvoidsetServerHost(Stringip){
- System.out.println("修改后的服务器网址为"+ip);
- SERVER_HOST=ip;
- }
- }
上述代码实现了发送端的功能,现在代码结构如下:
本实例中对音频没有添加时间戳处理,实际测试中没有太大的影响,可以听的清楚双方的语音对话,如果想要添加
时间戳的话,就在音频录制 AudioRecord的 read方法出得到时间戳,然后附加给UDP包。
接收端的原理已经在本系列文章的第三篇中讲述清楚,下一篇将贴出代码实现过程,有写的不好的地方,欢迎各位指点!
Android ilbc 语音对话示范(五)接收端处理
此系列文章拖了N久,有好多人发邮件来询问我第五次的文章为什么没有写,其实非常抱歉,本人学生一个,暑假一直
去公司实习,最近又忙着各种招聘找工作,没有时间好好写,现在抽空把最后一篇补上,水平有限,如过有不对的,请
各位指正~
前四篇文章分别介绍了 “代码结构”,“程序流程”,以及”发送方的处理”,现在就把接收方的处理流程做个介绍;
如上图所示,接收方的操作有三个类:AudioDecoder(负责解码),AudioPlayer(负责播放解码后的音频),
AudioReceiver(负责从服务器接收音频数据包),这三个类的流程在第三篇中有详细的介绍。
1.AudioReceiver代码:
AudioReceiver使用UDP方式从服务端接收音频数据,其过程比较简单,直接上代码:
- packagexmu.swordbearer.audio.receiver;
- importjava.io.IOException;
- importjava.net.DatagramPacket;
- importjava.net.DatagramSocket;
- importjava.net.SocketException;
- importxmu.swordbearer.audio.MyConfig;
- importandroid.util.Log;
- publicclassAudioReceiverimplementsRunnable{
- StringLOG="NETReciever";
- intport=MyConfig.CLIENT_PORT;//接收的端口
- DatagramSocketsocket;
- DatagramPacketpacket;
- booleanisRunning=false;
- privatebyte[]packetBuf=newbyte[1024];
- privateintpacketSize=1024;
- /*
- *开始接收数据
- */
- publicvoidstartRecieving(){
- if(socket==null){
- try{
- socket=newDatagramSocket(port);
- packet=newDatagramPacket(packetBuf,packetSize);
- }catch(SocketExceptione){
- }
- }
- newThread(this).start();
- }
- /*
- *停止接收数据
- */
- publicvoidstopRecieving(){
- isRunning=false;
- }
- /*
- *释放资源
- */
- privatevoidrelease(){
- if(packet!=null){
- packet=null;
- }
- if(socket!=null){
- socket.close();
- socket=null;
- }
- }
- publicvoidrun(){
- //在接收前,要先启动解码器
- AudioDecoderdecoder=AudioDecoder.getInstance();
- decoder.startDecoding();
- isRunning=true;
- try{
- while(isRunning){
- socket.receive(packet);
- //每接收一个UDP包,就交给解码器,等待解码
- decoder.addData(packet.getData(),packet.getLength());
- }
- }catch(IOExceptione){
- Log.e(LOG,LOG+"RECIEVEERROR!");
- }
- //接收完成,停止解码器,释放资源
- decoder.stopDecoding();
- release();
- Log.e(LOG,LOG+"stoprecieving");
- }
- }
2.AudioDecoder代码:
解码的过程也很简单,由于接收端接收到了音频数据,然后就把数据交给解码器,所以解码的主要工作就是把接收端的数
据取出来进行解码,如果解码正确,就将解码后的数据再转交给AudioPlayer去播放,这三个类之间是依次传递的 :
AudioReceiver---->AudioDecoder--->AudioPlayer
下面代码中有个List变量private List<AudioData> dataList = null;这个就是用来存放数据的,每次解码时,dataList.remove(0),
从最前端取出数据进行解码:
- packagexmu.swordbearer.audio.receiver;
- importjava.util.Collections;
- importjava.util.LinkedList;
- importjava.util.List;
- importxmu.swordbearer.audio.AudioCodec;
- importxmu.swordbearer.audio.data.AudioData;
- importandroid.util.Log;
- publicclassAudioDecoderimplementsRunnable{
- StringLOG="CODECDecoder";
- privatestaticAudioDecoderdecoder;
- privatestaticfinalintMAX_BUFFER_SIZE=2048;
- privatebyte[]decodedData=newbyte[1024];//dataofdecoded
- privatebooleanisDecoding=false;
- privateList<AudioData>dataList=null;
- publicstaticAudioDecodergetInstance(){
- if(decoder==null){
- decoder=newAudioDecoder();
- }
- returndecoder;
- }
- privateAudioDecoder(){
- this.dataList=Collections
- .synchronizedList(newLinkedList<AudioData>());
- }
- /*
- *addDatatobedecoded
- *
- *@data:thedatarecievedfromserver
- *
- *@size:datasize
- */
- publicvoidaddData(byte[]data,intsize){
- AudioDataadata=newAudioData();
- adata.setSize(size);
- byte[]tempData=newbyte[size];
- System.arraycopy(data,0,tempData,0,size);
- adata.setRealData(tempData);
- dataList.add(adata);
- System.out.println(LOG+"adddataonce");
- }
- /*
- *startdecodeAMRdata
- */
- publicvoidstartDecoding(){
- System.out.println(LOG+"startdecoder");
- if(isDecoding){
- return;
- }
- newThread(this).start();
- }
- publicvoidrun(){
- //startplayerfirst
- AudioPlayerplayer=AudioPlayer.getInstance();
- player.startPlaying();
- //
- this.isDecoding=true;
- //initILBCparameter:30,20,15
- AudioCodec.audio_codec_init(30);
- Log.d(LOG,LOG+"initializeddecoder");
- intdecodeSize=0;
- while(isDecoding){
- while(dataList.size()>0){
- AudioDataencodedData=dataList.remove(0);
- decodedData=newbyte[MAX_BUFFER_SIZE];
- byte[]data=encodedData.getRealData();
- //
- decodeSize=AudioCodec.audio_decode(data,0,
- encodedData.getSize(),decodedData,0);
- if(decodeSize>0){
- //adddecodedaudiotoplayer
- player.addData(decodedData,decodeSize);
- //cleardata
- decodedData=newbyte[decodedData.length];
- }
- }
- }
- System.out.println(LOG+"stopdecoder");
- //stopplaybackaudio
- player.stopPlaying();
- }
- publicvoidstopDecoding(){
- this.isDecoding=false;
- }
- }
3.AudioPlayer代码:
播放器的工作流程其实和解码器一模一样,都是启动一个线程,然后不断从自己的 dataList中提取数据。
不过要注意,播放器的一些参数配置非常的关键;
播放声音时,使用了Android自带的 AudioTrack 这个类,它有这个方法:
public int write(byte[] audioData,int offsetInBytes, int sizeInBytes)可以直接播放;
所有播放器的代码如下:
- packagexmu.swordbearer.audio.receiver;
- importjava.util.Collections;
- importjava.util.LinkedList;
- importjava.util.List;
- importxmu.swordbearer.audio.data.AudioData;
- importandroid.media.AudioFormat;
- importandroid.media.AudioManager;
- importandroid.media.AudioRecord;
- importandroid.media.AudioTrack;
- importandroid.util.Log;
- publicclassAudioPlayerimplementsRunnable{
- StringLOG="AudioPlayer";
- privatestaticAudioPlayerplayer;
- privateList<AudioData>dataList=null;
- privateAudioDataplayData;
- privatebooleanisPlaying=false;
- privateAudioTrackaudioTrack;
- privatestaticfinalintsampleRate=8000;
- //注意:参数配置
- privatestaticfinalintchannelConfig=AudioFormat.CHANNEL_IN_MONO;
- privatestaticfinalintaudioFormat=AudioFormat.ENCODING_PCM_16BIT;
- privateAudioPlayer(){
- dataList=Collections.synchronizedList(newLinkedList<AudioData>());
- }
- publicstaticAudioPlayergetInstance(){
- if(player==null){
- player=newAudioPlayer();
- }
- returnplayer;
- }
- publicvoidaddData(byte[]rawData,intsize){
- AudioDatadecodedData=newAudioData();
- decodedData.setSize(size);
- byte[]tempData=newbyte[size];
- System.arraycopy(rawData,0,tempData,0,size);
- decodedData.setRealData(tempData);
- dataList.add(decodedData);
- }
- /*
- *initPlayerparameters
- */
- privatebooleaninitAudioTrack(){
- intbufferSize=AudioRecord.getMinBufferSize(sampleRate,
- channelConfig,audioFormat);
- if(bufferSize<0){
- Log.e(LOG,LOG+"initializeerror!");
- returnfalse;
- }
- audioTrack=newAudioTrack(AudioManager.STREAM_MUSIC,sampleRate,
- channelConfig,audioFormat,bufferSize,AudioTrack.MODE_STREAM);
- //setvolume:设置播放音量
- audioTrack.setStereoVolume(1.0f,1.0f);
- audioTrack.play();
- returntrue;
- }
- privatevoidplayFromList(){
- while(dataList.size()>0&&isPlaying){
- playData=dataList.remove(0);
- audioTrack.write(playData.getRealData(),0,playData.getSize());
- }
- }
- publicvoidstartPlaying(){
- if(isPlaying){
- return;
- }
- newThread(this).start();
- }
- publicvoidrun(){
- this.isPlaying=true;
- if(!initAudioTrack()){
- Log.e(LOG,LOG+"initializedplayererror!");
- return;
- }
- while(isPlaying){
- if(dataList.size()>0){
- playFromList();
- }else{
- try{
- Thread.sleep(20);
- }catch(InterruptedExceptione){
- }
- }
- }
- if(this.audioTrack!=null){
- if(this.audioTrack.getPlayState()==AudioTrack.PLAYSTATE_PLAYING){
- this.audioTrack.stop();
- this.audioTrack.release();
- }
- }
- Log.d(LOG,LOG+"endplaying");
- }
- publicvoidstopPlaying(){
- this.isPlaying=false;
- }
- }
4.简易服务端:
为了方便测试,我自己用Java 写了一个UDP的服务器,其功能非常的弱,就是接收,然后转发给另一方:
- importjava.io.IOException;
- importjava.net.DatagramPacket;
- importjava.net.DatagramSocket;
- importjava.net.InetAddress;
- importjava.net.SocketException;
- importjava.net.UnknownHostException;
- publicclassAudioServerimplementsRunnable{
- DatagramSocketsocket;
- DatagramPacketpacket;//从客户端接收到的UDP包
- DatagramPacketsendPkt;//转发给另一个客户端的UDP包
- byte[]pktBuffer=newbyte[1024];
- intbufferSize=1024;
- booleanisRunning=false;
- intmyport=5656;
- /////////////
- StringclientIpStr="192.168.1.104";
- InetAddressclientIp;
- intclientPort=5757;
- publicAudioServer(){
- try{
- clientIp=InetAddress.getByName(clientIpStr);
- }catch(UnknownHostExceptione1){
- e1.printStackTrace();
- }
- try{
- socket=newDatagramSocket(myport);
- packet=newDatagramPacket(pktBuffer,bufferSize);
- }catch(SocketExceptione){
- e.printStackTrace();
- }
- System.out.println("服务器初始化完成");
- }
- publicvoidstartServer(){
- this.isRunning=true;
- newThread(this).start();
- }
- publicvoidrun(){
- try{
- while(isRunning){
- socket.receive(packet);
- sendPkt=newDatagramPacket(packet.getData(),
- packet.getLength(),packet.getAddress(),clientPort);
- socket.send(sendPkt);
- try{
- Thread.sleep(20);
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }
- }
- }catch(IOExceptione){
- }
- }
- //main
- publicstaticvoidmain(String[]args){
- newAudioServer().startServer();
- }
- }
5.结语:
Android使用 ILBC 进行语音通话的大致过程就讲述完了,此系列只是做一个ILBC 使用原理的介绍,距离真正的语音
通话还有很多工作要做,缺点还是很多的:
1. 文章中介绍的只是单方通话,如果要做成双方互相通话或者一对多的通话,就需要增加更多的流程处理,其服务端
也要做很多工作;
2. 实时性:本程序在局域网中使用时,实时性还是较高的,但是再广域网中,效果可能会有所下降,除此之外,本
程序还缺少时间戳的处理,如果网络状况不理想,或者数据延迟,就会导致语音播放前后混乱;
3. 服务器很弱:真正的流媒体服务器,需要很强的功能,来对数据进行处理,我是为了方便,就写了一个简单的,
最近打算移植live555,用来做专门的流媒体服务器,用RTP协议对数据进行封装,这样效果应该会好很多。
更多相关文章
- Android 继承SQLiteOpenHelper自定义DBHelper存取数据与图像
- android 网络异步加载数据进度条
- Android--遍历SQLite数据库下的所有表名
- android带图片的AlertDialog和文件管理器(代码)
- android 用代码画虚线边框背景
- 在eclipse中查看Android SDK源代码
- Android 对话框【Dialog】去除白色边框代码
- Android 音频播放
- Android 版本分布数据:Android 2.3 估超过 50%