一、引言
Java是一门适合于分布式计算环境、尤其是Internet程序设计
语言。这不仅仅在于java具有很
安全性和可移植性,还在于java为Internet编程提供了丰富
网络类库
支持。利用这些网络类库,可以轻松编写多种类型
网络通信程序。然而由于某些限制,Java在传输多媒体信息方面
应用不是很广,大部分
应用都集中在网络上传输语音等音频信号
方面。传输音频信号应用方案一般有两种,一是应用于数据广播
多对一传输,例如音频数据服务器向数个客户端发送音频数据信号,其最广泛
应用则是某些网上
IP电话,大家经常可以看到不少这种提供在线IP电话服务
网站
客户端都是使用
嵌在网页上
Java Applet程序,用来实现拨号、通话等等基本
网络电话功能; 第二种方案则是我们今天要涉及
部分,一对一
音频信号数据
传输。这种方案
应用范围更广。大家都去过语音聊天室,大部分
语音聊天室
语音聊天功能
实现就是使用
Java技术,大家对这样网页
源代码分析一下就可以发现这一点。
字串6
我曾开发一个项目,涉及使用java来实现在网络上传输语音数据。开发中遇到不少问题,而且在互联网上发现关于java语音传输
资料比较少,寻找了许多天,最终从一个开放源代码
一个简单
Answer Machine 演示程序中获得了解决问题
方法。今天我就把我在点对点传输音频信号方面
一些经验拿出来,与大家共同探讨这方面
问题。
二、存在
问题 字串4
在网络上传输音频
方面存在
问题主要可以归纳为以下几点: 字串4
1 双方之间
网络连接 字串1
要进行频数据
传输,首先就是要建立数据连结。常用
通讯协议中,TCP较可靠,所以用在不允许数据丢失
应用上。而UDP则较多应用于处理速度要求较快、数据传输可靠性要求不是很高
应用上,如数据广播。通信协议
选择取决于我们所要做
应用
类型。怎样建立网络连接,稳定
接收和发送音频信号
数据流是关键。
字串8
2 音频信号
采集以及回放
在进行音频信号
采集中我们必须考虑到采样率
问题,声音信号
采样率有8Khz、16Khz、32Khz、44Khz等,每种数据采样虑产生
数据量都不一样,越高
采样率产生
数据量越大,所以我们要选择合适
采样率以适应网络
带宽。
字串5
3 音频数字信号
编码与解码。
如果把直接采集到
音频信号数据流在网络上进行传输,它所占有
带宽也是十分大
,以8Khz
采样率采集14位
音频数据那么就有以下这样
一个式子:
14 bit * 8000/second=112,000 bits/second or112kbps 字串9
从中我们可以看出以这样
方式传输音频数据,每秒需要向网络中发送112kb
数据。所以。从节省带宽
角度考虑,我们很有必要对这样
数据进行压缩。对多媒体信号
压缩我们有许多可以选择
格式,如mp2、mp3、GSM等等。同样,我们这里也存在一个对压缩格式进行选择
问题,考虑到音频数据传输
及时性,对传输
音频数据质量
要求,以及各种压缩格式
压缩比率以及进行压缩和解压缩所要耗费
系统资源等方面问题,选择合适
压缩格式就显得尤为重要。
字串3
三、解决
方法
字串2
下面就针对前面提出
问题讨论一下解决
办法。 字串2
1 双方之间
网络连接
字串9
Java在这方面有其独特
优势,Java提供了丰富
网络类库
支持,可以轻松编写多种类型
网络通信程序。在我下面
例子中我就使用了TCP/IP协议,通过Java
Socket类进行编程。 字串7
2 音频信号
采集和回放以及音频数字信号
编码与解码
在解决这两个问题
时候,在网上很幸运地通过一些文章
介绍,找到了Answer Machine 演示程序
源代码(由of jsresources.org
Florian Bomers 和Matthias Pfisterer编写,网址http://www.jsresources.org/apps/am.html)。在这个程序代码中,有几个解决我们问题所需要
类,而且作者将这些类封装
很
,我们基本不需要做什么改动,只需要屏蔽其中
调试信息
输出就行了,更可贵
是它还封装了几种常见
音频格式。其中
GSM格式(Global System for Mobile Telecommunications)就是我们下面例子中采用
压缩格式,GSM格式可以将128kbps
音频数据流 (16bit通过8k Hz
音频采样) 压缩为13kbps
音频数据流,非常适合语音信号
传送,所以可谓是一石二鸟。
我分析过这几个类
源代码,不得不佩服它
作者,每个类
源代码都很精炼,大家可以自己分析一下。
了下面就给大家讲讲这几个类,并且将它们用到
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类
重要方法:
名称:getLineAudioFormat
调用格式:getLineAudioFormat(整型音频格式代号) 字串7
返回值: 根据传递音频格式代号生成
AudioFormat对象。 字串2
说道这里大家可能要问了,那么通过Java Sound API可以直接使用GSM格式吗?答案是比较复杂,但同样有解决
办法,作者在这里使用了另外
开源程序
类库-tritonus
GSM编码解码库。大家需要在这里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
调用格式:getAudioInputStream()
返回值: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
javac am*.java amaudio*.java 字串5
jar cmf packagingmanifest.mf am.jar am*.class 字串5
amaudio*.class
字串6
说明一下,我将以上提到
Java源码文件放在了am目录下,编译之后可以得到一个8k
am.jar文件,我们下一步所需要做
就是在我们
程序中引用这个包。
四、实例介绍
有了以上
基本
介绍,我就可以通过对我写
一个极为简单
语音对讲软件代码
解释让大家更清楚地了解一下这几个模块
具体使用方法,大家可以从中获得开发具有诸如网络电话、自动应答等功能
软件
类似方法,用于语音数据
传输。 字串3
程序
结构: 字串5
整个程序分三层,作用分别如下:
. 顶层: 用户界面 字串1
. 中间层: 控制层
字串3
. 底层: 传输层 字串8
程序有两个主要
类: (表)
| 类名 | 描述 |
| CallLink | 网络传输层,用于接收或发送音频数据。 |
| VoiceSender | 作为第二个启动 线程提供从音频硬件捕获并编码![]() 数据给网络传输层。 |
程序
主类jphone使用了Runnable和ActionListener接口,主类除了基本
几个方法之外,还具有方法initAudioHardware()、ShowMSG、startPhone分别用于初始化AudioCapture类与AudioPlayStream类、显示程序状态和开始程序。主类jphone具有两个子类VoiceSender和CallLink。 字串6
子类VoiceSender同样使用了Runnable接口,它在程序中作为第二个启动
线程负责发送捕获到
音频数据。CallLink子类就是负责建立scoket连接,并且负责接收或发送网络数据、监听网络连接等功能
实现。它具有主要
方法是getInputStream()、getOutputStream()、listen()、open()、close()等。
为了让大家更清楚
了解程序
结构请大家看下面
类图。 字串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在一个循环中不断
将音频数据转换为音频信号,完成类似电话听筒
功能。
子类VoiceSender 就作为第二线程启动
时候,startPhone() 方法传递给它
参数是实例化
CallLink 子类curCallLink , 子类VoiceSender 通过实例化
AudioCapture 对象PhoneMIC 将音频信号压缩成GSM数据,并通过curCallLink 将音频数据发送到具有目标IP 地址
计算机上,完成类似电话受话器
功能。
在这里实例化
CallLink 子类curCallLink 就相当于两个电话之间
电话线,这样通过我以上
解释大家对程序
原理就有一个大概
了解了吧。 字串3
其中
音频数据发送线程和音频数据接收线程是同步
,不过考虑到网络
因素,可能在声音
传输上有一些延迟,不过由于延迟比较小对及时听到对方
话语影响不大。
字串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() 方法打开音频捕获和音频回放通道完成它们
初始化了。如以下
形式:
PhoneMIC.open();
PhoneSPK.open(); 初始化完成之后要使AudioPlayStream 对象播放声音还需要以下过程,首先建立一个缓冲区(字节数组)用于存放从网络传来
音频数据流,然后执行AudioPlayStream 对象
start() 方法使AudioPlayStream
对象开始声音
回放,这时执行一个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() 方法
返回值,执行
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 秒左右敲敲你
麦克风,听到没有,是不是也有“嘣、嘣、嘣”
声音?
![我要研发网[www.51dev.com]](/templets/images/toplogo.gif)
