本文へスキップ

plonk:ゲームのサウンドトラックまるごとを 2 kB で ​

plonk は、コードから曲を作る Web ゲーム向けの音楽エンジンです。曲は数百バイトのテキストで、エンジンは brotli で約 2 kB。ふつうの圧縮済みの音楽ループは、ひとつで 300 kB から 1 MB ほどあります。私たちが Poki 向けに作っているマッチ 3 ゲーム gloop では、サウンドトラック全体を入れてもビルドが 2.2 kB 増えただけでした。プレイグラウンドで実際に聴いてみてください。

サイズ(brotli)
よくある圧縮済みの音楽ループ300 kB〜1 MB
plonk のエンジン(esbuild、minify 済み)約 2.1 kB
gloop のサウンドトラック、エンジン込み(debuff build のレポートより)2.2 kB
bash
npm install plonkjs
ts
import { Player } from "plonkjs";

new Player().play({
  bpm: 112,
  instruments: {
    lead: { wave: "square", decay: 0.14, sustain: 0.35, echo: 0.3 },
    kick: { wave: "sine", drop: 24, decay: 0.12, sustain: 0 },
  },
  sections: { a: { lead: "C5 . E5 G5 . E5 C5 .", kick: "x . . . x . . ." } },
  order: ["a"],
});

なぜ作ったのか ​

gloop では、ぷるぷるの生き物、スライム、文字まで、すべてをコードで描いています。そうするとゲームは小さく保てますし、細部まで調整できます。そのなかで音楽だけが浮いていました。シンセのピコピコ音が少しあるだけ。ちゃんとした曲を 1 つ入れたら、それだけでゲームの残り全部より大きくなってしまうからです。

Poki ではこのサイズが効いてきます。ダウンロードの大きさで、プレイヤーがクリックしてから遊び始めるまでの速さが決まり、Poki はまさにそこを計測しています。だから音楽も、アートと同じやり方で作る必要がありました。

曲はテキスト ​

1 ステップは 16 分音符です。C5 は音符、C4+E4+G4 は和音、x はドラムの打音、. は休符、- は音を伸ばします。曲は、こうしたパターンでできたセクションを順番に鳴らしたものです。

ts
sections: {
  a: {
    lead: "C5 . E5 G5 . E5 C5 . | D5 . F5 A5 . F5 D5 .",
    bass: "C3 . . C3 . . G2 . | D3 . . D3 . . A2 .",
    kick: "x . . . x . . .",
  },
},

短いパターンはセクションを埋めるまで繰り返されます。なので、長いメロディの下で1 小節のドラムループを回せます。楽器はオシレーターかフィルターをかけたノイズにエンベロープを付けたものです。さらに、音符ごとにスイープできるレゾナンスフィルター、デチューン、ピッチドロップ、ビブラート、テンポに同期したエコーも使えます。gloop のぶにゅっとしたベースは、たった 1 行です。

ts
bass: { wave: "sawtooth", octave: -1, volume: 0.22, filter: { cutoff: 380, q: 9, sweep: 1400 } },

曲はテキストなので、コーディングエージェントも書いたり編集したりできます。notes() は曲をタイミング付きの音符のリストに変えます。だからエージェントやテストでも、聴けない音楽を確かめられます。

ゲームに反応する音楽 ​

gloop のテーマ曲は1 つだけで、それを曲げて使っています。マップ画面ではドラム抜きの 85% の速さで流れます。レベルに入るとドラムが加わります。残り 3 手になると 12% 速くなります。レベルが終わると、ジングルの下でドラムがすっと消えます。

ts
player.layer("kick", 0, 0.8); // fade the kick out
player.tempo(1.12); // speed up, without a jump

音符はフレームではなくオーディオクロック上で先にスケジュールされます。そのため、ゲームでフレームが落ちても音楽はずれません。

どのエンジンでも ​

plonk には依存関係がありません。エンジンのオーディオコンテキストと出力先のノードを渡せば、音楽はゲームの音量やミュートに従います。

ts
new Player({ context: audio.ctx, destination: audio.bus("music") }); // debuff
new Player({ context: sound.context, destination: sound.destination }); // Phaser

できないこと ​

plonk はシンセです。鳴らせるのは矩形波、のこぎり波、三角波、サイン波と、フィルターをかけたノイズです。録音した楽器や音声ファイルは再生できませんし、録音された BGM の代わりにはなりません。でも、ほとんどコストのかからない、弾むようなチップチューン風のサウンドトラックが欲しいなら、ぴったりです。

プレイグラウンドを開いて、パターンを変えて、鳴らしてみてください。作ったものはぜひ Discord で見せてください。