音調與頻域
此章中如果對音樂部分不感興趣,可忽略
程式碼部分也沒有更多的新 api ,重要的還是相關的物理與聲音的相關知識
到目前為止我們已經學過了聲音的基礎屬性:定時與音量。為了能處理更復雜的的情況,例如聲音的均衡(比如,增加低音和降低高音),我們需要更復雜的工具。此章節將介紹一些用於更有趣的轉換工具,這些工具能模擬不同的聲音環境並且可以直接使用 JavaScript 操作。
重要理論:音調基礎
音樂是由多種聲音同時播放所構成的。由樂器產生的聲音可以非常的複雜,因為聲音透過樂器的各個部分反射,並以獨特的方式形成。然而這些音調都有共同的特徵:從物理上講,它們都是週期性的波形。這些週期波形在我們耳朵聽來就是音調。衡量音的音調由頻率決定,每秒重複波形的次數被叫作赫茲。頻率是波峰之間的時間差(單位為秒)。如圖 4-1 所示,如果我們時間維度減半,我們最終得到的是相應加倍的頻率,同樣的音色,高一個八度。與之相反,如果我們將頻率延展兩倍,這會使音色降低一個八度。所以,我們的耳朵感知到的音調(就像音量)是指數級地:每一個八度,頻率翻一倍。
注:八度音,是指兩個音之間的音高距離,其中一個音的頻率是另一個音的兩倍
注: 國際標準音高 A4 為 440 赫茲, 比 A4 高一個八度的 A5 即為 880 赫茲,比 A4 低一個八度的 A3 則為 220 赫茲
圖 4-1
八度音分成12個半音。每個相鄰的半音對具有相同的頻率比(至少在同等程度上的音調是這樣), 換句話說, A4 到 A#4 的比率 與 A#4 到 B 是相等的。
A#4 在音樂中表示一個音符, A# 指 A 音升高半音的表示方法,4 指的是音高的位置
圖 4-1 展示了我們如何推導每個連續半音之間的比例。得到的結論:
- 為了把一個音符向上轉一個八度,我們把這個音符的頻率加倍
- 每個八度音分成12個半音,同等程度的音調擁有相等的頻率比
讓我們定義 f0 表示某個頻率, f1 表示高八度的同音。我們知道它們之間的關係:
f1 = 2 * f0
接下來,設 k 作為 兩個連續半音間的乘數。由於八度音階有12個半音,可得:
f1 = f0 * k * k * k * ... * k(12x) = f0 * k^12
解上面的方程組,得:
2 * f0 = f0 * k^12
解得 k:
k = 2^(1/12) = 1.0595...
其實沒那麼麻煩,所有這些與半音相關的偏移實際上都不需要手動執行,因為很多音訊環境(包括 Web Audio API)包含了失諧(detune)的概念,它使頻域線性化。Detune 的單位是分,每個八度包含 1200 分, 每個半音包含了 100 分。透過指定1200的調諧,就提高了一個八度,指定 -1200 則降低一個八度。
音調(Pitch) 與 playbackRate
Web Audio API 在每個 AudioSourceNode 上提供了一個 playbackRate 引數。這個引數值可以影響任何 sound Buffer。注意,在這種情況下,音高和樣本的時長都會受到影響。有一些複雜的方法試圖隻影響音高而不影響音訊的時長,但是要以通用的方式做到這一點是非常困難的,否則就會在混合中引入奇奇怪怪的東西。
在“重要理論:音調基礎”一節中討論過, 我們簡單的透過半音比率 2^(1/12) 乘以頻率計算出連續的半音訊率. 如果你正在開發一種樂器或在遊戲設定中使用隨機音調,這將非常有用。下面的程式碼演示了在半音中以給定的頻率偏移音調:
function playNote(semitones) {
// 假定新的 source 從 buffer 中建立了.
var semitoneRatio = Math.pow(2, 1/12);
source.playbackRate.value = Math.pow(semitoneRatio, semitones);
source.start(0);
}
正如我們之前討論過的那樣,我們的耳朵是以指數級感知音調的。將音調當作指數量對待是不方便的,這樣的話我們會經常和一些怪怪的值打交道比如 2 的 12 次方根。取而代之的是我們可以使用失諧引數(detune) 指定要偏移多少分。 所以你可以以更簡單的方式重寫上面的失諧函式了:
function playNote(semitones) {
source.detune.value = semitones * 100;
source.start(0);
}
如果你的音調轉了太多的半音(比如,playNote(24)), 你會開始聽到失真。正因如此,數字鋼琴為每個樂器包括多個樣本。好的數字鋼琴完全避免音高彎曲,幷包括為每個鍵專門錄製的單獨樣本。
優秀的數字鋼琴通常為每個鍵包含多個樣本,這些樣本會根據按鍵的速度回放
多個聲音多種變化
在遊戲中的一個重要特點就是會同時播放多個音效。想象一下在射擊遊戲內多角色用機槍掃射的效果。每把機槍每秒多次掃射,這會同時播放十多種音效。用相同的資源播放實現不同的效果這正是 Web Audio API 的亮點。
如果槍聲效果都是都一種,那就顯得單調了,就算聲音根據目標距離的不同而變化(稍後會討論到“Spatialized Sound” 空間音訊),但這效果顯示還不夠。幸運的是 Web Audio API 提供了之前例子中提到過的可簡單改變音效的至少兩種方式:
-
子彈發射期間的輕微變化
-
透過改變音調模擬真實世界的隨機
利用我們已掌握的時間與音調的知識,實現這兩種效果就相當簡單直接了:
function shootRound(numberOfRounds, timeBetweenRounds) {
var time = context.currentTime;
// 使用同一個 buffer 實現快速連續播放多種音效
for (var i = 0; i < numberOfRounds; i++) {
var source = this.makeSource(bulletBuffer);
source.playbackRate.value = 1 + Math.random() * RANDOM_PLAYBACK;
source.start(time + i * timeBetweenRounds + Math.random() * RANDOM_VOLUME);
}
}
以上程式碼實現 demo 可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch04/index.html
Web Audio API 會在播放時自動一次性合併,本質上就是將多個音訊波形疊加在一起。這會導致一些問題比如音訊被裁剪,這我們之前已經討論過了。此例中在載入的音訊檔案的 AudioBuffer 上新增了一些變化。在完全合成的音訊情況下,這點小缺點是值得接受的。
重要理論:理解頻域(Frequency Domain)
到目前為止,我們的理論討論,我們只研究了聲音隨時間變化的壓強函式。另一種觀察聲音的有用方法是繪製振幅,看看它是如何隨頻率變化的。
時域和頻域圖之間的關係圖是基於傅立葉分解。正如之前所見,自然界中音波經常是週期迴圈。在數學上來講,週期性的音波可視作一系列簡單的不同幅度與頻率正弦波的疊加。越多的正弦波疊加,越能更準確的模擬原函式。透過。傅立葉變換我們可以找到組成訊號的各個正弦波,更多的細節已超出本書討論的範圍。存在很多演算法也能達到相同的目的,比較知名的演算法是 Fast Fourier Transform(FFT)。好訊息是 Web Audio API 實現了這一演算法。我們後面會討論到。
一般來說,我們可以取一個聲波,算出其組成的波,在新圖上以點(頻率,振幅)的形式繪製得到頻域圖。
如圖 4-2 展示了存一個 440Hz 的純音符(稱為 A4)
圖 4-2 一個完美的 1-KHz 正弦波在時間與頻域上的表示
讓我們關注在頻域上,我們可以更好的感受到聲音的質量,包括音調內容,噪聲,等等。高階的音調檢測演算法就是基於頻域。真實樂器產生的聲音都包含泛音(overtones) , 所以 A4 被鋼琴演奏出的頻域圖和小號演奏出的同一個聲調看起來非常不一樣。不管聲音的複雜性如何,同樣的傅立葉分解思想都適用。圖 4-3 展示了一個更復雜的音訊片斷的頻域。
圖 4-3 一個更復雜的音訊片斷的時域和頻域
隨著時間的推移,這些圖上表現非常不同。如果你非常慢速地回放圖 4-3 中的聲音,並觀察它沿著每個圖象移動,您會注意到時域圖是(在左邊)從左向右分析記錄移動。頻域圖(右側)是波形在某一時刻的頻率分析,它可能變化的更快更不可預測。
重要地是,當檢測的聲音不被認為具有特定的音調時,頻域分析研究聲音時還是時分有用的。風聲,敲擊聲,槍聲,在頻域上有著明顯的區別表達。舉個例子,白噪聲擁有的是比較平坦的頻域頻譜範圍,這是因為每個頻率都是相等的。
基於振盪器(oscillator)的直接聲音合成
正如本書早前討論過的,聲音訊號在 Web Audio API 中被表示成 AudioBuffer 浮點陣列形式。大多情況下, buffer 緩衝是從載入的一個聲音檔案或者從聲音流建立的。在一些專案中,我們可能希望合進自己的聲音。我們可以透過JavaScript 來進它進行程式設計建立 audio buffer,簡單的數學函式週期計算得到值並把值賦值給陣列。透過這種方法,我們可以手動更改正弦波的幅度與頻率,甚至聯合多個正弦波建立任意的聲音 [ 回想一下之前提到的傅立葉變換 ]。
想象中用 JavaScript 來做這些工作可能低效且複雜。但是 Web Audio API 提供了原函式讓你使用振盪器來實現:振盪器節點(OscillatorNode). 這些節點擁有可配置的頻率和失諧。它們也有著自己這一類的波形。內建有 正弦,三角,鋸齒,方波,如下圖示:
圖 4-4,振盪器(oscillator)能計算得到的幾種聲波圖
利用 AudioBufferSourceNodes 振盪器(oscillator)可以很方便的在音訊圖中使用。以下是一個例子:
function play(semitone) {
// Create some sweet sweet nodes.
var oscillator = context.createOscillator();
oscillator.connect(context.destination);
// Play a sine type curve at A4 frequency (440hz).
oscillator.frequency.value = 440;
oscillator.detune.value = semitone * 100;
// Note: this constant will be replaced with "sine".
oscillator.type = oscillator.SINE;
oscillator.start(0);
}
除了這些基本的波型別,你還可以使用諧波表為你的振盪器建立一個自定義的波表, 這樣你就可以更高效的建立比上面這個例子中更復雜的波形。這在音樂合成應用中時分重要,但這超出了本書討論的範圍。
注:轉載請註明出處部落格園:王二狗Sheldon池中物 (willian12345@126.com)