FFmpeg+SDL で音声再生 (1) 準備編

早々に放置気味ですが. 今日は FFmpeg のライブラリーと SDL を連携して音声を再生する話です. 書いとかないと忘れそうなのでメモ.

使うことになった背景

SDL プログラムで BGM や効果音を再生しようと思ったらほとんどの場合 SDL_mixer を使うと思います. とても便利なライブラリーなのですが, タイミング周りを制御しようとすると使えません. 大抵のゲームでは BGM や効果音は任意のタイミングで再生開始ができてミキシングができればそれでいいのですが, 今作っているゲーム (音ゲー) では音声に同期した処理が必要となりました. そういうわけで SDL_mixer の便利な部分が使えません.

となると SDL のオーディオ部分を直に使わないといけないのです. それは仕方ないんですが, さすがに MP3 やら ogg やらのデコードを自分で書く気にはなりません. で, まあ適当に思いついたのが FFmpeg でした. 対応形式も多いので, 使えたら便利かなと思って手を出した次第. でもドキュメントがすごく適当なので苦戦. というわけでこのメモに行き着きます.

参考文献

FFmpeg+SDL という内容にどんぴしゃりの文書が http://dranger.com/ffmpeg/ にあります. 僕も主にこれを読んでやり方をつかみました. でも少し内容が古く, FFmpeg ライブラリー側では deprecated になっている機能も使っているので注意.

あとは FFmpeg の公式サイトにある doxygen の出力 です. 公式なドキュメントは多分これしかありません. 上のガイドを読んで感じをつかんで, 関数の名前を覚えたらそれを中心に doxygen を漁るのみ. でも結構 C 言語的なオブジェクト指向 (関数ポインターを使うような) が使われまくってて, 処理追いにくいんですよね. それを何度もやりたくないから今回のメモなんですが. ちなみにこのメモではコーディングは C++ で行います. あと動画再生については触れないので注意.

概要

FFmpeg は言わずとしれたメディア変換ソフトですが, そのエンジン部分はライブラリー*1としても公開されています. 今回はそれを使ってメディアファイルをデコードして, SDL のオーディオデバイスに流し込もう, という話. ついでに音声と同期する処理の話も. 疲れてきたら二回に分けるかも.

用語とモノの設定をまず説明. メディアファイルやウェブ上に置かれたメディアデータのことをコンテナーと言います. コンテナーには一つ以上のストリームが入っています. 例えば音声ファイルなら音声ストリーム, 音付きの動画なら動画ストリームと音声ストリームが入っています (この二つは別々にあると考える). ストリームにはフレームがたくさん並んだデータが入っています.

処理の大枠は「ストリームを開き, 順次フレームを読み込んでデコードしながらデバイスに流す」という内容になります. このとき, フレームの読み込みはパケットを単位に行われます. 動画ストリームの場合は 1 パケットに 1 フレームのみ入っていて分かりやすいですが, 音声の場合は形式によります. 各フレームのデータ量が固定の形式では 1 パケットに複数フレーム入ることがあります. MPEG などはフレームごとのデータ量が可変で, この場合は 1 パケットに 1 フレームのみが入っています. 両方に対応したかったら, 1 パケットに複数フレーム入る可能性も考慮してコーディングする必要があります.

デコードはデコーダーで行いますが, 一般にその逆を行うエンコーダーデコーダーを組にして, 合わせてコーデックと呼びます. FFmpeg ライブラリーでもこの役割は AVCodec などといった名前の構造体が果たします.

準備

C++ から FFmpeg ライブラリーを使うための準備. まあまずは http://ffmpeg.org/download.html を参考に subversion やら git やらからソースを落としてビルド, インストールします. 以下, ヘッダー群は にインストールされたと仮定します.

FFmpeg のヘッダーでは C++ は考慮されていません. なのでインクルードするときは

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}

などとします. また C99 で追加された stdint.h を必要とします. これが (現行の) VC++ には入っていなかったりするので, その場合は http://www.kijineko.co.jp/node/63 などからもらってきます. で, 厄介なことにさらにこれが SDL 内での stdint の補完と競合したりするのですが......その場合はどっちかのヘッダーの一部を書き換えるなどしか解決策はないのかな.

コンパイル時には libavcodec.a, libavformat.a, libavutil.a とリンクします (Windows なら拡張子は lib). 実行時には対応する so/dylib/DLL が必要です.

FFmpeg の機能を使うための準備は一瞬です.

av_register_all();

これだけ. 使えるコーデックの情報などを集めたりします. 必要なものだけを呼び出すための関数も用意されていますが, av_register_all() も別に重くないので, 普通はこれで OK.

SDL のオーディオ機能の初期化は次のようにします.

SDL_AudioSpec spec, desired;
desired.freq = 44100;  // or 22050, 11025
desired.format = AUDIO_S16SYS;  // 符号付き 16 ビット, バイトオーダーはシステムに従う
desired.channel = 2;  // ステレオを表す
desired.samples = 1024;  // 1 フレーム中のサンプル数. 2 の累乗で 512 〜 8192 を指定.
desired.callback = &someCallbackFunction;  // データを詰めるコールバック関数
desired.userdata = some_data;  // コールバック関数に毎回渡す void* 型引数
if (SDL_OpenAudio(&desired, &spec) < 0) ...;  // 負ならエラー

freq はサンプリング周波数で, samples はバッファー内のサンプル数です. 各フレーム (callback の呼び出し. 上に書いた FFmpeg の「フレーム」とは別モノ) で samples 個のサンプルを詰める処理をします. たとえば上の設定だと 1 秒間に 44100 / 1024 回のフレームを処理することになります (つまりこの値が音声の FPS となる). freq を大きくすると音質が上がります (44100 は音楽 CD の周波数と同じ) が, 音質は当然元となる音声データにも左右されます. samples を小さくすると FPS が上がり, より反応速度の速い処理を行えますが, 処理が重くなったときに細かいフレーム落ちが発生する恐れも出てきます.

上のように書くと, spec に実際に使われる設定が代入されます. これは desired とは異なる可能性があります. SDL_OpenAudio(&desired, NULL) のように呼び出すこともできて, この場合ユーザーは desired の設定のままコールバック関数を書くことができます. この場合, spec に入るはずだった設定が desired と異なる場合は SDL が自動的にデータを変換してくれます (ただしその分処理は重くなるはず).

この解説では上の設定が通った (あるいは desired に NULL を設定した) と仮定して進めます. それ以外の場合は適宜読み替えてください.

SDL のオーディオ機能が要求する度に callback が呼び出されます. シグネチャーは

void someCallbackFunction(void* userdata, Uint8* stream, int len);

です. この関数が呼び出されるたびに, 長さ len バイトだけ stream にデータを書き込みます. stream には format で設定した型の整数が channel 個だけ並んでできたサンプルの列を書き込みます. ステレオ (channel == 2) の場合, 各サンプルには LR の順に書き込みます.

この関数は main() とは別のスレッドで呼び出されます.

あとは次回

なんか準備だけで長々してきたので, 肝心の FFmpeg の処理はほとんど書いてないけど続きは次回. 流れとしては上に書いた通り, パケット読み込みとデコード, そしてミキシング処理を書くことになります.

*1:ライブラリーはいくつかに分かれていて, libavcodec, libavformat, libavutil, libavfilter, libswscale, etc といった名前です. でも総称はありません. FFmpeg ライブラリーだとか libav* だとかと適当に呼んでます. 後者で行くと libswscale だけ仲間はずれだけど. 今回の話は前 3 つ分だけが関わります (他をリンクしなくても動く).