go-mp3: Pure Go な MP3 デコーダー

# tl;dr (要約)

Go の MP3 デコーダー実装がなかったので、既存の C のものを移植し、 [go-mp3](https://github.com/hajimehoshi/go-mp3) というライブラリを作りました。

# 背景

Pure Go な実装の MP3 デコーダーを探したのですが、自分の探す限りでは見つかりませんでした。 [tcolgate/mp3](https://github.com/tcolgate/mp3) が最も近かろうと思われましたが、これはどうやらコンテナをデコードするのみのようです。

ないものはしょうがないので、自分で実装することにしました。フルスクラッチで実装するモチベーションはなかったため、既存の別言語の物を転用することにしました。今回は [PDMP3](https://github.com/technosaurus/PDMP3) という Public Domain なデコーダーを採用しました。 PDMP3 は C で書かれていますが、これを手で Go に移植したわけです。できあがったものが [go-mp3](https://github.com/hajimehoshi/go-mp3) です。

# 移植

移植にあたっては cgo を使って、まず PDMP3 素の状態で Go から叩けることを確認しました。最初はこの MP3 デコーダーは自作ゲームライブラリ [Ebiten](https://hajimehoshi.github.io/ebiten/) の 1 ライブラリとして実装していたのですが、デコーダーを使ったオーディオのサンプルがそのまま動くことを確認しました。[最初のコミット](https://github.com/hajimehoshi/ebiten/commit/bf3f6c9ff6c75b14d484a385265c562c5a404448) の時点で、 C ファイルがほぼそのまま使われています。

実際に動くのを確認した上で、関数 1 個 1 個を C から Go に移植していきました ([変更履歴](https://github.com/hajimehoshi/ebiten/commits/88c5e4ca2d52863c5e6075cfb8b79f22517413df))。どのコミットをとっても概ね動くようになっています。

たまに実装をミスって、[条件演算子のコンバートを間違えてしまって高音が変な音になっていたり](https://github.com/hajimehoshi/ebiten/commit/bf9a25956580283c814d91d9fd057de1c5afd76e)、 [LR が逆になってしまったり](https://github.com/hajimehoshi/ebiten/commit/76320010f441c03dbb1e3b34f82c64c097233a76)を直す必要がありました。本来はユニットテストがあるのが望ましいのですが、今回は自分の耳を頼りにテストしました。 LR 逆転はしばらく気づかなかったので危なかったですね。実によろしくない。今思えばオリジナルの状態の出力を正として自動テストができましたね。

あとは、オリジナル実装はグローバル変数を多用しており、デコード状態を同時に 1 個しか持てませんでした。可読性も低かったので、これを適宜構造体のメンバに移動したりなどしました。例えば [`v_vec` というグローバル変数がデコード状態に使われていたのを、 `frame` 構造体に移しました](https://github.com/hajimehoshi/ebiten/commit/8efd0e725ff6f6f4c1ce18b95ff0320911de42d3)。

他細かい話ですと、一部再生できなかった MP3 があったのを修正しました。すでに[最初のコミット](https://github.com/hajimehoshi/ebiten/commit/bf3f6c9ff6c75b14d484a385265c562c5a404448)の時点で反映済みで、 [`is_header`](https://github.com/hajimehoshi/ebiten/blob/bf3f6c9ff6c75b14d484a385265c562c5a404448/audio/mp3/pdmp3.c#L1159) という関数がそれです。 MP3 はフレームという最小単位がたくさん並んで構成されます。デコーダーは、先頭のビット列を見て特定のビットの並びならばフレームの開始とみなします。オリジナル実装ではヘッダの解析が仕様どおり過ぎたせいか、「その特定のビットの並びだが実はフレームの開始ではない」 MP3 ファイルをうまく解析できませんでした。解析の際、追加で別のビットフィールドをみて、そのフィールドとしてあり得ない値が来るのであれば、フレーム開始でないと判断するようにしました。果たしてこれで正解なのか正直良くわかりません。というか MP3 は確実なフレーム開始判定方法がないように思われるのですがどうなんでしょう。

とりあえず動けば良いというノリで移植したため、コードが雑然としています。時間があればいずれ整理したいと考えています。

# そもそもなぜ MP3 デコーダーが必要だったのか

ゲーム開発のために、デスクトップとモダンブラウザ両方で動く音楽フォーマットのデコーダーが欲しかったからです。

最初は Ogg/Vorbis メインでいこうと思っていました。 Ogg/Vorbis はライセンスフリーですし、 [Go のデコーダー実装](https://github.com/jfreymuth/oggvorbis)もあってよかったのですが、 [Safari や Edge が対応していません](http://caniuse.com/#feat=ogg-vorbis)。 同じくライセンスフリーな Opus も似たような状況で、 [Safari が対応していません](http://caniuse.com/#feat=opus)。

MP3 や AAC といったフォーマットなら[大体のブラウザが対応](http://caniuse.com/#search=mp3)しています。  AAC は特許およびライセンス料支払い義務が存在しますが、 MP3 は今年になって特許の殆どが期限切れとなり、[ライセンス料を支払う必要がなくなりました](https://www.iis.fraunhofer.de/en/ff/amm/prod/audiocodec/audiocodecs/mp3.html)。

# おまけ: `io.Copy` による MP3 プレイヤー

過去に [Oto](https://github.com/hajimehoshi/oto) というマルチプラットフォームなサウンド再生ライブラリを作りました。 Oto は `io.Writer` な `Player` を提供し、 PCM バイト列を書き込むことで音が再生できます。

go-mp3 が `io.Reader`、 Oto が `io.Writer` を提供するので、 `io.Copy` でつなぐことによって MP3 を再生することが出来ました。

https://github.com/hajimehoshi/go-mp3/blob/master/example/main.go (要 cgo) (追記: Windows で cgo は不要になりました!)

```go
package main

import (
	"io"
	"log"
	"os"

	"github.com/hajimehoshi/oto"

	"github.com/hajimehoshi/go-mp3"
)

func run() error {
	f, err := os.Open("classic.mp3")
	if err != nil {
		return err
	}
	defer f.Close()
	d, err := mp3.Decode(f)
	if err != nil {
		return err
	}
	defer d.Close()
	p, err := oto.NewPlayer(d.SampleRate(), 2, 2, 65536)
	if err != nil {
		return err
	}
	defer p.Close()
	if _, err := io.Copy(p, d); err != nil {
		return err
	}
	return nil
}

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}
```

これで `go run main.go` とすると音がなります。楽しい!