高级Web Aduio API用法
PUBLISHED
简介
在之前的文章中,我们已经介绍了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;
下面给出的图片显示出所有预定义的波形形状:
我们可以改变的其它属性为:波形的频率(赫兹)和失谐因子(森特)。 失谐是抵消声音频率的过程。 如果你是一个音乐家,你可能熟悉术语“森特”。 对于不知道这个术语的人,我会做一个简单的解释。 一森特是一个半音的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);
示例应用程序
在了解了所有元素的基础知识之后,我们可以来描述该示例应用是如何工作的。 下图显示了应用程序的外观。
应用程序被分成若干部分,它们是:
- 声音文件 - 包含在应用程序中的声音文件的列表。 如果你想试试自己的文件,你必须更换应用程序的“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 = 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。 您可以使用这些知识,写一个声音播放器,音频频谱显示和均衡器。