Java教程是为JDK 8编写的。本页面描述的示例和实践不利用后续版本中引入的改进,并可能使用已不再可用的技术。
请参阅Java语言更改,了解Java SE 9及其后续版本中更新的语言特性的摘要。
请参阅JDK发布说明,了解所有JDK版本的新功能、增强功能和已删除或弃用选项的信息。
在MIDI的世界中,序列器是指任何能够精确播放或记录一系列时间戳的MIDI消息的硬件或软件设备。同样,在Java Sound API中,Sequencer
抽象接口定义了一个可以播放和记录MidiEvent
对象序列的对象的属性。一个Sequencer
通常从标准MIDI文件中加载这些MidiEvent
序列,或将它们保存到这样的文件中。序列也可以进行编辑。以下页面将解释如何使用Sequencer
对象以及相关的类和接口来完成这些任务。
为了对Sequencer
是什么有直观的理解,可以将其类比为磁带录音机,在许多方面它们是相似的。磁带录音机播放音频,而序列器播放MIDI数据。一个序列是一个多轨道的、线性的、按时间顺序记录的MIDI音乐数据,序列器可以以不同的速度播放它们,倒带、快进到特定点、录制或复制到文件中进行存储。
传输和接收MIDI消息解释了设备通常拥有Receiver
对象、Transmitter
对象或两者。要播放音乐,设备通常通过Receiver
接收MidiMessages
,而Receiver
通常通过属于Sequencer
的Transmitter
接收它们。拥有此Receiver
的设备可能是一个合成器,它会直接生成音频,或者是一个MIDI输出端口,它通过物理电缆将MIDI数据传输到一些外部设备。同样,要记录音乐,一系列时间戳的MidiMessages
通常被发送到Sequencer
拥有的Receiver
中,它们被放置在一个Sequence
对象中。通常发送消息的对象是与硬件输入端口相关联的Transmitter
,端口通过中继从外部乐器获取MIDI数据。然而,发送消息的设备也可能是其他的Sequencer
,或者任何拥有Transmitter
的设备。此外,正如前面所述,程序可以在不使用任何Transmitter
的情况下发送消息。
Sequencer
本身具有Receivers
和Transmitters
。当它正在录制时,实际上是通过它的Receivers
获取MidiMessages
。在播放期间,它使用它的Transmitters
发送存储在它已经记录(或从文件加载)的Sequence
中的MidiMessages
。
在Java Sound API中,可以将Sequencer
的角色视为MidiMessages
的聚合器和“解聚器”。一系列独立的MidiMessages
被发送到Sequencer
,每个MidiMessages
都有自己的时间戳,标记了音乐事件的时间。这些MidiMessages
被封装在MidiEvent
对象中,并通过Sequencer.record
方法收集在Sequence
对象中。Sequence
是一个包含MidiEvents
聚合的数据结构,通常表示一系列音符,通常是整首歌曲或作品。在播放时,Sequencer
再次从Sequence
中的MidiEvent
对象中提取MidiMessages
,然后将它们传输给一个或多个设备,这些设备可以将它们转换为声音、保存它们、修改它们或将它们传递给其他设备。
有些序列器可能既没有发送器也没有接收器。例如,它们可能会根据键盘或鼠标事件从头开始创建MidiEvents
,而不是通过Receivers
接收MidiMessages
。类似地,它们可能通过与内部合成器直接通信(实际上可以是同一个对象作为序列器)来播放音乐,而不是将MidiMessages
发送到与其他对象关联的Receiver
。然而,本文的其余部分假设使用Receivers
和Transmitters
的正常情况。
应用程序可以直接将MIDI消息发送到设备,而不使用序列器,就像在传输和接收MIDI消息中所描述的那样。程序只需在每次发送消息时调用Receiver.send
方法。这是一种直接的方法,当程序自己实时创建消息时很有用。例如,考虑一个程序,允许用户通过点击屏幕上的钢琴键盘来播放音符。当程序接收到鼠标按下事件时,它会立即向合成器发送相应的音符开启消息。
如前所述,程序可以为发送到设备的每个MIDI消息包括一个时间戳。然而,这些时间戳仅用于微调定时,以纠正处理延迟。调用者通常无法设置任意时间戳;传递给Receiver.send
的时间值必须接近当前时间,否则接收设备可能无法正确安排消息。这意味着如果应用程序想要提前为整个音乐片段创建一个MIDI消息队列(而不是在实时事件的响应中创建每个消息),它必须非常小心地安排每个Receiver.send
的调用,以便接近正确的时间。
幸运的是,大多数应用程序不必担心这样的调度。程序可以使用一个Sequencer
对象来管理其MIDI消息队列,而不是直接调用Receiver.send
。Sequencer负责调度和发送消息,也就是按照正确的时间播放音乐。一般来说,当您需要将非实时的MIDI消息序列转换为实时序列(如播放)或相反(如录制)时,使用sequencer是有利的。Sequencer最常用于从MIDI文件播放数据和从MIDI输入端口录制数据。
在查看Sequencer
API之前,了解存储在序列中的数据类型将会有所帮助。
在Java Sound API中,sequencer在组织记录的MIDI数据的方式上紧密遵循Standard MIDI Files规范。如上所述,Sequence
是按时间组织的MidiEvents
的聚合。但是,Sequence
比仅仅是线性的MidiEvents
序列具有更多结构:Sequence
实际上包含全局的时间信息和一组Tracks
,而Tracks
本身则保存了MidiEvent
数据。因此,sequencer播放的数据由三层次的对象组成:Sequencer
、Track
和MidiEvent
。
在这些对象的传统用法中,Sequence
代表一个完整的音乐作品或作品的一部分,每个Track
对应于合奏中的一个声部或演奏者。在这个模型中,特定Track
上的所有数据也会被编码到为该声部或演奏者保留的特定MIDI通道中。
这种组织数据的方式对于编辑序列是方便的,但请注意这只是使用Tracks
的常规方式。在Track
类的定义中没有任何东西阻止它包含不同MIDI通道上的混合MidiEvents
的情况。例如,整个多通道MIDI作品可以混合并录制到一个Track
上。此外,Type 0的标准MIDI文件(相对于Type 1和Type 2)根据定义只包含一个轨道;因此从这样的文件中读取的Sequence
将必然具有单个Track
对象。
如MIDI包概述中所讨论的,Java Sound API包括与构成大多数标准MIDI消息的原始两个或三个字节序列对应的MidiMessage
对象。MidiEvent
只是一个带有附加的定时值的MidiMessage
的封装。(我们可以说,一个序列实际上由四层或五层数据组成,而不是三层,因为表面上最低层MidiEvent
实际上包含较低级别的MidiMessage
,同样MidiMessage
对象包含一个由标准MIDI消息组成的字节数组。)
在Java Sound API中,有两种不同的方式可以将MidiMessages
与时间值关联起来。一种是在上面提到的“何时使用Sequencer”的方式。这种技术在不使用Transmitter将消息发送到Receiver和理解时间戳下已经详细描述过。在那里,我们看到Receiver
的send
方法接受一个MidiMessage
参数和一个时间戳参数。这种时间戳只能用微秒表示。
另一种指定MidiMessage
时间的方式是将其封装在MidiEvent
中。在这种情况下,时间以稍微抽象的单位称为ticks表示。
一个tick的持续时间是多少?它可以在序列之间(但不能在序列内部)有所不同,并且其值存储在标准MIDI文件的头部。一个tick的大小以以下两种单位之一给出:
如果单位是PPQ,则tick的大小表示为四分音符的一部分,这是一个相对而不是绝对的时间值。四分音符是一个音乐持续时间值,通常对应于音乐的一个节拍(4/4拍子中的四分之一节拍)。四分音符的持续时间取决于节奏,如果序列包含节奏变化事件,节奏可以在音乐的过程中变化。因此,如果序列的时间增量(ticks)每个四分音符发生96次,那么每个事件的时间值以音乐术语来衡量该事件在音乐中的位置,而不是作为绝对时间值。
另一方面,在SMPTE的情况下,单位测量绝对时间,而节奏的概念是不适用的。实际上,有四种不同的SMPTE约定可供选择,它们指的是每秒的电影帧数。每秒帧数可以是24、25、29.97或30。使用SMPTE时间码,tick的大小表示为一帧的一部分。
在Java Sound API中,您可以调用Sequence.getDivisionType
来了解特定序列中使用的单位类型,即PPQ或SMPTE单位之一。然后,您可以在调用Sequence.getResolution
之后计算tick的大小。如果分割类型是PPQ,则后者方法返回每个四分音符的tick数;如果分割类型是SMPTE约定之一,则返回每个SMPTE帧的tick数。在PPQ的情况下,您可以使用以下公式获取tick的大小:
ticksPerSecond = resolution * (currentTempoInBeatsPerMinute / 60.0); tickSize = 1.0 / ticksPerSecond;
framesPerSecond = (divisionType == Sequence.SMPTE_24 ? 24 : (divisionType == Sequence.SMPTE_25 ? 25 : (divisionType == Sequence.SMPTE_30 ? 30 : (divisionType == Sequence.SMPTE_30DROP ?
29.97)))); ticksPerSecond = resolution * framesPerSecond; tickSize = 1.0 / ticksPerSecond;
Java Sound API中的时间定义与标准MIDI文件规范相同。然而,有一个重要的区别。在MidiEvents
中包含的tick值衡量的是累积时间,而不是增量时间。在标准MIDI文件中,每个事件的定时信息衡量的是自前一个事件开始的时间流逝量,这称为增量时间。但在Java Sound API中,ticks不是增量值,它们是前一个事件的时间值加上增量值。换句话说,在Java Sound API中,每个事件的定时值始终大于序列中前一个事件的定时值(如果事件应该同时发生,则相等)。每个事件的定时值衡量的是自序列开始以来的时间流逝。
总之,Java Sound API使用MIDI ticks或微秒表示定时信息。 MidiEvents
以MIDI ticks的形式存储定时信息。可以从Sequence
的全局定时信息和(如果序列使用基于节拍的定时)当前的音乐节拍计算出tick的持续时间。另一方面,发送到Receiver
的MidiMessage
的时间戳总是以微秒表示。
这种设计的一个目标是避免时间概念的冲突。一个Sequencer
的工作是解释其MidiEvents
中的时间单位,这些单位可能是PPQ单位,并将其转换为以微秒为单位的绝对时间,考虑当前的节拍。此外,序列器还必须将微秒表达为接收消息的设备打开时的相对时间。请注意,一个序列器可以有多个发射器,每个发射器将消息传递给一个可能与完全不同的设备关联的不同接收器。因此,可以看出,序列器必须能够同时执行多个转换,确保每个设备接收到适合其时间概念的时间戳。
更复杂的是,不同的设备可能基于不同的源(例如操作系统的时钟或由声卡维护的时钟)更新其时间概念。这意味着它们的时间可能相对于序列器的时间漂移。为了与序列器保持同步,一些设备允许自己成为序列器的时间概念的"从属"。设置主从关系将在后面的MidiEvent
中讨论。