Edited at
EbitenDay 18

Safari で Ogg を再生する


tl;dr

Safari は Ogg に対応していないため、再生するために自前でデコードする必要があります。筆者は stbvorbis.js を開発し、 macOS や iOS の Safari で Ogg ファイルを再生することに成功しました。


背景

ご存知の通り Ebiten はマルチプラットフォームな 2D ゲームライブラリです。どのプラットフォームでもなるべく同様に動く様、日々努力しています。実際これを実現するのはなかなか大変です。具体的には Safari で Ogg がデコードできません

一方 MP3 は大抵のブラウザでデコードが可能です。よって Ogg は諦めて MP3 を推すという方向性もあり、一時期は実際にそうしていました。しかし MP3 には開始時に無音区間があり、エンコーダによって長さがバラバラであるという問題があります。これはゲームにおいては特に問題で、ループする曲の場合に無音区間を正確にスキップしないと音が途切れてしまいます。

その他ライセンスがクリーンなフォーマットとしては Opus などがありますが、ライブラリやツールの充実具合は Ogg が一番です。よってここはなるべく Ogg を使いたいところでした。

Ogg デコーダの JavaScript 実装は ogv.js などいくつかあるのですが、「デコードした結果のバイト列を返す」機能を持ち「オーバーキル過ぎない (余計な機能がない)」ライブラリは見つかりませんでした。この要件は Ebiten と組み合わせるには絶対に必要な要件でした。

ちなみに Web ブラウザ以外の環境は Pure Go な Ogg デコーダ実装を使用しています。 Ebiten を Web ブラウザで利用するときには GopherJS を使用します。 Pure Go な Ogg デコーダをこれで変換してもよいのですが、いかんせんパフォーマンスが良くありません。特にモバイルブラウザだと致命的です。


stbvorbis.js

ということで作りました。 stbvorbis.js です。ブラウザの Ogg デコーダです。これは以下のような特徴があります。



  • WebAssembly を使った高速なデコード: stb という Public Domain な C のライブラリが元になっています。これを Emscripten を使って WebAssembly に変換しました。


    • なおフォールバックとして asm.js 版もあります。主にくらむぼんさん (@krmbn0576) による貢献です。ありがとう!




  • WebWorker を使った非同期的なデコード

やっていることは至極単純で、デコード関数が呼ばれるときに Dedicated Worker を 1 個立ち上げ、中で WebAssembly 版 stb vorbis を実行するだけです。


API

Ebiten から利用する場合は stbvorbis.js の API を気にする必要は全くありませんが、 stbvorbis.js の API を参考までに載せておきます。

実際の使用例は exampleindex.html を見ると良いでしょう。


decode

stbvorbis.decode(buf: ArrayBuffer|Uint8Array, callback: function(event: Object))

decode は与えられた Ogg/Vorbis のデータをデコードする。

与えられた callback はデコードが進むかエラーが発生したときに呼ばれる。渡ってくる引数はオブジェクトであり、キーは以下の通り:

キー名
説明

data

Float32Array の配列で、チャンネルごとのデコードされたストリームを表す。

sampleRate
サンプルレート (例: 44100)

eof
ストリームが終了したら true、そうでなければ false。これが true のときは datanull である。

error
エラーが発生したら文字列、そうでなければ null

この関数は単純なのですが、デコード済みデータをまとめて ArrayBuffer に置く必要があり、メモリ効率がよろしくありません。 ReadableStreamResponse 型を取る関数を作っても良かったのですが、後述の「(fetch すら動かないような) iOS 8 でも動くべき」という要件から、「関数を返し、その返り値の関数に入力ストリームチャンクを与えるようにする」という仕様の関数を作りました。


decodeStream

stbvorbis.decodeStream(callback: function(event: Object)): function(event: Object)

decodeStream は与えられた Ogg/Vorbis のデータをデコードする。

与えられた callback はデコードが進むかエラーが発生したときに呼ばれる。渡ってくる引数はオブジェクトであり、キーは以下の通り:

キー名
説明

data

Float32Array の配列で、チャンネルごとのデコードされたストリームを表す。

sampleRate
サンプルレート (例: 44100)

eof
出力ストリームが終了したら true、そうでなければ false。これが true のときは datanull である。

error
エラーが発生したら文字列、そうでなければ null

戻り値の関数は入力ストリームを与えるのに使われる。引き数のオブジェクトのキーは以下の通り:

キー名
説明

data
エンコードストリームを表す ArrayBufferUint8Array

eof
入力ストリームが終了したら true を、そうでなければ false。これが true のときは data は無視される。


RPG ツクール MV

この stbvorbis.js は RPG ツクール MV のプラグイン OggOnly.js としても使用されています。プラグインはくらむぼんさん作です。

RPG ツクール MV はゲーム部分がすべて JavaScript で書かれた純粋な Web アプリケーションです。ゲームはブラウザ上で動きます。音楽フォーマットは Ogg と M4A をサポートしていて、公式素材やツクール向けの素材は両方のフォーマットを用意するのが普通です。理由は当然、 Safari 以外で Ogg、 Safari で M4A を使用するためです。しかし同じ音楽のために 2 種類のフォーマットを用意するのは手間がかかります。またサーバーに置くデータ量も、単純計算ですが、本来必要な量の倍になってしまいます。

OggOnly.js は上記の問題を解決するプラグインです。まだ iOS Safari 上でのパフォーマンスがイマイチですが、利用してくださる方や、くらむぼんさんのフィードバックを受けて日々改善中です。

ちなみに stbvorbis.js は「iOS 8 でも動く」という要件を満たすために、 asm.js バージョンも作っています。また実装も async/await どころか class などを使わずに割と古いスタイルの JavaScript で記述しています。 fetch も使えないような過酷な環境です。 Babel を使ってモダンな JavaScript を古い JavaScript にトランスパイルすることも検討しました。しかし Babel はグローバルな補助関数を挿入してしまい、 Function.toString を使って Worker を起動するやり方と非常に相性が悪かったので断念しました。


余談

自分の書く Ebiten の記事は「Ebiten でこんなのが作れる」というよりは「Ebiten はこうやってできている」というレイヤーの話ばかりですね。果たして需要はあるのでしょうか。私は一体いつゲームを作れるのでしょうか :-P