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

原文:docs.oracle.com/javase/tutorial/reallybigindex.html

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(); 
          }
    
    

    Sequence的构造函数接受divisionType和时间分辨率作为参数。divisionType参数指定分辨率参数的单位。在这种情况下,我们指定正在创建的Sequence的时间分辨率为每四分音符 10 脉冲。Sequence构造函数的另一个可选参数是轨道数参数,这将导致初始序列以指定数量的(最初为空)Tracks开始。否则,Sequence将创建为没有初始Tracks;它们可以根据需要随后添加。

  4. Sequence中创建一个空的Track,使用Sequence.createTrack。如果Sequence是使用初始Tracks创建的,则此步骤是不必要的。

  5. 使用Sequencer.setSequence,选择我们的新Sequence来接收录制。setSequence方法将现有的SequenceSequencer绑定,这在某种程度上类似于将磁带加载到磁带录音机上。

  6. 对于每个要录制的Track,调用Sequencer.recordEnable。如果需要,在Sequence中通过调用Sequence.getTracks获取可用的Tracks的引用。

  7. Sequencer上调用startRecording

  8. 完成录制后,调用Sequencer.stopSequencer.stopRecording

  9. 使用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

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

使用高级音序器功能

译文:docs.oracle.com/javase/tutorial/sound/MIDI-seq-adv.html

到目前为止,我们专注于简单的 MIDI 数据播放和录制。本节将简要描述通过Sequencer接口和Sequence类的方法可用的一些更高级功能。

移动到序列中的任意位置

有两个Sequencer方法可以获取序列中音序器的当前位置。其中的第一个:

long getTickPosition()

返回从序列开始测量的 MIDI 滴答位置。第二种方法:

long getMicrosecondPosition()

返回当前位置的微秒数。此方法假定序列以存储在 MIDI 文件或Sequence中的默认速率播放。如果您按照下面描述的方式更改了播放速度,则它不会返回不同的值。

您也可以根据一个单位或另一个单位设置音序器的当前位置:

void setTickPosition(long tick)

void setMicrosecondPosition(long microsecond)

更改播放速度

如前所述,序列的速度由其速度确定,可以在序列的过程中变化。序列可以包含封装标准 MIDI 速度变化消息的事件。当音序器处理此类事件时,它会根据指定的速度更改播放速度。此外,您可以通过调用任何这些Sequencer方法来以编程方式更改速度:

    public void setTempoInBPM(float bpm)
    public void setTempoInMPQ(float mpq)
    public void setTempoFactor(float factor)

这些方法中的前两种分别设置每分钟的节拍数或每个四分音符的微秒数的速度。速度将保持在指定值,直到再次调用其中一种方法,或者在序列中遇到速度变化事件,此时当前速度将被新指定的速度覆盖。

第三种方法,setTempoFactor,性质不同。它会按比例缩放音序器设置的任何速度(无论是通过速度变化事件还是通过上述前两种方法之一)。默认比例为 1.0(无变化)。尽管此方法会导致播放或录制速度比标称速度快或慢(除非因子为 1.0),但不会改变标称速度。换句话说,getTempoInBPMgetTempoInMPQ返回的速度值不受速度因子影响,尽管速度因子会影响实际播放或录制速率。此外,如果速度通过速度变化事件或前两种方法之一更改,它仍会按上次设置的速度因子进行缩放。但是,如果加载新序列,则速度因子将重置为 1.0。

请注意,当序列的分割类型为 SMPTE 类型之一而不是 PPQ 时,所有这些速度变化指令都无效。

静音或独奏序列中的单独轨道

对于音序器的用户来说,通常可以方便地关闭某些轨道,以更清楚地听到音乐中发生的情况。一个功能齐全的音序器程序允许用户在播放过程中选择哪些轨道应该发声。(更准确地说,由于音序器实际上不会自己发声,用户选择哪些轨道将为音序器产生的 MIDI 消息流做出贡献。)通常,每个轨道上都有两种类型的图形控件:一个静音按钮和一个独奏按钮。如果激活了静音按钮,那个轨道在任何情况下都不会发声,直到静音按钮被停用。独奏是一个不太为人知的功能。它大致相当于静音的反义词。如果任何轨道上的独奏按钮被激活,只有那些独奏按钮被激活的轨道才会发声。这个功能让用户可以快速试听少量轨道,而无需将所有其他轨道静音。静音按钮通常优先于独奏按钮:如果两者都被激活,那个轨道不会发声。

使用Sequencer方法,静音或独奏轨道(以及查询轨道当前的静音或独奏状态)是很容易实现的。假设我们已经获得了默认的Sequencer并且已经将序列数据加载到其中。要静音序列中的第五个轨道,可以按照以下步骤进行:

    sequencer.setTrackMute(4, true);
    boolean muted = sequencer.getTrackMute(4);
    if (!muted) { 
        return;         // muting failed
    }

有几点需要注意上面的代码片段。首先,序列的轨道编号从 0 开始,以总轨道数减 1 结束。此外,setTrackMute的第二个参数是一个布尔值。如果为 true,则请求是将轨道静音;否则请求是取消静音指定的轨道。最后,为了测试静音是否生效,我们调用Sequencer getTrackMute方法,将要查询的轨道号传递给它。如果它返回true,正如我们在这种情况下所期望的那样,那么静音请求成功。如果返回false,则请求失败。

静音请求可能因各种原因而失败。例如,setTrackMute调用中指定的轨道号可能超过了总轨道数,或者音序器可能不支持静音。通过调用getTrackMute,我们可以确定我们的请求是成功还是失败。

顺便说一句,getTrackMute返回的布尔值确实可以告诉我们是否发生了失败,但它无法告诉我们失败的原因。我们可以测试看看失败是否是由于将无效的轨道号传递给setTrackMute方法引起的。为此,我们可以调用SequencegetTracks方法,该方法返回一个包含序列中所有轨道的数组。如果在setTrackMute调用中指定的轨道号超过了此数组的长度,则我们知道我们指定了一个无效的轨道号。

如果静音请求成功,那么在我们的示例中,当序列播放时第五个轨道将不会发声,当前静音的任何其他轨道也不会发声。

独奏轨道的方法和技巧与静音非常相似。要独奏一个轨道,调用SequencesetTrackSolo方法:

void setTrackSolo(int track, boolean bSolo)

setTrackMute相同,第一个参数指定基于零的轨道编号,第二个参数如果为true,则指定该轨道应处于独奏模式;否则该轨道不应处于独奏状态。

默认情况下,轨道既不静音也不独奏。

与其他 MIDI 设备同步

Sequencer有一个名为Sequencer.SyncMode的内部类。SyncMode对象代表 MIDI 序列器的时间概念如何与主设备或从设备同步的一种方式。如果序列器正在与主设备同步,序列器会根据来自主设备的某些 MIDI 消息调整其当前时间。如果序列器有一个从设备,序列器同样会发送 MIDI 消息来控制从设备的定时。

有三种预定义模式指定了序列器可能的主设备:INTERNAL_CLOCKMIDI_SYNCMIDI_TIME_CODE。后两种模式在序列器接收来自另一设备的 MIDI 消息时起作用。在这两种模式下,序列器的时间会根据系统实时定时时钟消息或 MIDI 时间码(MTC)消息进行重置。 (有关这些消息类型的更多信息,请参阅 MIDI 规范。)这两种模式也可以用作从模式,此时序列器会向其接收器发送相应类型的 MIDI 消息。第四种模式NO_SYNC用于指示序列器不应向其接收器发送定时信息。

通过调用带有支持的SyncMode对象作为参数的setMasterSyncMode方法,您可以指定序列器的定时如何受控。同样,setSlaveSyncMode方法确定序列器将向其接收器发送哪些定时信息。这些信息控制使用序列器作为主定时源的设备的定时。

指定特殊事件监听器

序列的每个轨道可以包含许多不同类型的MidiEvents。这些事件包括音符开和音符关消息、程序更改、控制更改和元事件。Java Sound API 为最后两种事件类型(控制更改事件和元事件)指定了“监听器”接口。您可以使用这些接口在播放序列时接收这些事件发生时的通知。

支持ControllerEventListener接口的对象可以在Sequencer处理特定控制变化消息时接收通知。控制变化消息是一种标准的 MIDI 消息类型,代表了 MIDI 控制器值的变化,比如音高弯曲轮或数据滑块。 (请参阅 MIDI 规范获取控制变化消息的完整列表。)当在序列播放过程中处理这样的消息时,消息指示任何设备(可能是合成器)从序列器接收数据以更新某些参数的值。该参数通常控制声音合成的某些方面,比如如果控制器是音高弯曲轮,则控制当前发声音符的音高。当录制序列时,控制变化消息意味着在创建消息的外部物理设备上移动了一个控制器,或者在软件中模拟了这样的移动。

这里是ControllerEventListener接口的使用方法。假设你已经开发了一个实现ControllerEventListener接口的类,意味着你的类包含以下方法:

    void controlChange(ShortMessage msg)

假设你已经创建了一个类的实例,并将其赋给一个名为myListener的变量。如果你在程序的某个地方包含以下语句:

    int[] controllersOfInterest = { 1, 2, 4 };
    sequencer.addControllerEventListener(myListener,
        controllersOfInterest);

那么你的类的controlChange方法将在Sequencer处理 MIDI 控制器编号为 1、2 或 4 的控制变化消息时被调用。换句话说,当Sequencer处理设置任何已注册控制器的值的请求时,Sequencer将调用你的类的controlChange方法。(请注意,将 MIDI 控制器编号分配给特定控制设备的详细信息在 MIDI 1.0 规范中有详细说明。)

controlChange方法接收一个包含受影响的控制器编号和控制器设置的新值的ShortMessage。你可以使用ShortMessage.getData1方法获取控制器编号,并使用ShortMessage.getData2方法获取控制器值的新设置。

另一种特殊事件监听器的类型由MetaEventListener接口定义。根据标准 MIDI 文件 1.0 规范,元消息是不在 MIDI 线协议中存在但可以嵌入到 MIDI 文件中的消息。它们对合成器没有意义,但可以被序列器解释。元消息包括指令(如变速命令)、歌词或其他文本以及其他指示(如音轨结束)。

MetaEventListener机制类似于ControllerEventListener。在任何需要在Sequencer处理MetaMessage时收到通知的类中实现MetaEventListener接口。这涉及向类中添加以下方法:

void meta(MetaMessage msg)

通过将此类的实例作为参数传递给Sequencer addMetaEventListener方法来注册该类的实例:

boolean b = sequencer.addMetaEventListener
        (myMetaListener);

这与ControllerEventListener接口所采取的方法略有不同,因为您必须注册以接收所有MetaMessages,而不仅仅是感兴趣的部分。如果顺序器在其序列中遇到MetaMessage,它将调用myMetaListener.meta,并将遇到的MetaMessage传递给它。meta方法可以调用其MetaMessage参数上的getType,以获取一个从 0 到 127 的整数,该整数表示消息类型,如标准 MIDI 文件 1.0 规范所定义。

合成声音

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

大多数使用 Java Sound API 的 MIDI 包的程序都是用来合成声音。之前讨论过的整个 MIDI 文件、事件、序列和序列器的装置几乎总是最终将音乐数据发送到合成器以转换为音频。(可能的例外包括将 MIDI 转换为音乐符号的程序,可以被音乐家阅读,以及向外部 MIDI 控制设备发送消息的程序,如混音台。)

因此,Synthesizer接口对于 MIDI 包至关重要。本页展示了如何操作合成器播放声音。许多程序将简单地使用序列器将 MIDI 文件数据发送到合成器,并且不需要直接调用许多Synthesizer方法。然而,也可以直接控制合成器,而不使用序列器甚至MidiMessage对象,如本页末尾所述。

对于不熟悉 MIDI 的读者来说,合成架构可能看起来很复杂。其 API 包括三个接口:

和四个类:

作为对所有这些 API 的定位,下一节解释了一些 MIDI 合成的基础知识以及它们如何在 Java Sound API 中反映。随后的部分将更详细地查看 API。

理解 MIDI 合成

合成器是如何产生声音的?根据其实现方式,它可能使用一种或多种声音合成技术。例如,许多合成器使用波表合成。波表合成器从内存中读取存储的音频片段,以不同的采样率播放它们,并循环播放它们以创建不同音高和持续时间的音符。例如,要合成萨克斯风演奏 C#4 音符(MIDI 音符号 61)的声音,合成器可能会访问从萨克斯风演奏中音符中央 C(MIDI 音符号 60)的录音中提取的一个非常短的片段,然后以比录制时略快的采样率不断循环播放这个片段,从而创建一个略高音高的长音符。其他合成器使用诸如频率调制(FM)、加法合成或物理建模等技术,这些技术不使用存储的音频,而是使用不同的算法从头开始生成音频。

乐器

所有合成技术共同之处在于能够创造许多种声音。不同的算法,或者同一算法内不同参数的设置,会产生不同的声音结果。一个乐器是合成某种类型声音的规范。该声音可能模拟传统乐器,如钢琴或小提琴;也可能模拟其他类型的声源,例如电话或直升机;或者根本不模拟任何“现实世界”的声音。一个名为通用 MIDI 的规范定义了一个标准的 128 种乐器列表,但大多数合成器也允许使用其他乐器。许多合成器提供一系列内置乐器,始终可供使用;一些合成器还支持加载额外乐器的机制。

一个乐器可能是特定供应商的——换句话说,仅适用于一个合成器或同一供应商的几个型号。当两个不同的合成器使用不同的声音合成技术,或者即使基本技术相同,但使用不同的内部算法和参数时,就会出现不兼容性。由于合成技术的细节通常是专有的,因此不兼容性是常见的。Java Sound API 包括检测给定合成器是否支持给定乐器的方法。

一个乐器通常可以被视为一个预设;你不必了解产生其声音的合成技术的细节。然而,你仍然可以改变其声音的各个方面。每个 Note On 消息指定一个单独音符的音高和音量。你还可以通过其他 MIDI 命令如控制器消息或系统专用消息来改变声音。

通道

许多合成器是多音轨(有时称为多音色),意味着它们可以同时演奏不同乐器的音符。 (音色是使听众能够区分一种乐器与其他乐器的特征音质。) 多音轨合成器可以模拟整个真实乐器的合奏,而不仅仅是一次一个乐器。 MIDI 合成器通常通过利用 MIDI 规范允许数据传输的不同 MIDI 通道来实现此功能。在这种情况下,合成器实际上是一组发声单元,每个单元模拟不同的乐器,并独立响应在不同 MIDI 通道上接收到的消息。由于 MIDI 规范仅提供 16 个通道,典型的 MIDI 合成器可以同时演奏多达 16 种不同的乐器。合成器接收一系列 MIDI 命令,其中许多是通道命令。 (通道命令针对特定的 MIDI 通道;有关更多信息,请参阅 MIDI 规范。) 如果合成器是多音轨的,它会根据命令中指示的通道号将每个通道命令路由到正确的发声单元。

在 Java Sound API 中,这些发声单元是实现MidiChannel接口的类的实例。一个synthesizer对象至少有一个MidiChannel对象。如果合成器是多音轨的,通常有多个,通常是 16 个。每个MidiChannel代表一个独立的发声单元。

因为合成器的MidiChannel对象更多或更少是独立的,将乐器分配给通道不必是唯一的。例如,所有 16 个通道都可以演奏钢琴音色,就好像有一个由 16 台钢琴组成的合奏团。任何分组都是可能的—例如,通道 1、5 和 8 可以演奏吉他声音,而通道 2 和 3 演奏打击乐,通道 12 有低音音色。在给定的 MIDI 通道上演奏的乐器可以动态更改;这被称为程序更改

尽管大多数合成器一次只能激活 16 个或更少的乐器,但这些乐器通常可以从更大的选择中选择,并根据需要分配到特定的通道。

声音库和音色

在合成器中,乐器按照银行号和程序号进行层次化组织。银行和程序可以被视为乐器的二维表中的行和列。一个银行是一个程序的集合。 MIDI 规范允许一个银行中最多有 128 个程序,最多有 128 个银行。然而,特定的合成器可能仅支持一个银行,或几个银行,并且可能支持每个银行少于 128 个程序。

在 Java Sound API 中,层次结构中有一个更高级别的部分:声音库。声音库可以包含多达 128 个银行,每个银行包含多达 128 个乐器。一些合成器可以将整个声音库加载到内存中。

要从当前声音库中选择一个乐器,您需要指定一个银行号和一个程序号。MIDI 规范通过两个 MIDI 命令实现了这一点:银行选择和程序更改。在 Java Sound API 中,银行号和程序号的组合封装在一个Patch对象中。通过指定一个新的 patch,您可以更改 MIDI 通道的当前乐器。该 patch 可以被视为当前声音库中乐器的二维索引。

您可能想知道声音库是否也是按数字索引的。答案是否定的;MIDI 规范没有提供这一点。在 Java Sound API 中,可以通过读取声音库文件来获取Soundbank对象。如果合成器支持声音库,它的乐器可以根据需要单独加载到合成器中,或者一次性全部加载。许多合成器都有一个内置或默认的声音库;该声音库中包含的乐器始终对合成器可用。

声音

区分合成器可以同时播放的音色数量和音符数量是很重要的。前者在“通道”下面已经描述过。同时播放多个音符的能力被称为复音。即使一个合成器不是多音色的,通常也可以同时播放多个音符(所有音符具有相同的音色,但不同的音高)。例如,播放任何和弦,比如 G 大三和弦或 B 小七和弦,都需要复音。任何实时生成声音的合成器都有一个限制,即它可以同时合成的音符数量。在 Java Sound API 中,合成器通过getMaxPolyphony方法报告这个限制。

声音是一系列单音符,比如一个人可以唱的旋律。复音包括多个声音,比如合唱团唱的部分。例如,一个 32 声音的合成器可以同时播放 32 个音符。(然而,一些 MIDI 文献使用“声音”一词的含义不同,类似于“乐器”或“音色”的含义。)

将传入的 MIDI 音符分配给特定声音的过程称为声音分配。合成器维护一个声音列表,跟踪哪些声音是活动的(意味着它们当前有音符在响)。当一个音符停止响时,声音变为非活动状态,意味着它现在可以接受合成器接收到的下一个音符请求。一个传入的 MIDI 命令流很容易请求比合成器能够生成的更多同时音符。当所有合成器的声音都是活动的时,下一个 Note On 请求应该如何处理?合成器可以实现不同的策略:最近请求的音符可以被忽略;或者通过停止另一个音符,比如最近启动的音符,来播放它。

尽管 MIDI 规范并不要求这样做,合成器可以公开每个声音的内容。Java Sound API 包括一个VoiceStatus类来实现这一目的。

一个VoiceStatus报告了声音当前的活动或非活动状态,MIDI 通道,银行和程序号,MIDI 音符号,以及 MIDI 音量。

有了这个背景,让我们来看一下 Java Sound API 合成的具体细节。

管理乐器和音色库

在许多情况下,一个程序可以使用Synthesizer对象而几乎不需要显式调用任何合成 API。例如,假设你正在播放一个标准的 MIDI 文件。你将其加载到一个Sequence对象中,通过让一个序列器将数据发送到默认的合成器来播放。序列中的数据按照预期控制合成器,按时播放所有正确的音符。

然而,有些情况下这种简单的情景是不够的。序列包含正确的音乐,但乐器听起来全错了!这种不幸的情况可能是因为 MIDI 文件的创建者心目中的乐器与当前加载到合成器中的乐器不同。

MIDI 1.0 规范提供了银行选择和程序更改命令,这些命令影响每个 MIDI 通道上当前播放的乐器。然而,该规范并未定义每个补丁位置(银行和程序号)应该放置什么乐器。较新的 General MIDI 规范通过定义一个包含 128 个与特定乐器声音对应的程序的银行来解决这个问题。General MIDI 合成器使用 128 个与指定集合匹配的乐器。即使播放应该是相同乐器的不同 General MIDI 合成器听起来可能会有很大不同。然而,一个 MIDI 文件在大多数情况下应该听起来相似(即使不完全相同),无论哪个 General MIDI 合成器在播放它。

尽管如此,并非所有的 MIDI 文件创建者都希望受限于 General MIDI 定义的 128 种音色。本节展示如何更改合成器附带的默认乐器集合。(如果没有默认设置,意味着在访问合成器时没有加载任何乐器,那么无论如何你都必须使用这个 API 开始。)

了解加载的乐器

要了解当前加载到合成器中的乐器是否符合你的要求,可以调用这个Synthesizer方法:

Instrument[] getLoadedInstruments() 

并遍历返回的数组,查看当前加载的确切乐器。很可能,你会在用户界面中显示乐器的名称(使用InstrumentgetName方法),让用户决定是否使用这些乐器或加载其他乐器。Instrument API 包括一个报告乐器属于哪个声音库的方法。声音库的名称可能帮助你的程序或用户确定乐器的确切信息。

这个Synthesizer方法:

Soundbank getDefaultSoundbank() 

给出默认的声音库。Soundbank API 包括检索声音库名称、供应商和版本号的方法,通过这些信息,程序或用户可以验证库的身份。然而,当你第一次获得一个合成器时,不能假设默认声音库中的乐器已经被加载到合成器中。例如,一个合成器可能有大量内置乐器可供使用,但由于其有限的内存,它可能不会自动加载它们。

加载不同的乐器

用户可能决定加载与当前乐器不同的乐器(或者你可能以编程方式做出这个决定)。以下方法告诉你合成器附带哪些乐器(而不必从声音库文件加载):

Instrument[] getAvailableInstruments()

你可以通过调用以下方法加载任何这些乐器:

boolean loadInstrument(Instrument instrument) 

乐器被加载到合成器中,位置由乐器的Patch对象指定(可以使用InstrumentgetPatch方法检索)。

要从其他声音库加载乐器,首先调用SynthesizerisSupportedSoundbank方法,确保声音库与此合成器兼容(如果不兼容,可以遍历系统的合成器尝试找到支持声音库的合成器)。然后可以调用这些方法之一从声音库加载乐器:

boolean loadAllInstruments(Soundbank soundbank) 
boolean loadInstruments(Soundbank soundbank, 
  Patch[] patchList) 

正如名称所示,第一个加载给定声音库中的所有乐器,第二个加载声音库中选择的乐器。你也可以使用SoundbankgetInstruments方法访问所有乐器,然后遍历它们,并使用loadInstrument逐个加载选择的乐器。

不需要加载的所有乐器来自同一个声音库。您可以使用loadInstrumentloadInstruments从一个声音库加载某些乐器,从另一个声音库加载另一组乐器,依此类推。

每个乐器都有自己的Patch对象,指定了乐器应加载到合成器的位置。该位置由银行号和程序号定义。没有 API 可以通过更改补丁的银行或程序号来更改位置。

然而,可以使用Synthesizer的以下方法将乐器加载到除其补丁指定位置之外的位置:

boolean remapInstrument(Instrument from, Instrument to) 

此方法从合成器中卸载其第一个参数,并将其第二个参数放置在第一个参数占用的合成器补丁位置。

卸载乐器

将乐器加载到程序位置会自动卸载该位置已经加载的任何乐器,如果有的话。您还可以显式卸载乐器,而不一定要用新的替换它们。Synthesizer包括三个与三个加载方法对应的卸载方法。如果合成器接收到选择当前未加载任何乐器的程序位置的程序更改消息,则在发送程序更改消息的 MIDI 通道上不会有任何声音。

访问声音库资源

一些合成器在其声音库中存储除乐器之外的其他信息。例如,波表合成器存储一个或多个乐器可以访问的音频样本。因为样本可能被多个乐器共享,它们独立于任何乐器存储在声音库中。Soundbank接口和Instrument类都提供一个名为getSoundbankResources的方法调用,返回一个SoundbankResource对象列表。这些对象的细节特定于为其设计声音库的合成器。在波表合成的情况下,资源可能是一个封装自音频录音片段的一系列音频样本的对象。使用其他合成技术的合成器可能在合成器的SoundbankResources数组中存储其他类型的对象。

查询合成器的功能和当前状态

Synthesizer接口包括返回有关合成器功能的信息的方法:

    public long getLatency()
    public int getMaxPolyphony()

延迟度量了传递 MIDI 消息到合成器并合成器实际产生相应结果之间的最坏情况延迟。例如,合成器在接收到音符开启事件后可能需要几毫秒才开始生成音频。

getMaxPolyphony 方法指示合成器可以同时发出多少音符,如前面在 Voices 下讨论的那样。如同在同一讨论中提到的,合成器可以提供关于其音色的信息。这是通过以下方法实现的:

public VoiceStatus[] getVoiceStatus()

返回的数组中的每个 VoiceStatus 报告了音色的当前活动或非活动状态、MIDI 通道、银行和程序号、MIDI 音符号码和 MIDI 音量。数组的长度通常应该与 getMaxPolyphony 返回的相同数量一样。如果合成器没有播放,所有其 VoiceStatus 对象的 active 字段都设置为 false

您可以通过检索其 MidiChannel 对象并查询其状态来了解有关合成器当前状态的其他信息。这将在下一节中更详细地讨论。

使用通道

有时访问合成器的 MidiChannel 对象直接是有用或必要的。本节讨论了这种情况。

在不使用序列器的情况下控制合成器

当使用序列器时,比如从 MIDI 文件中读取的序列,您不需要自己向合成器发送 MIDI 命令。相反,您只需将序列加载到序列器中,将序列器连接到合成器,并让其运行。序列器负责安排事件的时间表,结果是可预测的音乐表现。当所需音乐事先已知时,这种情况是有效的,这在从文件中读取时是正确的。

然而,在某些情况下,音乐是在播放时即时生成的。例如,用户界面可能会显示一个音乐键盘或吉他指板,并允许用户通过鼠标点击随意弹奏音符。另一个例子,一个应用程序可能使用合成器不是为了演奏音乐本身,而是为了根据用户的操作生成音效。这种情况在游戏中很典型。最后一个例子,应用程序可能确实正在播放从文件中读取的音乐,但用户界面允许用户与音乐互动,动态地改变它。在所有这些情况下,应用程序直接向合成器发送命令,因为 MIDI 消息需要立即传递,而不是被安排在将来的某个确定时间点。

有至少两种方法可以将 MIDI 消息发送到合成器而不使用序列器。第一种方法是构造一个 MidiMessage 并通过 Receiver 的 send 方法将其传递给合成器。例如,要在 MIDI 通道 5(从 1 开始计数)上立即产生中央 C(MIDI 音符号码 60),可以执行以下操作:

    ShortMessage myMsg = new ShortMessage();
    // Play the note Middle C (60) moderately loud
    // (velocity = 93)on channel 4 (zero-based).
    myMsg.setMessage(ShortMessage.NOTE_ON, 4, 60, 93); 
    Synthesizer synth = MidiSystem.getSynthesizer();
    Receiver synthRcvr = synth.getReceiver();
    synthRcvr.send(myMsg, -1); // -1 means no time stamp

第二种方法是完全绕过消息传递层(即 MidiMessageReceiver API),直接与合成器的 MidiChannel 对象交互。您首先需要检索合成器的 MidiChannel 对象,使用以下 Synthesizer 方法:

public MidiChannel[] getChannels()

之后,您可以直接调用所需的MidiChannel方法。这比将相应的MidiMessages发送到合成器的Receiver并让合成器处理与其自己的MidiChannels的通信更直接。例如,前面示例对应的代码将是:

    Synthesizer synth = MidiSystem.getSynthesizer();
    MidiChannel chan[] = synth.getChannels(); 
    // Check for null; maybe not all 16 channels exist.
    if (chan[4] != null) {
         chan[4].noteOn(60, 93); 
    }

获取通道的当前状态

MidiChannel接口提供了与 MIDI 规范中定义的每个“通道音频”或“通道模式”消息一一对应的方法。我们在前面的示例中看到了使用 noteOn 方法的情况。但是,除了这些经典方法之外,Java Sound API 的MidiChannel接口还添加了一些“get”方法,用于检索最近由相应的音频或模式“set”方法设置的值:

    int       getChannelPressure()
    int       getController(int controller)
    boolean   getMono()
    boolean   getOmni() 
    int       getPitchBend() 
    int       getPolyPressure(int noteNumber)
    int       getProgram()

这些方法可能对向用户显示通道状态或决定随后发送给通道的值很有用。

静音和独奏通道

Java Sound API 添加了每个通道独奏和静音的概念,这不是 MIDI 规范所要求的。这类似于 MIDI 序列轨道上的独奏和静音。

如果静音打开,该通道将不会发声,但其他通道不受影响。如果独奏打开,该通道和任何其他独奏的通道将会发声(如果它没有被静音),但其他通道不会发声。同时被独奏和静音的通道将不会发声。MidiChannel API 包括四种方法:

    boolean      getMute() 
    boolean      getSolo()
    void         setMute(boolean muteState) 
    void         setSolo(boolean soloState)

允许播放合成声音

任何已安装的 MIDI 合成器产生的音频通常会通过采样音频系统路由。如果您的程序没有权限播放音频,则合成器的声音将听不到,并且会抛出安全异常。有关音频权限的更多信息,请参阅之前关于使用音频资源的权限使用音频资源的权限的讨论。

服务提供者接口简介

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

什么是服务?

服务是声音处理功能单元,当应用程序使用 Java Sound API 的实现时自动可用。它们由执行读取、写入、混合、处理和转换音频和 MIDI 数据工作的对象组成。Java Sound API 的实现通常提供一组基本服务,但 API 中也包含机制,支持第三方开发人员(或实现供应商自身)开发新声音服务。这些新服务可以“插入”到现有安装的实现中,扩展其功能而不需要发布新版本。在 Java Sound API 架构中,第三方服务被集成到系统中,以便应用程序的接口与“内置”服务的接口相同。在某些情况下,使用 javax.sound.sampledjavax.sound.midi 包的应用程序开发人员甚至可能不知道他们正在使用第三方服务。

潜在的第三方采样音频服务示例包括:

  • 声音文件读取器和写入器

  • 在不同音频数据格式之间转换的转换器

  • 新的音频混音器和输入/输出设备,无论是纯粹在软件中实现,还是在硬件中具有软件接口

第三方 MIDI 服务可能包括:

  • MIDI 文件读取器和写入器

  • 用于各种类型声音库文件的读取器(通常特定于特定合成器)

  • 受 MIDI 控制的声音合成器、音序器和 I/O 端口,无论是纯粹在软件中实现,还是在硬件中具有软件接口

服务如何工作

javax.sound.sampledjavax.sound.midi 包为希望在其应用程序中包含声音服务的应用程序开发人员提供功能。这些包是声音服务的消费者,提供接口以获取有关音频和 MIDI 服务的信息、控制和访问。此外,Java Sound API 还提供了两个定义抽象类的包,供声音服务的提供者使用:javax.sound.sampled.spijavax.sound.midi.spi 包。

新声音服务的开发人员实现 SPI 包中适当类的具体子类。这些子类以及支持新服务所需的任何其他类都放在一个包含所包含服务描述的 Java 存档(JAR)存档文件中。当此 JAR 文件安装在用户的 CLASSPATH 中时,运行时系统会自动使新服务可用,扩展 Java 平台运行时系统的功能。

一旦安装了新服务,它就可以像以前安装的任何服务一样访问。服务的消费者可以通过调用javax.sound.sampledjavax.sound.midi包中的AudioSystemMidiSystem类的方法来获取有关新服务的信息,或获取新服务类的实例,以返回有关新服务的信息,或返回新的或现有服务类的实例。应用程序无需直接引用 SPI 包(及其子类)中的类来使用已安装的服务。

例如,假设一个名为 Acme Software, Inc.的假想服务提供商有兴趣提供一个允许应用程序读取新格式声音文件的包(但其音频数据是标准数据格式的)。SPI 类AudioFileReader可以被子类化为一个名为AcmeAudioFileReader的类。在新的子类中,Acme 将提供AudioFileReader中定义的所有方法的实现;在这种情况下,只有两个方法(带参数变体),getAudioFileFormatgetAudioInputStream。然后,当应用程序尝试读取一个恰好是 Acme 文件格式的声音文件时,它会调用javax.sound.sampled中的AudioSystem类的方法来访问文件和有关文件的信息。方法AudioSystem.getAudioInputStreamAudioSystem.getAudioFileFormat提供了一个标准的 API 来读取音频流;安装了AcmeAudioFileReader类后,此接口会被扩展以透明地支持新文件类型。应用程序开发人员不需要直接访问新注册的 SPI 类:AudioSystem对象方法会将查询传递给已安装的AcmeAudioFileReader类。

为什么要有这些“工厂”类?为什么不允许应用程序开发人员直接访问新提供的服务?这是一种可能的方法,但通过门卫系统对象管理和实例化所有服务可以使应用程序开发人员免于了解已安装服务的身份。应用程序开发人员只需使用对他们有价值的服务,甚至可能都没有意识到。同时,这种架构允许服务提供者有效地管理其包中的可用资源。

通常,新声音服务的使用对应用程序是透明的。例如,想象一种情况,应用程序开发人员想要从文件中读取音频流。假设thePathName标识了一个音频输入文件,程序会这样做:

    File theInFile = new File(thePathName);
    AudioInputStream theInStream = AudioSystem.getAudioInputStream(theInFile); 

在幕后,AudioSystem确定了哪个已安装的服务可以读取文件,并要求其提供音频数据作为AudioInputStream对象。开发人员可能不知道或甚至不关心输入音频文件是某种新文件格式(例如 Acme 的格式),这些格式由已安装的第三方服务支持。程序与流的第一次接触是通过AudioSystem对象,其后所有对流及其属性的访问都是通过AudioInputStream的方法。这两者都是javax.sound.sampled API 中的标准对象;新文件格式可能需要的特殊处理完全被隐藏起来。

服务提供者如何准备新服务

服务提供者以特殊格式的 JAR 文件提供其新服务,这些文件将被安装在用户系统中 Java 运行时将找到的目录中。JAR 文件是存档文件,每个文件包含一组文件,这些文件可能在存档中的分层目录结构中组织。关于放入这些存档的类文件的准备细节将在接下来的几页中讨论,这些页面描述了音频和 MIDI SPI 包的具体内容;在这里,我们只是概述 JAR 文件创建的过程。

新服务或服务的 JAR 文件应包含 JAR 文件中支持的每个服务的类文件。遵循 Java 平台的约定,每个类文件都具有新定义类的名称,这是一个抽象服务提供者类的具体子类。JAR 文件还必须包含新服务实现所需的任何支持类。为了使运行时系统的服务提供者机制能够定位新服务,JAR 文件还必须包含特殊文件(下文描述),将 SPI 类名称映射到正在定义的新子类。

继续我们上面的例子,假设 Acme Software, Inc.正在分发一套新的采样音频服务包。假设这个包包含两个新服务:

  • AcmeAudioFileReader类,如上所述,是AudioFileReader的子类

  • 一个名为AcmeAudioFileWriterAudioFileWriter子类,将以 Acme 的新格式编写声音文件

从一个任意目录开始——我们称之为/devel——我们创建子目录并将新的类文件放入其中,以一种组织方式来给出新类将被引用的期望路径名:

    com/acme/AcmeAudioFileReader.class
    com/acme/AcmeAudioFileWriter.class

此外,对于每个新的 SPI 类的子类,我们在一个名为META-INF/services的特殊命名目录中创建一个映射文件。文件的名称是被子类化的 SPI 类的名称,文件包含该 SPI 抽象类的新子类的名称。

我们创建文件

  META-INF/services/javax.sound.sampled.spi.AudioFileReader

包括

    # Providers of sound file-reading services 
    # (a comment line begins with a pound sign)
    com.acme.AcmeAudioFileReader

以及文件

  META-INF/services/javax.sound.sampled.spi.AudioFileWriter

包括

    # Providers of sound file-writing services 
    com.acme.AcmeAudioFileWriter

现在我们在任何目录中运行jar命令行:

jar cvf acme.jar -C /devel .

-C选项会导致jar切换到/devel目录,而不是使用执行命令的目录。最后的句点参数指示jar归档该目录的所有内容(即/devel),但不包括目录本身。

这次运行将创建文件acme.jar,其中包含以下内容:

com/acme/AcmeAudioFileReader.class
com/acme/AcmeAudioFileWriter.class
META-INF/services/javax.sound.sampled.spi.AudioFileReader
META-INF/services/javax.sound.sampled.spi.AudioFileWriter
META-INF/Manifest.mf

文件Manifest.mf是由jar工具本身生成的,其中列出了存档中包含的所有文件。

用户如何安装新服务

对于希望通过他们的应用程序获得新服务访问权限的最终用户(或系统管理员),安装是简单的。他们将提供的 JAR 文件放在他们的CLASSPATH中的一个目录中。在执行时,Java 运行时会在需要时找到引用的类。

安装同一服务的多个提供者并不是错误。例如,两个不同的服务提供者可能提供支持读取相同类型的声音文件。在这种情况下,系统会任意选择一个提供者。在意识到哪个提供者被选择的用户应该只安装所需的那个。

提供采样音频服务

原文:docs.oracle.com/javase/tutorial/sound/SPI-providing-sampled.html

正如你所知,Java Sound API 包括两个包,javax.sound.sampled.spijavax.sound.midi.spi,它们定义了抽象类,供声音服务的开发者使用。通过实现并安装这些抽象类的子类,服务提供者可以注册新服务,扩展运行时系统的功能。本页面告诉你如何使用 javax.sound.sampled.spi 包来提供处理采样音频的新服务。

javax.sound.sampled.spi 包中有四个抽象类,代表着你可以为采样音频系统提供的四种不同类型的服务:

  • AudioFileWriter 提供音频文件写入服务。这些服务使应用程序能够将音频数据流写入特定类型的文件。

  • AudioFileReader 提供文件读取服务。这些服务使应用程序能够确定音频文件的特性,并获取一个流,从中可以读取文件的音频数据。

  • FormatConversionProvider 提供音频数据格式转换服务。这些服务允许应用程序将音频流从一种数据格式转换为另一种。

  • MixerProvider 提供特定类型混音器的管理。这种机制允许应用程序获取关于给定类型混音器的信息,并访问实例。

总结之前的讨论,服务提供者可以扩展运行时系统的功能。典型的 SPI 类有两种类型的方法:一种是响应关于特定提供者提供的服务类型的查询,另一种是直接执行新服务,或返回实际提供服务的对象实例。运行时环境的服务提供者机制提供了已安装服务与音频系统的注册,以及新服务提供者类的管理

本质上,服务实例与应用程序开发人员之间存在双重隔离。应用程序从不直接创建服务对象的实例,例如混音器或格式转换器,以用于其音频处理任务。程序甚至不会直接从管理它们的 SPI 类中请求这些对象。应用程序向javax.sound.sampled包中的AudioSystem对象发出请求,AudioSystem反过来使用 SPI 对象来处理这些查询和服务请求。

新音频服务的存在对用户和应用程序员可能是完全透明的。所有应用程序引用都通过javax.sound.sampled包的标准对象,主要是AudioSystem,新服务可能提供的特殊处理通常是完全隐藏的。

在本讨论中,我们将继续使用类似AcmeMixerAcmeMixerProvider的名称来指代新的 SPI 子类。

提供音频文件写入服务

让我们从AudioFileWriter开始,这是较简单的 SPI 类之一。

实现AudioFileWriter方法的子类必须提供一组方法的实现,以处理关于类支持的文件格式和文件类型的查询,以及提供实际将提供的音频数据流写入FileOutputStream的方法。

AudioFileWriter包括两个在基类中具有具体实现的方法:

boolean isFileTypeSupported(AudioFileFormat.Type fileType) 
boolean isFileTypeSupported(AudioFileFormat.Type fileType, AudioInputStream stream) 

这些方法中的第一个方法通知调用者,此文件写入器是否可以写入指定类型的声音文件。这个方法是一个一般性的查询,如果文件写入器可以写入那种类型的文件,它将返回true,假设文件写入器被提供适当的音频数据。然而,写入文件的能力可能取决于传递给文件写入器的特定音频数据的格式。文件写入器可能不支持每种音频数据格式,或者约束可能由文件格式本身施加。(并非所有类型的音频数据都可以写入所有类型的声音文件。)因此,第二个方法更具体,询问特定的AudioInputStream是否可以写入特定类型的文件。

通常情况下,您不需要覆盖这两个具体方法。每个方法只是调用两个其他查询方法之一并遍历返回的结果的包装器。这两个其他查询方法是抽象的,因此需要在子类中实现:

abstract AudioFileFormat.Type[] getAudioFileTypes() 
abstract AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream) 

这些方法直接对应于前两个方法。每个方法返回所有支持的文件类型的数组-在第一个方法的情况下,通常是所有一般支持的,在第二个方法的情况下,是特定音频流支持的所有文件类型。第一个方法的典型实现可能简单地返回一个由文件写入器构造函数初始化的数组。第二个方法的实现可能测试流的AudioFormat对象,以查看请求的文件类型是否支持该数据格式。

AudioFileWriter的最后两个方法执行实际的文件写入工作:

abstract int write(AudioInputStream stream, 
     AudioFileFormat.Type fileType, java.io.File out) 
abstract int write(AudioInputStream stream, 
     AudioFileFormat.Type fileType, java.io.OutputStream out) 

这些方法将代表音频数据的字节流写入到第三个参数指定的流或文件中。如何完成这项工作的细节取决于指定类型文件的结构。write方法必须按照该格式声音文件的规定方式写入文件的头部和音频数据(无论是标准类型的声音文件还是新的、可能是专有的类型)。

提供音频文件读取服务

AudioFileReader类由六个抽象方法组成,您的子类需要实现这些方法-实际上,两个不同的重载方法,每个方法都可以接受FileURLInputStream参数。这两个重载方法中的第一个接受有关指定文件格式的查询:

abstract AudioFileFormat getAudioFileFormat(java.io.File file) 
abstract AudioFileFormat getAudioFileFormat(java.io.InputStream stream) 
abstract AudioFileFormat getAudioFileFormat(java.net.URL url) 

getAudioFileFormat方法的典型实现读取并解析声音文件的头部,以确定其文件格式。查看AudioFileFormat类的描述,了解需要从头部读取哪些字段,并参考特定文件类型的规范,以了解如何解析头部。

因为调用者将流作为参数提供给此方法,希望该方法不改变流,文件读取器通常应该从标记流开始。在读取到头部结束后,应该将流重置到其原始位置。

另一个重载的AudioFileReader方法提供文件读取服务,通过返回一个AudioInputStream,从中可以读取文件的音频数据:

abstract AudioInputStream getAudioInputStream(java.io.File file) 
abstract AudioInputStream getAudioInputStream(java.io.InputStream stream) 
abstract AudioInputStream getAudioInputStream(java.net.URL url) 

通常,getAudioInputStream的实现返回一个绕到文件数据块(在头部之后)开头的AudioInputStream,准备好进行读取。然而,可以想象,文件读取器返回的AudioInputStream可能表示一种从文件中包含的内容解码出来的数据流。重要的是,该方法返回一个格式化的流,从中可以读取文件中包含的音频数据。返回的AudioInputStream对象中封装的AudioFormat将告知调用者有关流的数据格式,通常情况下,但不一定是文件本身的数据格式。

通常,返回的流是AudioInputStream的一个实例;您不太可能需要对AudioInputStream进行子类化。

提供格式转换服务

FormatConversionProvider子类将具有一个音频数据格式的AudioInputStream转换为具有另一种格式的AudioInputStream。前者(输入)流被称为流,后者(输出)流被称为目标流。回想一下,AudioInputStream包含一个AudioFormat,而AudioFormat又包含一种特定类型的数据编码,由AudioFormat.Encoding对象表示。源流中的格式和编码称为源格式和源编码,目标流中的格式和编码同样被称为目标格式和目标编码。

转换工作是在FormatConversionProvider的重载抽象方法getAudioInputStream中执行的。该类还具有用于了解所有支持的目标和源格式和编码的抽象查询方法。有具体的包装方法用于查询特定的转换。

getAudioInputStream的两个变体是:

abstract AudioInputStream getAudioInputStream(AudioFormat.Encoding targetEncoding, 
     AudioInputStream sourceStream) 

abstract AudioInputStream getAudioInputStream(AudioFormat targetFormat, 
     AudioInputStream sourceStream) 

这些根据调用者是指定完整目标格式还是只是格式的编码而有所不同的第一个参数。

getAudioInputStream的典型实现通过返回一个围绕原始(源)AudioInputStream的新的AudioInputStream子类来工作,并在调用read方法时对其数据应用数据格式转换。例如,考虑一个名为AcmeCodec的新FormatConversionProvider子类的情况,它与一个名为AcmeCodecStream的新AudioInputStream子类一起工作。

AcmeCodec的第二个getAudioInputStream方法的实现可能是:

public AudioInputStream getAudioInputStream
      (AudioFormat outputFormat, AudioInputStream stream) {
        AudioInputStream cs = null;
        AudioFormat inputFormat = stream.getFormat();
        if (inputFormat.matches(outputFormat) ) {
            cs = stream;
        } else {
            cs = (AudioInputStream)
                (new AcmeCodecStream(stream, outputFormat));
            tempBuffer = new byte[tempBufferSize];
        }
        return cs;
    }

实际的格式转换发生在返回的AcmeCodecStream的新read方法中,它是AudioInputStream的子类。同样,访问这个返回的AcmeCodecStream的应用程序只需将其视为AudioInputStream进行操作,而不需要了解其实现的细节。

FormatConversionProvider的其他方法都允许查询对象支持的输入和输出编码和格式。以下四个方法是抽象的,需要被实现:

abstract AudioFormat.Encoding[] getSourceEncodings() 
abstract AudioFormat.Encoding[] getTargetEncodings() 
abstract AudioFormat.Encoding[] getTargetEncodings(
    AudioFormat sourceFormat) 
abstract  AudioFormat[] getTargetFormats(
    AudioFormat.Encoding targetEncoding, 
    AudioFormat sourceFormat) 

与上面讨论的AudioFileReader类的查询方法一样,这些查询通常通过检查对象的私有数据,并且对于后两种方法,将它们与参数进行比较来处理。

剩下的四个FormatConversionProvider方法是具体的,通常不需要被重写:

boolean isConversionSupported(
    AudioFormat.Encoding targetEncoding,
    AudioFormat sourceFormat) 
boolean isConversionSupported(AudioFormat targetFormat, 
    AudioFormat sourceFormat) 
boolean isSourceEncodingSupported(
    AudioFormat.Encoding sourceEncoding) 
boolean isTargetEncodingSupported(
    AudioFormat.Encoding targetEncoding) 

AudioFileWriter.isFileTypeSupported()类似,这些方法的默认实现本质上是调用其他查询方法之一并遍历返回的结果的包装器。

提供新类型的混音器

正如其名称所示,MixerProvider提供混音器的实例。每个具体的MixerProvider子类都充当应用程序使用的Mixer对象的工厂。当然,只有在定义一个或多个新的Mixer接口的实现时,定义新的MixerProvider才有意义。就像上面的FormatConversionProvider示例中,我们的getAudioInputStream方法返回了一个执行转换的AudioInputStream子类一样,我们的新类AcmeMixerProvider有一个getMixer方法,返回实现Mixer接口的另一个新类的实例。我们将后者称为AcmeMixer。特别是如果混音器是硬件实现的,提供者可能仅支持所请求设备的一个静态实例。如果是这样,它应该在每次调用getMixer时返回这个静态实例。

由于AcmeMixer支持Mixer接口,应用程序不需要任何额外的信息来访问其基本功能。然而,如果AcmeMixer支持Mixer接口中未定义的功能,并且供应商希望使这些扩展功能对应用程序可访问,那么混音器当然应该被定义为一个公共类,具有额外的、有文档记录的公共方法,以便希望利用这些扩展功能的程序可以导入AcmeMixer并将getMixer返回的对象转换为这种类型。

另外两种MixerProvider的方法是:

abstract Mixer.Info[] getMixerInfo() 

boolean isMixerSupported(Mixer.Info info) 

这些方法允许音频系统确定这个特定的提供者类是否可以提供应用程序需要的设备。换句话说,AudioSystem对象可以迭代所有已安装的MixerProviders,看看哪些,如果有的话,可以提供应用程序请求的AudioSystem的设备。getMixerInfo方法返回一个包含有关此提供程序对象提供的混音器类型信息的对象数组。系统可以将这些信息对象与其他提供程序的信息一起传递给应用程序。

一个MixerProvider可以提供多种类型的混音器。当系统调用MixerProvidergetMixerInfo方法时,它会得到一个信息对象列表,标识此提供程序支持的不同类型的混音器。然后系统可以调用MixerProvider.getMixer(Mixer.Info)来获取每个感兴趣的混音器。

你的子类需要实现getMixerInfo,因为它是抽象的。isMixerSupported方法是具体的,通常不需要被覆盖。默认实现只是将提供的Mixer.InfogetMixerInfo返回的数组中的每一个进行比较。

提供 MIDI 服务

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

服务提供者接口简介 解释了javax.sound.sampled.spijavax.sound.midi.spi包定义了供声音服务开发人员使用的抽象类。通过实现这些抽象类的子类,服务提供者可以创建一个扩展运行时系统功能的新服务。前一节介绍了如何使用javax.sound.sampled.spi包。本节讨论如何使用javax.sound.midi.spi包为处理 MIDI 设备和文件提供新服务。

javax.sound.midi.spi 包中有四个抽象类,代表着你可以为 MIDI 系统提供的四种不同类型的服务:

  • MidiFileWriter 提供了 MIDI 文件写入服务。这些服务使应用程序能够将其生成或处理的 MIDI Sequence 保存到 MIDI 文件中。

  • MidiFileReader 提供了从 MIDI 文件中返回 MIDI Sequence 供应用程序使用的文件读取服务。

  • MidiDeviceProvider 提供了一个或多个特定类型的 MIDI 设备实例,可能包括硬件设备。

  • SoundbankReader 提供了声音库文件读取服务。SoundbankReader的具体子类解析给定的声音库文件,生成一个可以加载到Synthesizer中的Soundbank对象。

应用程序不会直接创建服务对象的实例,无论是提供者对象,比如MidiDeviceProvider,还是由提供者对象提供的对象,比如Synthesizer。程序也不会直接引用 SPI 类。相反,应用程序会向javax.sound.midi包中的MidiSystem对象发出请求,而MidiSystem又会使用javax.sound.midi.spi类的具体子类来处理这些请求。

提供 MIDI 文件写入服务

有三种标准的 MIDI 文件格式,Java Sound API 的实现都可以支持:Type 0、Type 1 和 Type 2。这些文件格式在文件中的 MIDI 序列数据的内部表示上有所不同,并且适用于不同类型的序列。如果一个实现本身不支持所有三种类型,服务提供者可以为未实现的类型提供支持。还有标准 MIDI 文件格式的变体,其中一些是专有的,同样可以由第三方供应商支持。

写入 MIDI 文件的能力由MidiFileWriter的具体子类提供。这个抽象类直接类比于javax.sampled.spi.AudioFileWriter。同样,方法被分组为查询方法,用于了解可以写入哪些类型的文件,以及用于实际写入文件的方法。与AudioFileWriter一样,其中两个查询方法是具体的:

boolean isFileTypeSupported(int fileType)
boolean isFileTypeSupported(int fileType, Sequence sequence) 

其中第一个提供关于文件写入器是否可以写入指定类型的 MIDI 文件类型的一般信息。第二个方法更具体:它询问特定 Sequence 是否可以写入指定类型的 MIDI 文件。通常情况下,您不需要覆盖这两个具体方法中的任何一个。在默认实现中,每个方法调用另外两个相应的查询方法之一,并遍历返回的结果。作为抽象方法,这另外两个查询方法需要在子类中实现:

abstract int[] getMidiFileTypes() 
abstract int[] getMidiFileTypes(Sequence sequence) 

其中第一个返回一个数组,其中包含一般支持的所有文件类型。一个典型的实现可能会在文件写入器的构造函数中初始化数组,并从这个方法返回数组。从文件类型集中,第二个方法找到文件写入器可以写入给定 Sequence 的子集。根据 MIDI 规范,不是所有类型的序列都可以写入所有类型的 MIDI 文件。

MidiFileWriter子类的write方法执行将给定Sequence中的数据编码为请求的 MIDI 文件类型的正确数据格式,将编码流写入文件或输出流的操作:

abstract int write(Sequence in, int fileType, 
                   java.io.File out) 
abstract int write(Sequence in, int fileType, 
                   java.io.OutputStream out) 

为了实现这一点,write方法必须通过迭代Sequence的轨道来解析Sequence,构建适当的文件头,并将头部和轨道写入输出。当然,MIDI 文件的头部格式由 MIDI 规范定义。它包括诸如标识这是 MIDI 文件的“魔数”,头部长度,轨道数以及序列的定时信息(分频类型和分辨率)等信息。MIDI 文件的其余部分由轨道数据组成,格式由 MIDI 规范定义。

让我们简要地看一下应用程序、MIDI 系统和服务提供者如何合作编写 MIDI 文件。在典型情况下,一个应用程序有一个特定的 MIDI Sequence要保存到文件中。程序查询MidiSystem对象,看看对于手头的特定Sequence支持哪些 MIDI 文件格式(如果有的话),然后尝试写入文件。MidiSystem.getMidiFileTypes(Sequence)方法返回系统可以写入特定序列的所有 MIDI 文件类型的数组。它通过调用每个已安装的MidiFileWriter服务的相应getMidiFileTypes方法来实现这一点,并将结果收集并以整数数组的形式返回,这可以被视为与给定Sequence兼容的所有文件类型的主列表。在写入Sequence到文件时,调用MidiSystem.write传递一个表示文件类型的整数,以及要写入的Sequence和输出文件;MidiSystem使用提供的类型来决定哪个已安装的MidiFileWriter应处理写入请求,并将相应的写入分派给适当的MidiFileWriter

提供 MIDI 文件读取服务

MidiFileReader抽象类直接类似于javax.sampled.spi.AudioFileReader类。两者都包括两个重载的方法,每个方法都可以接受FileURLInputStream参数。重载方法中的第一个返回指定文件的文件格式。对于MidiFileReader,API 为:

abstract MidiFileFormat getMidiFileFormat(java.io.File file) 
abstract MidiFileFormat getMidiFileFormat(
    java.io.InputStream stream) 
abstract MidiFileFormat getMidiFileFormat(java.net.URL url) 

具体的子类必须实现这些方法来返回一个填充的MidiFileFormat对象,描述指定 MIDI 文件(或流或 URL)的格式,假设该文件是文件读取器支持的类型,并且包含有效的头信息。否则,应抛出InvalidMidiDataException

另一个重载的方法从给定的文件、流或 URL 返回一个 MIDI Sequence

abstract Sequence getSequence(java.io.File file) 
abstract Sequence getSequence(java.io.InputStream stream) 
abstract Sequence getSequence(java.net.URL url) 

getSequence方法执行解析 MIDI 输入文件中的字节并构造相应Sequence对象的实际工作。这基本上是MidiFileWriter.write使用的过程的反向。由于 MIDI 文件的内容(由 MIDI 规范定义)与 Java Sound API 定义的Sequence对象之间存在一对一的对应关系,因此解析的细节是直接的。如果传递给getSequence的文件包含文件读取器无法解析的数据(例如,因为文件已损坏或不符合 MIDI 规范),则应抛出InvalidMidiDataException

提供特定的 MIDI 设备

一个MidiDeviceProvider可以被视为提供一种或多种特定类型的 MIDI 设备的工厂。该类包括一个返回 MIDI 设备实例的方法,以及查询方法来了解该提供者可以提供哪些类型的设备。

与其他 javax.sound.midi.spi 服务一样,应用程序开发人员通过调用 MidiSystem 方法间接访问 MidiDeviceProvider 服务,本例中是 MidiSystem.getMidiDeviceMidiSystem.getMidiDeviceInfo。子类化 MidiDeviceProvider 的目的是提供一种新类型的设备,因此服务开发人员还必须为返回的设备创建一个相应的类,就像我们在 javax.sound.sampled.spi 包中看到的 MixerProvider 一样。在那里,返回的设备类实现了 javax.sound.sampled.Mixer 接口;这里实现了 javax.sound.midi.MidiDevice 接口。它还可以实现 MidiDevice 的子接口,如 SynthesizerSequencer

因为 MidiDeviceProvider 的单个子类可以提供多种类型的 MidiDevice,所以该类的 getDeviceInfo 方法返回一个枚举不同可用 MidiDevicesMidiDevice.Info 对象数组:

abstract MidiDevice.Info[] getDeviceInfo() 

返回的数组当然可以只包含一个元素。提供者的典型实现可能会在其构造函数中初始化一个数组,并在此处返回它。这使得 MidiSystem 可以遍历所有已安装的 MidiDeviceProviders 来构建所有已安装设备的列表。然后,MidiSystem 可以将这个列表(MidiDevice.Info[] 数组)返回给应用程序。

MidiDeviceProvider 还包括一个具体的查询方法:

boolean isDeviceSupported(MidiDevice.Info info) 

这个方法允许系统查询提供者关于特定类型设备的信息。通常情况下,你不需要重写这个便利方法。默认实现会遍历由 getDeviceInfo 返回的数组,并将参数与每个元素进行比较。

第三个也是最后一个 MidiDeviceProvider 方法返回请求的设备:

abstract MidiDevice getDevice(MidiDevice.Info info) 

这个方法应该首先测试参数,确保它描述的是该提供者可以提供的设备。如果不是,它应该抛出一个 IllegalArgumentException。否则,它会返回该设备。

提供声音库文件读取服务

SoundBank 是一组可以加载到 Synthesizer 中的 InstrumentsInstrument 是一个实现声音合成算法的对象,产生特定类型的声音,并包含相关的名称和信息字符串。SoundBank 大致对应于 MIDI 规范中的一个银行,但它是一个更广泛和可寻址的集合;可以更好地将其视为 MIDI 银行的集合。

SoundbankReader 由一个重载的方法组成,系统调用该方法从声音库文件中读取一个 Soundbank 对象:

abstract Soundbank getSoundbank(java.io.File file) 
abstract Soundbank getSoundbank(java.io.InputStream stream) 
abstract Soundbank getSoundbank(java.net.URL url) 

SoundbankReader 的具体子类将与特定提供者定义的 SoundBankInstrumentSynthesizer 实现配合工作,以允许系统从文件中加载 SoundBank 到特定 Synthesizer 类的实例中。合成技术可能在一个 Synthesizer 到另一个 Synthesizer 之间有很大差异,因此,存储在 InstrumentSoundBank 中为 Synthesizer 合成过程提供控制或规范数据的数据可以采用各种形式。一种合成技术可能只需要少量字节的参数数据;另一种可能基于广泛的声音样本。SoundBank 中存在的资源将取决于它们加载到的 Synthesizer 的性质,因此 SoundbankReader 子类的 getSoundbank 方法的实现可以访问特定类型 SoundBank 的知识。此外,SoundbankReader 的特定子类了解用于存储 SoundBank 数据的特定文件格式。该文件格式可能是供应商特定和专有的。

SoundBank 只是一个接口,对 SoundBank 对象的内容只有弱约束。要实现这个接口,对象必须支持的方法(getResourcesgetInstrumentsgetVendorgetName 等)对对象包含的数据有宽松的要求。例如,getResourcesgetInstruments 可以返回空数组。子类化的 SoundBank 对象的实际内容,特别是它的乐器和非乐器资源,由服务提供者定义。因此,解析声音库文件的机制完全取决于该特定类型声音库文件的规范。

声音库文件是在 Java Sound API 之外创建的,通常由可以加载该类型声音库的合成器的供应商创建。一些供应商可能会提供用于创建此类文件的最终用户工具。

Trail: JavaBeans™

原文:docs.oracle.com/javase/tutorial/javabeans/index.html

JavaBeans™使得重用软件组件变得容易。开发人员可以使用其他人编写的软件组件,而无需了解其内部工作原理。

要理解为什么软件组件很有用,想象一个工人在组装汽车。例如,她不需要从头开始建造收音机,而是简单地获取一个收音机并将其与汽车的其余部分连接起来。

本教程描述了 JavaBeans 的使用方法,包括以下课程:

通过展示如何使用 NetBeans 构建应用程序,快速介绍了 JavaBeans。

描述了用于 bean 属性、方法和事件的编码模式。它还概述了使用BeanInfo来定制开发体验在构建工具中的使用。

包括 bean 持久性、长期持久性和定制。

课程:快速入门

原文:docs.oracle.com/javase/tutorial/javabeans/quick/index.html

这节课描述了如何使用 NetBeans 构建一个简单的应用程序。有了像 NetBeans 这样的好工具,您可以将 JavaBeans 组件组装成一个应用程序,而无需编写任何代码。

这节课的前三页展示了如何使用 Java 平台中的图形 bean 创建一个简单的应用程序。最后一页演示了将第三方 bean 整合到您的应用程序中有多么容易。

  • 创建一个项目描述了在 NetBeans 中设置新项目的步骤。

  • 按钮是一个 Bean 展示了如何向应用程序的用户界面添加一个 bean,并描述了属性和事件。

  • 连接应用程序介绍了如何使用 NetBeans 响应应用程序中的 bean 事件。

  • 使用第三方 Bean 展示了向调色板添加新 bean 并在您的应用程序中使用它有多么容易。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建一个项目

原文:docs.oracle.com/javase/tutorial/javabeans/quick/project.html

学习 JavaBeans 最简单的方法是开始使用它们。首先,下载并安装最新版本的 NetBeans。本教程描述了如何使用 NetBeans 7.0 版本。

NetBeans 是一个bean 构建工具,这意味着它识别 JavaBeans 组件(bean)并使您能够轻松地将组件拼接在一起形成应用程序。

按钮是一个 Bean

启动 NetBeans。从菜单中选择文件 > 新建项目…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击查看完整图片

类别列表中选择Java,从项目列表中选择Java 应用程序。点击下一步 >

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击查看完整图片

输入SnapApp作为应用程序名称。取消选中创建主类,然后点击完成。NetBeans 创建新项目,您可以在 NetBeans 的项目窗格中看到它:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

SnapApp项目上按住控制键单击,从弹出菜单中选择新建 > JFrame 表单…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击查看完整图片

在类名中填写SnapFrame,包名填写snapapp。点击完成。NetBeans 创建新类并显示其可视化设计工具:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击查看完整图片

在左侧的项目窗格中,您可以看到新创建的SnapFrame类。屏幕中央是 NetBeans 的可视化设计工具。右侧是Palette,其中包含您可以在可视化设计工具中添加到框架中的所有组件。

一个按钮是一个 Bean

原文:docs.oracle.com/javase/tutorial/javabeans/quick/button.html

仔细看看工具栏。列出的所有组件都是 bean。组件按功能分组。滚动查找Swing 控件组,然后点击按钮并将其拖到可视化设计器中。按钮就是一个 bean!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 NetBeans 右侧的工具栏下方是一个检查器窗格,你可以用它来检查和操作按钮。尝试关闭底部的输出窗口,以便给检查器窗格更多的空间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

属性

一个 bean 的属性是你可以改变的影响其外观或内部状态的事物。在这个例子中的按钮,属性包括前景色、字体和按钮上显示的文本。属性分为两组。属性列出了最常用的属性,而其他属性显示了不常用的属性。

继续编辑按钮的属性。对于一些属性,你可以直接在表格中输入值。对于其他属性,点击**…按钮来编辑值。例如,点击前景属性右侧的…**。一个颜色选择对话框弹出,你可以选择一个新的前景文本颜色。尝试一些其他属性看看会发生什么。注意你没有编写任何代码。

事件

Bean 也可以触发事件。点击 bean 属性窗格中的事件按钮。你会看到按钮能够触发的每一个事件的列表。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你可以使用 NetBeans 来通过它们的事件和属性连接 bean。要看看这是如何工作的,从工具栏中拖动一个标签SnapFrame的可视化设计器中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

编辑标签的属性,直到看起来完美为止。

连接应用程序

原文:docs.oracle.com/javase/tutorial/javabeans/quick/wiring.html

要将按钮和标签连接在一起,请点击可视化设计工具栏中的连接模式按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击SnapFrame表单中的按钮。NetBeans 用红色轮廓显示按钮,以显示它是将生成事件的组件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击标签。NetBeans 的连接向导弹出。首先,您将选择要响应的事件。对于按钮来说,这是动作事件。点击动作旁边的**+,然后选择actionPerformed**。点击下一步

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在您可以选择按钮触发其动作事件时发生的情况。连接向导列出标签 bean 中的所有属性。在列表中选择text,然后点击下一步

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

连接向导的最后一个屏幕中,填写您希望为text属性设置的值。点击,然后输入您按下了按钮!或类似的内容。点击完成

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NetBeans 将组件连接在一起,并在源代码编辑器中展示其成果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击源代码工具栏中的设计按钮返回 UI 设计器。点击运行主项目或按F6构建和运行您的项目。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NetBeans 构建并运行项目。它会要求您标识主类,即SnapFrame。当应用程序窗口弹出时,点击按钮。您将在标签中看到您的不朽散文。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意,您没有编写任何代码。这就是 JavaBeans 的真正力量——借助像 NetBeans 这样的优秀构建工具,您可以快速将组件连接在一起,创建一个运行的应用程序。

使用第三方 Bean

原文:docs.oracle.com/javase/tutorial/javabeans/quick/addbean.html

几乎任何代码都可以打包为 bean。到目前为止,您看到的 bean 都是可视化 bean,但 bean 可以提供功能而不必具有可见组件。

JavaBeans 的强大之处在于您可以使用软件组件,而无需编写它们或了解其实现。

本页描述了如何将 JavaBean 添加到您的应用程序中,并利用其功能。

添加一个 Bean 到 NetBeans 调色板

下载一个示例 JavaBean 组件,BumperSticker``。Beans 以 JAR 文件的形式分发。将文件保存在计算机的某个位置。BumperSticker是一个图形组件,公开一个名为go()的方法,用于启动动画。

要将BumperSticker添加到 NetBeans 调色板中,请从 NetBeans 菜单中选择工具 > 调色板 > Swing/AWT 组件

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击从 JAR 添加…按钮。NetBeans 会要求您定位包含您希望添加到调色板中的 bean 的 JAR 文件。找到您刚下载的文件,然后点击下一步

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NetBeans 显示了 JAR 文件中的类列表。选择您希望添加到调色板中的类。在这种情况下,选择BumperSticker并点击下一步

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后,NetBeans 需要知道调色板的哪个部分将接收新的 bean。选择Beans,然后点击完成

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击关闭使调色板管理器窗口消失。现在看看调色板。BumperStickerBeans部分中。

使用您的新 JavaBean

BumperSticker从调色板拖到您的表单中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以像处理其他 bean 一样处理BumperSticker实例。要看到这一点,将另一个按钮拖到表单中。这个按钮将启动BumperSticker的动画。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将按钮连接到BumperSticker bean,就像您已经将第一个按钮连接到文本字段一样。

  1. 首先,点击连接模式按钮。

  2. 点击第二个按钮。NetBeans 会给它一个红色轮廓。

  3. 点击BumperSticker组件。连接向导弹出。

  4. 点击**+旁边的action**,然后选择actionPerformed。点击下一步 >

  5. 选择方法调用,然后从列表中选择go()。点击完成

如果您对任何步骤感到不确定,请查看连接应用程序。这里的过程非常相似。

再次运行该应用程序。当你点击第二个按钮时,BumperSticker 组件会动画显示心形的颜色。

再次注意,你已经制作出一个功能完善的应用程序,而没有编写任何代码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Lesson: 编写 JavaBeans 组件

原文:docs.oracle.com/javase/tutorial/javabeans/writing/index.html

编写 JavaBeans 组件非常容易。你不需要特殊的工具,也不必实现任何接口。编写 bean 只是遵循某些编码约定的问题。你所需要做的就是让你的类看起来像一个 bean —— 使用 bean 的工具将能够识别和使用你的 bean。

然而,NetBeans 提供了一些功能,使编写 bean 更容易。此外,Java SE API 包括一些支持类来帮助实现常见任务。

本课程中的代码示例基于一个简单的图形组件称为FaceBean

FaceBean 源代码仅包括:

FaceBean.java

包括 FaceBean 源代码的整个 NetBeans 项目:

FaceBean.zip

一个 bean 是一个遵循 JavaBeans 指南的方法名的 Java 类。一个 bean 构建工具使用内省来检查 bean 类。基于这种检查,bean 构建工具可以找出 bean 的属性、方法和事件。

以下各节描述了 JavaBeans 指南中关于属性、方法和事件的内容。最后,一个关于BeanInfo的部分展示了如何定制开发者与你的 bean 的体验。

属性

原文:docs.oracle.com/javase/tutorial/javabeans/writing/properties.html

要在 bean 类中定义属性,请提供公共的 getter 和 setter 方法。例如,以下方法定义了一个名为mouthWidthint属性:

public class FaceBean {
    private int mMouthWidth = 90;

    public int getMouthWidth() {
        return mMouthWidth;
    }

    public void setMouthWidth(int mw) {
        mMouthWidth = mw;
    }
}

像 NetBeans 这样的构建工具识别方法名,并在其属性列表中显示mouthWidth属性。它还识别类型int,并提供一个适当的编辑器,以便在设计时操作属性。

此示例显示了一个可读写的属性。还有其他组合也是可能的。例如,只读属性具有 getter 方法但没有 setter。只写属性只有 setter 方法。

boolean属性的特殊情况允许使用is而不是get来定义访问方法。例如,boolean属性running的访问器可以如下所示:

public boolean isRunning() {
    // ...
}

可用并在以下部分描述基本属性的各种特殊化。

索引属性

索引属性是一个数组而不是单个值。在这种情况下,bean 类提供了一个用于获取和设置整个数组的方法。以下是一个名为testGradesint[]属性的示例:

public int[] getTestGrades() {
    return mTestGrades;
}

public void setTestGrades(int[] tg) {
    mTestGrades = tg;
}

对于索引属性,bean 类还提供了用于获取和设置数组特定元素的方法。

public int getTestGrades(int index) {
    return mTestGrades[index];
}

public void setTestGrades(int index, int grade) {
    mTestGrades[index] = grade;
}

绑定属性

绑定属性在其值更改时通知监听器。这有两个含义:

  1. bean 类包括用于管理 bean 监听器的addPropertyChangeListener()removePropertyChangeListener()方法。

  2. 当绑定属性更改时,bean 向其注册的监听器发送PropertyChangeEvent

PropertyChangeEventPropertyChangeListener位于java.beans包中。

java.beans包还包括一个名为PropertyChangeSupport的类,它负责大部分绑定属性的工作。这个方便的类跟踪属性监听器,并包含一个方便的方法,向所有注册的监听器触发属性更改事件。

以下示例显示了如何使用PropertyChangeSupport使mouthWidth属性成为绑定属性。绑定属性的必要添加部分用粗体显示。

import java.beans.*;

public class FaceBean {
    private int mMouthWidth = 90;
    private PropertyChangeSupport mPcs =
        new PropertyChangeSupport(this);

    public int getMouthWidth() {
        return mMouthWidth;
    }

    public void setMouthWidth(int mw) {
        int oldMouthWidth = mMouthWidth;
        mMouthWidth = mw;
        mPcs.firePropertyChange("mouthWidth",
                                   oldMouthWidth, mw);
    }

    public void
    addPropertyChangeListener(PropertyChangeListener listener) {
        mPcs.addPropertyChangeListener(listener);
    }

    public void
    removePropertyChangeListener(PropertyChangeListener listener) {
        mPcs.removePropertyChangeListener(listener);
    }
}

绑定属性可以直接与其他 bean 属性绑定,使用像 NetBeans 这样的构建工具。例如,您可以将滑块组件的value属性绑定到示例中显示的mouthWidth属性。NetBeans 允许您在不编写任何代码的情况下执行此操作。

约束属性

约束属性是一种特殊类型的绑定属性。对于约束属性,bean 跟踪一组否决监听器。当约束属性即将更改时,监听器会就更改进行协商。任何一个监听器都有机会否决更改,此时属性保持不变。

否决监听器与属性更改监听器是分开的。幸运的是,java.beans包中包含一个VetoableChangeSupport类,大大简化了受限属性。

mouthWidth示例的更改显示为粗体:

import java.beans.*;

public class FaceBean {
    private int mMouthWidth = 90;
    private PropertyChangeSupport mPcs =
        new PropertyChangeSupport(this);
    private VetoableChangeSupport mVcs =
        new VetoableChangeSupport(this);

    public int getMouthWidth() {
        return mMouthWidth;
    }

    public void
    setMouthWidth(int mw) throws PropertyVetoException {
        int oldMouthWidth = mMouthWidth;
        mVcs.fireVetoableChange("mouthWidth",
                                    oldMouthWidth, mw);
        mMouthWidth = mw;
        mPcs.firePropertyChange("mouthWidth",
                                 oldMouthWidth, mw);
    }

    public void
    addPropertyChangeListener(PropertyChangeListener listener) {
        mPcs.addPropertyChangeListener(listener);
    }

    public void
    removePropertyChangeListener(PropertyChangeListener listener) {
        mPcs.removePropertyChangeListener(listener);
    }

    public void
    addVetoableChangeListener(VetoableChangeListener listener) {
        mVcs.addVetoableChangeListener(listener);
    }

    public void
    removeVetoableChangeListener(VetoableChangeListener listener) {
        mVcs.removeVetoableChangeListener(listener);
    }
}

NetBeans 中的开发支持

创建 bean 属性的编码模式很简单,但有时很难确定是否一切都正确。NetBeans 支持属性模式,因此您在编写代码时可以立即看到结果。

要利用此功能,请查看Navigator窗格,通常位于 NetBeans 窗口的左下角。通常,此窗格处于Members View模式,显示当前类中定义的所有方法和字段。

单击组合框切换到Bean Patterns视图。您将看到一个属性列表,其中列出了 NetBeans 可以从您的方法定义中推断出的属性。随着您的输入,NetBeans 会更新此列表,这是检查您工作的方便方式。

在下面的示例中,NetBeans 发现了可读写的mouthWidth属性和可读写的索引testGrades属性。此外,NetBeans 还意识到FaceBean允许注册PropertyChangeListenerVetoableChangeListener

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

方法

原文:docs.oracle.com/javase/tutorial/javabeans/writing/methods.html

一个方法是豆子能做的事情。任何不是属性定义的公共方法都是一个豆子方法。当你在像 NetBeans 这样的构建工具的上下文中使用一个豆子时,你可以将豆子的方法作为应用程序的一部分。例如,你可以将按钮按下与调用你的豆子方法之一联系起来。

事件

原文:docs.oracle.com/javase/tutorial/javabeans/writing/events.html

一个 bean 类可以触发任何类型的事件,包括自定义事件。与属性一样,事件通过特定模式的方法名称来识别。

public void add*<Event>*Listener(*<Event>*Listener a)
public void remove*<Event>*Listener(*<Event>*Listener a)

监听器类型必须是java.util.EventListener的子类。

例如,Swing 的JButton是一个 bean,当用户点击它时会触发action事件。JButton包括以下方法(实际上是从AbstractButton继承而来),这些方法是事件的 bean 模式:

public void addActionListener(ActionListener l);
public void removeActionListener(ActionListener l);

Bean 事件被构建工具识别,并可用于将组件进行连线。例如,您可以将按钮的action事件连接起来,以触发某些操作,比如调用另一个 bean 的方法。

使用 BeanInfo

原文:docs.oracle.com/javase/tutorial/javabeans/writing/beaninfo.html

Bean,尤其是图形组件,可能有大量属性。如果你的类继承自 ComponentJComponent 或其他 Swing 类,它已经拥有一百多个属性。虽然像 NetBeans 这样的构建工具使编辑 bean 属性变得容易,但对于经验不足的程序员来说,很难找到要编辑的正确属性。

BeanInfo 概述

BeanInfo 是一个改变你的 bean 在构建工具中显示方式的类。构建工具可以查询 BeanInfo 以找出应该首先显示哪些属性以及哪些应该隐藏。

你的 bean 对应的 BeanInfo 类应该与 bean 类同名,只是在末尾加上 BeanInfo。例如,FaceBean 类有一个对应的描述它的 FaceBeanBeanInfo 类。

虽然可以手动实现 BeanInfo 类,但使用像 NetBeans 这样的工具来编辑 BeanInfo 会更容易。

在 NetBeans 中创建 BeanInfo

项目窗格中,按住 Control 键单击 bean 类的名称,然后从上下文菜单中选择BeanInfo Editor…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NetBeans 注意到你没有 BeanInfo 并询问是否要创建一个。点击Yes

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NetBeans 会创建一个新的类并将你带到源代码编辑器。点击Designer切换到可视化编辑器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击查看完整图片

从可视化编辑器左侧的列表中选择属性,然后在右侧编辑其属性。如果你不希望某个特定属性出现在使用构建工具的开发人员面前,点击Hidden。要表示某个属性应该在其他属性之前显示,点击Preferred。你还可以指示属性是否绑定或受限。

你可以为 bean 的事件源和方法提供类似的信息。

当构建工具加载你的 bean 类以将其添加到工具栏时,它会自动找到相应的 BeanInfo 并用它来决定如何向开发人员展示你的 bean。

课程:高级 JavaBeans 主题

原文:docs.oracle.com/javase/tutorial/javabeans/advanced/index.html

JavaBeans 开发入门很容易,但是 Bean 具有令人惊讶的深度。本课程涵盖了一些更高级的主题,包括 Bean 可以如何存储(持久化)以及如何为自定义数据类型提供自定义编辑器。

  • Bean Persistence 描述了保存和重建 Bean 的机制。

  • Long Term Persistence 涵盖了将 Bean 表示为 XML。

  • Bean Customization 概述了为自定义数据类型创建编辑器组件。

Bean 持久性

原文:docs.oracle.com/javase/tutorial/javabeans/advanced/persistence.html

当 bean 的属性、字段和状态信息被保存到存储中并从中检索时,bean 具有持久性的属性。组件模型提供了一种持久性机制,使组件的状态能够被存储在非易失性位置以供以后检索。

使持久性成为可能的机制称为 序列化。对象序列化意味着将对象转换为数据流并将其写入存储。任何使用该 bean 的小程序、应用程序或工具都可以通过反序列化来“重建”它。然后对象将恢复到其原始状态。

例如,Java 应用程序可以在 Microsoft Windows 机器上序列化一个 Frame 窗口,然后将序列化文件通过电子邮件发送到 Solaris 机器,然后 Java 应用程序可以将 Frame 窗口恢复到在 Microsoft Windows 机器上存在的确切状态。

任何使用该 bean 的小程序、应用程序或工具都可以通过 反序列化 来“重建”它。

所有的 bean 都必须持久化。为了持久化,您的 bean 必须通过实现 java.io.Serializable(在 API 参考文档中)接口或 java.io.Externalizable(在 API 参考文档中)接口来支持序列化。这些接口为您提供了自动序列化和定制序列化的选择。如果类的继承层次结构中的任何类实现了 SerializableExternalizable,那么该类就是可序列化的。

可序列化的类

只要该类或父类实现了 java.io.Serializable 接口,任何类都可以序列化。支持序列化的示例类包括 ComponentStringDateVectorHashtable。因此,Component 类的任何子类,包括 Applet,都可以被序列化。不支持序列化的显著类包括 ImageThreadSocketInputStream。尝试序列化这些类型的对象将导致 NotSerializableException

Java 对象序列化 API 自动将大多数 Serializable 对象的字段序列化到存储流中。这包括原始类型、数组和字符串。API 不会序列化或反序列化被标记为 transient 或 static 的字段。

控制序列化

您可以控制 bean 所经历的序列化级别。控制序列化的三种方式是:

  • 通过 Serializable 接口实现的自动序列化。Java 序列化软件将整个对象序列化,除了 transient 和 static 字段。

  • 定制序列化。通过使用 transient(或 static)修饰符标记要排除的字段,可以选择性地排除不想序列化的字段。

  • 自定义文件格式,由Externalizable接口及其两个方法实现。Beans 以特定文件格式编写。

默认序列化:Serializable 接口

Serializable接口通过使用 Java 对象序列化工具提供自动序列化。Serializable不声明任何方法;它充当标记,告诉对象序列化工具你的 bean 类是可序列化的。将你的类标记为Serializable意味着你告诉 Java 虚拟机(JVM)你已经确保你的类将与默认序列化一起使用。以下是与Serializable接口一起使用的一些重要点:

  • 实现Serializable的类必须能够访问超类的无参数构造函数。当对象从.ser文件中“重建”时,将调用此构造函数。

  • 如果在超类中已经实现了Serializable,那么你不需要在你的类中实现它。

  • 所有字段(除了静态和瞬态字段)都会被序列化。使用transient修饰符指定不想序列化的字段,并指定不可序列化的类。

使用transient关键字进行选择性序列化

要在Serializable对象中排除字段的序列化,请使用transient修饰符标记字段。

transient int status;

默认序列化不会序列化transientstatic字段。

选择性序列化:writeObject 和 readObject

如果你的可序列化类包含以下两种方法之一(签名必须完全相同),则默认序列化将不会发生。

private void writeObject(java.io.ObjectOutputStream out)
    throws IOException;
private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;

通过编写自己的writeObjectreadObject方法的实现,可以控制更复杂对象的序列化。当需要对默认序列化无法处理的对象进行序列化,或者需要向序列化流添加不是对象数据成员的数据时,请实现writeObject。实现readObject以重建使用writeObject写入的数据流。

Externalizable 接口

当需要完全控制 bean 的序列化时(例如,写入和读取特定文件格式时),请使用Externalizable接口。要使用Externalizable接口,你需要实现两个方法:readExternalwriteExternal。实现Externalizable的类必须有一个无参数构造函数。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值