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

iOSのMobile Safari でWeb Audio API でサウンドが再生されない現象のうち、タッチ制約による制限について紹介します。

タッチ制限

iOSのMobile Safariではタッチ制約があり、最初のstart()メソッドの呼び出しは、ユーザー操作(タッチ)を起点にした実行コンテキストである必要があります。
この制約に従ってコードを記述すると、以下の2パターンのコード記述方法があります。

パターン1:onload でサウンドを読み込み、タップで再生

  • onload イベントで、XMLHttpRequestインスタンスを作成し、send()メソッドを呼び出し、HttpRequestを作成する
  • リンクやボタンのタップで、AudioContextを作成し、HttpRequestから取得したデーターを渡してサウンドを再生する

パターン2:ユーザー操作でサウンドを読み込む

パターン1の方法では、再生できるサウンドの選択肢が増えると、すべてのサウンドに対してHttpRequestを作成する必要があり、準備に長い時間を要します。WiFi環境で、40MB程度のサウンドファイルでも0.5~1秒ほどの準備時間が必要になります。リンクやボタンのタップでサウンドの読み込みをする場合は、以下の処理になります。
  • リンクやボタンのタップで、XMLHttpRequestインスタンスを作成し、send()メソッドを呼び出し、HttpRequestを作成する
  • リンクやボタンのタップで、AudioContextを作成し、HttpRequestから取得したデーターを渡してサウンドを再生する

ここで、XMLHttpRequestを作成し、send()メソッド後のonloadイベント(XMLHttpRequest.onload)でAudioContextの作成と再生ができればよいのですが、XMLHttpRequestのonloadイベントは、ユーザー操作(タッチ)を起点にした実行コンテキストでないため、再生がブロックされてしまいます。

コード例

正しく動作する例 (ページ読み込み時にHttpRequestを準備する場合)

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
	<meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, user-scale=yes, initial-scale=1.0, maximum-scale=5.0" />

  <script type="text/javascript">
    var request, source, stopflag=0;

    window.onload = function () {
      request = new XMLHttpRequest();
      request.open("GET", "sound2.mp3", true);
      request.responseType = "arraybuffer";
      request.onload = completeOnLoad;
      request.send();
    };

    function completeOnLoad() {
      window.AudioContext = window.AudioContext || window.webkitAudioContext;
      context = new AudioContext();
      
      // オーディオをデコード
      context.decodeAudioData(request.response, function (buf) {
        source.buffer = buf;
        source.loop = false;

      });

      source = context.createBufferSource();

      var elem = document.getElementById("Play");
      elem.addEventListener("click", playStart, false);

      var elem2 = document.getElementById("Pause");
      elem2.addEventListener("click", playPause, false);
    }

    function playStart() {
      source.connect(context.destination);
      source.start(0);
    }

    function playPause() {
        if (stopflag == 0) {
            context.suspend();
            stopflag = 1;
        } else {
            context.resume();
            stopflag = 0;
        }
    }

  </script>

</head>
<body>
  <a id="Play" href="javascript:void(0);">再生</a><br />
  <a id="Pause" href="javascript:void(0);">停止/再開</a><br />
</body>
</html>

実行結果はこちらのページを参照してください。

正しく動作する例 (リンククリック時にHttpRequestを準備し、再生をクリックでサウンド再生)

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
	<meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, user-scale=yes, initial-scale=1.0, maximum-scale=5.0" />

  <script type="text/javascript">
    var request, source, stopflag = 0, status = 0;

    function playSound() {
      if (status == 0) {
        status = 1;
        request = new XMLHttpRequest();
        request.open("GET", "sound2.mp3", true);
        request.responseType = "arraybuffer";
        request.onload = completeOnLoad;
        request.send();
      }
      else if (status == 2) {
        window.AudioContext = window.AudioContext || window.webkitAudioContext;
        context = new AudioContext();

        source = context.createBufferSource();

        // オーディオをデコード
        context.decodeAudioData(request.response, function (buf) {
          source.buffer = buf;
          source.loop = false;
          source.connect(context.destination);
          source.start(0);
        });
      }
    }

    function completeOnLoad() {
      status = 2;
      var elem = document.getElementById("Play");
      elem.innerText = "再生";
    }


    function playPause() {
        if (stopflag == 0) {
            context.suspend();
            stopflag = 1;
        } else {
            context.resume();
            stopflag = 0;
        }
    }
  </script>

</head>
<body>
  <a id="Play" href="javascript:void(0);" onclick="playSound();">読み込み</a><br />
  <a id="Pause" href="javascript:void(0);" onclick="playPause();">停止/再開</a><br />
</body>
</html>

実行結果

上記のHTMLファイルをWebブラウザで表示します。下図のページが表示されます。


[読み込み]のリンクをクリックするとサウンドファイルが読み込まれます。読み込みが完了するとリンクが[再生]に変わります。


[再生]のリンクをクリックするとサウンドの再生が始まります。


[停止/再開]リンクをクリックするとサウンドが停止します。もう一度クリックすると停止した位置から再生が再開します。


上記のコードでは、サウンドが再生されますが、読み込みに一度タップし、読み込み後にもう一回「再生」リンクをタップするため、操作が2度になってしまいます。

ブロックされてしまう例

上記の問題を解決するため、サウンド再生開始をXMLHttpRequestのonloadイベントに移すと、iOSのMobile Safari, iOSのGoogle Chromeでは タッチ制約で再生がブロックされてしまいます。
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title></title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, user-scale=yes, initial-scale=1.0, maximum-scale=5.0" />

  <script type="text/javascript">
    var request, source, stopflag = 0;

    function playSound() {
      request = new XMLHttpRequest();
      request.open("GET", "sound2.mp3", true);
      request.responseType = "arraybuffer";
      request.onload = completeOnLoad;
      request.send();
    }

    function completeOnLoad() {
      var elem = document.getElementById("Play");
      elem.innerText = "再生中";

      window.AudioContext = window.AudioContext || window.webkitAudioContext;
      context = new AudioContext();

      source = context.createBufferSource();

      // オーディオをデコード
      context.decodeAudioData(request.response, function (buf) {
        source.buffer = buf;
        source.loop = false;
        source.connect(context.destination);
        source.start(0);
      });

    }

    function playPause() {
      if (stopflag == 0) {
        context.suspend();
        stopflag = 1;
      } else {
        context.resume();
        stopflag = 0;
      }
    }
  </script>

</head>
<body>
  <a id="Play" href="javascript:void(0);" onclick="playSound();">再生</a><br />
  <a id="Pause" href="javascript:void(0);" onclick="playPause();">停止/再開</a><br />
</body>
</html>

回避策

この問題を回避する方法として、リンクをタップしてサウンドを読み込むと同時に、AudioContextを作成し、一度再生しておく回避策があります。タップ操作で発生するイベント内に、AudioContextの作成とcreateBufferSource()の作成、BufferSourceの再生をあらかじめ実行し、その後、本来再生するファイルを読み込みます。一度BufferSourceが再生されているため、XMLHttpRequestのonloadイベントでstartメソッドを実行してもブロックされずにサウンドが再生できます。

コード例

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title></title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, user-scale=yes, initial-scale=1.0, maximum-scale=5.0" />

  <script type="text/javascript">

    //iOS対応版
    var request;
    var status = 0;
    var stopflag = 0;

    function playSound() {
      window.AudioContext = window.AudioContext || window.webkitAudioContext;
      context = new AudioContext();
      context.createBufferSource().start(0);

      request = new XMLHttpRequest();
      request.open("GET", "sound2.mp3", true);
      request.responseType = "arraybuffer";
      request.onload = completeOnLoad;
      request.send();
    }

    function completeOnLoad() {
      var elem = document.getElementById("Play");
      elem.innerText = "再生中";
      source = context.createBufferSource();

      // オーディオをデコード
      context.decodeAudioData(request.response, function (buf) {
        source.buffer = buf;
        source.loop = false;
        source.connect(context.destination);
        source.start(0);
      });
    }

    function playPause() {
      if (stopflag == 0) {
        context.suspend();
        stopflag = 1;
      } else {
        context.resume();
        stopflag = 0;
      }
    }
  </script>

</head>
<body>
  <a id="Play" href="javascript:void(0);" onclick="playSound();">読み込み</a><br />
  <a id="Pasue" href="javascript:void(0);" onclick="playPause();">停止/再開</a><br />
</body>
</html>

解説

対策版では、playSound()関数の冒頭で、AudioContextオブジェクトを作成し、作成直後の状態でcreateBufferSource() メソッドを呼び出し空のバッファソースを作成します。
作成したバッファソースのstart() メソッドを呼び出し空のサウンドを再生します。
一度BufferSourceが再生されているため、XMLHttpRequestのonloadイベントでstartメソッドを実行してもブロックされずにサウンドが再生できる状態になります。
うまく動作しない場合のplaySound() 関数
  function playSound() {
      request = new XMLHttpRequest();
      request.open("GET", "sound2.mp3", true);
      request.responseType = "arraybuffer";
      request.onload = completeOnLoad;
      request.send();
    }
対策版のplaySound() 関数
  function playSound() {
      window.AudioContext = window.AudioContext || window.webkitAudioContext;
      context = new AudioContext();
      context.createBufferSource().start(0);

      request = new XMLHttpRequest();
      request.open("GET", "sound2.mp3", true);
      request.responseType = "arraybuffer";
      request.onload = completeOnLoad;
      request.send();
    }
補足
Mac OS X のSafariでも同様の現象が発生します。

実行結果

上記のHTMLファイルをiOSのWebブラウザで開きます。下図のページが表示されます。
[読み込み]リンクをタップします。タップするとリンクが[再生中]に変化しサウンドが再生されます。[停止/再開]リンクをタップするとサウンドの再生が一時停止します。


補足

iOSのバージョンによっては、事前の再生が無い場合でもサウンド再生ができる場合があります。
下記のHTMLファイルでは、playSound 関数内にオーディオコンテキストの作成コードや context.createBufferSource().start(0); のコードがありませんが、 iOS(iOS 14)では[読み込み]リンクのタップによりサウンドを再生できます。
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title></title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, user-scale=yes, initial-scale=1.0, maximum-scale=5.0" />

  <script type="text/javascript">

    //iOS対応版
    var request;
    var status = 0;
    var stopflag = 0;
    var context;

    function playSound() {
      request = new XMLHttpRequest();
      request.open("GET", "sound2.mp3", true);
      request.responseType = "arraybuffer";
      request.onload = completeOnLoad;
      request.send();
    }

    function completeOnLoad() {
      var elem = document.getElementById("Play");
      elem.innerText = "再生中";

      window.AudioContext = window.AudioContext || window.webkitAudioContext;
      context = new AudioContext();
      source = context.createBufferSource();

      // オーディオをデコード
      context.decodeAudioData(request.response, function (buf) {
        source.buffer = buf;
        source.loop = false;
        source.connect(context.destination);
        source.start(0);
      });
    }

    function playPause() {
      if (stopflag == 0) {
        context.suspend();
        stopflag = 1;
      } else {
        context.resume();
        stopflag = 0;
      }
    }
  </script>

</head>
<body>
  <a id="Play" href="javascript:void(0);" onclick="playSound();">読み込み</a><br />
  <a id="Pasue" href="javascript:void(0);" onclick="playPause();">停止/再開</a><br />
</body>
</html>
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2021-03-28
作成日: 2015-10-17
iPentec all rights reserverd.