文档

Java™教程
隐藏目录
播放音频
指南: 声音

播放音频

回放有时被称为演示渲染。这些是适用于声音以外的其他媒体的通用术语。其基本特点是将一系列数据传递到某个位置,最终由用户感知。如果数据是基于时间的,比如声音,那么必须以正确的速率传递。尤其是对于声音而言,保持数据流的速率非常重要,因为声音播放中断通常会产生大声的点击声或刺耳的失真声。Java Sound API旨在帮助应用程序平稳连续地播放声音,即使是非常长的声音。

之前你看到了如何从音频系统或混音器中获取线路。在这里,你将学习如何通过线路播放声音。

正如你所知,有两种线路可以用来播放声音:一个是Clip,另一个是SourceDataLine。两者之间的主要区别在于,使用Clip时你在播放之前一次性指定所有的声音数据,而使用SourceDataLine时你在播放期间持续不断地写入新的数据缓冲区。尽管有许多情况可以使用ClipSourceDataLine,以下标准有助于确定哪种线路更适合特定的情况:

使用Clip

您可以按照之前描述的方法获取Clip;构造一个带有Clip.class的DataLine.Info对象作为第一个参数,并将此DataLine.Info作为参数传递给AudioSystem或Mixer的getLine方法。

获取一个line只意味着您已经得到了一个引用的方式;getLine不会真正为您保留该line。由于mixer可能只有有限数量的所需类型的line可用,因此在您调用getLine获取clip后,可能会有另一个应用程序在您准备开始播放之前抢走clip。要实际使用clip,您需要通过调用以下Clip方法之一将其保留给您的程序的独占使用:

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

尽管上述第二个open方法中有一个bufferSize参数,但是Clip(与SourceDataLine不同)不包括用于向缓冲区写入新数据的方法。这里的bufferSize参数只是指定要加载到clip中的字节数组的大小。它不是一个您可以随后加载更多数据的缓冲区,就像SourceDataLine的缓冲区一样。

打开clip后,您可以使用Clip的setFramePosition或setMicroSecondPosition方法指定数据在何处开始播放。否则,它将从开头开始。您还可以使用setLoopPoints方法配置重复循环播放。

当您准备好开始播放时,只需调用start方法。要停止或暂停clip,调用stop方法,要恢复播放,再次调用start方法。clip会记住停止播放的媒体位置,因此不需要显式的暂停和恢复方法。如果您不希望它从离开的地方恢复,可以使用上面提到的帧或微秒定位方法将clip“倒回”到开头(或任何其他位置)。

通过调用DataLine的getLevel和isActive方法,可以监视Clip的音量级别和活动状态(活动与非活动)。活动的Clip是当前正在播放声音的Clip。

使用SourceDataLine

获取SourceDataLine类似于获取Clip。打开SourceDataLine也类似于打开Clip,目的是再次保留该line。但是,您使用继承自DataLine的不同方法:

void open(AudioFormat format)

注意,当你打开一个SourceDataLine时,还没有将任何声音数据与该行关联,与打开Clip不同。相反,你只需指定要播放的音频数据的格式。系统会选择一个默认的缓冲区长度。

你还可以使用以下变体指定特定的缓冲区长度(以字节为单位):

void open(AudioFormat format, int bufferSize)

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

除了使用上述描述的open方法外,还可以使用Lineopen()方法打开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以与第一个缓冲区的结束同步!相反,你可以利用write方法的阻塞行为:

以下是一个示例,通过从流中读取的数据块进行迭代,逐个将数据块写入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方法在最后一个循环迭代中被调用,直到最后一个缓冲区被写入才会完成循环。无论是否使用阻塞技术,你可能希望将该播放循环在应用程序的其余部分中的一个单独线程中调用,这样在播放长音频时,你的程序不会出现冻结的情况。在循环的每次迭代中,你可以测试用户是否请求停止播放。这样的请求需要将上面的代码中的stopped布尔值设置为true。

由于write在所有数据完成播放之前返回,你如何知道播放实际上已经完成了呢?一种方法是在写入最后一个缓冲区的数据后调用DataLinedrain方法。这个方法会阻塞直到所有数据都被播放完。当控制权返回到你的程序时,你可以释放该线路,如果需要的话,而不必担心提前中断任何音频样本的播放:

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

当然,你也可以故意提前停止播放。例如,应用程序可能为用户提供了一个停止按钮。调用DataLinestop方法可以立即停止播放,即使在缓冲区的中间。这将保留缓冲区中未播放的数据,所以如果随后调用start,播放将从中断的地方恢复。如果这不是你想要发生的,你可以通过调用flush来丢弃缓冲区中剩余的数据。

当数据流的流动被停止时,SourceDataLine会生成一个STOP事件,无论是通过drain方法、stop方法、flush方法,还是因为应用程序在调用write之前到达了播放缓冲区的末尾以提供新的数据。一个STOP事件并不一定意味着stop方法被调用了,也并不一定意味着随后调用isRunning将返回false。然而,它确实意味着isActive将返回false。(当调用了start方法后,isRunning方法将返回true,即使生成了一个STOP事件,只有在调用stop方法后,isRunning方法才会开始返回false。)重要的是要意识到STARTSTOP事件对应的是isActive,而不是isRunning

监控线路的状态

一旦你开始播放声音,你如何知道它何时完成?我们在上面看到了一种解决方案,即在写入最后一个数据缓冲区后调用drain方法,但这种方法只适用于SourceDataLine。另一种适用于SourceDataLineClips的方法是注册接收线路状态变化时的通知。这些通知以LineEvent对象的形式生成,有四种类型:OPENCLOSESTARTSTOP

你的程序中任何实现了LineListener接口的对象都可以注册接收这些通知。要实现LineListener接口,该对象只需要一个接受LineEvent参数的更新方法。要将该对象注册为线路的监听器之一,你可以调用以下Line方法:

public void addLineListener(LineListener listener)

每当线路打开、关闭、开始或停止时,它会向所有的监听器发送一个update消息。您的对象可以查询接收到的LineEvent。首先,您可以调用LineEvent.getLine来确保停止的线路是您关心的线路。在我们讨论的情况下,您想知道音频是否已经结束,所以您可以检查LineEvent的类型是否为STOP。如果是,您可以检查音频的当前位置,它也存储在LineEvent对象中,并将其与音频的长度(如果已知)进行比较,以确定它是否已经到达了结束,而不是被其他方式停止(比如用户点击了停止按钮,尽管您可能在代码的其他地方能够确定这个原因)。

同样,如果您需要知道线路何时打开、关闭或开始,您可以使用相同的机制。LineEvents由不同类型的线路生成,不仅仅是ClipsSourceDataLines。然而,在Port的情况下,您不能指望得到一个事件来了解线路的打开或关闭状态。例如,当创建一个Port时,它可能是初始打开的,所以您不会调用open方法,而且Port也不会生成OPEN事件。(请参阅之前对选择输入和输出端口的讨论。)

多个线路上的同步播放

如果您同时播放多个音轨,您可能希望它们都在完全相同的时间开始和结束。一些混音器通过其synchronize方法来实现这种行为,该方法允许您对一组数据线路应用openclosestartstop等操作,而不是需要单独控制每个线路。此外,可以控制将操作应用于线路的精度。

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

boolean isSynchronizationSupported(Line[] lines, boolean  maintainSync)

第一个参数指定了一组特定的数据线路,第二个参数指示必须维护同步的精确度。如果第二个参数为true,则查询是询问混音器是否能够始终在指定线路上以样本精确的精度进行控制;否则,在播放过程中只需要在开始和停止操作期间要求精确同步。

处理传出音频

某些源数据线具有信号处理控件,例如增益、平移、混响和采样率控件。输出端口上也可能存在类似的控件,尤其是增益控件。有关如何确定线路是否具有这些控件以及如何使用它们的更多信息,请参见使用控件处理音频


上一页: 访问音频系统资源
下一页: 捕获音频