排序器简介

原文: https://docs.oracle.com/javase/tutorial/sound/MIDI-seq-intro.html

在 MIDI 的世界中,音序器是可以精确播放或录制带有时间戳的 MIDI 信息的序列的任何硬件或软件设备。类似地,在 Java Sound API 中, Sequencer 抽象接口定义了可以播放和记录 MidiEvent 对象序列的对象的属性。 Sequencer通常从标准 MIDI 文件加载这些MidiEvent序列或将它们保存到这样的文件中。序列也可以编辑。以下几页介绍了如何使用Sequencer对象以及相关的类和接口来完成此类任务。

为了直观地理解Sequencer是什么,可以通过类似于磁带录音机的方式来考虑它,音序器在很多方面都很像。录音机播放音频时,音序器播放 MIDI 数据。序列是 MIDI 音乐数据的多轨,线性,时间顺序记录,音序器可以以各种速度播放,倒带,穿梭到特定点,记录到或复制到文件以便存储。

发送和接收 MIDI 信息解释说设备通常具有Receiver对象,Transmitter对象或两者。对于播放音乐,设备通常通过Receiver接收MidiMessagesReceiver通常通过属于SequencerTransmitter接收它们。拥有此Receiver的设备可能是Synthesizer,它将直接生成音频,或者它可能是 MIDI 输出端口,它通过物理电缆将 MIDI 数据传输到某些外部设备。类似地,对于记录音乐,一系列带时间戳的MidiMessages通常被发送到Sequencer拥有的Receiver,它将它们放在Sequence对象中。通常,发送消息的对象是与硬件输入端口相关联的Transmitter,并且端口中继从外部仪器获得的 MIDI 数据。但是,负责发送消息的设备可能是某些其他Sequencer或具有Transmitter的任何其他设备。此外,如前所述,程序可以在不使用任何Transmitter的情况下发送消息。

A Sequencer本身同时具有ReceiversTransmitters。当它正在录制时,它实际上通过其Receivers获得MidiMessages。在播放过程中,它使用Transmitters发送存储在Sequence中的MidiMessages,它已记录(或从文件加载)。

在 Java Sound API 中考虑Sequencer字符的一种方法是作为MidiMessages的聚合器和“解聚合器”。一系列独立的MidiMessages(每个都是独立的)被发送到Sequencer以及它自己的标记音乐事件定时的时间戳。这些MidiMessages封装在MidiEvent对象中,并通过Sequencer.record方法的作用收集在Sequence对象中。 Sequence是包含MidiEvents的聚合的数据结构,它通常表示一系列音符,通常是整首歌曲或乐曲。播放时,Sequencer再次从Sequence中的MidiEvent对象中提取MidiMessages,然后将它们发送到一个或多个设备,这些设备将它们渲染成声音,保存,修改或传递它们在某些其他设备上。

某些音序器可能既没有发射器也没有接收器。例如,他们可能会因键盘或鼠标事件而从头开始创建MidiEvents,而不是通过Receivers接收MidiMessages。类似地,他们可以通过直接与内部合成器(实际上可能与定序器相同的对象)进行通信来播放音乐,而不是将MidiMessages发送到与单独对象相关联的Receiver。但是,本讨论的其余部分假设使用ReceiversTransmitters的序列发生器的正常情况。

何时使用音序器

应用程序可以直接将 MIDI 信息发送到设备,而不使用音序器,如发送和接收 MIDI 信息中所述。程序只是在每次想要发送消息时调用Receiver.send方法。这是一种直接的方法,当程序本身实时创建消息时非常有用。例如,考虑一个程序,让用户通过点击屏幕钢琴键盘来演奏音符。当程序获得鼠标按下事件时,它会立即向合成器发送相应的 Note On 消息。

如前所述,程序可以包含一个时间戳,每个 MIDI 消息都发送到设备的接收器。但是,这些时间戳仅用于微调时序,以纠正处理延迟。呼叫者通常不能设置任意时间戳;传递给Receiver.send的时间值必须接近当前时间,否则接收设备可能无法正确安排消息。这意味着如果应用程序想要提前为整段音乐创建 MIDI 消息队列(而不是为了响应实时事件而创建每条消息),则必须非常小心地安排每个音乐。几乎在适当的时候调用Receiver.send

幸运的是,大多数应用程序不必关心这样的调度。程序可以使用Sequencer对象来管理 MIDI 消息队列,而不是调用Receiver.send本身。音序器负责调度和发送消息 - 换句话说,以正确的时序播放音乐。通常,每当您需要将非实时系列的 MIDI 信息转换为实时系列(如播放时)或反之亦然(如录制时)时,使用音序器是有利的。排序器最常用于播放 MIDI 文件中的数据和从 MIDI 输入端口录制数据。

了解序列数据

在检查Sequencer API 之前,它有助于理解序列中存储的数据类型。

序列和曲目

在 Java Sound API 中,序列发生器以组织录制的 MIDI 数据的方式严格遵循标准 MIDI 文件规范。如上所述,SequenceMidiEvents的聚合,按时组织。但是Sequence的结构比MidiEvents的线性系列更多:Sequence实际上包含全局定时信息加上Tracks的集合,而Tracks本身也包含Tracks Sequence ]数据。因此,序列发生器播放的数据由三级对象层次结构组成:SequencerTrackMidiEvent

在这些对象的常规使用中,Sequence表示完整的音乐作品或乐曲的一部分,每个Track对应于整体中的声音或演奏者。在此模型中,特定Track上的所有数据因此也将被编码到为该语音或播放器保留的特定 MIDI 通道中。

这种组织数据的方式很方便编辑序列,但请注意,这只是使用Tracks的传统方法。 Track类的定义中没有任何内容可以防止它在不同的 MIDI 通道上包含MidiEvents的混合。例如,可以将整个多声道 MIDI 合成混合并记录到一个Track上。此外,类型 0 的标准 MIDI 文件(与类型 1 和类型 2 相对)根据定义仅包含一个轨道;所以从这样的文件中读取的Sequence必然会有一个Track对象。

MidiEvents 和 Ticks

MIDI 包概述中所述,Java Sound API 包含MidiMessage对象,这些对象对应于构成大多数标准 MIDI 消息的原始双字节或三字节序列。 MidiEvent只是MidiMessage的包装以及指定事件发生时间的伴随定时值。 (我们可能会说序列实际上由四级或五级数据层次组成,而不是三级,因为表面上的最低级别MidiEvent实际上包含一个较低级别的MidiMessage,同样MidiMessage对象包含一个包含标准 MIDI 消息的字节数组。)

在 Java Sound API 中,MidiMessages可以通过两种不同的方式与时序值相关联。一种是上面“何时使用音序器”中提到的方法。在不使用发送器理解时间戳向接收器发送消息的情况下详细描述了该技术。在那里,我们看到Receiversend方法采用MidiMessage参数和时间戳参数。这种时间戳只能用微秒表示。

MidiMessage指定其时序的另一种方式是封装在MidiEvent中。在这种情况下,时间以稍微更抽象的单位表示,称为 ticks

嘀嗒的持续时间是多长?它可以在序列之间变化(但不在序列中),并且其值存储在标准 MIDI 文件的标题中。刻度的大小以两种单位之一给出:

  • 每季度的脉冲(滴答),缩写为 PPQ
  • 每帧刻度,也称为 SMPTE 时间码(电影电视工程师协会采用的标准)

如果单位是 PPQ,则刻度的大小表示为四分音符的分数,它是相对的,而不是绝对的时间值。四分音符是音乐持续时间值,其通常对应于音乐的一个节拍(四分之一时间的四分之一)。四分音符的持续时间取决于节奏,如果音序包含节奏变化事件,则速度可以在音乐过程中变化。因此,如果序列的时间增量(滴答)发生,例如每四分音符 96 次,则每个事件的定时值以音乐术语测量该事件的位置,而不是绝对时间值。

另一方面,在 SMPTE 的情况下,单位测量绝对时间,并且节奏的概念不适用。实际上有四种不同的 SMPTE 约定,它们指的是每秒的运动图像帧数。每秒帧数可以是 24,25,29.97 或 30.对于 SMPTE 时间码,刻度的大小表示为帧的一小部分。

在 Java Sound API 中,您可以调用Sequence.getDivisionType来了解在特定序列中使用哪种类型的单元,即 PPQ 或其中一个 SMPTE 单元。然后,您可以在调用Sequence.getResolution后计算刻度的大小。如果除法类型是 PPQ,则后一种方法返回每四分音符的滴答数,如果除法类型是 SMPTE 约定之一,则返回每个 SMPTE 帧。在 PPQ 的情况下,您可以使用此公式获得刻度的大小:

  1. ticksPerSecond =
  2. resolution * (currentTempoInBeatsPerMinute / 60.0);
  3. tickSize = 1.0 / ticksPerSecond;

和 SMPTE 案例中的这个公式:

  1. framesPerSecond =
  2. (divisionType == Sequence.SMPTE_24 ? 24
  3. : (divisionType == Sequence.SMPTE_25 ? 25
  4. : (divisionType == Sequence.SMPTE_30 ? 30
  5. : (divisionType == Sequence.SMPTE_30DROP ?
  6. 29.97))));
  7. ticksPerSecond = resolution * framesPerSecond;
  8. tickSize = 1.0 / ticksPerSecond;

Java Sound API 对序列中时序的定义反映了标准 MIDI 文件规范的定义。但是,有一个重要的区别。 MidiEvents中包含的刻度值测量累积时间,而不是 delta 时间。在标准 MIDI 文件中,每个事件的定时信息测量自序列中上一个事件开始以来经过的时间量。这称为增量时间。但是在 Java Sound API 中,刻度不是 delta 值;它们是前一事件的时间值加上的 delta 值。换句话说,在 Java Sound API 中,每个事件的时间值总是大于序列中前一个事件的时间值(或者如果事件应该是同时的,则相等)。每个事件的计时值测量自序列开始以来经过的时间。

总而言之,Java Sound API 以 MIDI 刻度或微秒表示时序信息。 MidiEvents以 MIDI 刻度存储定时信息。可以从Sequence's全局定时信息计算滴答的持续时间,并且如果序列使用基于速度的定时,则可以计算当前的音乐节奏。另一方面,与发送到ReceiverMidiMessage相关联的时间戳始终以微秒表示。

这个设计的一个目标是避免冲突的时间概念。 Sequencer的作用是解释其MidiEvents中可能具有 PPQ 单位的时间单位,并将这些时间单位转换为以微秒为单位的绝对时间,并考虑当前的速度。定序器还必须表示相对于打开接收消息的设备的时间的微秒。请注意,定序器可以有多个发送器,每个发送器将消息传送到可能与完全不同的设备相关联的不同接收器。然后,您可以看到,序列发生器必须能够同时执行多个转换,确保每个设备都接收适合其时间概念的时间戳。

为了使问题更复杂,不同的设备可能会根据不同的来源(例如操​​作系统的时钟或声卡维护的时钟)更新其时间概念。这意味着它们的时序可能相对于音序器而言漂移。为了与定序器保持同步,一些设备允许自己成为定序器时间概念的“奴隶”。设置主设备和从设备将在后面的 MidiEvent 中讨论。