Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改以了解Java SE 9及后续版本中更新的语言功能的摘要。
请参阅JDK发行说明以获取有关所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息。
介绍部分简要介绍了Java Sound API的MIDI功能。接下来的讨论将更详细地介绍Java Sound API的MIDI架构,通过javax.sound.midi
包进行访问。还解释了一些MIDI的基本特性,以便将Java Sound API的MIDI功能放入背景中。然后,它继续讨论了Java Sound API对MIDI的处理方式,为后续的编程任务做准备。MIDI API的以下讨论分为两个主要领域:数据和设备。
音乐乐器数字接口(MIDI)标准定义了一种用于电子音乐设备(如电子键盘乐器和个人计算机)的通信协议。MIDI数据可以在现场演出期间通过特殊电缆传输,也可以存储在一种标准类型的文件中以供以后播放或编辑。
本节介绍了一些MIDI基础知识,不涉及Java Sound API。讨论旨在为熟悉MIDI的读者提供复习,同时也是对不熟悉MIDI的读者的简要介绍,为接下来的Java Sound API的MIDI包讨论提供背景。如果您对MIDI有很好的了解,可以安全地跳过本节。在编写实质性的MIDI应用程序之前,对MIDI不熟悉的程序员可能需要更详细的MIDI描述。有关详细的MIDI 1.0规范,请参阅仅在纸质版本中提供的http://www.midi.org(尽管您可能会在网上找到改写或摘要版本)。
MIDI既是硬件规范,也是软件规范。要理解MIDI的设计,了解其历史是有帮助的。MIDI最初是为在合成器等电子键盘乐器之间传递音乐事件(如按键)而设计的。称为音序器的硬件设备存储了可以控制合成器的音符序列,允许录制音乐演奏并随后播放。后来,开发了将MIDI乐器连接到计算机串口的硬件接口,允许在软件中实现音序器。最近,计算机声卡已经整合了MIDI I/O的硬件以及合成音乐声音的硬件。如今,许多MIDI用户只处理声卡,从不连接到外部MIDI设备。由于CPU已经足够快,因此合成器也可以在软件中实现。声卡只需要用于音频I/O,并且在某些应用程序中,用于与外部MIDI设备进行通信。
MIDI规范的硬件部分规定了MIDI电缆的引脚配置以及这些电缆插入的插孔。这部分内容对我们来说并不重要。因为原本需要硬件设备的设备,如序列器和合成器,现在可以在软件中实现,所以大多数程序员了解MIDI硬件设备的唯一原因可能只是为了理解MIDI中的隐喻。然而,外部MIDI硬件设备仍对一些重要的音乐应用至关重要,因此Java Sound API支持MIDI数据的输入和输出。
MIDI规范的软件部分非常广泛。这部分内容涉及到MIDI数据的结构以及合成器等设备应如何对该数据做出响应。重要的是要了解MIDI数据可以是流式的或序列化的。这种二重性反映在完整的MIDI 1.0详细规范的两个不同部分中:
我们将通过检查这两个MIDI规范的每个部分的目的来解释所谓的流式传输和序列化。
这两个MIDI规范部分中的第一个部分描述了非正式地称为"MIDI线协议"的内容。MIDI线协议,也就是原始的MIDI协议,基于这样一个假设,即MIDI数据是通过MIDI电缆(即"线")发送的。电缆将数字数据从一个MIDI设备传输到另一个设备。每个MIDI设备可能是一种乐器或类似设备,也可能是一台配备有MIDI功能的声卡或MIDI串口接口的通用计算机。
MIDI数据,按照MIDI线协议的定义,被组织成消息。不同类型的消息由消息中的第一个字节(称为状态字节)区分。(状态字节是唯一一个最高位设置为1的字节。)消息中跟随状态字节的字节称为数据字节。某些MIDI消息,称为通道消息,其状态字节包含四个位用于指定通道消息的类型和另外四个位用于指定通道号。因此有16个MIDI通道;接收MIDI消息的设备可以设置为响应所有或只有一个这些虚拟通道上的通道消息。通常,每个MIDI通道(不应与音频通道混淆)用于发送不同乐器的音符。例如,两个常见的通道消息是Note On和Note Off,分别用于开始播放音符和停止播放音符。这两个消息都需要两个数据字节:第一个指定音符的音高,第二个指定其"速度"(假设是键盘乐器播放该音符时按下或释放键的速度)。
MIDI线协议定义了MIDI数据的流模型。这个协议的一个核心特点是MIDI数据的字节以实时方式传递,也就是说它们是被流式传输的。数据本身不包含时间信息;每个事件都是在接收到时处理的,并且假定它们在正确的时间到达。这种模型适用于音符由现场音乐家生成的情况,但如果您希望将音符存储以供以后播放,或者希望在非实时情况下组合它们,则不够用。当您意识到MIDI最初是设计用于音乐演奏,作为键盘音乐家控制多个合成器的一种方式(在许多音乐家使用计算机之前),这种限制是可以理解的。(规范的第一个版本发布于1984年。)
MIDI规范的标准MIDI文件部分解决了MIDI线协议中的时间限制。标准MIDI文件是一个包含MIDI事件的数字文件。一个事件只是一个MIDI消息,根据MIDI线协议定义,但附加了一个指定事件时间的额外信息。(还有一些不对应于MIDI线协议消息的事件,我们将在下一节中看到。)附加的时间信息是一系列字节,指示何时执行消息描述的操作。换句话说,标准MIDI文件不仅指定要播放哪些音符,还指定何时播放每个音符。它有点像一个乐谱。
标准MIDI文件中的信息称为序列。标准MIDI文件包含一个或多个轨道。每个轨道通常包含一个单独乐器会在音乐由现场音乐家演奏时演奏的音符。一个音序器是一个可以读取序列并在正确时间传递其中包含的MIDI消息的软件或硬件设备。一个音序器有点像一个管弦乐队指挥:它有所有音符的信息,包括它们的时间,它告诉另一个实体何时执行这些音符。
现在我们已经勾勒出MIDI规范对流式和序列化音乐数据的方法,让我们看看Java Sound API如何表示这些数据。
MidiMessage
是表示“原始”MIDI消息的抽象类。 “原始”MIDI消息通常是由MIDI线协议定义的消息。它还可以是标准MIDI文件规范定义的事件之一,但不包含事件的时间信息。Java Sound API中有三个类别的原始MIDI消息,分别由这三个相应的MidiMessage
子类表示:
ShortMessage
是最常见的消息类型,状态字节后最多跟随两个数据字节。通道消息,例如Note On和Note Off,都是短消息,还有其他一些消息也是。SysexMessage
包含系统专有的MIDI消息。它们可能有很多字节,并且通常包含制造商特定的指令。MetaMessage
出现在MIDI文件中,但不出现在MIDI传输协议中。元消息包含数据,例如歌词或速度设置,对于合成器来说通常没有意义,但对于音序器可能有用。 如前所述,标准MIDI文件包含了用于封装“原始”MIDI消息的事件以及定时信息。Java Sound API的 MidiEvent
类的实例表示了一个存储在标准MIDI文件中的事件。
MidiEvent
的API包括设置和获取事件的定时值的方法。还有一个方法可以检索其嵌入的原始MIDI消息,该消息是MidiMessage
的子类的实例,下面将进行讨论。(嵌入的原始MIDI消息只能在构造MidiEvent
时设置。)
如前所述,标准MIDI文件将事件排列成音轨。通常,文件代表一首音乐作品,每个音轨代表一个乐器演奏的部分。乐器演奏的每个音符由至少两个事件表示:开始音符的Note On事件和结束音符的Note Off事件。音轨还可以包含与音符不对应的事件,例如元事件(上面已提到)。
Java Sound API将MIDI数据组织成三级层次结构:
一个Track
是MidiEvents
的集合,而一个Sequence
是Tracks
的集合。这个层次结构反映了标准MIDI文件规范中的文件、轨道和事件。(注意:这是一个包含和拥有的层次结构,不是一个继承的类层次结构。这三个类直接继承自java.lang.Object
。)
Sequences
可以从MIDI文件中读取,也可以从头开始创建并通过添加Tracks
到Sequence
来进行编辑(或移除)。同样地,可以向序列中的轨道添加或移除MidiEvents
。
前一节解释了MIDI消息在Java Sound API中的表示方式。然而,MIDI消息并不是孤立存在的,它们通常从一个设备发送到另一个设备。使用Java Sound API的程序可以从头开始生成MIDI消息,但更常见的是这些消息由软件设备(如序列器)创建,或通过MIDI输入端口从计算机外部接收。这样的设备通常将这些消息发送给另一个设备,如合成器或MIDI输出端口。
在外部MIDI硬件设备的世界中,许多设备可以将MIDI消息传输给其他设备,也可以从其他设备接收消息。类似地,在Java Sound API中,实现MidiDevice
接口的软件对象可以传输和接收消息。这样的对象可以完全由软件实现,也可以作为与硬件(如声卡的MIDI功能)交互的接口。基本的MidiDevice
接口提供了MIDI输入或输出端口通常需要的所有功能。然而,合成器和序列器进一步实现了MidiDevice
的子接口之一:Synthesizer
或Sequencer
。
MidiDevice
接口包括一个用于打开和关闭设备的API。它还包括一个名为MidiDevice.Info
的内部类,提供设备的文本描述,包括名称、供应商和版本。如果你读过本教程的音频采样部分,这个API可能会很熟悉,因为它的设计类似于javax.sampled.Mixer
接口,它表示音频设备并具有类似的内部类Mixer.Info
。
大多数MIDI设备都能够发送MidiMessages
、接收它们或两者兼具。设备发送数据的方式是通过一个或多个它所“拥有”的发送器对象。类似地,设备接收数据的方式是通过一个或多个它的接收器对象。发送器对象实现了Transmitter
接口,而接收器对象实现了Receiver
接口。
每个发送器一次只能连接到一个接收器,反之亦然。一个将其MIDI消息同时发送给多个其他设备的设备,通过具有多个发送器,每个发送器连接到不同设备的接收器来实现。同样,能够同时从多个来源接收MIDI消息的设备必须通过多个接收器来实现。
音序器是一种用于捕获和播放MIDI事件序列的设备。它具有发送器,因为它通常将存储在序列中的MIDI消息发送到另一个设备,例如合成器或MIDI输出端口。它也具有接收器,因为它可以捕获MIDI消息并将其存储在序列中。在其超级接口MidiDevice
上,Sequencer
添加了基本MIDI序列操作的方法。音序器可以从MIDI文件加载序列,查询和设置序列的速度,并将其他设备与之同步。应用程序可以注册一个对象,以在音序器处理某种类型事件时收到通知。
合成器是一种用于生成声音的设备。它是javax.sound.midi
包中唯一生成音频数据的对象。合成器设备控制一组MIDI通道对象,通常是16个,因为MIDI规范要求16个MIDI通道。这些MIDI通道对象是实现MidiChannel
接口的类的实例,其方法代表了MIDI规范的“通道音符消息”和“通道模式消息”。
应用程序可以通过直接调用合成器的MIDI通道对象的方法来生成声音。然而,更常见的是,合成器响应发送到其一个或多个接收器的消息来生成声音。例如,这些消息可能是由音序器或MIDI输入端口发送的。合成器解析其接收器接收到的每个消息,并通常根据事件中指定的MIDI通道号发送相应的命令(如noteOn
或controlChange
)到其一个MidiChannel
对象中。
MidiChannel
使用这些消息中的音符信息合成音乐。例如,noteOn
消息指定了音符的音高和“速度”(音量)。然而,音符信息是不足的;合成器还需要关于如何为每个音符创建音频信号的精确指令。这些指令由一个 Instrument
表示。每个 Instrument
通常模拟一个不同的真实乐器或音效。这些 Instruments
可能作为合成器的预设,或者可以从声音库文件中加载。在合成器中,Instruments
按照银行号(可以视为行)和程序号(列)进行排列。
本节为理解 MIDI 数据提供了背景,并介绍了与 Java Sound API 中 MIDI 相关的一些重要接口和类。后续章节将展示您如何在应用程序中访问和使用这些对象。