桌面pc的性能日益提高,java虚拟机的优化技术也不断获得突破,这一切使得用java处理实时信号成为可能。本文将通过设计和构造一个支持实时mp3、wav和ogg音频格式解码/回放的java音乐播放器,阐述用javasound api编写音频处理程序的思路和一般过程。
javasound是一个小巧的低层api,支持数字音频和midi数据的记录/回放。在jdk 1.3.0之前,javasound是一个标准的java扩展api,但从java 2的1.3.0版开始,javasound就被包含到jdk之中。由于java有着跨平台(操作系统、硬件平台)的特点,基于javasound的音频处理程序(包括本文的程序)能够在任何实现了java 1.3+的系统上运行,无需加装任何支持软件。
一、javasound的体系结构
当前jdk的javasound api随同java媒体框架(jmf,java media framework)一起发布,主页在java.sun.com/products/java-media/jmf/,适合jdk 1.1以及更高的版本。除了jdk实现的javasound api之外,还有一个源代码开放的javasound实现是tritonus,主页在http://www.tritonus.org/。
图一描述了javasound api的体系结构,虚线表示sun的javasound标准定义的api调用。上面一根虚线表示我们编写音频处理程序要调用的api,javasound api包含在javax.sound.sampled和javax.sound.midi包中。两根虚线之间的部分就是javasound api的具体实现。

图一:javasound体系结构
就象上面一根虚线表示的api具有统一标准一样,在所有的javasound实现中,图一下面一根虚线表示的spi(服务提供者接口, service provider interface)也是统一的。spi的作用是以插件(plug-in)的形式提供自定义的扩展模块,我们只要提供与spi兼容的插件扩展模块,就可以在不改变api的情况下扩展音频处理程序的能力。spi包含在java.sound.sampled.spi和javax.sound.midi.spi包中。
例如,假设有一个只能播放wav文件的程序,我们只要增加一个支持mp3文件解码的插件模块,就可以在不改动播放程序的任何一行代码的前提下,为这个播放程序添加播放mp3的能力。
二、javasound混频原理图二阐述了javasound的混频器原理。在处理输入音频的应用中,对于来自各种音频输入端口的信号,例如麦克风、cd播放器、磁带播放器,等等,我们可以在它们到达targetdataline之前,利用混频器控制输入混频,最后在程序中通过targetdataline获得数字化的音频输入流。

图二:javasound混频器
类似地,在处理输出音频的应用中,混频器用来对一系列来自sourcedataline的数据进行混频处理,经处理后的信号可输出到各种输出端口,例如扬声器、耳机等。sourcedataline是一个可写入音频信号数字流的设备,例如,我们可以从一个wav文件读取内容写入到sourcedataline,然后再通过扬声器输出。
输入到混频器的信号可以来源于剪辑。剪辑(clip)是一个包含一段完整音频数据流的设备,或者说,剪辑就是一个缓冲在内存中的完整音频数据流。在一些要求反复播放音乐片段的场合,例如游戏的背景音乐,剪辑是很有用的。
图三描述了javasound api中一些常用的类、接口及其关系,所有图三显示的类、接口都通过line这个基本接口统一起来。line接口用来关闭/打开设备、注册事件监听器,以及提供一些用来调整声音效果的对象,例如调整音量大小的对象。audiosystem在javasound体系中起着一个工厂(factory)类的作用,提供了一系列的静态方法,我们通过这些静态方法来获取javasound系统默认配置的资源(所谓静态方法,就是可以在不创建audiosystem实例的情况下直接调用的方法)。

图三:常用的javasound类
顺便说明一下,在当前(jdk 1.4)实现的javasound的默认配置中,输入声音来自本地声卡的麦克风,输出声音到本地声卡的扬声器。应当说当前实现的javasound对端口和混频器的支持还不完善,但对于包括本文音乐播放器在内的许多应用来说,默认实现的javasound配置已经足够了。
三、音频数据与存储格式
取样得到的音频数据――也就是从targetdataline输入或从sourcedataline输出的数据,必须符合音频格式的标准。音频数据的格式选项由audioformat类封装,主要选项包括:编码方式,可以是pcm(pulse code modulation,脉冲编码调制)、mp3等;通道数量;取样率;帧速率;等等。
音频数据可以用多种格式保存到磁盘上。在javasound参考实现中,直接支持的文件格式包括wav(windows)、aiff(主要用于apple的macintosh)以及au(主要用于unix),音频文件的格式由audiofileformat类指定。
并非所有音频数据格式都可以保存到任意音频文件格式(或从音频文件回放),具体由平台和操作系统的类型决定。为简单计,本文的播放器只考虑包含pcm mono或stereo数据的wav文件,这是当前流行的音频数据/文件格式组合,常用于cd音质的音频数据。压缩的音频数据(例如mp3和ogg vorbis)通常有各自特殊的存储格式(如.mp3和.ogg),通常不以wav/aiff/au格式存储。
我们要编写的音乐播放器(图四)由表一所示的几个类构成。鉴于构造用户界面往往需要大量的代码,且这些代码通常可以用ide自动生成,所以下文只对一些关键的gui元素略作介绍,不再给出完整的代码。

图四:播放器的用户界面
播放器的用户界面主要由一个带菜单的jframe框架、一个名称为filenameslist的jlist和几个jbutton构成。框架有一个私有的testbase成员,其实例在guiinit()方法的末尾通过pbase = new testbase()语句初始化。
表一 
用户界面中的按钮用类似下面的代码创建,其中addbttnicontext()是一个私有方法,它把一个图标放到按钮的文字标签之上。java程序的用户界面和windows界面风格迥异,建议读者使用java开发工具自带的图标,或者从java图标库下载(例如http://developer.java.sun.com/developer/techdocs/hi/repository/)。
| jbutton playbttn = new jbutton(); ... addbttnicontext(playbttn, "播放", "play24.gif"); playbttn.addactionlistener(new java.awt.event.actionlistener() { public void actionperformed(actionevent e) { playclick(e); } }); |
当用户点击一个按钮,与该按钮对应的xxxclick()事件句柄函数开始执行。播放器共有5个按钮,相应的事件句柄也有5个:playclick(“播放”按钮),stopclick(“停止”按钮),pauseclick(“暂停”按钮),prevclick(“后退”按钮),nextclick(“前进”按钮)。
例如,点击“播放”按钮时,playclick()句柄首先获得jlist中选中的文件,然后调用testbase实例中的playfile()辅助方法播放文件。playclick()句柄的代码如下所示,注意它把音乐文件及其所在目录连接起来的方法是操作系统中立的。
void playclick(actionevent e) { |
stopclick()和pauseclick()方法分别调用testbase中的stop()和pause()方法。prevclick()和nextclick()句柄的任务稍微复杂一点。首先,它们要调用testbase中的stop()方法中止当前的播放动作,然后选中jlist中当前项目的前一项或后一项,最后调用playclick()播放新选中的音乐文件,如下所示。
| void prevclick(actionevent e) { pbase.stop(); filenameslist.setselectedindex( filenameslist.getselectedindex() - 1); playclick(e); } void nextclick(actionevent e) { pbase.stop(); filenameslist.setselectedindex((filenameslist.getselectedindex()+1) % curplaylistlength); playclick(e); } |
五、播放音乐
testbase类包含主要的播放逻辑。例如,当用户点击“播放”按钮,testbase类中的play()方法开始执行。
| public void play() { if ((!stopped) || (paused)) return; if (playerthread == null) { playerthread = new thread(this); playerthread.start(); try { thread.sleep(500); } catch (exception ex) {} } synchronized(synch) { stopped = false; synch.notifyall(); } } |
play()方法首先确认播放器当前已被终止播放,而不是暂停播放。然后它检查这是不是第一次调用play():如果是,则创建一个playerthread线程。我们用一个独立的线程负责音乐播放,这样,无论播放器正在读取文件、解码,还是正在把音频数据输出到扬声器,用户界面总是可操作的。
启动线程之后,play()方法锁定静态synch同步对象,将stopped标记设置为false,然后通知正在等待的线程(playerthread线程在开始播放音乐文件之前,会等待静态synch对象上的提醒通知)。
playerthread线程启动后,它的run()方法开始运行。这个线程一直执行while循环,直到threadexit标记变成true为止。在while循环中,线程首先等待“开始播放”的信号(当用户点击“播放”按钮时),然后播放音乐。表二列出了描述播放器状态的各个标记及其含义。
| public void run() { while (! threadexit) { waitforsignal(); if (! stopped) playmusic(); } } |

playmusic()方法利用javasound api播放当前选中的文件。首先要通过audiosystem类获得一个audioinputstream。然后,利用audioinputstream的getformat()获知音频数据的格式。在此基础上,我们试图通过getline()方法获得一个支持该种格式的sourcedataline。如果要播放的是wav文件,现在我们已经有了非压缩的pcm格式的音频数据,可以用line对象开始播放音频。
| ais= audiosystem.getaudioinputstream(new file(filetoplay)); … if (ais != null) { baseformat = ais.getformat(); line = getline(baseformat); ... } |
如果音频数据是压缩格式的,如mp3或ogg,必须先进行一次转换――把mp3/ogg解码成pcm。解码主要包括三个步骤:
1、创建一个解压缩结果的定制audioformat(pcm编码),但保留和原压缩流一样的取样率、通道信息等。
2、创建一个audioinputstream把原来的audioinputstream转换成新的audioformat格式。
3、获得一个处理解码后格式的sourcedataline。
如下所示:
| audioformat decodedformat = new audioformat( audioformat.encoding.pcm_signed, baseformat.getsamplerate(), 16, baseformat.getchannels(), baseformat.getchannels() * 2, baseformat.getsamplerate(), false); ais = audiosystem.getaudioinputstream(decodedformat, ais); line = getline(decodedformat); |
getline()方法的返回值是一个与参数中指定的audioformat兼容的sourcedataline。如果不能获得兼容的sourcedataline,getline()返回null。在getline()方法中,我们首先创建和填充一个dataline.info结构,调用audiosystem.getline()方法,将info结构传递给audiosystem类工厂。
| private sourcedataline getline(audioformat audioformat) { sourcedataline res = null; dataline.info info = new dataline.info(sourcedataline.class, audioformat); try { res = (sourcedataline) audiosystem.getline(info); res.open(audioformat); } catch (exception e) { } return res; } |
准备好audioinputstream和sourcedataline之后,playmusic()剩余的任务已经很简单:用一个循环从audioinputstream读取数据,然后写入到sourcedataline。
| int inbytes = 0; while ((inbytes != -1) && (!stopped) && (!threadexit)) { try { inbytes = ais.read(audiodata, 0, buffer_size); } catch (ioexception e) { e.printstacktrace(); } if (inbytes >= 0) { int outbytes = line.write(audiodata, 0, inbytes); } if (paused) waitforsignal(); } |
六、支持更多的音频格式
假设已经在test目录下准备好了所有的.java文件,执行javac *.java即可顺利编译,执行java test.testplayer就可以启动图一的播放器。但现在播放器只能播放有限的文件,因为jdk实现的javasound只支持wav、aiff和au。但是,我们可以用javasound spi为播放器增加对mp3和ogg vorbis的支持,只要下载和安装相应的插件jar文件即可。
java版的vorbis解码器可以从javacraft(http://www.jcraft.com/jorbis/)下载,最新版本是0.0.12。另外还要有一个jorbis解码器的spi封装器,这是使解码器在javasound下透明地运行所必需的,可以从http://www.javazoom.net/vorbisspi/vorbisspi.html下载。vorbisspi的最新版本是0.7。
对于mp3支持,javazoom也提供了一个兼容javasound的纯java解码器,称为javalayer(http://www.javazoom.net/javalayer/javalayer.html),最新的版本是0.2.0。注意要下载的是javalayer的j2se版,不要下载j2me版。
解开下载得到的文件,把所有jar文件放到播放器所在目录。用下面的命令启动播放器:java -classpath .;./jogg-0.0.5.jar;./jorbis-0.0.12.jar;./jl020.jar;./mp3sp.jar;./vorbisspi0.6.jar test.testplayer。如果你下载的解码器版本不同,启动命令也要作相应地改动。把spi扩展插件加入到了播放器的classpath之后,javasound就会在运行时自动使用它们。
闽公网安备 35060202000074号