初探 Web Audio API

隨著 web 技術的蓬勃發展,在網頁裡播放音樂已經可以不需要透過 flash 或 quicktime 等外掛程式輔助,純粹使用 HTML5 的 audio tag 就可以播放音樂,甚至可以透過 W3C 所規範的 Web Audio API 就可以做許多進階的聲音控制和應用,接觸到的時候覺得實在太有趣,一定要寫一些文章在記錄一下。

Web Audio API 參考:http://webaudio.github.io/web-audio-api/

Web Audio API 起手式

要使用 Web Audio API,第一步就是要創建一個 AudioContext,AudioContext 是一個聲音的容器,在 AudioContext 裡,我們可以針對聲音做各種的處理,在裡頭,AudioSourceNode 表示音訊來源,而聲音是以節點的方式 ( AudioNode ) 存在,每個節點都有輸入和輸出,只有來源節點沒有輸入,聲音除了可以用外部的輸入 ( wav、mp3、ogg...等 ),也可以使用振盪器 ( Oscillators ) 來產生,原理就有點像我們彈奏弦樂器,也是利用樂器內的弦震盪來發聲。

既然是初探 Web Audio API,就用一個簡單的例子入手,由於我們在範例裡要用的是 Web Audio API,就一定會用到 AudioContext,所以我們要先來看看瀏覽器有沒有支援 ( 行動裝置目前還不支援 ),一開始先這樣寫,套用有支援的 AudioContext。

var AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new window.AudioContext;

振盪器的屬性

有了裝載聲音的容器之後,接著就用振盪器來發點聲音,先用createOscillator()來產生振盪器,振盪器有四種屬性:

利用這四個屬性,我們就可以這樣做,建立三個音節,讓這三個音節做 detune 偏移,並且只播放兩秒後停止 ( 直接寫在 stop 裡面,要延後播放就寫在 start 裡 ),停止後會彈出一個 alert 告訴我們音樂停了。( 範例:web-audio-api-demo01.html )

var oscillator1 = context.createOscillator();
oscillator1.type = "square";
oscillator1.frequency.value = 440;
oscillator1.detune.value = -400;
oscillator1.connect(context.destination);
oscillator1.start();
oscillator1.stop(2);
oscillator1.onended = function(){alert('stop');};

var oscillator2 = context.createOscillator();
oscillator2.type = "square";
oscillator2.frequency.value = 440;
oscillator2.detune.value = -300;
oscillator2.connect(context.destination);
oscillator2.start();
oscillator2.stop(2);

var oscillator3 = context.createOscillator();
oscillator3.type = "square";
oscillator3.frequency.value = 440;
oscillator3.detune.value = 300;
oscillator3.connect(context.destination);
oscillator3.start();
oscillator3.stop(2);

控制發出的聲音

瞭解怎麼利用 Web Audio Api 發聲之後,接著就是要來控制我們所發出來的聲音,但是在我們要控制聲音之前,必須要先了解「節點 AudioNode」這個概念,在 AudioContext 裡,主要就是音樂節點的處理和控制,換個角度思考,AudioContext 就像是一張畫布,在上頭繪製的圖形,就是我們的音符,把音符串在一起,就變成了一首曲子,整個流程圖如下:

初探 Web Audio API

簡單來說,source 是我們的音訊來源,不論是用剛剛講的 oscillator 來做,或直接播放音訊,總之音訊來源就都必須轉換為 AudioNode,才能夠做進一步的控制,也就是進入「Processing Nodes」的階段,在這個階段我們就有許多的特殊節點模組 ( Nodes Modules ) 可以進行運算,最後就利用「destination」做輸出的動作,中間的每個步驟,我們都會使用「connect」來做連結,這也是為什麼在使用 Web Audio Api 的程式碼裡頭,都會出現許多 connect,同時也是上面的範例最後用了oscillator3.connect(context.destination);的原因。

詳細的 Nodes Modules 會在之後的篇幅介紹,這裏我先用到一個最簡單也是最容易理解的 Nodes Modules:「GainNode 音量節點」,顧名思義這就是拿來控制音量使用的,因為我們已經進入 AudioNode 的階段,所以寫法上就要做點小更動,剛剛我們是直接用 oscillator 去連結 context.destination,這裏因為 oscillator 先連結了 gainNode,所以再來我們是要用 gainNode 去連結 context.destination,下面的範例打開以後,就會先聽到兩秒的標準音量,再來又會聽到兩秒變成原本音量 0.3 倍的聲音。( 範例:web-audio-api-demo02.html )

var AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new window.AudioContext;

var oscillator1 = context.createOscillator();
oscillator1.type = "square";
oscillator1.frequency.value = 440;
oscillator1.detune.value = -400;
oscillator1.connect(context.destination);
oscillator1.start(AudioContext.currentTime+0);
oscillator1.stop(AudioContext.currentTime+2);

var oscillator2 = context.createOscillator();
oscillator2.type = "square";
oscillator2.frequency.value = 440;
oscillator2.detune.value = -400;
gainNode = context.createGain();  // 創建 gainNode
gainNode.gain.value = 0.3;        // 設定音量
oscillator2.connect(gainNode);    // 將聲音連到 gainNode 
gainNode.connect(context.destination); // 播放 gainNode
oscillator2.start(AudioContext.currentTime+2);
oscillator2.stop(AudioContext.currentTime+4);

上面的例子用到了一個新的東西叫做currentTimecurrentTime是從 AudioContext 建立的當下就會產生的時間,時間會不斷的進行直到我們將 AudioContext 移除,因此我們利用currentTime就可以確保聲音播放的時間,一定會在兩秒後或四秒後停止或播放。

做到這邊,其實我們已經可以做一個很簡單的聲音產生器,並且在聲音播放的過程中還可以去調整聲音,下面這個範例,我做了「播放」與「停止」的按鈕,點選播放就會開始發出聲音,停止就會沒有聲音,一開始以為用 start 和 stop 就可以解決,後來發現一個坑,就是「當 OscillatorNode 開始之後,只會執行一次 start 和 stop」,那該如何讓聲音靜止呢?回想一下剛剛我們用了connect連結context.destination來播放,要靜止就只要disconnect即可,類似把喇叭的音源線拔掉就可以,至於其他的程式,大多都是負責按鈕的點擊事件,以及 range 的拖拉事件,在點擊或拖拉的時候,即時改變 type、frequency、detun 和 volume。

第一部份的程式碼為創建 AudioContext 以及宣告一開始的聲音數值,這裏為什麼要先 disconnect 再 connect 呢。如果沒有這樣做,就會發現每做一個動作,聲音就會不斷的累加上去,就會越來越大聲越來越大聲,因此在這邊每做一個動作就要 disconnect,又為了避免還沒有 connect 的時候就 disconnect 會產生錯誤,因此用了一個 p 來做判斷。

var AudioContext = window.AudioContext || window.webkitAudioContext;
var context      = new window.AudioContext;
var oscillator   = context.createOscillator();
var sound,
    type = 'sine',
    frequency = 440,
    detune = 0,
    volume = 1,
    p = 0;

第二部分的程式碼是把畫面上的案扭都根據 id 變成變數:

var startBtn      = document.getElementById('startBtn'),
  stopBtn        = document.getElementById('stopBtn'),
  sineBtn        = document.getElementById('sineBtn'),
  squareBtn      = document.getElementById('squareBtn'),
  sawtoothBtn    = document.getElementById('sawtoothBtn'),
  triangleBtn    = document.getElementById('triangleBtn'),
  frequencyRange = document.getElementById('frequencyRange'),
  showFrequency  = document.getElementById('showFrequency'),
  detuneRange    = document.getElementById('detuneRange'),
  showDetune     = document.getElementById('showDetune'),
  volumeRange    = document.getElementById('volumeRange'),
  showVolume     = document.getElementById('showVolume');

接著就是把一些重複的行為做成函式,讓我們可以重複使用,connect 和 disconnect 都寫在這裡面,還有一些按鈕的樣式也寫在這裡。

var _gain = function(t,f,d,g){
  oscillator.type = t;
  oscillator.frequency.value = f;
  oscillator.detune.value = d;
  gainNodes = context.createGain();
  gainNodes.gain.value = g;  
  oscillator.connect(gainNodes);
  return gainNodes;
}

function _sound(){ if(p == 1){ sound.disconnect(context.destination); sound = _gain(type,frequency,detune,volume); sound.connect(context.destination); } }

function _typeClick(e){  
  sineBtn.style.outline = 'none';
  squareBtn.style.outline = 'none';
  sawtoothBtn.style.outline = 'none';
  triangleBtn.style.outline = 'none';
  e.style.outline = '3px solid #f80'; 
}

按鈕點選事件的程式,因為按鈕較多,這裏列出三個主要的按鈕:

startBtn.addEventListener('click',function(){
  p = 1;
  startBtn.disabled = true;
  stopBtn.disabled = false;
  sound = _gain(type,frequency,detune,volume);
  sound.connect(context.destination);
});

stopBtn.addEventListener('click',function(){
  p = 0;
  startBtn.disabled = false;
  stopBtn.disabled = true;
  sound.disconnect(context.destination);
});

sineBtn.addEventListener('click',function(){
  type = 'sine';
  _typeClick(this);
  _sound();
});

range 的部分,用了oninput

frequencyRange.oninput = function(){
  frequency = this.value;
  showFrequency.innerHTML = frequency;
  _sound();
};

detuneRange.oninput = function(){
  detune = this.value;
  showDetune.innerHTML = detune;
  _sound();
};

volumeRange.oninput = function(){
  volume = this.value;
  showVolume.innerHTML = volume;
  _sound();
};

最後當然就是要讓聲音啟動:

oscillator.start();

小結

到這邊為止,我們已經完成了一個很簡單的聲音產生器,這也是 Web Audio Api 最基本的起手勢,不嫌棄的話就玩玩看吧!( 範例:web-audio-api-demo03.html )

初探 Web Audio API

有興趣瞧瞧其他新文章嗎?