Safari で Ogg を再生する



# tl;dr

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

# 背景

ご存知の通り [Ebiten](https://hajimehoshi.github.io/ebiten) はマルチプラットフォームな 2D ゲームライブラリです。どのプラットフォームでもなるべく同様に動く様、日々努力しています。実際これを実現するのはなかなか大変です。具体的には [Safari で Ogg がデコードできません](https://caniuse.com/#feat=ogg-vorbis)。

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

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

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

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

# stbvorbis.js

ということで作りました。 [stbvorbis.js](https://github.com/hajimehoshi/stbvorbis.js) です。ブラウザの Ogg デコーダです。これは以下のような特徴があります。

* [WebAssembly](https://webassembly.org/) を使った高速なデコード: [stb](https://github.com/nothings/stb) という Public Domain な C のライブラリが元になっています。これを [Emscripten](http://kripken.github.io/emscripten-site/docs/compiling/WebAssembly.html) を使って WebAssembly に変換しました。
  * なおフォールバックとして asm.js 版もあります。主にくらむぼんさん (@krmbn0576) による貢献です。ありがとう!
* [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) を使った非同期的なデコード

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

# API

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

実際の使用例は [`example` の `index.html`](https://github.com/hajimehoshi/stbvorbis.js/blob/master/example/index.html) を見ると良いでしょう。

## `decode`

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

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

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

| キー名 | 説明 |
|:--|:--|
| `data` | `Float32Array` の配列で、チャンネルごとのデコードされたストリームを表す。 |
| `sampleRate` | サンプルレート (例: 44100) |
| `eof` | ストリームが終了したら `true`、そうでなければ `false`。これが `true` のときは `data` は `null` である。 |
| `error` | エラーが発生したら文字列、そうでなければ `null`。 |

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

## `decodeStream`

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

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

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

| キー名 | 説明 |
|:--|:--|
| `data` | `Float32Array` の配列で、チャンネルごとのデコードされたストリームを表す。 |
| `sampleRate` | サンプルレート (例: 44100) |
| `eof` | 出力ストリームが終了したら `true`、そうでなければ `false`。これが `true` のときは `data` は `null` である。 |
| `error` | エラーが発生したら文字列、そうでなければ `null`。 |

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

| キー名 | 説明 |
|:--|:--|
| `data` | エンコードストリームを表す `ArrayBuffer` か `Uint8Array`。 |
| `eof` | 入力ストリームが終了したら `true` を、そうでなければ `false`。これが `true` のときは `data` は無視される。 |


# RPG ツクール MV

この stbvorbis.js は RPG ツクール MV のプラグイン [OggOnly.js](https://forum.tkool.jp/index.php?threads/ogg%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A0%E3%81%91%E3%81%A7%E9%9F%B3%E5%A3%B0%E3%82%92%E5%86%8D%E7%94%9F%E3%81%99%E3%82%8B%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3.1099/) としても使用されています。プラグインはくらむぼんさん作です。

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` も使えないような過酷な環境](https://caniuse.com/#feat=fetch)です。 [Babel](https://babeljs.io/) を使ってモダンな JavaScript を古い JavaScript にトランスパイルすることも検討しました。しかし Babel はグローバルな補助関数を挿入してしまい、 `Function.toString` を使って Worker を起動するやり方と非常に相性が悪かったので断念しました。

# 余談

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