传输和接收 MIDI 信息

了解设备,发送器和接收器

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

Java Sound API 的消息传递系统中的基本模块是MidiDeviceinterface。 MidiDevices包括音序器(用于记录,播放,加载和编辑带时间戳的 MIDI 消息的序列),合成器(在由 MIDI 消息触发时生成声音)以及 MIDIImporting 和输出端口,数据通过该端口进出外部 MIDI 设备。 MIDI 端口通常需要的功能由基本MidiDeviceinterface描述。 SequencerSynthesizerinterface扩展了MidiDeviceinterface,以分别描述 MIDI 音序器和合成器的其他功能特性。充当定序器或合成器的具体类应实现这些interface。

MidiDevice通常拥有一个或多个实现ReceiverTransmitterinterface的辅助对象。这些interface代表将设备连接在一起的“插头”或“端口”,从而允许数据流入和流出设备。通过将一个MidiDeviceTransmitter连接到另一个Receiver,可以创建一个模块网络,其中数据从一个流向另一个。

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

Transmitterinterface包括用于设置和查询发送器向其发送MidiMessages的接收器的方法。设置接收器可构建两者之间的连接。 Receiverinterface包含一种向接收器发送MidiMessage的方法。通常,此方法由Transmitter调用。 TransmitterReceiverinterface都包含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.在这种情况下,接收消息的设备将try尽快响应该消息。

应用程序可以通过调用设备的getReceiver方法来获取MidiDevice的接收者。如果设备无法为程序提供接收器(通常是因为所有设备的接收器都已在使用中),则会抛出MidiUnavailableException。否则,从该方法返回的接收器可供程序立即使用。程序完成使用接收器后,应调用接收器的close方法。如果程序在调用close之后try在接收方上调用方法,则可能引发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 是任意的低调力度值,通常表示final演奏该音符的合成器应大声播放。 (MIDI 规范对速度的精确解释直到合成器对其当前乐器的实现为止.)然后,此 MIDI 消息以-1 的时间戳发送给接收器。现在,我们需要准确检查时间戳参数的含义,这是下一部分的主题。

了解时间戳记

如您所知,MIDI 规范具有不同的部分。一部分描述 MIDI“有线”协议(设备之间实时发送的消息),另一部分描述标准 MIDI 文件(作为事件存储在“序列”中的消息)。在规范的后半部分,标准 MIDI 文件中存储的每个事件都用指示该事件何时应播放的定时值进行标记。相比之下,设备总是在接收到 MIDI 有线协议中的消息后立即对其进行处理,因此它们没有随附的计时值。

Java Sound API 增加了一个附加功能。就像标准 MIDI 文件规范中一样,定时值出现在按 Sequences 存储的MidiEvent对象中(可能从 MIDI 文件读取)也就不足为奇了。但是,在 Java Sound API 中,甚至设备之间发送的消息(换句话说,就是与 MIDI 有线协议相对应的消息)也可以被赋予计时值,称为时间戳。这些时间戳关系到我们。

发送给设备的邮件的时间戳

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

这种时间戳旨在帮助补偿 os 或应用程序引入的延迟。重要的是要意识到,这些时间戳仅用于对计时进行细微调整,而不是用于实现可以在完全任意的时间安排事件的复杂队列(就像MidiEvent计时值一样)。

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

即使设备支持时间戳记,它也可能无法在您请求的确切时间安排事件。您不能指望发送时间戳很远的消息,而让设备按预期的方式处理它,当然也不能指望设备正确地调度时间戳过去的消息!由设备决定如何处理将来或过去相距太远的时间戳。发件人不知道设备认为相距太远,或者设备时间戳是否有问题。这种无知模仿了外部 MIDI 硬件设备的行为,这些设备在发送消息时却不知道它们是否被正确接收。 (MIDI 线协议是单向的.)

有些设备(通过Transmitter)发送带时间戳的消息。例如,由 MIDIImporting 端口发送的消息可能带有传入消息到达端口的时间标记。在某些系统上,事件处理机制会使消息的后续处理过程中失去一定量的计时精度。该消息的时间戳允许保留原始定时信息。

要了解设备是否支持时间戳,请调用以下MidiDevice方法:

long getMicrosecondPosition()

如果设备忽略时间戳,则此方法返回-1.否则,它将返回设备的当前时间概念,在确定后续发送消息的时间戳时,您作为发送方可以将其用作offset量。例如,如果您想在将来发送带有时间戳的消息五毫秒,则可以获取设备的当前位置(以微秒为单位),增加 5000 微秒,并将其用作时间戳。请记住,时间MidiDevice's的概念始终在打开设备时将时间设置为零。

现在,以所有有关时间戳的解释为背景,让我们返回Receiversend方法:

void send(MidiMessage message, long timeStamp)

根据接收设备的时间概念,timeStamp参数以微秒表示。如果设备不支持时间戳,它将仅忽略timeStamp参数。您无需为发送给接收者的邮件加上时间戳。您可以对timeStamp参数使用-1 来表示您不关心调整确切的时间;您只是将其留给接收设备来尽快处理邮件。但是,建议不要将某些消息发送给-1,而将其他消息发送给相同的接收者,同时使用明确的时间戳记。这样做可能会导致final时序不规则。

将发送器连接到接收器

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

连接到单个设备

我们将作为第一个示例的特定情况是将定序器连接到合成器。构建此连接后,启动定序器运行将使合成器根据定序器当前序列中的事件生成音频。现在,我们将忽略将序列从 MIDI 文件加载到音序器的过程。另外,我们将不涉及播放序列的机制。加载和播放序列将在播放,录制和编辑 MIDI 音序中详细讨论。 Synthesizing Sound中讨论了将乐器加载到合成器中的过程。现在,我们感兴趣的只是如何在定序器和合成器之间构建连接。这将说明将一个设备的发送器连接到另一设备的接收器的更一般的过程。

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

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
    }

一个实现实际上可能只有一个对象,它既充当默认的音序器又充当默认的合成器。换句话说,该实现可能使用同时实现Sequencerinterface和Synthesizerinterface的类。在这种情况下,可能没有必要像上面的代码中那样进行显式连接。但是,出于可移植性考虑,不采用这种配置更为安全。当然,如果需要,您可以测试这种情况:

if (seq instanceof Synthesizer)

尽管上面的显式连接在任何情况下都应该起作用。

连接到多个设备

前面的代码示例说明了发送器和接收器之间的一对一连接。但是,如果您需要向多个接收器发送相同的 MIDI 消息怎么办?例如,假设您要从外部设备catch MIDI 数据以驱动内部合成器,同时将数据记录到序列中。这种连接形式(有时称为“扇出”或“分离器”)很简单。以下语句显示了如何创建扇出连接,通过该连接,到达 MIDIImporting 端口的 MIDI 消息既发送到Synthesizer对象又发送到Sequencer对象。我们假设您已经获得并打开了三个设备:Importing 端口,音序器和合成器。 (要获取 Importing 端口,您需要遍历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方法的 Double 重调用,将结果分配给inPortTrans1inPortTrans2。如前所述,一个设备可以拥有多个发送器和接收器。每次为给定设备调用MidiDevice.getTransmitter()时,都会返回另一个发送器,直到没有可用的发送器为止,这时将引发异常。

要了解设备支持多少个 Launcher 和接收器,可以使用以下MidiDevice方法:

int getMaxTransmitters()
    int getMaxReceivers()

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

传输器一次只能将 MIDI 信息传输到一个接收器。 (每次调用Transmitter's setReceiver方法时,现有的Receiver(如果有的话)都会被新指定的方法替换。您可以通过调用Transmitter.getReceiver来判断发送器当前是否具有接收器。)但是,如果设备具有多个发送器,则它可以发送通过将每个发送器连接到不同的接收器,一次将数据传输到一个以上的设备,就像在上面的 Importing 端口中看到的那样。

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

Closing Connections

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

如果还用完了这些设备,则可以通过调用MidiDevice.close()使其类似地用于其他应用程序。关闭设备会自动关闭其所有发送器和接收器。