RSS
热门关键字:
当前位置 : 主页>编程开发>java编程>入门>列表

用Java实现网络语音信号传送

来源:我要研发网 作者: 时间:1970-01-01 点击:



  一、引言

  Java是一门适合于分布式计算环境、尤其是Internet程序设计语言。这不仅仅在于java具有很安全性和可移植性,还在于java为Internet编程提供了丰富网络类库支持。利用这些网络类库,可以轻松编写多种类型网络通信程序。然而由于某些限制,Java在传输多媒体信息方面应用不是很广,大部分应用都集中在网络上传输语音等音频信号方面。传输音频信号应用方案一般有两种,一是应用于数据广播多对一传输,例如音频数据服务器向数个客户端发送音频数据信号,其最广泛应用则是某些网上IP电话,大家经常可以看到不少这种提供在线IP电话服务网站客户端都是使用嵌在网页上Java Applet程序,用来实现拨号、通话等等基本网络电话功能; 第二种方案则是我们今天要涉及部分,一对一音频信号数据传输。这种方案应用范围更广。大家都去过语音聊天室,大部分语音聊天室语音聊天功能实现就是使用Java技术,大家对这样网页源代码分析一下就可以发现这一点。

字串6

  我曾开发一个项目,涉及使用java来实现在网络上传输语音数据。开发中遇到不少问题,而且在互联网上发现关于java语音传输资料比较少,寻找了许多天,最终从一个开放源代码一个简单Answer Machine 演示程序中获得了解决问题方法。今天我就把我在点对点传输音频信号方面一些经验拿出来,与大家共同探讨这方面问题。

字串5

  二、存在问题 字串4

  在网络上传输音频方面存在问题主要可以归纳为以下几点: 字串4

  1 双方之间网络连接 字串1

  要进行频数据传输,首先就是要建立数据连结。常用通讯协议中,TCP较可靠,所以用在不允许数据丢失应用上。而UDP则较多应用于处理速度要求较快、数据传输可靠性要求不是很高应用上,如数据广播。通信协议选择取决于我们所要做应用类型。怎样建立网络连接,稳定接收和发送音频信号数据流是关键。

字串8

  2 音频信号采集以及回放

字串8

  在进行音频信号采集中我们必须考虑到采样率问题,声音信号采样率有8Khz、16Khz、32Khz、44Khz等,每种数据采样虑产生数据量都不一样,越高采样率产生数据量越大,所以我们要选择合适采样率以适应网络带宽。

字串5

  3 音频数字信号编码与解码。

字串1

  如果把直接采集到音频信号数据流在网络上进行传输,它所占有带宽也是十分大,以8Khz采样率采集14位音频数据那么就有以下这样一个式子:

字串9

  14 bit * 8000/second=112,000 bits/second or112kbps 字串9

  从中我们可以看出以这样方式传输音频数据,每秒需要向网络中发送112kb数据。所以。从节省带宽角度考虑,我们很有必要对这样数据进行压缩。对多媒体信号压缩我们有许多可以选择格式,如mp2、mp3、GSM等等。同样,我们这里也存在一个对压缩格式进行选择问题,考虑到音频数据传输及时性,对传输音频数据质量要求,以及各种压缩格式压缩比率以及进行压缩和解压缩所要耗费系统资源等方面问题,选择合适压缩格式就显得尤为重要。

字串3

  三、解决方法

字串2

  下面就针对前面提出问题讨论一下解决办法。 字串2

  1 双方之间网络连接

字串9

  Java在这方面有其独特优势,Java提供了丰富网络类库支持,可以轻松编写多种类型网络通信程序。在我下面例子中我就使用了TCP/IP协议,通过JavaSocket类进行编程字串7

  2 音频信号采集和回放以及音频数字信号编码与解码

字串3

  在解决这两个问题时候,在网上很幸运地通过一些文章介绍,找到了Answer Machine 演示程序源代码(由of jsresources.orgFlorian Bomers 和Matthias Pfisterer编写,网址http://www.jsresources.org/apps/am.html)。在这个程序代码中,有几个解决我们问题所需要类,而且作者将这些类封装,我们基本不需要做什么改动,只需要屏蔽其中调试信息输出就行了,更可贵是它还封装了几种常见音频格式。其中GSM格式(Global System for Mobile Telecommunications)就是我们下面例子中采用压缩格式,GSM格式可以将128kbps 音频数据流 (16bit通过8k Hz音频采样) 压缩为13kbps 音频数据流,非常适合语音信号传送,所以可谓是一石二鸟。

字串3

  我分析过这几个类源代码,不得不佩服它作者,每个类源代码都很精炼,大家可以自己分析一下。了下面就给大家讲讲这几个类,并且将它们用到Java Sound API中类和函数等一并做个简单介绍,让大家对Java Sound API中常用类也有个大致了解。由于Java Sound API中类比较多。限于篇幅无法对所有用到类做详尽解释,以下内容只是简单提及了各个类用途和使用规范,有关Java Sound API中类具体介绍请大家访问这里http://java.sun.com/j2se/1.4.2/docs/api/, 查找javax.sound.sampled相关内容。 字串2

  以下提到几个文件是从Answer Machine 演示程序源代码中提取出来,由于是开放源代码程序,大家在使用时候请注意相关公共协议。

字串6

  ① AMAudioFormat类(封装在AMAudioFormat.java文件中) 字串4

  AMAudioFormat类封装了CD、FM、TELEPHONE、GSM这四种质量音频格式参数,使用起来也非常简单,这样我们在使用Java Sound API时就不用自己去写那些复杂代码了,但为了明白Java Sound API原理,我们需要对它代码做一下分析。它使用了Java Sound API中AudioFormat这个类,这个类非常重要,在Java中对任何音频数据使用都要实现通过它指定所需要使用音频格式,AudioFormat类有一个嵌套类AudioFormat.Encoding,实际上大部分对AudioFormat类使用都是使用这个嵌套类。 字串6

  AMAudioFormat类重要方法:

字串4

  名称:getLineAudioFormat

字串3

  调用格式:getLineAudioFormat(整型音频格式代号) 字串7

  返回值: 根据传递音频格式代号生成AudioFormat对象。 字串2

  说道这里大家可能要问了,那么通过Java Sound API可以直接使用GSM格式吗?答案是比较复杂,但同样有解决办法,作者在这里使用了另外开源程序类库-tritonusGSM编码解码库。大家需要在这里www.tritonus.org/plugins.html下载tritonous_share.jar和tritonus_gsm.jar两个文件,并在AMAudioFormat类中引用,这样就完成了GSM格式设置。需要告诉大家是在对AMAudioFormat.java这个类进行编译后,我们程序运行时候就可以不需要tritonous_share.jar和tritonus_gsm.jar这两个文件支持了。 字串3

  ② AudioCapture类(封装在AudioCapture.java文件中) 字串9

  AudioCapture类封装了从音频硬件捕获音频数据并自动编码为GSM音频压缩数据过程,并且通过它getAudioInputStream()方法提供给我们一个音频数据输入流,我们就可以直接将这个流发送到网络中。 字串8

  AudioCapture 类重要方法:

字串3

  名称:getAudioInputStream

字串2

  调用格式:getAudioInputStream()

字串6

  返回值:AudioInputStream对象

字串3

  AudioCapture 类使用了Java Sound API中AudioInputStream、AudioFormat、AudioSystem这几个类和TargetDataLine、LineListener接口。除了AudioFormat类我再简单介绍一下其他类: 字串1

  AudioInputStream 类是带有特殊音频格式和长度InputStream类,它有两个构造方法,分别是AudioInputStream(InputStream stream, AudioFormat format,long length)和AudioInputStream(TargetData -Line line)。 字串8

  TargetDataLine 接口是DataLine接口一种,通过它就可以直接从音频硬件获取数据了,它有几个常用方法,分别是:open(AudioFormat format)、void open(AudioFormat format, int bufferSize)、int read(byte[] b, int off, int len)。 字串2

  AudioSystem 类是Java标准音频系统入口点,在AudioSystem 类中使用他getLine() 方法创建TargetDataLine对象。 字串9

  LineListener接口用来对线路状态改变时间进行监听,他重要方法是update(LineEvent event)方法。

字串9

  ③ AudioPlayStream类(封装在AudioPlayStream.java文件中) 字串1

  AudioPlayStream类与AudioCapture类刚相反,它封装了GSM压缩音频数据解码和音频信号回放过程,提供给我们一个音频信号输出流。AudioCapture类用到Java Sound API中类它也基本都用到了,只是它使用了SourceDataLine接口而不是TargetDataLine接口

字串4

  ④ Debug类(封装在Debug.java文件中) 字串4

  Debug类主要用来在调试时输出讯息,代码很少,后来我把其中输出信息语句都屏蔽了,对程序运行没有影响。

字串7

  为了方便使用以上几个类,我们需要对它们进行编译和打包,编译时需要设置相关编译环境,以下是我们需要用到命令行 字串5

  set CLASSPATH=%CLASSPATH%;.;tritonus_gsm.jar;tritonus_share.jar

字串4

  javac am*.java amaudio*.java 字串5

  jar cmf packagingmanifest.mf am.jar am*.class 字串5

  amaudio*.class

字串6

  说明一下,我将以上提到Java源码文件放在了am目录下,编译之后可以得到一个8kam.jar文件,我们下一步所需要做就是在我们程序中引用这个包。

字串7

  四、实例介绍

字串9

  有了以上基本介绍,我就可以通过对我写一个极为简单语音对讲软件代码解释让大家更清楚地了解一下这几个模块具体使用方法,大家可以从中获得开发具有诸如网络电话、自动应答等功能软件类似方法,用于语音数据传输。 字串3

  程序结构: 字串5

  整个程序分三层,作用分别如下:

字串4

  . 顶层: 用户界面 字串1

  . 中间层: 控制层

字串3

  . 底层: 传输层 字串8

  程序有两个主要类: (表)

字串7

类名描述
CallLink网络传输层,用于接收或发送音频数据。
VoiceSender作为第二个启动线程提供从音频硬件捕获并编码数据给网络传输层。

  程序主类jphone使用了Runnable和ActionListener接口,主类除了基本几个方法之外,还具有方法initAudioHardware()、ShowMSG、startPhone分别用于初始化AudioCapture类与AudioPlayStream类、显示程序状态和开始程序。主类jphone具有两个子类VoiceSender和CallLink。 字串6

  子类VoiceSender同样使用了Runnable接口,它在程序中作为第二个启动线程负责发送捕获到音频数据。CallLink子类就是负责建立scoket连接,并且负责接收或发送网络数据、监听网络连接等功能实现。它具有主要方法是getInputStream()、getOutputStream()、listen()、open()、close()等。

字串2

  为了让大家更清楚了解程序结构请大家看下面类图。 字串6

   字串3

  程序基本工作流程:

字串9

  当程序启动时首先执行建立当前主类实例,当按下呼叫按钮时候执行startPhone()方法,startPhone()方法通过调用initAudioHardware()方法建立AudioCapture对象和AudioPlayStream对象实例PhoneMIC和PhoneSPK, 紧接着在建立CallLink子类实例curCallLink来与具有目标IP地址计算机进行scoket连接后,startPhone()方法又将子类VoiceSender作为secondThread线程启动,然后又调用run()方法。 run()方法通过已经建立CallLink子类实例curCallLink监听网络上数据(也就是等待别人呼叫),一旦有音频数据到来curCallLink 实例就为AudioPlayStream 对象PhoneSPK 提供网络传来音频数据,而PhoneSPK在一个循环中不断将音频数据转换为音频信号,完成类似电话听筒功能。

字串9

  子类VoiceSender 就作为第二线程启动时候,startPhone() 方法传递给它参数是实例化CallLink 子类curCallLink , 子类VoiceSender 通过实例化AudioCapture 对象PhoneMIC 将音频信号压缩成GSM数据,并通过curCallLink 将音频数据发送到具有目标IP 地址计算机上,完成类似电话受话器功能。

字串5

  在这里实例化CallLink 子类curCallLink 就相当于两个电话之间电话线,这样通过我以上解释大家对程序原理就有一个大概了解了吧。 字串3

  其中音频数据发送线程和音频数据接收线程是同步,不过考虑到网络因素,可能在声音传输上有一些延迟,不过由于延迟比较小对及时听到对方话语影响不大。

字串9

   字串4

  编写代码摘要: 字串2

  在使用AudioCapture 类和AudioPlayStream 类方法之前需要知道怎样初始化这两个类。在声明AudioCapture 对象时候需要传递给它一个静态整型值用于表达将音频信号压缩方式,这个静态整型常量可以是AMAudioFormat 类以下四个值之一: FORMAT_CODE_CD 、FORMAT_CODE_FM 、FORMAT

字串1

  _CODE_TELEPHONE 、FORMAT_CODE_GSM 。所以声明AudioCapture 对象就要用一下形式: 字串1

private AudioCapture PhoneMIC null;
PhoneMIC new AudioCapture(AMAudioFormat.
FORMAT_CODE_GSM);

  而声明AudioPlayStream 对象则不同,我们在初始化它时候需要传递给它一个AudioFormat 对象,用于通知它我们所要播放音频格式,这个AudioFormat 对象可以通过AMAudioFormat 类getLineAudioFormat(格式参数值)方法获得,其中格式参数取值和上面提到过AMAudioFormat 四个值相同,所以声明AudioPlayStream 对象就要用以下形式: 字串9

private AudioPlayStream PhoneSPK null;
AudioFormat format null;
format AMAudioFormat.getLineAudioFormat
(AMAudioFormat.FORMAT_CODE_GSM);
PhoneSPK new AudioPlayStream(format);

  在这之后就可以使用AudioCapture 和AudioPlayStream 对象open() 方法打开音频捕获和音频回放通道完成它们初始化了。如以下形式:

字串5

PhoneMIC.open();
PhoneSPK.open();

  初始化完成之后要使AudioPlayStream 对象播放声音还需要以下过程,首先建立一个缓冲区(字节数组)用于存放从网络传来音频数据流,然后执行AudioPlayStream 对象start() 方法使AudioPlayStream

字串5

  对象开始声音回放,这时执行一个while 循环,在循环中将音频流数据写入缓冲区,再使用AudioPlayStream对象write()方法将缓冲区数据还原成语音信号然后播放出来。如下面例子: 字串9

boolean complete false;
byte[] gsmdata new byte[bufSize];
int numBytesRead 0;
......
PhoneSPK.start();
......
complete false;
while((!Thread.currentThread().interrupted()) )
{
 try
 {
  numBytesReadplaybackInputStream.read(gsmdata);
  if(numBytesRead == -1)
  {
   complete=true;
   break;
  }
  PhoneSPK.write(gsmdata, 0, numBytesRead);
 }
 catch (IOException e)
 {
  System.exit(1);
 }
}

  其中complete 值用于标志终止声音播放异常原因。

字串2

  类似,初始化完成之后要使AudioCapture 对象捕获和压缩声音数据还需要其他操作,首先声明一个InputStream 对象,赋其值为AudioCapture 对象getAudioInputStream() 方法返回值,执行

字串4

  AudioCapture 对象start() 方法,然后在建立一个循环,将通过InputStream read() 方法得到数据发送到网络上。例如以下代码: 字串8

  InputStream myIStream null;
myIStream PhoneMIC.getAudioInputStream();
......
while((!Thread.currentThread().interrupted()))
b = myIStream.read(compressedVoice,0, bufSize);
sendStream.write(compressedVoice,0,b);
......

  通过使用CallLink 几个方法,我们可以方便传输和接收音频数据流。以下是它代码: 字串2

class CallLink
//使用套接字进行连接
String ipAddr null;
Socket outSock = null;
ServerSocket inServSock null;
Socket inSock null;
CallLink(String inIP)
ipAddr inIP;
void open() throws IOException, UnknownHostException
{//打开网路连接
if (ipAddr != null)
outSock=new Socket(ipAddr,TALK_PORT);
}
void listen() throws IOException
{// 监听,等候呼叫
inServSock new ServerSocket(TALK_PORT);
inSock inServSock.accept();
}
public InputStream getInputStream()throws IOException
{//返回音频数据输入流
if (inSock != null)
return inSock.getInputStream();
else
return null;
}
publicOutputStreamgetOutputStream()throwsIOException
{//返回音频数据输出流
if (outSock != null)
return outSock.getOutputStream();
else
return null;
}
void close() throws IOException
{//关闭网络连接
inSock.close();
outSock.close();
}

  程序代码总体有366 行,限于篇幅,这里就不一一列举了。

字串4

  编译以及程序使用方法: 字串1

  运行和编译本程序需要加上额外环境参数,为了方便使用可以建立以下内容批处理文件:(假设程序所需要包均在程序所在目录下lib 文件夹中) 字串7

  用于编译批处理程序c.bat 内容

字串3

  javac -classpath .;libam.jar jphone.java

字串2

  用于运行批处理程序r.bat 内容 字串1

  java -classpath .;libam.jar jphone

字串9

  启动时在A 计算机IP 地址框内输入要进行连接计算机B IP 地址,在计算机B IP 地址框内输入要进行连接计算机A IP 地址,让后分别点击“拨出电话”按钮就可以进行连接了。当然别忘了接上麦克风和打开音箱电源,呵呵。 字串9

  提醒大家,这里IP 地址栏里预先存在地址是127.0.0.1,也就是说,程序也可以和自己进行连接,点击“拨出电话”按钮,等8 秒左右敲敲你麦克风,听到没有,是不是也有“嘣、嘣、嘣”声音?

字串6

最新评论共有 0 位网友发表了评论
发表评论
评论内容:不能超过250字,需审核,请自觉遵守互联网相关政策法规。
用户名: 密码:
匿名?
注册
相关文章