22、Web Audio API 高级应用与实现

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 比特压碎器操作步骤

  1. 创建 AudioContext 对象,用于管理音频处理上下文。
  2. 创建 AudioBufferSourceNode 节点并设置循环播放,通过 fetch 方法获取音频文件,将其解码后赋值给 source.buffer
  3. 调用 context.audioWorklet.addModule 加载 bitCrushWorklet.js 模块。
  4. 创建 AudioWorkletNode 实例 bitcrusher ,将 source 连接到 bitcrusher ,再连接到 context.destination 进行输出。
  5. 监听 input 元素的 oninput 事件,根据用户操作更新 bitcrusher bitDepth 参数。

5.2 动态范围压缩器操作步骤

  1. 创建 AudioContext 对象。
  2. 创建 OscillatorNode 节点 source1 source2 ,以及 GainNode 节点 gainNode ,并将 source1 source2 连接到 gainNode
  3. 调用 context.audioWorklet.addModule 加载 compressorWorklet.js 模块。
  4. 创建 AudioWorkletNode 实例 compressor ,将 gainNode 连接到 compressor ,再连接到 context.destination 进行输出。
  5. 分别监听 Thresh Ratio Knee Attack Release 输入元素的 oninput 事件,根据用户操作更新 compressor 相应的参数。

5.3 立体声增强器操作步骤

  1. 创建 AudioContext 对象。
  2. 调用 context.audioWorklet.addModule 加载 stereoWidenerWorklet.js 模块。
  3. 创建 AudioWorkletNode 实例 Widener ,创建 OscillatorNode 节点 monoSource StereoPannerNode 节点 source ,将 monoSource 连接到 source ,再连接到 Widener
  4. 启动 monoSource ,将 Widener 连接到 context.destination 进行输出。
  5. 监听 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 音频处理流程

  1. 创建 AudioContext 对象。
  2. 调用 context.audioWorklet.addModule 加载 KSWorklets.js 模块。
  3. 创建 AudioWorkletNode 实例 Noise NoiseGain output feedbackDelay ,并进行节点连接。
  4. 监听 Decay Delay Width 输入元素的 oninput 事件,根据用户操作更新 feedbackDelay 相应的参数。
  5. 监听 Play 按钮的 onclick 事件,更新 feedbackDelay delayTime 参数,设置 NoiseGain 的增益值并进行线性衰减。

7.3 工作线程处理流程

  • noise-generator 工作线程:在 process 方法中为输出数组的每个元素生成随机噪声。
  • feedbackDelay-processor 工作线程:
    1. parameterDescriptors 中定义 gain delayTime 参数。
    2. constructor 中初始化 Buffer 数组、 ReadPtr WritePtr
    3. process 方法中,根据 delayTime 计算延迟样本数,遍历输出样本,根据输入和 Buffer 数组中的值计算输出,更新 Buffer 数组和指针。

8. 总结

通过上述对各种音频处理实例的介绍,我们可以看到使用 Web Audio API 和音频工作线程能够实现复杂的音频处理功能。比特压碎器、动态范围压缩器和立体声增强器等实例展示了如何通过音频工作线程灵活控制音频参数,实现不同的音频效果。循环缓冲区和延迟线的使用则为处理音频信号的延迟提供了有效的解决方案。而 Karplus - Strong 算法的实现则展示了如何在音频工作线程中构建反馈回路,解决了 Web Audio API 中延迟节点带来的问题。同时,对 Web Audio API 接口的梳理,让我们更加清晰地了解了其各个组件的功能和使用方式。在实际开发中,我们可以根据具体需求选择合适的音频处理方法和接口,构建出高质量的音频应用。

【四旋翼无人机】具备螺旋桨倾斜机构的全驱动四旋翼无人机:建模控制研究(Matlab代码、Simulink仿真实现)内容概要:本文围绕具备螺旋桨倾斜机构的全驱动四旋翼无人机展开研究,重点探讨其系统建模控制策略,结合Matlab代码Simulink仿真实现。文章详细分析了无人机的动力学模型,特别是引入螺旋桨倾斜机构后带来的全驱动特性,使其在姿态位置控制上具备更强的机动性自由度。研究涵盖了非线性系统建模、控制设计(如PID、MPC、非线性控制等)、仿真验证及动态响应分析,旨在提升无人机在复杂环境下的稳定性和控制精度。同时,文中提供的Matlab/Simulink资源便于读者复现实验并进一步优化控制算法。; 适合人群:具备一定控制理论基础和Matlab/Simulink仿真经验的研究生、科研人员及无人机控制系统开发工程师,尤其适合从事飞行建模先进控制算法研究的专业人员。; 使用场景及目标:①用于全驱动四旋翼无人机的动力学建模仿真平台搭建;②研究先进控制算法(如模型预测控制、非线性控制)在无人机系统中的应用;③支持科研论文复现、课程设计或毕业课题开发,推动无人机高机动控制技术的研究进展。; 阅读建议:建议读者结合文档提供的Matlab代码Simulink模型,逐步实现建模控制算法,重点关注坐标系定义、力矩分配逻辑及控制闭环的设计细节,同时可通过修改参数和添加扰动来验证系统的鲁棒性适应性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值