博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Web Audio API 六:高级主题

Posted on 2022-05-22 20:21  pencilCool  阅读(234)  评论(0编辑  收藏  举报

翻译自:https://webaudioapi.com/book/Web_Audio_API_Boris_Smus_html/ch06.html

这一章涵盖了非常重要的主题,但比书中其他部分略微复杂一些。我们将深入研究为声音添加效果,在没有任何音频缓冲区的情况下生成合成的声音效果,模拟不同声学环境的效果,以及在三维空间中对声音进行空间化。

Biquad滤波器

滤波器可以强调或减弱声音频谱的某些部分。从视觉上看,它可以在频域上显示为一个图形,称为频率响应图(见图6-1)。对于每个频率,图中的值越高,就越强调该部分的频率范围。一个向下倾斜的图形会更多地强调低频,而减少对高频的强调。

Web Audio 滤波器可以用三个参数进行配置:增益、频率和质量系数(也称为Q)。这些参数都会对频率响应图产生不同的影响。

有很多种类的滤波器可以用来实现某些种类的效果。

低通滤波器:使得声音更加消沉

高通滤波器:使得声音更加尖锐

带通滤波器
切断低频和高频(如电话滤波器)。

低频滤波器
影响声音中的低音量(如立体声中的低音旋钮)

高音滤波器
影响声音中的高音量(如立体声中的高音旋钮)

峰值滤波器
影响声音中的中音量(如立体声中的中音旋钮)

陷波滤波器
在一个狭窄的频率范围内去除不需要的声音

全通滤波器
创造相位器效果

image
Figure 6-1. Frequency response graph for a low-pass filter

所有这些biquad滤波器都源于一个共同的数学模型,都可以像图6-1中的低通滤波器那样绘制成图表。关于这些滤波器的更多细节,可以在数学要求较高的书中找到,如Perry R. Cook的《Real Sound Synthesis for Interactive Applications》(A K Peters,2002),如果你对音频基础知识感兴趣,我强烈推荐你阅读。

通过过滤器添加效果
使用网络音频API,我们可以使用BiquadFilterNodes来应用上面讨论的过滤器。这种类型的音频节点非常常用于建立均衡器和以有趣的方式操纵声音。让我们设置一个简单的低通滤波器,以消除声音样本中的低频噪音。

// 创建一个滤波器
var filter = context.createBiquadFilter()。
// 注意:网络音频规范正在从常量向字符串转变。
// filter.type = 'lowpass';
filter.type = filter.LOWPASS;
filter.frequency.value = 100;
// 将信号源连接到它,将滤波器连接到目的地。

BiquadFilterNode支持所有常用的二阶滤波器类型。我们可以用上一节中讨论的相同参数来配置这些节点,也可以通过使用节点上的getFrequencyResponse方法来可视化频率响应图。给定一个频率数组,这个函数返回对应于每个频率的响应幅度数组。

Chris Wilson和Chris Rogers制作了一个很好的可视化样本(图6-2),显示了Web Audio API中所有过滤器类型的频率响应。

image
Figure 6-2. A graph of the frequency response of a low-pass filter with parameters

程序生成的声音
到目前为止,我们一直假设你的游戏的声源是静态的。一个音频设计师创造了一堆资产,并把它们交给了你。然后,你根据当地的条件(例如,房间的环境以及声源和听众的相对位置),用一些参数化的方法来播放它们。这种方法有几个缺点。

声音资产将是非常大的。这在网络上尤其糟糕,你不是从硬盘上加载,而是从网络上加载(至少是第一次),这大约是一个数量级的慢。

即使有许多资产和对每个资产的调整,种类也很有限。

你需要通过搜索音效库来寻找资产,然后可能还要担心版权费。另外,有可能,任何给定的声音效果已经在其他应用程序中使用了,所以你的用户有意外的联想。

我们可以使用网络音频API来完全按程序生成声音。例如,让我们模拟枪声。我们从一个白噪声的缓冲区开始,我们可以用ScriptProcessorNode生成,如下。

函数 WhiteNoiseScript() {
this.node = context.createScriptProcessor(1024, 1, 2);
this.node.onaudioprocess = this.process。
}

WhiteNoiseScript.prototype.process = function(e) {
var L = e.outputBuffer.getChannelData(0);
var R = e.outputBuffer.getChannelData(1);
for (var i = 0; i < L.length; i++) {
L[i] = ((Math.random() * 2) - 1);
R[i] = L[i]。
}
};
关于ScriptProcessorNodes的更多信息,请看用JavaScript进行音频处理。

这段代码不是一个高效的实现,因为JavaScript需要不断地、动态地创建一个白噪声流。为了提高效率,我们可以通过编程生成一个白噪声的单声道AudioBuffer,如下。

函数 WhiteNoiseGenerated(callback) {
// 生成一个5秒的白噪声缓冲区。
var lengthInSamples = 5 * context.sampleRate;
var buffer = context.createBuffer(1, lengthInSamples, context.sampleRate);
var data = buffer.getChannelData(0);

for (var i = 0; i < lengthInSamples; i++) {
data[i] = ((Math.random() * 2) - 1);
}

// 从缓冲区创建一个源节点。
this.node = context.createBufferSource()。
this.node.buffer = buffer;
this.node.loop = true;
this.node.start(0);
}
接下来,我们可以在一个包络中模拟火炮发射的各个阶段--攻击、衰减和释放。

函数Envelope() {
this.node = context.createGain()
this.node.gain.value = 0。
}

Envelope.prototype.addEventToQueue = function() {
this.node.gain.linearRampToValueAtTime(0, context.currentTime)。
this.node.gain.linearRampToValueAtTime(1, context.currentTime + 0.001);
this.node.gain.linearRampToValueAtTime(0.3, context.currentTime + 0.101);
this.node.gain.linearRampToValueAtTime(0,context.currentTime + 0.500)。
};
最后,我们可以将声音输出连接到一个过滤器,以实现距离的模拟。

this.voices = [];
this.voiceIndex = 0;

var noise = new WhiteNoise();

var filter = context.createBiquadFilter();
filter.type = 0;
filter.Q.value = 1;
filter.frequency.value = 800;

//初始化多个声音。
for (var i = 0; i < VOICE_COUNT; i++) {
var voice = new Envelope();
noise.connect(voice.node);
voice.connect(filter);
this.voices.push(voice)。
}

var gainMaster = context.createGainNode();
gainMaster.gain.value = 5;
filter.connect(gainMaster)。

gainMaster.connect(context.destination)。
这个例子借用了BBC的枪声效果页面,并做了小的修改,包括移植到JavaScript。

正如你所看到的,这种方法非常强大,但很快就会变得很复杂,超出了本书的范围。关于程序性声音生成的更多信息,请看Andy Farnell的实用合成声音设计教程和书。

房间效果
在声音从源头到我们的耳朵之前,它在墙壁、建筑物、家具、地毯和其他物体上反弹。每一次这样的碰撞都会改变声音的属性。例如,在外面拍手和在大教堂里拍手听起来很不一样,后者会引起几秒钟的可闻混响。具有高生产价值的游戏旨在模仿这些效果。为每个声学环境创建一套单独的样本往往是非常昂贵的,因为这需要音频设计师付出大量的努力,需要大量的资产,从而需要大量的游戏数据。

网络音频API带有一个模拟这些不同声学环境的设施,叫做ConvolverNode。你可以从卷积引擎中得到的效果例子包括合唱效果、混响和类似电话的语音。

产生房间效果的想法是在房间里回放一个参考声音,将其录制下来,然后(比喻)取原始声音和录制声音之间的差异。这样做的结果是一个脉冲响应,捕捉到房间对声音的影响。这些脉冲响应是在非常特殊的演播室环境下精心录制的,自己做这个需要认真的投入。幸运的是,有一些网站托管了许多这些预先录制的脉冲响应文件(以音频文件形式存储),以方便你使用。

Web Audio API提供了一个简单的方法,使用ConvolverNode将这些脉冲响应应用于你的声音。这个节点需要一个脉冲响应缓冲区,它是一个普通的AudioBuffer,并将脉冲响应文件加载到它里面。卷积器实际上是一个非常复杂的滤波器(像BiquadFilterNode),但不是从一组效果类型中选择,而是可以用一个任意的滤波器响应来配置。

var impulseResponseBuffer = null;
函数 loadImpulseResponse() {
loadBuffer('impulse.wav', function(buffer) {
impulseResponseBuffer = buffer。
});
}

function play() {
// 为样本制作一个源节点。
var source = context.createBufferSource();
source.buffer = this.buffer;
// 为脉冲响应制作一个卷积器节点。
var convolver = context.createConvolver();
// 设置脉冲响应的缓冲区。
convolver.buffer = impulseResponseBuffer。
//连接图形。
source.connect(convolver)。
convolver.connect(context.destination)。
}
convolver节点通过计算卷积来 "粉碎 "输入的声音和它的脉冲响应,这是一个数学上的密集函数。其结果是,听起来好像是在记录脉冲响应的房间里产生的一样。在实践中,将原始声音(称为干混合)与卷积的声音(称为湿混合)进行混合,并使用等功率的交叉渐变来控制你想应用多少效果,这通常是有意义的。

也可以通过合成来产生这些脉冲响应,但这个话题不在本书的范围内。

空间化的声音
游戏通常设置在一个物体有空间位置的世界里,可以是二维的,也可以是三维的。如果是这种情况,空间化的音频可以大大增加体验的沉浸感。幸运的是,网络音频API带有内置的位置音频功能(目前为立体声),使用起来非常简单。

当你尝试使用空间化的声音时,请确保你是通过立体声扬声器(最好是耳机)来聆听。这将使你更好地了解左、右声道是如何被你的空间化方法转化的。

网络音频API模型有三个方面的复杂性在增加,其中有许多概念是从OpenAL借用的。

声源和听众的位置和方向

与源音频锥体相关的参数

音源和听众的相对速度

有一个听众(AudioListener)连接到Web Audio API上下文,可以通过位置和方向在空间上进行配置。每个音源可以通过一个panner节点(AudioPannerNode),该节点将输入的音频空间化。根据音源和听众的相对位置,Web Audio API计算出正确的增益修改。

关于API的假设,有几件事需要了解。首先,听众默认是在原点(0,0,0)。定位的API坐标是无单位的,所以在实践中,需要一些乘数的调整来使声音达到你想要的程度。其次,方向被指定为方向向量(长度为1)。最后,在这个坐标空间中,正Y指向上方,这与大多数计算机图形系统相反。

考虑到这些东西,下面是一个例子,说明如何通过一个panner节点(PannerNode)来改变一个被空间化的2D的源节点的位置。

// 将监听器定位在原点(默认的,只是为了明确起见而添加的)
context.listener.setPosition(0, 0, 0)。

// 将panner节点定位。
// 假设X和Y是屏幕坐标,监听器在屏幕中心。
var panner = context.createPanner();
var centerX = WIDTH/2;
var centerY = HEIGHT/2;
var x = (X - centerX) / WIDTH;
// Y坐标被翻转以匹配画布坐标空间。
var y = (Y - centerY) / HEIGHT;
// 将Z坐标稍稍放在听众后面。
var z = -0.5;
// 根据需要调整乘数。
var scaleFactor = 2;
panner.setPosition(x * scaleFactor, y * scaleFactor, z)。

// 将角度转换成单位矢量。
panner.setOrientation(Math.cos(angle), -Math.sin(angle), 1)。

// 将你要空间化的节点连接到panner上。
source.connect(panner)。
除了考虑到相对位置和方向,每个源都有一个可配置的音频锥,如图6-3所示。

image
Figure 6-3. A diagram of panners and the listener in 2D space

一旦你指定了内锥体和外锥体,你最终就会把空间分隔成三个部分,如图6-3所示。

内锥体

外锥体

两个锥体都不是

这些子空间中的每一个都可以有一个与之相关的增益倍数,作为位置模型的额外提示。例如,为了模拟有目标的声音,我们可能有以下配置。

panner.coneInnerAngle = 5。
panner.coneOuterAngle = 10;
panner.coneGain = 0.5。
panner.coneOuterGain = 0.2。
一个分散的声音可以有一套非常不同的参数。一个全向的声源有一个360度的内锥体,它的方向对空间化来说没有区别。

panner.coneInnerAngle = 180。
panner.coneGain = 0.5。
除了位置、方向和声锥之外,声源和听众还可以指定速度。这个值对于模拟多普勒效应导致的音高变化很重要。

用JavaScript进行音频处理
一般来说,网络音频API旨在提供足够的基元(主要通过音频节点)来完成大多数常见的音频任务。我们的想法是,这些模块是用C++编写的,比用JavaScript编写的相同代码快得多。

然而,该API还提供了一个ScriptProcessorNode,让网络开发者直接用JavaScript合成和处理音频。例如,你可以用这种方法制作自定义DSP效果的原型,或者为教育应用说明概念。

要开始使用,请创建一个ScriptProcessorNode。这个节点以节点参数(bufferSize)指定的块来处理声音,这个参数必须是2的幂。倾向于使用较大的缓冲区,因为如果主线程忙于其他事情,如页面重新布局、垃圾回收或JavaScript回调,它可以给你更多的安全系数来防止故障。

// 创建一个ScriptProcessorNode。
var processor = context.createScriptProcessor(2048);
//指定对每个缓冲区调用的onProcess函数。
processor.onaudioprocess = onProcess。
// 假设source存在,把它连接到一个脚本处理器。
source.connect(processor);
一旦你有音频数据管道进入一个JavaScript函数,你可以通过检查输入缓冲区来分析数据流,或者通过修改输出缓冲区直接改变输出。例如,我们可以通过实现下面的脚本处理器轻松地交换左右声道。

function onProcess(e) {
var leftIn = e.inputBuffer.getChannelData(0);
var rightIn = e.inputBuffer.getChannelData(1);
var leftOut = e.outputBuffer.getChannelData(0);
var rightOut = e.outputBuffer.getChannelData(1);

for (var i = 0; i < leftIn.length; i++) {
// 翻转左、右通道。
leftOut[i] = rightIn[i];
rightOut[i] = leftIn[i];
}
}
请注意,在生产中你不应该做这种通道交换,因为使用ChannelSplitterNode,然后再使用ChannelMergerNode要有效得多。作为另一个例子,我们可以在混合中添加一个随机噪声。我们通过简单地给信号添加一个随机偏移量来做到这一点。通过使信号完全随机,我们可以产生白噪声,这在许多应用中实际上是相当有用的[见程序生成的声音]。

函数onProcess(e) {
var leftOut = e.outputBuffer.getChannelData(0);
var rightOut = e.outputBuffer.getChannelData(1);

for (var i = 0; i < leftOut.length; i++) {
// 添加一些噪音
leftOut[i] += (Math.random() - 0.5) * NOISE_FACTOR;
rightOut[i] += (Math.random() - 0.5) * NOISE_FACTOR;
}
}
使用脚本处理节点的主要问题是性能。使用JavaScript来实现这些数学密集型的算法要比直接在浏览器的原生代码中实现这些算法慢得多。