小試瀏覽器 Web Audio API 聲音合成
| | | 0 | |
早在十幾年前我就知道用 <audio> 可以在網頁播 MP3,當時還寫了在 iPad 跑的英文單字練習互動網頁搶救小孩的菜英文,而我對瀏覽器的音效能力了解也一直停在這裡。
註:很遺憾,雖然後來我還搞了 單字朗讀 MP3、單字卡、高中 7000 單線上測試... 小閃光跟小木頭的英文終究都沒被救起來了,科技不敵命運的鎖鏈,無法扭轉乾坤 Orz
最近意外發現,瀏覽器能做的不只於此,現代瀏覽器都支援 Web Audio API,能模擬振盪器(Oscillator)生成指定頻率波形(弦波、方波...),並可混合多個音訊、串接濾波器發出聲音,搞出神奇的花式玩法。有玩過任天堂紅白機或 AdLib 音效卡 的老人對這種合成技術與電子音樂應不陌生,現在寫幾行 JavaScript 便能實現。
Web Audio API 以音訊節點串接音訊路由的概念生成及操控音訊。音訊節點透過其輸入與輸出連結成鏈狀或簡單的網狀結構,將數學計算模擬的振盪波、音樂/影片檔錄音,或是音訊串流連結到一個或多個效果節點(例如:殘響、雙二階濾波器、平移器、壓縮器),最後串接到最終目的地(耳機、喇叭等輸出)。有趣的是,它甚至有個 AnalyserNode 可將音波轉為 byte[],配合 Canvas 繪圖可模擬示波器顯示,觀察聲波形狀。
Web Audio API 涉及不少物理學與術語,想深入應用需要一定的專業知識背景,有興趣深入的同學,推薦 OXXO.STUDIO 有一系列好文,比 MDN 文件易讀好懂:
- 初探 Web Audio API
- 聲音參數與音量節點 ( AudioParam,GainNode )
- 立體聲 ( PannerNode,StereoPannerNode )
- 聲音濾波器 ( BiquadFilterNode )
- 建立音樂頻譜 ( createMediaElementSource )
- 建立音樂頻譜 2 ( analyser )
- 獲取錄音資訊 (getUserMedia)
總之,我寫了簡單振盪器發聲練習熟悉一下 Audio API 的用法:線上展示 原始碼

一不做二不休,再寫段小程式模擬傳統電話撥話過程,很有趣吧?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>電話撥號模擬</title>
</head>
<body>
<button id="play" type="button" style="font-size: 2em; margin: 12px; padding: 12px 24px">模擬電話撥號</button>
<script>
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
const DTMF = {
0: [941, 1336], 1: [697, 1209], 2: [697, 1336], 3: [697, 1477], 4: [770, 1209],
5: [770, 1336], 6: [770, 1477], 7: [852, 1209], 8: [852, 1336], 9: [852, 1477]
};
const phoneNumber = "28825252";
const playButton = document.querySelector("#play");
let audioContext;
let masterGain;
function createTones(frequencies, start, duration, volume = 0.08, type = "sine", endFrequency) {
for (const frequency of frequencies) createTone(frequency, start, duration, volume, type, endFrequency);
}
function createTone(frequency, start, duration, volume = 0.08, type = "sine", endFrequency) {
// OscillatorNode 用來產生指定頻率的週期波形,GainNode 負責控制音量
// 每次建立獨立節點,以便多個頻率可在同一時間播放,例如 DTMF 的雙音組合。
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
// 設定波形種類與起始頻率,start 是 AudioContext 的絕對時間
// 確保不會因 JavaScript 執行延遲,影響開始播放時機
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, start);
// 若有提供結束頻率,會音調持續期間做指數式滑音,接近人耳對音高的感受
if (endFrequency) oscillator.frequency.exponentialRampToValueAtTime(endFrequency, start + duration);
// 建立簡短的音量包絡(Envelope):從靜音淡入、維持指定音量,結尾再淡出
// 避免波形在非零振幅處突然開始或停止產生喀聲(click/pop)
gain.gain.setValueAtTime(0, start);
// 淡入最多 15 毫秒;短音調則使用其長度的四分之一,以免淡入占比過高
gain.gain.linearRampToValueAtTime(volume, start + Math.min(0.015, duration / 4));
// 在結束前至少保留 25 毫秒淡出空間,加入 Math.max 因應極短音調
gain.gain.setValueAtTime(volume, start + Math.max(0.015, duration - 0.025));
gain.gain.linearRampToValueAtTime(0, start + duration);
// 將「振盪器 -> 單音增益 -> 主增益 -> 喇叭」串接起來。
// 主增益可統一調整總音量
oscillator.connect(gain).connect(masterGain);
// 啟動與停止也使用 AudioContext 時間排程,多留 10 毫秒再銷毀節點,確保淡出自動化完整執行
// 停止後節點會由瀏覽器回收
oscillator.start(start);
oscillator.stop(start + duration + 0.01);
}
function sayHello(start = audioContext.currentTime) {
const duration = 0.45;
const voiceGain = audioContext.createGain();
// 建立 Envelope:淡入、維持、淡出,避免喀聲
voiceGain.gain.setValueAtTime(0, start);
voiceGain.gain.linearRampToValueAtTime(0.2, start + 0.04);
voiceGain.gain.setValueAtTime(0.2, start + duration - 0.08);
voiceGain.gain.linearRampToValueAtTime(0, start + duration);
voiceGain.connect(masterGain);
const vocalSource = audioContext.createOscillator();
const sourceGain = audioContext.createGain();
vocalSource.type = "sawtooth";
// 設定起始頻率,並在持續期間做滑音,模擬人聲的音高變化
vocalSource.frequency.setValueAtTime(145, start);
vocalSource.frequency.linearRampToValueAtTime(130, start + duration * 0.65);
vocalSource.frequency.exponentialRampToValueAtTime(170, start + duration);
sourceGain.gain.value = 0.25;
vocalSource.connect(sourceGain);
let formantOutput = sourceGain;
function createFormant(initialFrequency, endFrequency, gainDb, quality) {
// 用 BiquadFilterNode 模擬人聲的共振峰 (Formant)
const filter = audioContext.createBiquadFilter();
filter.type = "peaking";
filter.frequency.setValueAtTime(initialFrequency, start);
filter.frequency.linearRampToValueAtTime(endFrequency, start + duration);
// 品質因數 (Q),數值越高共振峰越尖銳,數值越低共振峰越寬廣
filter.Q.value = quality;
filter.gain.value = gainDb;
formantOutput.connect(filter);
formantOutput = filter;
}
// 加入三段共振峰,模擬人聲的主要共振頻率
createFormant(300, 430, 10, 2.5);
createFormant(650, 1900, 25, 3.5);
createFormant(2000, 2250, 45, 3);
formantOutput.connect(voiceGain);
vocalSource.start(start);
vocalSource.stop(start + duration + 0.01);
vocalSource.onended = () => {
playButton.disabled = false;
voiceGain.disconnect();
};
}
function scheduleSequence() {
const start = audioContext.currentTime + 0.08;
// 撥話音:350hz + 440hz
createTones([350, 440], start, 2, 0.055);
let cursor = start + 2.05;
for (const digit of phoneNumber) {
createTones(DTMF[digit], cursor, 0.12, 0.1);
cursor += 0.2;
}
// 鈴聲:480hz + 440hz
cursor += 0.5;
for (let i = 0; i < 2; i += 1) {
createTones([480, 440], cursor, 1, 0.055);
cursor += 2.5
}
// 模擬「喂」
sayHello(cursor);
}
async function playSequence() {
if (!AudioContextClass) return;
playButton.disabled = true;
audioContext ||= new AudioContextClass();
await audioContext.resume();
masterGain = audioContext.createGain();
masterGain.gain.value = 0.75;
masterGain.connect(audioContext.destination);
scheduleSequence();
}
playButton.addEventListener("click", playSequence);
</script>
</body>
</html>
Exploring the Web Audio API beyond HTML audio, including oscillators, filters, audio routing, visualization with AnalyserNode, and JavaScript-generated sounds. Includes a simple oscillator demo and a fun simulation of traditional telephone dialing tones.
Comments
Be the first to post a comment