Tech Sketch Bucket of Technical Chips by TIS Inc.

JavaScriptでバイナリファイルを扱う方法

Pocket

HTML5によって、<audio>や<video>などメディアファイルを扱う機能が拡充され、JavaScriptからも音声ファイルが取り扱えるようになりました。
各ブラウザ毎に対応状況は異なりますが、これらの機能を有効活用するためAjaxリクエストによって音楽のバイナリファイルを取得し、再生するまでのJavaScriptをまとめてみました。


はじめに

HTML5によって、<audio>や<video>などメディアファイルを扱う機能が拡充され、JavaScriptからも音声ファイルが取り扱えるようになりました。
ということで、各ブラウザ毎に対応状況は異なりますが、Ajaxリクエストによって音楽のバイナリファイルを取得し、再生するまでのJavaScriptをまとめてみました。サンプルは音声ファイルの受信、再生をCoffee Scriptで書いたものですが、バイナリファイルを取得した後の処理だけ変更すれば画像ファイルなどの受信にも使用できると思います。

サンプルの前提条件は以下のようになります。

  • 音声ファイルにGETアクセスすることはできません(というか、それが出来るならnew Audio(URL)で済んでしまうので)
  • パラメータ付きPOSTでリクエストするとContent-Type:audio/wavでバイナリが返ります
  • Firefox 15/Chrome 21で動くものにします

【方法1】jQuery + x-user-defined + charCodeAt/fromCharCode + 文字列結合

まず一つ目の方法ですが、どのプロジェクトにもあるであろうjQueryを使ってAjax通信を行い、バイナリファイルを取得する方法です。
残念ながらjQueryはバイナリの受信に向かない作りになっており、XHRをそのまま使う方法に比べると重い処理となります。直接扱えるようなパッチが提案されたこと( http://bugs.jquery.com/ticket/11461 )もあるようですが、互換性の問題で止められており、先は遠そうです。

サンプルソース:

サンプルソースの説明

[1] MimeTypeの上書き

beforeSendの中で直接MimeTypeを指定し、サーバから返ってきたMimeTypeを上書きするように設定します。
text/plainとしてバイナリデータを受信した場合、charsetの指定が無い場合にはバイナリファイルの中に含まれる0x80~0xFFが全て0xFFFD(65533)として受信される為、バイナリファイルを正常に取得することができません。そのため、このサンプルではoverrideMimeTypeを使用してcharset=x-user-definedを指定しています。それによって、以下のように変換が行われるようになります。

  • ASCII文字の範囲(0x00~0x7F)はそのまま(U+0000~U+007F)
  • それ以外(0x80~0xFF)はユニコードの私用領域(U+F780~U+F7FF)に割り当てる

全てが0xFFFDになってしまった場合にはどうやっても元のバイナリデータを取り出すことができませんが、この場合であれば0x80~0xFFのデータに関しても下位バイトを取り出すことでバイナリデータを正常に受信することができます。

[2] dataTypeの指定

最終的に欲しいデータの型としてbinaryを指定します。
convertersと対応していれば別にbinaryでなくても構いません。

[3] 受信データ→binaryのconverterを設定

受信したデータをdataTypeに指定された型に変換する際、converterが使われます。
今回は「* → binary」のconverterを指定しており、dataTypeにbinaryを指定しているので、どんなデータを受信したとしてもこのconverterが使われます。

「[1] mimeTypeの上書き」で書いたとおり、0x80~0xFFのデータが0xF780~0xF7FFに割り当てられている為、下位バイトを取り出してからadjustedResponseに結合しています。文字列結合が大量に発生しますが、160kbyteのデータを使って試してみたところ、Chromeでは14ms、Firefoxでは25ms程度と、これぐらいの容量であればそう気にならないようです。

下位バイトの取り出しは単なる論理演算なので、大容量ファイルなど結合にかかる時間が気になる場合にはfromCharCodeの呼び出しを工夫した方が効果があるようです。例えば、以下のように一定バイト数毎にまとめて処理をするようにconverterを書き換えると、ちょっと速くなります。
残念ながらGoogle Chromeではapplyで展開できる数に制限があるようで、受信データ全体をまとめて処理することはできませんが、2048バイト毎に区切って実行するだけでも1文字ずつ結合するよりは速くなりました。

  • 4Mbyteのデータの場合
    • Firefox 700ms前後 -> 620ms前後
    • Chrome 1600ms前後 -> 670ms前後

[4] Audioの生成、再生

ブラウザの音声ファイル対応にはかなりばらつきがありますが、FirefoxやChromeであればBase64変換したWavファイルに対応している為、受信したバイナリデータをDataURI形式で指定することで音を再生することができます。

【方法2】XHR + responseType + ArrayBuffer + Blob + createObjectURI

方法1ではjQueryを使って受信する方式でしたが、jQueryがバイナリに対応していない為に色々と無理が出ていました。
ブラウザによって対応状況に違いはありますが、素のXHRであればバイナリデータをそのまま受信することができるため、jQueryに頼らずにXHRをそのまま使う方法もあります。
高速な処理が可能ですが、以下2点に注意してください。

  • ブラウザによってArrayBufferやURL#createObjectURLの実装状況が異なります(Firefox 14, Chrome21で動作確認)
  • jQueryを用いていない為、ブラウザ毎の動作の違いを吸収するロジックも自分で記述する必要があります

[1] responseTypeの指定

レスポンスをArrayBufferで受け取れるようにresponseTypeを指定します。

[2] 受信データを元にArrayBufferViewを作成

responseTypeを指定したことによって受信データはArrayBuffer型になりますが、このままではデータの取り扱いが不便な為、ArrayBufferViewのサブクラスであるUint8Arrayとして扱うようにしています。

[3] ArrayBufferViewを元にBlobを作成

ArrayBufferView(のサブクラスであるUint8Array)とMimeTypeを渡すことでBlobオブジェクトを作成します。
Firefox15で修正済みですが、Firefox14ではArrayBufferViewが指定できないバグがあった為、Firefox14以下に対応する場合はview(ArrayBufferView)の代わりにview.buffer(ArrayBuffer)を指定する必要があります。(Chromeでは警告が出ますが動作自体は問題有りません)

[4] URL#createObjectURLでBlobをURL化

方法1の場合はDataURIを使って音楽データをAudioに渡していましたが、この方法だとBase64変換による容量増加もある為、大容量ファイルの場合に大きなメモリを消費してしまいます。createObjectURLを使うことでBlobオブジェクトを指し示す一時的なURL(例:blob:http%3A//【IPアドレス】/fc460c3f-a016-48e8-afab-5baba2cc3081)を生成でき、これをAudioクラスに渡すことでメモリ消費、CPU消費の少ないストリーミング再生を行うことができます。
(といっても今回の作りでは先にデータを全部受信している為、ストリーミング再生の効果は薄いですが)

参考: http://d.hatena.ne.jp/itchyny/20111119/1321710966

まとめ

  • jQuery使うならcharset=x-user-defined必須。converterを書こう
  • XHR使ったほうが早くてきれい。でも対応ブラウザ制限には気をつけて
  • 音楽再生に限るのならばsoundmanager2( http://www.schillmania.com/projects/soundmanager2/ )とか使うのも手
エンジニア採用中!私たちと一緒に働いてみませんか?