播放音频

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

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

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

例如,您可能将短声音文件读入剪辑。如果您想让声音播放不止一次,则ClipSourceDataLine更方便,尤其是当您要循环播放(重复播放全部或部分声音)时,尤其如此。如果您需要在声音中的任意位置开始播放,则Clipinterface提供了一种轻松进行播放的方法。最后,来自Clip的回放通常比来自SourceDataLine的缓冲回放具有更少的延迟。换句话说,由于声音已预先加载到剪辑中,因此可以立即开始播放,而不必 await 缓冲区被填满。

作为后一种情况的示例,假设您正在监视声音 Importing-即在catch声音时播放声音。如果您没有可直接从输出端口发送 Importing 音频的混音器,则您的应用程序将必须获取catch的数据并将其发送到音频输出混音器。在这种情况下,SourceDataLineClip更合适。当您响应用户的 Importing 以交互方式合成或处理声音数据时,会发生另一个无法预先知道的声音示例。例如,想象一个游戏,当用户移动鼠标时,通过将声音从一种声音“变形”到另一种声音来给出听觉反馈。声音转换的动态性质要求应用程序在播放期间不断更新声音数据,而不是在播放开始之前全部提供声音数据。

使用剪辑

如先前在获取所需类型的线下所述获得Clip;使用第一个参数Clip.class构造一个DataLine.Info对象,并将该DataLine.Info作为参数传递给AudioSystemMixergetLine方法。

获得一行只是意味着您已经有了引用它的方法; getLine实际上并没有为您保留行。由于混音器可能只有有限数量的所需类型的行,因此可能发生在调用getLine获取剪辑之后,另一个应用程序会跳入并抓取剪辑,然后准备开始播放。要实际使用该剪辑,您需要通过调用以下Clip方法之一将其保留为程序专用。

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

尽管上面的第二个open方法中有bufferSize参数,但Clip(与SourceDataLine不同)不包括用于将新数据写入缓冲区的方法。 bufferSize参数仅指定要加载到剪辑中的字节数组的数量。它不是一个缓冲区,以后可以像SourceDataLine's缓冲区那样将更多数据加载到该缓冲区中。

打开剪辑后,您可以使用Clip's setFramePositionsetMicroSecondPosition方法指定开始播放数据的位置。否则,它将从头开始。您还可以使用setLoopPoints方法将回放配置为重复循环。

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

Clip's的音量和活动状态(活动与不活动)可以分别通过调用DataLine方法getLevelisActive进行监视。活跃的Clip是当前正在播放声音的声音。

使用 SourceDataLine

获得SourceDataLine类似于获得Clip打开SourceDataLine也类似于打开Clip,其 Object 是再次保留线路。但是,您使用了从DataLine继承的另一种方法:

void open(AudioFormat format)

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

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

void open(AudioFormat format, int bufferSize)

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

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

SourceDataLine打开后,即可开始播放声音。为此,您可以调用DataLine's start 方法,然后将数据重复写入该行的回放缓冲区。

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

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

数组的offset量以字节表示,数组的 Long 度也是如此。

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

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

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

// read chunks from a stream and write them to a source data 
line 
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 方法是在一个循环内调用的,直到最后一次循环迭代中写入最后一个缓冲区后,该方法才会完成。无论是否使用阻塞技术,您都可能希望在与应用程序其余部分不同的线程中调用此播放循环,以使您的程序在播放 Long 声音时不会冻结。在循环的每次迭代中,您可以测试用户是否已请求停止播放。这样的请求需要将上面代码中使用的stopped布尔值设置为true

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

line.write(b, offset, numBytesToWrite); 
//this is the final invocation of write
line.drain();
line.stop();
line.close();
line = null;

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

SourceDataLine会在数据流停止时(无论是通过流失方法,停止方法还是刷新方法启动该停止,还是因为在调用应用程序之前已到达播放缓冲区的末尾)生成STOP事件write再次提供新数据。 STOP事件不一定意味着已调用stop方法,也不一定意味着随后的isRunning调用将返回false。但是,这确实意味着isActive将返回false。 (调用start方法后,即使生成STOP事件,isRunning方法也将返回true,并且仅在调用stop方法后它才会开始返回false.)重要的是要认识到STARTSTOP事件对应到isActive而不是isRunning

监视线路状态

开始播放声音后,如何查找结束时间?我们在上面看到了一个解决方案,在写入最后一个数据缓冲区后调用了drain方法,但是该方法仅适用于SourceDataLine。另一种对SourceDataLinesClips都有效的方法是注册,以便在该行更改其状态时从该行接收通知。这些通知以LineEvent对象的形式生成,有四种类型:OPENCLOSESTARTSTOP

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

public void addLineListener(LineListener listener)

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

同样,如果您需要知道何时打开,关闭或启动该行,则可以使用相同的机制。 LineEvents是由不同种类的线生成的,而不仅仅是ClipsSourceDataLines。但是,在Port的情况下,您不能指望通过获取事件来了解线路的打开或关闭状态。例如,一个Port可能在创建时最初处于打开状态,因此您不必调用open方法,并且Port永远不会生成OPEN事件。 (请参见前面对选择 Importing 和输出端口的讨论。)

同步多行播放

如果要同时播放多条音频轨道,则可能希望它们完全在同一时间开始和停止。一些混合器使用其synchronize方法来简化此行为,该方法使您可以使用单个命令将openclosestartstop之类的操作应用于一组数据线,而不必分别控制每条线。此外,将操作应用于线路的精确度是可控制的。

要确定特定的混频器是否为指定的一组数据线提供此功能,请调用Mixerinterface的isSynchronizationSupported方法:

boolean isSynchronizationSupported(Line[] lines, boolean  maintainSync)

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

处理外发音频

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

首页