# 发送和接收 MIDI 信息

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

## 了解设备，发射器和接收器

一旦您了解了 MIDI 数据的工作原理，Java Sound API 就会为 MIDI 数据指定一种灵活且易于使用的消息路由体系结构。该系统基于模块连接设计：不同的模块（每个模块执行特定任务）可以互连（联网），使数据从一个模块流向另一个模块。

Java Sound API 消息传递系统中的基本模块是 [`MidiDevice`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/MidiDevice.html) 接口。 `MidiDevices`包括音序器（记录，播放，加载和编辑带时间戳的 MIDI 信息的序列），合成器（由 MIDI 信息触发时产生声音），以及 MIDI 输入和输出端口，数据来自和传输到外部 MIDI 设备。 MIDI 端口通常需要的功能由基本`MidiDevice`接口描述。 [`Sequencer`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/Sequencer.html) 和 [`Synthesizer`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/Synthesizer.html) 接口扩展了`MidiDevice`接口，分别描述了 MIDI 音序器和合成器的附加功能特性。充当定序器或合成器的具体类应该实现这些接口。

A `MidiDevice`通常拥有一个或多个实现`Receiver`或`Transmitter`接口的辅助对象。这些接口代表将设备连接在一起的“插头”或“门户”，允许数据流入和流出它们。通过将一个`MidiDevice`的`Transmitter`连接到另一个`MidiDevice`的`Receiver`，您可以创建一个模块网络，其中数据从一个到另一个。

`MidiDevice`接口包括用于确定设备可以同时支持多少个发送器和接收器对象的方法，以及用于访问这些对象的其他方法。 MIDI 输出端口通常至少有一个`Receiver`，通过它可以接收输出消息;类似地，合成器通常响应发送到其`Receiver`或`Receivers`的消息。 MIDI 输入端口通常至少有一个`Transmitter`，它传播传入的消息。全功能音序器支持在录制期间接收消息的`Receivers`和在播放期间发送消息的`Transmitters`。

`Transmitter`接口包括设置和查询发送器发送其`MidiMessages`的接收器的方法。设置接收器建立两者之间的连接。 `Receiver`接口包含一个向接收器发送`MidiMessage`的方法。通常，此方法由`Transmitter`调用。 `Transmitter`和`Receiver`接口都包含`close`方法，可以释放先前连接的发送器或接收器，使其可用于不同的连接。

我们现在将研究如何使用发射器和接收器。在讨论连接两个设备的典型情况（例如将音序器挂钩到合成器）之前，我们将研究一种更简单的情况，即将 MIDI 消息直接从应用程序发送到设备。研究这个简单的场景应该可以更容易理解 Java Sound API 如何安排在两个设备之间发送 MIDI 消息。

## 在不使用发送器的情况下向接收器发送消息

假设您想从头创建一条 MIDI 消息，然后将其发送给某个接收器。您可以创建一个新的空白`ShortMessage`，然后使用以下`ShortMessage`方法用 MIDI 数据填充它：

```
void setMessage(int command, int channel, int data1,
         int data2) 

```

准备好发送消息后，可以使用此`Receiver`方法将其发送到`Receiver`对象：

```
void send(MidiMessage message, long timeStamp)

```

暂时解释时间戳参数。现在，我们只是提到如果你不关心指定一个精确的时间，它的值可以设置为-1。在这种情况下，接收消息的设备将尽快响应该消息。

应用程序可以通过调用设备的`getReceiver`方法获得`MidiDevice`的接收器。如果设备无法为程序提供接收器（通常因为所有设备的接收器已在使用中），则抛出`MidiUnavailableException`。否则，从该方法返回的接收器可供程序立即使用。程序完成使用接收器后，应调用接收器的`close`方法。如果程序在调用`close`后尝试在接收器上调用方法，则可能会抛出`IllegalStateException`。

作为不使用发送器发送消息的具体简单示例，让我们向默认接收器发送 Note On 消息，该接收器通常与 MIDI 输出端口或合成器等设备相关联。我们通过创建一个合适的`ShortMessage`并将其作为参数传递给`Receiver's` `send`方法来实现：

```
  ShortMessage myMsg = new ShortMessage();
  // Start playing the note Middle C (60), 
  // moderately loud (velocity = 93).
  myMsg.setMessage(ShortMessage.NOTE_ON, 0, 60, 93);
  long timeStamp = -1;
  Receiver       rcvr = MidiSystem.getReceiver();
  rcvr.send(myMsg, timeStamp);

```

此代码使用`ShortMessage`的静态整数字段，即`NOTE_ON`，用作 MIDI 消息的状态字节。 MIDI 消息的其他部分被赋予显式数值作为`setMessage`方法的参数。零表示音符将使用 MIDI 通道编号 1 播放; 60 表示中间 C;并且 93 是任意按键速度值，这通常表示最终播放音符的合成器应该稍微大声播放。 （MIDI 规范将速度的精确解释留给合成器对其当前乐器的实现。）然后将该 MIDI 信息发送到接收器，时间戳为-1。我们现在需要准确检查时间戳参数的含义，这是下一节的主题。

## 了解时间戳

如您所知，MIDI 规格有不同的部分。一部分描述了 MIDI“线”协议（实时在设备之间发送的消息），另一部分描述了标准 MIDI 文件（在“序列”中存储为事件的消息）。在规范的后半部分，存储在标准 MIDI 文件中的每个事件都标记有一个定时值，该定时值指示何时应该播放该事件。相比之下，MIDI 线协议中的消息总是应该立即处理，只要它们被设备接收，因此它们没有伴随的定时值。

Java Sound API 增加了额外的功能。毫不奇怪，时序值存在于序列中存储的`MidiEvent`对象中（可能从 MIDI 文件中读取），就像在标准 MIDI 文件规范中一样。但是在 Java Sound API 中，甚至设备之间发送的消息 - 换言之，与 MIDI 线协议相对应的消息 - 也可以被赋予定时值，称为 _ 时间戳 _。正是这些时间戳在这里引起了我们的关注。

### 发送到设备的邮件的时间戳

Java Sound API 中设备之间发送的消息可选择的时间戳与标准 MIDI 文件中的定时值完全不同。 MIDI 文件中的定时值通常基于音乐概念，例如节拍和节拍，每个事件的定时测量自上一个事件以来经过的时间。相反，发送到设备的`Receiver`对象的消息上的时间戳始终以微秒为单位测量绝对时间。具体来说，它测量自拥有接收器的设备打开以来经过的微秒数。

这种时间戳旨在帮助补偿操作系统或应用程序引入的延迟。重要的是要意识到这些时间戳用于对时序进行微调，而不是实现可以在完全任意时间安排事件的复杂队列（如`MidiEvent`时序值那样）。

发送到设备的消息上的时间戳（通过`Receiver`）可以为设备提供精确的定时信息。设备在处理消息时可能会使用此信息。例如，它可能会将事件的时间调整几毫秒，以匹配时间戳中的信息。另一方面，并​​非所有设备都支持时间戳，因此设备可能会完全忽略消息的时间戳。

即使设备支持时间戳，它也可能无法在您请求的时间内安排事件。您不能期望发送时间戳很远的消息，并让设备按照您的意图处理它，您当然不能指望设备正确安排时间戳过去的消息！由设备决定如何处理将来或过去太远的时间戳。发件人不知道设备认为太远，或者设备是否有时间戳问题。这种无知模仿了外部 MIDI 硬件设备的行为，这些设备在不知道是否正确接收消息的情况下发送消息。 （MIDI 线协议是单向的。）

某些设备发送带时间戳的消息（通过`Transmitter`）。例如，MIDI 输入端口发送的消息可能会标记传入消息到达端口的时间。在某些系统上，事件处理机制会导致在后续消息处理过程中丢失一定量的定时精度。消息的时间戳允许保留原始定时信息。

要了解设备是否支持时间戳，请调用`MidiDevice`的以下方法：

```
    long getMicrosecondPosition()

```

如果设备忽略时间戳，则此方法返回-1。否则，它将返回设备当前的时间概念，您可以在确定随后发送的邮件的时间戳时将其用作偏移量。例如，如果要在将来发送带有时间戳 5 毫秒的消息，则可以以微秒为单位获取设备的当前位置，添加 5000 微秒，并将其用作时间戳。请记住，`MidiDevice's`时间概念在设备打开时总是将时间置零。

现在，以时间戳的所有解释为背景，让我们回到`Receiver`的`send`方法：

```
void send(MidiMessage message, long timeStamp)

```

根据接收设备的时间概念，`timeStamp`参数以微秒表示。如果设备不支持时间戳，则只会忽略`timeStamp`参数。您不需要将发送给接收方的消息加上时间戳。你可以使用-1 作为`timeStamp`参数来表示你不关心调整确切的时间;你只是将它留给接收设备来尽快处理消息。但是，不建议发送带有一些消息的-1 和带有发送到同一接收器的其他消息的显式时间戳。这样做可能会导致合成时间的不规则。

## 将发射器连接到接收器

我们已经看到了如何在不使用发射器的情况下将 MIDI 信息直接发送到接收器。现在让我们看一下更常见的情况，你不是从头开始创建 MIDI 消息，而是简单地将设备连接在一起，以便其中一个可以将 MIDI 消息发送给另一个。

### 连接到单个设备

我们将作为第一个例子的特定情况是将音序器连接到合成器。建立此连接后，启动顺控程序将使合成器从顺控程序当前序列中的事件生成音频。现在，我们将忽略将序列从 MIDI 文件加载到音序器的过程。另外，我们不会进入播放序列的机制。在[播放，录制和编辑 MIDI 序列](MIDI-seq-intro.html)中详细讨论了加载和播放序列。将乐器加载到合成器中将在 [Synthesizing Sound](MIDI-synth.html) 中讨论。目前，我们感兴趣的是如何在音序器和合成器之间建立连接。这将用于说明将一个设备的发射器连接到另一个设备的接收器的更一般过程。

为简单起见，我们将使用默认音序器和默认合成器。

```
    Sequencer           seq;
    Transmitter         seqTrans;
    Synthesizer         synth;
    Receiver         synthRcvr;
    try {
          seq     = MidiSystem.getSequencer();
          seqTrans = seq.getTransmitter();
          synth   = MidiSystem.getSynthesizer();
          synthRcvr = synth.getReceiver(); 
          seqTrans.setReceiver(synthRcvr);      
    } catch (MidiUnavailableException e) {
          // handle or throw exception
    }

```

实现实际上可能只有一个对象，它既可以作为默认的音序器，也可以作为默认的合成器。换句话说，实现可能使用实现`Sequencer`接口和`Synthesizer`接口的类。在这种情况下，可能没有必要在上面的代码中进行显式连接。但是，为了便于携带，不假设这样的配置更安全。如果需要，您可以测试这种情况，当然：

```
if (seq instanceof Synthesizer)

```

虽然上面的显式连接在任何情况下都应该有效。

### 连接到多个设备

前面的代码示例说明了发送器和接收器之间的一对一连接。但是，如果您需要将相同的 MIDI 信息发送到多个接收器，该怎么办？例如，假设您想从外部设备捕获 MIDI 数据以驱动内部合成器，同时将数据记录到序列中。这种形式的连接，有时被称为“扇出”或“分离器”，是直截了当的。以下语句显示如何创建扇出连接，通过该连接将到达 MIDI 输入端口的 MIDI 信息发送到`Synthesizer`对象和`Sequencer`对象。我们假设您已经获得并打开了三个设备：输入端口，音序器和合成器。 （要获取输入端口，您需要迭代`MidiSystem.getMidiDeviceInfo`返回的所有项目。）

```
    Synthesizer  synth;
    Sequencer    seq;
    MidiDevice   inputPort;
    // [obtain and open the three devices...]
    Transmitter   inPortTrans1, inPortTrans2;
    Receiver            synthRcvr;
    Receiver            seqRcvr;
    try {
          inPortTrans1 = inputPort.getTransmitter();
          synthRcvr = synth.getReceiver(); 
          inPortTrans1.setReceiver(synthRcvr);
          inPortTrans2 = inputPort.getTransmitter();
          seqRcvr = seq.getReceiver(); 
          inPortTrans2.setReceiver(seqRcvr);
    } catch (MidiUnavailableException e) {
          // handle or throw exception
    }

```

此代码引入了`MidiDevice.getTransmitter`方法的双重调用，将结果分配给`inPortTrans1`和`inPortTrans2`。如前所述，设备可以拥有多个发送器和接收器。每次为给定设备调用`MidiDevice.getTransmitter()`时，将返回另一个发送器，直到不再可用，此时将抛出异常。

要了解设备支持的发送器和接收器数量，可以使用以下`MidiDevice`方法：

```
    int getMaxTransmitters()
    int getMaxReceivers()

```

这些方法返回设备拥有的总数，而不是当前可用的数量。

发射器一次只能向一个接收器发送 MIDI 信息。 （每次调用`Transmitter's setReceiver`方法时，现有的`Receiver`（如果有）将被新指定的替换。您可以通过调用`Transmitter.getReceiver`来判断发送器当前是否有接收器。）但是，如果设备有通过将每个发射器连接到不同的接收器，它可以一次向多个设备发送数据，如上面输入端口的情况所示。

类似地，设备可以使用其多个接收器一次从多个设备接收。所需的多接收器代码很简单，直接类似于上面的多发送器代码。单个接收器也可以一次接收来自多个发送器的消息。

### 关闭连接

完成连接后，您可以通过为您获得的每个发送器和接收器调用`close`方法来释放其资源。 `Transmitter`和`Receiver`接口各自具有`close`方法。请注意，调用`Transmitter.setReceiver`不会关闭发射器的当前接收器。接收器处于打开状态，它仍然可以从连接到它的任何其他发射器接收消息。

如果你也完成了这些设备，你可以通过调用`MidiDevice.close()`类似地将它们提供给其他应用程序。关闭设备会自动关闭其所有发射器和接收器。