こんにちは!iimonでエンジニアをしている、でっさんです。
本記事はiimon Advent Calendar 2025 14日目の記事となります!
今年はダイエットに挑戦したものの、チートデイを設けすぎてリバウンドしてしまいました。 来年は、みなさんの助言を真摯に受け止めていこうと思っています。
さて、今年のアドベントカレンダーのテーマですが、 社内の雑談で「JavaScript だけで音を鳴らせるよ」という話を聞いたのがきっかけで、 JavaScriptのWeb Audio APIに興味を持ちました。
せっかくなので、ただ調べるだけではなく 実際に何かを演奏するところまで作ってみようと思い、 この季節にぴったりのジングルベルを演奏することをゴールにしました。
この記事では、Web Audio API を使って以下を実装していきます。
- 4種類の波形(sine / square / sawtooth / triangle)の音を聴き比べる
- ドレミファソラシドを鳴らす
- ジングルベルを演奏する
今回の構成
次の3ファイルで実装しました。
- index.html:画面
- src/audio.ts:音声ロジック(Web Audio API)
- src/main.ts:DOMと音声ロジックをつなぐ
プロジェクト構成は以下になります。
├─ index.html
├─ package.json
├─ dist/
│ └─ main.js # ビルド後のバンドル
└─ src/
├─ audio.ts
└─ main.ts
Web Audio API のざっくり概要
Web Audio API は、ブラウザ内で音声処理を行うための API です。
代表的なノード:
- AudioContext:音の時間軸・スケジューリングを管理
- OscillatorNode:音の波形を生成する発振器。4種類(sine / square / sawtooth / triangle)
- GainNode:音量をコントロールするノード
- AudioDestinationNode:音が出力される場所。通常はスピーカー
流れはこのような形です。
AudioContext
└ OscillatorNode (波形・周波数)
↓
GainNode (音量)
↓
AudioDestination (スピーカー)
4種類の波形(OscillatorType)の違い
OscillatorNode には、以下の4種類の波形を指定できます。
- sine
- square
- sawtooth
- triangle
同じ「ド」の音でも、波形によって音色が大きく変わります。
簡単な図と一緒に紹介します。
■sine(正弦波)— なめらかで優しい音
なめらかな波
~~~~~~~~ ~~~~~~~~
フルートや音叉のような、やわらかい響き。
一番「素直」な音で、倍音成分が少ないきれいな音です。
■square(矩形波)— レトロゲームのピコピコ音
カクカクした四角い波(ON/OFF が極端)
┌─┐ ┌─┐ │ │ │ │ └─┴───┴─┴───
ゲームボーイやファミコンのような、いかにも「電子音」という感じのサウンドです。 奇数次の倍音が強く、かなり目立つ音になります。
■sawtooth(のこぎり波)— 金属的で尖った音
斜めに上がってストンと落ちる波
/| /| /| /| / | / | / | / |
高音成分(高い周波数の倍音)がたくさん含まれており、
ブラス系シンセやベースの元になるような、鋭いサウンドになります。
■triangle(三角波)— ベルのような柔らかい金属音
坂道を上り下りするような波
/‾\ /‾\ /‾\ / \/ \/ \
正弦波より少しだけ角があり、柔らかい金属音のような響き。
音名と周波数(NOTES の定義)
Web Audio API では、音の高さは 「周波数(Hz)」で指定します。 「ドレミ」のような音名はそのままでは使えず、 対応する周波数値を自分で設定する必要があります。
今回の演奏で使用する C4〜C5 の主要な音階を 定義するとこのような形になります。
export const NOTES = {
C4: 261.63, // ド
D4: 293.66, // レ
E4: 329.63, // ミ
F4: 349.23, // ファ
G4: 392.0, // ソ
A4: 440.0, // ラ
B4: 493.88, // シ
C5: 523.25, // 高いド
} as const;
これらの値は、音響の世界で広く使われている 12 平均律 に基づいており、 基準音 A4 = 440Hz から半音ごとに決まっています。
audio.ts:Web Audio 周りのロジック
ここからは、実際のコードを載せていきます。
まずは、音を鳴らす処理だけをまとめた役割のsrc/audio.ts です。
// src/audio.ts const audioCtx = new AudioContext(); /** * C4〜C5 の音名と周波数(Hz)の対応表 */ const NOTES = { C4: 261.63, // ド D4: 293.66, // レ E4: 329.63, // ミ F4: 349.23, // ファ G4: 392.0, // ソ A4: 440.0, // ラ B4: 493.88, // シ C5: 523.25, // 高いド } as const; type NoteName = keyof typeof NOTES; type NoteOrRest = NoteName | "REST"; /** * ScoreItem: * - note: どの音を鳴らすか(REST のときは休符) * - length: 拍の長さ(1.0 = 1拍, 0.5 = 半拍, 2.0 = 2拍 というイメージ) */ type ScoreItem = { note: NoteOrRest; length: number; }; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); /** * 単音を鳴らす関数。 * * @param freq - 周波数(Hz) * @param type - 波形の種類(sine / square / sawtooth / triangle) * @param duration - 何秒間鳴らすか(秒単位) * */ function playNote( freq: number, type: OscillatorType, duration = 0.4, ) { // 1. ノードを作成 const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); // 2. 波形タイプと周波数を設定 osc.type = type; osc.frequency.value = freq; // 3. 処理経路を構築:osc → gain → 出力 osc.connect(gain); gain.connect(audioCtx.destination); // 4. 現在時刻を基準に、音の開始・終了を予約 const now = audioCtx.currentTime; osc.start(now); // すぐに鳴らし始める // 5. 音量カーブを設定(最初は1.0、そのあと0付近に減少させていく) gain.gain.setValueAtTime(1, now); gain.gain.exponentialRampToValueAtTime(0.0001, now + duration); // duration 秒後に発振を停止 osc.stop(now + duration); } /** * ドレミファソラシドを順番に鳴らす関数 * * @param type - 波形の種類 * @param bpm - テンポ。大きいほど速くなる * */ export async function playScale( type: OscillatorType, bpm = 100, ) { // 1拍あたりの長さを設定(ミリ秒) const beatMs = (60 / bpm) * 1000; // 2. 下のド (C4) から高いド (C5) までのスケール const SCALE: NoteName[] = ["C4","D4","E4","F4","G4","A4","B4","C5"]; // 3. 1音ずつ鳴らす for (const note of SCALE) { playNote(NOTES[note], type); await sleep(beatMs); } } /** * ジングルベルのメロディー * * - note: E4, F4 などの音名。REST のときは休符 * - length: 拍の長さ。0.5 = 半拍, 1.0 = 1拍, 2.0 = 2拍 のイメージ */ const JINGLE_SCORE: ScoreItem[] = [ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 1.0 }, // ミーー { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 1.0 }, // ミーー { note: "E4", length: 0.5 }, // ミ { note: "G4", length: 0.5 }, // ソ { note: "C4", length: 0.5 }, // ド { note: "D4", length: 0.5 }, // レ { note: "E4", length: 2.0 }, // ミーーー { note: "F4", length: 0.5 }, // ファ { note: "F4", length: 0.5 }, // ファ { note: "F4", length: 0.75 }, // ファー { note: "F4", length: 0.25 }, // ファ(短く) { note: "F4", length: 0.5 }, // ファ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "D4", length: 0.5 }, // レ { note: "D4", length: 0.5 }, // レ { note: "E4", length: 0.5 }, // ミ { note: "D4", length: 1.0 }, // レーー { note: "G4", length: 1.0 }, // ソーー // フレーズ間の小さな休符 { note: "REST", length: 0.1 }, // rest(無音) { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 1.0 }, // ミーー { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 1.0 }, // ミーー { note: "E4", length: 0.5 }, // ミ { note: "G4", length: 0.5 }, // ソ { note: "C4", length: 0.5 }, // ド { note: "D4", length: 0.5 }, // レ { note: "E4", length: 2.0 }, // ミーーー { note: "F4", length: 0.5 }, // ファ { note: "F4", length: 0.5 }, // ファ { note: "F4", length: 0.75 }, // ファー { note: "F4", length: 0.25 }, // ファ(短く) { note: "F4", length: 0.5 }, // ファ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "E4", length: 0.5 }, // ミ { note: "G4", length: 0.5 }, // ソ { note: "G4", length: 0.5 }, // ソ { note: "F4", length: 0.5 }, // ファ { note: "D4", length: 0.5 }, // レ { note: "C4", length: 2.0 }, // ドーーー ]; /** * ジングルベルを演奏する関数 */ export async function playJingleBellsFull( type: OscillatorType, bpm = 140, ) { // 1拍の長さをミリ秒に変換 const baseBeatMs = (60 / bpm) * 1000; for (const item of JINGLE_SCORE) { const durationMs = baseBeatMs * item.length; if (item.note !== "REST") { // length をそのまま duration に渡しているので、 // 1拍ぶんの音を length 倍だけ伸ばして鳴らすイメージ playNote(NOTES[item.note], type, item.length); } // 音を鳴らしている間(または休符の間)は待機する await sleep(durationMs); } }
main.ts:DOMと音鳴らしの接続
// src/main.ts import { playScale, playJingleBellsFull } from "./audio.js"; /** * ドレミ(各波形)ボタンのイベント設定 */ document.getElementById("btn-sine")?.addEventListener("click", () => { // 正弦波(sine) playScale("sine"); }); document.getElementById("btn-square")?.addEventListener("click", () => { // 矩形波(square) playScale("square"); }); document.getElementById("btn-saw")?.addEventListener("click", () => { // のこぎり波(sawtooth) playScale("sawtooth"); }); document.getElementById("btn-tri")?.addEventListener("click", () => { // 三角波(triangle) playScale("triangle"); }); // ジングルベル用の波形セレクトボックス const waveSelect = document.getElementById("wave-select") as HTMLSelectElement | null; // ジングルベルの再生 document.getElementById("btn-jingle")?.addEventListener("click", () => { const selectedWave = (waveSelect?.value ?? "triangle") as OscillatorType; playJingleBellsFull(selectedWave, 140); });
index.html:デモ UI
<!-- index.html --> <!doctype html> <html> <head> <meta charset="UTF-8" /> <title>Web Audio API × TypeScript × Jingle Bells</title> <style> body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; padding: 2rem; } h1 { margin-bottom: 1rem; } section { margin-bottom: 2rem; } button { margin: 0.25rem; padding: 0.5rem 0.75rem; cursor: pointer; } </style> </head> <body> <h1>Web Audio APIのデモ画面</h1> <p style="color: red;">音が鳴るので注意してください。</p> <section> <h2>ドレミファソラシド(波形別)</h2> <p>同じドレミでも、波形を変えると音色が変わります。</p> <button id="btn-sine">sine(正弦波:やわらかい音)</button> <button id="btn-square">square(矩形波:レトロゲーム系)</button> <button id="btn-saw">sawtooth(のこぎり波:金属的で尖った音)</button> <button id="btn-tri">triangle(三角波:ベルっぽい音)</button> </section> <section> <h2>ジングルベル</h2> <p>波形を選んでから「再生」ボタンを押すと、ジングルベルが鳴ります。</p> <p>triangle(三角波)の音がジングルベルにあっていそうです。</p> <label> 波形を選択: <select id="wave-select"> <option value="sine">sine(正弦波:柔らかい)</option> <option value="square">square(矩形波:ピコピコ音)</option> <option value="sawtooth">sawtooth(のこぎり波:金属的)</option> <option value="triangle">triangle(三角波:ベルっぽい響き)</option> </select> </label> <div style="margin-top: 0.5rem;"> <button id="btn-jingle">▶ 再生</button> </div> </section> <script type="module" src="./dist/main.js"></script> </body> </html>
package.json
{ "name": "web-audio-jinglebell", "version": "1.0.0", "scripts": { "build": "tsc", "serve": "http-server ." }, "devDependencies": { "http-server": "^14.1.1", "typescript": "^5.9.3" } }
GitHub Pagesで公開
今回のような静的サイト(HTML + JS)は、GitHub Pagesを使うと簡単に公開できます。
プロジェクト構成は冒頭で説明した以下の内容になります。
├─ index.html
├─ package.json
├─ dist/
│ └─ main.js // ビルド済みバンドル
└─ src/
├─ audio.ts
└─ main.ts
ビルド方法
# tsconfig.jsonを生成 npx tsc --init # パッケージをインストール後にビルド npm install npm run build
これでGitHub Pagesで公開できる状態になります。
あとは GitHub に push して、 「Settings → Pages → Branch: main / Root」 を選択すると、 およそ 10秒程度でサイトが公開 されます。
以降の変更も、push後10秒ぐらいで更新されるので、すごい便利でした。
詳細な手順については公式ドキュメントを参照してください。
3. 動作確認
Web Audio API × TypeScript × Jingle Bells
こちらが公開されたURLになります。
- ドレミボタンを押すと、波形ごとにドレミが鳴るか
- 波形を選択して「再生」を押すと曲が流れるか
を確認してみてください!
まとめ
ブラウザで音が鳴るだけでもすごいのに、 波形を変えたりジングルベルを演奏できるのは本当に面白いですね!
ファミコン世代としては、square波のピコピコ音が鳴った瞬間にテンションが上がりました。 当時の音源はまったく別の技術で動いているようなので、そのあたりも詳しく調べてみようと思います!
最後に
ここまで記事を読んでいただきありがとうございました!
弊社ではエンジニアを募集しております。 ご興味がありましたらカジュアル面談も可能ですので、下記リンクより是非ご応募ください! iimon採用サイト / Wantedly
明日のアドベントカレンダーの担当は「あめちゃん」です! 町中華仲間なので、どんな記事を書いてくれるか楽しみですね!!
参考
音を操るWeb技術 - Web Audio API入門 - ICS MEDIA
著作権について
本記事で扱う「ジングルベル(Jingle Bells)」は、 1857年に James Lord Pierpont によって作曲された楽曲であり、 作曲者の没後 70 年以上が経過しているため パブリックドメイン(著作権フリー) となっています。