17.3.1JavaSound体系结构使用JavaSoundAPI,可以实现各种基于声音的应用,例如声音录制、音乐播放、网络电话、音乐编辑等。JavaSoundAPI又以各种解码和合成器SPI(ServiceProviderInterface,服务提供者接口)为基础,实现各种音乐格式的解码与转码。它们之间的关系如图17-5所示。SPI的作用是以插件(Plug-In)的形式提供自定义的扩展模块,我们只要提供与SPI兼容的插件扩展模块,就可以在不改变API的情况下扩展音频处理程序的能力。例如,假设有一个只能播放WAV文件的程序,我们只要增加一个支持MP3文件解码的插件模块,就可以在不改动播放程序的任何一行代码的前提下,为这个播放程序添加播放MP3的能力。在后文的MP3音乐播放器程序中我们将演示该模块的安装与使用。JavaSoundAPI包含在javax.sound.sampled和javax.sound.midi包中,分别用以处理数字音频simpled-audio和MIDI。SPI包含在java.sound.sampled.spi和javax.sound.midi.spi包中,提供了第三方的扩展接口。17.3.2音频输入/输出原理音频的输入和输出需要分别使用类TargetDataLine和SourceDataLine,分别代表了输入和输出的设备,它们都实现了Line接口。Line接口用来关闭/打开设备、注册事件监听器,以及提供一些用来调整声音效果的对象,例如调整音量大小的对象。AudioSystem在JavaSound体系中起着一个工厂(Factory)类的作用,提供了一系列的静态方法,我们通过这些静态方法来获取JavaSound系统默认配置的资源。它们之间的关系如图17-6所示。在处理输入音频时,对于来自各种音频输入端口的信号,例如麦克风、CD播放器、磁带播放器等,可以在它们到达TargetDataLine之前,利用混频器控制输入混频,最后在程序中通过TargetDataLine获得数字化的音频输入流。类似地,在处理输出音频时,混频器用来对一系列来自SourceDataLine的数据进行混频处理,经处理后的信号可输出到各种输出端口,例如扬声器、耳机等。SourceDataLine是一个可写入音频信号数字流的设备,例如,我们可以从一个WAV文件读取内容写入到SourceDataLine,然后再通过扬声器输出。其流程图如图17-7所示。图17-6类关系图图17-7音频输入输入流程图输入到混频器的信号也可以来源于剪辑(Clip)。剪辑是一个包含一段完整音频数据流的设备,或者说,剪辑就是一个缓存在内存中的完整音频数据流。在一些要求反复播放音乐片段的场合,例如游戏的背景音乐,剪辑是很有用的。17.3.3音频的数据格式音频数据——也就是从TargetDataLine输入或从SourceDataLine输出的数据,必须符合音频格式的标准。音频数据的格式选项由AudioFormat类封装,主要选项包括:编码方式(可以是PCM(PulseCodeModulation,脉冲编码调制)、MP3等)、通道数量、取样率、帧速率等。根据不同的参数,AudioFormat提供了3个构造函数publicAudioFormat(AudioFormat.Encodingencoding,floatsampleRate,intsampleSizeInBits,intchannels,intframeSize,floatframeRate,booleanbigEndian);publicAudioFormat(AudioFormat.Encodingencoding,floatsampleRate,intsampleSizeInBits,intchannels,intframeSize,floatframeRate,booleanbigEndian,MapString,Objectproperties);publicAudioFormat(floatsampleRate,intsampleSizeInBits,intchannels,booleansigned,booleanbigEndian);其中的变量意义如下。Øencoding:音频编码。音频编码的常见类型是脉冲编码调制(PCM),它只是声音波形的线性(比例)表示形式。有了PCM,每个样本中存储的数字都与该时间点上的声压瞬时振幅成比例。这些数字通常是有符号的或无符号的整数。除了PCM外,其他编码还有mu-law和a-law,它们是常用于记录语音的声音振幅的非线性映射。取得编码类型的类为AudioFormat.Encoding,该类包含了4个静态变量的类型:staticAudioFormat.EncodingALAW;//指定a-law编码数据staticAudioFormat.EncodingPCM_SIGNED;//指定有符号的线性PCM数据staticAudioFormat.EncodingPCM_UNSIGNED;//指定无符号的线性PCM数据staticAudioFormat.EncodingULAW;//指定u-law编码数据如果不指定该变量,则默认使用线性PCM编码。ØsampleRate:每秒样本数。即取样率,表示每一秒钟取样的频率,可选值有8000、11025、16000、22050、44100。比如对于8000,表示每一秒钟会取样8000次,也就是采集8000次声音。ØsampleSizeInBits:每个样本中的位数。可以为8bit和16bit,即每一个声音样本使用8bit或16bit数据表示。Øchannels:声道数(单声道为1,立体声为2……)。ØframeSize:每帧包含的字节数。ØframeRate:每秒帧数。ØbigEndian:指是否以big-endian字节顺序存储数据(false意味着little-endian)。Øproperties:包含格式属性的MapString,Object对象。例如下例所示,使用第3个构造函数创建了一个音频格式对象:floatsampleRate=16000.0F;//8000,11025,16000,22050,44100intsampleSizeInBits=16;//8,16intchannels=1;//1,2booleansigned=true;//true,falsebooleanbigEndian=false;//true,falseAudioFormataudioFormat=newAudioFormat(sampleRate,sampleSizeInBits,channels,signed,bigEndian);17.3.4音频的录制创建了音频格式对象后,就可以使用该格式进行录音了。音频的录制需要经过以下的6步。(1)取得输入设备信息。创建了音频数据格式对象后,就可以根据该对象取得输入设备信息DataLine.Info:DataLine.InfodataLineInfo=newDataLine.Info(TargetDataLine.class,audioFormat);(2)取得输入设备。根据该设备信息,使用AudioSystem的getLine()方法取得输入设备对象:TargetDataLinetargetDataLine=(TargetDataLine)AudioSystem.getLine(dataLineInfo);(3)打开输入设备。按照指定的音频格式打开该设备targetDataLine:targetDataLine.open(audioFormat);(4)开始录音。启动该设备,即可开始录音:targetDataLine.start();(5)读取录音数据。可以循环调用该录音设备的读取函数读取录音数据到tempBuffer数组中:bytetempBuffer[]=newbyte[10000];intcnt=targetDataLine.read(tempBuffer,0,tempBuffer.length);(6)保存录音数据。每一次读取的数据都会临时保存在数据tempBuffer中,然后可以将数组加入到一个缓存数组byteArrayOutputStream中保存起来:ByteArrayOutputStreambyteArrayOutputStream=newByteArrayOutputStream();byteArrayOutputStream.write(tempBuffer,0,cnt);以上的循环读取过程需要使用一个while(true)循环,表示不停的录音,整合后的过程如下所示://循环录音try{bytetempBuffer[]=newbyte[10000];while(true){//读取10000个数据intcnt=targetDataLine.read(tempBuffer,0,tempBuffer.length);if(cnt0){//保存该数据byteArrayOutputStream.write(tempBuffer,0,cnt);}}byteArrayOutputStream.close();}catch(Exceptione){e.printStackTrace();}这样,每一次读取的录音数据就会保存在byteArrayOutputStream对象中了,供后期的播放和保存使用。17.3.5音频的播放以上的录音数据保存在byteArrayOutputStream缓存对象中,接下来就可以播放该对象中的数据了。与录制的过程相对应,播放的过程需要如下的6步。(1)取得输出设备信息。使用录音时相同的音频格式取得输出设备信息DataLine.Info:DataLine.InfodataLineInfo=newDataLine.Info(SourceDataLine.class,audioFormat);(2)取得输出设备。根据输出设备信息取得输出设备对象SourceDataLine:SourceDataLinesourceDataLine=(SourceDataLine)AudioSystem.getLine(dataLineInfo);(3)打开输出设备。按照指定的音频格式打开该设备:sourceDataLine.open(audioFormat);(4)开始播放。启动该设备,即可开始播放录音:sourceDataLine.start();(5)取得录音数据。从录音数据缓存byteArrayOutputStream中取得录音数据,并转换成输入流audioInputStream:byteaudioData[]=byteArrayOutputStream.toByteArray();InputStreambyteArrayInputStream=newByteArrayInputStream(audioData);AudioInputStreamaudioInputStream=newAudioInputStream(byteArrayInputStream,audioFormat,audioData.length/audioFormat.getFrameSize());(6)播放录音数据。循环读取audioInputStream输入源中的音频数据,对于读取到的数据tempBuffer,直接写入sourceDataLine对象即可播出声音了。循环结构的过程如下所示:try{intcnt;//读取数据到缓存数据while((cnt=audioInputStream.read(tempBuffer,0,tempBuffer.length))!=-1){if(cnt0){//播放缓存数据sourceDataLine.write(tempBuffer,0,cnt);}}//Block等待临时数据被输出为空sourceDataLine.drain();sourceData