来源:
----------------------------------------------------------------------------------------------------------------------
近刚刚做成了Android ilbc的项目,实现了语音对话功能,效果不错,ilbc将音频数据编码压缩为AMR格式,这种格式的音频压缩率很高,
960B的数据经过编码后长度仅仅为100B ,如此小的数据非常适合移动网络下的语音传输,节省大量的带宽,当然,高压缩比就意味着语
音质量损失高,不过实际使用中,AMR格式完全能够满足语音对话的要求。
之前使用别人给的一个现成的demo,可是发现仅仅是从GitHub扣下来的,源地址如下 ,
这个我没有真正试过到底如何,不过首先不爽的就是使用人家的现成的代码后,你会发现连里面的包名都不能改,一改就报错,因为编
译的 .so 库无法对应到该包名下面,并且,经过查看里面的Android.mk 发现,他使用的是 webrtc,这个是google 推出的一个开源项目,
里面带有 iLBC的库,我把这个 webrtc下载下来,整整有190多M!并且用这个编译,时间非常漫长,还容易出错,一狠心,干脆自己编
译一份.so 库出来。
最终,自己编写了一个简易的DEMO,包括一个服务端,项目最终源代码在这里:
接下来的后续文章中,将在Ubuntu系统上演示如何完整地实现语音对话的基本功能。
其二:
基于上一篇中提到的google网站的一份代码,这个需要git下载,我上传了一份在CSDN,进行了修改:下载链接: ()。
现在开始讲解代码结构搭建环节:
- 要求:
- 环境:Ubuntu 12.04 (其他Linux环境皆可),Android 2.2 及以上系统
- 工具:Elicpse 3.7 ,Android NDK r7 ,Android SDK
1.新建工程:
打开Eclipse,新建一个Android 程序,名称为 AndroidILBC 。
2.添加底层代码:
将下载的源码中的 jni 文件夹复制到新建的工程的根目录下,此时,代码结构如下:
3.Android.mk编写:
- LOCAL_PATH := $(call my-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)
- # Build JNI wrapper
- 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 文件,里面总共只有五个函数,负责音频编解码器的初始化,以及音频的编码和解码。其中的三个方法:
- jint Java_com_googlecode_androidilbc_Codec_init(
- JNIEnv *env, jobject this, jint mode)
- 和
- jint Java_com_googlecode_androidilbc_Codec_encode(
- JNIEnv *env, jobject this,
- jbyteArray sampleArray, jint sampleOffset, jint sampleLength,
- jbyteArray dataArray, jint dataOffset)
- 和
- jint Java_com_googlecode_androidilbc_Codec_decode(
- JNIEnv *env, jobject this,
- jbyteArray dataArray, jint dataOffset, jint dataLength,
- jbyteArray sampleArray, jint sampleOffset)
根据这三个函数的名称就可以知道,使用来让 Java层代码调用的三个函数,现在我们对这三个函数进行改造(仅仅是换个函数名称而已)
5.写Java层native 方法:
在程序的Java层代码中,建立一个包,用于放 NDK 的java层代码,比如我建一个名为 xmu.swordbearer.audio 的包,里面新建一个类:
AudioCodec.java ,在这个类中只负责对底层C函数进行调用,相当于一个工具类。新建三个 public staic native int 方法:
- package xmu.swordbearer.audio;
- public class AudioCodec {
- // initialize decoder and encoder
- public static native int audio_codec_init(int mode);
- // encode
- public static native int audio_encode(byte[] sample, int sampleOffset,
- int sampleLength, byte[] data, int dataOffset);
- // decode
- public static native int audio_decode(byte[] data, int dataOffset,
- int dataLength, byte[] sample, int sampleLength);
- }
三个方法分别用于初始化,音频编码,音频解码,在这里只需声明为 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
- */
- JNIEXPORT jint JNICALL Java_xmu_swordbearer_audio_AudioCodec_audio_1codec_1init
- (JNIEnv *, jclass, jint);
- /*
- * Class: xmu_swordbearer_audio_AudioCodec
- * Method: audio_encode
- * Signature: ([BII[BI)I
- */
- JNIEXPORT jint JNICALL Java_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
- */
- JNIEXPORT jint JNICALL Java_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真正的代码就是几
千行,不多,但是精。要想掌握得花一定的时间。目前这系列教程只是完成了底层的开发,接下来将完善整个系统,文章中有写的不好的,希
望多多指教,互相学习!
上一篇文章中详细讲述了 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的类,代表一段音频数据:
- public class AudioData {
- int size;
- byte[] realData;
- //long timestamp;
- public int getSize() {
- return size;
- }
- public void setSize(int size) {
- this.size = size;
- }
- public byte[] getRealData() {
- return realData;
- }
- public void setRealData(byte[] realData) {
- this.realData = realData;
- }
- //public long getTimestamp() {
- // return timestamp;
- // }
- //
- // public void setTimestamp(long timestamp) {
- // this.timestamp = timestamp;
- // }
- }
- public class AudioRecorder implements Runnable {
- String LOG = "Recorder ";
- private boolean isRecording = false;
- private AudioRecord audioRecord;
- private static final int audioSource = MediaRecorder.AudioSource.MIC;
- private static final int sampleRate = 8000;
- private static final int channelConfig = AudioFormat.CHANNEL_IN_MONO;
- private static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
- private static final int BUFFER_FRAME_SIZE =960;
- private int audioBufSize = 0;
- //
- private byte[] samples;// 缓冲区
- private int bufferRead = 0;// 从recorder中读取的samples的大小
- private int bufferSize = 0;// samples的大小
- // 开始录制
- public void startRecording() {
- bufferSize = BUFFER_FRAME_SIZE;
- audioBufSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig,
- audioFormat);
- if (audioBufSize == AudioRecord.ERROR_BAD_VALUE) {
- Log.e(LOG, "audioBufSize error");
- return;
- }
- samples = new byte[audioBufSize];
- // 初始化recorder
- if (null == audioRecord) {
- audioRecord = new AudioRecord(audioSource, sampleRate,
- channelConfig, audioFormat, audioBufSize);
- }
- new Thread(this).start();
- }
- // 停止录制
- public void stopRecording() {
- this.isRecording = false;
- }
- public boolean isRecording() {
- return isRecording;
- }
- // run
- public void run() {
- // 录制前,先启动解码器
- AudioEncoder encoder = AudioEncoder.getInstance();
- encoder.startEncoding();
- System.out.println(LOG + "audioRecord startRecording()");
- 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 (InterruptedException e) {
- e.printStackTrace();
- }
- }
- System.out.println(LOG + "录制结束");
- audioRecord.stop();
- encoder.stopEncoding();
- }
- }
3. AudioEncoder,负责调用NDK 方法实现音频的编码,每编码一次,就交给AudioSender 去发送:
- public class AudioEncoder implements Runnable {
- String LOG = "AudioEncoder";
- private static AudioEncoder encoder;
- private boolean isEncoding = false;
- private List<AudioData> dataList = null;// 存放数据
- public static AudioEncoder getInstance() {
- if (encoder == null) {
- encoder = new AudioEncoder();
- }
- return encoder;
- }
- private AudioEncoder() {
- dataList = Collections.synchronizedList(new LinkedList<AudioData>());
- }
- public void addData(byte[] data, int size) {
- AudioData rawData = new AudioData();
- rawData.setSize(size);
- byte[] tempData = new byte[size];
- System.arraycopy(data, 0, tempData, 0, size);
- rawData.setRealData(tempData);
- dataList.add(rawData);
- }
- // 开始编码
- public void startEncoding() {
- System.out.println(LOG + "解码线程启动");
- if (isEncoding) {
- Log.e(LOG, "编码器已经启动,不能再次启动");
- return;
- }
- new Thread(this).start();
- }
- // 结束
- public void stopEncoding() {
- this.isEncoding = false;
- }
- public void run() {
- // 先启动发送端
- AudioSender sender = new AudioSender();
- sender.startSending();
- int encodeSize = 0;
- byte[] encodedData = new byte[256];
- // 初始化编码器
- AudioCodec.audio_codec_init(30);
- isEncoding = true;
- while (isEncoding) {
- if (dataList.size() == 0) {
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- continue;
- }
- if (isEncoding) {
- AudioData rawData = dataList.remove(0);
- encodedData = new byte[rawData.getSize()];
- //
- encodeSize = AudioCodec.audio_encode(rawData.getRealData(), 0,
- rawData.getSize(), encodedData, 0);
- System.out.println();
- if (encodeSize > 0) {
- sender.addData(encodedData, encodeSize);
- // 清空数据
- encodedData = new byte[encodedData.length];
- }
- }
- }
- System.out.println(LOG + "编码结束");
- sender.stopSending();
- }
- }
4. AudioSender类,负责音频数据的发送,使用UDP协议将编码后的AMR音频数据发送到服务器端,这个类功能简单:
- public class AudioSender implements Runnable {
- String LOG = "AudioSender ";
- private boolean isSendering = false;
- private List<AudioData> dataList;
- DatagramSocket socket;
- DatagramPacket dataPacket;
- private InetAddress ip;
- private int port;
- public AudioSender() {
- dataList = Collections.synchronizedList(new LinkedList<AudioData>());
- try {
- try {
- ip = InetAddress.getByName(MyConfig.SERVER_HOST);
- this.port = MyConfig.SERVER_PORT;
- socket = new DatagramSocket();
- } catch (UnknownHostException e) {
- e.printStackTrace();
- }
- } catch (SocketException e) {
- e.printStackTrace();
- }
- }
- // 添加数据
- public void addData(byte[] data, int size) {
- AudioData encodedData = new AudioData();
- encodedData.setSize(size);
- byte[] tempData = new byte[size];
- System.arraycopy(data, 0, tempData, 0, size);
- encodedData.setRealData(tempData);
- dataList.add(encodedData);
- }
- // 发送数据
- private void sendData(byte[] data, int size) {
- try {
- dataPacket = new DatagramPacket(data, size, ip, port);
- dataPacket.setData(data);
- socket.send(dataPacket);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- // 开始发送
- public void startSending() {
- System.out.println(LOG + "发送线程启动");
- new Thread(this).start();
- }
- // 停止发送
- public void stopSending() {
- this.isSendering = false;
- }
- // run
- public void run() {
- this.isSendering = true;
- System.out.println(LOG + "开始发送数据");
- while (isSendering) {
- if (dataList.size() > 0) {
- AudioData encodedData = dataList.remove(0);
- sendData(encodedData.getRealData(), encodedData.getSize());
- }
- }
- System.out.println(LOG + "发送结束");
- }
- }
5. 另外,上述类中有一个 MyConfig 类,主要放一些 配置参数:
- public class MyConfig {
- public static String SERVER_HOST = "192.168.1.130";// 服务器的IP
- public static final int SERVER_PORT = 5656;// 服务器的监听端口
- public static final int CLIENT_PORT = 5757;//
- public static final int AUDIO_STATUS_RECORDING = 0;//手机端的状态:录音 or 播放
- public static final int AUDIO_STATUS_LISTENING = 1;
- public static void setServerHost(String ip) {
- System.out.println("修改后的服务器网址为 " + ip);
- SERVER_HOST = ip;
- }
- }
上述代码实现了发送端的功能,现在代码结构如下:
本实例中对音频没有添加时间戳处理,实际测试中没有太大的影响,可以听的清楚双方的语音对话,如果想要添加
时间戳的话,就在音频录制 AudioRecord的 read方法出得到时间戳,然后附加给UDP包。
接收端的原理已经在本系列文章的第三篇中讲述清楚,下一篇将贴出代码实现过程,有写的不好的地方,欢迎各位指点!
此系列文章拖了N久,有好多人发邮件来询问我第五次的文章为什么没有写,其实非常抱歉,本人学生一个,暑假一直
去公司实习,最近又忙着各种招聘找工作,没有时间好好写,现在抽空把最后一篇补上,水平有限,如过有不对的,请
各位指正~
前四篇文章分别介绍了 “代码结构”,“程序流程”,以及”发送方的处理”,现在就把接收方的处理流程做个介绍;
如上图所示,接收方的操作有三个类:AudioDecoder(负责解码),AudioPlayer(负责播放解码后的音频),
AudioReceiver(负责从服务器接收音频数据包),这三个类的流程在第三篇中有详细的介绍。
1.AudioReceiver代码:
AudioReceiver使用UDP方式从服务端接收音频数据,其过程比较简单,直接上代码:
- package xmu.swordbearer.audio.receiver;
- import java.io.IOException;
- import java.net.DatagramPacket;
- import java.net.DatagramSocket;
- import java.net.SocketException;
- import xmu.swordbearer.audio.MyConfig;
- import android.util.Log;
- public class AudioReceiver implements Runnable {
- String LOG = "NET Reciever ";
- int port = MyConfig.CLIENT_PORT;// 接收的端口
- DatagramSocket socket;
- DatagramPacket packet;
- boolean isRunning = false;
- private byte[] packetBuf = new byte[1024];
- private int packetSize = 1024;
- /*
- * 开始接收数据
- */
- public void startRecieving() {
- if (socket == null) {
- try {
- socket = new DatagramSocket(port);
- packet = new DatagramPacket(packetBuf, packetSize);
- } catch (SocketException e) {
- }
- }
- new Thread(this).start();
- }
- /*
- * 停止接收数据
- */
- public void stopRecieving() {
- isRunning = false;
- }
- /*
- * 释放资源
- */
- private void release() {
- if (packet != null) {
- packet = null;
- }
- if (socket != null) {
- socket.close();
- socket = null;
- }
- }
- public void run() {
- // 在接收前,要先启动解码器
- AudioDecoder decoder = AudioDecoder.getInstance();
- decoder.startDecoding();
- isRunning = true;
- try {
- while (isRunning) {
- socket.receive(packet);
- // 每接收一个UDP包,就交给解码器,等待解码
- decoder.addData(packet.getData(), packet.getLength());
- }
- } catch (IOException e) {
- Log.e(LOG, LOG + "RECIEVE ERROR!");
- }
- // 接收完成,停止解码器,释放资源
- decoder.stopDecoding();
- release();
- Log.e(LOG, LOG + "stop recieving");
- }
- }
2.AudioDecoder代码:
解码的过程也很简单,由于接收端接收到了音频数据,然后就把数据交给解码器,所以解码的主要工作就是把接收端的数
据取出来进行解码,如果解码正确,就将解码后的数据再转交给AudioPlayer去播放,这三个类之间是依次传递的 :
AudioReceiver---->AudioDecoder--->AudioPlayer
下面代码中有个List变量 private List<AudioData> dataList = null;这个就是用来存放数据的,每次解码时,dataList.remove(0),
从最前端取出数据进行解码:
- package xmu.swordbearer.audio.receiver;
- import java.util.Collections;
- import java.util.LinkedList;
- import java.util.List;
- import xmu.swordbearer.audio.AudioCodec;
- import xmu.swordbearer.audio.data.AudioData;
- import android.util.Log;
- public class AudioDecoder implements Runnable {
- String LOG = "CODEC Decoder ";
- private static AudioDecoder decoder;
- private static final int MAX_BUFFER_SIZE = 2048;
- private byte[] decodedData = new byte[1024];// data of decoded
- private boolean isDecoding = false;
- private List<AudioData> dataList = null;
- public static AudioDecoder getInstance() {
- if (decoder == null) {
- decoder = new AudioDecoder();
- }
- return decoder;
- }
- private AudioDecoder() {
- this.dataList = Collections
- .synchronizedList(new LinkedList<AudioData>());
- }
- /*
- * add Data to be decoded
- *
- * @ data:the data recieved from server
- *
- * @ size:data size
- */
- public void addData(byte[] data, int size) {
- AudioData adata = new AudioData();
- adata.setSize(size);
- byte[] tempData = new byte[size];
- System.arraycopy(data, 0, tempData, 0, size);
- adata.setRealData(tempData);
- dataList.add(adata);
- System.out.println(LOG + "add data once");
- }
- /*
- * start decode AMR data
- */
- public void startDecoding() {
- System.out.println(LOG + "start decoder");
- if (isDecoding) {
- return;
- }
- new Thread(this).start();
- }
- public void run() {
- // start player first
- AudioPlayer player = AudioPlayer.getInstance();
- player.startPlaying();
- //
- this.isDecoding = true;
- // init ILBC parameter:30 ,20, 15
- AudioCodec.audio_codec_init(30);
- Log.d(LOG, LOG + "initialized decoder");
- int decodeSize = 0;
- while (isDecoding) {
- while (dataList.size() > 0) {
- AudioData encodedData = dataList.remove(0);
- decodedData = new byte[MAX_BUFFER_SIZE];
- byte[] data = encodedData.getRealData();
- //
- decodeSize = AudioCodec.audio_decode(data, 0,
- encodedData.getSize(), decodedData, 0);
- if (decodeSize > 0) {
- // add decoded audio to player
- player.addData(decodedData, decodeSize);
- // clear data
- decodedData = new byte[decodedData.length];
- }
- }
- }
- System.out.println(LOG + "stop decoder");
- // stop playback audio
- player.stopPlaying();
- }
- public void stopDecoding() {
- this.isDecoding = false;
- }
- }
3.AudioPlayer代码:
播放器的工作流程其实和解码器一模一样,都是启动一个线程,然后不断从自己的 dataList中提取数据。
不过要注意,播放器的一些参数配置非常的关键;
播放声音时,使用了Android自带的 AudioTrack 这个类,它有这个方法:
public int write(byte[] audioData,int offsetInBytes, int sizeInBytes)可以直接播放;
所有播放器的代码如下:
- package xmu.swordbearer.audio.receiver;
- import java.util.Collections;
- import java.util.LinkedList;
- import java.util.List;
- import xmu.swordbearer.audio.data.AudioData;
- import android.media.AudioFormat;
- import android.media.AudioManager;
- import android.media.AudioRecord;
- import android.media.AudioTrack;
- import android.util.Log;
- public class AudioPlayer implements Runnable {
- String LOG = "AudioPlayer ";
- private static AudioPlayer player;
- private List<AudioData> dataList = null;
- private AudioData playData;
- private boolean isPlaying = false;
- private AudioTrack audioTrack;
- private static final int sampleRate = 8000;
- // 注意:参数配置
- private static final int channelConfig = AudioFormat.CHANNEL_IN_MONO;
- private static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
- private AudioPlayer() {
- dataList = Collections.synchronizedList(new LinkedList<AudioData>());
- }
- public static AudioPlayer getInstance() {
- if (player == null) {
- player = new AudioPlayer();
- }
- return player;
- }
- public void addData(byte[] rawData, int size) {
- AudioData decodedData = new AudioData();
- decodedData.setSize(size);
- byte[] tempData = new byte[size];
- System.arraycopy(rawData, 0, tempData, 0, size);
- decodedData.setRealData(tempData);
- dataList.add(decodedData);
- }
- /*
- * init Player parameters
- */
- private boolean initAudioTrack() {
- int bufferSize = AudioRecord.getMinBufferSize(sampleRate,
- channelConfig, audioFormat);
- if (bufferSize < 0) {
- Log.e(LOG, LOG + "initialize error!");
- return false;
- }
- audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
- channelConfig, audioFormat, bufferSize, AudioTrack.MODE_STREAM);
- // set volume:设置播放音量
- audioTrack.setStereoVolume(1.0f, 1.0f);
- audioTrack.play();
- return true;
- }
- private void playFromList() {
- while (dataList.size() > 0 && isPlaying) {
- playData = dataList.remove(0);
- audioTrack.write(playData.getRealData(), 0, playData.getSize());
- }
- }
- public void startPlaying() {
- if (isPlaying) {
- return;
- }
- new Thread(this).start();
- }
- public void run() {
- this.isPlaying = true;
- if (!initAudioTrack()) {
- Log.e(LOG, LOG + "initialized player error!");
- return;
- }
- while (isPlaying) {
- if (dataList.size() > 0) {
- playFromList();
- } else {
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- }
- }
- }
- if (this.audioTrack != null) {
- if (this.audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
- this.audioTrack.stop();
- this.audioTrack.release();
- }
- }
- Log.d(LOG, LOG + "end playing");
- }
- public void stopPlaying() {
- this.isPlaying = false;
- }
- }
4.简易服务端:
为了方便测试,我自己用Java 写了一个UDP的服务器,其功能非常的弱,就是接收,然后转发给另一方:
- import java.io.IOException;
- import java.net.DatagramPacket;
- import java.net.DatagramSocket;
- import java.net.InetAddress;
- import java.net.SocketException;
- import java.net.UnknownHostException;
- public class AudioServer implements Runnable {
- DatagramSocket socket;
- DatagramPacket packet;// 从客户端接收到的UDP包
- DatagramPacket sendPkt;// 转发给另一个客户端的UDP包
- byte[] pktBuffer = new byte[1024];
- int bufferSize = 1024;
- boolean isRunning = false;
- int myport = 5656;
- // ///
- String clientIpStr = "192.168.1.104";
- InetAddress clientIp;
- int clientPort = 5757;
- public AudioServer() {
- try {
- clientIp = InetAddress.getByName(clientIpStr);
- } catch (UnknownHostException e1) {
- e1.printStackTrace();
- }
- try {
- socket = new DatagramSocket(myport);
- packet = new DatagramPacket(pktBuffer, bufferSize);
- } catch (SocketException e) {
- e.printStackTrace();
- }
- System.out.println("服务器初始化完成");
- }
- public void startServer() {
- this.isRunning = true;
- new Thread(this).start();
- }
- public void run() {
- try {
- while (isRunning) {
- socket.receive(packet);
- sendPkt = new DatagramPacket(packet.getData(),
- packet.getLength(), packet.getAddress(), clientPort);
- socket.send(sendPkt);
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- } catch (IOException e) {
- }
- }
- // main
- public static void main(String[] args) {
- new AudioServer().startServer();
- }
- }
5.结语:
Android使用 ILBC 进行语音通话的大致过程就讲述完了,此系列只是做一个ILBC 使用原理的介绍,距离真正的语音
通话还有很多工作要做,缺点还是很多的:
1. 文章中介绍的只是单方通话,如果要做成双方互相通话或者一对多的通话,就需要增加更多的流程处理,其服务端
也要做很多工作;
2. 实时性:本程序在局域网中使用时,实时性还是较高的,但是再广域网中,效果可能会有所下降,除此之外,本
程序还缺少时间戳的处理,如果网络状况不理想,或者数据延迟,就会导致语音播放前后混乱;
3. 服务器很弱:真正的流媒体服务器,需要很强的功能,来对数据进行处理,我是为了方便,就写了一个简单的,
最近打算移植live555,用来做专门的流媒体服务器,用RTP协议对数据进行封装,这样效果应该会好很多。