Web Audio APIの使い方:音を鳴らす・作る・波形で見せる入門
ブラウザのWeb Audio APIをやさしく解説。AudioContextの起動、Oscillatorで音を作る、Gain/Filterで加工、AnalyserNodeで波形表示、autoplay対策まで、コピペで動くコード付き。
ボタンを押したら、ポンッと小さな音が鳴る。たったそれだけのことが、ブラウザでは意外と一筋縄でいきません。
僕も最初、new Audio("click.mp3").play() で済むだろうと思っていました。ところがページを開いた瞬間に鳴らそうとしたら、コンソールに「ユーザー操作なしに音は出せません」と無情なエラー。しかも効果音のたびにmp3ファイルを用意するのも面倒くさい。
そこで行き着いたのが Web Audio API です。音をファイルから流すだけでなく、コードで音そのものを作り、音量やエフェクトをかけ、波形として画面に描く。ブラウザの中に小さな音響スタジオを置くようなものだと思ってください。
この記事では、その入口を順番に開けていきます。専門用語は出てきたところで言い換えるので、音の知識がなくても大丈夫です。
この記事の要点
- Web Audio APIは ノードをケーブルでつなぐ 発想。音源→加工→出口の順に
connect()で配線する。 - 音の入口は
AudioContext。最初の起動はクリックやタップの中でやる(autoplay制限の回避)。 OscillatorNodeを使えば、音声ファイルなしで「ポーン」という音をコードだけで作れる。- 音量は
GainNode、こもり具合はBiquadFilterNode(フィルター)で調整する。 AnalyserNodeで音の波形データを取り出し、Canvasに描けば可視化できる。- 一番ハマるのは autoplay制限。
context.stateが"suspended"のままならresume()をユーザー操作から呼ぶ。
Web Audio APIは「ケーブルで機材をつなぐ」イメージ
ライブのステージを思い浮かべてください。マイク(音源)からケーブルが伸びて、ミキサー(音量・エフェクト)を通り、スピーカー(出口)から音が出ます。Web Audio APIはこれと同じ構造で、登場するのは3種類だけです。
- 音源ノード:音を生み出す側。
OscillatorNode(音を合成)や、ファイルを読み込んだAudioBufferSourceNode。 - 加工ノード:途中で音を変える。
GainNode(音量)、BiquadFilterNode(フィルター)など。 - 出口:
context.destination。スピーカーやイヤホンに相当する終着点。
これらを connect() というメソッドでつないでいきます。たとえば「音源→音量調整→スピーカー」なら、oscillator.connect(gain) して gain.connect(context.destination) する。コードがそのまま配線図になるので、慣れると頭の中で音の通り道が見えてきます。
この全部を束ねている親玉が AudioContext です。音の世界の「電源を入れたミキサー卓」だと思ってください。ノードを作るのも、現在の再生時刻を知るのも、すべてここ経由です。
まずAudioContextを起動する(ここでつまずく)
最初の関門が AudioContext の起動です。理屈は単純なのに、9割の初心者がここで「鳴らない」と悩みます。
理由はブラウザの autoplay(自動再生)制限。広告サイトが勝手に音を鳴らす迷惑を防ぐため、ブラウザは「ユーザーが明確に操作するまで音を出さない」と決めています。だから window.onload の中で AudioContext を作って音を出そうとしても、状態が "suspended"(一時停止)のまま、うんともすんとも言いません。
正解は、クリックやタップのハンドラーの中で起動することです。
let audioContext = null;
// ボタンが押されて初めてAudioContextを作る/再開する
async function ensureAudio() {
// 初回だけ生成(2回目以降は使い回す)
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
// suspended(停止中)なら、ユーザー操作のうちに再開する
if (audioContext.state === "suspended") {
await audioContext.resume();
}
return audioContext;
}
document.querySelector("#start").addEventListener("click", async () => {
const ctx = await ensureAudio();
console.log("AudioContextの状態:", ctx.state); // "running" になれば成功
});
ポイントは2つ。AudioContext は一度だけ作って使い回すこと(毎クリック作ると無駄が積もります)。そして resume() を ユーザー操作の流れの中で 呼ぶこと。この2点を守るだけで、「鳴らない」の大半は消えます。
window.webkitAudioContext を併記しているのは、古いSafariへの保険です。最近のブラウザなら window.AudioContext だけで動きますが、書いておいて損はありません。
OscillatorNodeで音を「作る」
音声ファイルを用意しなくても、OscillatorNode(オシレーター=発振器)を使えばコードだけで音が作れます。オシレーターは、決めた高さ(周波数)の波を延々と生み出す部品です。
周波数の単位はHz(ヘルツ)で、数字が大きいほど高い音になります。440Hzは「ラ」の音、880Hzはその1オクターブ上のラ。波の形(type)も選べて、"sine" は柔らかい音、"square" はピコピコしたレトロゲーム風、"sawtooth" はブザーっぽい音になります。
async function beep() {
const ctx = await ensureAudio();
const oscillator = ctx.createOscillator(); // 音源(発振器)
oscillator.type = "sine"; // 波の形:sine=柔らかい音
oscillator.frequency.value = 880; // 高さ:880Hz(高めのラ)
oscillator.connect(ctx.destination); // 音源→スピーカーへ配線
oscillator.start(); // 鳴らし始める
oscillator.stop(ctx.currentTime + 0.2); // 0.2秒後に止める
}
ひとつ覚えておきたいのが、オシレーターは使い捨てだということ。一度 start() して stop() したノードは、もう再利用できません。次に鳴らすときは createOscillator() で作り直します。同じノードを使い回そうとしてエラーになるのは、初心者の通過儀礼みたいなものです。
GainNodeとBiquadFilterNodeで音を加工する
さっきの beep() をそのまま鳴らすと、実は耳障りな「プチッ」というノイズが乗ります。音が一瞬でゼロから最大音量に立ち上がるせいです。これを直すのが GainNode(ゲイン=音量)。
GainNode は音量つまみそのもので、gain.value を0〜1で動かします。ただ値を即座に変えるとまたプチッと鳴るので、setValueAtTime や exponentialRampToValueAtTime(指定時間をかけて滑らかに変化)でフェードさせるのがコツです。
さらに BiquadFilterNode(フィルター)を挟むと、音のこもり具合をいじれます。type: "lowpass"(ローパス)にすると高い成分を削ってモコモコした音に、"highpass"(ハイパス)にすると低音を削ってシャリシャリした音になります。配線は「音源→フィルター→音量→スピーカー」の順です。
async function softBeep() {
const ctx = await ensureAudio();
const now = ctx.currentTime;
const oscillator = ctx.createOscillator();
const filter = ctx.createBiquadFilter();
const gain = ctx.createGain();
oscillator.type = "sine";
oscillator.frequency.value = 660;
filter.type = "lowpass"; // 高い成分を削ってやわらかく
filter.frequency.value = 1200; // この周波数より上を抑える
// 音量を一瞬で上げずに、20msかけてフェードイン → 250msでフェードアウト
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(0.3, now + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.25);
// 配線:音源 → フィルター → 音量 → スピーカー
oscillator.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
oscillator.start(now);
oscillator.stop(now + 0.3);
}
exponentialRampToValueAtTime には注意点があります。目標値に0を渡すとエラーになるので、ゼロの代わりに 0.0001 のような極小値を使います。最初は「なんで0じゃダメなの」と戸惑いますが、これがブラウザの仕様です。
AnalyserNodeで波形を可視化する
音を「見せる」役割を担うのが AnalyserNode(アナライザー=分析ノード)です。再生中の音から、波の形を表す数値の配列をリアルタイムに取り出せます。これをCanvasに描けば、よくある波打つビジュアライザーが作れます。
getByteTimeDomainData(時間領域データ)は、音の振幅を短い間隔で並べた配列を返します。「振幅」は音の波の上下の大きさで、無音なら真ん中(128)で一直線、音が鳴ると上下に波打ちます。周波数分析より直感的で、再生中かどうかが一目でわかります。
配線は 音源 → analyser → スピーカー。アナライザーは音をそのまま素通しさせるので、間に挟んでも音は変わりません。
ここまでの部品を全部つないだ、そのままコピペで動く完成形 が次のコードです。HTMLファイルに貼ってブラウザで開けば、ボタンひとつで音が鳴り、波形が動きます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>Web Audio API デモ</title>
</head>
<body>
<button id="play">音を鳴らして波形を見る</button>
<canvas id="scope" width="600" height="160"
style="display:block;background:#0f172a;margin-top:12px"></canvas>
<script>
let audioContext = null;
// ユーザー操作の中でAudioContextを起動する
async function ensureAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === "suspended") {
await audioContext.resume();
}
return audioContext;
}
const canvas = document.getElementById("scope");
const canvasCtx = canvas.getContext("2d");
document.getElementById("play").addEventListener("click", async () => {
const ctx = await ensureAudio();
const now = ctx.currentTime;
// 音源・音量・分析ノードを用意
const oscillator = ctx.createOscillator();
const gain = ctx.createGain();
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048; // 取り出す波形データの細かさ
oscillator.type = "sawtooth";
oscillator.frequency.value = 220;
// プチノイズ防止のフェード
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(0.25, now + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, now + 1.4);
// 配線:音源 → 音量 → 分析 → スピーカー
oscillator.connect(gain);
gain.connect(analyser);
analyser.connect(ctx.destination);
oscillator.start(now);
oscillator.stop(now + 1.5);
// 波形データを受け取る入れ物
const buffer = new Uint8Array(analyser.fftSize);
// 1フレームごとに波形を描く
function draw() {
analyser.getByteTimeDomainData(buffer); // 現在の波形を取得
canvasCtx.fillStyle = "#0f172a";
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = "#22c55e";
canvasCtx.beginPath();
const sliceWidth = canvas.width / buffer.length;
for (let i = 0; i < buffer.length; i++) {
const v = buffer[i] / 128; // 128が中心、上下に波打つ
const y = (v * canvas.height) / 2;
const x = i * sliceWidth;
if (i === 0) canvasCtx.moveTo(x, y);
else canvasCtx.lineTo(x, y);
}
canvasCtx.stroke();
requestAnimationFrame(draw);
}
draw();
});
</script>
</body>
</html>
requestAnimationFrame(次の描画タイミングで関数を呼ぶ仕組み)でループを回し、毎フレーム波形を取り直しているのがミソです。fftSize は取り出すデータの細かさで、大きいほど波形がなめらかになりますが、その分だけ描画も重くなります。Reactなど本格的なUIに組み込む手順は、内部リンクのClaude CodeのCanvas開発でぼやけ・スマホ崩れを直す実装術が参考になります。
僕がハマった落とし穴3つ
正直に白状すると、最初の音声デモは事故だらけでした。
1つ目は、autoplay制限を甘く見たこと。 useEffect やページ読み込み直後に AudioContext を作って「鳴らない、壊れてる」と30分悩みました。原因は単純で、ユーザー操作の外で起動していただけ。context.state を console.log していれば、"suspended" という答えが最初から出ていたんです。状態を表示する一行をケチった自分を呪いました。
2つ目は、ノードを止め忘れてCPUを食い続けたこと。 ビジュアライザーの requestAnimationFrame ループを止めずに別画面へ遷移したら、見えないところで描画が回りっぱなし。ノートPCのファンが唸り出して気づきました。画面を離れるときは cancelAnimationFrame でループを止め、ノードは disconnect() で外す。これを習慣にしてから静かになりました。
3つ目は、フェードをサボってプチノイズを撒き散らしたこと。 音量を gain.value = 0.3 と即代入していた頃は、鳴らすたびに「プチッ」。exponentialRampToValueAtTime で20msだけフェードを入れたら、嘘みたいに消えました。たった2行の差で、安っぽい音と上品な音が分かれます。
始めるなら、この順番で
いきなり凝った音楽アプリを作ろうとしないでください。順番に積むのが結局いちばん速いです。
AudioContextをボタンクリックで起動し、stateをログに出す。 まず"running"を見届ける。ここが土台です。OscillatorNodeで「ポーン」と一発鳴らす。 音源→スピーカーの最短配線で、音が出る感覚をつかむ。GainNodeを挟んでフェードを入れる。 プチノイズが消える気持ちよさを体感する。AnalyserNodeを足してCanvasに波形を描く。 ここまで来れば、もう立派な音響アプリです。
この4段を踏めば、マイク録音やエフェクトチェーンといった応用も、同じ「ノードをつなぐ」発想の延長で理解できます。音をたくさん扱うUIは描画負荷も上がりやすいので、表示の重さが気になったらWebパフォーマンス改善はCore Web Vitalsを測ってから直すの測り方も合わせてどうぞ。
よくある質問
Q. audio 要素(<audio>)と何が違うんですか?
A. <audio> はファイルをそのまま再生する完成品の再生機です。手軽な反面、音を加工したり波形を取り出したりはできません。Web Audio APIは音を部品レベルで組み立てるので、合成・エフェクト・可視化まで自由に作れます。単純なBGM再生なら <audio>、効果音やビジュアライザーならWeb Audio APIです。
Q. iPhoneのSafariで音が鳴りません。
A. ほぼ確実にautoplay制限です。AudioContext の起動と resume() を、必ずタップなどのユーザー操作のハンドラー内で呼んでください。iOSは特に厳しく、touchend や click の中でないと state が "running" になりません。window.onload での起動は通りません。
Q. オシレーターを使い回したら InvalidStateNode のようなエラーが出ました。
A. OscillatorNode も AudioBufferSourceNode も使い捨てです。一度 start() したら再利用できないので、鳴らすたびに createOscillator() で新しく作ってください。作って・つないで・鳴らして・捨てる、を毎回繰り返すのが正しい使い方です。
Q. 音にプチプチとノイズが乗ります。
A. 音量が一瞬で立ち上がっているのが原因です。gain.gain を即代入せず、setValueAtTime と exponentialRampToValueAtTime で20ms程度のフェードを入れてください。なお目標値に 0 は渡せないので、0.0001 のような極小値を使います。
Q. 周波数や波形を分析したいときは?
A. この記事では時間領域の getByteTimeDomainData を使いましたが、getByteFrequencyData を使えば「どの高さの音がどれくらい含まれるか」という周波数スペクトラムが取れます。イコライザー風の棒グラフを作りたいときはこちらです。詳しい仕様はMDNのWeb Audio APIに網羅されています。
実際に試した結果
この記事のデモを自分で組み直してみて、改めて「Web Audio APIは配線図だ」という感覚が腑に落ちました。oscillator.connect(gain).connect(analyser).connect(destination) と書いた瞬間、頭の中で音の通り道がスッとつながる。最初は難しそうに見えた AnalyserNode も、波形データの配列をCanvasに点で打っていくだけだと分かれば、怖くありませんでした。
逆に、一番時間を溶かしたのはやっぱり autoplay制限です。鳴らない原因の9割はここで、context.state を一行ログに出すだけで即解決します。難しいのはAPIそのものではなく、「ブラウザがいつ音を許すか」というルールのほう。そこさえ押さえれば、あとはノードをつなぐ遊びです。まずは上のHTMLをコピペして、ボタンを押して波形を踊らせるところから始めてみてください。
学びを実務やチーム開発に広げたい方は、Claude Code研修・導入相談や教材一覧も覗いてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。