Web Audio API 的 IIRFilterNode
接口是实现通用无限脉冲响应(IIR
)滤波器的音频节点处理器,这种类型的滤波器可用于实现音调控制设备和图形均衡器,并且可以指定滤波器的响应参数,以便根据需要进行调节。本文将探讨如何实现一个滤波器,并在一个简单示例中加以运用。
示例
本指南中的简单示例提供了一个播放 / 暂停按钮,用于启动和暂停音频播放,还提供了一个切换开关,用于打开和关闭无限脉冲响应(IIR)滤波器,以此改变声音的音调。此外,示例中还提供了一个画布,在上面会绘制音频的频率响应,这样就能直观的看到无限脉冲响应滤波器产生的效果。
你可以 在 CodePen 上查看完整的演示示例。也可以 查看 GitHub 上的源代码。它包含了针对不同低通频率的一些不同系数值 —— 你可以将 filterNumber
常量更改为 0
到 3
之间的某个值,以查看不同的可用效果。
浏览器支持
尽管 IIR
滤波器实现的时间比一些长期存在的 Web Audio API 功能(如双二阶滤波器)要晚些,但在现代浏览器中也已经得到了很好的支持。
无限脉冲响应滤波器节点(IIRFilterNode
)
Web Audio API 现在带有 IIRFilterNode
接口。它是什么,又与已有的双二阶滤波器节点(BiquadFilterNode)有何不同呢?
无限脉冲响应(IIR
)滤波器是音频和数字信号处理中使用的两种主要滤波器类型之一,另一种类型是有限脉冲响应(FIR
)滤波器。
双二阶(biquad
)滤波器实际上是一种特定类型的无限脉冲响应滤波器。它是一种常用类型,并且在 Web Audio API 中已经作为一个节点来使用了。如果你选用了这个节点,那么一些复杂的工作就已经完成了。例如,如果你想从声音中滤除较低的频率,可以将其 type
设置为 highpass
(高通),然后设定要过滤(或截止)的频率。
当使用 IIRFilterNode
而非 BiquadFilterNode
时,是在自行创建滤波器,而不只是选择一种预先设定好的类型。因此,你可以创建高通滤波器、低通滤波器,或者更具定制性的滤波器。而这正是无限脉冲响应滤波器节点的有用之处 —— 如果现有的设置都不符合你的需求,你可以自行创建。除此之外,如果你的音频图谱中需要一个高通滤波器和一个带通滤波器,你可以只用一个无限脉冲响应滤波器节点来替代原本需要的两个双二阶滤波器节点。
使用无限脉冲响应滤波器节点时,需要设置该滤波器所需的 feedforward
(前馈值)和 feedback
(反馈值) —— 这些值决定了滤波器的特性。缺点是,这涉及到一些复杂的数学计算。
设置无限脉冲响应滤波器系数
当创建无限脉冲响应(IIR
)滤波器时,我们将 feedforward
和 feedback
系数作为选项传递(系数是我们描述值的方式)。这两个参数都是数组,并且每个数组包含的元素都不能超过 20
个。
在设置系数时,feedforward
不能全部设为零,否则就不会向滤波器发送任何内容,而如下设置是可以的:
const feedForward = [0.00020298, 0.0004059599, 0.00020298];
feedback
不能从零开始,否则在第一次传递时不会返回任何内容:
const feedBackward = [1.0126964558, -1.9991880801, 0.9873035442];
注:
这些数值是基于 Web Audio API 规范的滤波器特性 里所指定的低通滤波器来计算的。随着这个滤波器节点越来越受欢迎,我们应该能够整理出更多的系数值。
在音频图中使用IIRFilter
首先,创建上下文和过滤器节点:
const audioCtx = new AudioContext();
const iirFilter = audioCtx.createIIRFilter(feedForward, feedBack);
接下来,我们需要一个用于播放的音频源。我们使用一个自定义函数 playSoundNode()
来进行设置,该函数会根据一个已有的 AudioBuffer
创建缓冲源,将其连接到默认的音频播放目标上,启动播放,并返回该缓冲源:
function playSourceNode(audioContext, audioBuffer) {
const soundSource = audioContext.createBufferSource();
soundSource.buffer = audioBuffer;
soundSource.connect(audioContext.destination);
soundSource.start();
return soundSource;
}
按下播放按钮时调用此函数,播放按钮的HTML
看起来像这样:
<button
class="button-play"
role="switch"
data-playing="false"
aria-pressed="false">
Play
</button>
click
事件监听器的启动方式如下:
playButton.addEventListener(
"click",
() => {
if (playButton.dataset.playing === "false") {
srcNode = playSourceNode(audioCtx, sample);
// …
}
},
false,
);
用于打开和关闭 IIR
滤波器的开关是以类似的方式设置的。首先,来看 HTML
部分:
<button
class="button-filter"
role="switch"
data-filteron="false"
aria-pressed="false"
aria-describedby="label"
disabled></button>
然后,滤波器按钮的 click
处理程序会将 IIRFilter
连接到音频图谱上,位于音频源和音频播放目标之间:
filterButton.addEventListener(
"click",
() => {
if (filterButton.dataset.filteron === "false") {
srcNode.disconnect(audioCtx.destination);
srcNode.connect(iirFilter).connect(audioCtx.destination);
// …
}
},
false,
);
频率响应
在 IIRFilterNode
实例上,我们只有一个可用的方法,即 getFrequencyResponse ()
方法,它能让我们看到传入该滤波器的音频的频率发生了怎样的变化。
让我们利用从这个方法获取回来的数据,绘制出所建滤波器的频率图。
我们需要创建三个数组。其中一个数组用于存放希望获取其幅度响应和相位响应的频率值,另外两个是空数组,用于接收数据。这三个数组都必须是 Float32Array
类型,并且大小相同。
// arrays for our frequency response
const totalArrayItems = 30;
let myFrequencyArray = new Float32Array(totalArrayItems);
const magResponseOutput = new Float32Array(totalArrayItems);
const phaseResponseOutput = new Float32Array(totalArrayItems);
接下来,用希望返回数据的频率值填充第一个数组:
myFrequencyArray = myFrequencyArray.map((item, index) => 1.4 ** index);
可以采用线性方法,但在处理频率时,采用对数方法要好得多,所以我们用一些频率值来填充数组,使数组元素越往后其对应的频率值越大。
接下来,我们获取响应数据:
iirFilter.getFrequencyResponse(
myFrequencyArray,
magResponseOutput,
phaseResponseOutput,
);
我们可以利用这些数据在二维画布环境中绘制一个滤波器频率图。
// Create a canvas element and append it to our DOM
const canvasContainer = document.querySelector(".filter-graph");
const canvasEl = document.createElement("canvas");
canvasContainer.appendChild(canvasEl);
// Set 2d context and set dimensions
const canvasCtx = canvasEl.getContext("2d");
const width = canvasContainer.offsetWidth;
const height = canvasContainer.offsetHeight;
canvasEl.width = width;
canvasEl.height = height;
// Set background fill
canvasCtx.fillStyle = "white";
canvasCtx.fillRect(0, 0, width, height);
// Set up some spacing based on size
const spacing = width / 16;
const fontSize = Math.floor(spacing / 1.5);
// Draw our axis
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = "grey";
canvasCtx.beginPath();
canvasCtx.moveTo(spacing, spacing);
canvasCtx.lineTo(spacing, height - spacing);
canvasCtx.lineTo(width - spacing, height - spacing);
canvasCtx.stroke();
// Axis is gain by frequency -> make labels
canvasCtx.font = `${fontSize}px sans-serif`;
canvasCtx.fillStyle = "grey";
canvasCtx.fillText("1", spacing - fontSize, spacing + fontSize);
canvasCtx.fillText("g", spacing - fontSize, (height - spacing + fontSize) / 2);
canvasCtx.fillText("0", spacing - fontSize, height - spacing + fontSize);
canvasCtx.fillText("Hz", width / 2, height - spacing + fontSize);
canvasCtx.fillText("20k", width - spacing, height - spacing + fontSize);
// Loop over our magnitude response data and plot our filter
canvasCtx.beginPath();
magResponseOutput.forEach((magResponseData, i) => {
if (i === 0) {
canvasCtx.moveTo(spacing, height - magResponseData * 100 - spacing);
} else {
canvasCtx.lineTo(
(width / totalArrayItems) * i,
height - magResponseData * 100 - spacing,
);
}
});
canvasCtx.stroke();
总结
IIRFilter
示例就演示到这里了。这应该已经向你解释清楚了它的基本功能、用途和工作原理了。
评论0
暂时没有评论