博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Web Audio API 一:基础

Posted on 2022-05-21 09:27  pencilCool  阅读(662)  评论(0编辑  收藏  举报

翻译自 https://webaudioapi.com/book/Web_Audio_API_Boris_Smus_html/ch01.html#ch01

基础知识

本章将介绍如何开始使用 Web Audio API,支持哪些浏览器,如何检测API是否可用,什么是音频图(audio graph),什么是音频节点(audio node),如何将节点连接在一起,一些基本的节点类型,最后是如何加载声音文件和播放声音。

web audio 简史

在网络上播放声音的最开始的方式是通过 <bgsound>标签,它让网站作者在访问者打开他们的网页时自动播放背景音乐。这个功能只在 Internet Explorer 中使用,从未被其他浏览器标准化或采用。Netscape 用<embed>标签实现了类似的功能,提供了基本相同的功能。

Flash是第一个在网络上播放音频的跨浏览器方案,但它有一个显著的缺点,即需要一个插件来运行。最近,浏览器供应商已经团结在HTML5 <audio>元素周围,该元素为所有现代浏览器的音频播放提供本地支持。

尽管网络上的音频不再需要插件,但

  • 没有精确的时间控制
  • 对一次播放的声音数量有很大限制
  • 没有稳定的办法来预先缓冲声音
  • 不支持实时音效
  • 没有分析声音的方法

已经有一些人试图在Web上创建一个强大的音频API来解决我之前描述的一些限制。一个值得注意的例子是Mozilla Firefox中设计和实现 Audio Data API。Mozilla的方法从一个<audio>元素开始,用额外的功能扩展其JavaScript API。这个API有一个有限的音频图(在后面的 "音频背景 "中会有更多的介绍),并且在第一次实现之后还没有被采用。。它目前在Firefox中已被废弃,转而使用Web Audio API。

与Audio Data API 相比,Web Audio API是一个全新的模型,完全独立于<audio>标签,尽管它与其他 web API有整合点(见与其他技术整合)。它是一个高水平的JavaScript API,用于在网络应用中处理和合成音频。这个API的目标是包括现代游戏引擎中的功能,以及现代桌面音频制作应用中的一些混合、处理和过滤任务。其结果是一个多功能的API,可用于各种与音频有关的任务,从游戏、互动应用到非常高级的音乐合成应用和可视化。

游戏和互动性

音频是使互动体验如此引人注目的一个重要部分。如果你不相信我,可以试着在看电影时把音量调低。

游戏也不例外! 我最美好的视频游戏记忆是音乐和声音效果。现在,在我最喜欢的一些游戏发布近20年后,我仍然无法忘记近藤浩二的塞尔达和马特-乌尔曼的暗黑破坏神原声带。从暴雪的《魔兽争霸》和《星际争霸》系列中的单元点击反应到任天堂经典游戏的样本,即使是这些精心设计的游戏的声音效果也是一眼就能认出的。

在游戏之外,声音效果也是非常重要的。从命令行时代开始,它们就一直存在于用户界面(UI)中,在那里,某些类型的错误会导致可听到的哔哔声。同样的想法延续到了现代用户界面中,在那里,恰当安置的声音对于通知、钟声,当然还有像Skype这样的音频和视频通信应用来说是至关重要的。Google Now和Siri等助理软件提供了丰富的、基于声音的反馈。随着我们进一步深入到一个无处不在的计算世界,基于语音和手势的界面,适合无屏幕的互动,越来越依赖于音频反馈。最后,对于有视力障碍的计算机用户来说,音频提示、语音合成和语音识别对于创造一个可用的体验是至关重要的。

交互式音频提出了一些有趣的挑战。为了创造令人信服的游戏中的音乐,设计师需要适应玩家可能发现自己处于的所有潜在的不可预测的游戏状态。在实践中,游戏的部分可以持续一个未知的时间,声音可以与环境互动,并以复杂的方式混合,需要环境特定的效果和相对的声音定位。最后,可能会有大量的声音同时播放,所有这些声音都需要在一起听起来很好,并在不引入质量和性能缺陷的情况下呈现。

音频上下文

网络音频API是围绕音频上下文的概念建立的。音频上下文是一个音频节点的有向图,它定义了音频流如何从其源头(通常是一个音频文件)流向其目的地(通常是你的扬声器)。当音频通过每个节点时,其属性可以被修改或检查。最简单的音频上下文是直接形成一个源节点到目的地节点的连接(图1-1)。


Figure 1-1. The simplest audio context

一个音频上下文可以很复杂,在源和目的地之间包含许多节点(图1-2),以进行任意高级的合成或分析。

图1-1和图1-2将音频节点显示为块。箭头代表节点之间的连接。节点通常可以有多个传入和传出的连接。默认情况下,如果有多个输入连接进入一个节点,Web Audio API会简单地将传入的音频信号混合在一起。

音频节点图的概念并不新鲜,它源于流行的音频框架,如苹果的CoreAudio,它有一个类似的音频处理图API。这个想法本身甚至更早,起源于1960年代的早期音频环境,如Moog模块化合成器系统。

image
Figure 1-2. A more complex audio context

初始化一个音频上下文

Web Audio API 目前由Chrome和Safari浏览器实现(包括iOS 6的MobileSafari),网络开发者可通过JavaScript使用。在这些浏览器中,音频上下文的构造函数是 webkit-prefixed,也就是说,你不是创建一个新的AudioContext,而是创建一个新的webkitAudioContext。不过,随着API的稳定,以及其他浏览器供应商的实施,这种情况在未来肯定会有所改变。Mozilla已经公开表示,他们正在Firefox中实施Web Audio API,Opera也已经开始参与这个工作组。

考虑到这一点,这里有一个初始化你的音频上下文的自由方式,包括其他实现(一旦存在)。

var contextClass = (window.AudioContext || 
  window.webkitAudioContext || 
  window.mozAudioContext || 
  window.oAudioContext || 
  window.msAudioContext)。
  if (contextClass) {
  // 网络音频API是可用的。
  var context = new contextClass();
} else {
  // 网络音频API不可用。要求用户使用一个支持的浏览器。
}

一个音频上下文可以支持多个声音输入和复杂的音频图,所以一般来说,我们只需要为每个音频应用程序创建一个。音频上下文实例包括许多用于创建音频节点和操作全局音频偏好的方法。幸运的是,这些方法没有被webkit限定,而且相对稳定。不过,该API仍在不断变化,所以要注意破坏性的变化(见废止说明)。

web Audio Node 的类型

音频语境的主要用途之一是创建新的音频节点。广义上讲,有几种音频节点:

  • 声源节点
    声音来源,如音频缓冲器、现场音频输入、
  • 修改节点
    滤波器、卷积器、平移器、JS处理器等。
  • 分析节点
    分析器和JS处理器
  • 目的地节点
    音频输出和离线处理缓冲区

来源不需要基于声音文件,而可以是来自现场乐器或麦克风的实时输入,也可以是音频元素的音频输出的重定向[参见用<audio>标签设置背景音乐],或者完全是合成的声音[参见用JavaScript进行音频处理]。虽然最终的目标节点通常是扬声器,但你也可以在没有声音回放的情况下进行处理(例如,如果你想做纯粹的可视化),或者做离线处理,这将导致音频流被写入目标缓冲区供以后使用。

连接 Audio Graph

任何音频节点的输出都可以通过connect()函数连接到任何其他音频节点的输入。在下面的例子中,我们将一个源节点的输出连接到一个增益节点,并将增益节点的输出连接到上下文的destination。

// 创建源。
var source = context.createBufferSource()。
// 创建增益节点。
var gain = context.createGain();
// 连接源和过滤器,过滤器和目的地。
source.connect(gain);
gain.connect(context.destination)。
注意,context.destination是一个特殊的节点,与你系统的默认音频输出相关。前面的代码产生的音频图看起来像图1-3。

image
Figure 1-3. Our first audio graph

一旦我们像这样连接了一个图,我们就可以动态地改变它。我们可以通过调用node.disconnect(outputNumber)来断开音频节点与图的连接。例如,要在源和目的地之间重新路由一个直接的连接,绕过中间节点,我们可以这样做。

source.disconnect(0);
gain.disconnect(0);
source.connect(context.destination);

模块化路由

在许多游戏中,多个声源被组合起来,形成最终的混音。来源包括背景音乐、游戏音效、UI反馈声音,以及在多人游戏环境中,来自其他玩家的语音聊天。web audio API的一个重要特点是,它可以让你把所有这些不同的通道分开,并让你完全控制每一个通道,或者把它们全部放在一起。这样一个设置的音频图可能看起来像图1-4。

image
Figure 1-4. Multiple sources with individual gain control as well as a master gain

我们为每一个通道都关联了一个单独的增益节点,还创建了一个主增益节点来控制它们。有了这样的设置,你的玩家就可以很容易地分别控制每个通道的电平,精确到他们想要的方式。例如,许多人喜欢在关闭背景音乐的情况下玩游戏。

什么是声音?

就物理学而言,声音是一种在空气或其他介质中传播的纵波(有时称为压力波)。声音的来源导致空气中的分子振动并相互碰撞。这就造成了高压和低压的区域,这些区域聚集在一起,又散开,形成带状。如果你能冻结时间,看一看声波的模式,你可能会看到类似图1-5的东西。
image
Figure 1-5. A sound pressure wave traveling through air particles

在数学上,声音可以被表示为一个函数,它在时间域中的压力值范围。图1-6显示了这样一个函数的图。你可以看到,它与图1-5类似,高值对应于密集颗粒的区域(高压),而低值对应于稀疏颗粒的区域(低压)。

image
Figure 1-6. A mathematical representation of the sound wave in Figure 1-5

可以追溯到二十世纪初的电子技术,使我们第一次有可能捕捉和重现声音。麦克风接收压力波并将其转换为电信号,其中(例如)+5伏对应最高压力,-5伏对应最低压力。反之,音频扬声器将这一电压转换回我们能听到的压力波。

无论我们是分析声音还是合成声音,对音频程序员来说,有趣的部分都在图1-7的黑盒子里,其任务是操纵音频信号。在音频的早期,这个空间是由模拟滤波器和其他硬件占据的,以今天的标准来看,这些都是过时的。今天,许多老式的模拟设备都有现代的数字对应物。但是,在我们用软件来处理这些有趣的东西之前,我们需要用一种计算机可以处理的方式来表示声音。

image
Figure 1-7. Recording and playback

什么是数字声音?

我们可以通过对模拟信号以某种频率进行时间采样,并在每次采样时将信号编码为一个数字。我们对模拟信号的采样率称为采样率。在许多声音应用中,一个常见的采样率是44.1kHz。这意味着,每秒钟的声音有44100个数字记录。这些数字本身必须在一定范围内。通常有一定数量的比特分配给每个值,这被称为比特深度。对于大多数录制的数字音频,包括CD,比特深度是16,这通常对大多数听众来说是足够的。发烧友们更喜欢24位深度,它能提供足够的精度,与更高的深度相比,人们的耳朵听不出差别。

将模拟信号转换为数字信号的过程称为量化(或采样),如图1-8所示。

image
Figure 1-8. Analog sound being quantized, or transformed into digital sound

在图1-8中,量化后的数字版本与模拟版本不太一样,因为条形图和平滑线之间存在差异。差异(显示为蓝色)随着更高的采样率和比特深度而减少。然而,增加这些数值也增加了将这些声音保存在内存、磁盘或网络上所需的存储量。为了节省空间,电话系统经常使用低至8千赫兹的采样率,因为使人类声音可理解所需的频率范围远远小于我们的全部可听频率范围。

通过对声音进行数字化处理,计算机可以把声音当作长的数字阵列。这种编码方式被称为脉冲编码调制(PCM)。由于计算机在处理阵列方面非常出色,PCM变成了大多数数字音频应用的一个非常强大的基础。在网络音频API的世界里,这个代表声音的长数组被抽象为AudioBuffer。AudioBuffers可以存储多个音频通道(通常是立体声,也就是左、右通道),表示为在-1和1之间归一化的浮点数数组。同样的信号也可以表示为整数数组,在16位中,范围从(-215)到(215-1)。

音频编码格式
PCM格式的原始音频相当大,会使用额外的内存,浪费硬盘的空间,下载时还会占用额外的带宽。正因为如此,音频通常以压缩格式存储。有两种压缩方式:有损和无损。无损压缩(例如,FLAC)保证当你压缩和解压缩一个声音时,其比特是相同的。有损音频压缩(如MP3)利用了人类听觉的特点,通过丢弃那些我们可能无论如何也听不到的比特来节省额外的空间。对于大多数人来说,有损格式通常是足够好的,但一些发烧友除外。

一个常用的衡量音频压缩量的标准被称为比特率,它指的是每秒钟播放的音频所需的比特数量。比特率越高,单位时间内可分配的数据就越多,因此需要的压缩量就越少。通常,有损格式,如MP3,是由其比特率描述的(常见的比特率是128和192 Kb/s)。有可能以任意的比特率对有损编解码器进行编码。例如,电话质量的人类语音经常被比作8 Kb/s的MP3。一些格式,如OGG,支持可变比特率,即比特率随时间变化。注意不要把这个概念与采样率或比特深度混淆[见《什么是声音》]!

浏览器对不同音频格式的支持有很大的不同。一般来说,如果网络音频API在浏览器中实现,它使用的加载代码和

关于更多最新的音频格式支持名册,见link

加载和播放声音

Web Audio API对缓冲区和源节点进行了明确的区分。这种架构的想法是将音频资源与播放状态解耦。用唱片机来比喻,缓冲区就像唱片,源就像播放头,只是在Web Audio API的世界里,你可以在任何数量的播放头上同时播放同一张唱片 因为许多应用涉及到同一缓冲区的多个版本同时播放,这种模式是必不可少的。例如,如果你想让多个弹跳球的声音快速连续发射,你需要只加载一次弹跳缓冲区,并安排多个播放源[见多声音的变化]。

为了将音频样本加载到Web Audio API中,我们可以使用XMLHttpRequest,并用context.decodeAudioData处理结果。这一切都以异步方式发生,不会阻塞主UI线程。

var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';

// 异步解码
request.onload = function() {
  context.decodeAudioData(request.response, function(theBuffer) {
    buffer = theBuffer;
  }, onError)
}
request.send()

音频缓冲区只是一个可能的播放源。其他来源包括从麦克风或线路输入设备或<audio>标签等直接输入(见与其他技术整合)。

一旦你加载了缓冲区,你可以为它创建一个源节点(AudioBufferSourceNode),将源节点连接到你的音频图中,并在源节点上调用start(0)。
要停止一个声音,在源节点上调用stop(0)。注意,这两个函数的调用都需要在当前音频上下文的坐标系中的一个时间(见完美的时间和延迟)。

function playSound(buffer) {
  var source = context.createBufferSource();
  source.buffer = buffer;
  source.connect(context.destination);
  source.start(0);
}

游戏中经常有背景音乐循环播放。然而,要注意选择过于重复:如果玩家被困在一个区域或关卡中,而背景中连续播放同一个样本,可能值得逐渐淡出该曲目,以防止产生挫败感。另一个策略是有不同强度的混音,根据游戏情况逐渐交叉淡出[见渐变的音频参数]。

把这一切放在一起

从前面的代码列表中可以看出,要在Web Audio API中播放声音,还需要一些设置。对于一个真正的游戏,可以考虑围绕Web Audio API实现一个JavaScript抽象。这个想法的一个例子是下面的BufferLoader类。它把所有东西都放在一个简单的加载器中,给定一组路径,返回一组音频缓冲区。下面是这样一个类的使用方法。

window.onload = init;
var context;
var bufferLoader;

function init() {
  context = new webkitAudioContext();

  bufferLoader = new BufferLoader(
    context,
    [
      '../sounds/hyper-reality/br-jam-loop.wav',
      '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

  bufferLoader.load();
}

function finishedLoading(bufferList) {
  // Create two sources and play them both together.
  var source1 = context.createBufferSource();
  var source2 = context.createBufferSource();
  source1.buffer = bufferList[0];
  source2.buffer = bufferList[1];

  source1.connect(context.destination);
  source2.connect(context.destination);
  source1.start(0);
  source2.start(0);
}

关于BufferLoader的简单参考实现,请看
https://webaudioapi.com/samples/script-processor/