第二章 完美的时间模型和时延
网页音频API比<audio>标签更强大的一处在于它有一个低延迟、精准的时间模型。
对于游戏和交互式应用来说,低延迟是非常重要的,因为你通常需要非常快的用声音来响应用户操作。如果这个反馈不是即时的,用户会感觉到这个延迟,这是不让人满意的。实际上,由于人类听觉的缺陷,延迟的误差可高达20毫秒,不过这个数字还会随着很多其他的因素来改变。
精准的时间模型使你能够把事件安排在未来特定的时间。这对用脚本控制(播放)的场景和音乐应用来说非常有用。

时间模型
音频上下文的一个关键点在于它提供了一个连贯的时间模型和时间参照框架。重要的是,这个模型与JavaScript时间控制器,比如说setTimeout、setInterval、和new Date()不同。它也和window.performance.now()提供的性能时钟不同。
在特定的音频上下文坐标系中,你将在网页音频API中使用的所有绝对时间都是使用秒作为单位(不是毫秒)。当前时间可以从音频上下文的currentTime属性中获得。尽管时间使用的单位是秒,但是却是以高精度的浮点值来存储的。

精确的播放和暂停
在游戏和其他对时间要求苛刻的应用中,start()函数让声音播放的精确调度变得简单。为了获得工作时的属性,得先确定你的声音缓冲是已经预加载了的。[详勘加载和播放声音]如果没有预加载的缓冲,你将等待一段时间让浏览器取到声音文件,然后网页音频API来解码它。在这种情况下失败的状况是你想立马播放一段声音但缓冲却在加载和解码中造成的。
你可以在调用start()函数时指定第一个参数来设置声音在一个精确的时间之后播放。这个参数在AudioContext的currentTime所在的坐标系中。如果这个参数比currentTime小,它将马上播放。因此,start(0)总是马上播放声音。但是要把声音设置在5秒后,你需要调用start(context.currentTime + 5)。
声音缓冲也可以指定在特定的时间偏移来播放,这需要在调用start()时指定第二个参数,并且用第三个可选的参数限定播放时间长度。例如,如果你想暂停声音然后从之前暂停的位置开始播放,我们可以实现一个pause函数,用这个函数记录当前上下文中已经播放的时间长度和最后的播放时间偏移:
// Assume context is a web audio context, buffer is a pre-loaded audio buffer.
var startOffset = 0;
var startTime = 0;

function pause() {
source.stop();
// Measure how much time passed since the last pause.
startOffset += context.currentTime - startTime;
}
一旦一个源节点完成了播放,它就不能回头来播放了。要回播这段缓冲,你需要创建一个新的源节点(AudioBufferSourceNode)并且掉用start()函数:
function play() {
startTime = context.currentTime;
var source = context.createBufferSource();
// Connect graph
source.buffer = this.buffer;
source.loop = true;
source.connect(context.destination);
// Start playback, but make sure we stay in bound of the buffer.
source.start(0, startOffset % buffer.duration);
}
尽管一开始觉得重新创建一个源节点似乎效率低下,但要记住源节点在这种模式下是高度优化的。要记住如果你有了一个AudioBuffer的句柄,你就不需要再重新请求一次同样的声音资源。有了AudioBuffer,你就可以清楚的分开缓冲数据和播放器,并且可以轻松同时重叠的播放多个不同段的声音缓冲。如果你需要重复这种模式,你只需要用一个类似于之前代码片段中的playSound(buffer)帮助函数来封装播放即可。

精准的节奏调度
网页音频接口使得开发者可以精确调度播放。为了阐释这个特性,我们先设置一段简单的节奏音轨。图示2-1所示的架子鼓范例可能是最简单的,但是确实最有名的:每8分音符一个踩擦,没4分音符交替的大鼓和军鼓,使用的拍号是4/4拍。


假设我们已经加载了大鼓和军鼓和踩擦的缓冲,播放的代码代码如下所示:
for (var bar = 0; bar < 2; bar++) {
var time = startTime + bar * 8 * eighthNoteTime;
// Play the bass (kick) drum on beats 1, 5
playSound(kick, time);
playSound(kick, time + 4 * eighthNoteTime);

// Play the snare drum on beats 3, 7
playSound(snare, time + 2 * eighthNoteTime);
playSound(snare, time + 6 * eighthNoteTime);

// Play the hihat every eighth note.
for (var i = 0; i < 8; ++i) {
playSound(hihat, time + i * eighthNoteTime);
}
}
一旦你调度了的声音,你就没有办法取消未来的播放。所有如果你正在做一个快速改变的应用,把声音调度得太远是不合适的。处理这种问题的一个好办法是用JavaScript的时间控制器和事件队列创建你自己的调度器。这种方法在“两个时钟的故事”中有描述。

改变音频参数
许多不同类型的音频节点都有可配置的参数。例如,GainNode有一个增长参数来控制增长因子,这个增长因子影响所有通过该节点的声音。明确的说,增长因子为1不会影响声音响度,为0.5会使其降为一半,为2会使其增倍。[详勘“音量、增长因子、响度”]让我们建立如下音频图:
// Create a gain node.
var gainNode = context.createGain();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);
在API的上下文中,音频参数是AudioParam的实例。这些节点的值可以直接设置参数实例的value属性来改变。
// Reduce the volume.
gainNode.gain.value = 0.5;
这些值也可以在后续通过精确的调度使参数改变。我们将使用setTimeout来调度,但是这种方式是不精确的,有如下原因
1、基于毫秒的时间可能不够精确
2、主JS线程可能正忙于处理其他高优先级的任务比如页面渲染、垃圾回收和其他API的回调,这将导致延迟。
3、JS时间控制器可能被页面标签的状态影响。例如,相比较于当前状态下的标签而言,在后台运行状态下的间隔时间控制器会调用的慢一些。
我们调用setValueAtTime()函数而不是直接设置值,这个函数需要一个需要设置的值和一个开始时间作为参数。例如,下面的代码片段在1秒后设置一个GainNode增长值:
gainNode.gain.setValueAtTime(0.5, context.currentTime + 1);

逐渐改变音频参数
在很多情况下,你可能更需要逐渐改变参数而不是突然改变。例如,在做一个音乐播放应用的时候,我们想让当前音轨逐渐停止,然后让一个新的音轨逐渐播放,来避免刺耳的转变。当你通过调用很多遍之前提到的setValueAtTime函数来实现时,你会发现这样做非常不方便。
网页音频API提供了一个便捷的RampToValue方法集合来逐渐改变任何参数的值。这个函数是linearRampToValueAtTime()和exponentialRampToValueAtTime()。这两个函数在差别在于切换的方式。在某些情况下,指数改变更好,因为我们发现很多声音都是用的指数方式。
我们拿一个交互式渐变的调度作为例子。给定一个播放列表,我们把音轨之间的切换以如下方式调度:一个增长因子减小、接下来播放的音轨的增长因子增大,两个调度都在当前音轨结束前一小段时间时开始。
function createSource(buffer) {
var source = context.createBufferSource();
var gainNode = context.createGainNode();
source.buffer = buffer;
// Connect source to gain.
source.connect(gainNode);
// Connect gain to destination.
gainNode.connect(context.destination);

return {
source: source,
gainNode: gainNode
};
}

function playHelper(buffers, iterations, fadeTime) {
var currTime = context.currentTime;
for (var i = 0; i < iterations; i++) {
// For each buffer, schedule its playback in the future.
for (var j = 0; j < buffers.length; j++) {
var buffer = buffers[j];
var duration = buffer.duration;
var info = createSource(buffer);
var source = info.source;
var gainNode = info.gainNode;
// Fade it in.
gainNode.gain.linearRampToValueAtTime(0, currTime);
gainNode.gain.linearRampToValueAtTime(1, currTime + fadeTime);
// Then fade it out.
gainNode.gain.linearRampToValueAtTime(1, currTime + duration-fadeTime);
gainNode.gain.linearRampToValueAtTime(0, currTime + duration);

// Play the track now.
source.noteOn(currTime);

// Increment time for the next iteration.
currTime += duration - fadeTime;
}
}
}

自定义时间曲线
如果线性和指数曲线不满足你的需求,你也可以通过给setValueCurveAtTime一个值数组来指定你自己的值曲线。有了这个函数,你通过提供一个时间值来自定义时间曲线。这是调用很多次setValueAtTime的便捷方法,而且也应该在这种情况下被使用。例如,如果我们想做一个颤音效果,我们可以给一个GainNode的增长AudioParam应用一个震荡曲线。如图示2-2


前述震荡曲线可以用以下代码来实现:
var DURATION = 2;
var FREQUENCY = 1;
var SCALE = 0.4;

// Split the time into valueCount discrete steps.
var valueCount = 4096;
// Create a sinusoidal value curve.
var values = new Float32Array(valueCount);
for (var i = 0; i < valueCount; i++) {
var percent = (i / valueCount) * DURATION*FREQUENCY;
values[i] = 1 + (Math.sin(percent * 2*Math.PI) * SCALE);
// Set the last value to one, to restore playbackRate to normal at the end.
if (i == valueCount - 1) {
values[i] = 1;
}
}
// Apply it to the gain node immediately, and make it last for 2 seconds.
this.gainNode.gain.setValueCurveAtTime(values, context.currentTime, DURATION);
在前面的代码片段中,我们已经手动计算了一个正弦曲线并把它赋值给增长参数来创造一个颤音效果。而且它使用了很多数学知识。
这是一个网络音频API非常有用的特性,它让我们可以更加简单的建立类似于颤音的效果。我们可以创建任意的音频流,将其与其他AudioNode连接而不是连接到AudioParam。这是创建许多声音特效的重要的基础。前述代码实际上是一个叫做低频振荡器(LFO)的例子,把它应用在增长节点上可以创造颤音、逐步和滑音等效果。使用振荡器节点[详勘“基于振荡器的直接声音合成”],我们可以简单重构之前的例子,如下所示:
// Create oscillator.
var osc = context.createOscillator();
osc.frequency.value = FREQUENCY;
var gain = context.createGain();
gain.gain.value = SCALE;
osc.connect(gain);
gain.connect(this.gainNode.gain);

// Start immediately, and stop in 2 seconds.
osc.start(0);
osc.stop(context.currentTime + DURATION);
后一种方法比我们自定义值更加高效,同时它还让我们避免了写正弦函数和创造循环来重复效果的繁琐。

posted on 2016-02-25 18:14  iStartan  阅读(294)  评论(0编辑  收藏  举报