Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,可能使用不再可用的技术。
请参阅Java语言更改,了解Java SE 9及后续版本中更新的语言功能的概述。
请参阅JDK发布说明,了解所有JDK版本的新功能、增强功能以及已删除或已弃用选项的信息。
回放有时被称为演示或渲染。这些是适用于声音以外的其他媒体的通用术语。其基本特点是将一系列数据传递到某个位置,最终由用户感知。如果数据是基于时间的,比如声音,那么必须以正确的速率传递。尤其是对于声音而言,保持数据流的速率非常重要,因为声音播放中断通常会产生大声的点击声或刺耳的失真声。Java Sound API旨在帮助应用程序平稳连续地播放声音,即使是非常长的声音。
之前你看到了如何从音频系统或混音器中获取线路。在这里,你将学习如何通过线路播放声音。
正如你所知,有两种线路可以用来播放声音:一个是Clip
,另一个是SourceDataLine
。两者之间的主要区别在于,使用Clip
时你在播放之前一次性指定所有的声音数据,而使用SourceDataLine
时你在播放期间持续不断地写入新的数据缓冲区。尽管有许多情况可以使用Clip
或SourceDataLine
,以下标准有助于确定哪种线路更适合特定的情况:
Clip
。
例如,你可以将一个短音频文件读入到一个剪辑中。如果你希望声音循环播放多次,Clip
比SourceDataLine
更方便,尤其是如果你希望播放循环(反复通过声音的全部或部分)。如果你需要在声音的任意位置开始播放,剪辑接口提供了一个简单的方法来实现。最后,与SourceDataLine
的缓冲播放相比,剪辑播放通常具有更低的延迟。换句话说,由于声音预加载到剪辑中,播放可以立即开始,而无需等待缓冲区填充。
SourceDataLine
。
作为后一种情况的示例,假设你正在监视音频输入,也就是在捕获音频时进行回放。如果你没有一个可以将输入音频直接发送到输出端口的混音器,你的应用程序将需要将捕获的数据发送到音频输出混音器。在这种情况下,SourceDataLine
比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类似于获取Clip。打开SourceDataLine也类似于打开Clip,目的是再次保留该line。但是,您使用继承自DataLine的不同方法:
void open(AudioFormat format)
注意,当你打开一个SourceDataLine
时,还没有将任何声音数据与该行关联,与打开Clip
不同。相反,你只需指定要播放的音频数据的格式。系统会选择一个默认的缓冲区长度。
void open(AudioFormat format, int bufferSize)
为了与类似方法保持一致,bufferSize
参数以字节表示,但它必须对应于整数个帧。
除了使用上述描述的open方法外,还可以使用Line
的open()
方法打开SourceDataLine
,不带参数。在这种情况下,行将以其默认音频格式和缓冲区大小打开。然而,您不能以后更改这些值。如果您想了解行的默认音频格式和缓冲区大小,即使在行尚未打开之前,您也可以调用DataLine
的getFormat
和getBufferSize
方法。
一旦SourceDataLine
打开,您就可以开始播放声音。您可以通过调用DataLine
的start方法来实现,然后重复将数据写入行的播放缓冲区。
start方法允许行在缓冲区中有任何数据时开始播放声音。通过以下方法将数据放入缓冲区:
int write(byte[] b, int offset, int length)
数组中的偏移量以字节为单位表示,数组的长度也是如此。
行会尽快将数据发送给混音器。当混音器将数据传递给其目标时,SourceDataLine
会生成一个START
事件。(在Java Sound API的典型实现中,源行将数据传递给混音器和混音器将数据传递给其目标之间的延迟可以忽略不计,即远远小于一个样本的时间。)此START
事件会发送到行的监听器,如下面的监视线路状态中所解释的。此时行被认为是活动的,因此DataLine
的isActive
方法将返回true
。请注意,所有这些只有在缓冲区包含要播放的数据时才会发生,不一定是在调用start方法的时候立即发生。如果你在一个新的SourceDataLine
上调用了start
,但从未向缓冲区写入数据,那么该行将永远不会被激活,也不会发送START
事件。(但在这种情况下,DataLine
的isRunning
方法将返回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
在所有数据完成播放之前返回,你如何知道播放实际上已经完成了呢?一种方法是在写入最后一个缓冲区的数据后调用DataLine
的drain
方法。这个方法会阻塞直到所有数据都被播放完。当控制权返回到你的程序时,你可以释放该线路,如果需要的话,而不必担心提前中断任何音频样本的播放:
line.write(b, offset, numBytesToWrite); //这是最后一次调用write line.drain(); line.stop(); line.close(); line = null;
当然,你也可以故意提前停止播放。例如,应用程序可能为用户提供了一个停止按钮。调用DataLine
的stop
方法可以立即停止播放,即使在缓冲区的中间。这将保留缓冲区中未播放的数据,所以如果随后调用start
,播放将从中断的地方恢复。如果这不是你想要发生的,你可以通过调用flush
来丢弃缓冲区中剩余的数据。
当数据流的流动被停止时,SourceDataLine
会生成一个STOP
事件,无论是通过drain
方法、stop
方法、flush
方法,还是因为应用程序在调用write
之前到达了播放缓冲区的末尾以提供新的数据。一个STOP
事件并不一定意味着stop
方法被调用了,也并不一定意味着随后调用isRunning
将返回false
。然而,它确实意味着isActive
将返回false
。(当调用了start
方法后,isRunning
方法将返回true
,即使生成了一个STOP
事件,只有在调用stop
方法后,isRunning
方法才会开始返回false
。)重要的是要意识到START
和STOP
事件对应的是isActive
,而不是isRunning
。
一旦你开始播放声音,你如何知道它何时完成?我们在上面看到了一种解决方案,即在写入最后一个数据缓冲区后调用drain
方法,但这种方法只适用于SourceDataLine
。另一种适用于SourceDataLine
和Clips
的方法是注册接收线路状态变化时的通知。这些通知以LineEvent
对象的形式生成,有四种类型:OPEN
、CLOSE
、START
和STOP
。
你的程序中任何实现了LineListener
接口的对象都可以注册接收这些通知。要实现LineListener
接口,该对象只需要一个接受LineEvent
参数的更新方法。要将该对象注册为线路的监听器之一,你可以调用以下Line
方法:
public void addLineListener(LineListener listener)
每当线路打开、关闭、开始或停止时,它会向所有的监听器发送一个update
消息。您的对象可以查询接收到的LineEvent
。首先,您可以调用LineEvent.getLine
来确保停止的线路是您关心的线路。在我们讨论的情况下,您想知道音频是否已经结束,所以您可以检查LineEvent
的类型是否为STOP
。如果是,您可以检查音频的当前位置,它也存储在LineEvent
对象中,并将其与音频的长度(如果已知)进行比较,以确定它是否已经到达了结束,而不是被其他方式停止(比如用户点击了停止按钮,尽管您可能在代码的其他地方能够确定这个原因)。
同样,如果您需要知道线路何时打开、关闭或开始,您可以使用相同的机制。LineEvents
由不同类型的线路生成,不仅仅是Clips
和SourceDataLines
。然而,在Port
的情况下,您不能指望得到一个事件来了解线路的打开或关闭状态。例如,当创建一个Port
时,它可能是初始打开的,所以您不会调用open
方法,而且Port
也不会生成OPEN
事件。(请参阅之前对选择输入和输出端口的讨论。)
如果您同时播放多个音轨,您可能希望它们都在完全相同的时间开始和结束。一些混音器通过其synchronize
方法来实现这种行为,该方法允许您对一组数据线路应用open
、close
、start
和stop
等操作,而不是需要单独控制每个线路。此外,可以控制将操作应用于线路的精度。
要了解特定混音器是否为指定的数据线路组提供此功能,请调用Mixer
接口的isSynchronizationSupported
方法:
boolean isSynchronizationSupported(Line[] lines, boolean maintainSync)
第一个参数指定了一组特定的数据线路,第二个参数指示必须维护同步的精确度。如果第二个参数为true
,则查询是询问混音器是否能够始终在指定线路上以样本精确的精度进行控制;否则,在播放过程中只需要在开始和停止操作期间要求精确同步。
某些源数据线具有信号处理控件,例如增益、平移、混响和采样率控件。输出端口上也可能存在类似的控件,尤其是增益控件。有关如何确定线路是否具有这些控件以及如何使用它们的更多信息,请参见使用控件处理音频。