音序器简介

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

要想直观地了解Sequencer是什么,可比拟录音机,就像音序器在许多方面都类似。磁带录音机播放音频,而音序器播放 MIDI 数据。音序是 MIDI 音乐数据的多轨,线性,有时间 Sequences 的录音,音序器可以各种速度播放,倒带,穿梭到特定点,记录或复制到文件中进行存储。

传输和接收 MIDI 信息解释说,设备通常具有Receiver个对象,Transmitter个对象或两者。为了“播放”音乐,设备通常通过Receiver接收MidiMessages,而Receiver通常又从属于SequencerTransmitter接收它们。拥有此Receiver的设备可能是Synthesizer,它将直接生成音频,或者可能是 MIDI 输出端口,该端口通过物理电缆将 MIDI 数据传输到某些外部设备。同样,要录制音乐,通常会将一系列带时间戳的MidiMessages发送到Sequencer所拥有的Receiver,然后将它们放置在Sequence对象中。通常,发送消息的对象是与硬件 Importing 端口关联的Transmitter,并且该端口中继从外部乐器获取的 MIDI 数据。但是,负责发送消息的设备可能是其他Sequencer或具有Transmitter的任何其他设备。此外,如前所述,程序可以完全不使用任何Transmitter发送消息。

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

想到Sequencer在 Java Sound API 中的作用的一种方法是作为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 消息的非实时序列转换为实时序列(如在播放中),反之亦然(如 Logging)时,使用音序器是有利的。音序器最常用于播放 MIDI 文件中的数据和录制来自 MIDIImporting 端口的数据。

了解序列数据

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

序列和曲目

在 Java Sound API 中,音序器以组织录制的 MIDI 数据的方式严格遵循“标准 MIDI 文件”规范。如上所述,SequenceMidiEvents的集合,按时间组织。但是Sequence的结构不仅仅是线性的MidiEvents系列:Sequence实际上包含全局时序信息以及Tracks的集合,而Tracks本身保存MidiEvent数据。因此,音序器播放的数据由三层对象组成:SequencerTrackMidiEvent

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

这种组织数据的方式对于编辑序列很方便,但是请注意,这只是使用Tracks的常规方式。 Track类的定义中没有什么可以阻止它在不同的 MIDI 通道上包含MidiEvents的混合。例如,可以混合整个多通道 MIDI 合成并将其记录到一个Track上。同样,根据定义,类型 0(与类型 1 和类型 2 相对)的标准 MIDI 文件仅包含一个音轨。因此,从此类文件中读取的Sequence将必然具有单个Track对象。

Midi 活动和壁虱

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

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

MidiMessage可以指定其时间的另一种方法是将其封装在MidiEvent中。在这种情况下,时间用称为* ticks *的抽象单位表示。

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

  • 每四分音符个脉冲(滴答声),缩写为 PPQ

  • 每帧刻度,也称为 SMPTE 时间码(美国电影电视工程师协会采用的标准)

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

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

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

ticksPerSecond =  
    resolution * (currentTempoInBeatsPerMinute / 60.0);
tickSize = 1.0 / ticksPerSecond;

和 SMPTE 的公式:

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中包含的刻度值测量的是“累积”时间,而不是“增量”时间。在标准 MIDI 文件中,每个事件的定时信息都测量自序列中上一个事件发生以来经过的时间。这称为增量时间。但是在 Java Sound API 中,刻度不是增量值;它们是前一个事件的时间值加上增量值。换句话说,在 Java Sound API 中,每个事件的时序值始终大于序列中前一个事件的时序值(如果假定这些事件是同时发生的,则该值等于)。每个事件的计时值衡量自序列开始以来经过的时间。

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

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

更复杂的是,不同的设备可能会基于不同的来源(例如 os 的时钟或声卡维护的时钟)来更新其时间概念。这意味着它们的时序可能会相对于音序器漂移。为了与定序器保持同步,某些设备允许自己成为定序器的时间概念的“奴隶”。稍后在MidiEvent下讨论设置主机和从机。