博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
android 网络语音电话合集 此文为转载备份
阅读量:5052 次
发布时间:2019-06-12

本文共 21605 字,大约阅读时间需要 72 分钟。

 

来源:   

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

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

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

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

       之前使用别人给的一个现成的demo,可是发现仅仅是从GitHub扣下来的,源地址如下 ,

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

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

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

译一份.so 库出来。

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

       

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

其二:

 

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

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

  1. 要求:  
  2. 环境:Ubuntu 12.04 (其他Linux环境皆可),Android 2.2 及以上系统  
  3.   
  4. 工具:Elicpse 3.7 ,Android NDK r7 ,Android SDK   

1.新建工程:

 

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

 

2.添加底层代码:

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

                              

 

3.Android.mk编写:

 

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

 

4.代码分析:

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

 

  1. jint Java_com_googlecode_androidilbc_Codec_init(  
  2.         JNIEnv *env, jobject this, jint mode)  
  3. 和  
  4.   
  5. jint Java_com_googlecode_androidilbc_Codec_encode(  
  6.         JNIEnv *env, jobject this,  
  7.         jbyteArray sampleArray, jint sampleOffset, jint sampleLength,  
  8.         jbyteArray dataArray, jint dataOffset)  
  9. 和  
  10. jint Java_com_googlecode_androidilbc_Codec_decode(  
  11.         JNIEnv *env, jobject this,  
  12.         jbyteArray dataArray, jint dataOffset, jint dataLength,  
  13.         jbyteArray sampleArray, jint sampleOffset)  

 

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

 

5.写Java层native 方法:

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

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

 

  1. package xmu.swordbearer.audio;  
  2.   
  3. public class AudioCodec {  
  4.     // initialize decoder and encoder  
  5.     public static native int audio_codec_init(int mode);  
  6.   
  7.     // encode  
  8.     public static native int audio_encode(byte[] sample, int sampleOffset,  
  9.             int sampleLength, byte[] data, int dataOffset);  
  10.   
  11.     // decode  
  12.     public static native int audio_decode(byte[] data, int dataOffset,  
  13.             int dataLength, byte[] sample, int sampleLength);  
  14. }  

 

三个方法分别用于初始化,音频编码,音频解码,在这里只需声明为 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. JNIEXPORT jint JNICALL Java_xmu_swordbearer_audio_AudioCodec_audio_1codec_1init  
  7.   (JNIEnv *, jclass, jint);  
  8.   
  9. /* 
  10.  * Class:     xmu_swordbearer_audio_AudioCodec 
  11.  * Method:    audio_encode 
  12.  * Signature: ([BII[BI)I 
  13.  */  
  14. JNIEXPORT jint JNICALL Java_xmu_swordbearer_audio_AudioCodec_audio_1encode  
  15.   (JNIEnv *, jclass, jbyteArray, jint, jint, jbyteArray, jint);  
  16.   
  17. /* 
  18.  * Class:     xmu_swordbearer_audio_AudioCodec 
  19.  * Method:    audio_decode 
  20.  * Signature: ([BII[BI)I 
  21.  */  
  22. JNIEXPORT jint JNICALL Java_xmu_swordbearer_audio_AudioCodec_audio_1decode  
  23.   (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.   
  5. Java_com_googlecode_androidilbc_Codec_encode  
  6. 改为:  
  7. Java_xmu_swordbearer_audio_AudioCodec_audio_1encode  
  8.   
  9. Java_com_googlecode_androidilbc_Codec_decode  
  10. 改为:  
  11. 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真正的代码就是几

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

望多多指教,互相学习!

  

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

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

   感觉非常不错,绘制功能强大,推荐给大家,需要注册才能使用。  

 

   图解:

 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. 发送方有三个线程,接受方也有三个线程,一一对应,下一篇正式开始贴代码 ;

 

 

上一文章中提到:

 

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

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

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

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

 

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

 

 

  1. public class AudioData {  
  2.     int size;  
  3.     byte[] realData;  
  4.     //long timestamp;  
  5.   
  6.     public int getSize() {  
  7.         return size;  
  8.     }  
  9.   
  10.     public void setSize(int size) {  
  11.         this.size = size;  
  12.     }  
  13.   
  14.     public byte[] getRealData() {  
  15.         return realData;  
  16.     }  
  17.   
  18.     public void setRealData(byte[] realData) {  
  19.         this.realData = realData;  
  20.     }  
  21.   
  22.     //public long getTimestamp() {
      
  23.     // return timestamp;  
  24.     // }  
  25.     //  
  26.     // public void setTimestamp(long timestamp) {
      
  27.     // this.timestamp = timestamp;  
  28.     // }  
  29. }  

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

 

 

 

  1. public class AudioRecorder implements Runnable {  
  2.   
  3.     String LOG = "Recorder ";  
  4.   
  5.     private boolean isRecording = false;  
  6.     private AudioRecord audioRecord;  
  7.   
  8.     private static final int audioSource = MediaRecorder.AudioSource.MIC;  
  9.     private static final int sampleRate = 8000;  
  10.     private static final int channelConfig = AudioFormat.CHANNEL_IN_MONO;  
  11.     private static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;  
  12.     private static final int BUFFER_FRAME_SIZE =960;  
  13.     private int audioBufSize = 0;  
  14.   
  15.     //  
  16.     private byte[] samples;// 缓冲区  
  17.     private int bufferRead = 0;// 从recorder中读取的samples的大小  
  18.   
  19.     private int bufferSize = 0;// samples的大小  
  20.   
  21.     // 开始录制  
  22.     public void startRecording() {  
  23.         bufferSize = BUFFER_FRAME_SIZE;  
  24.   
  25.         audioBufSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig,  
  26.                 audioFormat);  
  27.         if (audioBufSize == AudioRecord.ERROR_BAD_VALUE) {  
  28.             Log.e(LOG, "audioBufSize error");  
  29.             return;  
  30.         }  
  31.         samples = new byte[audioBufSize];  
  32.         // 初始化recorder  
  33.         if (null == audioRecord) {  
  34.             audioRecord = new AudioRecord(audioSource, sampleRate,  
  35.                     channelConfig, audioFormat, audioBufSize);  
  36.         }  
  37.         new Thread(this).start();  
  38.     }  
  39.   
  40.     // 停止录制  
  41.     public void stopRecording() {  
  42.         this.isRecording = false;  
  43.     }  
  44.   
  45.     public boolean isRecording() {  
  46.         return isRecording;  
  47.     }  
  48.   
  49.     // run  
  50.     public void run() {  
  51.         // 录制前,先启动解码器  
  52.         AudioEncoder encoder = AudioEncoder.getInstance();  
  53.         encoder.startEncoding();  
  54.   
  55.         System.out.println(LOG + "audioRecord startRecording()");  
  56.         audioRecord.startRecording();  
  57.   
  58.         this.isRecording = true;  
  59.         while (isRecording) {  
  60.             bufferRead = audioRecord.read(samples, 0, bufferSize);  
  61.             if (bufferRead > 0) {  
  62.                 // 将数据添加给解码器  
  63.                 encoder.addData(samples, bufferRead);  
  64.             }  
  65.   
  66.             try {  
  67.                 Thread.sleep(20);  
  68.             } catch (InterruptedException e) {  
  69.                 e.printStackTrace();  
  70.             }  
  71.         }  
  72.         System.out.println(LOG + "录制结束");  
  73.         audioRecord.stop();  
  74.         encoder.stopEncoding();  
  75.     }  
  76. }  

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

 

 

  1. public class AudioEncoder implements Runnable {  
  2.     String LOG = "AudioEncoder";  
  3.   
  4.     private static AudioEncoder encoder;  
  5.     private boolean isEncoding = false;  
  6.   
  7.     private List<AudioData> dataList = null;// 存放数据  
  8.   
  9.     public static AudioEncoder getInstance() {  
  10.         if (encoder == null) {  
  11.             encoder = new AudioEncoder();  
  12.         }  
  13.         return encoder;  
  14.     }  
  15.   
  16.     private AudioEncoder() {  
  17.         dataList = Collections.synchronizedList(new LinkedList<AudioData>());  
  18.     }  
  19.   
  20.     public void addData(byte[] data, int size) {  
  21.         AudioData rawData = new AudioData();  
  22.         rawData.setSize(size);  
  23.         byte[] tempData = new byte[size];  
  24.         System.arraycopy(data, 0, tempData, 0, size);  
  25.         rawData.setRealData(tempData);  
  26.         dataList.add(rawData);  
  27.     }  
  28.   
  29.     // 开始编码  
  30.     public void startEncoding() {  
  31.         System.out.println(LOG + "解码线程启动");  
  32.         if (isEncoding) {  
  33.             Log.e(LOG, "编码器已经启动,不能再次启动");  
  34.             return;  
  35.         }  
  36.         new Thread(this).start();  
  37.     }  
  38.   
  39.     // 结束  
  40.     public void stopEncoding() {  
  41.         this.isEncoding = false;  
  42.     }  
  43.   
  44.     public void run() {  
  45.         // 先启动发送端  
  46.         AudioSender sender = new AudioSender();  
  47.         sender.startSending();  
  48.   
  49.         int encodeSize = 0;  
  50.         byte[] encodedData = new byte[256];  
  51.   
  52.         // 初始化编码器  
  53.         AudioCodec.audio_codec_init(30);  
  54.   
  55.         isEncoding = true;  
  56.         while (isEncoding) {  
  57.             if (dataList.size() == 0) {  
  58.                 try {  
  59.                     Thread.sleep(20);  
  60.                 } catch (InterruptedException e) {  
  61.                     e.printStackTrace();  
  62.                 }  
  63.                 continue;  
  64.             }  
  65.             if (isEncoding) {  
  66.                 AudioData rawData = dataList.remove(0);  
  67.                 encodedData = new byte[rawData.getSize()];  
  68.                 //  
  69.                 encodeSize = AudioCodec.audio_encode(rawData.getRealData(), 0,  
  70.                         rawData.getSize(), encodedData, 0);  
  71.                 System.out.println();  
  72.                 if (encodeSize > 0) {  
  73.                     sender.addData(encodedData, encodeSize);  
  74.                     // 清空数据  
  75.                     encodedData = new byte[encodedData.length];  
  76.                 }  
  77.             }  
  78.         }  
  79.         System.out.println(LOG + "编码结束");  
  80.         sender.stopSending();  
  81.     }  
  82. }  

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

  1. public class AudioSender implements Runnable {  
  2.     String LOG = "AudioSender ";  
  3.   
  4.     private boolean isSendering = false;  
  5.     private List<AudioData> dataList;  
  6.   
  7.     DatagramSocket socket;  
  8.     DatagramPacket dataPacket;  
  9.     private InetAddress ip;  
  10.     private int port;  
  11.   
  12.     public AudioSender() {  
  13.         dataList = Collections.synchronizedList(new LinkedList<AudioData>());  
  14.         try {  
  15.             try {  
  16.                 ip = InetAddress.getByName(MyConfig.SERVER_HOST);  
  17.                 this.port = MyConfig.SERVER_PORT;  
  18.                 socket = new DatagramSocket();  
  19.             } catch (UnknownHostException e) {  
  20.                 e.printStackTrace();  
  21.             }  
  22.         } catch (SocketException e) {  
  23.             e.printStackTrace();  
  24.         }  
  25.     }  
  26.   
  27.     // 添加数据  
  28.     public void addData(byte[] data, int size) {  
  29.         AudioData encodedData = new AudioData();  
  30.         encodedData.setSize(size);  
  31.         byte[] tempData = new byte[size];  
  32.         System.arraycopy(data, 0, tempData, 0, size);  
  33.         encodedData.setRealData(tempData);  
  34.         dataList.add(encodedData);  
  35.     }  
  36.   
  37.     // 发送数据  
  38.     private void sendData(byte[] data, int size) {  
  39.         try {  
  40.             dataPacket = new DatagramPacket(data, size, ip, port);  
  41.             dataPacket.setData(data);  
  42.             socket.send(dataPacket);  
  43.         } catch (IOException e) {  
  44.             e.printStackTrace();  
  45.         }  
  46.     }  
  47.   
  48.     // 开始发送  
  49.     public void startSending() {  
  50.         System.out.println(LOG + "发送线程启动");  
  51.         new Thread(this).start();  
  52.     }  
  53.   
  54.     // 停止发送  
  55.     public void stopSending() {  
  56.         this.isSendering = false;  
  57.     }  
  58.   
  59.     // run  
  60.     public void run() {  
  61.         this.isSendering = true;  
  62.         System.out.println(LOG + "开始发送数据");  
  63.         while (isSendering) {  
  64.             if (dataList.size() > 0) {  
  65.                 AudioData encodedData = dataList.remove(0);  
  66.                 sendData(encodedData.getRealData(), encodedData.getSize());  
  67.             }  
  68.         }  
  69.         System.out.println(LOG + "发送结束");  
  70.     }  
  71. }  

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

  

  1. public class MyConfig {  
  2.     public static String SERVER_HOST = "192.168.1.130";// 服务器的IP  
  3.     public static final int SERVER_PORT = 5656;// 服务器的监听端口  
  4.     public static final int CLIENT_PORT = 5757;//  
  5.   
  6.     public static final int AUDIO_STATUS_RECORDING = 0;//手机端的状态:录音 or 播放  
  7.     public static final int AUDIO_STATUS_LISTENING = 1;  
  8.   
  9.     public static void setServerHost(String ip) {  
  10.         System.out.println("修改后的服务器网址为  " + ip);  
  11.         SERVER_HOST = ip;  
  12.     }  
  13. }  

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

 

                           

 

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

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

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

 

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

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

各位指正~

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

           

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

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

1.AudioReceiver代码:

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

 

  1. package xmu.swordbearer.audio.receiver;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.DatagramPacket;  
  5. import java.net.DatagramSocket;  
  6. import java.net.SocketException;  
  7.   
  8. import xmu.swordbearer.audio.MyConfig;  
  9. import android.util.Log;  
  10.   
  11. public class AudioReceiver implements Runnable {  
  12.     String LOG = "NET Reciever ";  
  13.     int port = MyConfig.CLIENT_PORT;// 接收的端口  
  14.     DatagramSocket socket;  
  15.     DatagramPacket packet;  
  16.     boolean isRunning = false;  
  17.   
  18.     private byte[] packetBuf = new byte[1024];  
  19.     private int packetSize = 1024;  
  20.   
  21.     /* 
  22.      * 开始接收数据 
  23.      */  
  24.     public void startRecieving() {  
  25.         if (socket == null) {  
  26.             try {  
  27.                 socket = new DatagramSocket(port);  
  28.                 packet = new DatagramPacket(packetBuf, packetSize);  
  29.             } catch (SocketException e) {  
  30.             }  
  31.         }  
  32.         new Thread(this).start();  
  33.     }  
  34.   
  35.     /* 
  36.      * 停止接收数据 
  37.      */  
  38.     public void stopRecieving() {  
  39.         isRunning = false;  
  40.     }  
  41.   
  42.     /* 
  43.      * 释放资源 
  44.      */  
  45.     private void release() {  
  46.         if (packet != null) {  
  47.             packet = null;  
  48.         }  
  49.         if (socket != null) {  
  50.             socket.close();  
  51.             socket = null;  
  52.         }  
  53.     }  
  54.   
  55.     public void run() {  
  56.         // 在接收前,要先启动解码器  
  57.         AudioDecoder decoder = AudioDecoder.getInstance();  
  58.         decoder.startDecoding();  
  59.   
  60.         isRunning = true;  
  61.         try {  
  62.             while (isRunning) {  
  63.                 socket.receive(packet);  
  64.                 // 每接收一个UDP包,就交给解码器,等待解码  
  65.                 decoder.addData(packet.getData(), packet.getLength());  
  66.             }  
  67.   
  68.         } catch (IOException e) {  
  69.             Log.e(LOG, LOG + "RECIEVE ERROR!");  
  70.         }  
  71.         // 接收完成,停止解码器,释放资源  
  72.         decoder.stopDecoding();  
  73.         release();  
  74.         Log.e(LOG, LOG + "stop recieving");  
  75.     }  
  76.   
  77. }  

 

2.AudioDecoder代码:

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

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

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

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

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

 

  1. package xmu.swordbearer.audio.receiver;  
  2.   
  3. import java.util.Collections;  
  4. import java.util.LinkedList;  
  5. import java.util.List;  
  6.   
  7. import xmu.swordbearer.audio.AudioCodec;  
  8. import xmu.swordbearer.audio.data.AudioData;  
  9. import android.util.Log;  
  10.   
  11. public class AudioDecoder implements Runnable {  
  12.   
  13.     String LOG = "CODEC Decoder ";  
  14.     private static AudioDecoder decoder;  
  15.   
  16.     private static final int MAX_BUFFER_SIZE = 2048;  
  17.   
  18.     private byte[] decodedData = new byte[1024];// data of decoded  
  19.     private boolean isDecoding = false;  
  20.     private List<AudioData> dataList = null;  
  21.   
  22.     public static AudioDecoder getInstance() {  
  23.         if (decoder == null) {  
  24.             decoder = new AudioDecoder();  
  25.         }  
  26.         return decoder;  
  27.     }  
  28.   
  29.     private AudioDecoder() {  
  30.         this.dataList = Collections  
  31.                 .synchronizedList(new LinkedList<AudioData>());  
  32.     }  
  33.   
  34.     /* 
  35.      * add Data to be decoded 
  36.      *  
  37.      * @ data:the data recieved from server 
  38.      *  
  39.      * @ size:data size 
  40.      */  
  41.     public void addData(byte[] data, int size) {  
  42.         AudioData adata = new AudioData();  
  43.         adata.setSize(size);  
  44.         byte[] tempData = new byte[size];  
  45.         System.arraycopy(data, 0, tempData, 0, size);  
  46.         adata.setRealData(tempData);  
  47.         dataList.add(adata);  
  48.         System.out.println(LOG + "add data once");  
  49.   
  50.     }  
  51.   
  52.     /* 
  53.      * start decode AMR data 
  54.      */  
  55.     public void startDecoding() {  
  56.         System.out.println(LOG + "start decoder");  
  57.         if (isDecoding) {  
  58.             return;  
  59.         }  
  60.         new Thread(this).start();  
  61.     }  
  62.   
  63.     public void run() {  
  64.         // start player first  
  65.         AudioPlayer player = AudioPlayer.getInstance();  
  66.         player.startPlaying();  
  67.         //  
  68.         this.isDecoding = true;  
  69.         // init ILBC parameter:30 ,20, 15  
  70.         AudioCodec.audio_codec_init(30);  
  71.   
  72.         Log.d(LOG, LOG + "initialized decoder");  
  73.         int decodeSize = 0;  
  74.         while (isDecoding) {  
  75.             while (dataList.size() > 0) {  
  76.                 AudioData encodedData = dataList.remove(0);  
  77.                 decodedData = new byte[MAX_BUFFER_SIZE];  
  78.   
  79.                 byte[] data = encodedData.getRealData();  
  80.                 //  
  81.                 decodeSize = AudioCodec.audio_decode(data, 0,  
  82.                         encodedData.getSize(), decodedData, 0);  
  83.                 if (decodeSize > 0) {  
  84.                     // add decoded audio to player  
  85.                     player.addData(decodedData, decodeSize);  
  86.                     // clear data  
  87.                     decodedData = new byte[decodedData.length];  
  88.                 }  
  89.             }  
  90.         }  
  91.         System.out.println(LOG + "stop decoder");  
  92.         // stop playback audio  
  93.         player.stopPlaying();  
  94.     }  
  95.   
  96.     public void stopDecoding() {  
  97.         this.isDecoding = false;  
  98.     }  
  99. }  

3.AudioPlayer代码:

 

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

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

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

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

所有播放器的代码如下:

 

  1. package xmu.swordbearer.audio.receiver;  
  2.   
  3. import java.util.Collections;  
  4. import java.util.LinkedList;  
  5. import java.util.List;  
  6.   
  7. import xmu.swordbearer.audio.data.AudioData;  
  8. import android.media.AudioFormat;  
  9. import android.media.AudioManager;  
  10. import android.media.AudioRecord;  
  11. import android.media.AudioTrack;  
  12. import android.util.Log;  
  13.   
  14. public class AudioPlayer implements Runnable {  
  15.     String LOG = "AudioPlayer ";  
  16.     private static AudioPlayer player;  
  17.   
  18.     private List<AudioData> dataList = null;  
  19.     private AudioData playData;  
  20.     private boolean isPlaying = false;  
  21.   
  22.     private AudioTrack audioTrack;  
  23.   
  24.     private static final int sampleRate = 8000;  
  25.     // 注意:参数配置  
  26.     private static final int channelConfig = AudioFormat.CHANNEL_IN_MONO;  
  27.     private static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;  
  28.   
  29.     private AudioPlayer() {  
  30.         dataList = Collections.synchronizedList(new LinkedList<AudioData>());  
  31.     }  
  32.   
  33.     public static AudioPlayer getInstance() {  
  34.         if (player == null) {  
  35.             player = new AudioPlayer();  
  36.         }  
  37.         return player;  
  38.     }  
  39.   
  40.     public void addData(byte[] rawData, int size) {  
  41.         AudioData decodedData = new AudioData();  
  42.         decodedData.setSize(size);  
  43.   
  44.         byte[] tempData = new byte[size];  
  45.         System.arraycopy(rawData, 0, tempData, 0, size);  
  46.         decodedData.setRealData(tempData);  
  47.         dataList.add(decodedData);  
  48.     }  
  49.   
  50.     /* 
  51.      * init Player parameters 
  52.      */  
  53.     private boolean initAudioTrack() {  
  54.         int bufferSize = AudioRecord.getMinBufferSize(sampleRate,  
  55.                 channelConfig, audioFormat);  
  56.         if (bufferSize < 0) {  
  57.             Log.e(LOG, LOG + "initialize error!");  
  58.             return false;  
  59.         }  
  60.         audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,  
  61.                 channelConfig, audioFormat, bufferSize, AudioTrack.MODE_STREAM);  
  62.         // set volume:设置播放音量  
  63.         audioTrack.setStereoVolume(1.0f, 1.0f);  
  64.         audioTrack.play();  
  65.         return true;  
  66.     }  
  67.   
  68.     private void playFromList() {  
  69.         while (dataList.size() > 0 && isPlaying) {  
  70.             playData = dataList.remove(0);  
  71.             audioTrack.write(playData.getRealData(), 0, playData.getSize());  
  72.         }  
  73.     }  
  74.   
  75.     public void startPlaying() {  
  76.         if (isPlaying) {  
  77.             return;  
  78.         }  
  79.         new Thread(this).start();  
  80.     }  
  81.   
  82.     public void run() {  
  83.         this.isPlaying = true;  
  84.           
  85.         if (!initAudioTrack()) {  
  86.             Log.e(LOG, LOG + "initialized player error!");  
  87.             return;  
  88.         }  
  89.         while (isPlaying) {  
  90.             if (dataList.size() > 0) {  
  91.                 playFromList();  
  92.             } else {  
  93.                 try {  
  94.                     Thread.sleep(20);  
  95.                 } catch (InterruptedException e) {  
  96.                 }  
  97.             }  
  98.         }  
  99.         if (this.audioTrack != null) {  
  100.             if (this.audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {  
  101.                 this.audioTrack.stop();  
  102.                 this.audioTrack.release();  
  103.             }  
  104.         }  
  105.         Log.d(LOG, LOG + "end playing");  
  106.     }  
  107.   
  108.     public void stopPlaying() {  
  109.         this.isPlaying = false;  
  110.     }  
  111. }  

 

 

4.简易服务端:

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

  1. import java.io.IOException;  
  2. import java.net.DatagramPacket;  
  3. import java.net.DatagramSocket;  
  4. import java.net.InetAddress;  
  5. import java.net.SocketException;  
  6. import java.net.UnknownHostException;  
  7.   
  8. public class AudioServer implements Runnable {  
  9.   
  10.     DatagramSocket socket;  
  11.     DatagramPacket packet;// 从客户端接收到的UDP包  
  12.     DatagramPacket sendPkt;// 转发给另一个客户端的UDP包  
  13.   
  14.     byte[] pktBuffer = new byte[1024];  
  15.     int bufferSize = 1024;  
  16.     boolean isRunning = false;  
  17.     int myport = 5656;  
  18.   
  19.     // ///  
  20.     String clientIpStr = "192.168.1.104";  
  21.     InetAddress clientIp;  
  22.     int clientPort = 5757;  
  23.   
  24.     public AudioServer() {  
  25.         try {  
  26.             clientIp = InetAddress.getByName(clientIpStr);  
  27.         } catch (UnknownHostException e1) {  
  28.             e1.printStackTrace();  
  29.         }  
  30.         try {  
  31.             socket = new DatagramSocket(myport);  
  32.             packet = new DatagramPacket(pktBuffer, bufferSize);  
  33.         } catch (SocketException e) {  
  34.             e.printStackTrace();  
  35.         }  
  36.         System.out.println("服务器初始化完成");  
  37.     }  
  38.   
  39.     public void startServer() {  
  40.         this.isRunning = true;  
  41.         new Thread(this).start();  
  42.     }  
  43.   
  44.     public void run() {  
  45.         try {  
  46.             while (isRunning) {  
  47.                 socket.receive(packet);  
  48.                 sendPkt = new DatagramPacket(packet.getData(),  
  49.                         packet.getLength(), packet.getAddress(), clientPort);  
  50.                 socket.send(sendPkt);  
  51.                 try {  
  52.                     Thread.sleep(20);  
  53.                 } catch (InterruptedException e) {  
  54.                     e.printStackTrace();  
  55.                 }  
  56.             }  
  57.         } catch (IOException e) {  
  58.         }  
  59.     }  
  60.   
  61.     // main  
  62.     public static void main(String[] args) {  
  63.         new AudioServer().startServer();  
  64.     }  
  65. }   

5.结语:

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

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

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

也要做很多工作;

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

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

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

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

 

 

转载于:https://www.cnblogs.com/songtzu/archive/2013/02/18/2915395.html

你可能感兴趣的文章
php读取文件内容的三种方式(转)
查看>>
hadoop数据备份
查看>>
二分图匹配 学习笔记
查看>>
poj 2154:Color【polya计数,Euler函数】
查看>>
【转】前台和后台线程
查看>>
洛谷 P1964 【mc生存】卖东西
查看>>
纯CSS制作自适应分页条-分享------彭记(019)
查看>>
VS WebDev.WebServer40
查看>>
openjudge 2971:抓住那头牛 解题报告
查看>>
如何实现redis集群?
查看>>
架构中的集成难点
查看>>
正则表达式
查看>>
liunx系统虚拟机下安装tomcat9以及访问tomcat案例
查看>>
Oracle 插入Date数据
查看>>
word文档操作
查看>>
UIpageControl
查看>>
js判断是否为IE浏览器,是返回true,否返回false
查看>>
Linux性能分析 vmstat基本语法
查看>>
SpringMVC框架学习笔记(2)——使用注解开发SpringMVC
查看>>
深入理解递归函数的调用过程
查看>>