20、Web Audio API:PannerNode与Audio Worklets的探索

Web Audio API:PannerNode与Audio Worklets的探索

1. PannerNode的应用

在音频处理中,PannerNode 是一个非常有用的工具,它可以用于在 3D 空间中定位音频源。以下是一个使用 PannerNode 的示例代码:

<input type='range' min=-8 max=8 step='any' id='panX'>
Left / Right<br>
<input type='range' min=-8 max=8 step='any' id='panY'>
Down / Up    <br>
<input type='range' min=-8 max=8 step='any' id='panZ'>
Front / Back<br>
<button onclick='context.resume()'>Start</button>
<script src='panner.js'></script>
let context = new AudioContext()
let source = new AudioBufferSourceNode(context, { loop: true })
fetch('drone.wav')
  .then(response => response.arrayBuffer())
  .then(buffer => context.decodeAudioData(buffer))
  .then(data => source.buffer = data)
source.start()
let panNode = new PannerNode(context)
panNode.refDistance = 0.5
panNode.panningModel = 'HRTF'
source.connect(panNode).connect(context.destination)
panX.oninput = () => panNode.positionX.value = panX.value
panY.oninput = () => panNode.positionY.value = panY.value
panZ.oninput = () => panNode.positionZ.value = panZ.value

这个示例展示了如何使用 PannerNode 在 3D 空间中移动音频源。通过三个滑块(panX、panY 和 panZ),可以控制音频源在左右、上下和前后方向的位置。音频源模拟了军事无人机的螺旋桨声音,让你可以想象自己在高处控制无人机在周围移动。

这里使用了默认的圆锥参数,因为音频源是全向的,源的方向并不重要。同时,使用了 ‘HRTF’ 作为 panning 模型,这在使用耳机时可以提供更好的定位效果,特别是在渲染高度时。

需要注意的是,一些空间化系统(如 Microsoft Direct3D)使用左手坐标系。在将应用程序从一个系统移植到另一个系统时,必须注意数据的转换。

2. Audio Worklets 的引入

Web Audio API 的真正强大之处在于引入了 Audio Worklets。Audio Worklets 允许开发者创建自己的音频节点,从而实现更多的音频处理和合成任务。

以下是创建和使用 Audio Worklet 的一般步骤:
1. 在主全局作用域之外定义一个音频工作处理器类,用于对音频数据执行某些操作。
2. 在主全局作用域中,调用音频上下文的 audioWorklet.addModule(moduleURL) 方法,其中 moduleURL 是包含音频工作处理器的 JavaScript 文件。
3. 通过将处理器的名称传递给 AudioWorkletNode() 构造函数来创建音频处理节点。
4. 设置节点所需的任何音频参数,这些参数在音频工作处理器中定义。
5. 像使用内置音频节点一样连接和使用 AudioWorkletNodes

3. 简单的音频工作示例:噪声源

下面是一个简单的音频工作示例,用于创建一个噪声源:

<button onclick=context.resume()>Start</button>
<script>
  let context = new AudioContext()
  context.audioWorklet.addModule('basicNoise.js').then(() => {
    let NoiseNode = new AudioWorkletNode(context, 'noise-generator')
    NoiseNode.connect(context.destination)
  })
</script>
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
  }
})

这个示例创建了一个简单的节点,生成一个噪声源,每个样本是一个介于 -1 和 +1 之间的随机数。它的工作方式类似于现有的源节点(如 constantSource oscillatorNode 等),但不需要使用任何参数,也不需要启动。点击按钮后,应该可以听到白噪声。

4. 音频工作处理器

音频工作处理器是一个 JavaScript 模块,用于定义和安装自定义音频处理器类。以下是关于音频工作处理器的一些要点:
- 工作处理器必须位于与加载它的位置不同的文件中。
- registerProcessor() 方法用于注册处理器类,该类是 AudioWorkletProcessor 类的扩展。
- 处理操作发生在 process() 方法中,该方法接收传入的音频数据,并将处理后的数据写回。
- process() 方法必须返回 true 以保持处理器处于活动状态。

5. 音频工作节点

在主处理线程中,创建音频工作节点的步骤如下:
1. 使用音频上下文的 audioWorklet.addModule(moduleURL) 方法加载包含处理器的模块。
2. 使用 new AudioWorkletNode(context, name) 创建自定义节点,其中 context 是音频上下文, name 是处理器的名称。
3. 将新节点连接到目标,就像连接任何默认音频节点一样。

6. 使用 async await 创建音频工作节点

JavaScript 提供了 async await 语法作为 Promise 语句的替代方案,使异步函数的结构更像典型的函数。以下是一个使用 async await 创建音频工作节点的示例:

<button onclick='context.resume()'>Start</button>
<script>
  let context = new AudioContext()
  async function audioGraph(context) {
    await context.audioWorklet.addModule('basicnoise.js')
    let NoiseNode = new AudioWorkletNode(context, 'noise-generator')
    NoiseNode.connect(context.destination)
  }
  audioGraph(context)
</script>

需要注意的是, async 函数返回一个 Promise。在 async 函数外部,通常无法访问使用 await 创建的音频工作节点。因此,所有涉及该节点的操作都必须在 async 函数内完成。

7. 输入、输出和选项:最大工作处理器

音频工作节点的输入和输出列表可能一开始会让人感到困惑。一个音频工作节点可以有多个输入源,每个输入源又是一个通道数组。输入的数量在创建节点时是固定的,但每个输入的通道数量可以在节点创建后更改。

以下是一个示例,用于找到多个输入的每个样本的最大绝对值:

<button onclick=context.resume()>Start</button>
<script>
  let context = new AudioContext()
  context.audioWorklet.addModule('multipleInputs.js').then(() => {
    let Source1 = new ConstantSourceNode(context, { offset: 1 })
    let Channel2_1 = new ConstantSourceNode(context, { offset: 2 })
    let Channel2_2 = new ConstantSourceNode(context, { offset: 4 })
    let Source2 = new ChannelMergerNode(context, { numberOfInputs: 2 })
    Channel2_1.connect(Source2, 0, 0)
    Channel2_2.connect(Source2, 0, 1)
    let MaxNode = new AudioWorkletNode(context, 'max-abs-value', { numberOfInputs: 2 })
    Source1.connect(MaxNode, 0, 0)
    Source2.connect(MaxNode, 0, 1)
    Source1.connect(MaxNode)
    Source2.connect(MaxNode)
    Source1.start()
    Channel2_1.start()
    Channel2_2.start()
  })
</script>
registerProcessor('max-abs-value', class extends AudioWorkletProcessor {
  process(inputs, outputs) {
    let output = outputs[0][0]
    for (let i = 0; i < inputs.length; i++) {
      for (let j = 0; j < inputs[i].length; j++) {
        for (let k = 0; k < inputs[i][j].length; k++) {
          output[k] = Math.max(Math.abs(inputs[i][j][k]), output[k])
        }
      }
    }
    return true
  }
})

这个示例使用两个源进行测试:一个单通道的恒定源和一个由每个通道的恒定源组成的双通道源。输出通道应该填充为 4,这是每个输入的每个样本的绝对值的最大值。

音频工作节点的选项包括:
| 选项 | 描述 | 默认值 |
| ---- | ---- | ---- |
| numberOfInputs | 输入到节点的初始输入数量 | 1 |
| numberOfOutputs | 从节点输出的初始输出数量 | 1 |
| outputChannelCount | 每个输出的通道数量数组 | 无 |
| parameterData | 用户定义的键值对列表,用于设置任何 AudioParams 的初始值 | 无 |
| processorOptions | 包含任何用户定义数据的对象,用于初始化关联的 AudioWorkletProcessor 中的自定义属性 | 无 |

8. 输入、输出和选项:panX 工作处理器

除了音频工作节点的选项外,音频工作节点还继承了音频节点的选项: channelCount channelCountMode channelInterpretation

以下是一个类似于 Supercollider 的 panX 插件的音频工作示例:

<button onclick=context.resume()>Start</button>
Panning: <input type=range min=-1 max=1 step=any id=Panning>
Speaker: <input type=range min=1 max=5 step=1 id=Speaker>
<script>
  let context = new AudioContext()
  context.audioWorklet.addModule('panX.js').then(() => {
    let Source = new OscillatorNode(context)
    Source.start()
    const panX = new AudioWorkletNode(context, 'panX-processor', {
      channelCount: 5,
      channelCountMode: 'explicit',
      channelInterpretation: 'discrete'
    })
    console.log(panX.channelCount)
    Source.connect(panX)
    var Splitter = context.createChannelSplitter(5)
    panX.connect(Splitter)
    Splitter.connect(context.destination, 1)
    Panning.oninput = function () {
      panX.parameters.get('pan').value = this.value
    }
    Speaker.oninput = function () {
      Splitter.disconnect()
      Splitter.connect(context.destination, this.value - 1)
    }
  })
</script>
registerProcessor('panX-processor', class extends AudioWorkletProcessor {
  static get parameterDescriptors() {
    return [{ name: 'pan', defaultValue: 0 }]
  }
  process(inputs, outputs, parameters) {
    let input = inputs[0], output = outputs[0]
    let nChannels = input.length
    var position = (nChannels - 1) * (1 + parameters.pan[0]) / 2
    for (let i = 0; i < nChannels; i++) {
      if ((position - i <= -1) || (position - i >= 1)) {
        for (let j = 0; j < output[i].length; j++) output[i][j] = 0
      } else {
        for (let j = 0; j < output[i].length; j++)
          output[i][j] = input[0][j] * Math.cos((position - i) * Math.PI / 2)
      }
    }
    return true
  }
})

这个处理器使用通道数量和 panning 参数来确定应用于每个通道的增益。为了简单起见,假设输入是单声道的,并且是为五个扬声器阵列进行 panning,不提供 panning 宽度的控制。

mermaid 流程图

graph LR
    A[开始] --> B[创建音频上下文]
    B --> C[加载音频工作模块]
    C --> D[创建音频工作节点]
    D --> E[连接音频工作节点到目标]
    E --> F[处理音频数据]
    F --> G[输出音频]
    G --> H[结束]

通过以上内容,我们可以看到 Web Audio API 中 PannerNode 和 Audio Worklets 的强大功能。PannerNode 可以实现音频源在 3D 空间中的定位,而 Audio Worklets 则允许开发者创建自定义的音频节点,实现更多复杂的音频处理和合成任务。无论是简单的噪声源还是复杂的多输入处理,都可以通过这些技术来实现。

9. 音频工作节点的参数设置与数据传递

在使用音频工作节点时,参数设置和数据传递是非常重要的环节。参数可以控制音频节点的行为,而数据传递则允许在不同的组件之间共享信息。

panX 工作处理器为例,在处理器类中通过 static get parameterDescriptors() 方法定义了参数:

static get parameterDescriptors() {
  return [{ name: 'pan', defaultValue: 0 }]
}

在主处理线程中,可以通过 panX.parameters.get('pan').value 来设置参数的值:

Panning.oninput = function () {
  panX.parameters.get('pan').value = this.value
}

除了参数设置,还可以通过 processorOptions 传递自定义数据。例如,在创建音频工作节点时:

let MaxNode = new AudioWorkletNode(context, 'max-inputs', {
  numberOfInputs: 2,
  processorOptions: { customData: 'some value' }
});

在处理器中可以通过 options 参数访问这些数据:

registerProcessor('max-inputs', class extends AudioWorkletProcessor {
  constructor(options) {
    super(options);
    const customData = options.processorOptions.customData;
    // 使用 customData 进行初始化
  }
  process(inputs, outputs) {
    // 处理音频数据
    return true;
  }
});
10. 音频工作节点的性能考虑

在使用音频工作节点时,性能是一个需要考虑的重要因素。由于音频处理通常需要实时进行,因此任何性能瓶颈都可能导致音频延迟或失真。

以下是一些提高音频工作节点性能的建议:
- 减少不必要的计算 :在处理器的 process() 方法中,尽量减少不必要的计算。例如,避免在循环中进行复杂的数学运算。
- 合理使用内存 :注意内存的使用,避免创建过多的临时对象。例如,在处理音频数据时,可以复用已有的数组。
- 优化算法 :选择合适的算法来处理音频数据。例如,使用高效的滤波器算法可以提高音频处理的效率。

11. 音频工作节点的错误处理

在实际应用中,可能会遇到各种错误,如模块加载失败、参数设置错误等。因此,需要进行适当的错误处理。

在加载音频工作模块时,可以使用 catch() 方法捕获错误:

context.audioWorklet.addModule('basicNoise.js')
 .then(() => {
    // 模块加载成功
  })
 .catch((error) => {
    console.error('Failed to load audio worklet module:', error);
  });

在处理器的 process() 方法中,也可以进行错误处理。例如,如果输入数据不符合预期,可以返回 false 来停止处理器的运行:

registerProcessor('noise-generator', class extends AudioWorkletProcessor {
  process(inputs, outputs) {
    if (inputs.length === 0) {
      console.error('No input audio data');
      return false;
    }
    let output = outputs[0][0];
    for (let i = 0; i < output.length; ++i) output[i] = 2 * Math.random() - 1;
    return true;
  }
});
12. 音频工作节点的应用场景

音频工作节点可以应用于各种场景,以下是一些常见的应用场景:
| 应用场景 | 描述 |
| ---- | ---- |
| 音频特效 | 创建自定义的音频特效,如混响、失真等。 |
| 音频合成 | 合成不同的音频源,创建独特的音频效果。 |
| 实时音频处理 | 对实时音频流进行处理,如语音识别、音频过滤等。 |
| 游戏音频 | 在游戏中实现音频的空间化和交互效果。 |

13. 总结

通过对 Web Audio API 中 PannerNode 和 Audio Worklets 的探索,我们了解到了它们的强大功能和应用场景。

PannerNode 可以实现音频源在 3D 空间中的定位,为音频应用带来更加真实的听觉体验。而 Audio Worklets 则允许开发者创建自定义的音频节点,实现各种复杂的音频处理和合成任务。

在使用这些技术时,需要注意参数设置、性能优化和错误处理等方面。同时,合理利用音频工作节点的选项和数据传递机制,可以更好地实现音频应用的需求。

mermaid 流程图

graph LR
    A[开始] --> B[创建音频上下文]
    B --> C{加载模块成功?}
    C -- 是 --> D[创建音频工作节点]
    C -- 否 --> E[错误处理]
    D --> F[设置参数和数据传递]
    F --> G[连接音频节点]
    G --> H[处理音频数据]
    H --> I{处理成功?}
    I -- 是 --> J[输出音频]
    I -- 否 --> K[错误处理]
    J --> L[结束]
    E --> L
    K --> L

总之,Web Audio API 为开发者提供了丰富的工具和功能,通过 PannerNode 和 Audio Worklets 的结合,可以创造出更加精彩的音频应用。无论是简单的音频效果还是复杂的音频系统,都可以在这个平台上实现。希望本文能够帮助开发者更好地理解和应用这些技术,为音频领域的发展做出贡献。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值