[HTML5] 音声ファイルの事前ダウンロード - audioタグ編

前回はaudioタグをJavaScriptで操作しましたが、事前にダウンロードが完了した状態で再生を開始することができませんでした。例えばボタンをクリックした瞬間に鳴らす場合にラグが発生したり、再生途中で停止(ダウンロード待ち)する恐れがあります。今回はダウンロードが完了しないと音声が再生できないような処理を追加します。

この手の処理はWebAudioAPIを用いたサンプルがネット上には転がっていますが、単にダウンロードされたことを検知したいだけで利用するには気が重いというか正直などころ面倒なので、前回同様audioタグを活かす方向で作成します。

音声ファイルの事前ダウンロード

実行例

以下から実際のサンプルをお試しいただけます。音声ファイルのダウンロードが終わるまで「NowLoading」と表示され、完了すると再生ボタンが現れます。 miku3.net

前回と同様に音声ファイルは「魔王魂」さんからお借りしました。

ソース

ボタンの上に表示されるアイコンはFontAwesomeを利用しています。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>HTML5 Audio Sample2</title>
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <style>
    #contents{display: none}
    #btn-play{ width:100px; height:40px; padding:10px; font-size:18px; }
  </style>
</head>
<body>

<h1>HTML5 Audio Sample2</h1>

<!-- 音声ファイル -->
<audio id="bgm1"></audio>

<!-- ローディング中表示 -->
<div id="nowloading">
  Now Loading...
</div>
<!-- /ローディング中 -->

<!-- 再生ボタン(ダウンロードが完了するまで非表示) -->
<div id="contents">
  <button id="btn-play" type="button"><i class="fas fa-play"></i></button>
</div>
<!-- /再生ボタン -->

<script>
  const bgm1 = document.querySelector("#bgm1");      // <audio>
  const btn  = document.querySelector("#btn-play");  // <button>

  /**
   * [event] ページ読み込み完了時に実行
   */
  window.onload = () =>{
    const nowloading = document.querySelector("#nowloading");  // NowLoading部分
    const contents   = document.querySelector("#contents");    // 再生ボタンの部分

    // MP3をサーバから取得
    loadMP3("op.mp3.base64", bgm1, ()=>{
      nowloading.style.display = "none";    // Now Loadingを非表示
      contents.style.display   = "block";   // 再生ボタンを表示
    });
  }

  /**
   * Base64エンコードされたMP3を取得
   *
   * @param url        {string}   - 取得するMP3ファイルのURL
   * @param audio      {object}   - <audio>タグのオブジェクト
   * @param [callback] {function} - 成功時に実行する関数(オプション)
   * @param [fail]     {function} - 失敗時に実行する関数(オプション)
   */
  function loadMP3(url, audio, callback=null, fail=null){
    fetch(url)
      .then( (response)=>{
        return response.text();  // Base64はテキスト情報なのでtext()で処理
      })
      .then( (text)=>{
        // <audio>にDataURIをセット
        audio.setAttribute("src", `data:audio/mp3;base64,${text}`);

        // callback関数を実行
        if( callback !== null ){
          callback();
        }
      })
      .catch((error)=>{
        console.log(`[error]loadMP3(): ${error} (${url})`);

        if( fail !== null ){
          fail(error);
        }
      });
  }


  /**
   * [event] 再生ボタンクリック時に実行
   */
  btn.addEventListener("click", ()=>{
    // pausedがtrue=>停止, false=>再生中
    if( ! bgm1.paused ){
      btn.innerHTML = '<i class="fas fa-play"></i>';  //「再生ボタン」に変更
      bgm1.pause();
    }
    else{
      btn.innerHTML = '<i class="fas fa-pause"></i>';  //「一時停止ボタン」に変更
      bgm1.play();
    }
  });

  /**
   * [event] 再生終了時に実行
   */
  bgm1.addEventListener("ended", ()=>{
    bgm1.currentTime = 0;  // 再生位置を先頭に移動
    btn.innerHTML = '<i class="fas fa-play"></i>';  // 「再生ボタン」に変更
  });
</script>
</body>
</html>

解説

基本的な考え方

AjaxやFetch APIなどを利用すればファイルをネットワーク越しに取得し、取得し終わったタイミングをこちらで把握することができます。要はこの取ってきたファイルをaudioタグにセットできれば良いということになります。

// Fetch APIの場合
fetch("https://exmaple.com/foo.mp3")
  .then( (response)=>{
    // 取得したデータの中身を取り出して次のthen()に送る
    return response.blob();
  })
  .then( (blob)=>{
    // ファイルを取得し終わると最終的にここが実行される。
    // ここでaudioタグにデータをセットしたい。
  })

FetchAPIの基本的な利用方法は以下の記事を参照ください。 blog.katsubemakito.net

audioタグにデータをセットする

通常audioタグは以下のようにsrc属性にファイルのパスを書くわけですが、DataURIを使用するとsrc属性に音声データを埋め込むことができます。

<audio src="file.mp3"></audio>

こんなイメージです。最終的に「音声データ」とある場所をFetchAPIなどで取ってきて置き換える寸法。

<audio src="data:audio/mp3;base64,(音声データ)"></audio>

audioタグにsrc属性を追加するコードは以下の通り。

<audio id="bgm1"></audio>

<script>
  const bgm1 = document.querySelector("#bgm1");  // <audio>
  const data = "(音声データ)";  // 代入する値はFetchAPIなどで取ってくる

  // src属性をセットする
  bgm1.setAttribute("src", `data:audio/mp3;base64,${data}`);
</script>

ここまでで主要な登場人物は全員登場しました。このあと問題になるのはDataURIにするためにBase64というフォーマットに変換して上げる必要がある点です。

音声ファイルをBase64に変換

Base64に変換するための関数がJavaScriptにも用意されていますが、今回は事前に変換済みのファイルを作成しブラウザ上では取ってくるだけにしたいと思います。

Base64への変換は簡単で、LinuxやmacOSのコマンドラインでbase64コマンドを叩くだけです。これでop.mp3.base64という名前のファイルが出来ますのでこの中身を前述の「音声データ」とあった場所に埋め込みます。

$ base64 op.mp3 > op.mp3.base64

実際に中身をテキストエディタやcatコマンドなどで覗いて見ると以下の通り単なる文字列がずらーと並んでいるのがわかりますね。

$ cat op.mp3.base64
//uQZAAAAAAAaQYAAAAAAA0gwAAAAAABpBwAAAAAADSDgAAA///(中略)

Windowsも標準の機能だけでできるみたいですね。一番最初と最後の行は不要なので削除する必要はあります。 qiita.com

ここまでのまとめ

はい、ではここまでのコードを整理すると以下のようになります。最初のサンプルではこれにエラー処理や画面の表示切り替えなどを加えた物になっており若干長めのコードですが原理は非常にシンプルですね。

<audio id="bgm1"></audio>

<script>
const bgm1 = document.querySelector("#bgm1");  // <audio>

fetch("op.mp3.base64")
  .then( (response)=>{
    return response.text();  // Base64はテキスト情報なのでtext()で処理
  })
  .then( (text)=>{
    // <audio>にDataURIをセット
    bgm1.setAttribute("src", `data:audio/mp3;base64,${text}`);
  });
</script>

この方法の利点と欠点

⭕クライアントの実装が楽

これに付きますw WebAudioAPIは様々なことが行える反面、最初の学習コストも高くコードを書くのもちょっと面倒。今回のように高度な音声処理を行わない、パフォーマンスをそれほど求められない場合、非常に簡便なコードで済ますことができます。またクライアントでBase64へのエンコードを行っていませんので、ブラウザ側の処理をかなり端折れています。

❌ファイル容量が増える

Base64形式に変換するとファイル容量が大きくなります。今回の場合だと2割ほど増えてしまいました。基本的にバイナリの方が軽いですからね。

$ ls -lh
-rw-r--r--   1 katsube  staff   807K  2 20 19:04 op.mp3
-rw-r--r--   1 katsube  staff   1.0M  2 20 20:11 op.mp3.base64

解決方法もあります。サーバからブラウザへ送信する際にGZipなどで圧縮することで、実はオリジナルのファイルと同等か、場合によってはそれよりも軽くすることが可能です。実際に今回利用したファイルを圧縮したら数%ほどですが軽量化に成功しています。

$ gzip op.mp3.base64
$ ls -lh
-rw-r--r--  1 katsube  staff   807K  2 20 19:04 op.mp3
-rw-r--r--  1 katsube  staff   782K  2 20 20:11 op.mp3.base64.gz

Apacheの場合だとmod_deflateモジュールを使うことで自動的にGZipで転送してくれるので、通常はWebサーバの機能で対応します。 httpd.apache.org

❌変換が面倒

音声ファイルを変更する度に毎回コマンドを叩くのが面倒ですね。 Gulpなどのタスクランナーを利用する、Jenkinsなどを使いGitにcommit/pushしたタイミングで自動的に変換するといった方法を取ることで自動化することができます。

❌一部のブラウで容量制限がある

主にマイクロソフト製のブラウザでDataURIの文字列長に制限があります。

IE
8(2009年3月リリース)で32kbyteまで、9〜11で4Gbyteまで
Edge
18(2018年11月リリース)までは4Gbyteまで

IEは緩やかに滅びる方向にありますし、EdgeはGoogle Chromeと同様のwebkitへ刷新されます。よってこのあたりのブラウザをターゲットとしない場合は事実上気にする必要はありません。……ただあまりにでかいファイルはメモリに乗り切らなくなりますし、CPUなどのリソース消費も激しくなりますので程々にw

caniuse.com

参考ページ