概要
何をするの?
Web Audio APIでElectronで製作したゲーム用のBGMを再生します。
HTMLAudioElementじゃダメなの?
長い間HTMLAudioElement(new Audio()
で生成できるオブジェクトのこと。audio
タグと同等。)を使っていました。しかし、ループ素材を再生するとどうもうまくいかないのです。
var audio = new Audio("<音声のURL>");
audio.loop = true;
audio.play();
このソースコードで再生しても、微妙な隙間があります。そして何よりめんどくさいのが、BGMが複数ある際、それぞれvolume
プロパティを設定したり保持したりする必要があります。めんどくさいです。
Web Audio APIってすげー(小並感)
Web Audio APIというのは、HTMLAudioElementより多くのことができる低レベル(かな?)音声のAPIです。
これを使えばJavaScriptでDAWができます。シンセサイザー等も作れるみたいです。
結論だけ見たい人向け
BGM再生
var audioContext = new AudioContext();
var source = audioContext.createBufferSource();
source.buffer = await audioContext.decodeAudioData(audioArrayBuffer);
source.loop = true;
source.connect(audioContext.destination);
source.start();
利点
- BGMを途切れさせずに再生できます。
- 一括で音量を制御できます。
- ディレイ等のエフェクトをかけられます。
パフォーマンスのTips
- 読み込みは一括ロードだと時間がかかるので、なるべく使う時になったらロードするのがおすすめです。どういうソフトウェアにもよりますが…
- ゲームならマップ移動時にBGMが変わる等なので、マップロード時に読み込みとか。
- 使い終わったら解放しましょう。メモリ管理に慣れていなければ特に気をつけましょう。
- nullを代入して強制gcしか今私には思いつきません。
BGMを再生しよう
コードはどうなってんだコードは!
AudioContextにAudioBufferSourceNode等のソースを接続します。コネクトします……。
var audioContext = new AudioContext();
var source = audioContext.createBufferSource();
source.buffer = await audioContext.decodeAudioData(audioArrayBuffer);
source.loop = true;
source.connect(audioContext.destination);
source.start();
これでAudioContextから音が再生されます。
HTMLAudioElementとは違って音の途切れ目がありません!
ループBGM気持ちよすぎだろ!
BGMの切り替え処理はこんな感じで、一度止めて、AudioContextとの接続を切断してから
source.stop();
source.disconnect();
また新しいNodeを接続して再生。
var source = audioContext.createBufferSource();
source.buffer = await audioContext.decodeAudioData(audioArrayBuffer);
source.loop = true;
source.connect(audioContext.destination);
source.start();
これで終わるわけねえだろ!!!!そんなんなら記事にしないわ!!!
残念ながら、私の場合はこれで終わりませんでした。
パフォーマンス面でHTMLAudioElementに大きく劣っている部分が出てきてしまったためです。
パフォーマンスを改善しよう
一括ロードは時間がかかる
プリロード作戦は失敗
作っていたゲームでは読み込み時にアセットを全てプリロードする仕組みになっています。
BGMもプリロードするので、関数の名前と経験的にsource
をキャッシュ変数に最初に全部突っ込んでいました。しかし、重い。
何しろ重い。
たった10個のBGMのロードに5秒くらいかかります。
Promise.all
で並列処理をしているのですが、なぜかこんなに時間がかかります。
必要になったらロードする作戦は成功
プリロードをやめて、マップ移動時にBGMをロードするようにしました。
すると、1つのBGMのロードに0.5秒くらいしかかからなくなりました。並列処理本当にやってた?
メモリが大変なことになっている
どんどん増えていくメモリ
ん?
ちょっと待て。
BGMを切り替えるとどんどんメモリ使用量が増えていくが!?!?1
たぶんメモリに保持してる(たぶん)
var source = audioContext.createBufferSource();
source.buffer = await audioContext.decodeAudioData(audioArrayBuffer);
Web Audio API「やあ」
お前さてはメモリに全部データを置いているな!?!?!?
GCしましょう(Electron向け)
ごめんなさい!Electron向けです。Webは…知りません……。
Electronのバージョンは最新版です。
electron.app.commandLine.appendSwitch("js-flags", "--expose-gc");
GC用の関数を使えるようにフラグを設定。Readyイベント前でないと使用できないと思います。
source.stop();
source.disconnect();
source = null;
gc();
停止する関数にgc()
を追加して、強制的にガベージコレクションします。
音量調整・エフェクトをかける
音量調整
音量を変更したい場合、SourceのNodeとAudioContext
のNodeの間にエフェクトのNodeを刺す必要があります。
var gainNode = audioContext.createGain();
gainNode.connect(audioContext.destination);
こうして作ったgainNodeに
// source.connect(audioContext.destination);
source.connect(gainNode);
source.start();
今まで再生していたconnect
メソッドの引数をaudioContext.destination
からgainNode
に変更します。2
このgainNodeを
gainNode.gain.value = 0.5;
こうして音量を調整します。
エフェクト
このセクションはコード未検証です。なので、実際のコード例はありません。
audioContext.createDelay(5.0);
で作成したNodeをContextに刺すとディレイが使えたりします。
audioContext.createOscillator();
では、単純なシンセサイザーが作れたりします。このメソッドはMDNのドキュメントが一番わかりやすいです。
audioContext.createDynamicsCompressor();
では、コンプレッサーが作れます。
結論:すごい
このようにWeb Audio APIではDAWみたいに軽いエフェクトをかけたり、音声のより細かい操作をすることができます。
すごいです(小並感)