Synthesizing Sound

大多数使用 Java Sound API 的 MIDI 程序包的程序都是用来合成声音的。之前讨论过的 MIDI 文件,事件,序列和音序器的整个装置几乎总是以final将音乐数据发送到合成器以转换为音频为目标。 (可能的 exception 包括将 MIDI 转换为乐谱并可由音乐家读取的程序,以及将消息发送到外部 MIDI 控制的设备(如调音台)的程序。)

因此Synthesizerinterface是 MIDI 包的基础。本页显示如何操纵合成器播放声音。许多程序将仅使用音序器将 MIDI 文件数据发送到合成器,而无需直接调用许多Synthesizer方法。但是,可以在不使用定序器甚至MidiMessage对象的情况下直接控制合成器,如本页面末尾所述。

对于不熟悉 MIDI 的 Reader 而言,合成架构可能看起来很复杂。其 API 包括三个interface:

和四个class:

作为所有这些 API 的介绍,下一节将说明 MIDI 合成的一些基础知识以及它们如何在 Java Sound API 中得到体现。随后的部分将更详细地介绍 API。

了解 MIDI 综合

合成器如何产生声音?根据其实现,它可以使用一种或多种声音合成技术。例如,许多合成器都使用波表合成。波形表合成器从内存中读取存储的音频片段,以不同的采样率播放它们,然后循环播放以创建不同音高和持续时间的音符。例如,要合成演奏音符 C#4(MIDI 音符编号 61)的萨克斯风的声音,合成器可能会从演奏音符 Middle C(MIDI 音符编号 60)的萨克斯风的录音中获取非常短的片段,然后然后以比记录时稍快的采样率反复循环浏览此代码段,从而产生音调稍高的 Long 音。其他合成器使用诸如调频(FM),加法合成或物理建模之类的技术,这些技术不利用存储的音频,而是使用不同的算法从头开始生成音频。

Instruments

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

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

通常可以将乐器视为预设;您无需了解产生声音的合成技术的细节。但是,您仍然可以改变其声音的各个方面。每条“音符打开”消息均指定单个音符的音高和音量。您还可以通过其他 MIDI 命令(例如控制器消息或系统专有消息)更改声音。

Channels

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

在 Java Sound API 中,这些声音生成单元是实现MidiChannelinterface的类的实例。 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 命令来实现此 Object:库选择和程序更改。在 Java Sound API 中,将银行编号和程序编号的组合封装在Patch对象中。您可以通过指定新的音色来更改 MIDI 通道的当前乐器。可以将补丁视为当前音库中乐器的二维索引。

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

Voices

区分合成器可以同时演奏的音色数量和它可以同时演奏的音符数量非常重要。前者已在上方的“Channel”中进行了说明。一次播放多个音符的能力称为 polyphony *。即使不是多音色的合成器,通常一次也可以演奏一个以上的音符(所有音色相同,但音高不同)。例如,弹奏任何和弦,例如 G 大三和弦或 B 小七和弦,都需要复音。任何实时生成声音的合成器都会限制其一次合成的音符数量。在 Java Sound API 中,合成器通过getMaxPolyphony方法报告此限制。

声音是一连 String 的单音符,例如一个人可以唱歌的旋律。和弦由多种声音组成,例如合唱团演唱的部分。例如,一个 32 声合成器可以同时演奏 32 个音符。 (但是,某些 MIDI 文献使用的“ voice”一词的含义不同,类似于“ instrument”或“ timbre”的含义.)

将传入的 MIDI 音符分配给特定声音的过程称为语音分配。合成器会维护一个声音列表,跟踪哪些声音处于活动状态(这意味着它们当前有音符发声)。当音符停止发声时,声音将变为非活动状态,这意味着它现在可以自由接受合成器收到的下一个音符开启请求。 MIDI 命令的 Importing 流可以轻松请求比合成器所能生成的更多同时音符。当所有合成器的声音都处于活动状态时,应如何处理下一个“应要求提供 注解”?合成器可以实现不同的策略:可以忽略最近请求的音符;也可以通过中止另一个音符(例如最近启动的音符)来演奏。

尽管不需要 MIDI 规范,但合成器可以公开其每个声音的内容。为此,Java Sound API 包括VoiceStatus类。

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

在此背景下,让我们研究用于综合的 Java Sound API 的细节。

管理 乐器和声库

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

但是,在某些情况下,这种简单的方案是不够的。音序包含正确的音乐,但乐器听起来都不对!之所以出现这种不幸情况,是因为 MIDI 文件的创建者所考虑的乐器与当前加载到合成器中的乐器有所不同。

MIDI 1.0 规范提供了库选择和程序更改命令,这些命令会影响每个 MIDI 通道上当前正在演奏的乐器。但是,规范没有定义在每个音色位置(库和程序号)应驻留什么乐器。最新的通用 MIDI 规范通过定义包含 128 个与特定乐器声音相对应的程序的库来解决此问题。通用 MIDI 合成器使用 128 个与该指定设置匹配的乐器。不同的通用 MIDI 合成器的音色可能会完全不同,即使在演奏本来应该是同一乐器的乐器时也是如此。但是,无论播放哪个通用 MIDI 合成器,MIDI 文件在大多数情况下应该听起来相似(即使不相同)。

尽管如此,并非所有 MIDI 文件的创建者都希望限于通用 MIDI 定义的 128 个音色集。本节说明如何从合成器随附的默认设置更改乐器。 (如果没有默认值,这意味着在访问合成器时未加载任何乐器,则无论如何都必须使用此 API.)

了解要加载哪些乐器

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

Instrument[] getLoadedInstruments()

并遍历返回的数组以查看当前正在加载的仪器。最有可能的是,您将在用户interface中显示乐器的名称(使用InstrumentgetName方法),然后让用户决定是使用这些乐器还是加载其他乐器。 Instrument API 包括一种报告乐器属于哪个音库的方法。音库的名称可能有助于您的程序或用户确定乐器的确切含义。

Synthesizer方法:

Soundbank getDefaultSoundbank()

给您默认的声音库。 Soundbank API 包括检索音库名称,供应商和版本号的方法,程序或用户可以通过该方法来验证音库的身份。但是,您无法假定当您首次获得合成器时,默认音色库中的乐器已加载到合成器中。例如,一个合成器可能有各种各样的内置乐器可供使用,但是由于内存有限,它可能无法自动加载它们。

加载不同的乐器

用户可能决定加载不同于当前工具的乐器(或者您可以通过编程方式做出决定)。以下方法告诉您合成器附带哪些乐器(相对于必须从音库文件中加载):

Instrument[] getAvailableInstruments()

您可以通过调用以下任何一种工具来加载:

boolean loadInstrument(Instrument instrument)

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

要从其他声库加载乐器,请首先调用Synthesizer's isSupportedSoundbank方法以确保该声库与此合成器兼容(如果不兼容,则可以遍历系统的合成器以try找到支持该声库的合成器)。然后,您可以调用以下方法之一从音色库加载乐器:

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

顾名思义,其中的第一个从给定的音库加载整个乐器集,第二个从声库加载选定的乐器。您还可以使用Soundbank's getInstruments方法访问所有乐器,然后对其进行迭代并使用loadInstrument一次加载选定的乐器。

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

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

但是,可以使用以下Synthesizer方法将乐器加载到其补丁程序指定的位置以外的位置:

boolean remapInstrument(Instrument from, Instrument to)

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

Unloading Instruments

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

访问 Soundbank 资源

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

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

Synthesizerinterface包含返回有关合成器功能信息的方法:

public long getLatency()
    public int getMaxPolyphony()

延迟测量的是在 MIDI 消息传递到合成器的时间与合成器实际产生相应结果的时间之间的最坏情况下的延迟。例如,在收到音符打开事件后,合成器可能需要花费几毫秒的时间才能开始生成音频。

getMaxPolyphony方法指示合成器可以同时发出多少音符,如先前在Voices下讨论。如同一讨论中所述,合成器可以提供有关其声音的信息。这可以通过以下方法完成:

public VoiceStatus[] getVoiceStatus()

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

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

Using Channels

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

不使用定序器控制合成器

使用诸如从 MIDI 文件读取的音序时,您无需自己将 MIDI 命令发送到合成器。相反,您只需将序列加载到音序器中,然后将音序器连接到合成器,然后运行即可。音序器负责安排事件的时间,结果是可预测的音乐演奏。如果事先知道所需的音乐,这种情况就可以很好地工作,从文件中读取音乐时也是如此。

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

至少有两种不使用音序器将 MIDI 消息发送到合成器的方法。首先是构造一个MidiMessage,并使用Receiver的 send 方法将其传递到合成器。例如,要立即在 MIDI 通道 5(基于 1)上产生 Middle 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对象进行交互。首先,您需要使用以下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); 
    }

获取 Channels 的当前状态

MidiChannelinterface提供了与 MIDI 规范定义的每个“通道语音”或“通道 Pattern”消息一对一对应的方法。在上一个示例中,我们看到了使用 noteOn 方法的一种情况。但是,除了这些规范方法之外,Java Sound API 的MidiChannelinterface还添加了一些“获取”方法来检索由相应语音或 Pattern“设置”方法最近设置的值:

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

这些方法对于将通道状态显示给用户或确定随后要向通道发送哪些值很有用。

静音和独奏 Channels

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

如果启用了静音,则此声道不会响起,但其他声道不受影响。如果打开了独奏,则此声道以及任何其他独奏的声道都会发声(如果未静音),但是不会响起其他声道。独奏和静音的通道都不会发出声音。 MidiChannel API 包含四种方法:

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

播放合成声音的权限

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