Web Audio API的使用

简介

现代的手机应用和游戏没有不需要音频的。 然而,很长一段时间,开发者在他们的应用中都是依靠外部浏览器的插件来实现声音的。 幸运地,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音频元素来播放声音。 你可以在某些动作发生时再播放和操作声音。 这是游戏开发过程中的一个巨大的优势。 现在你应该可以在应用程序中添加声音或制作简单的音乐播放器了。

文件附件: