WebAudio API 入门

简介: 之前也有做过前端音频相关的操作,之前的做法是创建一个 audio 标签,需要播放提示音时调用 audio 实例的 play 方法,从而实现提示音的效果。

之前也有做过前端音频相关的操作,之前的做法是创建一个 audio 标签,需要播放提示音时调用 audio 实例的 play 方法,从而实现提示音的效果。

2021 年 6 月 17 日 — 万维网联盟(W3C)宣布 Web Audio API 现成为一项正式标准,支持在 Web 上创建和操作音乐及音频。

基于 Web Audio API 我们可以做很多有意思的事,例如在线乐器,在线游戏等,用户反馈等。

其浏览器的支持率已经非常高了,我们完全可以在主流浏览器应用这个 API。

1682561546(1).png


Audio API 的概念


在进行声音播放之前先来看下西 Audio 最基本的一些概念。

AudioContext 是音频上下文,可以在上下文中进行音频操作,如果你使用 Canvas,那么你对上下文应该是更容易理解的。AudioContext 可以理解为一个容器,之后所有发生的事情都需要在 AudioContext 范围内。

在上下文中通过AudioNode 音频节点来进行音频的操作,它是音频路由图(多个音频节点之间相关连接组成路由图)中最基本的组成单位,必须要工作在音频上下文中,并且一个音频节点只能绑定一个音频上下文。

音频节点有用自己的输入和输出,通过 connect 方法来连接不同的音频节点,所有的音频节点最终的出口是 AudioContext.destination,也就是扬声器等播放设备。

1682561567(1).png

destination 也是一种音频节点,此外常见的音频节点还有两类:SourceNodeProcessNode。sourceNode指能产生音频的节点,只有输出,没有输入;processNode是音频处理节点,可能会拥有一个或多个输入,一个输出或多个输出,例如下图所示:

1682561585(1).png

多个输入的情况如声道合并 ChannelMergerNode,那么就存在声道拆分 ChannelSplitterNode 可以有多个输出。


使用 Audio API 播放声音


音源

播放声音一定有一个音源,这个音源的主要来源有:

  • OscillatorNode:振荡器,创建某种指定类型的振荡器波,内置了四种波形"sine""square""sawtooth""triangle",分别是正弦波,方形波,锯齿波,三角波,此外还提供了自定义振荡器。可以从波形图中看到他们的命名由来
    1682561610(1).png
  • AudioBufferSourceNode:由内存音频数据组成的音源,数据存放在 AudioBuffer 中,主要是从网络请求中解析出来的二进制数据;
  • MediaElementAudioSourceNode:由 H5 元素生成的音源,video 或 audio 标签;
  • MediaStreamAudioSourceNode:通过 MediaStream 的形式获取的音源,如navigator.mediaDevices.getUserMedia、navigator.mediaDevices.getDisplayMedia。

可以通过下面的代码来播放余弦波振荡器发出的声音

const context = new AudioContext()
context.createOscillator()
// const osc = new OscillatorNode(context)
// const amp = new GainNode(context, { gain: 0.5 })
const osc = context.createOscillator()
const amp = context.createGain({ gain: 0.5 })
osc.start()
window.addEventListener('load', () => {
  const playBtn = document.getElementById('start-audio')
  playBtn.addEventListener('click', () => {
    osc.connect(amp)
    amp.connect(context.destination)
    context.resume()
  })
  const pauseBtn = document.getElementById('pause-audio')
  pauseBtn.addEventListener('click', () => osc.disconnect())
})
复制代码

你也可以通过下面的按钮来进行尝试四种振荡器(📢!!!调小音量

1682561636(1).png

音频处理器

上面的代码中其实就用到了音频处理器——GainNode,用于控制音频的音量。我们可以通过 AudioContext 的原型链来看到各种音频处理节点。

1682561665(1).png

在这些create方法中,每个音频节点都有自己的构造函数,在实例化的时候需要绑定 context,所以 AudioContext 提供了简便的创建方式,可以省略显式的 context 绑定。

下面我们来逐一认识各个处理器:

效果器 构造函数 输入 输出 通道数 用途
双二阶滤波器 BiquadFilterNode 1 1 2 控制音调、均衡器
线性卷积 ConvolverNode 1 1 1,2,4 音频混响
延迟 DelayNode 1 1 2 延迟输出
动态压缩 DynamicsCompressorNode 1 1 2 降低信号中最响部分的音量
音量 GainNode 1 1 2 音量增益
立体声 StereoPannerNode 1 1 2 控制左右声道的偏移
非线性畸变器 WaveShaperNode 1 1 2 添加暖调处理
周期性波形 PeriodWave - - - 自定义振荡器
分析器 AnalyserNode 1 1/0 1 音频分析或可视化
通道分离器 ChannelSplitterNode 1 变量,默认 6 - 将音频源的通道分离
通道合并器 ChannelMergerNode 变量,默认 6 1 2 多个输入流合并到一个流

音频控制

音频的控制主要有播放、停止、暂停/继续,在播放之前需要初始化音频源,音频源的创建我们上面说过有四种方式,并且我们已经演示了通过振荡器发生的过程,我们继续看一下另外的创建音频源的方式。

第一种,通过 arraybuffer 创建音频源。

首先创建一个 BufferSource,接着我们通过 fetch 请求获取到了音频文件的二进制数据,并将其解析为 arraybuffer,然后将 buffer 数据使用 AudioContext.decodeAudioData 来进行解析(老版本是回调的形式,新版本支持了 Promise),然后将数据喂给 BufferSource,最后将 BufferSource 输出到播放设备即可。

let sourceBuffer 
function loadResource() {
  sourceBuffer = context.createBufferSource()
  fetch('./bgm.aac')
    .then(res => res.arrayBuffer())
    .then(buffer => context.decodeAudioData(buffer))
    .then(audioBuffer => {
      sourceBuffer.buffer = audioBuffer
      sourceBuffer.connect(context.destination)
      sourceBuffer.start(0)
    })
}
function stopMusic() {
  context.suspend()
}
function startMusic() {
  if(!sourceBuffer)
    loadResource()
  context.resume()
}
复制代码

值得注意的是,使用音频上下文的暂停是整体的暂停,如果你的音频路由图中是有一个音频源那么可以使用这种方式来进行音频控制,如果是多音频源需要单独暂停某个音频源的话还是需要使用 AudioNode.disconnect。

你可以体验一下另一种形式的播放,选择一个本地文件来播放

image.png

第二种,通过音频/视频元素创建。

在已有的音频/视频元素基础上,通过 AudioContext.createMediaElementSource 来创建音频源。

const audioElement = document.getElementById('audio')
let elemContext
let gainNode
audioElement.addEventListener('play', () => {
  if (!elemContext) {
    elemContext = new AudioContext()
    gainNode = elemContext.createGain()
    const source = elemContext.createMediaElementSource(audioElement)
    source.connect(gainNode)
    gainNode.connect(elemContext.destination)  
  }
})
function voiceAdd() {
  if (gainNode.gain.value < 1)
    gainNode.gain.value += 0.1
}
function voiceSub() {
  if (gainNode.gain.value > 0)
    gainNode.gain.value -= 0.1
}
window.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowDown')
    voiceSub()
  if (e.key === 'ArrowUp')
    voiceAdd()
})
复制代码

媒体元素在使用createMediaElementSource处理之后,控制权将会由 AudioContext 接管。但是仍然可以使用原有的控制面板来控制播放/暂停。

第三种是通过媒体流来创建音源。

通过浏览器的媒体方法获取媒体流,将媒体流作为音频源进行创建,然后就可以进行音频节点的处理,这样就可以对音频进行效果增益

let mediaStream, streamContext;
function playAudioByStream() {
  streamContext = new AudioContext()
  const video = document.getElementById('video')
  navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
  }).then(stream => {
    video.srcObject = stream
    video.onloadedmetadata = (e) => {
      video.play()
      video.muted = true
    }
    const source = streamContext.createMediaStreamSource(stream)
    const delay = streamContext.createDelay(3)
    source.connect(delay)
    delay.connect(streamContext.destination)
  })
}
复制代码

除了使用媒体流进行处理之外,还可以使用createMediaStreamTrackSource 将某个音频轨道作为音频源进行处理。


音频可视化


常见的音频可视化有音乐频谱,音频波形匹配(音色验证),我们以音乐频谱为例来看一下音频可视化的操作流程。

和音频可视化相关的 AudioNode 是 AnalyserNode,可以用来显示波形和频谱。分析器节点会在指定的频率域捕获音频数据,这取决于设置的fftSize(默认2048)。

频谱

使用getByteFrequencyData获取频率的数据,这个方法接受一个数组参数,会将分析的结果放入这个数据,但是这个数组并不是日常使用的普通数组。因为产生的数据类型是 8 位无符号整形,所以我们需要构造一个Uint8Array 数组。

如果使用getFloatFrequencyData则需要使用 32 位精度浮点数 Float32Array。

function drawFrequency() {
  analyser.fftSize = 512;
  const bufferLengthFrequency = analyser.frequencyBinCount;
  const dataArrayFrequency = new Uint8Array(bufferLengthFrequency);
  analyser.getByteFrequencyData(dataArrayAlt);
}
复制代码

然后我们就可以从dataArrayFrequency中获取频谱数据了,我们可以结合 canvas 来进行可视化的绘制。

const drawFrequency = function () {
    drawVisual = requestAnimationFrame(drawFrequency);
    analyser.getByteFrequencyData(dataArrayFrequency);
    canvasCtx.fillStyle = "rgb(0, 0, 0)";
    canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
    const barWidth = (WIDTH / bufferLengthFrequency) * 2.5;
    let barHeight;
    let x = 0;
    for (let i = 0; i < bufferLengthFrequency; i++) {
      barHeight = dataArrayFrequency[i];
      canvasCtx.fillStyle = "rgb(" + (barHeight + 100) + ",50,50)";
      canvasCtx.fillRect(
        x,
        HEIGHT - barHeight / 2,
        barWidth,
        barHeight / 2
      );
      x += barWidth + 1;
    }
  };
复制代码

通过不断调用绘制方法可以一直使用实时的频率数据进行渲染,以达到频谱跃动的效果

1682561713(1).png

波形

使用getByteTimeDomainData获取波形数据,同样的需要使用 Uint8Array 数组,如果使用getFloatTimeDomainData则需要使用 Float32Array。

和频率相同的操作,但是波形图的绘制方式需要修改

const drawTimeDomain = function () {
    drawVisual = requestAnimationFrame(drawTimeDomain);
    analyser.getByteTimeDomainData(dataArray);
    canvasCtx.fillStyle = "rgb(200, 200, 200)";
    canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
    canvasCtx.lineWidth = 2;
    canvasCtx.strokeStyle = "rgb(0, 0, 0)";
    canvasCtx.beginPath();
    const sliceWidth = (WIDTH * 1.0) / bufferLength;
    let x = 0;
    for (let i = 0; i < bufferLength; i++) {
      let v = dataArray[i] / 128.0;
      let y = (v * HEIGHT) / 2;
      if (i === 0) {
        canvasCtx.moveTo(x, y);
      } else {
        canvasCtx.lineTo(x, y);
      }
      x += sliceWidth;
    }
    canvasCtx.lineTo(canvas.width, canvas.height / 2);
    canvasCtx.stroke();
  };
复制代码

1682561731(1).png

相关文章
|
2月前
|
开发框架 .NET API
RESTful API 设计与实现:C# 开发者的一分钟入门
【10月更文挑战第5天】本文从零开始,介绍了如何使用 C# 和 ASP.NET Core 设计并实现一个简单的 RESTful API。首先解释了 RESTful API 的概念及其核心原则,然后详细说明了设计 RESTful API 的关键步骤,包括资源识别、URI 设计、HTTP 方法选择、状态码使用和错误处理。最后,通过一个用户管理 API 的示例,演示了如何创建项目、定义模型、实现控制器及运行测试,帮助读者掌握 RESTful API 的开发技巧。
71 7
|
4月前
|
前端开发 JavaScript 安全
入门Vue+.NET 8 Web Api记录(一)
入门Vue+.NET 8 Web Api记录(一)
154 4
|
6月前
|
JavaScript API 开发者
GraphQL API开发入门:比RESTful更高效的数据查询方式
**GraphQL API开发入门摘要** GraphQL是一种更高效的数据查询方式,解决RESTful API的过度或不足获取数据问题。它允许客户端按需获取数据,减少网络传输,支持一次请求获取多资源。强类型和自描述特性方便了开发。文章通过一个简单的Node.js示例,展示如何使用`apollo-server-express`搭建GraphQL服务器,包括定义Schema、实现Resolver和创建服务器。通过测试,显示了GraphQL如何提供精确数据和优化查询效率。对于复杂数据需求,GraphQL是现代API设计的有效选择。
77 0
|
2月前
|
机器学习/深度学习 算法 API
机器学习入门(五):KNN概述 | K 近邻算法 API,K值选择问题
机器学习入门(五):KNN概述 | K 近邻算法 API,K值选择问题
|
4月前
|
开发者
告别繁琐代码,JSF标签库带你走进高效开发的新时代!
【8月更文挑战第31天】JSF(JavaServer Faces)标准标签库为页面开发提供了大量组件标签,如`&lt;h:inputText&gt;`、`&lt;h:dataTable&gt;`等,简化代码、提升效率并确保稳定性。本文通过示例展示如何使用这些标签实现常见功能,如创建登录表单和展示数据列表,帮助开发者更高效地进行Web应用开发。
46 0
|
4月前
|
前端开发 API 开发者
【React状态管理新思路】Context API入门:从零开始摆脱props钻孔的优雅之道,全面解析与实战案例分享!
【8月更文挑战第31天】React 的 Context API 有效解决了多级组件间状态传递的 &quot;props 钻孔&quot; 问题,使代码更简洁、易维护。本文通过电子商务网站登录状态管理案例,详细介绍了 Context API 的使用方法,包括创建、提供及消费 Context,以及处理多个 Context 的场景,适合各水平开发者学习与应用,提高开发效率和代码质量。
42 0
|
4月前
|
API 开发工具
langchain 入门指南(一)- 准备 API KEY
langchain 入门指南(一)- 准备 API KEY
263 0
|
7月前
|
API 开发工具 开发者
抖音商品详情API入门:为开发者和商家打造增长工具箱
抖音商品详情API入门:为开发者和商家打造增长工具箱
|
6月前
|
Linux API 数据安全/隐私保护
一文搞懂:【零基础】易盛9.0API入门二:登陆
一文搞懂:【零基础】易盛9.0API入门二:登陆
98 1
|
JavaScript 前端开发 API
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(三)
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(三)
587 1