JavaScript
iOS
WebAudioAPI

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

はじめに

iOSブラウザではパケ死対策のため、ユーザー操作があるまでオーディオを鳴らさないようになっていますが、一旦再生してしまえば、あとは好きなタイミングで鳴らすことが可能になります(アンロック状態)。
しかしiOS向けのJS製ゲームを制作していて、しょっちゅうこれに引っかかることがあったので、具体的なパターンをそれぞれ検証して、どうすればいいかをまとめてみました。

前提等

  • WebAudioAPIを使用
  • iOS 9.01(iPad Air 1) mobile safariでチェック
  • PCでのチェックはGoogle Chrome 69で確認
追記

Chromeも次バージョンよりユーザー操作が必要になるようです(2018年12月から)

基本

  • アンロックの条件は「ユーザイベントのハンドラで一度でも音を鳴らす」こと。(無音でも良い)
  • ユーザーイベントとはtouchstart, touchend, click等(iOSのバージョンによって有効なイベントが多少違う?)
  • ただしユーザイベントハンドラ内であっても、非同期処理をはさんでしまうとNG。

サンプルコードについて

以下のサンプルコード中、waなるものが出てきますが、これは本記事用に作ったwebAudio用の即席ライブラリです。再生、音源ロード、無音再生はこちらを通して行ってます。
詳細なコードはレポジトリをご覧下さい。

事前ロード編

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

パターン1-1: 事前ロード -> コールバックで再生処理

window.onload = function() {
  // ページ読み込みと同時にロード
  wa.loadFile("./assets/sample.mp3", function(buffer) {
    wa.play(buffer);
  });
}

再生されない。ユーザー操作を介していないため。

パターン1-2: 事前ロード -> コールバックで再生処理、さらにタップイベントに無音再生を仕込む

window.onload = function() {
  // ページ読み込みと同時にロード
  wa.loadFile("./assets/sample.mp3", function(buffer) {
    wa.play(buffer);

    // ユーザーイベント
    var event = "click";
    document.addEventListener(event, function() {
      wa.playSilent();
    });
  });
}

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

パターン1-3: 事前ロード後、タップイベントにサウンド再生を仕込む

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

タップすると再生される。
普通ですね。

パターン1-4: 事前ロード後、タップイベントに非同期処理を挟んだサウンド再生を仕込む

 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の場合、待機時間によって鳴ったり鳴らなかったりする?

パターン1-5: 事前ロード後、タップイベントに無音再生&非同期処理を挟んだサウンド再生を仕込む

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");
        });
      });
    });
  }

タップすると(非同期処理後に)再生される。
一度解除してしまえば非同期だろうとなんだろうと問題なし。

動的ロード編

ユーザイベントを受けて動的にロード(以下、動的ロード)を行う場合

パターン2-1: タップ -> ロード -> コールバックで再生処理

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

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

パターン2-2: タップ -> ロード時に無音再生 -> コールバックで再生処理

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

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

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

その他引っかかりそうな点

  • アンロックされるのは再生を行ったAudioContextインスタンスのみとなる。
    例えば利用しているライブラリにサウンド再生機能があった場合、その再生に使われるコンテキストに対してアンロック処理をしなければならない。
    この場合、自分で new AudioContext()で新しくcontextを作って再生しても意味がないことに注意。

  • howler.jsなど、ライブラリ側で勝手に無音再生イベントを仕込んでくれることもあり、意識しなくてもアンロックされていることもある。

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

まとめ

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

参考

iOSのMobile Safari でWeb Audio API を利用したサウンドが再生されない (タッチ制約による制限)