文档

Java™ 教程
隐藏目录
传输和接收 MIDI 消息
教程:声音

传输和接收MIDI消息

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

Java音频API指定了一种灵活且易于使用的MIDI数据消息路由架构,一旦你理解了它的工作原理,就可以轻松应用。该系统基于模块连接设计:不同的模块,每个模块执行特定的任务,可以相互连接(组成网络),使数据能够从一个模块流向另一个模块。

Java音频API消息系统中的基本模块是MidiDevice接口。MidiDevice包括时序器(用于记录、播放、加载和编辑时间戳MIDI消息序列)、合成器(通过MIDI消息触发时生成声音)以及MIDI输入和输出端口,通过这些端口可以与外部MIDI设备进行数据传输。通常所需的MIDI端口功能由基本的MidiDevice接口描述。SequencerSynthesizer接口扩展了MidiDevice接口,以描述MIDI时序器和合成器的附加功能。作为时序器或合成器的具体类应该实现这些接口。

MidiDevice通常拥有一个或多个实现ReceiverTransmitter接口的辅助对象。这些接口代表连接设备的“插头”或“门户”,允许数据在它们之间流动。通过将一个MidiDeviceTransmitter连接到另一个Receiver,可以创建一个模块网络,数据可以从一个模块流向另一个模块。

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

Transmitter接口包含用于设置和查询发送器发送其MidiMessages的接收器的方法。设置接收器会建立两者之间的连接。Receiver接口包含一个将MidiMessage发送给接收器的方法。通常,此方法由Transmitter调用。TransmitterReceiver接口都包含一个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并将其作为参数传递给Receiversend方法来实现:

  ShortMessage myMsg = new ShortMessage();
  // 开始播放中音C(60),音量适中(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增加了一个额外的变化。毫不奇怪,与标准MIDI文件规范一样,在存储在序列中的MidiEvent对象中也存在时间值(可能从MIDI文件中读取),但是在Java Sound API中,甚至设备之间发送的消息,换句话说,与MIDI线缆协议相对应的消息也可以带有时间值,称为时间戳。这些时间戳就是我们关心的。

发送给设备的消息的时间戳

Java Sound API中可选附带在设备之间发送的消息的时间戳与标准MIDI文件中的时间值非常不同。MIDI文件中的时间值通常基于音乐概念,例如节拍和速度,每个事件的时间值测量自上一个事件以来经过的时间。相比之下,发送到设备的消息的时间戳总是以微秒为单位度量的绝对时间。具体而言,它测量自接收器所属的设备打开以来经过的微秒数。

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

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

即使设备支持时间戳,它也可能不会按照您请求的时间安排事件。您不能期望发送一个时间戳远在未来的消息并让设备按照您的意图处理它,您当然也不能期望设备正确安排一个时间戳过去的消息!设备决定如何处理时间戳距离太远或在过去的情况。发送方不知道设备认为距离太远,或者设备是否有任何时间戳的问题。这种无知模拟了外部MIDI硬件设备的行为,它们发送消息时从未知道消息是否被正确接收。(MIDI线缆协议是单向的。)

一些设备通过传输器(Transmitter)发送带有时间戳的消息。例如,MIDI输入端口发送的消息可能带有消息到达端口的时间戳。在某些系统中,事件处理机制在处理消息时会丢失一定的时间精度。消息的时间戳可以保留原始的时间信息。

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

    long getMicrosecondPosition()

如果设备忽略时间戳,则此方法返回-1。否则,它将返回设备的当前时间概念,您作为发送者可以在确定随后发送的消息的时间戳时使用该时间作为偏移量。例如,如果您想发送一个带有未来五毫秒时间戳的消息,您可以获取设备当前的微秒位置,加上5000微秒,并将其作为时间戳使用。请记住,MidiDevice的时间概念总是将时间零放在设备打开的时间。

现在,有了所有关于时间戳的解释,让我们回到Receiversend方法:

void send(MidiMessage message, long timeStamp)

timeStamp参数以微秒表示,根据接收设备的时间概念。如果设备不支持时间戳,则简单地忽略timeStamp参数。您不需要为发送给接收器的消息加上时间戳。您可以使用-1作为timeStamp参数,表示您不关心调整精确时间;您只是将消息的处理留给接收设备尽快处理。然而,不建议在发送到同一个接收器的消息中同时使用-1和显式时间戳。这样做可能会导致结果时间不规则。

连接传输器到接收器

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

连接到单个设备

我们将以连接一个音序器到一个合成器的特定情况作为第一个例子。完成此连接后,启动音序器将导致合成器根据音序器当前序列中的事件生成音频。现在,我们将忽略从MIDI文件加载序列到音序器中的过程。此外,我们不会涉及播放序列的机制。加载和播放序列的详细讨论请参见播放、录制和编辑MIDI序列。加载乐器到合成器的讨论请参见合成声音。目前,我们只关注如何建立音序器和合成器之间的连接。这将作为连接一个设备的传输器到另一个设备的接收器的更一般过程的说明。

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

    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) {
          // 处理或抛出异常
    }

实现可能实际上有一个同时充当默认序列器和默认合成器的单个对象。换句话说,实现可能使用一个同时实现Sequencer接口和Synthesizer接口的类。在这种情况下,可能不需要进行上面代码中的显式连接。但出于可移植性考虑,最好不要假设这样的配置。当然,如果需要,您可以测试这种情况:

if (seq instanceof Synthesizer)

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

连接多个设备

前面的代码示例演示了一个发射器和接收器之间的一对一连接。但是,如果您需要将相同的MIDI消息发送到多个接收器怎么办?例如,假设您希望从外部设备捕获MIDI数据以驱动内部合成器,同时将数据记录到一个序列中。这种连接形式有时称为“分发”或“分流”,非常简单。以下代码展示了如何创建一个分发连接,通过该连接,到达MIDI输入端口的MIDI消息将同时发送到Synthesizer对象和Sequencer对象。我们假设您已经获得并打开了三个设备:输入端口、序列器和合成器。(要获取输入端口,您需要遍历MidiSystem.getMidiDeviceInfo返回的所有项目。)

    Synthesizer  synth;
    Sequencer    seq;
    MidiDevice   inputPort;
    // [获得并打开三个设备...]
    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) {
          // 处理或抛出异常
    }

这段代码介绍了MidiDevice.getTransmitter方法的双重调用,将结果分别赋给inPortTrans1inPortTrans2。如前所述,一个设备可以拥有多个发射器和接收器。每次对给定设备调用MidiDevice.getTransmitter()时,都会返回另一个发射器,直到没有更多可用的发射器,此时将抛出异常。

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

    int getMaxTransmitters()
    int getMaxReceivers()

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

一个发射器一次只能向一个接收器传输MIDI消息。(每次调用Transmitter的setReceiver方法时,现有的接收器(如果有)将被新指定的接收器替换。您可以通过调用Transmitter.getReceiver来判断发射器当前是否有接收器。)然而,如果设备有多个发射器,它可以通过将每个发射器连接到不同的接收器来同时向多个设备发送数据,就像我们在上面的输入端口的情况下所看到的那样。

同样,设备可以使用其多个接收器同时从多个设备接收。所需的多接收器代码是直接类比上述多发射器代码的简单代码。同一接收器也可以同时接收来自多个发射器的消息。

关闭连接

当您完成一个连接后,可以通过调用您获得的每个发射器和接收器的close方法来释放其资源。 TransmitterReceiver接口都有一个close方法。请注意,调用Transmitter.setReceiver不会关闭发射器的当前接收器。接收器保持打开状态,仍然可以接收来自任何连接到它的其他发射器的消息。

如果您也完成了对设备的使用,可以通过调用MidiDevice.close()将它们提供给其他应用程序。关闭设备会自动关闭其所有发射器和接收器。


上一页: 访问MIDI系统资源
下一页: 引入序列器