Web Audio API的使用
PUBLISHED
简介
现代的手机应用和游戏没有不需要音频的。 然而,很长一段时间,开发者在他们的应用中都是依靠外部浏览器的插件来实现声音的。 幸运地,W3C引进了Web音频API。 最新的说明书依然在起草中。 这是一个2011才出现的比较新的技术,下面链接可以找到: Tizen 2.0.0最终的WebKit引擎实现了2011年10月15日发布的第一版规范。 该技术被设计成既易用又强大的。
你不应该去想web音频API中的HTML音频标签。 音频标签提供基础功能,比如播放音乐;Web音频API有更多的功能。 工程师根据规范中介绍的特性可以精确的控制声波。 可以用来创建专业的声音编辑器。 修改的声音数据可以当做html音频标签的来源或者保存到文件中。 然而,默认情况不提供第二个选项。 你需要使用文件API压缩数据。 Web音频API依然在进化,甚至我们现在可以看到让人诧异的使用例子。
所有的API函数的入口点是一个上下文对象,我们调用它的构造函数webkitAudioContext() 来获取(起草阶段使用“webkit”作为前缀)。 所有的函数被封装在这个对象中。
var context = new webkitAudioContext();
什么是节点,如何使用它?
一个声音总是有它的来源和目的地。 来源可以被解码成声音/音乐 文件数据,或者可以被API合成。 目的地是一个接收声音数据并播放声音的扬声器(S)设备。
源和目的有两个基本节点。 节点是具有特定功能的基本web音频API的元素。 有很多不同功能的节点:GainNode 调节音量,PannerNode 模拟立体声,DelayNode 延迟声音,AnalyserNode 从声波中取出越来越多的数据。 可以在W3C网站上看到更多关于可用的节点的说明: http://www.w3.org/TR/2011/WD-webaudio-20111215/. 为了听到声音,你需要把源和目的节点连接起来。 然而,你可以在他们之间插入前面提到的任何节点。 使用connect()函数去连接一个节点到另一个节点,该函数的参数是需要传输数据的另一个节点。 最小的两个节点的连接被称为路由图。 一个路由图的例子显示在下面的图片。
通道图 |
通道图可以更加复杂。 上一个例子中,我们只示例了一个通道。 在一个实际的应用中,可以包含很多连接不同源和目的节点的通道。 例如,我们可能想同时播放两个声音,每个声音有独立的音量控制。 这种情况如下图所示。
两个来源的通道图 |
加载和播放声音
现在有了web音频API的工作原理,我们可以试着加载并播放声音了。 我们可以使用许多方法加载声音数据,唯一的要求是以数组格式获取数据。 我们将会展示如何使用 XMLHttpRequest获取数据。 通过XMLHttpRequest (通常叫做AJAX),如果我们在XML配置文件中声明特殊的存取权限,我们可以从不同域加载文件。 我们需要设置应用需要访问的domain。 然而,如果你想从Tizen设备内部内存中加载声音文件,你可以使用Tizen的文件系统API。 此外,每个控件有它私有的存储,位于叫做 ‘wgt-private’的本地。 本文不介绍Tizen 文件系统API。
Making AJAX 请求
当请求 AJAX ,我们需要设置一些熟悉,如URL,请求方法,请求类型和应答类型。 我们设置在XMLHttpRequest对象的open()的前三个参数。 我们使用异步的GET方法。 文件的URL可以是一个内部文件的路径,可以是这三种类型(WAV,MP3或者OGG)或者URL匹配的存取模式的外部文件。 在config.xml文件中,我们可以添加一个ACCESS标签到应用中,它可以存取匹配的外部资源。 如果我们想去允许从任何domain加载文件,我们可以使用下面的代码。
<access origin="*" subdomains="true"/>
如果我们想限制从明确domain加载文件,我们使用下面的代码:
<access origin="http://example.com" subdomains="true"/>
我们返回到AJAX请求。 应答类型应该设置到数组,因为文件数据是二进制。 下面的代码显示AJAX请求从应用程序目录加载声音文件。
xhr = new XMLHttpRequest(); xhr.open('GET', './sound.mp3', true); xhr.responseType = 'arraybuffer'; xhr.onload = function () { /* Processing response data - xhr.response */ }; xhr.onerror = function () { /* Handling errors */ }; xhr.send();
解码文件数据
现在有文件的数据,我们必须对其进行解码。 像前面提到的,我们可以加载这三种格式的文件:WAV, MP3 或者 OGG。 这些文件的数据被转化成PCM/raw格式,为了后面的使用。
我们使用decodeAudioData() 函数从webkitAudioContext对象解码数据。 它有三个参数。 第一个是我们从xhr.response 对象AJAX请求的二进制数据。 第二个参数是成功执行的功能,第三个参数是错误执行的功能。 当decodeAudioData() 执行完,它以解码的音频数据作为参数调用一个回调函数。 下面的代码演示了解码过程。
/* Decoding audio data. */ var context = new webkitAudioContext(); context.decodeAudioData(xhr.response, function onSuccess (buffer) { if (! buffer) { alert('Error decoding file data.'); return; } }, function onError (error) { alert('Error decoding file data.'); });
播放声音
我们有声音的数据,但我们没有SourceNode对象,所以我们需要创建一个SourceNode对象。 我们通过调用上下文对象的createBufferSource()函数创建它。
var source = context.createBufferSource(); /* Create SourceNode. */
稍后,我们会把AudioBuffer数据分配给SourceNode的缓存属性。
source.buffer = buffer; /* buffer variable is data of AudioBuffer type from the decodeAudioData() function. */
正如在文章的开始时提到的,我们必须将SourceNode连接到DestinationNode。
source.connect(context.destination); /* Connect SourceNode to DestinationNode. */
把0作为函数noteOn()的参数,就可以立即播放声音了。 这个参数指定了播放声音的延迟时间,以毫秒为单位。
source.noteOn(0); /* Play sound. */
Tizen的2.0.0 WebKit引擎实现了W3C Web Audio API的第一个规范。 在这个规范中,我们使用noteOn()函数来播放生硬,但是在现在的版本中,noteOn()函数被start()函数取代了。
为路由图添加节点
现在我们只用了两个节点,SourceNode和DestinationNode。 怎样为例子添加一个GainNode节点呢?只要将GainNode分别和SourceNode及DestinationNode连接就可以了。 你可以添加尽可能多的节点。 声音处理从路由图中的起始节点开始,到目的节点结束。 让我们来看看下面的代码。
var gainNode = context.createGainNode(); /* In the newest Audio API specification it's createGain(). */ source.connect(gainNode); /* Connect the SourceNode to the GainNode. */ gainNode.connect(context.destination); /* Connect the GainNode to the DestinationNode. */
要比方声音,要调用SourceNode中的noteOn()函数。
source.noteOn(0); /* Play sound that will be processed by the GainNode. */
如所提到的,GainNode用于控制声音的音量。 这种类型的对象有一个gain属性,该属性是一个值属性。 gain属性的值用来控制声音的大小。
gainNode.gain.value = 0.5; /* Decrease the volume level by half. */ gainNode.gain.value = 2; /* Increase the volume level two times. */
示例应用程序
让我们来看看本文所附的示例应用程序。 它使用两个库来创建UI,并调用AJAX的函数。 这两个库是jQuery 1.9.0和jQuery Mobile 1.3.0。 该应用程序可以播放各种来源的声音或音乐。 首先,你可以运行应用程序中存储的文件。 在本应用中,文件存放在./sound目录下。 另一个源是在Tizen设备内存中的Music目录下。 最后就是网页上的外部文件。 此外,你还可以改变音量,或者模拟在三维的空间中播放声音。 为了使应用程序简单,我限于它的声源仅移动到听众的左或右侧。 下面的屏幕截图显示了应用程序的主屏幕。
应用程序的主屏幕 |
在应用程序中使用的所有的声音都来自SoundJay网站http://www.soundjay.com/。
我将讨论的代码对于了解网络音频API的部分是必不可少的。 其他部分,如用户界面的实现将被省略。
入口点是一个应用程序模块,该模块有下面几个私有方法和一个公共接口:
- init() - 初始化应用程序;由装载事件调用,
- listFilesInMusicDir() - 列出所有在Music目录下的文件,
- loadSound() - 异步地下载声音,并在下载成功时执行回调函数。
- playSound() - 播放指定名称的声音。
var app = (function () { /* ... */ return { init : _init, listFilesInMusicDir : _listFilesInMusicDir, loadSound : _loadSound, playSound : _playSound }; }()); window.onload = app.init;
这个模块有一些值得注意的私有变量:
- _files - 保存所有文件数据的数组。 每个元素是拥有下列属性的对象:
- name - 内部文件名称,
- uri - 文件的URI或者URL,
- buffer - 解码后的声音缓存。
- _context - 在init()函数中初始化的上下文对象,
- _source - 存储SourceNode,
- _gainNode,_pannerNode - 获取声音的对象。
这些变量大部分都在_init()函数中被初始化了。
_context = new webkitAudioContext(); /* Create context object. */ _source = null; /* Create gain and panner nodes. */ _gainNode = _context.createGainNode(); // createGain() _pannerNode = _context.createPanner(); /* Connect panner node to the gain node and later gain node to the * destination node. */ _pannerNode.connect(_gainNode); _gainNode.connect(_context.destination);
注意路由图的创建。 我们将PannerNode链接到GainNode,稍后,GainNode将被链接到DestinationNode。 稍后,你将会看到,在_playSound()方法中,我们将SourceNode链接到PannerNode来关闭图,并且播放声音。
在初始化期间,我们也会给Gain和Panner节点的属性绑定滑块。
/* onVolumeChangeListener changes volume of the sound. */ onVolumeChangeListener = function () { /* Slider's values range between 0 and 200 but the GainNode's default value equals 1. We have to divide slider's value by 100, but first we convert string value to the integer value. */ _gainNode.gain.value = parseInt(this.value, 10) / 100; }; /* onPannerChangeListener changes sound's position in space. */ onPannerChangeListener = function () { /* setPosition() method takes 3 arguments x, y and z position of the sound in three dimensional space. We control only x axis. */ _pannerNode.setPosition(this.value, 0, 0); };
通过点击不同的UI元素,我们可以下载声音,并在下载完成后自动播放。
app.loadSound(file, function () { app.playSound(soundName); });
现在,我们来重点介绍一下两个函数:_loadSound()和_playSound()。
函数_loadSound有三个参数。 第一个参数是带有名字和uri属性的文件对象。 后两个参数是调用成功或失败时的回调函数。 首先,我们设置默认的参数值,并定义一个空的_file对象数组。 接下来就检查是否已经下载一个相同名字的文件。 如果文件在_file数组中存在,我们就执行回调函数。
/* Check if file with the same name is already in the list. */ isLoaded = false; $.each(_files, function isFileAlreadyLoaded (i) { if (_files[i].name === file.name) { /* Set flag indicating that file is already loaded and stop 'each' function. */ isLoaded = true; return false; } });
如果文件没有被下载过,我们就需要通过发送AJAX请求来获取文件数据。
/* Do AJAX request. */ doXHRRequest = function () { xhr = new XMLHttpRequest(); xhr.open('GET', file.uri, true); xhr.responseType = 'arraybuffer'; xhr.onload = onRequestLoad; xhr.onloadstart = tlib.view.showLoader; xhr.onerror = onRequestError; xhr.send(); };
在获取到文件数据之后,我们使用onRequestLoad功能中的decodeAudioData()函数对数据进行解码,我们将_file数组和文件名称及文件的URI/URL存放到缓存中。 解码完成后,就执行成功的回调函数,如果出错,就执行失败的回调函数。
/* When audio data is decoded add it to the files list. */ onDecodeAudioDataSuccess = function (buffer) { if (!buffer) { errorCallback('Error decoding file ' + file.uri + ' data.'); return; } /* Add sound file to loaded sounds list when loading succeeded. */ _files.push({ name : file.name, uri : file.uri, buffer : buffer }); /* Hide loading indicator. */ tlib.view.hideLoader(); /* Execute callback function. */ successCallback(); }; /* When loading file is finished try to decode its audio data. */ onRequestLoad = function () { /* Decode audio data. */ _context.decodeAudioData(xhr.response, onDecodeAudioDataSuccess, onDecodeAudioDataError); };
正如我们前面所看到的,成功的回调函数其实就是playSound()函数,这里,我们就讨论一下这个函数。 首先我们要检查是否已经有声音在播放。 如果有,我们就要调用noteOff()函数来停止。 这个函数的参数是一个毫秒单位的时间,它表示正在播放的声音会在多长时间后停止。 注意,noteOff()函数在最新的Web Audio API规范中已经不赞成使用了。 在声音停止后,我们就从文件数组中遍历要播放的声音。 要播放声音,我们要做一下几步动作:创建一个SoundNode对象,为SoundObject添加一个缓存,将SourceNode链接到路由图的下一个节点上,最后通过执行noteOn()函数来播放声音。 这个过程示于下面的代码。
_playSound = function (name) { /* Check whether any sound is being played. */ if (_source && _source.playbackState === _source.PLAYING_STATE) { _source.noteOff(0); // stop() _source = null; } $.each(_files, function (i, file) { if (file.name === name) { /* Create SourceNode and add buffer to it. */ _source = _context.createBufferSource(); _source.buffer = file.buffer; /* Connect the SourceNode to the next node in the routing graph * which is the PannerNode and play the sound. */ _source.connect(_pannerNode); _source.noteOn(0); // start() return false; } }); };
总结
我希望本文能帮助你了解Web Audio API的使用及熟悉下载和播放声音的流程。 通过使用Web音频API,可以让你不用应用程序中使用HTML音频元素来播放声音。 你可以在某些动作发生时再播放和操作声音。 这是游戏开发过程中的一个巨大的优势。 现在你应该可以在应用程序中添加声音或制作简单的音乐播放器了。