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

# 概要

## 何をするの？

Web Audio APIでElectronで製作したゲーム用のBGMを再生します。

## HTMLAudioElementじゃダメなの？

長い間HTMLAudioElement（`new Audio()`で生成できるオブジェクトのこと。`audio`タグと同等。）を使っていました。しかし、ループ素材を再生するとどうもうまくいかないのです。

```javascript
var audio = new Audio("<音声のURL>");

audio.loop = true;
audio.play();
```

このソースコードで再生しても、微妙な隙間があります。そして何よりめんどくさいのが、BGMが複数ある際、それぞれ`volume`プロパティを設定したり保持したりする必要があります。めんどくさいです。

## Web Audio APIってすげー（小並感）

Web Audio APIというのは、HTMLAudioElementより多くのことができる低レベル（かな？）音声のAPIです。

これを使えばJavaScriptでDAWができます。シンセサイザー等も作れるみたいです。

# 結論だけ見たい人向け

## BGM再生

```javascript
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等のソースを接続します。~~コネクトします……。~~

```javascript
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との接続を切断してから

```javascript
source.stop();
source.disconnect();
```

また新しいNodeを接続して再生。

```javascript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309889/3a8a8246-c658-48f9-79d1-6a3cc750ebf6.png)

ん？

![353.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309889/136df75f-08c5-34f1-93a5-7c6b0acfa24a.png)

ちょっと待て。

![410.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309889/5fdf38e4-0133-da47-b923-ad3203f62969.png)

BGMを切り替えるとどんどんメモリ使用量が増えていくが！？！？[^1]

### たぶんメモリに保持してる（たぶん）

```javascript
var source = audioContext.createBufferSource();
source.buffer = await audioContext.decodeAudioData(audioArrayBuffer);
```

Web Audio API「やあ」

**お前さてはメモリに全部データを置いているな！？！？！？**

### GCしましょう（Electron向け）

ごめんなさい！Electron向けです。Webは…知りません……。

Electronのバージョンは最新版です。

```javascript
electron.app.commandLine.appendSwitch("js-flags", "--expose-gc");
```

GC用の関数を使えるようにフラグを設定。Readyイベント前でないと使用できないと思います。

```javascript
source.stop();
source.disconnect();
source = null;
gc();
```

停止する関数に`gc()`を追加して、強制的にガベージコレクションします。

# 音量調整・エフェクトをかける

## 音量調整

音量を変更したい場合、**SourceのNodeと`AudioContext`のNodeの間にエフェクトのNodeを刺す**必要があります。

```javascript
var gainNode = audioContext.createGain();
gainNode.connect(audioContext.destination);
```

こうして作ったgainNodeに

```javascript
// source.connect(audioContext.destination);
source.connect(gainNode);
source.start();
```

今まで再生していた`connect`メソッドの引数を`audioContext.destination`から`gainNode`に変更します。[^2]

このgainNodeを

```javascript
gainNode.gain.value = 0.5;
```

こうして音量を調整します。

## エフェクト

このセクションはコード未検証です。なので、実際のコード例はありません。

```javascript
audioContext.createDelay(5.0);
```

で作成したNodeをContextに刺すとディレイが使えたりします。

```javascript
audioContext.createOscillator();
```

では、単純なシンセサイザーが作れたりします。このメソッドは[MDNのドキュメント](https://developer.mozilla.org/ja/docs/Web/API/BaseAudioContext/createOscillator)が一番わかりやすいです。

```javascript
audioContext.createDynamicsCompressor();
```

では、コンプレッサーが作れます。

# 結論：すごい

このようにWeb Audio APIではDAWみたいに軽いエフェクトをかけたり、音声のより細かい操作をすることができます。

すごいです（小並感）

[^1]: ごめん正直に言うとスクショとってないから適当に別のプロセスのスクショとっただけ。画像はイメージです。画像だけに。
[^2]: `gainNode.destination`ではありません。（一敗）