Web Audio API 第2章 完美的播放時機控制

Web Audio API 第2章 完美的播放時機控制

相較於

低延時對於遊戲或交互式應用來說非常重要,因爲交互操作時要快速響應給用戶的聽覺。如果響應的不及時,用戶就會察覺到延時,這種體驗相當不好。

在實踐中,由於人類聽覺的不完美,延遲的餘地可達20毫秒左右,但具體延遲多少取決於許多因素。精確的可控時間使得能夠在特定時間安排事件。這對於腳本場景和音樂應用來說非常重要

時間模型

其中一個重要的點是,音頻上下文 AudioContext 提供了一致的計時模型和時間的幀率。重要的是此模型有別於我們常用的 Javascript 腳本所用的計時器 如 setTimeout, setInterval, new Date()。也有別於 window.performance.now() 提供的性能分析時鐘

在 Web Audio API 音頻上下文系統座標中所有你打交道的的絕對時間單位是秒而不是毫秒。當前時間可通過音頻上下文的 currentTime 屬性獲取。同樣它也是秒爲單位,時間存儲爲高精度的浮點數存儲。

精確的播放與復播

在遊戲或其它需要精確時間控制的應用中 start() 方法用於控制安排精確的播放。爲了保證正確運行,需要確保緩衝已提前加載。如果沒有提前緩衝, 爲了 Web Audio API 能解碼,需要等等瀏覽器完成加載音頻文件。如果沒有加載或解碼完畢就去播放或精準的控制播放那麼很有可能會失敗。

start() 方法的第一個參數可用於聲音精確定位控制在哪裏開始播放。此參數是 AudioContext 音頻上下文座標系內的 currentTime, 如果傳參小於 currentTIme, 則它會立即播放。因爲 start(0) 就是直接開始播放的意思 ,如果想要控制延遲 5 秒後播放,則需要 start(context.currentTime + 5)。

聲音的緩衝也可以從特定位置開始播放,使用 start() 方法的第二個參數控制,第三個可選參數用於時長特殊限制。舉個例子,如果我們想暫停後在暫停的位置重新開始恢復播放,我們需要實現記錄聲音在當前 session 播放了多久並追蹤偏移量用於後面恢復播放

start 方法即 AudioBufferSourceNode.start([when][, offset][, duration]);

可參考 https://developer.mozilla.org/zh-CN/docs/Web/API/AudioBufferSourceNode/start

// 假定 context 是網頁 audio context 上下文
var startOffset = 0; 
var startTime = 0;
function pause() {
  source.stop();
  // 計算距離上次播放暫停時過去了多久
  startOffset += context.currentTime - startTime;
}

一旦源節點播放完畢,它無法再重播。爲了重播底層的緩衝區,你需要新建一個新的源節點(AudioBufferSourceNode) 並調用 start():

function play() {
  startTime = context.currentTime;
  var source = context.createBufferSource();
  source.buffer = this.buffer;
  source.loop = true;
  source.connect(context.destination);
  // 開始播放,但確保我們限定在 buffer 緩衝區的範圍內 
  source.start(0, startOffset % buffer.duration);
  
}

儘管重新新建一個源節點看起來非常的低效,牢記,這種模式下源節點被着重優化過了。請記住,如果你在處理 AudioBuffer, 播放同一個聲音你無需重新請求資源。當 AudioBuffer 緩衝區與播放功能被分拆後,就可以實現同一時間內播放不同版本的緩衝區。如果你感覺需要重複這樣的方式調用 ,那麼你可以在將它封裝成一個簡單的方法函數比如 playSound(buffer) 就像在第一章代碼片斷中有提到過的。

以上代碼實現 demo 可參考 examples/ch02/index1.html

規劃精確的節奏

Web Audio API 允許開發人員在精確地規劃播放。爲了演示,讓我們先設置一個簡單的節奏軌道。也許最簡單的要屬廣爲人知的 爵士鼓模式(drumkit pattern) 如圖 2-1,hihat每8個音符演奏一次,kick和snare在四分音符上交替演奏

注: kick 是底鼓,就是架子鼓組裏面最下面最大的那個鼓,聲音是咚咚咚的;

hihat是鼓手左邊兩片合在一起的鑔片 閉鑔 是次次次的聲音,開鑔是擦擦擦的聲音

snare 是離鼓手最*的*放的小鼓,叫軍鼓,打上去是咔咔咔的聲音;

image

假定我們已搞定了 kick, snare,和 hihat 緩衝,那麼代碼實現就比較簡單:

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計時器和事件隊列創建自己的調度器。這種方法在《雙鐘的故事》中有描述

譯者注:《雙鐘的故事》即 《A Tale of Two Clocks》 寓言故事大致告訴人們不能依靠單獨一種方式,需要依靠多種方式方法解決問題

譯者注:具體音樂原理不重要,重要的是反應出可對音頻延時播放, 聽的就是個“動次打次”

以上代碼實現 demo 可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch02/index2.html

更改音頻參數

很多音頻節點類形的參數都是可配的。舉個例子,GainNode 擁有 gain 參數用於控制通過 gain 節點的聲音音量乘數。特別的一點是,參數如果是1則不影響幅度,0.5 降一半,2 則是雙倍。讓我們設置一個:

譯者注: gain節點或稱增益節點通常用於調節音頻信號的音量

  // 創建 gain node.
  var gainNode = context.createGain();
  // 連接  source 到 gain node. 
  source.connect(gainNode);
  // 連接  gain node 至  destination. 
  gainNode.connect(context.destination);

在 context API 中,音頻參數用音頻實例表示。這些值可通過節點直接變更:

// 減小音量
gainNode.gain.value = 0.5;

當然也可以晚一點修改值,通過精確安排在後續更改。我們也可以使用 setTimeout 來延時修改,但它不夠精確,原因有幾點:

  1. 毫秒基數的計時可能不夠精確
  2. 主 JS 進程可能很忙需要處理更高優先級的任務比如頁面佈局,垃圾回收以及其它 API 可能導致延時的回調函數隊列等
  3. JS 計時器可能會受到瀏覽器 tab 的狀態影響。舉個例子,interval 計時器相比於 tab 在前臺運行時,tab 在後臺運行時觸發的更慢

我們可以直接調用 setValueAtTime() 方法來代替直接設值,它需要一個值與開始時間作爲參數。舉個例子,下面的代碼片斷一秒就搞定了 GainNode的 gain 值設置

gainNode.gain.setValueAtTime(0.5, context.currentTime + 1);

以上代碼實現 demo 可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch02/index3.html

漸變的音頻參數

在很多例子中,相較於直接硬生生設置一個參數,你可能更傾向於漸變設值。舉個例子,當開發音樂應用時,我們希望當前的聲音軌道漸隱,然後新的聲音軌道漸入而避免生硬的切換。當然你也可以利用多次調用 setValueAtTime 函數的方法實現類似的效果,但顯然這種方法不太方便。

Web Audio API 提供了一個堆方便的 RampToValue 方法,能夠漸變任何參數。 它們是 linearRampToValueAtTime() 和 exponentialRampToValueAtTime()。兩者的區別在於發生變換的方式。在一些用例中,exponential 變換更加敏感,因爲我們以指數方式感知聲音的許多方面。

讓我們用一個例子來展示交叉變換吧。給定一個播放列表,我們可以在音軌間安排變換降低當前播放的音軌音量並且增加下一條音軌的音量。兩者都發生在當前曲目結束播放之前稍早的時候:

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 (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;
      // 漸入
      gainNode.gain.linearRampToValueAtTime(0, currTime); 
      gainNode.gain.linearRampToValueAtTime(1, currTime + fadeTime);
      // 漸出
      gainNode.gain.linearRampToValueAtTime(1, currTime + duration-fadeTime);
      gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
      // 播放當前音頻.
      source.noteOn(currTime);
      // 爲下次迭代累加時間
      currTime += duration - fadeTime;
    }
  } 
}

譯者注: 原文中的代碼過時了, 實際實現請參考我的 demo 實現

標準 https://webaudio.github.io/web-audio-api/#dom-gainnode-gain

以上代碼實現 demo 可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch02/index4.html

定製時間曲線

如果線性曲線和指數曲線都無法滿足你的需求,你也可以自己定製自己的曲線值通過傳遞一個數組給 setValueCurveAtTime 函數實現。有了這個函數,你可以通過傳遞數組實現自定義時間曲線。它是創建一堆 setValueAtTime 函數調用的快捷調用。舉個例子,如果我們想創建顫音效果,我們可以通過傳遞振盪曲線作爲 GainNode 的 gain 參數值,如圖 2-2

image

上圖的振盪曲線實現代碼如下:


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);

上面的代碼片斷我們手動計算出了正弦曲線並將其設置到 gain 的參數內創造出顫音效果。好吧,它用了一點點數學..

這給我們帶來了 Web Audio API 的一個非常重要的特性, 它使得我們創建像顫音這樣的特效變的非常容易。這個重要的點子是很多音頻特效的基礎。上述的代碼實際上是被稱爲低頻振盪(LFO)效果應用的一個例子, LFO 經常用於創建特效,如 vibrato 震動 phasing 分隊 和 tremolo 顫音。通過對音頻節點應用振盪,我們很容易重寫之前的例子:

// 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);

createOscillator https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode

相比於我們之前創建的自定義曲線後面的代碼要更高效,重現了效果但它幫我們省了用手動循環創建正弦函數

以上振盪器節點的代碼實現 demo 可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch02/index5.html


注:轉載請註明出處博客園:王二狗Sheldon池中物 ([email protected])

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章