【前端周刊】20191101 初识 Web Audio

文章由再见落日余晖投稿,感谢@人间最美四月天建哥审校。javascript

注:本文使用的Web Audio API遵循W3C在18年9月发布的候选推荐版本,本文代码在Chrome76中测试经过,请注意代码兼容性。若有错误,请不吝指正。html

Web Audio Api的兼容性(数据来自can i use)

引言

每段缘起始都很简单,或是擦肩而过的那阵清风,或是四目相对的那缕温情,亦是共经患难的那份情谊……初春的一天,在我被工做中出现的问题折磨的不堪之际,经朋友引荐,她出如今个人视野中。我忘不了她解决问题时的飒爽英姿,那干练的身影在我脑海中迟迟不能散去。我有意要了解她,却总感受只在冰山一角。终于经历了几个月的徘徊后,我下定决心再也不等待,开一个系列来介绍个人理解。Why now? Why not?java

本文是《认识 Web Audio》系列文章的第一篇,主要涉及音频上下文的一些简单概念的介绍和使用。整个系列分为如下文章:ios

  • 碰见你,很幸运——初识 Web Audio
  • 知其然,知其因此然——Web Audio原理探究
  • 我和音符有个约会——音频可视化篇
  • 为你弹奏肖邦的夜曲——音频创做篇
  • 是谁带来远古的呼唤——音频空间化篇

Web Audio背景

长期以来在网页上播放音视频都是一个痛点,虽然HTML5引入了audio、video标签来实现基本的音视频播放,但却不足以应付更复杂状况如游戏引擎、实时交互等场景下的音频处理。因而,Web Audio应运而生。但要说明的是,它之于audio标签并非替代关系,而是相似img和canvas的一种补充的关系。Web Audio播放音频能实现安卓、ios的一致性,消除诸如播放有延迟、不能循环播放等问题,如howler.js(固然不只于此);其次能够利用振荡器、滤波器等创造本身的乐器,进行交互式音乐的构建、实现歌曲的演奏,如Tone.js;还能够对音频播放进行可视化,如Pts.js……git

Web Audio是应在web浏览器中处理音频的需求而产生的,它早已不是一个新鲜的概念,但并无大规模使用倒是一个不争的事实。除去不值一提的兼容性问题,我想缘由大概有几个,首先是API居多,由于它要实如今浏览器产生、处理音频必需要有相应的接口,但和专业音频制做程序仍有必定差距,不是特别能吸引专业人士;而后就是要实现实时WebRTC、游戏级引擎的音频特效等所须要的专业知识(数字信号处理、通讯原理等)所带来的学习成本也是让不少人望而却步。不过大势所趋,它还在处于不断发展中,相信将来前景仍是不错的。github

Web Audio提供的合成声音、添加特效、音频可视化功能,是在音频上下文完成的,它将不一样的操做细化为对应的节点实现了模块化(Modular routing),各个节点又能够经过connect方法相链接,进行必要的处理后,最终输出到目标节点(音频上下文的destination属性,一般是声音输出设备如扬声器)。整个流程链接在一块儿造成一个音频路由图(audio routing graph)。web

Web Audio Api简单工做流程为:ajax

  1. 建立音频上下文(AudioContext或OfflineAudioContext)
  2. 在上下文中建立声音来源(如audio标签、xmlhttprequest请求获得的arraybuffer、oscillator振荡器产生的各类波形)
  3. 建立特效节点(如混响、各类滤波、平移、压缩)
  4. 选择音频的最终产出地(如系统扬声器)
  5. 未来源连到特效节点,将特效节点连到目的节点
    Web Audio Api使得咱们能够更精确(时间上能够作到无延迟、空间上能够产生衰减效果模拟真实环境)的操做音频,这是audio标签所不具有的。举个例子来讲,若是要实现音频的渐入渐出效果,原始方法须要使用定时器来不断更改volume来实现,而使用web audio只需将源节点连到增益节点(GainNode),设置gainNode.gain.linearRampToValueAtTime(value, endTime)就能实如今endTime-currentTime时间段内,声音值从原始到value值的变化,能够说操纵很轻松了。

Web Audio基础

在正式使用以前,首先了解一下Web Audio Api的部分接口。canvas

音频上下文接口
  • BaseAudioContext是实际使用的音频上下文AudioContext(用于实时渲染)和OfflineAudioContext(用于离线渲染)的基类,不能被直接实例化。音频节点都在其内建立并相互链接在一块儿,容许信号最终链接到AudioDestinationNode节点进行播放,造成一个音频路由图。其包含的部分属性以下:浏览器

    • state属性是一个枚举属性,取值为能够是suspended/running/closed,表示音频上下文的当前状态。前两个可经过实例的resume()suspend()方法切换,调用close()以后音频上下文将会释放系统资源,不能再次使用。
    • currentTime属性表示上下文的运行时间,当上下文处于running状态时该值会以均匀的速度单调递增,由渲染线程控制,不必定和处理的音频时间同步。
    • destination属性是一个AudioDestinationNode节点,一般是实际的音频输出设备。
    • sampleRate只读属性,表示采样率,在处理过程当中保持不变,所以实时处理中不支持采样率转换。在不设置的状况下默认为音频输出设备的采样率。若是处理时使用的采样率和音频输出设备的采样率不一致会进行重采样。
  • AudioContext实时音频上下文,来直接为用户产生信号。AudioContext初始化时state默认为suspended,由于自动播放策略限制,必须通过用户操做后才容许处于运行状态,这可经过resume()恢复音频上下文,或者调用AudioBufferSourceNode的start()时来恢复上下文。主动暂停上下文使用suspend(),关闭使用close()但要注意的是虽然两者均可以释放包括线程,进程和音频流等系统资源,但前者可经过resume()恢复运行,然后者意味着释放全部资源,此后将没法使用或再次恢复它。构造函数中可传入contextOptions的对象,若是选项中包含sampleRate属性,则采样率设置为该属性,不然使用默认输出设备的采样率。

  • OfflineAudioContext离线音频上下文,虽然有destination属性,但实际并不会渲染到音频输出硬件中,但会尽量快的渲染(通常来讲要比实时渲染更快),经过startRendering()返回AudioBuffer,适用于那些能够在后台进行音频处理的场景。使用OfflineAudioContext(numberOfChannels, length, sampleRate)构造函数进行初始化,参数必填,不过也能够将一个包含这三个属性的对象(sampleRate/length必须属性)做为参数传递。由于构造时就已经肯定了长度,因此在渲染length/smapleRate时间以后其状态就会变为closed,不能继续使用了。和AudioContext不一样的是,离线音频上下文只有suspend(suspendTime)方法,没有close()方法,在音频数据渲染完以后主动关闭。终止时必须传入终止时间,以在指定时刻终止,该方法通常来讲只有在同步操做音频数据时才有用。

音频节点、数据和音频控制接口
  • AudioNode:音频节点,是全部展现在音频路由图中节点模块(如音频源节点、目的节点、过滤器节点、增益节点等)的基类,不能直接实例化,提供connect方法实现节点间的链接。

  • AudioBuffer:音频缓冲区,由createBuffer(numberOfChannels, length, sampleRate)或构造器建立而来,参数都是必填,生成指定长度的音频buffer,该buffer默认初始化为0。该buffer包含duration(=length/sampleRate)、length、numberOfChannels、sampleRate属性。可经过getChannelData(channel)方法获取某一通道下的音频数据,返回一个Float32Array类型的数据。它只是保存数据源,不是真正的音频节点,只有赋值给相应的AudioNode才有做用。像AudioBufferSourceNode的buffer就是AudioBuffer类型。

  • AudioParam:音频参数接口,控制AudioNode的某个方面,好比音量。该接口除了value属性,其余属性都是只读的。能够直接为参数的value赋值,也可使用AudioParam的方法实现预先设定。setValueAtTime(value, startTime)能够作到的当AudioContext的currentTime走到给定的startTime时间后进行赋值为value。linearRampToValueAtTime(value, endTime)能够作到从当前时间到endTime从当前值线性变化到value值。经常使用场景是实现声音播放的渐隐渐现,来避免使用定时器来控制音量大小。setTargetAtTime(target, startTime, timeConstant)可用于实现声音信号的衰变。

音频源节点
  • AudioBufferSourceNode:音频数据源节点,由音频上下文的createBufferSourceNode()方法或构造器建立获得。它接受一个AudioBuffer做为buffer属性来提供数据源,数据源必须来自内存。

  • MediaElementAudioSourceNode:媒体元素节点,由指定的HTMLMediaElement(一般为audio或video标签)建立获得,调用该方法后,音频播放的控制权移交给当前音频上下文的路由图。即只有当前AudioContext状态为running时才能播放,而且要链接到AudioContext的destination节点才能播放出声音。它容许咱们操做audio、video声音轨道数据。

  • MediaStreamAudioSourceNode:媒体流音频节点,获取实时媒体流的音频源。使用音频上下文的createMediaStreamSource(mediaStream)方法或new MediaStreanAudioSourceNode(context, {mediaStream})建立而来

音频目的节点
  • MediaStreamAudioDestinationNode:音频流输出目的节点,数据存储在stream属性中,作临时存储用。

  • AudioDestinationNode:音频目的节点,每一个AudioContext只有惟一的一个该节点,由BaseAudioContext的destination属性提供,通常为对应的音频输出硬件,如扬声器。

Web Audio的使用

接下来从加载音频的角度讲述如何使用Web Audio。加载音频方式主要有四种

  • 使用HTMLMediaElement元素(如audio/video)做为数据源,适合应用于音频数据量很大的场景

    下面是一个接管页面audio标签音频播放的示例,并使用gainNode节点来控制音频的音量

<audio src="./1.mp3" controls></audio>
<button>播放</button>
<input type="range" value="1" min="0" max="3" step=".1" />
<script> const ac = new AudioContext() const range = document.querySelector('input[type="range"]') const btn = document.querySelector('button') const audio = document.querySelector('audio') const ms = ac.createMediaElementSource(audio) // 等价于如下语句 // const bf = new MediaElementAudioSourceNode(ac, { mediaElement: audio }) const gainNode = ac.createGain() gainNode.gain.value = 1 ms.connect(gainNode).connect(ac.destination) btn.onclick = function() { // AudioContext若处于suspended状态,须要恢复后才能正常播放 if (ac.state === 'suspended') ac.resume() audio.play() } range.onchange = e => { gainNode.gain.value = e.target.value } </script>
复制代码
  • ajax异步获取数据:由于只有当所有加载到音频数据后才能使用,因此通常应用在少许音频数据场景中

    下面是一个先使用离线音频上下文异步获取到音频数据再传给音频上下文进行播放的例子

const ac = new AudioContext()
// 这里只是为展现OfflineAudioContext的使用,实际上彻底能够直接用AudioContext异步获取到数据进行播放
const oc = new OfflineAudioContext(2, 44000 * 100, 44000)
async function fetchAudio(src) {
  const file = await fetch(src)
  const fileBuffer = await file.arrayBuffer()
  const buffer = await oc.decodeAudioData(fileBuffer)
  const bufferSource = oc.createBufferSource()
  bufferSource.buffer = buffer
  bufferSource.connect(oc.destination)
  bufferSource.start(0)
  oc.startRendering()
   .then(buffer => {
     // 这里对a、b进行赋值是为了给第三个例子的buffer创造来源
     window.a = buffer.getChannelData(0)
     window.b = buffer.getChannelData(1)
     const bs = new AudioBufferSourceNode(ac, { buffer })
     bs.connect(ac.destination)
     bs.start(0)
   })
   .catch(e => {
     console.log(e)
   })
}
fetchAudio('1.mp3')
复制代码
  • 自定义声音:使用OscillatorNode产生相似正弦、锯齿形波形或者建立buffer并使用数据填充

    下面展现了一个使用自定义数据填充AudioBuffer的例子,在建立buffer时须要传入信道数、采样率、采样长度,使用AudioBuffer的copyToChannel()方法能够实现将一个Float32Arrays类型数据复制到指定信道。固然数据也能够本身产生,mdn官网上有一个使用随机数生成噪声的例子

const ac = new AudioContext()
const buffer2 = ac.createBuffer(2, 10 * 41000, 41000)
// 这里使用了上面第二个例子代码中的数据来源
buffer2.copyToChannel(a, 0, 0)
buffer2.copyToChannel(b, 1, 0)
const bf = ac.createBufferSource()
bf.buffer = buffer2
bf.connect(ac.destination)
bf.start()
复制代码
  • 经过Media Stream API获取相机和麦克风的数据:使用MediaSteamAudioSourceNode来实现,适用于WebRTC或想要录制音频的场景

    下面是一个使用getUserMedia和MediaRecorder进行录音的例子。getUserMedia获取用户录音的权限,由于在录音过程当中不须要输出,因此将音频流保存至MediaStreamAudioDestinationNode节点,而且在音频流传递过程当中还使用了低通滤波器实现低音加强,录完音保存至audio标签,使用原生控件可进行播放和下载。一样,此例子只作展现用,其实直接将mediaStream做为参数传递给MediaRecorder构造器也是能实现录音。不过要注意的是MediaRecorder目前还仅在高版本PC浏览器中受支持,不太适合应用于生产环境。

<audio controls></audio>
<button>开始录音</button>
<script>
  const ac = new AudioContext()
  const biquadFilter = ac.createBiquadFilter()
  const msa = new MediaStreamAudioDestinationNode(ac)
  const mediaRecorder = new MediaRecorder(msa.stream)
  let chunks = [],
    start = false
  const btn = document.querySelector('button')
  const audio = document.querySelector('audio')
  // 麦克风->低音滤波->音频流目的节点 在这个过程当中进行录制->audio标签播放
  navigator.mediaDevices
    .getUserMedia({
      audio: true
    })
    .then(mediaStream => {
      const ms = ac.createMediaStreamSource(mediaStream)
      biquadFilter.type = 'lowshelf'
      biquadFilter.frequency.value = 40000
      biquadFilter.gain.value = 1
      ms.connect(biquadFilter).connect(msa)
    })
    .catch(e => console.log(e))
  // 点击按钮开始录音或结束录音
  btn.onclick = () => {
    if (!start) {
      mediaRecorder.start()
      btn.innerText = '结束录音'
    } else {
      mediaRecorder.stop()
      btn.innerText = '开始录音'
    }
    start = !start
  }
  // 把获得的录音流保存至chunk
  mediaRecorder.ondataavailable = e => {
    chunks.push(e.data)
  }
  mediaRecorder.onstop = e => {
    const blob = new Blob(chunks, { type: 'audio/ogg; codecs=opus' })
    audio.src = URL.createObjectURL(blob)
  }
</script>
复制代码

参考资料

  1. Web Audio API规范
  2. MDN Web_Audio_API专题
相关文章
相关标签/搜索