# 提供 MIDI 服务

> 原文： [https://docs.oracle.com/javase/tutorial/sound/SPI-providing-MIDI.html](https://docs.oracle.com/javase/tutorial/sound/SPI-providing-MIDI.html)

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

`javax.sound.midi.spi`包中有四个抽象类，它们代表了您可以为 MIDI 系统提供的四种不同类型的服务：

*   [`MidiFileWriter`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/spi/MidiFileWriter.html) 提供 MIDI 文件写入服务。这些服务使应用程序可以将已生成或处理的 MIDI `Sequence`保存到 MIDI 文件。
*   [`MidiFileReader`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/spi/MidiFileReader.html) 提供文件读取服务，从 MIDI 文件返回 MIDI `Sequence`，以便在应用程序中使用。
*   [`MidiDeviceProvider`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/spi/MidiDeviceProvider.html) 提供一种或多种特定类型 MIDI 设备的实例，可能包括硬件设备。
*   [`SoundbankReader`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/spi/SoundbankReader.html) 提供音库文件读取服务。 `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`一样，两种查询方法是具体的：

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

```

第一个提供有关文件编写器是否可以写入指定类型的 MIDI 文件类型的一般信息。第二种方法更具体：它询问是否可以将特定序列写入指定类型的 MIDI 文件。通常，您不需要覆盖这两种具体方法中的任何一种。在默认实现中，每个都调用另外两个相应的查询方法之一，并迭代返回的结果。作为抽象，这些其他两个查询方法需要在子类中实现：

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

```

第一个返回一般支持的所有文件类型的数组。典型的实现可能会在文件编写器的构造器中初始化数组，并从此方法返回数组。从该组文件类型中，第二种方法查找文件编写者可以写入给定序列的子集。根据 MIDI 规范，并非所有类型的序列都可以写入所有类型的 MIDI 文件。

`MidiFileWriter`子类的`write`方法将给定`Sequence`中的数据编码为所请求类型的 MIDI 文件的正确数据格式，将编码流写入文件或输出流：

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

```

为此，`write`方法必须通过遍历其轨道来解析`Sequence`，构造适当的文件头，并将标题和轨道写入输出。 MIDI 文件的标题格式当然是由 MIDI 规范定义的。它包括诸如将其识别为 MIDI 文件的“幻数”，标题长度，轨道数以及序列的定时信息（分割类型和分辨率）等信息。 MIDI 文件的其余部分由轨道数据组成，采用 MIDI 规范定义的格式。

让我们简要介绍一下应用程序，MIDI 系统和服务提供商如何配合写入 MIDI 文件。在典型情况下，应用程序具有特定的 MIDI `Sequence`以保存到文件。在尝试写入文件之前，程序查询`MidiSystem`对象以查看特定`Sequence`支持的 MIDI 文件格式（如果有）。 `MidiSystem.getMidiFileTypes(Sequence)`方法返回系统可以写入特定序列的所有 MIDI 文件类型的数组。它通过为每个已安装的`MidiFileWriter`服务调用相应的`getMidiFileTypes`方法，并在一个整数数组中收集和返回结果，这可以被认为是与给定[兼容的]所有文件类型的主列表。 COD6]。当将`Sequence`写入文件时，对`MidiSystem.write`的调用将传递一个表示文件类型的整数，以及要写入的`Sequence`和输出文件; `MidiSystem`使用提供的类型来决定安装的`MidiFileWriter`应该处理写请求，并将相应的`write`发送到适当的`MidiFileWriter`。

## 提供 MIDI 文件阅读服务

`MidiFileReader`抽象类直接类似于`javax.sampled.spi.AudioFileReader`类。两者都包含两个重载方法，每个方法都可以采用`File`，`URL`或`InputStream`参数。第一个重载方法返回指定文件的文件格式。在`MidiFileReader`的情况下，API 是：

```java
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`：

```java
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 设备

A `MidiDeviceProvider`可以被视为提供一种或多种特定类型 MIDI 设备的工厂。该类包含一个返回 MIDI 设备实例的方法，以及用于了解此供应器可以提供哪种设备的查询方法。

与其他`javax.sound.midi.spi`服务一样，应用程序开发人员通过调用`MidiSystem`方法间接访问`MidiDeviceProvider`服务，在本例中为`MidiSystem.getMidiDevice`和`MidiSystem.getMidiDeviceInfo`。子类化`MidiDeviceProvider`的目的是提供一种新类型的设备，因此服务开发人员还必须为返回的设备创建一个附带的类 - 正如我们在`javax.sound.sampled.spi`包中使用`MixerProvider`所看到的那样。在那里，返回的设备的类实现了`javax.sound.sampled.Mixer`接口;这里它实现了`javax.sound.midi.MidiDevice`接口。它也可能实现`MidiDevice`的子接口，例如`Synthesizer`或`Sequencer`。

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

```java
abstract MidiDevice.Info[] getDeviceInfo() 

```

当然，返回的数组可以包含单个元素。供应器的典型实现可能会在其构造器中初始化数组并在此处返回它。这允许`MidiSystem`迭代所有已安装的`MidiDeviceProviders`以构建所有已安装设备的列表。然后，`MidiSystem`可以将此列表（`MidiDevice.Info[]`数组）返回给应用程序。

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

```java
boolean isDeviceSupported(MidiDevice.Info info) 

```

该方法允许系统向提供者查询特定类型的设备。通常，您不需要覆盖此便捷方法。默认实现迭代 getDeviceInfo 返回的数组，并将参数与每个元素进行比较。

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

```java
abstract MidiDevice getDevice(MidiDevice.Info info) 

```

此方法应首先测试参数，以确保它描述此供应器可以提供的设备。如果没有，则应该抛出`IllegalArgumentException`。否则，它返回设备。

## 提供 Soundbank 文件阅读服务

A `SoundBank`是一组`Instruments`，可以加载到`Synthesizer`中。 `Instrument`是声音合成算法的实现，其产生特定类型的声音，并且包括伴随的名称和信息字符串。 `SoundBank`大致对应于 MIDI 规范中的一个库，但它是一个更广泛和可寻址的集合;它可能更好地被认为是 MIDI 银行的集合。

`SoundbankReader`由单个重载方法组成，系统调用该方法从 soundbank 文件中读取`Soundbank`对象：

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

```

`SoundbankReader`的具体子类将与特定提供者定义的`SoundBank`，`Instrument`和`Synthesizer`实现协同工作，以允许系统将`SoundBank`从文件加载到特定实例`Synthesizer` ]班。合成技术可能与`Synthesizer`相差甚远，因此，存储在`Instrument`或`SoundBank`中的数据可为`Synthesizer`的合成过程提供控制或规格数据。形式。一种合成技术可能只需要几个字节的参数数据;另一种可能基于广泛的声音样例。 `SoundBank`中存在的资源将取决于它们加载到的`Synthesizer`的性质，因此`SoundbankReader`子类的`getSoundbank`方法的实现可以获得特定类型的`getSoundbank`方法的知识。 COD14]。此外，`SoundbankReader`的特定子类理解用于存储`SoundBank`数据的特定文件格式。该文件格式可能是供应商特定的和专有的。

`SoundBank`只是一个接口，对`SoundBank`对象的内容只有很弱的约束。对象必须支持实现此接口的方法（`getResources`，`getInstruments`，`getVendor`，`getName`等）对对象包含的数据施加了宽松的要求。例如，`getResources`和`getInstruments`可以返回空数组。子类`SoundBank`对象的实际内容，特别是其仪器及其非仪器资源，由服务提供商定义。因此，解析音库文件的机制完全取决于特定类型的音库文件的规范。

Soundbank 文件是在 Java Sound API 之外创建的，通常由可以加载那种音库的合成器的供应商创建。某些供应商可能会提供最终用户工具来创建此类文件。