# 播放音频

> 原文： [https://docs.oracle.com/javase/tutorial/sound/playing.html](https://docs.oracle.com/javase/tutorial/sound/playing.html)

回放有时被称为 _ 呈现 _ 或 _ 呈现 _。这些是除声音之外适用于其他类型媒体的通用术语。基本特征是在某处传送一系列数据以供用户最终感知。如果数据是基于时间的，就像声音一样，它必须以正确的速率传送。由于声音甚至比视频更重要，因此保持数据流的速率非常重要，因为声音播放的中断通常会产生很大的咔嗒声或刺激性的失真。 Java Sound API 旨在帮助应用程序平滑连续地播放声音，甚至是非常长的声音。

之前您已经了解了如何从音频系统或混音器获取线路。在这里，您将学习如何通过线路播放声音。

如您所知，有两种线路可用于播放声音： [`Clip`](https://docs.oracle.com/javase/8/docs/api/javax/sound/sampled/Clip.html) 和 [`SourceDataLine`](https://docs.oracle.com/javase/8/docs/api/javax/sound/sampled/SourceDataLine.html) 。两者之间的主要区别在于，使用`Clip`可以在播放前一次指定所有声音数据，而使用`SourceDataLine`可以在播放期间连续写入新的数据缓冲区。尽管在许多情况下您可以使用`Clip`或`SourceDataLine`，但以下标准有助于确定哪种线更适合特定情况：

*   Use a `Clip` when you have non-real-time sound data that can be preloaded into memory.

    例如，您可能会将一个简短的声音文件读入剪辑。如果您希望声音不止一次播放，`Clip`比`SourceDataLine`更方便，特别是如果您希望播放循环（重复循环播放全部或部分声音）。如果您需要在声音中的任意位置开始播放，`Clip`界面提供了一种方法来轻松完成。最后，从`Clip`播放通常比`SourceDataLine`的缓冲播放具有更少的延迟。换句话说，因为声音被预加载到剪辑中，所以可以立即开始播放，而不必等待缓冲区被填充。

*   Use a `SourceDataLine` for streaming data, such as a long sound file that won't all fit in memory at once, or a sound whose data can't be known in advance of playback.

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

## 使用剪辑

如前面[获取所需类型的行](accessing.html#113154)所述，获得`Clip`;使用`Clip.class`为第一个参数构造`DataLine.Info`对象，并将此`DataLine.Info`作为参数传递给`AudioSystem`或`Mixer`的`getLine`方法。

获得一条线就意味着你已经有了一种方法来引用它; `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` `setFramePosition`或`setMicroSecondPosition`方法指定数据应在何处开始播放。否则，它将从头开始。您还可以使用`setLoopPoints`方法将播放配置为重复循环。

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

可以通过分别调用`DataLine`方法`getLevel`和`isActive`来监测`Clip's`体积水平和活动状态（活动与非活动）。活动`Clip`是当前播放声音的那个。

## 使用 SourceDataLine

获得`SourceDataLine`类似于获得`Clip`。 打开`SourceDataLine`也类似于打开`Clip`，因为目的是再次保留该行。但是，您使用从`DataLine`继承的其他方法：

```
void open(AudioFormat format)

```

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

您还可以使用以下变量规定某个缓冲区长度（以字节为单位）：

```
void open(AudioFormat format, int bufferSize)

```

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

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

`SourceDataLine`打开后，您可以开始播放声音。您可以通过调用`DataLine's` start 方法，然后将数据重复写入行的播放缓冲区来完成此操作。

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

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

```

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

线路开始尽快向其混音器发送数据。当混音器本身将数据传送到目标时，`SourceDataLine`会生成`START`事件。 （在 Java Sound API 的典型实现中，源线将数据传送到混音器的时刻与混音器将数据传送到目标的时刻之间的延迟可以忽略不计，即远远少于一个时间。样本。）此`START`事件被发送到线路的侦听器，如下面[监视线路状态](#113711)中所述。该行现在被认为是活动的，因此`DataLine`的`isActive`方法将返回`true`。请注意，所有这些只在缓冲区包含要播放的数据时发生，而不一定在调用 start 方法时发生。如果在新的`SourceDataLine`上调用`start`但从未将数据写入缓冲区，则该行永远不会处于活动状态，并且永远不会发送`START`事件。 （但是，在这种情况下，`DataLine`的`isRunning`方法将返回`true`。）

那么你怎么知道要写入缓冲区的数据量，以及何时发送第二批数据？幸运的是，您不需要第二次调用 write 来与第一个缓冲区的末尾同步！相反，您可以利用`write`方法的阻塞行为：

*   只要数据写入缓冲区，该方法就会返回。它不会等到缓冲区中的所有数据都播放完毕。 （如果确实如此，您可能没有时间编写下一个缓冲区而不会在音频中创建不连续性。）
*   尝试写入比缓冲区更多的数据是可以的。在这种情况下，方法会阻止（不返回），直到您请求的所有数据实际都放在缓冲区中。换句话说，一次将一个缓冲区的数据值写入缓冲区并播放，直到剩余的数据全部放入缓冲区，此时方法返回。无论方法是否阻塞，只要可以写入来自此调用的最后一个缓冲区的数据，它就会返回。同样，这意味着您的代码很可能在最后一个缓冲区的数据完成播放之前重新获得控制权。
*   虽然在很多情况下可以写入比缓冲区更多的数据，如果你想确定下一次发出的写操作没有阻塞，你可以限制写入数字的字节数`DataLine's` `available`方法返回。

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

由于`write`在所有数据播放完毕之前返回，您如何知道播放何时实际完成？一种方法是在写入最后一个缓冲区的数据后调用`DataLine`的`drain`方法。此方法将阻止，直到播放完所有数据。当控制返回到您的程序时，您可以根据需要释放该行，而不必担心过早地切断任何音频样本的播放：

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

```

当然，你可以故意提前停止播放。例如，应用程序可能为用户提供“停止”按钮。调用`DataLine's stop`方法立即停止播放，即使在缓冲区中间也是如此。这会在缓冲区中留下任何未播放的数据，因此如果您随后调用`start`，播放将从中断处继续播放。如果这不是您想要发生的，您可以通过调用`flush`来丢弃缓冲区中剩余的数据。

A `SourceDataLine`在数据流停止时生成`STOP`事件，无论此停止是通过排水方法，停止方法还是冲洗方法启动，还是因为结束在应用程序再次调用`write`以提供新数据之前，已达到回放缓冲区。 `STOP`事件并不一定意味着调用`stop`方法，并不一定意味着`isRunning`的后续调用将返回`false`。但是，它确实意味着`isActive`将返回`false`。 （当调用`start`方法时，`isRunning`方法将返回`true`，即使生成了`STOP`事件，只有在调用`stop`方法后它才会开始返回`false` 。）重要的是要认识到`START`和`STOP`事件对应于`isActive`，而不是`isRunning`。

## 监控线路状态

一旦你开始播放声音，你怎么知道它什么时候结束？我们看到上面的一个解决方案，在写完最后一个数据缓冲区后调用`drain`方法，但该方法仅适用于`SourceDataLine`。另一种适用于`SourceDataLines`和`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`事件。 （参见前面对[选择输入和输出端口](accessing.html#113216)的讨论。）

## 同步多行播放

如果您同时播放多首音频曲目，您可能希望让它们全部在同一时间开始和停止。有些混音器使用`synchronize`方法来促进这种行为，这种方法允许您使用单个命令将`open`，`close`，`start`和`stop`等操作应用于一组数据行，而不必使用单独控制每一行。此外，操作应用于线的准确度是可控的。

要确定特定混频器是否为指定的数据线组提供此功能，请调用`Mixer`接口的`isSynchronizationSupported`方法：

```
boolean isSynchronizationSupported(Line[] lines, boolean  maintainSync)

```

第一个参数指定一组特定数据行，第二个参数指示必须保持同步的准确性。如果第二个参数是`true`，则查询询问混频器是否能够始终保持样本精确度以控制指定行;否则，仅在启动和停止操作期间需要精确同步，而不是在整个回放期间。

## 处理传出音频

某些源数据线具有信号处理控制，如增益，声像，混响和采样率控制。类似的控制，尤其是增益控制，也可能出现在输出端口上。有关如何确定线路是否具有此类控件以及如何使用它们的更多信息，请参阅[使用控件处理音频](controls.html)。