高级Web Aduio API用法

简介

在之前的文章中,我们已经介绍了W3C Web音频API的基本信息,这些API的说明书还在起草阶段,Tizen已经从2011年10月15日开始编写第一版的说明书了。 本文介绍如何从内部内存和外接源加载声音,播放声音和控制音量。 本文包含关于如何使用提到的函数以及利用PannerNode在三维空间播放声音的实现细节, 本文中,我们将研究节点的主题。 我将展示如何使用内置的过滤器,如何合成声音,如何可视化声谱等等。 因此,你可以写一些更高级的声音应用,不仅可以播放声音,也可以创建和修改声音。

节点描述

在本节中,我将介绍可用于创建,修改和可视化声音的节点。 更多详细信息请参考W3C Web Audio API说明书

振荡器节点

振荡器节点是0输入,这意味着它总是在一个音频源路由图的第一个节点。 它会产生一个周期性波形。 要创建一个振荡器节点,我们必须调用上下文函数“createOscillator”。

var context = new webkitAudioContext();
var oscillator = new context.createOscillator();

我们可以按照我们的需求对节点进行设置。 首先,我们有五种不同的波形:正弦波(0),正方形波(1),锯齿波(2),三角形波(3)和自定义波(4)。 最后一个将在后面说明。 我们通过设置节点的“type”属性更改波形类型。

oscillator.type = oscillator.SINE;
oscillator.type = oscillator.TRIANGLE; 

下面给出的图片显示出所有预定义的波形形状:

Fig.1:预定义波形

我们可以改变的其它属性为:波形的频率(赫兹)和失谐因子(森特)。 失谐是抵消声音频率的过程。 如果你是一个音乐家,你可能熟悉术语“森特”。 对于不知道这个术语的人,我会做一个简单的解释。 一森特是一个半音的1/100,一个半音是一个八度的1/12。 一个八度是在钢琴键盘上的两个连续的C音之间的距离。 了解了声音合成相关的技术细节,通过使用多个波形并且让它们失谐,您可以合成任何乐器。

oscillator.frequency.value = 440; // Set waveform frequency to 440 Hz
oscillator.detune.value = Math.pow(2, 1/12) * 10; // Offset sound by 10 semitones

对于更复杂的波形,你可以使用“creaateWaveTable”和“setWaveTable”上下文函数创建你自己的波形,并且设置振荡节点的类型为“custom”。 这不是本文的主题,但如果你想阅读更多关于它的内容,我建议您参考该文档

双二阶过滤节点

这是一个功能强大的节点,可以用来控制基本音调(低音,中,高音),或创建一个均衡器,这将在本文中包含的示例应用程序中显示。 您可以用多个双二阶过滤节点来创建更复杂的过滤器。 一般来说,它所做的事情是获取频率或者某个频率范围,对它们进行增强或削弱。 要创建此类型的节点,只需要调用‘createBiquadFilter’函数。

var filter = context.createBiquadFilter();
// Connect source node to the filter
source.connect(filter);

此节点有几个属性来控制其行为:类型,频率,增益和品质。 浏览这个网页,http://webaudio-io2012.appspot.com/,你可以播放它们,看看它们是如何影响声音的频谱的。

类型属性可以是下列之一:0(低通),1(高通),2(带通),3(低架),4(高货架),5(峰值),6(缺口), 7(全通)。 这些是我们要用到的滤波器的类型。 每种类型的详细解释可以在W3C的Web Audio API规范中找到。

filter.type = filter.LOWPASS; // 0
filter.type = filter.PEAKING; // 5

频率属性控制了哪些频率或频率范围会被滤波器影响。 有些类型的滤波器只是提升或衰减某一频率及其周围的频率。 例如,低通滤波器可以使频率在0到指定值之间修改。

filter.frequency.value = 440; // in Hertz

质量和增益特性并常用。 有些滤波器只使用质量属性或增益属性。 熟悉滤波器影响声音频谱的最佳方法可以在前面提到的页面中找到,并且能观察到波形随属性的变化。 通常它们控制多少声谱应当增强或减弱,以及周边频率的哪些范围受影响。 下表列出了哪些属性在哪些滤波器中使用。

滤波器 质量 增益
低通
高通
带通
低架
高架
高峰
缺口
全通
// In peaking filter both parameters are used
filter.type = filter.PEAKING;
filter.Q.value = 2; // Quality parameter
filter.gain.value = 10;

// In low pass filter only quality parameter is used
filter.type = filter.PEAKING;
filter.Q.value = 15;

分析器节点

我们可以用这个节点实时地使声谱可视化。 我们有两种方法来获取和显示声音数据。 频域的方法是相应频率的声音功能的分析,而在时域是相应时间的分析。 为了获得当前的声音数据,我们可以调用节点三个函数中的一个:getFloatFrequencyData,getByteFrequencyData,getByteTimeDomainData。 正如你所看到的,前两个的区别仅在于数据的类型。 我们必须传递一个给定类型的数组作为这些函数的第一个参数,如下面代码所示。

var analyser = context.createAnalyser();

// Connect source node to the analyser
source.connect(analyser);

// Create arrays to store sound data
var fFrequencyData = new Float32Array(analyser.frequencyBinCount);
var bFrequencyData = new Uint8Array(analyser.frequencyBinCount);

// Retrieve data
analyser.getFloatFrequencyData(fFrequencyData);
analyser.getByteFrequencyData(bFrequencyData);
analyser.getByteTimeDomainData(bFrequencyData);

我们创造了Float32Array和Uint8Array两种类型对象,它们是HTML5的新元素。 它们是视图(数组),可以封装Array或者ArrayBuffer,转换它们的元素为给定的类型:32位浮点数或8位无符号整数。 数组大小应等于该节点的“frequencyBinCount”参数,但是如果数组小了,则多余的元素将被忽略。 我们用数据填充视图(数组),现在可以用于可视化声音。

for (var i = 0; i < bFrequencyData.lenght; i++) {
    // Do something with sound data
    bFrequencyData[i];
}

数据可视化的过程将在示例应用程序部分详细描述。

手动缓存数据的创建

声音是不同频率波形的组合,声音数据是表示该组合值的数组。 在大多数情况下,您会从文件加载声音数据,但有时也可能逐字节地创建声音数据。 我将描述噪音创建的过程,但是如果你有音效合成的知识,你可以尝试创建自己的波形函数,并结合他们。

这个过程从创建一个AudioBuffer对象开始。 您可以通过调用上下文对象的“createBuffer”函数创建它。 它有三个参数:

  • 声道数 - 1为单声道,2为立体声等。
  • 缓冲区长度 - 确定你要提供多少个采样帧,
  • 采样率 - 决定了每秒被播放的采样帧。 采样率决定了声音的质量。 一般来说,它应该大于该信号的最大频率的两倍进行采样。 在这个条件下,如果最高频率为22050赫兹,我们应该将其设置为44100赫兹。
// Create buffer with two channels
var buffer = context.createBuffer(2, 44100, 44100);

创建缓冲区之后,我们要通过调用缓冲区的“getChannelData”函数,传递信道数(从0开始)作为参数,来获取信道的数据。

var leftChannelData = buffer.getChannelData(0);
var rightChannelData = buffer.getChannelData(1);

现在,我们可以循环遍历缓冲区,并设置每个缓冲区的元素。 该信道的数据是在缓冲(ArrayBuffer)区顶部的视图(Float32Array)。 每个缓冲区的元素应该是-1和1之间的值。 正如你可能知道的,声音是一种空气振动。 振动的越快(越频繁),我们听到的声音越高。 如果缓冲区长度里设置同样一个值来表示一个正弦振幅,而稍后表示两个振幅,按么第二个将被视为具有较高音调(更高的声音)。

var data = buffer.getChannelData(0);
for (i = 0; i < data.length; i++) {
    data[i] = (Math.random() - 0.5) * 2; // Noise
    // In following lines I've presented various functions generating sound data (tones).
    // data[i] = Math.sin(1  * 180 * (i / data.length)); // One waveform oscillation
    // data[i] = Math.sin(2  * 180 * (i / data.length)); // Two waveform oscillations
    // data[i] = Math.sin(4  * 180 * (i / data.length)); // Four waveform oscillations
    // data[i] = Math.sin(8  * 180 * (i / data.length)); // Eight waveform oscillations
    // data[i] = Math.sin(16 * 180 * (i / data.length)); // Sixteen waveform oscillations
}

最后,缓冲区准备好之后,我们可以在SourceNode结点使用它并播放。

var buffer = context.createBuffer(1, 44100, 44100);
var data = buffer.getChannelData(0);
for (i = 0; i < data.length; i++) {
    /* Prepare data */
}

// Create source node
var source = context.createBufferSource();
source.loop = true; // Make sure that sound will repeat over and over again
source.buffer = buffer; // Assign our buffer to the source node buffer

// Connect to the destination and play
source.connect(context.destination);
source.noteOn(0);

示例应用程序

在了解了所有元素的基础知识之后,我们可以来描述该示例应用是如何工作的。 下图显示了应用程序的外观。

图2:应用程序截图

应用程序被分成若干部分,它们是:

  • 声音文件 - 包含在应用程序中的声音文件的列表。 如果你想试试自己的文件,你必须更换应用程序的“sound”目录下的相应文件,
  • 合成的声音 - 2个合成声音,一个使用了OscillatorNode,第二是人工填充的AudioBuffer,
  • 音量控制 - 音量的控制,
  • 播放速率 - 控制声音播放速度,
  • 滤波器/均衡器 - 是声音处理部分,在这里你可以在滤波器和均衡器面板中进行选择,或禁止任何声音修改。 当选择该选项时,面板的视图会改变,更多的选项就会出现。 在“Filter”面板中,您可以尝试每个双二阶滤波器,并更改其设置。 在“equalizer”面板中,您可以放大或衰减给定的频率等级。

在该应用程序的底部,有一个面板,显示可视化的音频频谱。 当用户播放一个声音,你可以通过点击出现在音频频谱面板的停止按钮来停止。 让我们来看看应用程序的最重要的部分。

该应用程序使用的jQuery(版本1.8.2)库和jQuery Mobile(版本1.3.0)库,它们的文件都包含在首部。 此外,它还是用了Tizen Lib库,尤其是Tizen Lib库中的三个模块:

  • 日志 - 用于记录错误,告警等,
  • 网络 - 用于检查网络连接是否可用,
  • 视图 - 显示了jQuery Mobile的装载器。

我不打算介绍如何加载和播放声音的工作,因为这些已经在前面讲解Web Audio API基础的文章中讲解过了。 在阅读这块之前你应该先熟悉以前的文章。 

该应用程序的核心是main.js文件和“api”模块,它具有公共的接口,与以前的应用程序几乎是一样的。 注释描述的很清楚,但是有两个功能没有在前面的应用中呈现。 “stopSound”只是停止播放任何声音,“generateAndPlaySound”使用其中一种方法产生声音。 我们将在以后讨论细节。

重要的私有功能

_createRoutingGraph()函数

在以前的应用程序中,播放声音的时候,我们只是以一种方式连接节点。 在此应用中,路由图可以根据我们选择什么样的选项而改变。 所以,我们将负责创建路由图的逻辑移动到独立的函数中。

_createRoutingGraph = function () {
    if (!_source) {
        return;
    }

    /* First disconnect source node form any node it's connected to.
     * We do it to make sure there is only one route in graph. */
    _source.disconnect(0);

    switch (_option) {
        case 'filters':
            _source.connect(_filterNode);
            break;
        case 'equalizer':
            _source.connect(_equalizerNodes[0]);
            break;
        case 'disabled':
            _source.connect(_gainNode);
            break;
    }
};

正如你可以在上面代码中看到,我们根据“_option”变量的值用合适的节点连接源节点。 正如我之前写的,你可以从三个选项中进行选择:滤波器,均衡器或禁用。 不像先前的应用,现在源节点被放置在“API”模块的范围。 我这样做,是因为我们希望任何时刻都能有一个源节点的引用,例如停止播放的声音。 当有声音要播放并且用户改变了他/她想要的效果(滤波器/均衡器/禁用),“createRoutingGraph”函数应该被执行;

_isSoundPlaying()函数

这是顺序函数,首先检查是否有任何源节点已被创建。 如果是,那么我们检查源的“playbackState的属性。 如果它等于PLAYING_STATE变量的值,这意味着声音正在播放。

_isSoundPlaying = function () {
    return _source && _source.playbackState === _source.PLAYING_STATE;
};

_setPlaybackRate()函数

此函数增加或减少声音回放的速度。 默认值是1,表示正常速度。 0表示该声音已经停止/暂停。 我们首先要检查是否有声音正在播放,如果有,我们检查“playbackRate”属性是否存在,这个属性不是在所有源节点点中都存在。 OscillatorNode,源节点不具有该属性。 最后,我们设置一个新值。

_setPlaybackRate = function (val) {
    /* Changing playback rate  */
    if (_isSoundPlaying()) {
        /* We have to check existence of playbackRate property in case of
         * OscillatorNode in which we can change that value. */
        if (_source.hasOwnProperty('playbackRate')) {
            _source.playbackRate.value = val;
        }
    }
};

_playSound()函数

在函数的开始,我们在屏幕中间显示jQuery Mobile加载器来通知用户声音处理已经开始。 我们遍历所有已加载的文件,获取与指定名字匹配的文件。 接下来,我们停止正在播放的声音。 我们创建了一个SourceNode并把它放在私有“_Source”变量,这个变量在整个“api”模块是可见的。 后来,我们要创建一个路由图并调用“noteOn()”函数播放声音。 最后,我们隐藏了加载器并调用“setPlaybackRate()”函数来确保播放速度与目前的播放速率滑块的值相匹配。 有可能有一种情况,就是当没有声音播放的时候,用户改变了回放速率,我们必须要对这种情况进行处理。 当“_Source”变量为空,我们无法做到。

_playSound = function (name) {
    tlib.view.showLoader();

    /* Look for the sound buffer in files list and play sound from that buffer. */
    $.each(_files, function (i, file) {
        if (file.name === name) {
            _stopSound();

            /* Create SourceNode and add buffer to it. */
            _source = _context.createBufferSource();
            _source.buffer = file.buffer;
            /* Connect nodes to create routing graph. */
            _createRoutingGraph();
            _source.noteOn(0); // start()
            /* Set playback rate to a new value because it could be changed
             * while no sound was played. */
            _setPlaybackRate($('#playback-rate').val());
            tlib.view.hideLoader();

            return false;
        }
    });
};

_stopSound()函数

我们检查是否有任何声源数据,并调用“noteOff()”函数。 后来我们分配一个空值给“_Source”变量来处理目前的源节点。

_stopSound = function () {
    /* Check whether there is any source. */
    if (_source) {
        _source.noteOff(0); // stop()
        _source = null;
    }
};

声音合成

声音合成发生在“_generateAndPlaySound()”函数中。 它需要用函数名作为参数来产生声音。 首先,我们要检查是否存在存储在“app”模块中的私有“_generationMethods'对象具有给定名称的函数。 如果是,我们必须停止正在播放的任何声音。 接下来,我们调用特定的方法,创建一个路由图并且播放声音。 我们要做的最后一件事是设置回放速度的新值 - 同样的事情,我们在“_playSound()”函数做过。

_generateAndPlaySound = function (name) {
    if (_generationMethods.hasOwnProperty(name)) {
        _stopSound();

        /* Generate sound with the given method. */
        _source = _generationMethods[name]();
        /* Connect nodes to create routing graph. */
        _createRoutingGraph();
        _source.noteOn(0); // start()
        /* Set playback rate to a new value because it could be changed
         * while no sound was played. */
        _setPlaybackRate($('#playback-rate').val());
    }
};

让我们来描述存储在“_generationMethods”对象的两种生成方法。

振荡器(_generationMethods.oscillator)

振荡器的方法很简单,但我们只使用OscillatorNode的基本属性。 如我在这篇文章的开头所写,如果您精通声音合成技术,你可能会利用所有的振荡器的函数。此处我设定振荡器的波型为“triangle”,设置频率为100 Hz和失谐400分。 该方法返回所创建的源节点。

_generationMethods.oscillator = function generateSoundWithOscillator() {
    var source;

    source = _context.createOscillator();
    source.type = source.TRIANGLE; /* 0 - Sine wave, 1 - square wave, 2 - sawtooth wave, 3 - triangle wave */
    source.frequency.value = 100;
    source.detune.value = 400;

    return source;
};

手动(_generationMethods.manual)

手动方法使用一个缓冲区,我们填充数据到这个缓冲区来产生噪声。 我们设置缓冲区的大小为44100,其回放速率为44100,这意味着所有的44100样本帧会在一秒钟内播放。 我们设定“createBuffer()”函数的第一个参数为1,它是指1个通道(单声道)。 我们得到这个通道数据。

buffer = _context.createBuffer(1, 44100, 44100);
data = buffer.getChannelData(0);

我们接下来要做的事情就是填充缓冲区。 我们遍历所有元素,并应用到每一个函数的结果:

(Math.random() - 0.5) * 2。

在“random()”函数产生0到1之间的随机数,一个采样帧的值可以有-1和1之间的值,所以我们希望扩展随机值到采样帧范围。 假如您假象“random()”函数是在X/Y坐标系中的图,第一件事就是将图下移0.5个单位,然后将该数值乘以2,将它们扩展到<-1;1>的范围。

for (i = 0; i < data.length; i++) {
    data[i] = (Math.random() - 0.5) * 2;
}

接下来,我们创建了一个SourceNode并设置所创建数据作为它的缓冲区。 我们也必须设置一个循环属性为true来播放超过一秒钟的声音。

source = _context.createBufferSource();
source.loop = true;
source.buffer = buffer;

声音可视化

如我在这篇文章的开头所写,声音可视化可以使用AnalyserNode创建。 我们必须调用获取声音数据的三大功能之一。 我们没有能够绑定的监听器,所以我们必须在一个指定的时间间隔后手动触发数据函数。 要做到这一点,当程序启动时我们启用一个定时器

_startTimer = function () {
    /* Draw sound wave spectrum every 10 milliseconds. */
    _timer = setInterval(_timerFunction, 10);
};

当应用程序被关闭时停止它。 

_stopTimer = function () {
    /* Reset timer for drawing sound wave spectrum. */
    if (_timer) {
        clearInterval(_timer);
        _timer = null;
    }
};

/* ... */

/* onAppExitListener stops visualization timer and exits application. */
onAppExitListener = function () {
    _stopTimer();
    tizen.application.getCurrentApplication().exit();
};

“_timerFunction”每10毫秒被调用一次。 它重绘声谱,并检查是否有任何声音在播放。 如果是的话,它会在频谱显示“stop”按钮,否则隐藏它。

_timerFunction = function () {
    _draw();
    if (_isSoundPlaying()) {
        $('#stop').show();
    } else {
        $('#stop').hide();
    }
};

频谱绘图过程发生在“_draw()”函数中。 要绘制的音频频谱,我们使用了canvas元素。 我们不能为在频谱的所有频率显示频谱条,因为在屏幕上没有有足够的空间。 我们要忽略一些频率。 在绘制函数中,我们需要使用一些变量来计算有能显示多少条(barCount)。 首先,我们指定一个条的宽度(barWidth)和相邻之间的间距(barSpacing),有了屏幕宽度(width),我们便可以计算出这些条的总数了。 接下来,在进行for循环的时候,我们必须要知道需要省略buffer中多少个元素。

此外,我们根据声音大小给频谱条上颜色。 高强度会使频谱条微红,低强度会使频谱条偏绿。 我用了HSL颜色模型,因为它可以只改变颜色,并保持亮度和饱和度处于同一水平。 唯一剩下要做的就是确定频谱条在画布上的位置。

_draw = function () {
    var canvas, context, width, height, barWidth, barHeight, barSpacing, frequencyData, barCount, loopStep, i, hue;

    canvas = $('canvas')[0];
    context = canvas.getContext('2d');
    width = canvas.width;
    height = canvas.height;
    barWidth = 10;
    barSpacing = 2;

    context.clearRect(0, 0, width, height);
    frequencyData = new Uint8Array(_analyserNode.frequencyBinCount);
    _analyserNode.getByteFrequencyData(frequencyData);
    barCount = Math.round(width / (barWidth + barSpacing));
    loopStep = Math.floor(frequencyData.length / barCount);

    for (i = 0; i < barCount; i++) {
        barHeight = frequencyData[i * loopStep];
        hue = parseInt(120 * (1 - (barHeight / 255)), 10);
        context.fillStyle = 'hsl(' + hue + ',75%,50%)';
        context.fillRect(((barWidth + barSpacing) * i) + (barSpacing / 2), height, barWidth - barSpacing, -barHeight);
    }
};

滤波器和均衡器

滤波器很容易实现。 它只是一个BiquadFilterNode,有一些滑块连接到它的属性中。 我们可以改变滤波器类型,频率,质量和增益特性。

我们需要多注意下均衡器。 它由6个 BiquaFilderNode组成,每个BiquaFilderNode连接到图形中的一条线。

_equalizerNodes = [
    _context.createBiquadFilter(),
    _context.createBiquadFilter(),
    _context.createBiquadFilter(),
    _context.createBiquadFilter(),
    _context.createBiquadFilter(),
    _context.createBiquadFilter()

];

_equalizerNodes[0].connect(_equalizerNodes[1]);
_equalizerNodes[1].connect(_equalizerNodes[2]);
_equalizerNodes[2].connect(_equalizerNodes[3]);
_equalizerNodes[3].connect(_equalizerNodes[4]);
_equalizerNodes[4].connect(_equalizerNodes[5]);
_equalizerNodes[5].connect(_gainNode);

每个滤波器的类型均设置为PEAKING。 如果我们将质量值设置为2,那么我们可以通过均衡器滑块操作的唯一属性是“gain”。 我们可以在-50和50之间的修改增益值,衰减或提升给定的频率。 说到频率,我们不得不提到为何连续元素使用这样的值,而不是其他的。 这是因为频率频谱中存在一个对数标度。

_equalizerNodes[0].frequency.value = 50;
_equalizerNodes[1].frequency.value = 160;
_equalizerNodes[2].frequency.value = 500;
_equalizerNodes[3].frequency.value = 1600;
_equalizerNodes[4].frequency.value = 5000;
_equalizerNodes[5].frequency.value = 20000;

总结

本文中涉及的主题介绍了先进的声音处理和合成。 它说明了如何在高级任务中使用Web Audio API。 您可以使用这些知识,写一个声音播放器,音频频谱显示和均衡器。

 

文件附件: