さて、前回の記事では「タイル用の画像ファイルを1枚も使わずCanvas2Dでプロシージャル生成している」という話をしたわけですが、実は2048×3Dのサウンド周り(js/audio.js)もまったく同じ思想で作られています。すなわち、MP3やWAV、OGGといった外部の音声ファイルを1つも読み込まず、すべてブラウザ標準のWeb Audio APIによる減算合成・FM風変調・ノイズフィルタリングだけでリアルタイムに鳴らしているわけです。

なぜWebゲームでプロシージャル音源を採用したのかというと、「数メガバイトのBGMファイルのダウンロード待ちをゼロにしたい」「ループの継ぎ目(シーム)で音がプツッと途切れる現象を根絶したい」「コンボ数や合体したタイルの数値(2〜8192)に応じてリアルタイムにピッチや倍音構成を変化させたい」という3つの理由があります。今回はSoundManagerクラス(js/audio.js)のシンセサイザー回路設計を詳しく公開します。

01. CLASSIC / HARD用BGM:Em7 → Cmaj7 → G → D のアンビエント・パッド合成

まず、思考系モードであるCLASSICおよびHARDモードのBGM(startClassicBGM())です。パズルをじっくり考えているときに耳障りにならないよう、ここでは打楽器(ドラム)を排除し、4つのコード(和音)が3.8秒周期でゆったりと移り変わるサイバーパンク・アンビエントパッドを合成しています。

// js/audio.js : startClassicBGM() のコード進行定義(周波数 Hz)
const chords = [
  [164.81, 196.00, 246.94, 293.66], // Em7   (E3, G3, B3, D4)
  [130.81, 164.81, 196.00, 246.94], // Cmaj7 (C3, E3, G3, B3)
  [196.00, 246.94, 293.66, 392.00], // G     (G3, B3, D4, G4)
  [146.83, 185.00, 220.00, 293.66]  // D     (D3, F#3, A3, D4)
];

単なるOscillatorNodeをそのまま鳴らすと昔のポケコンのような味気ない電子音になってしまうため、以下の3つの音響処理を重ねています。

  • デチューン(微細ピッチずらし)によるコーラス感:構成音ごとにtriangle(三角波)とsine(正弦波)を重ね、osc.detune.value = (idx - 1.5) * 6(セント単位のピッチずれ)を与えることで、アナログシンセのような厚みのあるうねりを作っています。
  • ローパスフィルタ(BiquadFilterNode)のエンベロープ開閉:カットオフ周波数を500Hz → 1100Hz → 400Hzと4.2秒かけてゆっくりスイープさせることで、呼吸するように音色が明滅するパッドサウンドになります。
  • 60%の確率で鳴るペンタトニック・ベル(bellNotes):コードが切り替わるたびに、1オクターブ上のE5, G5, A5, B5, D6からランダムに1音を選び、長いリリースを持つサイン波のベル音を薄く重ねることで、同じ4小節の繰り返しでも毎回少しずつ違うメロディに聴こえる生成音楽(ジェネレーティブ・ミュージック)になっています。

02. PARTY用BGM:136 BPMの4つ打ちキック・アシッドベース・アルペジオのシーケンサー

一方、4×4×4のタイムアタックであるPARTYモードに切り替えると、BGMエンジンはstartPartyBGM()へバトンタッチし、テンポ136 BPM(16分音符1ステップあたり約110.29ミリ秒)の16ステップ・ダンスシーケンサーが走り出します。

PARTYモード(136 BPM)の5パート合成シーケンサー構成表
パート名 発音ステップ(0〜15) 波形・フィルタ回路の構成(js/audio.js)
4つ打ちキック(Kick) step % 4 === 0
(0, 4, 8, 12)
sine波の周波数を160Hz → 38Hzへ0.11秒で指数減衰(exponentialRampToValueAtTime)させ、バスドラムのアタックと重低音を作る。
ハンドクラップ(Clap) step % 8 === 4
(4, 12:2拍目・4拍目)
ホワイトノイズバッファをbandpassフィルタ(中心周波数1400Hz, Q = 1.2)に通し、0.09秒で急速減衰させる。
オフビート・ハイハット 奇数ステップ(裏拍)すべて ホワイトノイズをhighpassフィルタ(6500Hz以上のみ通過)に通し、0.04秒の鋭い金属音を作る(裏拍のstep % 4 === 2でアクセント強調)。
アシッド・ベースライン 16ステップのパターン配列 sawtooth(ノコギリ波)をレゾナンス強めのローパスフィルタ(Q = 4.0、カットオフ1600Hz → 260Hz)に通し、TB-303風のうねるベースを合成。
きらめくシンセ・アルペジオ 偶数ステップ(表拍) square(矩形波)+バンドパスフィルタ(1800Hz)で、A4 → C5 → E5 → G5 → A5と駆け上がるアルペジオを演奏。

これら5パートすべてが、たった120行ほどのJavaScriptコードからリアルタイムに生成されています。音声ファイルがゼロなので、CLASSICからPARTYへモードを切り替えた瞬間、0ミリ秒のラグでアンビエントから136 BPMのダンスビートへ切り替わるわけです。

03. ブロック移動音(playMoveSound):バンドパス・ホワイトノイズによる「物理摩擦音」

次に、毎ターン必ず耳にする「ブロックをスライドさせたときの移動音(playMoveSound())」の設計です。実は初期プロトタイプでは、移動音にも短いサイン波(「ピッ」「ポッ」という電子音)を使っていました。しかし何千手もプレイしていると、移動のたびに音程のある電子音が鳴るのは耳が疲れるうえ、合体したときのチャイム音と混ざって判別しづらくなってしまったのです。

そこで現在のバージョンでは、「音程のあるオシレーターを廃止し、ホワイトノイズをバンドパスフィルタで削り出した『シュッ』という物理的な空気摩擦音」に全面刷新しました。

// js/audio.js : playMoveSound() における物理摩擦ノイズ合成
const bufferSize = Math.floor(this.ctx.sampleRate * 0.065);
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
  // 急速に減衰するエンベロープをかけたホワイトノイズ
  data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 1.8);
}
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(520, now);
filter.frequency.exponentialRampToValueAtTime(1100, now + 0.055);
filter.Q.value = 2.2;

長さわずか0.065秒(65ミリ秒)のノイズに対し、バンドパスフィルタの中心周波数を520Hz → 1100Hzへ跳ね上げることで、重みのあるタイルがレールの上を高速で滑ったような「シュッ」という心地よい摩擦音が生まれます。これなら何千回スワイプしても聴覚疲労が起きず、次に説明する「合体チャイム音」とのコントラストも明確になるわけです。

04. 数値・コンボ連動の倍音マージチャイムと、GLITCH誘爆の重低音ショックウェーブ

そしてブロック同士が合体した瞬間(playMergeSound(maxMergedValue, combo))には、物理摩擦音とは対照的な澄んだ和音チャイムが鳴り響きます。

// js/audio.js : 数値の対数(log2)とコンボ数に応じた周波数計算
const level = Math.max(1, Math.log2(maxMergedValue || 4));
const baseFreq = 220 * Math.pow(1.08, level) + Math.min(combo, 10) * 18;
  • 数値が大きくなるほど音程が上がり、倍音が増える:合体後の数値の2を底とする対数Math.log2(maxMergedValue)に応じて基本周波数baseFreqが上昇します。さらにmaxMergedValue >= 64以上の大型タイルが合体したときは、基本波(1倍音)と完全5度(1.498倍音)に加えて、2オクターブ上のキラキラした倍音(2倍音)が追加レイヤーとして重なる設計になっています。
  • HARDモードのGLITCH破壊音(playGlitchDestroySound(count)):HARDモードで6近傍ショックウェーブによってGLITCHブロックを粉砕したときは、sawtooth波の重低音ドロップ(180Hz → 42Hz)とバンドパスノイズのガラス破砕音、さらに高音域のシアン・スパークル(1200Hz → 2400Hz)を同時発音させ、「邪魔な障害物を吹き飛ばした!」という爽快感を聴覚面からもフィードバックしています。

もし普段ミュートでプレイしている方がいたら、画面右上のスピーカーアイコン(またはBGM / SEスライダー)をオンにして、クラシックのアンビエントとパーティーの136 BPMビートを聴き比べてみてもらえると嬉しいわけです。次回(第8回)は、スマートフォンとPCの両方で誤操作ゼロを目指した「ハイブリッド・タッチ判定(Raycaster × 24px安全マージン)と視野角自動フレーミング計算」について解説します!

Web Audio Synthesizer

プロシージャルBGMと効果音を聴きながらプレイする

CLASSICのアンビエントパッドとPARTYの136 BPMビートは、ゲーム画面のサウンド設定からいつでも切り替えられます。

🎵 PARTY(136 BPM)で遊ぶ ➔ 🎹 CLASSIC(アンビエント)で遊ぶ