Web Audio API 高级应用与实现
1. 音频处理实例
1.1 比特压碎器(Bit Crusher)
比特压碎器用于改变信号编码的比特数。传统实现方式在改变比特深度时,需要为波形整形器创建新曲线,这会消耗大量存储和计算资源。使用音频工作线程(Audio Worklet)可以避免这些问题,让处理过程依赖于比特深度音频参数。
以下是使用音频工作线程实现比特压碎器的代码示例:
<button onclick=context.resume()>Start</button>
<input type=range min=1 max=16 value=12 id='Crush'>
<script>
const context = new AudioContext()
let source = new AudioBufferSourceNode(context, {loop:true})
fetch('trumpet.wav')
.then(response => response.arrayBuffer())
.then(buffer => context.decodeAudioData(buffer))
.then(data => source.buffer = data)
source.start()
context.audioWorklet.addModule('bitCrushWorklet.js').then(() => {
let bitcrusher = new AudioWorkletNode(context, 'bitcrusher')
source.connect(bitcrusher).connect(context.destination)
Crush.oninput = function() {
bitcrusher.parameters.get('bitDepth').value = this.value
}
})
</script>
registerProcessor('bitcrusher', class extends AudioWorkletProcessor {
static get parameterDescriptors () {
return [{name:'bitDepth', defaultValue:12, minValue:1, maxValue:16}]
}
process (inputs, outputs, parameters) {
const input = inputs[0], output = outputs[0]
const bitDepth = parameters.bitDepth
if (bitDepth.length > 1) {
for (let channel = 0; channel < output.length; ++channel) {
for (let i = 0; i < output[channel].length; ++i) {
let step = 2 / Math.pow(2, bitDepth[i])
output[channel][i] = step * (Math.floor(input[channel][i] / step) + 0.5)
}
}
} else {
const step = 2 / Math.pow(2, bitDepth[0])
for (let channel = 0; channel < output.length; ++channel) {
for (let i = 0; i < output[channel].length; ++i) {
output[channel][i] = step * (Math.floor(input[channel][i] / step) + 0.5)
}
}
}
return true
}
})
1.2 动态范围压缩器(Dynamic Range Compressor)
Web Audio API 中的 DynamicsCompressorNode 规范不明确,且存在至少一帧的固有延迟。可以使用音频工作线程实现一个更简单的动态范围压缩器,具有快速响应和易于修改的特点。
以下是使用音频工作线程实现动态范围压缩器的代码示例:
<button onclick='context.resume()'>Start</button><br>
<input id='Thresh' type='range' min=-90 max=0 value=-30>Threshold<br>
<input id='Ratio' type='range' min=1 max=20 value=12>Ratio<br>
<input id='Knee' type='range' min=0 max=40 value=0>Knee<br>
<input id='Attack' type='range' min=0 max=1000 value=0>Attack<br>
<input id='Release' type='range' min=0 max=1000 value=5>Release
<script src='compressor.js'></script>
let context = new AudioContext()
const source1 = new OscillatorNode(context)
const source2 = new OscillatorNode(context, {frequency:0.25})
const gainNode = new GainNode(context)
source1.start()
source2.start()
source1.connect(gainNode)
source2.connect(gainNode.gain)
context.audioWorklet.addModule('compressorWorklet.js').then(() => {
let compressor = new AudioWorkletNode(context, 'compressor')
gainNode.connect(compressor).connect(context.destination)
Thresh.oninput = () => {
compressor.parameters.get('threshold').value = Thresh.value
}
Ratio.oninput = () => {
compressor.parameters.get('ratio').value = Ratio.value
}
Knee.oninput = () => {
compressor.parameters.get('knee').value = Knee.value
}
Attack.oninput = () => {
compressor.parameters.get('attack').value = Attack.value
}
Release.oninput = () => {
compressor.parameters.get('release').value = Release.value
}
})
registerProcessor('compressor', class extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{name:'threshold', defaultValue:-30},
{name:'ratio', defaultValue:12},
{name:'knee', defaultValue:0},
{name:'attack', defaultValue:0},
{name:'release', defaultValue:5}
]
}
constructor() {
super()
this.yPrev = 0
this.port.onmessage = () => this.port.postMessage(this.reduction)
}
process(inputs, outputs, parameters) {
let alphaAttack = Math.exp(-1/(parameters.attack[0]*sampleRate/1000))
let alphaRelease = Math.exp(-1/(parameters.release[0]*sampleRate/1000))
let x, x_dB, y_dB, x_G, y_G, x_B, y_B, c_dB, c
let T = parameters.threshold[0]
let R = parameters.ratio[0]
let W = parameters.knee[0]
for (let j = 0; j < outputs[0][0].length; ++j) {
x_dB = inputs[0][0][j]
y_dB = Math.max(10*Math.log10(x_dB*x_dB), -120)
x_G = y_dB
if ((x_G - T) <= -W/2) y_G = x_G
else if ((x_G - T) >= W/2) y_G = T + (x_G - T)/R
else y_G = x_G + (1/R - 1)*(x_G - T + W/2)*(x_G - T + W/2)/(2*W)
x_B = x_G - y_G
if (x_B > this.yPrev) {
y_B = x_B * (1 - alphaAttack) + alphaAttack * this.yPrev
} else {
y_B = x_B * (1 - alphaRelease) + alphaRelease * this.yPrev
}
this.yPrev = y_B
c_dB = -y_B
this.reduction = c_dB
c = Math.pow(10, c_dB/20)
outputs[0][0][j] = c * inputs[0][0][j]
}
return true
}
})
1.3 立体声增强器(Stereo Enhancer)
传统的立体声增强器实现存在信号功率无法保持的问题。使用音频工作线程可以轻松解决这个问题,通过使用变量 norm 确保在立体声宽度变窄或变宽时保持信号功率。
以下是使用音频工作线程实现立体声增强器的代码示例:
<input type='range' min=-1 max=1 value=0 step='any' id='width'>
<script>
let context = new AudioContext()
context.audioWorklet.addModule('stereoWidenerWorklet.js').then(() => {
let Widener = new AudioWorkletNode(context, 'stereo-widener')
let monoSource = context.createOscillator()
let source = new StereoPannerNode(context, {pan:0.5})
source.pan.value = 0.5
monoSource.connect(source)
source.connect(Widener)
monoSource.start()
Widener.connect(context.destination)
width.oninput = () => {
context.resume()
Widener.parameters.get('width').value = width.value
}
})
</script>
registerProcessor('stereo-widener', class extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [{name:'width', defaultValue:0, minValue:-1, maxValue:1}]
}
constructor(options) {
super()
options.numberOfOutputs = 2
}
process(inputs, outputs, parameters) {
const input = inputs[0], output = outputs[0]
for (let channel = 0; channel < input.length; ++channel) {
for (let i = 0; i < input[channel].length; ++i) {
let L, R, M, S, newM, newS, newL, newR, W, norm
L = input[0][i]
if (input.length > 1) R = input[1][i]
else R = input[0][i]
M = (L + R)/Math.sqrt(2)
S = (L - R)/Math.sqrt(2)
W = (parameters.width[0] + 1)*Math.PI/4
newM = M*Math.cos(W)
newS = S*Math.sin(W)
newL = (newM + newS)/Math.sqrt(2)
newR = (newM - newS)/Math.sqrt(2)
norm = Math.sqrt((L*L + R*R) / (newL*newL + newR*newR))
output[0][i] = norm*newL
output[1][i] = norm*newR
}
}
return true
}
})
2. 循环缓冲区和延迟线
音频工作线程节点通常以 128 个样本为一个块进行输入处理,并输出 128 个样本。在许多音频应用中,处理过程也是逐帧或逐块进行的。在数字音频工作站(DAWs)中,通常有一个默认的缓冲区大小,常见的值有 32、64、128、256、512、1024 和 2048,且帧的时间持续时间取决于采样频率。
2.1 固定延迟音频工作线程(Fixed Delay AudioWorklet)
以下是一个简单的音频工作线程,用于将信号延迟 12 个样本:
registerProcessor('fixedDelay', class extends AudioWorkletProcessor {
constructor() {
super()
this.Buffer = new Array(20).fill(0)
}
process(inputs, outputs) {
let delaySamples = 12
for (let i = 0; i < outputs[0][0].length; ++i) {
outputs[0][0][i] = inputs[0][0][i]
for (let j = 1; j < delaySamples; j++) {
this.Buffer[j - 1] = this.Buffer[j]
}
this.Buffer[delaySamples - 1] = inputs[0][0][i]
}
return true
}
})
2.2 使用循环缓冲区的固定延迟音频工作线程(Fixed Delay AudioWorklet with Circular Buffer)
为了提高效率,可以使用循环缓冲区和读写指针来实现固定延迟:
registerProcessor('fixedDelay2', class extends AudioWorkletProcessor {
constructor() {
super()
this.Buffer = new Array(48000).fill(0)
this.ReadPtr = 0
this.WritePtr = 0
}
process(inputs, outputs) {
let delaySamples = 12
let bufferSize = this.Buffer.length
for (let i = 0; i < outputs[0][0].length; ++i) {
while (this.WritePtr > bufferSize) this.WritePtr -= bufferSize
this.ReadPtr = this.WritePtr - delaySamples
while (this.ReadPtr < 0) this.ReadPtr += bufferSize
this.Buffer[this.WritePtr] = inputs[0][0][i]
outputs[0][0][i] = this.Buffer[this.ReadPtr]
this.WritePtr++
this.ReadPtr++
}
return true
}
})
3. 重新审视 Karplus - Strong 算法
Karplus - Strong 算法用于模拟拨弦音效,但在 Web Audio API 中,当延迟节点出现在反馈回路中时,会自动增加一个块的延迟,导致产生的音符最高频率受到限制。可以通过在音频工作线程中实现反馈回路来解决这个问题。
以下是使用音频工作线程实现 Karplus - Strong 算法的代码示例:
<p>Decay</p>
<input type='range' id='Decay' min=0.8 max=0.99 value=0.9 step='any'>
<span id='DecayLabel'></span>
<p>Delay (ms)</p>
<input type='range' id='Delay' min=0 max=20 value=10 step='any'>
<span id='DelayLabel'></span>
<p>Width (ms)</p>
<input type='range' id='Width' min=0 max='20' value=10 step='any'>
<span id='WidthLabel'></span>
<p>
<input type='button' value='play' id='Play'>
<script>
DecayLabel.innerHTML = Decay.value
DelayLabel.innerHTML = Delay.value
</script>
<script src='KarplusStrongV2.js'></script>
var context = new AudioContext()
context.audioWorklet.addModule('KSWorklets.js').then(() => {
let Noise = new AudioWorkletNode(context, 'noise-generator'),
NoiseGain = new GainNode(context, {gain:0}),
output = new GainNode(context),
feedbackDelay = new AudioWorkletNode(
context,
'feedbackDelay-processor',
{parameterData: {delayTime:5, gain:0.9}}
)
Noise.connect(NoiseGain)
NoiseGain.connect(output)
NoiseGain.connect(feedbackDelay)
feedbackDelay.connect(output)
output.connect(context.destination)
Decay.oninput = function() {
feedbackDelay.parameters.get('gain').value = this.value
DecayLabel.innerHTML = this.value
}
Delay.oninput = function() {
feedbackDelay.parameters.get('delayTime').value = this.value
DelayLabel.innerHTML = this.value
}
Width.oninput = function() {
WidthLabel.innerHTML = this.value
}
Play.onclick = function() {
context.resume()
var newDelay = Number(Delay.value) + 1000*128/context.sampleRate
feedbackDelay.parameters.get('delayTime').value = newDelay
let now = context.currentTime
NoiseGain.gain.setValueAtTime(0.5, now)
NoiseGain.gain.linearRampToValueAtTime(0, now + Width.value/1000)
}
})
registerProcessor('noise-generator', class extends AudioWorkletProcessor {
process(inputs, outputs) {
let output = outputs[0][0]
for (let i = 0; i < output.length; ++i) {
output[i] = 2*Math.random() - 1
}
return true
}
})
registerProcessor('feedbackDelay-processor', class extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{name:'gain', defaultValue:0.9, minValue:-1, maxValue:1},
{name:'delayTime', defaultValue:10, minValue:0, maxValue:1000}
]
}
constructor() {
super()
this.Buffer = new Array(48000).fill(0)
this.ReadPtr = 0
this.WritePtr = 0
}
process(inputs, outputs, parameters) {
let output = outputs[0][0], input = inputs[0][0]
let delaySamples = Math.round(sampleRate*parameters.delayTime[0]/1000)
let bufferSize = this.Buffer.length
for (let i = 0; i < outputs[0][0].length; ++i) {
output[i] = input[i] + parameters.gain[0]*this.Buffer[this.ReadPtr]
this.Buffer[this.WritePtr] = output[i]
this.WritePtr++
if (this.WritePtr >= bufferSize) this.WritePtr -= bufferSize
this.ReadPtr = this.WritePtr - delaySamples
if (this.ReadPtr < 0) this.ReadPtr += bufferSize
}
return true
}
})
4. Web Audio API 接口
Web Audio API 有一系列接口用于访问其所有功能,其中许多是音频节点,还有支持音频节点和音频图使用的方法和对象。以下是音频节点的列表:
| 音频节点 | 关联方法 | 调度源 | 目标 |
| — | — | — | — |
| AnalyserNode | createAnalyser | X | |
| AudioBufferSourceNode | createBufferSource | X | X |
| AudioDestinationNode
| | | X |
| AudioWorkletNode
* | |? |? |
| BiquadFilterNode | createBiquadFilter | | |
| ChannelMergerNode | createChannelMerger | | |
| ChannelSplitterNode | createChannelSplitter | | |
| ConstantSourceNode | createConstantSource | X | X |
| ConvolverNode | createConvolver | | |
| DelayNode | createDelay | | |
| DynamicsCompressorNode | createDynamicsCompressor | | |
| GainNode | createGain | | |
| IIRFilterNode | createIIRFilter | | |
| MediaElementAudioSourceNode | createMediaElementSource | X | |
| MediaStreamAudioDestinationNode | createMediaStreamDestination | | X |
| MediaStreamAudioSourceNode | createMediaStreamSource | X | |
| OscillatorNode | createOscillator | X | X |
| PannerNode | createPanner | | |
| StereoPannerNode | createStereoPanner | | |
| WaveShaperNode | createWaveShaper | | |
注: AudioDestinationNode 只能通过 AudioContext.destination 访问; * AudioWorkletNodes 可以作为源、目标或中间节点。
以下是非音频节点接口的列表:
| 接口 | 相关内容 | 描述位置 |
| — | — | — |
| AudioBuffer | 音频源 | |
| AudioContext | 音频图 | |
| AudioListener | 空间化 | |
| AudioNode | 音频图 | |
| AudioScheduledSourceNode | 音频源 | |
| AudioWorklet | 自定义节点 | |
| AudioParam | 音频图 | |
| AudioWorkletGlobalScope | 自定义节点 | |
| AudioWorkletProcessor | 自定义节点 | |
| BaseAudioContext | 音频图 | |
| OfflineAudioCompletionEvent | 非实时处理 | |
| OfflineAudioContext | 非实时处理 | |
| PeriodicWave | 音频源 | |
5. 各音频处理实例的操作步骤总结
5.1 比特压碎器操作步骤
-
创建
AudioContext对象,用于管理音频处理上下文。 -
创建
AudioBufferSourceNode节点并设置循环播放,通过fetch方法获取音频文件,将其解码后赋值给source.buffer。 -
调用
context.audioWorklet.addModule加载bitCrushWorklet.js模块。 -
创建
AudioWorkletNode实例bitcrusher,将source连接到bitcrusher,再连接到context.destination进行输出。 -
监听
input元素的oninput事件,根据用户操作更新bitcrusher的bitDepth参数。
5.2 动态范围压缩器操作步骤
-
创建
AudioContext对象。 -
创建
OscillatorNode节点source1和source2,以及GainNode节点gainNode,并将source1和source2连接到gainNode。 -
调用
context.audioWorklet.addModule加载compressorWorklet.js模块。 -
创建
AudioWorkletNode实例compressor,将gainNode连接到compressor,再连接到context.destination进行输出。 -
分别监听
Thresh、Ratio、Knee、Attack和Release输入元素的oninput事件,根据用户操作更新compressor相应的参数。
5.3 立体声增强器操作步骤
-
创建
AudioContext对象。 -
调用
context.audioWorklet.addModule加载stereoWidenerWorklet.js模块。 -
创建
AudioWorkletNode实例Widener,创建OscillatorNode节点monoSource和StereoPannerNode节点source,将monoSource连接到source,再连接到Widener。 -
启动
monoSource,将Widener连接到context.destination进行输出。 -
监听
width输入元素的oninput事件,根据用户操作更新Widener的width参数。
6. 循环缓冲区和延迟线的工作流程分析
6.1 固定延迟音频工作线程流程
graph TD;
A[开始处理] --> B[初始化 Buffer 数组];
B --> C[设置延迟样本数为 12];
C --> D[遍历输出样本];
D --> E[将输入样本赋值给输出样本];
E --> F[移动 Buffer 数组元素];
F --> G[将当前输入样本存入 Buffer 数组末尾];
G --> H{是否还有样本};
H -- 是 --> D;
H -- 否 --> I[结束处理];
6.2 使用循环缓冲区的固定延迟音频工作线程流程
graph TD;
A[开始处理] --> B[初始化 Buffer 数组、ReadPtr 和 WritePtr];
B --> C[设置延迟样本数为 12];
C --> D[获取 Buffer 数组长度];
D --> E[遍历输出样本];
E --> F{WritePtr 是否超出数组长度};
F -- 是 --> G[调整 WritePtr];
F -- 否 --> H[计算 ReadPtr];
G --> H;
H --> I{ReadPtr 是否小于 0};
I -- 是 --> J[调整 ReadPtr];
I -- 否 --> K[将输入样本存入 Buffer 数组 WritePtr 位置];
J --> K;
K --> L[从 Buffer 数组 ReadPtr 位置读取样本赋值给输出];
L --> M[更新 WritePtr 和 ReadPtr];
M --> N{是否还有样本};
N -- 是 --> E;
N -- 否 --> O[结束处理];
7. Karplus - Strong 算法实现的详细步骤
7.1 页面元素设置
在 HTML 中设置
Decay
、
Delay
、
Width
输入范围元素和
Play
按钮,并通过 JavaScript 初始化显示标签内容。
7.2 音频处理流程
-
创建
AudioContext对象。 -
调用
context.audioWorklet.addModule加载KSWorklets.js模块。 -
创建
AudioWorkletNode实例Noise、NoiseGain、output和feedbackDelay,并进行节点连接。 -
监听
Decay、Delay和Width输入元素的oninput事件,根据用户操作更新feedbackDelay相应的参数。 -
监听
Play按钮的onclick事件,更新feedbackDelay的delayTime参数,设置NoiseGain的增益值并进行线性衰减。
7.3 工作线程处理流程
-
noise-generator工作线程:在process方法中为输出数组的每个元素生成随机噪声。 -
feedbackDelay-processor工作线程:-
在
parameterDescriptors中定义gain和delayTime参数。 -
在
constructor中初始化Buffer数组、ReadPtr和WritePtr。 -
在
process方法中,根据delayTime计算延迟样本数,遍历输出样本,根据输入和Buffer数组中的值计算输出,更新Buffer数组和指针。
-
在
8. 总结
通过上述对各种音频处理实例的介绍,我们可以看到使用 Web Audio API 和音频工作线程能够实现复杂的音频处理功能。比特压碎器、动态范围压缩器和立体声增强器等实例展示了如何通过音频工作线程灵活控制音频参数,实现不同的音频效果。循环缓冲区和延迟线的使用则为处理音频信号的延迟提供了有效的解决方案。而 Karplus - Strong 算法的实现则展示了如何在音频工作线程中构建反馈回路,解决了 Web Audio API 中延迟节点带来的问题。同时,对 Web Audio API 接口的梳理,让我们更加清晰地了解了其各个组件的功能和使用方式。在实际开发中,我们可以根据具体需求选择合适的音频处理方法和接口,构建出高质量的音频应用。
超级会员免费看
537

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



