LoginSignup
2
3

More than 1 year has passed since last update.

Web Audio APIでループBGMを再生しよう!

Last updated at Posted at 2022-07-09

概要

何をするの?

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秒くらいしかかからなくなりました。並列処理本当にやってた?

メモリが大変なことになっている

どんどん増えていくメモリ

158.png

ん?

353.png

ちょっと待て。

410.png

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みたいに軽いエフェクトをかけたり、音声のより細かい操作をすることができます。

すごいです(小並感)

  1. ごめん正直に言うとスクショとってないから適当に別のプロセスのスクショとっただけ。画像はイメージです。画像だけに。

  2. gainNode.destinationではありません。(一敗)

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3