# 合成声音

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

大多数利用 Java Sound API MIDI 包的程序都是为了合成声音。先前讨论过的整个 MIDI 文件，事件，序列和序列器的设备几乎总是具有最终将音乐数据发送到合成器以转换成音频的目标。 （可能的例外包括将 MIDI 转换为可由音乐家读取的乐谱的程序，以及将消息发送到外部 MIDI 控制设备（如混音控制台）的程序。）

因此`Synthesizer`接口是 MIDI 包的基础。此页面显示如何操作合成器播放声音。许多程序只使用音序器将 MIDI 文件数据发送到合成器，而不需要直接调用许多`Synthesizer`方法。但是，可以直接控制合成器，而无需使用序列发生器甚至`MidiMessage`对象，如本页末尾所述。

对于不熟悉 MIDI 的读者来说，综合架构看起来很复杂。其 API 包括三个接口：

*   [`Synthesizer`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/Synthesizer.html)
*   [`MidiChannel`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/MidiChannel.html)
*   [`Soundbank`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/Soundbank.html)

和四个类：

*   [`Instrument`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/Instrument.html)
*   [`Patch`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/Patch.html)
*   [`SoundbankResource`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/SoundbankResource.html)
*   [`VoiceStatus`](https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/VoiceStatus.html)

作为所有这些 API 的定位，下一节将介绍 MIDI 合成的一些基础知识以及它们如何在 Java Sound API 中反映出来。后续部分将更详细地介绍 API。

## 理解 MIDI 合成

合成器如何产生声音？根据其实现，它可以使用一种或多种声音合成技术。例如，许多合成器使用波表合成。波表合成器从存储器中读取存储的音频片段，以不同的采样率播放它们并循环它们以创建不同音高和持续时间的音符。例如，为了合成演奏音符 C＃4（MIDI 音符编号 61）的萨克斯管的声音，合成器可以从演奏音符中间 C（MIDI 音符编号 60）的萨克斯管的录音中访问非常短的片段，并且然后以略高于记录时的采样率重复循环通过此片段，这会产生略高音高的长音符。其他合成器使用诸如频率调制（FM），加法合成或物理建模之类的技术，这些技术不利用存储的音频，而是使用不同的算法从头开始生成音频。

### 仪器

所有合成技术的共同点是能够创造多种声音。不同的算法或同一算法中的不同参数设置会产生不同的声音结果。 _ 乐器 _ 是用于合成某种声音的规格。那声音可以模仿传统的乐器，如钢琴或小提琴;它可以效仿其他类型的声源，例如电话或直升机;或者它可能根本不会模仿“真实世界”的声音。名为 General MIDI 的规范定义了 128 种乐器的标准列表，但大多数合成器也允许使用其他乐器。许多合成器提供了一系列始终可供使用的内置仪器;一些合成器还支持加载附加仪器的机制。

仪器可能是供应商特定的 - 换言之，仅适用于同一供应商的一个合成器或多个型号。当两个不同的合成器使用不同的声音合成技术或不同的内部算法和参数时，即使基本技术相同，也会导致这种不兼容性。由于合成技术的细节通常是专有的，因此不兼容性很常见。 Java Sound API 包括检测给定合成器是否支持给定仪器的方法。

通常可以将仪器视为预设;您不必了解产生声音的合成技术的细节。但是，您仍然可以改变其声音的各个方面。每条音符开启消息指定单个音符的音高和音量。您还可以通过其他 MIDI 命令（如控制器消息或系统专用消息）更改声音。

### 通道

许多合成器都是 _multimbral_ （有时称为 _polytimbral_ ），这意味着它们可以同时演奏不同乐器的音符。 （ _Timbre_ 是一种独特的音质，使听众能够将一种乐器与其他类型的乐器区分开来。）多重合成器可以模拟整个真实乐器的整体，而不是一次只有一种乐器。 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 个或更少的仪器处于活动状态，但这些仪器通常可以从更大的选择中选择，并根据需要分配给特定通道。

### Soundbanks 和补丁

仪器按照银行编号和程序编号在合成器中分层组织。银行和程序可以被视为二维工具表中的行和列。银行是一系列程序。 MIDI 规范允许银行中最多 128 个程序，最多 128 个库。但是，特定的合成器可能只支持一个银行或几个银行，并且每个银行可能支持少于 128 个程序。

在 Java Sound API 中，层次结构的级别更高：音库。 Soundbanks 最多可包含 128 个银行，每个银行最多包含 128 个工具。一些合成器可以将整个音库加载到内存中。

要从当前音库中选择乐器，请指定库号和程序号。 MIDI 规范通过两个 MIDI 命令完成此操作：存储区选择和程序更改。在 Java Sound API 中，银行编号和程序编号的组合封装在`Patch`对象中。您可以通过指定新的修补程序来更改 MIDI 通道的当前乐器。该贴片可以被认为是当前音库中乐器的二维索引。

你可能想知道 soundbanks 是否也用数字索引。答案是不; MIDI 规范没有提供此功能。在 Java Sound API 中，可以通过读取音库文件来获取`Soundbank`对象。如果合成器支持声音库，则可以根据需要将其乐器单独加载到合成器中，或者一次性加载到合成器中。许多合成器都有内置或默认的音库;声音库中包含的乐器始终可供合成器使用。

### 声音

区分合成器可以同时播放的 _ 音色 _ 的数量和它可以同时播放的 _ 音符 _ 的数量很重要。前者在“频道”中有所描述。一次播放多个音符的能力被称为 _ 复音 _。即使是非多音色的合成器通常也可以一次播放多个音符（所有音色都具有相同的音色，但音高不同）。例如，演奏任何和弦，例如 G 大调三和或小调七小和弦，都需要复音。任何实时生成声音的合成器都会限制它可以立即合成的音符数量。在 Java Sound API 中，合成器通过`getMaxPolyphony`方法报告此限制。

_ 声音 _ 是一连串的单音符，例如一个人可以唱的旋律。复调由多个声音组成，例如合唱团演唱的部分。例如，32 个语音合成器可以同时播放 32 个音符。 （但是，一些 MIDI 文学在不同意义上使用“声音”一词，类似于“乐器”或“音色”的含义。）

将传入的 MIDI 音符分配给特定语音的过程称为 _ 语音分配 _。合成器维护一个声音列表，跟踪哪些声音是活动的（意味着它们当前有声音发声）。当音符停止发声时，语音将变为非活动状态，这意味着它现在可以自由接受合成器接收的下一个音符开启请求。传入的 MIDI 命令流可以轻松地请求比合成器能够生成的更多同时音符。当所有合成器的声音都处于活动状态时，如何处理下一个 Note On 请求？合成器可以实现不同的策略：可以忽略最近请求的注释;或者它可以通过停止另一个音符来播放，例如最近最少开始的音符。

虽然 MIDI 规范不需要它，但合成器可以公开其每个声部的内容。 Java Sound API 包含用于此目的的`VoiceStatus`类。

A `VoiceStatus`报告语音当前的活动或非活动状态，MIDI 通道，库和程序编号，MIDI 音符编号和 MIDI 音量。

在此背景下，让我们来看看 Java Sound API 的具体细节。

## 管理仪器和 Soundbanks

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

但是，有些情况下这种简单的情况是不充分的。该序列包含正确的音乐，但乐器听起来都错了！这种不幸的情况可能是因为 MIDI 文件的创建者考虑的不同于当前加载到合成器中的乐器。

MIDI 1.0 规范提供了库选择和程序更改命令，这些命令会影响当前在每个 MIDI 通道上播放的乐器。但是，规范没有定义每个补丁位置（银行和程序编号）应该驻留哪种工具。最新的通用 MIDI 规范通过定义包含 128 个与特定乐器声音对应的程序的库来解决此问题。通用 MIDI 合成器使用 128 种与此指定设备匹配的乐器。不同的通用 MIDI 合成器听起来完全不同，即使在播放应该是同一乐器的时候也是如此。但是，MIDI 文件在大多数情况下应该听起来相似（即使不相同），无论哪种通用 MIDI 合成器正在播放它。

尽管如此，并非所有 MIDI 文件的创建者都想限制在通用 MIDI 定义的 128 个音色集中。本节介绍如何从合成器附带的默认设置更改乐器。 （如果没有默认值，意味着在访问合成器时没有加载任何乐器，则无论如何都必须使用此 API。）

### 学习加载什么仪器

要了解当前加载到合成器中的乐器是否是您想要的乐器，可以调用此`Synthesizer`方法：

```
Instrument[] getLoadedInstruments() 

```

并迭代返回的数组，以确切了解当前加载的乐器。最有可能的是，您将在用户界面中显示仪器的名称（使用`Instrument`的`getName`方法），并让用户决定是使用这些仪器还是加载其他仪器。 `Instrument` API 包括报告仪器所属的音库的方法。音库的名称可能有助于您的程序或用户确切地确定仪器是什么。

这个`Synthesizer`方法：

```
Soundbank getDefaultSoundbank() 

```

为您提供默认的音库。 `Soundbank` API 包括检索音库的名称，供应商和版本号的方法，程序或用户可以通过这些方法验证银行的身份。但是，当您第一次使用合成器时，您无法假设默认声音库中的乐器已加载到合成器中。例如，合成器可能有大量可供使用的内置仪器，但由于其内存有限，因此可能无法自动加载它们。

### 加载不同的仪器

用户可能决定加载与当前工具不同的工具（或者您可能以编程方式做出决定）。以下方法告诉您合成器附带哪些乐器（而不是从音库文件加载）：

```
Instrument[] getAvailableInstruments()

```

您可以通过调用以下方式加载任何这些工具：

```
boolean loadInstrument(Instrument instrument) 

```

仪器在仪器的`Patch`对象指定的位置加载到合成器中（可以使用`Instrument`的`getPatch`方法检索）。

要从其他声音库加载乐器，首先调用`Synthesizer's` `isSupportedSoundbank`方法以确保声音库与此合成器兼容（如果不是，您可以迭代系统的合成器以尝试找一个支持音库的人）。然后，您可以调用其中一种方法从音库加载乐器：

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

```

顾名思义，其中第一个从给定的音库加载整套乐器，第二个从音库加载选定的乐器。您也可以使用`Soundbank's` `getInstruments`方法访问所有仪器，然后迭代它们并使用`loadInstrument`一次加载一个选定的仪器。

您加载的所有乐器都不必来自同一个音库。您可以使用`loadInstrument`或`loadInstruments`从一个音库加载某些乐器，从另一个音库加载另一个乐器，依此类推。

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

但是，可以使用以下`Synthesizer`方法将仪器加载到其补丁指定的位置以外的位置：

```
boolean remapInstrument(Instrument from, Instrument to) 

```

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

### 卸载仪器

将仪器加载到程序位置会自动卸载该位置已有的任何仪器（如果有）。您还可以明确卸载仪器，而无需用新仪器替换它们。 `Synthesizer`包括三种与三种加载方法相对应的卸载方法。如果合成器接收到程序改变消息，该消息选择当前没有加载乐器的程序位置，则不会有来自发送程序改变消息的 MIDI 通道的任何声音。

### 访问 Soundbank 资源

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

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

`Synthesizer`接口包含返回有关合成器功能的信息的方法：

```
    public long getLatency()
    public int getMaxPolyphony()

```

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

`getMaxPolyphony`方法指示合成器可以同时发出多少音符，如前面[音色](#121777)中所述。如同一讨论中所述，合成器可以提供有关其声音的信息。这是通过以下方法完成的：

```
public VoiceStatus[] getVoiceStatus()

```

返回数组中的每个`VoiceStatus`都会报告语音的当前活动或非活动状态，MIDI 通道，库和程序编号，MIDI 音符编号和 MIDI 音量。数组的长度通常应与`getMaxPolyphony`返回的数字相同。如果合成器未播放，则其所有`VoiceStatus`对象的活动字段都设置为`false`。

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

## 使用频道

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

### 不使用音序器控制合成器

使用序列时，例如从 MIDI 文件中读取一个序列，您无需自己将 MIDI 命令发送到合成器。相反，您只需将序列加载到序列器中，将序列器连接到合成器，然后让它运行。音序器负责调度事件，结果是可预测的音乐表现。当预先知道所需的音乐时，这种情况可以正常工作，当从文件中读取时，这种情况就是如此。

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

至少有两种方法可以在不使用音序器的情况下将 MIDI 信息发送到合成器。第一种是构造`MidiMessage`并使用`Receiver`的发送方法将其传递给合成器。例如，要立即在 MIDI 通道 5（基于一个）上产生中间 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

```

第二种方法是完全绕过消息传递层（即`MidiMessage`和`Receiver` API），并直接与合成器的`MidiChannel`对象进行交互。首先需要使用以下`Synthesizer`方法检索合成器的`MidiChannel`对象：

```
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 合成器产生的音频通常通过采样音频系统进行路由。如果您的程序没有播放音频的权限，则不会听到合成器的声音，并且会抛出安全例外。有关音频权限的更多信息，请参阅前面关于使用音频资源的权限[使用音频资源的权限](accessing.html#113223)的讨论。