【Java Sound】(三)播放音频

  • Java Sound
  • (三)播放音频
  • 使用Clip
  • 使用SourceDataLine
  • 监控线路状态
  • 同步多线路播放
  • 处理输出音频


Java Sound

摘自:The Java™ Tutorials,翻译为机翻+少量修正

(三)播放音频

回放(playback)有时称为演示(presentation)或渲染(rendering)。这些是通用术语,也适用于音频以外的其他类型的媒体。基本特征是将数据序列传递到某个地方,以供用户最终感知。如果数据是基于时间的(如音频一样),则必须以正确的速率传送。音频甚至比视频要多,因此保持数据流的速率非常重要,因为声音播放的中断通常会产生很大的喀哒声(clicks)或令人讨厌的失真。Java Sound API旨在帮助应用程序平稳连续播放音频,即使是很长的音频。

之前,您了解了如何从音频系统或调音台获得线路。在这里,您将学习如何通过一条线播放声音。

如您所知,可以使用两种线路来播放声音:Clip和SourceDataLine。两者之间的主要区别在于,使用Clip可以一次指定所有声音数据,然后再播放,而使用SourceDataLine可以在播放过程中连续写入新的数据缓冲。尽管在很多情况下都可以在ClipSourceDataLine之间随意选用,但以下条件有助于确定哪种线更适合特定情况:

  • 使用Clip时,你有非实时声音数据可以预先加载到内存中。
    例如,您可能将短音频文件读入clip。如果您想播放音频不止一次,则ClipSourceDataLine更方便,尤其是当您要播放循环(重复播放全部或部分声音)时。如果您需要在声音中的任意位置开始播放,则Clip接口提供了一种轻松进行播放的方法。最后,从Clip播放的延迟通常比从SourceDataLine播放的缓冲的延迟少。换句话说,由于声音已预先加载到clip中,因此可以立即开始播放,而不必等待缓冲区被填满。
  • SourceDataLine用作流数据,例如无法一次放入内存的长声音文件,或者在播放之前无法得知其数据的声音。
    作为后一种情况的示例,假设您正在监视声音输入——即在捕获声音时播放声音。如果您没有可以直接从输出端口发送回输入音频的混音器,则您的应用程序将必须获取捕获的数据并将其发送到音频输出混音器。在这种情况下,SourceDataLineClip更合适。当您响应用户的输入以交互方式合成或操纵声音数据时,会发生另一个无法预先知道的声音示例。例如,想象一个游戏,当用户移动鼠标时,通过将声音从一种声音“变形”到另一种声音来给出听觉反馈。声音转换的动态性质要求应用程序在播放期间不断更新声音数据,而不是在播放开始前一次性提供到位。

使用Clip

您可以如前面在《获取所需类型的行》中所述获得Clip;为第一个参数构造一个DataLine.Info对象Clip.class,并将DataLine.Info作为参数传递给AudioSystemMixergetLine方法。

获得一条线只是意味着您已经有了一种引用它的方式。getLine实际上并没有为您预留线路。由于混音器可能只有有限数量的所需类型的线路,因此可能发生在调用getLine获取clip之后,正准备播放音频时,另一个应用程序会插一脚进来并抢走clip。要实际使用clip,您需要通过调用以下Clip方法之一,将其保留供程序专用:

void open(AudioInputStream stream)
void open(AudioFormat format, byte[] data, int offset, int bufferSize)

尽管上面第二种open方法中有参数bufferSizeClip(与SourceDataLine不同)并没有用于将新数据写入缓冲区的方法。这里的bufferSize参数仅指定要加载到clip中的字节数组的数量。它不像使用SourceDataLine时的缓冲区那样,让您可以将更多数据加载到其中。

打开clip后,您可以使用ClipsetFramePositionsetMicroSecondPosition方法指定应在数据中的哪一点开始播放。否则,它将从头开始。您也可以使用setLoopPoints方法将播放设置为重复循环。

准备开始播放时,只需调用该start方法。要停止或暂停clip,请调用stop方法,如果要继续播放,则再次调用start方法。clip会记住停止播放的媒体位置,因此不需要显式的暂停和恢复方法。如果您不希望它在中断处继续播放,则可以使用上述提到的帧或微秒定位方法将剪辑“倒回”到开头(或其他任何位置)。

Clip的音量和活跃状态(活跃与非活跃)可以分别通过调用DataLinegetLevelisActive方法来观测。正在播放音频的Clip是活跃的。

使用SourceDataLine

获得一个SourceDataLine与获得一个Clip相似。打开SourceDataLine也类似于打开Clip,目的是再次保留该线路。但是,您可以使用另一种方法,该方法继承自DataLine

void open(AudioFormat format)

请注意,打开SourceDataLine时,您尚未将任何声音数据与线路关联,这与打开Clip不同。相反,您只需指定要播放的音频数据的格式。系统使用默认缓冲区长度。

您还可以使用此变体方法规定一定的缓冲区长度(以字节为单位):

void open(AudioFormat format, int bufferSize)

为了与类似方法保持一致,该bufferSize参数以字节表示,但必须与整数个帧相对应。

除了使用上述open方法之外,还可以使用Line的不带参数的open()方法打开SourceDataLine。在这种情况下,将以其默认音频格式和缓冲区大小打开该线路。但是,您之后将无法更改它们。如果您想知道线路的默认音频格式和缓冲区大小,则可以调用DataLinegetFormatgetBufferSize方法,甚至在线路打开之前就可以调用。

一旦SourceDataLine打开,就可以开始播放声音。为此,您可以调用DataLine的start方法,然后将数据重复写入该线路的播放缓冲区。

start方法允许线路在缓冲区中有任何数据后立即开始播放声音。您可以通过以下方法将数据放入缓冲区:

int write(byte[] b, int offset, int length)

数组的偏移量以字节表示,数组的长度也是如此。

该线路开始尽快地将数据发送到混音器。当混音器本身将数据传递到其目标时,SourceDataLine生成一个START事件。(在Java Sound API的典型实现中,源代码线路将数据提供给混合器的那一刻与混合器将数据提供给其目标的那一刻之间的延迟可以忽略不计,即,该延迟远小于一个采样时间。该START事件被发送到该线路的侦听器,如下面在《监视线路状态》中所述。现在该线路被认为是活跃的,因此DataLineisActive方法将返回true。注意,所有这些仅在缓冲区包含要播放的数据时发生,而不一定是在调用start方法时。如果您在新的SourceDataLine调用了start,但从未将数据写入缓冲区,因此该行永远不会处于活动状态,START也永远不会发送事件。(但是,在这种情况下,DataLineisRunning方法将返回true。)

那么,您如何知道要向缓冲区写入多少数据,以及何时发送第二批数据呢?幸运的是,您无需计算第二次写入操作的时间,即可与第一个缓冲区的末尾同步!相反,您可以利用该write方法的阻塞行为:

  • 一旦将数据写入缓冲区,该方法即返回。它不会等到缓冲区中的所有数据播放完毕。(如果它等了,您可能没有办法在不造成音频不连续的情况下写入下一个缓冲。)
  • 可以尝试写入比缓冲区更多的数据。在这种情况下,该方法将阻塞(不返回),直到您实际请求的所有数据都已放入缓冲区中为止。换句话说,一次将一个缓冲中有价值的数据写入缓冲区并进行播放,直到剩余的数据全部放入缓冲区中为止,此时该方法返回。无论该方法是否阻塞,只要可以写入此调用中最后一个缓冲区的数据,它将立即返回。同样,这意味着您的代码很可能会在最后一个缓冲区的数据播放完成之前恢复控制。
  • 虽然在许多情况下可以写入比缓冲区容纳的数据更多的数据,但是如果您想确定下一次发出的写操作不会阻塞,则可以将写入的字节数限制为DataLineavailable方法返回的数字。

这是一个示例,它循环访问从流中读取的大块数据,一次将一个大块写入SourceDataLine以便回放:

//从流中读取大块并将其写入源数据线路
line.start();
while (total < totalToRead && !stopped){
    numBytesRead = stream.read(myData, 0, numBytesToRead);
    if (numBytesRead == -1) break;
    total += numBytesRead; 
    line.write(myData, 0, numBytesRead);
}

如果您不希望该write方法阻塞,则可以先调用该available方法(在循环内部),以找出不阻塞即可写入的字节数,然后在从流中读取数据之前将字节数限制为变量numBytesToRead。但是,在给定的示例中,阻塞并不重要,因为write方法是在循环内调用的,直到最后一次循环迭代中写入最后一个缓冲区后,该方法才会完成。无论您是否使用阻塞技术,您都可能希望在与应用程序其余部分分离的线程中调用此播放循环,以使您的程序在播放长音频时不会冻结。在循环的每次迭代中,您可以测试用户是否已请求停止播放。此类请求需要设置上面的代码中使用的boolean型变量stoppedtrue

由于write在所有数据播放完毕之前返回,那么您如何知道实际播放完成的时间呢?一种方法是在写入最后一个缓冲区的数据后调用DataLinedrain方法。此方法将阻塞,直到播放完所有数据为止。当控制权返回到程序时,如果需要,您可以释放该线路,而不必担心过早中断任何音频样本的播放:

line.write(b, offset, numBytesToWrite); 
//这是对write的最后一次调用
line.drain();
line.stop();
line.close();
line = null;

当然,您可以故意过早停止播放。例如,应用程序可能会向用户提供“停止”按钮。调用DataLinestop方法可立即停止播放,即使在缓冲区中间也是如此。这会将所有未播放的数据保留在缓冲区中,因此,如果您随后调用start,则会从中断处继续播放。如果那不是您想发生的事情,则可以通过调用flush丢弃缓冲区中剩余的数据。

每当停止数据流时,无论是通过drain方法,stop方法还是flush方法进行了该停止操作,还是因为在应用程序再次调用write写入新数据之前已到达播放缓冲区的末尾,SourceDataLine都会生成一个STOP事件。一个STOP事件并不一定意味着stop被调用,也并不一定意味着之后调用isRunning将返回false。但是,这确实意味着isActive将返回false。(当start被调用后,isRunning方法就返回true,即使生成了STOP事件;在调用stopisRunning就返回false。)重要的是要认识到STARTSTOP事件对应于isActive,而不是isRunning

监控线路状态

开始播放音频后,如何知道完成的时间呢?我们在上面看到了一个解决方案,在写入最后一个数据缓冲区后调用drain方法,但是该方法仅适用于SourceDataLine。对于SourceDataLinesClips都适用的另一种方法是,进行注册,每当线路改变了它的状态时,接收来自线路的通知。这些通知以LineEvent对象的形式来组织,其中有四种类型:OPENCLOSESTARTSTOP

程序中实现LineListener接口的任何对象都可以注册以接收此类通知。要实现LineListener接口,该对象仅需要一个带有LineEvent参数的update方法。若要将此对象注册为该线路的侦听器之一,请调用Line的以下方法:

public void addLineListener(LineListener listener)

每当该线路打开open,关闭close,开始start或停止stop时,它都会向其所有侦听器发送一条update消息。您的对象可以查询它收到的LineEvent。首先,您可以调用LineEvent.getLine以确保停止的线路是您想要的那一条。在我们讨论的这种情况下,您想知道音频是否结束,因此您可以查看LineEvent是否为STOP类型。如果是的话,您可以检查音频的当前位置(音频也存储在LineEvent对象中),并将其与声音的长度(如果知道)进行比较,以查看音频是否到达结尾,而不是因为其他的原因(例如用户单击“停止”按钮,尽管您可能可以在代码的其他位置确定该原因)。

同样,如果您需要知道何时打开open,关闭close或开始start该线路,则可以使用相同的机制。LineEvents会由各种不同类型的线路生成,而不仅仅是ClipsSourceDataLines。但是对于Port,您不能指望通过获取事件来了解线路的打开或关闭状态。例如,Port在最初创建时可能就是打开的,因此您无需调用该open方法,并且Port也不会生成OPEN事件。(请参阅前面有关《选择输入和输出端口》的讨论 。)

同步多线路播放

如果要同时播放多个音频轨道,则可能要使它们完全在同一时间开始和停止。一些混合器用它们的synchronize的方法促进这种行为,它允许使用例如openclosestartstop操作,一组线路使用单个命令控制,而不必单独控制每个数据线路。此外,将操作应用于线路的精确度是可控制的。

要找出特定的混频器是否为指定的一组数据线路提供此功能,请调用Mixer接口的isSynchronizationSupported方法:

boolean isSynchronizationSupported(Line[] lines, boolean  maintainSync)

第一个参数指定一组特定的数据线,第二个参数表示必须保持同步的精度。如果第二个参数是true,该查询询问混合器是否能够一直保持在指定的线路控制样本精确的精度 ; 否则,仅在开始和停止操作期间需要精确的同步,而在整个播放过程中则不需要。

我想,第二个参数的意思是,true代表每时每刻都保持样本的精确同步,false代表只在开始和停止的时候保证同步。

处理输出音频

某些源数据线具有信号处理控件,例如增益gain,声相pan,混响reverb和采样率sample-rete控件。类似的控件,尤其是增益控件,也可能出现在输出端口上。有关如何确定线路是否具有此类控件以及如何使用它们的更多信息,请参阅《使用控件处理音频》。