jtwang7 / Project-Note

开发小记 - 项目里的一些新奇玩意儿
1 stars 0 forks source link

AudioContext - 让你的项目跟随音乐律动起来 #1

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

AudioContext - 让你的项目跟随音乐律动起来

参考文章:

前言

想在项目中添加一个音乐播放器,起初用 <audio /> 标签试了一下,结果发现在 chrome / safari 等各类浏览器都报错了。无奈之下,转头看向了另一个音频播放 - AudioContext。由于个人对 AudioContext 并不熟悉,因此本篇仅简单介绍 AudioContext 以及如何用它完成一个简易的音乐播放功能。

AudioContext 构造函数

AudioContext 本质是一个构造函数。 在使用时,第一步先实例化 audioContext 对象:

// 出于兼容目的
const AudioContext = window.AudioContext || window.webkitAudioContext;
// 实例化对象
const audioContext = new AudioContext();

AudioContext 构造函数:可选参数 latencyHint 值有 3 个,一般使用默认值即可

  • balanced 平衡音频输出延迟和资源消耗
  • inteactive 默认值 提供最小的音频输出延迟最好没有干扰
  • playback 对比音频输出延迟,优先重放不被中断

示例:

const audioContext = new AudioContext({ latencyHint: "balanced" });

audioContext 实例

AudioContext 实例的属性:

console.log(audioContext);
// ----------------
audioWorklet: AudioWorklet {}
baseLatency: 0.005333333333333333
currentTime: 1.5946666666666667
destination: AudioDestinationNode {maxChannelCount: 2, context: AudioContext, numberOfInputs: 1, numberOfOutputs: 0, channelCount: 2, …}
listener: AudioListener {positionX: AudioParam, positionY: AudioParam, positionZ: AudioParam, forwardX: AudioParam, forwardY: AudioParam, …}
onstatechange: null
sampleRate: 48000
state: "running"

AudioNode 通用模块

AudioNode 是一个处理音频的通用模块,在实际应用中我们不会直接使用AudioNode,而是使用AudioNode的子类们,AudioNode的子类很多,均通过调用 audioContext 实例的创建方法得到:

节点名称 创建方式 含义
AudioDestinationNode 通过audioContext.destination属性获得,是audioContext默认创建好的 表示 context 的最终节点,一般是音频渲染设备
AudioBufferSourceNode 通过audioContext.createBufferSource()创建 音源
GainNode 通过audioContext.createGain()创建 调节音量

AudioNode 模块的处理方式是链式的,每个 AudioNode 代表一个处理声音的模块(比如音量GainNode,音源AudioSourceNode),一个 AudioNode 处理完成后交由下个 AudioNode 来处理,传递的方式由audioNode1.connect(audioNode2) 来实现。

AudioDestinationNode 是一个特殊的AudioNode,它代表处理完成后的声音播放的出口 (音频渲染设备),该出口是设备本身就有的,所以在创建 AudioContext 时默认已创建好了 AudioDestinationNode,可以直接通过 audioContext.destination 来获得。

AudioBufferSourceNode

该节点代表音源,由 audioContext.createBufferSource() 方法创建 节点创建后需要往其 buffer 属性上挂载需要播放的数据 (解码后的音频数据) 除了 buffer 属性外,还有一些其他的属性:

还有一个方法,一个事件

触发 onended 事件的两个条件:

  1. 调用了 AudioScheduledSourceNode.stop()
  2. 设置了循环属性 AudioScheduledSourceNode.loop = true

AudioDestinationNode

该节点代表声音输出,由 audioContext.createMediaStreamDestination() 创建 由于代表声音的输出节点,所以该节点不能再使用 connect 方法去连接其他节点,否则会报错 Uncaught DOMException: Failed to execute 'connect' on 'AudioNode': output index (0) exceeds number of outputs (0).

destinationNode 在创建 audioContext 时会自动挂载到 audioContext.destination 上,所以一般不需要创建。

属性: maxChannelCount(read-only):返回设备可以处理的最大通道数;

GainNode

该节点代表音量控制,由 audioContext.createGain() 创建 可以通过设置 gainNode.gain.value 的值来设置音量,值的范围是 [0, 1]

function play(decodedAudioData) {
    const audioContext = new AudioContext();
    const sourceNode = audioContext.createBufferSource();
    sourceNode.buffer = decodedAudioData;
    const gainNode = audioContext.createGain();
    sourceNode.connect(gainNode);
    gainNode.connect(audioContext.destination);

    gainNode.gain.value = 0.5;
    sourceNode.start(0);
}

audioBufferSourceNode实例的 start 方法,接受一个 number 参数,表示延迟 x 秒播放 (注意单位是 秒)

此外,还有一些节点和功能,请参考 AudioContext入入入门 学习,此处不再详细讲解。

实战:创建一个音频播放 demo

demo 流程:axios 请求音频数据 (arraybuffer 类型) => 音频解码 (调用 decodeAudioData 实例的 ) => AudioBufferSourceNode(把解码后的数据挂载到音源上) => 通过audioContext.destination 交由硬件播放

本 demo 由 react + hooks 编写

// 第一步:音频数据请求
useEffect(() => {
  try {
    if (autoPlay) {
      // axios 响应拦截器
      axios.interceptors.response.use((res) => {
        return res.data;
      })
      // axios 请求音频数据
      // 本 demo 将音频源数据放在了 react public 文件夹下,因此请求地址为 '/public/musics/xxx.mp3'
      // 注意:responseType 要指定为 arraybuffer 类型
      axios.get(
        process.env.PUBLIC_URL + '/musics/张远 - 嘉宾.mp3',
        { responseType: 'arraybuffer' }
      ).then(
        (res) => {
          // 音乐播放函数
          play(audioCtx, res, 0);
        }
      )
    }
  } catch (err) {
    console.log(err);
  }
}, [autoPlay])

// 音频播放
// 音频解码 -> 将音频挂载到音源 -> 连接声音播放出口 -> 播放
function play(
  audioContext, // AudioContext 实例
  data, // 音频数据
  delay, // 音乐入场延迟(s)
  audioBufferSourceNode = (() => (audioContext.createBufferSource()))() // 音源
) {
  // 调用实例 audioContext 的 decodeAudioData 方法对音频数据进行解码,buffer 为解码结果
  audioContext.decodeAudioData(data, (buffer) => {
    audioBufferSourceNode.buffer = buffer; // 解码结果挂载到音源的buffer属性上
    audioBufferSourceNode.connect(audioContext.destination); // 音源连接外放设备
    audioBufferSourceNode.start(delay); // 延迟 delay 秒播放
  })
}

// 暂停播放
function suspend(ctx) {
  if (ctx.state === 'running') {
    // ctx 传入 audioContext 实例,并调用 suspend() 方法暂停播放
    ctx.suspend();
    setPlayerState('suspended');
  }
}

// 恢复播放
function resume(ctx) {
  if (ctx.state === 'suspended') {
    // 调用 audioContext 的 resume() 方法从暂停位置恢复播放
    ctx.resume();
    setPlayerState('running');
  }
}