# 提供 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`一样，两种查询方法是具体的：

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

```

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

```
abstract int[] getMidiFileTypes() 
abstract int[] getMidiFileTypes(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`，构造适当的文件头，并将标题和轨道写入输出。 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 是：

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

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`对象数组：

```
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 文件阅读服务

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

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

```
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 之外创建的，通常由可以加载那种音库的合成器的供应商创建。某些供应商可能会提供最终用户工具来创建此类文件。