本Java教程是针对JDK 8编写的。本页面描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改以了解Java SE 9及后续版本中更新的语言特性的摘要。
请参阅JDK发布说明,了解有关所有JDK版本的新功能、增强功能和已删除或弃用选项的信息。
大多数使用Java Sound API的MIDI包的程序都是为了合成声音。之前讨论过的MIDI文件、事件、序列和顺序器的整个装置几乎总是有一个目标,即最终将音乐数据发送到合成器中转换为音频。(可能的例外包括将MIDI转换为音乐符号,供音乐家阅读的程序,以及向外部MIDI控制设备(如混音台)发送消息的程序。)
因此,Synthesizer
接口对于MIDI包是基础的。本页展示了如何操作合成器来播放声音。许多程序将简单地使用顺序器将MIDI文件数据发送到合成器中,并不需要直接调用许多Synthesizer
方法。然而,也可以直接控制合成器,而不使用顺序器甚至MidiMessage
对象,这在本页末尾有解释。
对于不熟悉MIDI的读者来说,合成架构可能看起来很复杂。其API包括三个接口:
为了对此API有个整体认识,接下来的部分解释了一些MIDI合成的基础知识以及它们在Java Sound API中的反映。后续部分将更详细地介绍API。
合成器如何生成声音?根据其实现方式,它可能使用一种或多种声音合成技术。例如,许多合成器使用波表合成。波表合成器从存储在内存中的音频片段中读取数据,以不同的采样率播放它们并循环它们,以创建不同音高和持续时间的音符。例如,要合成一个萨克斯风演奏C#4音符(MIDI音符号码61),合成器可能从一个录制的萨克斯风演奏中提取一个非常短的片段,该片段为中音C(MIDI音符号码60),然后以略快于其记录速率的采样率不断循环此片段,从而创建一个稍高音调的长音符。其他合成器使用诸如频率调制(FM)、加法合成或物理建模等技术,这些技术不使用存储的音频,而是使用不同的算法从头开始生成音频。
所有合成技术的共同之处在于能够创建各种类型的声音。不同的算法,或者在同一算法中的不同参数设置,会产生不同的声音结果。一个乐器是合成某种类型声音的规格说明。这种声音可以模拟传统乐器,如钢琴或小提琴;它可以模拟其他类型的声源,比如电话或直升机;或者它可以根本不模拟任何“现实世界”的声音。一个名为General MIDI的规范定义了一个标准的128个乐器列表,但大多数合成器也允许使用其他乐器。许多合成器提供一系列内置乐器,可以随时使用;一些合成器还支持加载其他乐器的机制。
一个乐器可能是特定供应商的,也就是说,只适用于一个合成器或同一供应商的几个型号。这种不兼容性是由于两个不同的合成器使用不同的声音合成技术,或者即使基本技术相同,使用不同的内部算法和参数而导致的。由于合成技术的细节通常是专有的,因此不兼容性很常见。Java Sound API提供了一种方法来检测给定合成器是否支持给定的乐器。
通常可以将乐器视为预设值;您不必了解产生其声音的合成技术的详细信息。但是,您仍然可以改变其声音的各个方面。每个音符开启消息都会指定一个音符的音高和音量。您还可以通过其他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
对象中。通过指定一个新的补丁,您可以更改MIDI通道的当前乐器。补丁可以被视为当前音色库中乐器的二维索引。
您可能想知道音色库是否也以数字方式索引。答案是否定的;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规范提供了bank-select和program-change命令,可以影响每个MIDI通道当前播放的乐器。然而,该规范没有定义每个补丁位置(bank和program号码)应该存在哪种乐器。最新的General MIDI规范解决了这个问题,通过定义包含128个与特定乐器声音相对应的程序的bank来解决。General MIDI合成器使用128个与指定集合相匹配的乐器。即使播放的是同一种乐器,不同的General MIDI合成器的声音也可能大不相同。然而,不论使用哪个General MIDI合成器播放,MIDI文件在大部分情况下应该听起来相似(即使不完全相同)。
然而,并不是所有的MIDI文件创建者都希望受到General MIDI定义的128个音色的限制。本节将展示如何更改合成器默认加载的乐器集合(如果没有默认集合,即在访问合成器时没有加载任何乐器,那么你必须使用此API来启动)。
为了确定合成器当前加载的乐器是否符合你的要求,你可以调用以下Synthesizer
方法:
Instrument[] getLoadedInstruments()
并遍历返回的数组,以查看当前加载的乐器。很可能,你会在用户界面中显示乐器的名称(使用Instrument
的getName
方法),并让用户决定是否使用这些乐器或加载其他乐器。Instrument
API包含了一个报告乐器所属声库的方法。声库的名称可能有助于你的程序或用户确切确定乐器是什么。
Soundbank getDefaultSoundbank()
可以获取默认的声库。Soundbank
API包括了一些方法来获取声库的名称、供应商和版本号,通过这些信息程序或用户可以验证声库的身份。然而,当你第一次获取一个合成器时,不能假设默认声库中的乐器已经加载到合成器中。例如,一个合成器可能有大量内置的可供使用的乐器,但由于其有限的内存,它可能不会自动加载它们。
用户可以决定加载与当前乐器不同的乐器(或者您可以以编程方式做出该决定)。下面的方法告诉您哪些乐器是随合成器一起提供的(而不是必须从声音库文件中加载):
Instrument[] getAvailableInstruments()
您可以通过调用以下方法来加载其中任何乐器:
boolean loadInstrument(Instrument instrument)
该乐器将根据乐器的Patch
对象加载到合成器的指定位置(可以使用Instrument
的getPatch
方法检索该对象)。
要从其他声音库加载乐器,首先调用Synthesizer
的isSupportedSoundbank
方法,以确保该声音库与该合成器兼容(如果不兼容,可以遍历系统的合成器以尝试找到支持该声音库的合成器)。然后,您可以调用以下方法之一从声音库加载乐器:
boolean loadAllInstruments(Soundbank soundbank) boolean loadInstruments(Soundbank soundbank, Patch[] patchList)
如其名称所示,第一个方法从给定的声音库加载整套乐器,而第二个方法从声音库加载选定的乐器。您还可以使用Soundbank
的getInstruments
方法访问所有乐器,然后遍历它们,并使用loadInstrument
逐个加载选定的乐器。
加载的乐器不一定都来自同一个声音库。您可以使用loadInstrument
或loadInstruments
从一个声音库加载某些乐器,从另一个声音库加载另一组乐器,依此类推。
每个乐器都有自己的Patch
对象,该对象指定了乐器应加载到合成器的位置。该位置由银行号和程序号定义。没有API可以通过更改乐器的银行或程序号来更改位置。
但是,可以使用Synthesizer
的以下方法将乐器加载到与其补丁所指定的位置不同的位置:
boolean remapInstrument(Instrument from, Instrument to)
该方法从合成器中卸载其第一个参数,并将其第二个参数放置在第一个参数曾经占用的合成器补丁位置上。
将乐器加载到程序位置会自动卸载该位置上已经存在的任何乐器,如果有的话。您也可以显式地卸载乐器而不一定要替换它们为新的乐器。Synthesizer
包括三种与加载方法对应的卸载方法。如果合成器接收到选择当前未加载任何乐器的程序更改消息,则该程序更改消息所在的MIDI通道将没有任何声音。
一些合成器除了乐器之外,还存储其他信息在它们的音库中。例如,wavetable合成器存储了一个或多个乐器可以访问的音频样本。因为这些样本可能被多个乐器共享,所以它们独立于任何乐器存储在音库中。音库接口和乐器类都提供了一个名为getSoundbankResources的方法,该方法返回一个SoundbankResource对象的列表。这些对象的细节取决于音库设计的合成器。在wavetable合成的情况下,一个资源可以是一个封装了一系列音频样本的对象,这些样本来自音频录音的一个片段。使用其他合成技术的合成器可能在合成器的SoundbankResources数组中存储其他类型的对象。
public long getLatency() public int getMaxPolyphony()
延迟测量了传递给合成器的MIDI消息和合成器实际产生相应结果之间的最坏延迟。例如,合成器在收到一个音符开启事件后,可能需要几毫秒的时间才能开始生成音频。
getMaxPolyphony方法指示合成器可以同时发出多少个音符,如前面在声音部分中讨论过的。如同在同一讨论中提到的,合成器可以提供有关它的音符的信息。这可以通过以下方法完成:
public VoiceStatus[] getVoiceStatus()
返回数组中的每个VoiceStatus报告了音符的当前活动或非活动状态、MIDI通道、库和程序编号、MIDI音符编号和MIDI音量。数组的长度通常应该与getMaxPolyphony返回的数值相同。如果合成器没有播放,所有的VoiceStatus对象的active字段都设置为false。
通过检索合成器的MidiChannel对象并查询它们的状态,您可以了解有关合成器当前状态的其他信息。这在下一节中更详细地讨论。
有时候直接访问合成器的MidiChannel对象是有用或必要的。本节讨论了这样的情况。
MidiChannel
接口提供了与 MIDI 规范中定义的每个“通道音符”或“通道模式”消息一一对应的方法。我们在前面的示例中已经看到了使用 noteOn 方法的情况。然而,除了这些基本方法之外,Java Sound API 的 MidiChannel
接口还添加了一些“获取”方法,用于检索由相应的音符或模式“设置”方法最近设置的值:
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 合成器生成的音频通常通过采样音频系统进行路由。如果您的程序没有权限播放音频,则无法听到合成器的声音,并且会抛出安全异常。有关音频权限的更多信息,请参阅之前的“使用音频资源的权限”讨论 使用音频资源的权限。