Java 中文官方教程 2022 版(三十三)(1)

简介: Java 中文官方教程 2022 版(三十三)

MIDI 包概述

原文:docs.oracle.com/javase/tutorial/sound/overview-MIDI.html

介绍简要介绍了 Java Sound API 的 MIDI 功能。接下来的讨论更详细地介绍了通过javax.sound.midi包访问的 Java Sound API 的 MIDI 架构。解释了一些 MIDI 本身的基本特性,作为一个复习或介绍,以便将 Java Sound API 的 MIDI 功能放入上下文中。然后继续讨论 Java Sound API 对 MIDI 的处理,为后续部分中解释的编程任务做准备。对 MIDI API 的讨论分为两个主要领域:数据和设备。

MIDI 复习:线缆和文件

音乐器数字接口(MIDI)标准定义了一种用于电子音乐设备(如电子键盘乐器和个人计算机)的通信协议。MIDI 数据可以在现场表演期间通过特殊电缆传输,并且还可以存储在标准类型的文件中以供以后播放或编辑。

本节回顾了一些 MIDI 基础知识,不涉及 Java Sound API。这个讨论旨在为熟悉 MIDI 的读者提供一个复习,为不熟悉的读者提供一个简要介绍,为随后讨论 Java Sound API 的 MIDI 包提供背景。如果您对 MIDI 有深入了解,可以安全地跳过本节。在编写大量 MIDI 应用程序之前,不熟悉 MIDI 的程序员可能需要比本教程中包含的更详细的 MIDI 描述。请参阅仅在硬拷贝中提供的完整 MIDI 1.0 详细规范,可从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 数据可以流式传输序列化。这种二元性反映了 Complete MIDI 1.0 详细规范的两个不同部分:

  • MIDI 1.0
  • 标准 MIDI 文件

通过检查 MIDI 规范的这两个部分的目的,我们将解释流式传输和序列化的含义。

MIDI 线协议中的流数据

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 对 MIDI 数据的表示

现在我们已经勾勒出 MIDI 规范对流式和序列化音乐数据的处理方式,让我们来看看 Java Sound API 如何表示这些数据。

MIDI 消息

MidiMessage 是表示“原始” MIDI 消息的抽象类。一个“原始” MIDI 消息通常是由 MIDI 传输协议定义的消息。它也可以是标准 MIDI 文件规范中定义的事件之一,但没有事件的时间信息。在 Java Sound API 中,有三类原始 MIDI 消息,分别由这三个相应的 MidiMessage 子类表示:

  • ShortMessages是最常见的消息,状态字节后最多有两个数据字节。通道消息,如 Note On 和 Note Off,都是短消息,还有一些其他消息也是短消息。
  • SysexMessages包含系统专用的 MIDI 消息。它们可能有许多字节,并且通常包含制造商特定的指令。
  • MetaMessages出现在 MIDI 文件中,但不出现在 MIDI 线协议中。元消息包含数据,例如歌词或速度设置,这对于音序器可能很有用,但通常对合成器没有意义。

MIDI 事件

正如我们所见,标准 MIDI 文件包含用于包装“原始”MIDI 消息以及时间信息的事件。Java Sound API 的MidiEvent类的实例代表了一个类似于标准 MIDI 文件中存储的事件。

MidiEvent的 API 包括设置和获取事件的时间值的方法。还有一个方法用于检索其嵌入的原始 MIDI 消息,这是MidiMessage子类的实例,接下来会讨论。(嵌入的原始 MIDI 消息只能在构造MidiEvent时设置。)

序列和轨道

如前所述,标准 MIDI 文件存储被安排到轨道中的事件。通常文件代表一个音乐作品,通常每个轨道代表一个部分,例如可能由单个乐器演奏。乐器演奏的每个音符至少由两个事件表示:开始音符的 Note On 和结束音符的 Note Off。轨道还可能包含不对应音符的事件,例如元事件(如上所述)。

Java Sound API 将 MIDI 数据组织成三部分层次结构:

TrackMidiEvents的集合,SequenceTracks的集合。这种层次结构反映了标准 MIDI 文件规范中的文件、轨道和事件。(注意:这是一种包含和拥有的层次结构;这不是一种继承的类层次结构。这三个类直接继承自java.lang.Object。)

Sequences可以从 MIDI 文件中读取,也可以从头开始创建并通过向Sequence添加Tracks(或删除它们)进行编辑。同样,MidiEvents可以添加到序列中的轨道中,也可以从中删除。

Java Sound API 对 MIDI 设备的表示

前一节解释了 MIDI 消息在 Java Sound API 中的表示方式。然而,MIDI 消息并不是独立存在的。它们通常从一个设备发送到另一个设备。使用 Java Sound API 的程序可以从头开始生成 MIDI 消息,但更常见的情况是这些消息是由软件设备(如序列器)创建的,或者通过 MIDI 输入端口从计算机外部接收。这样的设备通常会将这些消息发送到另一个设备,比如合成器或 MIDI 输出端口。

MidiDevice 接口

在外部 MIDI 硬件设备的世界中,许多设备可以将 MIDI 消息传输到其他设备,并从其他设备接收消息。同样,在 Java Sound API 中,实现MidiDevice接口的软件对象可以传输和接收消息。这样的对象可以纯粹在软件中实现,也可以作为硬件的接口,比如声卡的 MIDI 功能。基本的MidiDevice接口提供了 MIDI 输入或输出端口通常所需的所有功能。然而,合成器和序列器进一步实现了MidiDevice的子接口之一:SynthesizerSequencer

MidiDevice接口包括用于打开和关闭设备的 API。它还包括一个名为MidiDevice.Info的内部类,提供设备的文本描述,包括其名称、供应商和版本。如果您已经阅读了本教程的采样音频部分,那么这个 API 可能会听起来很熟悉,因为其设计类似于javax.sampled.Mixer接口,代表音频设备,并且具有类似的内部类Mixer.Info

发送器和接收器

大多数 MIDI 设备都能够发送MidiMessages、接收它们,或两者兼而有之。设备发送数据的方式是通过它“拥有”的一个或多个发送器对象。同样,设备接收数据的方式是通过一个或多个接收器对象。发送器对象实现了Transmitter接口,而接收器实现了Receiver接口。

每个发送器一次只能连接到一个接收器,反之亦然。一个设备如果同时向多个其他设备发送其 MIDI 消息,则通过拥有多个发送器,每个发送器连接到不同设备的接收器来实现。同样,一个设备如果要同时从多个来源接收 MIDI 消息,则必须通过多个接收器来实现。

序列器

一个序列器是一种捕获和播放 MIDI 事件序列的设备。它具有发射器,因为它通常将存储在序列中的 MIDI 消息发送到另一个设备,例如合成器或 MIDI 输出端口。它还具有接收器,因为它可以捕获 MIDI 消息并将其存储在序列中。在其超接口MidiDevice中,Sequencer添加了用于基本 MIDI 序列操作的方法。序列器可以从 MIDI 文件加载序列,查询和设置序列的速度,并将其他设备与其同步。应用程序可以注册一个对象,以便在序列器处理某些类型的事件时收到通知。

合成器

Synthesizer是一种产生声音的设备。它是javax.sound.midi包中唯一产生音频数据的对象。合成器设备控制一组 MIDI 通道对象 - 通常是 16 个,因为 MIDI 规范要求有 16 个 MIDI 通道。这些 MIDI 通道对象是实现MidiChannel接口的类的实例,其方法代表 MIDI 规范的“通道音频消息”和“通道模式消息”。

应用程序可以通过直接调用合成器的 MIDI 通道对象的方法来生成声音。然而,更常见的情况是,合成器响应发送到其一个或多个接收器的消息而生成声音。例如,这些消息可能是由序列器或 MIDI 输入端口发送的。合成器解析其接收器接收到的每条消息,并通常根据事件中指定的 MIDI 通道号将相应的命令(如noteOncontrolChange)发送到其一个MidiChannel对象。

MidiChannel使用这些消息中的音符信息来合成音乐。例如,noteOn消息指定了音符的音高和“速度”(音量)。然而,音符信息是不够的;合成器还需要关于如何为每个音符创建音频信号的精确指令。这些指令由一个Instrument表示。每个Instrument通常模拟不同的真实乐器或音效。Instruments可能作为合成器的预设提供,也可能从声音库文件中加载。在合成器中,Instruments按照银行号(可以将其视为行)和程序号(列)进行排列。

本节为理解 MIDI 数据提供了背景,并介绍了与 Java Sound API 中的 MIDI 相关的一些重要接口和类。后续章节将展示如何在应用程序中访问和使用这些对象。

访问 MIDI 系统资源

原文:docs.oracle.com/javase/tutorial/sound/accessing-MIDI.html

Java Sound API 为 MIDI 系统配置提供了灵活的模型,就像为采样音频系统配置一样。Java Sound API 的实现本身可以提供不同类型的 MIDI 设备,并且服务提供者和用户可以提供并安装其他设备。您可以编写程序,使其对计算机上安装的具体 MIDI 设备做出少量假设。相反,程序可以利用 MIDI 系统的默认设置,或者允许用户从可用设备中选择。

本节展示了您的程序如何了解已安装的 MIDI 资源,以及如何访问所需的资源。在访问并打开设备之后,您可以将它们连接在一起,如后面的 传输和接收 MIDI 消息 中所讨论的。

MidiSystem 类

Java Sound API 的 MIDI 包中的 MidiSystem 类的作用与采样音频包中的 AudioSystem 类的作用直接类似。MidiSystem 充当访问已安装 MIDI 资源的中转站。

您可以查询MidiSystem以了解安装了什么类型的设备,然后可以遍历可用设备并访问所需设备。例如,一个应用程序可能首先询问MidiSystem有哪些合成器可用,然后显示一个列表,用户可以从中选择一个。一个更简单的应用程序可能只使用系统的默认合成器。

MidiSystem 类还提供了在 MIDI 文件和Sequences之间进行转换的方法。它可以报告 MIDI 文件的文件格式,并且可以写入不同类型的文件。

应用程序可以从MidiSystem获取以下资源:

  • 顺序器
  • 合成器
  • 发射器(例如与 MIDI 输入端口相关联的发射器)
  • 接收器(例如与 MIDI 输出端口相关联的接收器)
  • 来自标准 MIDI 文件的数据
  • 来自声音库文件的数据

本页重点介绍了这些类型资源中的前四种。其他类型将在本教程的后面讨论。

获取默认设备

使用 Java Sound API 的典型 MIDI 应用程序程序首先获取所需的设备,这些设备可以包括一个或多个顺序器、合成器、输入端口或输出端口。

有一个默认的合成器设备,一个默认的定序器设备,一个默认的传输设备和一个默认的接收设备。后两个设备通常代表系统上可用的 MIDI 输入和输出端口,如果有的话。(在这里很容易混淆方向性。将端口的传输或接收视为与软件相关,而不是与连接到物理端口的任何外部物理设备相关。MIDI 输入端口传输来自外部设备的数据到 Java Sound API 的Receiver,同样,MIDI 输出端口接收来自软件对象的数据并将数据中继到外部设备。)

一个简单的应用程序可能只使用默认设备而不探索所有安装的设备。MidiSystem类包括以下方法来检索默认资源:

static Sequencer getSequencer()
static Synthesizer getSynthesizer()
static Receiver getReceiver()
static Transmitter getTransmitter()

前两种方法获取系统的默认排序和合成资源,这些资源可以代表物理设备或完全在软件中实现。getReceiver方法获取一个Receiver对象,该对象接收发送给它的 MIDI 消息并将其中继到默认接收设备。类似地,getTransmitter方法获取一个 Transmitter 对象,该对象可以代表默认传输设备向某个接收设备发送 MIDI 消息。

学习安装了哪些设备

与使用默认设备不同,更彻底的方法是从系统上安装的所有设备中选择所需的设备。应用程序可以通过编程方式选择所需的设备,或者可以显示可用设备列表,让用户选择要使用的设备。MidiSystem类提供了一个方法来了解安装了哪些设备,以及一个相应的方法来获取给定类型的设备。

以下是学习已安装设备的方法:

static MidiDevice.Info[] getMidiDeviceInfo()

如您所见,它返回一个信息对象数组。每个返回的MidiDevice.Info对象标识已安装的一种类型的定序器、合成器、端口或其他设备。(通常系统最多只有一个给定类型的实例。例如,来自某个供应商的特定型号的合成器只会安装一次。)MidiDevice.Info包括以下字符串来描述设备:

  • 名称
  • 版本号
  • 厂商(创建设备的公司)
  • 设备的描述

您可以在用户界面中显示这些字符串,让用户从设备列表中进行选择。

然而,要在程序中使用字符串来选择设备(而不是向用户显示字符串),你需要事先知道可能的字符串是什么。每个提供设备的公司应该在其文档中包含这些信息。需要或偏好特定设备的应用程序可以利用这些信息来定位该设备。这种方法的缺点是将程序限制在事先知道的设备实现上。

另一种更一般的方法是继续遍历MidiDevice.Info对象,获取每个相应的设备,并以编程方式确定是否适合使用(或至少适合包含在用户可以选择的列表中)。下一节将介绍如何执行此操作。

获取所需设备

一旦找到适当设备的信息对象,应用程序调用以下MidiSystem方法来获取相应的设备本身:

static MidiDevice getMidiDevice(MidiDevice.Info info)

如果您已经找到描述所需设备的信息对象,可以使用此方法。但是,如果无法解释getMidiDeviceInfo返回的信息对象以确定所需设备,且不想向用户显示所有设备的信息,您可以尝试以下操作:遍历getMidiDeviceInfo返回的所有MidiDevice.Info对象,使用上述方法获取相应设备,并测试每个设备以查看其是否合适。换句话说,您可以在将设备包含在向用户显示的列表中之前,查询每个设备的类和功能,或者以编程方式决定设备而不涉及用户。例如,如果您的程序需要合成器,可以获取每个已安装的设备,查看哪些是实现Synthesizer接口的类的实例,然后将它们显示在用户可以选择的列表中,如下所示:

// Obtain information about all the installed synthesizers.
Vector synthInfos;
MidiDevice device;
MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
for (int i = 0; i < infos.length; i++) {
    try {
        device = MidiSystem.getMidiDevice(infos[i]);
    } catch (MidiUnavailableException e) {
          // Handle or throw exception...
    }
    if (device instanceof Synthesizer) {
        synthInfos.add(infos[i]);
    }
}
// Now, display strings from synthInfos list in GUI. 

作为另一个示例,您可以以编程方式选择设备,而不涉及用户。假设您想获取可以同时播放最多音符的合成器。您遍历所有MidiDevice.Info对象,如上所述,但在确定设备是合成器后,通过调用SynthesizergetMaxPolyphony方法查询其功能。您保留具有最大音色的合成器,如下一节所述。即使您不要求用户选择合成器,您可能仍然显示所选MidiDevice.Info对象的字符串,仅供用户参考。

打开设备

前一节展示了如何获取已安装的设备。然而,设备可能已安装但不可用。例如,另一个应用程序可能独占使用它。要为您的程序实际保留设备,您需要使用MidiDevice方法open

if (!(device.isOpen())) {
    try {
      device.open();
  } catch (MidiUnavailableException e) {
          // Handle or throw exception...
  }
}

一旦您访问了设备并通过打开它来预留了它,您可能希望将其连接到一个或多个其他设备,以便让 MIDI 数据在它们之间流动。这个过程在传输和接收 MIDI 消息中有描述。

当完成对设备的操作后,通过调用MidiDeviceclose方法释放它,以便其他程序可以使用。

传输和接收 MIDI 消息

原文:docs.oracle.com/javase/tutorial/sound/MIDI-messages.html

理解设备、发射器和接收器

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

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

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

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();
  // Start playing the note Middle C (60), 
  // moderately loud (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 添加了一个额外的变化。毫不奇怪,MidiEvent 对象中存在时间值,这些对象存储在序列中(可能是从 MIDI 文件中读取的),就像标准 MIDI 文件规范中一样。但是在 Java Sound API 中,甚至在设备之间发送的消息——换句话说,对应 MIDI 传输协议的消息——也可以被赋予时间值,称为时间戳。我们关心的是这些时间戳。

发送到设备的消息上的时间戳

Java Sound API 中在设备之间发送的消息上可选附带的时间戳与标准 MIDI 文件中的时间值有很大不同。 MIDI 文件中的时间值通常基于音乐概念,如拍子和速度,每个事件的时间度量了自上一个事件以来经过的时间。相比之下,发送到设备的Receiver对象的消息上的时间戳始终以微秒为单位测量绝对时间。具体来说,它测量了自拥有接收器的设备打开以来经过的微秒数。

这种时间戳旨在帮助补偿操作系统或应用程序引入的延迟。重要的是要意识到,这些时间戳用于对时间进行微小调整,而不是实现可以在完全任意时间安排事件的复杂队列(就像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) {
          // handle or throw exception
    }

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

if (seq instanceof Synthesizer)

尽管上面的显式连接应该在任何情况下都能工作。

连接到多个设备

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

Synthesizer  synth;
    Sequencer    seq;
    MidiDevice   inputPort;
    // [obtain and open the three devices...]
    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) {
          // handle or throw exception
    }

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

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

int getMaxTransmitters()
    int getMaxReceivers()

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

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

同样,设备可以使用其多个接收器同时从多个设备接收。所需的多接收器代码很简单,直接类似于上面的多发射器代码。一个单一接收器也可以同时从多个发射器接收消息。

关闭连接

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

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

介绍 Sequencers

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

在 MIDI 世界中,sequencer是任何能精确播放或记录一系列时间戳 MIDI 消息的硬件或软件设备。同样,在 Java Sound API 中,Sequencer抽象接口定义了可以播放和记录MidiEvent对象序列的对象的属性。Sequencer通常从标准 MIDI 文件加载这些MidiEvent序列或将它们保存到这样的文件中。序列也可以进行编辑。以下页面解释了如何使用Sequencer对象以及相关的类和接口来完成这些任务。

要对Sequencer是什么有直观的理解,可以将其类比为磁带录音机,在许多方面Sequencer与磁带录音机相似。磁带录音机播放音频,而Sequencer播放 MIDI 数据。一个序列是多轨、线性、按时间顺序记录的 MIDI 音乐数据,Sequencer可以以不同速度播放,倒带,定位到特定点,录制或复制到文件进行存储。

传输和接收 MIDI 消息解释了设备通常具有Receiver对象、Transmitter对象或两者。为了播放音乐,设备通常通过Receiver接收MidiMessages,而Receiver通常是从属于SequencerTransmitter接收这些消息。拥有这个Receiver的设备可能是一个Synthesizer,它将直接生成音频,或者可能是一个 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

何时使用音序器

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

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

幸运的是,大多数应用程序不必担心这种调度问题。程序可以使用Sequencer对象来管理 MIDI 消息队列,而不是自己调用Receiver.sendSequencer负责调度和发送消息,换句话说,以正确的时间播放音乐。通常,在需要将非实时 MIDI 消息序列转换为实时序列(如播放)或反之(如录制)时,使用Sequencer是有利的。Sequencer最常用于播放来自 MIDI 文件的数据和从 MIDI 输入端口录制数据。

理解序列数据

在查看SequencerAPI 之前,了解存储在序列中的数据类型是有帮助的。

序列和轨道

在 Java Sound API 中,Sequencer在组织记录的 MIDI 数据方面紧密遵循标准 MIDI 文件规范。如上所述,Sequence是按时间组织的MidiEvents的聚合。但Sequence比仅仅是线性MidiEvents序列更具结构:Sequence实际上包含全局时间信息以及一组Tracks,而Tracks本身保存MidiEvent数据。因此,由Sequencer播放的数据由三级对象层次结构组成:SequencerTrackMidiEvent

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

这种数据组织方式便于编辑序列,但请注意,这只是一种使用Tracks的常规方式。Track类的定义中没有任何限制它只能包含不同 MIDI 通道上的MidiEvents的内容。例如,整个多通道 MIDI 作品可以混合录制到一个Track上。此外,Type 0 标准 MIDI 文件(与 Type 1 和 Type 2 相对)根据定义只包含一个轨道;因此,从这样的文件中读取的Sequence必然只有一个Track对象。

MidiEvents 和 Ticks

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

在 Java Sound API 中,MidiMessages可以与定时值关联的另一种方式有两种。 其中一种是上面提到的“何时使用 Sequencer”。 这种技术在不使用 Transmitter 向接收器发送消息和理解时间戳下有详细描述。 在那里,我们看到Receiversend方法接受一个MidiMessage参数和一个时间戳参数。 那种时间戳只能用微秒表示。

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

一个 tick 的持续时间是多少? 它可以在序列之间变化(但不会在序列内部变化),其值存储在标准 MIDI 文件的头部中。 一个 tick 的大小以两种类型的单位给出:

  • 每四分音符的脉冲(ticks),缩写为 PPQ
  • 每帧的 ticks,也称为 SMPTE 时间码(由电影和电视工程师协会采用的标准)

如果单位是 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,则后一种方法返回每四分音符的 ticks 数,或者如果分割类型是 SMPTE 约定之一,则返回每个 SMPTE 帧的 ticks 数。 在 PPQ 的情况下,可以使用以下公式来获取一个 tick 的大小:

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

总结一下,Java Sound API 以 MIDI ticks 或微秒表示时间信息。MidiEvents以 MIDI ticks 形式存储时间信息。一个 tick 的持续时间可以从Sequence的全局时间信息以及(如果序列使用基于速度的时间)当前的音乐速度计算出来。另一方面,发送给ReceiverMidiMessage的时间戳总是以微秒表示。

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

更复杂的是,不同设备可能基于不同来源(如操作系统的时钟或声卡维护的时钟)更新其时间概念。这意味着它们的时间可能相对于序列器的时间会有偏移。为了与序列器保持同步,一些设备允许自己成为序列器时间概念的“从属”。设置主从关系将在稍后的MidiEvent中讨论。

使用Sequencer方法

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

Sequencer接口提供了几个类别的方法:

  • 从 MIDI 文件或Sequence对象加载序列数据,并将当前加载的序列数据保存到 MIDI 文件。
  • 类似于磁带录音机的传输功能的方法,用于停止和开始播放和录制,启用和禁用特定轨道上的录制,并在Sequence中快进/快退当前播放或录制位置。
  • 高级方法用于查询和设置对象的同步和定时参数。Sequencer可以以不同的速度播放,一些Tracks静音,并且与其他对象处于各种同步状态。
  • 高级方法用于注册“监听器”对象,当Sequencer处理某些类型的 MIDI 事件时通知它们。

无论您将调用哪些Sequencer方法,第一步都是从系统获取Sequencer设备并为程序使用保留它。

获取一个Sequencer

应用程序不会实例化Sequencer;毕竟,Sequencer只是一个接口。相反,像 Java Sound API 的 MIDI 包中的所有设备一样,Sequencer通过静态的MidiSystem对象访问。如前面在访问 MIDI 系统资源中提到的,可以使用以下MidiSystem方法获取默认的Sequencer

static Sequencer getSequencer()

以下代码片段获取默认的Sequencer,获取其所需的任何系统资源,并使其可操作:

Sequencer sequencer;
// Get default sequencer.
sequencer = MidiSystem.getSequencer(); 
if (sequencer == null) {
    // Error -- sequencer device is not supported.
    // Inform user and return...
} else {
    // Acquire resources and make operational.
    sequencer.open();
}

调用open保留了Sequencer设备供程序使用。想象共享一个Sequencer并没有太多意义,因为它一次只能播放一个序列。当使用完Sequencer后,可以通过调用close使其可供其他程序使用。

可以按照访问 MIDI 系统资源中描述的方式获取非默认的Sequencer

加载一个序列

从系统获取并保留了一个Sequencer后,您需要加载Sequencer应该播放的数据。有三种典型的方法可以实现这一点:

  • 从 MIDI 文件中读取序列数据
  • 通过从另一个设备(如 MIDI 输入端口)接收 MIDI 消息实时录制
  • 通过向空序列添加轨道和向这些轨道添加MidiEvent对象来以编程方式构建它

现在我们将看一下获取序列数据的这种方式中的第一种。 (其他两种方式分别在录制和保存序列和编辑序列下描述。)这种方式实际上包括两种略有不同的方法。一种方法是将 MIDI 文件数据提供给InputStream,然后通过Sequencer.setSequence(InputStream)直接将其读取到sequencer中。使用此方法,您不需要显式创建Sequence对象。实际上,Sequencer实现甚至可能不会在幕后创建Sequence,因为一些sequencers具有处理直接从文件中处理数据的内置机制。

另一种方法是显式创建Sequence。如果要在播放之前编辑序列数据,则需要使用此方法。使用此方法,您调用MidiSystem的重载方法getSequence。该方法能够从InputStreamFileURL获取序列。该方法返回一个Sequence对象,然后可以将其加载到Sequencer中进行播放。在上面的代码摘录中,这是一个从File获取Sequence对象并将其加载到我们的sequencer的示例:

try {
    File myMidiFile = new File("seq1.mid");
    // Construct a Sequence object, and
    // load it into my sequencer.
    Sequence mySeq = MidiSystem.getSequence(myMidiFile);
    sequencer.setSequence(mySeq);
} catch (Exception e) {
   // Handle error and/or return
}

MidiSystemgetSequence方法一样,setSequence可能会抛出InvalidMidiDataException,在InputStream变体的情况下,还可能会抛出IOException,如果遇到任何问题。

播放一个序列

使用以下方法可以启动和停止Sequencer

void start()

void stop()

Sequencer.start方法开始播放序列。请注意,播放从序列中的当前位置开始。使用上面描述的setSequence方法加载现有序列会将sequencer的当前位置初始化为序列的开头。stop方法停止sequencer,但不会自动倒带当前Sequence。在不重置位置的情况下启动已停止的Sequence只是从当前位置恢复播放序列。在这种情况下,stop方法充当了暂停操作。但是,在开始播放之前,有各种Sequencer方法可将当前序列位置设置为任意值。(我们将在下面讨论这些方法。)

正如前面提到的,Sequencer通常具有一个或多个Transmitter对象,通过这些对象向Receiver发送MidiMessages。通过这些TransmittersSequencer播放Sequence,通过发出与当前Sequence中包含的MidiEvents相对应的适时MidiMessages。因此,播放Sequence的设置过程的一部分是在SequencerTransmitter对象上调用setReceiver方法,实际上将其输出连接到将使用播放数据的设备。有关TransmittersReceivers的更多详细信息,请参考传输和接收 MIDI 消息。

录制和保存序列

要将 MIDI 数据捕获到Sequence,然后保存到文件,需要执行一些除上述描述之外的额外步骤。以下概述显示了设置录制到Sequence中的Track所需的步骤:

  1. 使用MidiSystem.getSequencer获取一个新的用于录制的序列器,如上所述。
  2. 设置 MIDI 连接的“连线”。传输要录制的 MIDI 数据的对象应通过其setReceiver方法配置,以将数据发送到与录制Sequencer相关联的Receiver
  3. 创建一个新的Sequence对象,用于存储录制的数据。创建Sequence对象时,必须为序列指定全局时间信息。例如:
Sequence mySeq;
      try{
          mySeq = new Sequence(Sequence.PPQ, 10);
      } catch (Exception ex) { 
          ex.printStackTrace(); 
      }
  1. Sequence的构造函数接受divisionType和时间分辨率作为参数。divisionType参数指定分辨率参数的单位。在这种情况下,我们指定正在创建的Sequence的时间分辨率为每四分音符 10 脉冲。Sequence构造函数的另一个可选参数是轨道数参数,这将导致初始序列以指定数量的(最初为空)Tracks开始。否则,Sequence将创建为没有初始Tracks;它们可以根据需要随后添加。
  2. Sequence中创建一个空的Track,使用Sequence.createTrack。如果Sequence是使用初始Tracks创建的,则此步骤是不必要的。
  3. 使用Sequencer.setSequence,选择我们的新Sequence来接收录制。setSequence方法将现有的SequenceSequencer绑定,这在某种程度上类似于将磁带加载到磁带录音机上。
  4. 对于每个要录制的Track,调用Sequencer.recordEnable。如果需要,在Sequence中通过调用Sequence.getTracks获取可用的Tracks的引用。
  5. Sequencer上调用startRecording
  6. 完成录制后,调用Sequencer.stopSequencer.stopRecording
  7. 使用MidiSystem.write将录制的Sequence保存到 MIDI 文件中。MidiSystemwrite方法将Sequence作为其参数之一,并将该Sequence写入流或文件。

编辑序列

许多应用程序允许通过从文件加载来创建序列,并且有相当多的应用程序也允许通过从实时 MIDI 输入(即录制)捕获来创建序列。然而,一些程序将需要从头开始创建 MIDI 序列,无论是以编程方式还是响应用户输入。功能齐全的序列程序允许用户手动构建新序列,以及编辑现有序列。

在 Java Sound API 中,这些数据编辑操作不是通过Sequencer方法实现的,而是通过数据对象本身的方法实现:SequenceTrackMidiEvent。你可以使用Sequence构造函数之一创建一个空序列,然后通过调用以下Sequence方法向其添加轨道:

Track createTrack() 

如果你的程序允许用户编辑序列,你将需要这个Sequence方法来移除轨道:

boolean deleteTrack(Track track) 

一旦序列包含轨道,你可以通过调用Track类的方法来修改轨道的内容。Track中包含的MidiEventsjava.util.Vector的形式存储在Track对象中,而Track提供了一组方法来访问、添加和移除列表中的事件。addremove方法相当直观,用于向Track中添加或移除指定的MidiEvent。提供了一个get方法,它接受一个索引,返回存储在那里的MidiEvent。此外,还有sizetick方法,分别返回轨道中的MidiEvents数量和轨道的持续时间,以总Ticks数表示。

在将事件添加到轨道之前创建新事件时,当然会使用MidiEvent构造函数。要指定或修改嵌入在事件中的 MIDI 消息,可以调用适当的MidiMessage子类(ShortMessageSysexMessageMetaMessage)的setMessage方法。要修改事件应发生的时间,调用MidiEvent.setTick

综合起来,这些低级方法为完整功能的音序器程序所需的编辑功能提供了基础。

Java 中文官方教程 2022 版(三十三)(2)https://developer.aliyun.com/article/1488019

相关文章
|
2月前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
3天前
|
移动开发 前端开发 Java
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
|
17天前
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
95 26
|
24天前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
24天前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
1月前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
33 2
|
23天前
|
Java 数据库连接 编译器
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
42 0
|
1月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
1月前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
1月前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编