iOSでのオーディオ再生制限の解除方法いろいろ

# はじめに
iOS Safari含む近年のWebブラウザではユーザー体験向上やデータ通信量節約などの理由から、ユーザー操作があるまで音を鳴らせないようになっています。
しかしこの制限は一度ユーザー操作による再生を行ってしまえば解除され、あとは好きなタイミングで鳴らすことが可能となります（アンロック状態）。

iOS向けにブラウザ対応音楽ゲームを制作していて、この制限に引っかかることがよくあったので、具体的なパターンをそれぞれ検証して、どうすればいいかをまとめてみました。

### 前提など
* 音の再生にはWeb Audio APIを使用
* iOSは12.5.5および15.3.1 Safariで動作確認
* PCでもChrome/Chromium系ブラウザとFirefoxで動作確認
    * Chrome/Chromium系ブラウザでも[アンロックが必要](https://developer.chrome.com/blog/autoplay/#web-audio)ですが、iOSに比べると条件が緩いです。詳しくは記事後半[おまけ](#おまけchromium系ブラウザのアンロックについて)で
* （Androidについては本記事では対象ではないのと、まともな端末が手元に無くてほとんど未検証ですが、おそらくiOSの制限よりは緩いはず…）


# 本編
アンロック処理の基本について三行で言うと
- アンロックの条件は「ユーザーイベントのコールバック（ハンドラ）で一度でも音を鳴らす」こと。（無音でも良い）
- ユーザーイベントとはtouchstart, touchend, click等（iOSのバージョンによって有効なイベントが多少違う？）
- ただしユーザーイベントハンドラ内であっても、非同期処理をはさんでしまうとNG

### サンプルコードについて
以下のサンプルコード中、```wa```なるものが出てきますが、これは本記事用に作ったwebAudio用の即席ライブラリです。再生、音源ロード、無音再生はこちらを通して行ってます。
詳細なコードは[リポジトリ](https://github.com/pentamania/ios-unlock-test)をご覧下さい。


## 事前ロード編
ページ読み込み時にロード（以下、事前ロード）を行った場合
**（以下、見出しをクリックするとデモページに飛びます。音量注意）**

### [パターン１-１:  事前ロード -> コールバックで再生処理](https://pentamania.github.io/ios-unlock-test/cases/preload1.html)

```js
window.onload = function() {
  // ページ読み込みと同時にロード
  wa.loadFile("./assets/sample.mp3", function(buffer) {
    wa.play(buffer);
  });
}
```
**再生されない。**
ユーザー操作を介していないため。

### [パターン１-２:  事前ロード -> コールバックで再生処理、さらにタップイベントに無音再生を仕込む](https://pentamania.github.io/ios-unlock-test/cases/preload2.html)

```js
window.onload = function() {
  // ページ読み込みと同時にロード
  wa.loadFile("./assets/sample.mp3", function(buffer) {
    wa.play(buffer);
    
    // ユーザーイベント
    var event = "click";
    document.addEventListener(event, function() {
      wa.playSilent();
    });
  });
}
```

**タップすると再生が始まる。**
処理がサスペンドされているようで、無音再生によってロックが解除され、再生できる状態になると鳴り出す。

### [パターン１-３:  事前ロード後、タップイベントにサウンド再生を仕込む](https://pentamania.github.io/ios-unlock-test/cases/preload3.html)

```js
window.onload = function() {
    // ページ読み込みと同時にロード
    wa.loadFile("./assets/sample.mp3", function(buffer) {
      // ユーザーイベント
      var event = "click";
      document.addEventListener(event, function() {
        wa.play("sample.mp3");
      });
    });
  }
```

**タップすると再生される。**

### [パターン１-４:  事前ロード後、タップイベントに非同期処理を挟んだサウンド再生を仕込む](https://pentamania.github.io/ios-unlock-test/cases/preload4.html)

```js
 window.onload = function() {
    // ページ読み込みと同時にロード
    wa.loadFile("./assets/sample.mp3", function(buffer) {
      // ユーザーイベント
      var event = "click";
      document.addEventListener(event, function() {
        // 非同期処理後に再生
        wa.loadFile("./assets/kick.mp3", function(buffer) {
          wa.play("sample.mp3");
        });
      });
    });
  }
```

**タップしても再生されない。**
非同期処理をはさんでいるため。
（非同期処理としてsetTimeoutを使った場合、待機時間によって鳴ったり鳴らなかったりする？）

### [パターン１-５:  事前ロード後、タップイベントに無音再生＆非同期処理を挟んだサウンド再生を仕込む](https://pentamania.github.io/ios-unlock-test/cases/preload5.html)

```js
window.onload = function() {
    // ページ読み込みと同時にロード
    wa.loadFile("./assets/sample.mp3", function(buffer) {
      // ユーザーイベント
      var event = "click";
      document.addEventListener(event, function() {
        // 無音再生
        wa.playSilent();
        // 非同期処理後に再生
        wa.loadFile("./assets/kick.mp3", function(buffer) {
          wa.play("sample.mp3");
        });
      });
    });
  }
```

**タップすると（非同期処理後に）再生される。**
一度解除してしまえばその後どんなタイミングでも再生OK。


## 動的ロード編
ユーザイベントを受けて動的にロード（以下、動的ロード）を行う場合

### [パターン２-１:  タップ -> ロード -> コールバックで再生処理](//pentamania.github.io/ios-unlock-test/cases/dynaload1.html)

```js
var event = "click";
  document.addEventListener(event, function() {
    // ロード後コールバック再生
    wa.loadFile("./assets/sample.mp3", function(buffer) {
      wa.play(buffer);
    });
  });

```

**再生されない。**
ロードが非同期処理なので。

### [パターン２-２:  タップ -> ロード時に無音再生 -> コールバックで再生処理](//pentamania.github.io/ios-unlock-test/cases/dynaload2.html)

```js
 var event = "click";
  document.addEventListener(event, function() {
    // 無音再生
    wa.playSilent();

    // ロード後コールバック再生
    wa.loadFile("./assets/sample.mp3", function(buffer) {
      wa.play(buffer);
    });
  });
```


**タップで再生される。**
一度解除してしまえば以下略


## その他引っかかりそうな点
* アンロックされるのは再生を行ったAudioContextインスタンスのみのようです。
例えば利用しているライブラリにサウンド再生機能があった場合、その再生に使われるAudioContextオブジェクトに対してアンロック処理をしなければいけません。この場合、自分で ```new AudioContext()```で新しくcontextを作って再生しても意味がないことに注意。

* howler.jsなどのオーディオ系ライブラリ側で勝手に無音再生イベントを仕込んでくれることもあり、意識しなくてもアンロックされていることもあります。参考：[howler.jsソースコードのアンロック処理部分](https://github.com/goldfire/howler.js/blob/dab32ba9e4f2f8059613cdaa8836dd336f2bdc7e/src/howler.core.js#L301)

* 「発火イベントはtouchstart（もしくはtouchend）でなければならない」とされることもありますが、iOS9.01はどうやらclickでも大丈夫のようです。ただ、古いiOSも対応しているか不明のため、一応touchendに仕込んだほうがいいかもしれない。

* 端末の不具合か何かによってアンロックが効かないという現象に遭遇しました（iOS 15.3.1にて、原因不明）。その場合は端末再起動などで状態をリフレッシュするとよいかもしれません。

## まとめ
* 基本的にはロードは事前に済ませ、どこか適当なタイミングで一度ユーザ操作を誘導して無音を鳴らす。
* 動的に音源をロードしなければならないときは、非同期処理に気をつける。
* ライブラリを利用する場合はAudioContextの扱いに気をつける。

## おまけ：chromium系ブラウザのアンロックについて
chromium系ブラウザはiOSに比べると条件が緩めでユーザー操作で非同期処理をはさんでいてもOKだったりします。

また同じドメインのリンクから飛んできた場合は最初からアンロックされた状態となります。
例えばこの記事から[パターン1-2](https://pentamania.github.io/ios-unlock-test/cases/preload2.html)のページに直接遷移しても音は再生されませんが、[一覧ページ](https://pentamania.github.io/ios-unlock-test/cases/index.html)から同URLリンクを踏んで遷移すると即座に再生されるということが起こります。

[またMedia Engagement Indexなる数値が一定要件を満たしているサイトについては、最初からアンロックされていることもあるようです](https://developer.chrome.com/blog/autoplay/#media-engagement-index)。（デスクトップのみ）

## 参考
[iOSのMobile Safari でWeb Audio API を利用したサウンドが再生されない (タッチ制約による制限)](https://www.ipentec.com/document/document.aspx?page=software-mobile-safari-ios-web-audio-api-touch-limit)