Web Audio API:音频参数连接、调制及分析
1. 音频节点连接基础
在简单的 Web Audio API 应用中,常见的连接方式是将一个音频节点的输出直接连接到另一个音频节点的输入。不过,实际上还有多种连接方式和更多的连接功能。
连接方法主要有以下几种:
-
connect(AudioNode destinationNode, optional output, optional input)
-
connect(AudioParam destinationParam, optional output)
-
disconnect(optional output)
-
disconnect(AudioNode destinationNode, optional output, optional input)
-
disconnect(AudioParam destinationParam, optional output)
下面通过示例进行说明,假设有两个节点
nodeA
和
nodeB
,
nodeA
有音频参数
paramA
,
nodeB
有音频参数
paramB
:
- 基本连接方式:
nodeA.connect(nodeB)
- 带可选输出和输入索引的连接:
nodeA.connect(nodeB, output, input)
,其中
output
表示
nodeA
的输出流索引,
input
表示
nodeB
的输入流索引。
- 节点连接到参数:
nodeA.connect(nodeB.paramB)
- 指定输出连接到参数:
nodeA.connect(nodeB.paramB, output)
连接规则如下:
|规则描述|详情|
| ---- | ---- |
|输入节点输出求和|若音频节点有多个节点连接到其输入,这些节点的输出会相加形成输入缓冲区,再进行上下采样以匹配该音频节点的输入通道数。|
|连接到音频参数|参数会将连接的音频流进行下采样转换为单声道(如有需要),并与固有参数值相加。固有参数值是指没有音频连接时该参数的默认值。|
|连接上下文一致性|任何连接的目标(音频节点或音频参数)必须与发起连接的节点属于同一音频上下文,即
AudioNodes
不能在不同的
AudioContexts
之间共享。|
|多节点连接|多个音频节点可以连接到同一个音频节点或音频参数,一个音频节点也可以连接到多个音频节点和音频参数。|
|自连接|音频节点可以连接到自身、自身的参数,或连接到最终会返回到原节点或其参数的其他节点。|
|唯一连接|特定节点的给定输出与另一个特定节点的给定输入之间只能有一个连接,重复连接会被忽略。|
2. 断开节点连接
断开节点连接的选项比连接节点更多,例如可以在不指定连接目标的情况下断开节点的所有连接:
-
nodeA.disconnect()
:断开
nodeA
的所有连接。
-
nodeA.disconnect(output)
:断开
nodeA
指定输出的所有连接,
output
是输出索引。
-
disconnect(destinationNode, optional output, optional input)
:断开到特定目标节点的所有输出,或断开到该目标的特定输出,或断开到目标特定输入的特定输出。
-
disconnect(destinationParam, optional output)
:断开到特定目标音频参数的所有输出,或断开到该目标的特定输出。
断开与音频参数的连接时,固有值不受影响。例如,有一个
ConstantSourceNode
的偏移量为 0.5,一个
GainNode
的增益值为 1,当
ConstantSourceNode
连接到
GainNode
的增益参数时,该增益参数的计算值变为 1 + 0.5 = 1.5;断开连接后,增益参数的计算值会恢复到固有值 1。
3. 连接音频参数
通过将其他节点连接到音频参数,可以改变该参数的值,如
someNode.connect(offset.value)
,此时连接节点的音频输出会与固有参数值相加。但问题是,如何让其他节点在给定时间输出所需的值。
需要注意的是,自动化曲线只重置参数的固有值。例如:
someConstantSourceNode.connect(someGainNode.gain);
someGainNode.gain.setValueAtTime(0, context.currentTime + 1);
一秒后,固有值会跳转到 0,但计算值是固有值与连接到音频参数的所有节点的总和。若使用默认值,
GainNode
的增益为 1,
ConstantSourceNode
的偏移量为 1,则计算得到的增益参数在一秒前为 1 + 1 = 2,一秒后为 0 + 1 = 1。
4. 频率调制(FM)合成示例
调制是指用调制信号(调制器信号)改变周期性波形(载波信号)的属性。频率调制(FM)合成是一种特殊的调制方式,它通过用另一个波形调制一个波形的频率来产生声音,由 John Chowning 在 1973 年首次发现。
以简单正弦波为例,改变振幅是振幅调制,改变频率是频率调制,改变相位是相位调制。这里主要进行的是相位调制,它比频率调制更常见,且两者效果相似。
载波定义为:$c(t) = A\sin(2\pi f_c t + \phi)$,其中 $f_c$ 是载波频率。用调制器 $m(t)$ 调制频率后,输出信号为:$s(t) = A\sin(2\pi (f_c + Dm(t)) t + \phi)$,其中 $f_m$ 是调制频率,$D$ 是调制深度。
以下是 Web Audio API 实现 FM 合成的代码示例:
<input type='button' value='Start/Stop' id='StartStop'>
<p>Carrier Frequency</p>
<input type='range' id='CarrierFrequency' min=50 max=2000 value=800 step='any'>
<span id='CarrierFrequencyLabel'></span>
<p>Modulation Depth</p>
<input type='range' id='ModulationDepth' min=0 max=1000 value=200 step='any'>
<span id='ModulationDepthLabel'></span>
<p>Modulation Frequency</p>
<input type='range' id='ModulationFrequency' min=10 max=200 value=100 step='any'>
<span id='ModulationFrequencyLabel'></span>
<script>
//User Interface callbacks
CarrierFrequency.oninput = function() {
CarrierFrequencyLabel.innerHTML = this.value
Carrier.frequency.value=this.value
}
ModulationDepth.oninput = function() {
ModulationDepthLabel.innerHTML = this.value
Modulator.gain.value=this.value
}
ModulationFrequency.oninput = function() {
ModulationFrequencyLabel.innerHTML = this.value
ModulatorOsc.frequency.value=this.value
}
StartStop.onclick = function() {
if (context.state === 'suspended')
context.resume()
else
context.suspend()
}
</script>
<script src='FMSynthesis.js'></script>
//Define context and nodes
context = new AudioContext()
var Carrier = new OscillatorNode(context, {frequency:CarrierFrequency.value})
var ModulatorOsc = new OscillatorNode(context, {frequency:ModulationFrequency.value})
var Modulator = new GainNode(context, {gain:ModulationDepth.value})
//start any scheduled sources
Carrier.start()
ModulatorOsc.start()
//This multiples ModulatorOsc and modulation depth
ModulatorOsc.connect(Modulator)
//Carrier frequency is sum of intrinsic frequency offset & modulator
Modulator.connect(Carrier.frequency)
Carrier.connect(context.destination)
在这个示例中,调制器连接到载波的频率参数,计算得到的频率是固有频率值与调制器当前值的总和,从而实现对载波频率的调制。
5. 振幅调制(AM)合成示例
振幅调制(AM)和频率调制(FM)一样,既用于广播协议,也用于音频合成。FM 技术通过调制频率改变载波信号的频率,而 AM 则通过调制频率改变载波的振幅。FM 保真度更高,因为它更不易受干扰,但 AM 仍用于新闻和体育等基于语音的广播。
载波信号为正弦波,用调制器调制振幅后,输出信号为:$s(t) = A(1 + Dm(t))\sin(2\pi f_c t + \phi)$,其中 $f_m$ 是调制频率,$D$ 是调制深度。
以下是 Web Audio API 实现 AM 合成的代码示例:
<input type='button' value='Start/Stop' id='StartStop'>
<p>Carrier Frequency</p>
<input type='range' id='CarrierFrequency' min=50 max=2000 value=800 step='any'>
<span id='CarrierFrequencyLabel'></span>
<p>Modulation Depth</p>
<input type='range' id='ModulationDepth' min=0 max=1 value=0.1 step='any'>
<span id='ModulationDepthLabel'></span>
<p>Modulation Frequency</p>
<input type='range' id='ModulatorFrequency' min=10 max=200 value=100 step='any'>
<span id='ModulatorFrequencyLabel'></span>
<script>
CarrierFrequency.oninput = function() {
CarrierFrequencyLabel.innerHTML = this.value
Carrier.frequency.value=this.value
}
ModulationDepth.oninput = function() {
ModulationDepth.innerHTML = this.value
Modulator.gain.value=this.value
}
ModulatorFrequency.oninput = function() {
ModulatorFrequencyLabel.innerHTML = this.value
ModulatorOsc.frequency.value=this.value
}
StartStop.onclick = function() {
if (context.state === 'suspended')
context.resume()
else
context.suspend()
}
</script>
<script src='AMSynthesis.js'></script>
context = new AudioContext()
var Carrier = new OscillatorNode(context)
var ModulatorOsc = new OscillatorNode(context, {frequency:100,type:'square'})
var Modulator = context.createGain()
var AM = context.createGain()
Carrier.start()
ModulatorOsc.start()
ModulatorOsc.connect(Modulator)
Modulator.connect(AM.gain)
Carrier.connect(AM)
AM.connect(context.destination)
6. 颤音(Tremolo)和震音(Vibrato)效果
当调制频率约为 6 Hz 时,振幅调制信号类似于颤音效果。这通常使用低频振荡器(LFO)实现,LFO 是频率设置为非常低的值(通常在 0.5 到 10 Hz 之间)的振荡器。LFO 在音频效果和合成器中非常常见,是颤音、合唱和相位等效果的核心构建块。
随着频率接近音频速率(20 Hz),声音会变得粗糙。超过 20 Hz 时,我们无法听到振幅的周期性下降,而是开始听到边带频率或组合音,如和音(载波和调制器频率之和 $f_c + f_m$)和差音(载波和调制器频率之差 $f_c - f_m$)。
颤音与 AM 合成相关,震音与 FM 合成相关。不过,震音的实现方式与 FM 合成不同,通常通过改变延迟线的延迟来实现,即将 LFO 连接到
DelayNode
的延迟参数。
7. 环形调制(Ring Modulation)
环形调制是一种简单的振幅调制方式,调制器和载波直接相乘。与其他 AM 方法不同,环形调制允许生成的波形振幅降至零,而其他 AM 方法包含直流偏移,起到深度或强度控制的作用。也就是说,AM 使用单极调制波(不会低于零),而环形调制使用双极调制波。
可以基于 AM 合成的代码示例实现环形调制,只需在载波与调制器相乘时将固有增益设置为 0,即在
AM.gain.value = 0
后添加
var AM = context.createGain()
。
8. 音频分析与可视化 - AnalyserNode
Web Audio API 中的
AnalyserNode
可以用于音频分析和可视化。它既可以作为目标节点,也可以作为中间节点,不会修改传入的音频,而是提供用于分析、可视化或计量目的的音频流数据,数据可以是时域或频域的。
使用
AnalyserNode
进行音频分析的步骤如下:
1. 创建
AnalyserNode
实例。
2. 将其放置在音频图的适当位置。
3. 获取时域或频域数据进行分析或可视化。
AnalyserNode
有四个可选参数来定制输出:
-
fftSize
:定义用于频域分析的快速傅里叶变换(FFT)的缓冲区大小,以及时域分析的缓冲区大小。必须是 32($2^5$)到 32768($2^{15}$)之间的 2 的幂,默认值为 2048($2^{11}$)。较高的值会带来更精细的信号分析,但会牺牲一些性能。
-
minDecibels
(默认 -100):定义频率区间的最小功率(dB)。
-
maxDecibels
(默认 -30):定义频率区间的最大功率(dB)。
以下是简单的可视化示例代码框架:
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const analyser = audioContext.createAnalyser();
// 设置参数
analyser.fftSize = 2048;
analyser.minDecibels = -100;
analyser.maxDecibels = -30;
// 获取音频源并连接到 analyser
const source = audioContext.createMediaElementSource(audioElement);
source.connect(analyser);
analyser.connect(audioContext.destination);
// 获取频域数据
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
function draw() {
requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
// 在这里进行可视化操作,例如绘制柱状图
// ...
}
draw();
通过以上内容,我们了解了 Web Audio API 中音频节点的连接、调制以及分析可视化的相关知识和实现方法,利用这些技术可以创建出丰富多样的音频效果和应用。
Web Audio API:音频参数连接、调制及分析
9. 音频节点连接与断开规则总结
为了更清晰地理解音频节点的连接与断开操作,我们对前面的内容进行总结,以下是相关规则的表格呈现:
|操作类型|方法|说明|
| ---- | ---- | ---- |
|连接节点|
connect(AudioNode destinationNode, optional output, optional input)
|将一个音频节点的输出连接到另一个音频节点的输入,可指定输出和输入索引|
|连接到参数|
connect(AudioParam destinationParam, optional output)
|将一个音频节点的输出连接到另一个节点的音频参数,可指定输出索引|
|断开所有连接|
disconnect()
|断开节点的所有连接|
|断开指定输出连接|
disconnect(output)
|断开节点指定输出的所有连接|
|断开到目标节点连接|
disconnect(destinationNode, optional output, optional input)
|断开到特定目标节点的所有输出,或特定输出,或特定输入的特定输出|
|断开到目标参数连接|
disconnect(destinationParam, optional output)
|断开到特定目标音频参数的所有输出,或特定输出|
以下是音频节点连接与断开操作的 mermaid 流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B{选择操作类型}:::decision
B -->|连接节点| C(connect(AudioNode, optional output, optional input)):::process
B -->|连接到参数| D(connect(AudioParam, optional output)):::process
B -->|断开所有连接| E(disconnect()):::process
B -->|断开指定输出连接| F(disconnect(output)):::process
B -->|断开到目标节点连接| G(disconnect(destinationNode, optional output, optional input)):::process
B -->|断开到目标参数连接| H(disconnect(destinationParam, optional output)):::process
C --> I([结束]):::startend
D --> I
E --> I
F --> I
G --> I
H --> I
10. 调制效果对比分析
我们对前面介绍的几种调制效果(FM 合成、AM 合成、颤音、震音、环形调制)进行对比分析,以便更好地理解它们的特点和应用场景,具体如下表所示:
|调制效果|原理|特点|应用场景|
| ---- | ---- | ---- | ---- |
|FM 合成|用调制器波形调制载波波形的频率|可产生丰富多样的声音,常用于创造独特的音效|电子音乐制作、游戏音效等|
|AM 合成|用调制器波形调制载波波形的振幅|实现相对简单,在广播和音频合成中都有应用|新闻、体育广播,简单音频效果制作|
|颤音|调制频率约为 6 Hz 的振幅调制|声音有周期性的振幅变化,类似颤音效果|音乐表演中的情感表达,营造特定氛围|
|震音|与 FM 合成相关,通过改变延迟线延迟实现|声音有频率的周期性变化|音乐表演中的表现力增强|
|环形调制|调制器和载波直接相乘|允许波形振幅降至零,产生独特的声音效果|实验性音乐创作、特殊音效制作|
11. 音频分析与可视化的深入探讨
AnalyserNode
为音频分析和可视化提供了强大的功能,我们可以进一步探讨其在实际应用中的一些要点。
11.1 时域和频域数据的应用
- 时域数据 :反映了音频信号随时间的变化情况,可用于检测音频的起始、结束时间,以及音频信号的瞬时幅度变化。例如,在音频剪辑软件中,可以通过时域数据来精确地裁剪音频片段。
- 频域数据 :展示了音频信号在不同频率上的分布情况,可用于分析音频的音高、音色等特征。在音乐制作中,可以根据频域数据来调整音频的均衡器,突出或抑制某些频率范围的声音。
11.2 性能优化
在使用
AnalyserNode
进行音频分析和可视化时,性能优化是一个重要的考虑因素。由于
fftSize
的值越大,分析越精细,但性能开销也越大,因此需要根据实际需求选择合适的
fftSize
。例如,在实时音频处理场景中,如果对分析精度要求不是特别高,可以选择较小的
fftSize
以提高性能。
12. 综合应用示例 - 自定义音频效果器
结合前面介绍的音频节点连接、调制和分析可视化知识,我们可以创建一个自定义的音频效果器。以下是一个简单的实现思路和代码示例:
12.1 实现思路
- 创建音频上下文和相关节点,包括振荡器、增益节点、分析节点等。
- 通过用户界面控制音频节点的参数,如频率、振幅、调制深度等。
-
使用
AnalyserNode对音频信号进行分析,并将分析结果可视化。
12.2 代码示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Audio Effect</title>
</head>
<body>
<input type='button' value='Start/Stop' id='StartStop'>
<p>Carrier Frequency</p>
<input type='range' id='CarrierFrequency' min=50 max=2000 value=800 step='any'>
<span id='CarrierFrequencyLabel'></span>
<p>Modulation Depth</p>
<input type='range' id='ModulationDepth' min=0 max=1000 value=200 step='any'>
<span id='ModulationDepthLabel'></span>
<p>Modulation Frequency</p>
<input type='range' id='ModulationFrequency' min=10 max=200 value=100 step='any'>
<span id='ModulationFrequencyLabel'></span>
<canvas id="visualizer"></canvas>
<script>
const context = new AudioContext();
const carrier = new OscillatorNode(context, { frequency: document.getElementById('CarrierFrequency').value });
const modulatorOsc = new OscillatorNode(context, { frequency: document.getElementById('ModulationFrequency').value });
const modulator = context.createGain();
const analyser = context.createAnalyser();
carrier.start();
modulatorOsc.start();
modulatorOsc.connect(modulator);
modulator.connect(carrier.frequency);
carrier.connect(analyser);
analyser.connect(context.destination);
const canvas = document.getElementById('visualizer');
const canvasContext = canvas.getContext('2d');
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
function draw() {
requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let barHeight;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i];
canvasContext.fillStyle = 'rgb(' + (barHeight + 100) + ',50,50)';
canvasContext.fillRect(x, canvas.height - barHeight / 2, barWidth, barHeight / 2);
x += barWidth + 1;
}
}
draw();
document.getElementById('CarrierFrequency').oninput = function () {
document.getElementById('CarrierFrequencyLabel').innerHTML = this.value;
carrier.frequency.value = this.value;
}
document.getElementById('ModulationDepth').oninput = function () {
document.getElementById('ModulationDepthLabel').innerHTML = this.value;
modulator.gain.value = this.value;
}
document.getElementById('ModulationFrequency').oninput = function () {
document.getElementById('ModulationFrequencyLabel').innerHTML = this.value;
modulatorOsc.frequency.value = this.value;
}
document.getElementById('StartStop').onclick = function () {
if (context.state === 'suspended')
context.resume();
else
context.suspend();
}
</script>
</body>
</html>
通过以上代码,我们创建了一个简单的音频效果器,用户可以通过滑块控制载波频率、调制深度和调制频率,同时在画布上实时可视化音频的频域数据。
综上所述,Web Audio API 提供了丰富的功能和灵活的操作方式,通过合理运用音频节点的连接、调制以及分析可视化技术,我们可以开发出各种富有创意和实用价值的音频应用。无论是音乐制作、游戏开发还是音频处理工具,都可以借助 Web Audio API 实现更加出色的音频效果。


被折叠的 条评论
为什么被折叠?



