Java 中文官方教程 2022 版(三十三)(1)https://developer.aliyun.com/article/1488017
使用高级音序器功能
到目前为止,我们专注于简单的 MIDI 数据播放和录制。本节将简要描述通过Sequencer
接口和Sequence
类的方法可用的一些更高级功能。
移动到序列中的任意位置
有两个Sequencer
方法可以获取序列中音序器的当前位置。其中的第一个:
long getTickPosition()
返回从序列开始测量的 MIDI 滴答位置。第二种方法:
long getMicrosecondPosition()
返回当前位置的微秒数。此方法假定序列以存储在 MIDI 文件或Sequence
中的默认速率播放。如果您按照下面描述的方式更改了播放速度,则它不会返回不同的值。
您也可以根据一个单位或另一个单位设置音序器的当前位置:
void setTickPosition(long tick)
或
void setMicrosecondPosition(long microsecond)
更改播放速度
如前所述,序列的速度由其速度确定,可以在序列的过程中变化。序列可以包含封装标准 MIDI 速度变化消息的事件。当音序器处理此类事件时,它会根据指定的速度更改播放速度。此外,您可以通过调用任何这些Sequencer
方法来以编程方式更改速度:
public void setTempoInBPM(float bpm) public void setTempoInMPQ(float mpq) public void setTempoFactor(float factor)
这些方法中的前两种分别设置每分钟的节拍数或每个四分音符的微秒数的速度。速度将保持在指定值,直到再次调用其中一种方法,或者在序列中遇到速度变化事件,此时当前速度将被新指定的速度覆盖。
第三种方法,setTempoFactor
,性质不同。它会按比例缩放音序器设置的任何速度(无论是通过速度变化事件还是通过上述前两种方法之一)。默认比例为 1.0(无变化)。尽管此方法会导致播放或录制速度比标称速度快或慢(除非因子为 1.0),但不会改变标称速度。换句话说,getTempoInBPM
和getTempoInMPQ
返回的速度值不受速度因子影响,尽管速度因子会影响实际播放或录制速率。此外,如果速度通过速度变化事件或前两种方法之一更改,它仍会按上次设置的速度因子进行缩放。但是,如果加载新序列,则速度因子将重置为 1.0。
请注意,当序列的分割类型为 SMPTE 类型之一而不是 PPQ 时,所有这些速度变化指令都无效。
静音或独奏序列中的单独轨道
对于音序器的用户来说,通常可以方便地关闭某些轨道,以更清楚地听到音乐中发生的情况。一个功能齐全的音序器程序允许用户在播放过程中选择哪些轨道应该发声。(更准确地说,由于音序器实际上不会自己发声,用户选择哪些轨道将为音序器产生的 MIDI 消息流做出贡献。)通常,每个轨道上都有两种类型的图形控件:一个静音按钮和一个独奏按钮。如果激活了静音按钮,那个轨道在任何情况下都不会发声,直到静音按钮被停用。独奏是一个不太为人知的功能。它大致相当于静音的反义词。如果任何轨道上的独奏按钮被激活,只有那些独奏按钮被激活的轨道才会发声。这个功能让用户可以快速试听少量轨道,而无需将所有其他轨道静音。静音按钮通常优先于独奏按钮:如果两者都被激活,那个轨道不会发声。
使用Sequencer
方法,静音或独奏轨道(以及查询轨道当前的静音或独奏状态)是很容易实现的。假设我们已经获得了默认的Sequencer
并且已经将序列数据加载到其中。要静音序列中的第五个轨道,可以按照以下步骤进行:
sequencer.setTrackMute(4, true); boolean muted = sequencer.getTrackMute(4); if (!muted) { return; // muting failed }
有几点需要注意上面的代码片段。首先,序列的轨道编号从 0 开始,以总轨道数减 1 结束。此外,setTrackMute
的第二个参数是一个布尔值。如果为 true,则请求是将轨道静音;否则请求是取消静音指定的轨道。最后,为了测试静音是否生效,我们调用Sequencer getTrackMute
方法,将要查询的轨道号传递给它。如果它返回true
,正如我们在这种情况下所期望的那样,那么静音请求成功。如果返回false
,则请求失败。
静音请求可能因各种原因而失败。例如,setTrackMute
调用中指定的轨道号可能超过了总轨道数,或者音序器可能不支持静音。通过调用getTrackMute
,我们可以确定我们的请求是成功还是失败。
顺便说一句,getTrackMute
返回的布尔值确实可以告诉我们是否发生了失败,但它无法告诉我们失败的原因。我们可以测试看看失败是否是由于将无效的轨道号传递给setTrackMute
方法引起的。为此,我们可以调用Sequence
的getTracks
方法,该方法返回一个包含序列中所有轨道的数组。如果在setTrackMute
调用中指定的轨道号超过了此数组的长度,则我们知道我们指定了一个无效的轨道号。
如果静音请求成功,那么在我们的示例中,当序列播放时第五个轨道将不会发声,当前静音的任何其他轨道也不会发声。
独奏轨道的方法和技巧与静音非常相似。要独奏一个轨道,调用Sequence
的setTrackSolo
方法:
void setTrackSolo(int track, boolean bSolo)
与setTrackMute
相同,第一个参数指定基于零的轨道编号,第二个参数如果为true
,则指定该轨道应处于独奏模式;否则该轨道不应处于独奏状态。
默认情况下,轨道既不静音也不独奏。
与其他 MIDI 设备同步
Sequencer
有一个名为Sequencer.SyncMode
的内部类。SyncMode
对象代表 MIDI 序列器的时间概念如何与主设备或从设备同步的一种方式。如果序列器正在与主设备同步,序列器会根据来自主设备的某些 MIDI 消息调整其当前时间。如果序列器有一个从设备,序列器同样会发送 MIDI 消息来控制从设备的定时。
有三种预定义模式指定了序列器可能的主设备:INTERNAL_CLOCK
、MIDI_SYNC
和MIDI_TIME_CODE
。后两种模式在序列器接收来自另一设备的 MIDI 消息时起作用。在这两种模式下,序列器的时间会根据系统实时定时时钟消息或 MIDI 时间码(MTC)消息进行重置。 (有关这些消息类型的更多信息,请参阅 MIDI 规范。)这两种模式也可以用作从模式,此时序列器会向其接收器发送相应类型的 MIDI 消息。第四种模式NO_SYNC
用于指示序列器不应向其接收器发送定时信息。
通过调用带有支持的SyncMode
对象作为参数的setMasterSyncMode
方法,您可以指定序列器的定时如何受控。同样,setSlaveSyncMode
方法确定序列器将向其接收器发送哪些定时信息。这些信息控制使用序列器作为主定时源的设备的定时。
指定特殊事件监听器
序列的每个轨道可以包含许多不同类型的MidiEvents
。这些事件包括音符开和音符关消息、程序更改、控制更改和元事件。Java Sound API 为最后两种事件类型(控制更改事件和元事件)指定了“监听器”接口。您可以使用这些接口在播放序列时接收这些事件发生时的通知。
支持ControllerEventListener
接口的对象可以在Sequencer
处理特定控制变化消息时接收通知。控制变化消息是一种标准的 MIDI 消息类型,代表了 MIDI 控制器值的变化,比如音高弯曲轮或数据滑块。 (请参阅 MIDI 规范获取控制变化消息的完整列表。)当在序列播放过程中处理这样的消息时,消息指示任何设备(可能是合成器)从序列器接收数据以更新某些参数的值。该参数通常控制声音合成的某些方面,比如如果控制器是音高弯曲轮,则控制当前发声音符的音高。当录制序列时,控制变化消息意味着在创建消息的外部物理设备上移动了一个控制器,或者在软件中模拟了这样的移动。
这里是ControllerEventListener
接口的使用方法。假设你已经开发了一个实现ControllerEventListener
接口的类,意味着你的类包含以下方法:
void controlChange(ShortMessage msg)
假设你已经创建了一个类的实例,并将其赋给一个名为myListener
的变量。如果你在程序的某个地方包含以下语句:
int[] controllersOfInterest = { 1, 2, 4 }; sequencer.addControllerEventListener(myListener, controllersOfInterest);
那么你的类的controlChange
方法将在Sequencer
处理 MIDI 控制器编号为 1、2 或 4 的控制变化消息时被调用。换句话说,当Sequencer
处理设置任何已注册控制器的值的请求时,Sequencer
将调用你的类的controlChange
方法。(请注意,将 MIDI 控制器编号分配给特定控制设备的详细信息在 MIDI 1.0 规范中有详细说明。)
controlChange
方法接收一个包含受影响的控制器编号和控制器设置的新值的ShortMessage
。你可以使用ShortMessage.getData1
方法获取控制器编号,并使用ShortMessage.getData2
方法获取控制器值的新设置。
另一种特殊事件监听器的类型由MetaEventListener
接口定义。根据标准 MIDI 文件 1.0 规范,元消息是不在 MIDI 线协议中存在但可以嵌入到 MIDI 文件中的消息。它们对合成器没有意义,但可以被序列器解释。元消息包括指令(如变速命令)、歌词或其他文本以及其他指示(如音轨结束)。
MetaEventListener
机制类似于ControllerEventListener
。在任何需要在Sequencer
处理MetaMessage
时收到通知的类中实现MetaEventListener
接口。这涉及向类中添加以下方法:
void meta(MetaMessage msg) • 1
通过将此类的实例作为参数传递给Sequencer addMetaEventListener
方法来注册该类的实例:
boolean b = sequencer.addMetaEventListener (myMetaListener);
这与ControllerEventListener
接口所采取的方法略有不同,因为您必须注册以接收所有MetaMessages
,而不仅仅是感兴趣的部分。如果顺序器在其序列中遇到MetaMessage
,它将调用myMetaListener.meta
,并将遇到的MetaMessage
传递给它。meta
方法可以调用其MetaMessage
参数上的getType
,以获取一个从 0 到 127 的整数,该整数表示消息类型,如标准 MIDI 文件 1.0 规范所定义。
合成声音
大多数使用 Java Sound API 的 MIDI 包的程序都是用来合成声音。之前讨论过的整个 MIDI 文件、事件、序列和序列器的装置几乎总是最终将音乐数据发送到合成器以转换为音频。(可能的例外包括将 MIDI 转换为音乐符号的程序,可以被音乐家阅读,以及向外部 MIDI 控制设备发送消息的程序,如混音台。)
因此,Synthesizer
接口对于 MIDI 包至关重要。本页展示了如何操作合成器播放声音。许多程序将简单地使用序列器将 MIDI 文件数据发送到合成器,并且不需要直接调用许多Synthesizer
方法。然而,也可以直接控制合成器,而不使用序列器甚至MidiMessage
对象,如本页末尾所述。
对于不熟悉 MIDI 的读者来说,合成架构可能看起来很复杂。其 API 包括三个接口:
和四个类:
作为对所有这些 API 的定位,下一节解释了一些 MIDI 合成的基础知识以及它们如何在 Java Sound API 中反映。随后的部分将更详细地查看 API。
理解 MIDI 合成
合成器是如何产生声音的?根据其实现方式,它可能使用一种或多种声音合成技术。例如,许多合成器使用波表合成。波表合成器从内存中读取存储的音频片段,以不同的采样率播放它们,并循环播放它们以创建不同音高和持续时间的音符。例如,要合成萨克斯风演奏 C#4 音符(MIDI 音符号 61)的声音,合成器可能会访问从萨克斯风演奏中音符中央 C(MIDI 音符号 60)的录音中提取的一个非常短的片段,然后以比录制时略快的采样率不断循环播放这个片段,从而创建一个略高音高的长音符。其他合成器使用诸如频率调制(FM)、加法合成或物理建模等技术,这些技术不使用存储的音频,而是使用不同的算法从头开始生成音频。
乐器
所有合成技术共同之处在于能够创造许多种声音。不同的算法,或者同一算法内不同参数的设置,会产生不同的声音结果。一个乐器是合成某种类型声音的规范。该声音可能模拟传统乐器,如钢琴或小提琴;也可能模拟其他类型的声源,例如电话或直升机;或者根本不模拟任何“现实世界”的声音。一个名为通用 MIDI 的规范定义了一个标准的 128 种乐器列表,但大多数合成器也允许使用其他乐器。许多合成器提供一系列内置乐器,始终可供使用;一些合成器还支持加载额外乐器的机制。
一个乐器可能是特定供应商的——换句话说,仅适用于一个合成器或同一供应商的几个型号。当两个不同的合成器使用不同的声音合成技术,或者即使基本技术相同,但使用不同的内部算法和参数时,就会出现不兼容性。由于合成技术的细节通常是专有的,因此不兼容性是常见的。Java Sound API 包括检测给定合成器是否支持给定乐器的方法。
一个乐器通常可以被视为一个预设;你不必了解产生其声音的合成技术的细节。然而,你仍然可以改变其声音的各个方面。每个 Note On 消息指定一个单独音符的音高和音量。你还可以通过其他 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
对象中。通过指定一个新的 patch,您可以更改 MIDI 通道的当前乐器。该 patch 可以被视为当前声音库中乐器的二维索引。
您可能想知道声音库是否也是按数字索引的。答案是否定的;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 规范提供了银行选择和程序更改命令,这些命令影响每个 MIDI 通道上当前播放的乐器。然而,该规范并未定义每个补丁位置(银行和程序号)应该放置什么乐器。较新的 General MIDI 规范通过定义一个包含 128 个与特定乐器声音对应的程序的银行来解决这个问题。General MIDI 合成器使用 128 个与指定集合匹配的乐器。即使播放应该是相同乐器的不同 General MIDI 合成器听起来可能会有很大不同。然而,一个 MIDI 文件在大多数情况下应该听起来相似(即使不完全相同),无论哪个 General MIDI 合成器在播放它。
尽管如此,并非所有的 MIDI 文件创建者都希望受限于 General 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
的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 通道上不会有任何声音。
访问声音库资源
一些合成器在其声音库中存储除乐器之外的其他信息。例如,波表合成器存储一个或多个乐器可以访问的音频样本。因为样本可能被多个乐器共享,它们独立于任何乐器存储在声音库中。Soundbank
接口和Instrument
类都提供一个名为getSoundbankResources
的方法调用,返回一个SoundbankResource
对象列表。这些对象的细节特定于为其设计声音库的合成器。在波表合成的情况下,资源可能是一个封装自音频录音片段的一系列音频样本的对象。使用其他合成技术的合成器可能在合成器的SoundbankResources
数组中存储其他类型的对象。
查询合成器的功能和当前状态
Synthesizer
接口包括返回有关合成器功能的信息的方法:
public long getLatency() public int getMaxPolyphony()
延迟度量了传递 MIDI 消息到合成器并合成器实际产生相应结果之间的最坏情况延迟。例如,合成器在接收到音符开启事件后可能需要几毫秒才开始生成音频。
getMaxPolyphony
方法指示合成器可以同时发出多少音符,如前面在 Voices 下讨论的那样。如同在同一讨论中提到的,合成器可以提供关于其音色的信息。这是通过以下方法实现的:
public VoiceStatus[] getVoiceStatus()
返回的数组中的每个 VoiceStatus
报告了音色的当前活动或非活动状态、MIDI 通道、银行和程序号、MIDI 音符号码和 MIDI 音量。数组的长度通常应该与 getMaxPolyphony
返回的相同数量一样。如果合成器没有播放,所有其 VoiceStatus
对象的 active 字段都设置为 false
。
您可以通过检索其 MidiChannel
对象并查询其状态来了解有关合成器当前状态的其他信息。这将在下一节中更详细地讨论。
使用通道
有时访问合成器的 MidiChannel
对象直接是有用或必要的。本节讨论了这种情况。
在不使用序列器的情况下控制合成器
当使用序列器时,比如从 MIDI 文件中读取的序列,您不需要自己向合成器发送 MIDI 命令。相反,您只需将序列加载到序列器中,将序列器连接到合成器,并让其运行。序列器负责安排事件的时间表,结果是可预测的音乐表现。当所需音乐事先已知时,这种情况是有效的,这在从文件中读取时是正确的。
然而,在某些情况下,音乐是在播放时即时生成的。例如,用户界面可能会显示一个音乐键盘或吉他指板,并允许用户通过鼠标点击随意弹奏音符。另一个例子,一个应用程序可能使用合成器不是为了演奏音乐本身,而是为了根据用户的操作生成音效。这种情况在游戏中很典型。最后一个例子,应用程序可能确实正在播放从文件中读取的音乐,但用户界面允许用户与音乐互动,动态地改变它。在所有这些情况下,应用程序直接向合成器发送命令,因为 MIDI 消息需要立即传递,而不是被安排在将来的某个确定时间点。
有至少两种方法可以将 MIDI 消息发送到合成器而不使用序列器。第一种方法是构造一个 MidiMessage
并通过 Receiver
的 send 方法将其传递给合成器。例如,要在 MIDI 通道 5(从 1 开始计数)上立即产生中央 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
对象交互。您首先需要检索合成器的 MidiChannel
对象,使用以下 Synthesizer
方法:
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 合成器产生的音频通常会通过采样音频系统路由。如果您的程序没有权限播放音频,则合成器的声音将听不到,并且会抛出安全异常。有关音频权限的更多信息,请参阅之前关于使用音频资源的权限使用音频资源的权限的讨论。
服务提供者接口简介
什么是服务?
服务是声音处理功能单元,当应用程序使用 Java Sound API 的实现时自动可用。它们由执行读取、写入、混合、处理和转换音频和 MIDI 数据工作的对象组成。Java Sound API 的实现通常提供一组基本服务,但 API 中也包含机制,支持第三方开发人员(或实现供应商自身)开发新声音服务。这些新服务可以“插入”到现有安装的实现中,扩展其功能而不需要发布新版本。在 Java Sound API 架构中,第三方服务被集成到系统中,以便应用程序的接口与“内置”服务的接口相同。在某些情况下,使用 javax.sound.sampled
和 javax.sound.midi
包的应用程序开发人员甚至可能不知道他们正在使用第三方服务。
潜在的第三方采样音频服务示例包括:
- 声音文件读取器和写入器
- 在不同音频数据格式之间转换的转换器
- 新的音频混音器和输入/输出设备,无论是纯粹在软件中实现,还是在硬件中具有软件接口
第三方 MIDI 服务可能包括:
- MIDI 文件读取器和写入器
- 用于各种类型声音库文件的读取器(通常特定于特定合成器)
- 受 MIDI 控制的声音合成器、音序器和 I/O 端口,无论是纯粹在软件中实现,还是在硬件中具有软件接口
服务如何工作
javax.sound.sampled
和 javax.sound.midi
包为希望在其应用程序中包含声音服务的应用程序开发人员提供功能。这些包是声音服务的消费者,提供接口以获取有关音频和 MIDI 服务的信息、控制和访问。此外,Java Sound API 还提供了两个定义抽象类的包,供声音服务的提供者使用:javax.sound.sampled.spi
和 javax.sound.midi.spi
包。
新声音服务的开发人员实现 SPI 包中适当类的具体子类。这些子类以及支持新服务所需的任何其他类都放在一个包含所包含服务描述的 Java 存档(JAR)存档文件中。当此 JAR 文件安装在用户的 CLASSPATH
中时,运行时系统会自动使新服务可用,扩展 Java 平台运行时系统的功能。
一旦安装了新服务,它就可以像以前安装的任何服务一样访问。服务的消费者可以通过调用javax.sound.sampled
和javax.sound.midi
包中的AudioSystem
和MidiSystem
类的方法来获取有关新服务的信息,或获取新服务类的实例,以返回有关新服务的信息,或返回新的或现有服务类的实例。应用程序无需直接引用 SPI 包(及其子类)中的类来使用已安装的服务。
例如,假设一个名为 Acme Software, Inc.的假想服务提供商有兴趣提供一个允许应用程序读取新格式声音文件的包(但其音频数据是标准数据格式的)。SPI 类AudioFileReader
可以被子类化为一个名为AcmeAudioFileReader
的类。在新的子类中,Acme 将提供AudioFileReader
中定义的所有方法的实现;在这种情况下,只有两个方法(带参数变体),getAudioFileFormat
和getAudioInputStream
。然后,当应用程序尝试读取一个恰好是 Acme 文件格式的声音文件时,它会调用javax.sound.sampled
中的AudioSystem
类的方法来访问文件和有关文件的信息。方法AudioSystem.getAudioInputStream
和AudioSystem.getAudioFileFormat
提供了一个标准的 API 来读取音频流;安装了AcmeAudioFileReader
类后,此接口会被扩展以透明地支持新文件类型。应用程序开发人员不需要直接访问新注册的 SPI 类:AudioSystem
对象方法会将查询传递给已安装的AcmeAudioFileReader
类。
为什么要有这些“工厂”类?为什么不允许应用程序开发人员直接访问新提供的服务?这是一种可能的方法,但通过门卫系统对象管理和实例化所有服务可以使应用程序开发人员免于了解已安装服务的身份。应用程序开发人员只需使用对他们有价值的服务,甚至可能都没有意识到。同时,这种架构允许服务提供者有效地管理其包中的可用资源。
通常,新声音服务的使用对应用程序是透明的。例如,想象一种情况,应用程序开发人员想要从文件中读取音频流。假设thePathName
标识了一个音频输入文件,程序会这样做:
File theInFile = new File(thePathName); AudioInputStream theInStream = AudioSystem.getAudioInputStream(theInFile);
在幕后,AudioSystem
确定了哪个已安装的服务可以读取文件,并要求其提供音频数据作为AudioInputStream
对象。开发人员可能不知道或甚至不关心输入音频文件是某种新文件格式(例如 Acme 的格式),这些格式由已安装的第三方服务支持。程序与流的第一次接触是通过AudioSystem
对象,其后所有对流及其属性的访问都是通过AudioInputStream
的方法。这两者都是javax.sound.sampled
API 中的标准对象;新文件格式可能需要的特殊处理完全被隐藏起来。
服务提供者如何准备新服务
服务提供者以特殊格式的 JAR 文件提供其新服务,这些文件将被安装在用户系统中 Java 运行时将找到的目录中。JAR 文件是存档文件,每个文件包含一组文件,这些文件可能在存档中的分层目录结构中组织。关于放入这些存档的类文件的准备细节将在接下来的几页中讨论,这些页面描述了音频和 MIDI SPI 包的具体内容;在这里,我们只是概述 JAR 文件创建的过程。
新服务或服务的 JAR 文件应包含 JAR 文件中支持的每个服务的类文件。遵循 Java 平台的约定,每个类文件都具有新定义类的名称,这是一个抽象服务提供者类的具体子类。JAR 文件还必须包含新服务实现所需的任何支持类。为了使运行时系统的服务提供者机制能够定位新服务,JAR 文件还必须包含特殊文件(下文描述),将 SPI 类名称映射到正在定义的新子类。
继续我们上面的例子,假设 Acme Software, Inc.正在分发一套新的采样音频服务包。假设这个包包含两个新服务:
AcmeAudioFileReader
类,如上所述,是AudioFileReader
的子类- 一个名为
AcmeAudioFileWriter
的AudioFileWriter
子类,将以 Acme 的新格式编写声音文件
从一个任意目录开始——我们称之为/devel
——我们创建子目录并将新的类文件放入其中,以一种组织方式来给出新类将被引用的期望路径名:
com/acme/AcmeAudioFileReader.class com/acme/AcmeAudioFileWriter.class
此外,对于每个新的 SPI 类的子类,我们在一个名为META-INF/services
的特殊命名目录中创建一个映射文件。文件的名称是被子类化的 SPI 类的名称,文件包含该 SPI 抽象类的新子类的名称。
我们创建文件
META-INF/services/javax.sound.sampled.spi.AudioFileReader
包括
# Providers of sound file-reading services # (a comment line begins with a pound sign) com.acme.AcmeAudioFileReader
以及文件
META-INF/services/javax.sound.sampled.spi.AudioFileWriter
包括
# Providers of sound file-writing services com.acme.AcmeAudioFileWriter
现在我们在任何目录中运行jar
命令行:
jar cvf acme.jar -C /devel .
-C
选项会导致jar
切换到/devel
目录,而不是使用执行命令的目录。最后的句点参数指示jar
归档该目录的所有内容(即/devel
),但不包括目录本身。
这次运行将创建文件acme.jar
,其中包含以下内容:
com/acme/AcmeAudioFileReader.class com/acme/AcmeAudioFileWriter.class META-INF/services/javax.sound.sampled.spi.AudioFileReader META-INF/services/javax.sound.sampled.spi.AudioFileWriter META-INF/Manifest.mf
文件Manifest.mf
是由jar
工具本身生成的,其中列出了存档中包含的所有文件。
用户如何安装新服务
对于希望通过他们的应用程序获得新服务访问权限的最终用户(或系统管理员),安装是简单的。他们将提供的 JAR 文件放在他们的CLASSPATH
中的一个目录中。在执行时,Java 运行时会在需要时找到引用的类。
安装同一服务的多个提供者并不是错误。例如,两个不同的服务提供者可能提供支持读取相同类型的声音文件。在这种情况下,系统会任意选择一个提供者。在意识到哪个提供者被选择的用户应该只安装所需的那个。
提供采样音频服务
原文:
docs.oracle.com/javase/tutorial/sound/SPI-providing-sampled.html
正如你所知,Java Sound API 包括两个包,javax.sound.sampled.spi
和 javax.sound.midi.spi
,它们定义了抽象类,供声音服务的开发者使用。通过实现并安装这些抽象类的子类,服务提供者可以注册新服务,扩展运行时系统的功能。本页面告诉你如何使用 javax.sound.sampled.spi
包来提供处理采样音频的新服务。
javax.sound.sampled.spi
包中有四个抽象类,代表着你可以为采样音频系统提供的四种不同类型的服务:
AudioFileWriter
提供音频文件写入服务。这些服务使应用程序能够将音频数据流写入特定类型的文件。AudioFileReader
提供文件读取服务。这些服务使应用程序能够确定音频文件的特性,并获取一个流,从中可以读取文件的音频数据。FormatConversionProvider
提供音频数据格式转换服务。这些服务允许应用程序将音频流从一种数据格式转换为另一种。MixerProvider
提供特定类型混音器的管理。这种机制允许应用程序获取关于给定类型混音器的信息,并访问实例。
总结之前的讨论,服务提供者可以扩展运行时系统的功能。典型的 SPI 类有两种类型的方法:一种是响应关于特定提供者提供的服务类型的查询,另一种是直接执行新服务,或返回实际提供服务的对象实例。运行时环境的服务提供者机制提供了已安装服务与音频系统的注册,以及新服务提供者类的管理。
本质上,服务实例与应用程序开发人员之间存在双重隔离。应用程序从不直接创建服务对象的实例,例如混音器或格式转换器,以用于其音频处理任务。程序甚至不会直接从管理它们的 SPI 类中请求这些对象。应用程序向javax.sound.sampled
包中的AudioSystem
对象发出请求,AudioSystem
反过来使用 SPI 对象来处理这些查询和服务请求。
新音频服务的存在对用户和应用程序员可能是完全透明的。所有应用程序引用都通过javax.sound.sampled
包的标准对象,主要是AudioSystem
,新服务可能提供的特殊处理通常是完全隐藏的。
在本讨论中,我们将继续使用类似AcmeMixer
和AcmeMixerProvider
的名称来指代新的 SPI 子类。
提供音频文件写入服务
让我们从AudioFileWriter
开始,这是较简单的 SPI 类之一。
实现AudioFileWriter
方法的子类必须提供一组方法的实现,以处理关于类支持的文件格式和文件类型的查询,以及提供实际将提供的音频数据流写入File
或OutputStream
的方法。
AudioFileWriter
包括两个在基类中具有具体实现的方法:
boolean isFileTypeSupported(AudioFileFormat.Type fileType) boolean isFileTypeSupported(AudioFileFormat.Type fileType, AudioInputStream stream)
这些方法中的第一个方法通知调用者,此文件写入器是否可以写入指定类型的声音文件。这个方法是一个一般性的查询,如果文件写入器可以写入那种类型的文件,它将返回true
,假设文件写入器被提供适当的音频数据。然而,写入文件的能力可能取决于传递给文件写入器的特定音频数据的格式。文件写入器可能不支持每种音频数据格式,或者约束可能由文件格式本身施加。(并非所有类型的音频数据都可以写入所有类型的声音文件。)因此,第二个方法更具体,询问特定的AudioInputStream
是否可以写入特定类型的文件。
通常情况下,您不需要覆盖这两个具体方法。每个方法只是调用两个其他查询方法之一并遍历返回的结果的包装器。这两个其他查询方法是抽象的,因此需要在子类中实现:
abstract AudioFileFormat.Type[] getAudioFileTypes() abstract AudioFileFormat.Type[] getAudioFileTypes(AudioInputStream stream)
这些方法直接对应于前两个方法。每个方法返回所有支持的文件类型的数组-在第一个方法的情况下,通常是所有一般支持的,在第二个方法的情况下,是特定音频流支持的所有文件类型。第一个方法的典型实现可能简单地返回一个由文件写入器构造函数初始化的数组。第二个方法的实现可能测试流的AudioFormat
对象,以查看请求的文件类型是否支持该数据格式。
AudioFileWriter
的最后两个方法执行实际的文件写入工作:
abstract int write(AudioInputStream stream, AudioFileFormat.Type fileType, java.io.File out) abstract int write(AudioInputStream stream, AudioFileFormat.Type fileType, java.io.OutputStream out)
这些方法将代表音频数据的字节流写入到第三个参数指定的流或文件中。如何完成这项工作的细节取决于指定类型文件的结构。write
方法必须按照该格式声音文件的规定方式写入文件的头部和音频数据(无论是标准类型的声音文件还是新的、可能是专有的类型)。
提供音频文件读取服务
AudioFileReader
类由六个抽象方法组成,您的子类需要实现这些方法-实际上,两个不同的重载方法,每个方法都可以接受File
、URL
或InputStream
参数。这两个重载方法中的第一个接受有关指定文件格式的查询:
abstract AudioFileFormat getAudioFileFormat(java.io.File file) abstract AudioFileFormat getAudioFileFormat(java.io.InputStream stream) abstract AudioFileFormat getAudioFileFormat(java.net.URL url)
getAudioFileFormat
方法的典型实现读取并解析声音文件的头部,以确定其文件格式。查看AudioFileFormat
类的描述,了解需要从头部读取哪些字段,并参考特定文件类型的规范,以了解如何解析头部。
因为调用者将流作为参数提供给此方法,希望该方法不改变流,文件读取器通常应该从标记流开始。在读取到头部结束后,应该将流重置到其原始位置。
另一个重载的AudioFileReader
方法提供文件读取服务,通过返回一个AudioInputStream
,从中可以读取文件的音频数据:
abstract AudioInputStream getAudioInputStream(java.io.File file) abstract AudioInputStream getAudioInputStream(java.io.InputStream stream) abstract AudioInputStream getAudioInputStream(java.net.URL url)
通常,getAudioInputStream
的实现返回一个绕到文件数据块(在头部之后)开头的AudioInputStream
,准备好进行读取。然而,可以想象,文件读取器返回的AudioInputStream
可能表示一种从文件中包含的内容解码出来的数据流。重要的是,该方法返回一个格式化的流,从中可以读取文件中包含的音频数据。返回的AudioInputStream
对象中封装的AudioFormat
将告知调用者有关流的数据格式,通常情况下,但不一定是文件本身的数据格式。
通常,返回的流是AudioInputStream
的一个实例;您不太可能需要对AudioInputStream
进行子类化。
提供格式转换服务
FormatConversionProvider
子类将具有一个音频数据格式的AudioInputStream
转换为具有另一种格式的AudioInputStream
。前者(输入)流被称为源流,后者(输出)流被称为目标流。回想一下,AudioInputStream
包含一个AudioFormat
,而AudioFormat
又包含一种特定类型的数据编码,由AudioFormat.Encoding
对象表示。源流中的格式和编码称为源格式和源编码,目标流中的格式和编码同样被称为目标格式和目标编码。
转换工作是在FormatConversionProvider
的重载抽象方法getAudioInputStream
中执行的。该类还具有用于了解所有支持的目标和源格式和编码的抽象查询方法。有具体的包装方法用于查询特定的转换。
getAudioInputStream
的两个变体是:
abstract AudioInputStream getAudioInputStream(AudioFormat.Encoding targetEncoding, AudioInputStream sourceStream)
和
abstract AudioInputStream getAudioInputStream(AudioFormat targetFormat, AudioInputStream sourceStream)
这些根据调用者是指定完整目标格式还是只是格式的编码而有所不同的第一个参数。
getAudioInputStream
的典型实现通过返回一个围绕原始(源)AudioInputStream
的新的AudioInputStream
子类来工作,并在调用read
方法时对其数据应用数据格式转换。例如,考虑一个名为AcmeCodec
的新FormatConversionProvider
子类的情况,它与一个名为AcmeCodecStream
的新AudioInputStream
子类一起工作。
AcmeCodec
的第二个getAudioInputStream
方法的实现可能是:
public AudioInputStream getAudioInputStream (AudioFormat outputFormat, AudioInputStream stream) { AudioInputStream cs = null; AudioFormat inputFormat = stream.getFormat(); if (inputFormat.matches(outputFormat) ) { cs = stream; } else { cs = (AudioInputStream) (new AcmeCodecStream(stream, outputFormat)); tempBuffer = new byte[tempBufferSize]; } return cs; }
实际的格式转换发生在返回的AcmeCodecStream
的新read
方法中,它是AudioInputStream
的子类。同样,访问这个返回的AcmeCodecStream
的应用程序只需将其视为AudioInputStream
进行操作,而不需要了解其实现的细节。
FormatConversionProvider
的其他方法都允许查询对象支持的输入和输出编码和格式。以下四个方法是抽象的,需要被实现:
abstract AudioFormat.Encoding[] getSourceEncodings() abstract AudioFormat.Encoding[] getTargetEncodings() abstract AudioFormat.Encoding[] getTargetEncodings( AudioFormat sourceFormat) abstract AudioFormat[] getTargetFormats( AudioFormat.Encoding targetEncoding, AudioFormat sourceFormat)
与上面讨论的AudioFileReader
类的查询方法一样,这些查询通常通过检查对象的私有数据,并且对于后两种方法,将它们与参数进行比较来处理。
剩下的四个FormatConversionProvider
方法是具体的,通常不需要被重写:
boolean isConversionSupported( AudioFormat.Encoding targetEncoding, AudioFormat sourceFormat) boolean isConversionSupported(AudioFormat targetFormat, AudioFormat sourceFormat) boolean isSourceEncodingSupported( AudioFormat.Encoding sourceEncoding) boolean isTargetEncodingSupported( AudioFormat.Encoding targetEncoding)
与AudioFileWriter.isFileTypeSupported()
类似,这些方法的默认实现本质上是调用其他查询方法之一并遍历返回的结果的包装器。
提供新类型的混音器
正如其名称所示,MixerProvider
提供混音器的实例。每个具体的MixerProvider
子类都充当应用程序使用的Mixer
对象的工厂。当然,只有在定义一个或多个新的Mixer
接口的实现时,定义新的MixerProvider
才有意义。就像上面的FormatConversionProvider
示例中,我们的getAudioInputStream
方法返回了一个执行转换的AudioInputStream
子类一样,我们的新类AcmeMixerProvider
有一个getMixer
方法,返回实现Mixer
接口的另一个新类的实例。我们将后者称为AcmeMixer
。特别是如果混音器是硬件实现的,提供者可能仅支持所请求设备的一个静态实例。如果是这样,它应该在每次调用getMixer
时返回这个静态实例。
由于AcmeMixer
支持Mixer
接口,应用程序不需要任何额外的信息来访问其基本功能。然而,如果AcmeMixer
支持Mixer
接口中未定义的功能,并且供应商希望使这些扩展功能对应用程序可访问,那么混音器当然应该被定义为一个公共类,具有额外的、有文档记录的公共方法,以便希望利用这些扩展功能的程序可以导入AcmeMixer
并将getMixer
返回的对象转换为这种类型。
另外两种MixerProvider
的方法是:
abstract Mixer.Info[] getMixerInfo()
和
boolean isMixerSupported(Mixer.Info info)
这些方法允许音频系统确定这个特定的提供者类是否可以提供应用程序需要的设备。换句话说,AudioSystem
对象可以迭代所有已安装的MixerProviders
,看看哪些,如果有的话,可以提供应用程序请求的AudioSystem
的设备。getMixerInfo
方法返回一个包含有关此提供程序对象提供的混音器类型信息的对象数组。系统可以将这些信息对象与其他提供程序的信息一起传递给应用程序。
一个MixerProvider
可以提供多种类型的混音器。当系统调用MixerProvider
的getMixerInfo
方法时,它会得到一个信息对象列表,标识此提供程序支持的不同类型的混音器。然后系统可以调用MixerProvider.getMixer(Mixer.Info)
来获取每个感兴趣的混音器。
你的子类需要实现getMixerInfo
,因为它是抽象的。isMixerSupported
方法是具体的,通常不需要被覆盖。默认实现只是将提供的Mixer.Info
与getMixerInfo
返回的数组中的每一个进行比较。
提供 MIDI 服务
原文:
docs.oracle.com/javase/tutorial/sound/SPI-providing-MIDI.html
服务提供者接口简介 解释了javax.sound.sampled.spi
和javax.sound.midi.spi
包定义了供声音服务开发人员使用的抽象类。通过实现这些抽象类的子类,服务提供者可以创建一个扩展运行时系统功能的新服务。前一节介绍了如何使用javax.sound.sampled.spi
包。本节讨论如何使用javax.sound.midi.spi
包为处理 MIDI 设备和文件提供新服务。
javax.sound.midi.spi
包中有四个抽象类,代表着你可以为 MIDI 系统提供的四种不同类型的服务:
MidiFileWriter
提供了 MIDI 文件写入服务。这些服务使应用程序能够将其生成或处理的 MIDISequence
保存到 MIDI 文件中。MidiFileReader
提供了从 MIDI 文件中返回 MIDISequence
供应用程序使用的文件读取服务。MidiDeviceProvider
提供了一个或多个特定类型的 MIDI 设备实例,可能包括硬件设备。SoundbankReader
提供了声音库文件读取服务。SoundbankReader
的具体子类解析给定的声音库文件,生成一个可以加载到Synthesizer
中的Soundbank
对象。
应用程序不会直接创建服务对象的实例,无论是提供者对象,比如MidiDeviceProvider
,还是由提供者对象提供的对象,比如Synthesizer
。程序也不会直接引用 SPI 类。相反,应用程序会向javax.sound.midi
包中的MidiSystem
对象发出请求,而MidiSystem
又会使用javax.sound.midi.spi
类的具体子类来处理这些请求。
Java 中文官方教程 2022 版(三十三)(3)https://developer.aliyun.com/article/1488022