2. 図形の描画

めちゃくちゃ間があきましたが, SDL 1.3 の続きです. 今回は画像を使わない基本的な描画処理について. 相変わらずヘッダーとソースを見ながら何となく書いてるので, 間違ったことを書く可能性がありますがご容赦を.

注意: 前回の記事から今までに SDL 1.3 において, SDL_WindowID の代わりに SDL_Window* を使うように仕様の変更がありました. まだ開発中のライブラリーというだけあってこういう変更は今後もあるかもしれません. 大幅な改修はもうないと思いますが...

SDL 1.3 の画面描画は, レンダラー (レンダリングコンテキスト) を設定した上で各種の描画関数を呼び出す, という形で行います. レンダラーは各ウィンドウに一つずつ設定できます.

SDL_CreateRenderer 関数でレンダラーを作成し, SDL_SelectRenderer 関数でそれをカレントにします. 各種の描画関数は明示的にレンダラーを受け取りません. これらは SDL_SelectRenderer 関数で最後に設定されたレンダラーに対して描画を行うようです.

int SDL_CreateRenderer(SDL_Window* window, int index, Uint32 flags);
window に対するレンダラーを作成する. index はレンダリングドライバーのインデックスを表す. -1 を指定すると flags で指定した条件に合う最初のドライバーを自動的に選択する. flags は以下の enum 値の OR 結合を指定する. まず画面の更新方法について, SDL_RENDERER_SINGLEBUFFER でシングルバッファーを使う. SDL_RENDERER_PRESENTCOPY を指定するとダブルバッファーだが, 画面更新はフリップではなく全面コピーによって行う. SDL_RENDERER_PRESENTFLIP2 は一般的なダブルバッファーのフリップによる更新. SDL_RENDER_PRESENTFLIP3 はトリプルバッファー方式である. 残りのフラグは更新に関するオプションである. SDL_RENDERER_PRESENTDISCARD を指定すると, 画面更新後のバックバッファーは未定義状態となる. SDL_RENDERER_PRESENTVSYNC を指定すると垂直同期を行う. SDL_RENDERER_ACCLERATED を指定するとハードウェアアクセラレーションを利用しようとする. この関数は成功すれば 0 を, 失敗すれば -1 を返す.
int SDL_SelectRenderer(SDL_Window* window);
window に設定されたレンダラーをカレントにする. window に対してレンダラーが設定されていなければ -1 を返し, そうでなければ 0 を返す.
void SDL_DestroyRenderer(SDL_Window* window);
window に対して設定されたレンダラーを破棄し, そのレンダラーに紐付けられたテクスチャー (多分次回触れます) をすべて解放する.

設定したレンダラーに対して毎フレーム描画を行うことになります. 普通はまず画面全体を黒などで塗りつぶすことから始めるでしょう. この処理は SDL_SetRenderDrawColor 関数で描画色を設定した後に SDL_RenderClear 関数を呼び出して行います.

int SDL_SetRenderDrawColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a);
描画色を設定する. この描画色は画面のクリアーおよび各種基本図形の描画すべてに使われる. カレントなレンダラーがない場合は -1 を, そうでなければ 0 を返す.
int SDL_RenderClear();
現在の描画色で画面全体を塗りつぶす. カレントなレンダラーがない場合は -1 を, そうでなければ 0 を返す.

関数名に Render と入っているものは大抵, カレントなレンダラーに対しての処理です. SDL_RenderClear は後述する SDL_RenderFillRect を引数 NULL で呼び出すのと等価ですが, レンダリングドライバーによって専用の関数が用意されている場合にはそれを利用します (おそらく効率的) *1.

上のように, 描画色にはアルファー値を設定することができますが, アルファーブレンドを使う場合には明示的に有効化する必要があります.

int SDL_SetRenderDrawBlendMode(int blendMode);
カレントのレンダラーのブレンドモードを設定する. SDL_BLENDMODE_NONE を設定するとブレンドを行わない. SDL_BLENDMODE_MASK を設定するとアルファー値が 0 でない場合のみ描画する. SDL_BLENDMODE_BLEND の場合は一般的なアルファーブレンドを行う. SDL_BLENDMODE_ADD を指定すると加算ブレンドを行う. SDL_BLENDMODE_MOD を指定すると乗算ブレンドを行う. この関数は, カレントのレンダラーが取得できない場合は -1 を, そうでなければ 0 を返す.
int SDL_GetRenderDrawBlendMode(int* blendMode);
カレントのレンダラーの現在のブレンドモードを取得し, *blendMode に代入する. カレントのレンダラーが取得できない場合は -1 を, そうでなければ 0 を返す.

SDL が描画できる図形は点, 線分, 矩形のみです. 矩形については中を塗りつぶすこともできるし, 枠だけを描画することもできます. この他に画像などを読み込んだテクスチャーから矩形を描画することもできますが, これは次回以降に.

以下がこれらの図形を描画する関数です. カレントなレンダラーに現在の描画色で描画する, という点はすべて共通です. また戻り値は, カレントのレンダラーが得られない場合は -1, そうでなければ 0 です.

int SDL_RenderDrawPoint(int x, int y);
座標 (x, y) に点を描画する.
int SDL_RenderDrawPoints(const SDL_Point* points, int count);
points で指定された count 個の座標に点を描画する.
int SDL_RenderDrawLine(int x1, int y1, int x2, int y2);
点 (x1, y1) と点 (x2, y2) を結ぶ線分を描画する.
int SDL_RenderDrawLines(const SDL_Point* points, int count);
points で指定された count 個の点を順に結ぶ折れ線を描画する.
int SDL_RenderDrawRect(const SDL_Rect* rect);
rect で指定された矩形のアウトラインを描画する. rect に NULL を指定した場合, 画面全体のアウトラインを描画する. SDL_RenderDrawRects(&rect, 1) と等価.
int SDL_RenderDrawRects(const SDL_Rect** rect, int count);
rect で指定された count 個の矩形の枠をすべて描画する.
int SDL_RenderFillRect(const SDL_Rect* rect);
rect で指定された矩形を塗りつぶす. rect に NULL を指定した場合, 画面全体を塗りつぶす (ただしその場合, 上述した通り SDL_RenderClear の方が適している). SDL_RenderFillRects(&rect, 1) と等価.
int SDL_RenderFillRects(const SDL_Rect** rect, int count);
rect で指定された count 個の矩形をすべて塗りつぶす.

SDL_Point は 2 次元の座標を表す構造体で, SDL_Rect は 2 次元の矩形を表す構造体です. それぞれ以下のような定義になっています.

// SDL_Rect.h
typedef struct
{
    int x;
    int y;
} SDL_Point;

typedef struct SDL_Rect
{
    int x, y;
    int w, h;
} SDL_Rect;

ダブルバッファーやトリプルバッファーを使用している場合, 描画が終わったら画面に反映させる必要があります. これは SDL_RenderPresent 関数で行います.

void SDL_RenderPresent(void);
バックバッファーの内容を画面に反映される. レンダラー作成時の設定に従い, フリップまたはコピーを行う.

これで SDL 1.3 による画像を使わない基本的な描画処理は終わりです. 最後に 2 枚の矩形をアルファーブレンドで描画するサンプルを上げておきます.

#include <iostream>
#include <SDL.h>

// 描画処理
void render() {
  // 画面を黒一色でクリアーする.
  SDL_SetRenderDrawColor(0, 0, 0, 255);
  SDL_RenderClear();

  // アルファーブレンドを有効にする.
  SDL_SetRenderDrawBlendMode(SDL_BLENDMODE_BLEND);

  // 矩形を赤で塗りつぶす.
  {
    SDL_SetRenderDrawColor(255, 0, 0, 255);
    SDL_Rect rect = { 50, 100, 200, 300 };
    SDL_RenderFillRect(&rect);
  }

  // 矩形を半透明の青で塗りつぶす.
  {
    SDL_SetRenderDrawColor(0, 0, 255, 128);
    SDL_Rect rect = { 100, 50, 300, 200 };
    SDL_RenderFillRect(&rect);
  }

  // 画面を更新する.
  SDL_RenderPresent();
}

int main(int, char**) {
  if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER)) return 1;

  SDL_Window* window = SDL_CreateWindow("Render rectangles",
                                        SDL_WINDOWPOS_UNDEFINED,
                                        SDL_WINDOWPOS_UNDEFINED,
                                        640,
                                        480,
                                        SDL_WINDOW_SHOWN);

  if (SDL_CreateRenderer(window, 0, SDL_RENDERER_PRESENTFLIP2) == -1) {
    SDL_Quit();
    return 1;
  }

  SDL_Event event;
  for (bool quit = false;;) {
    // イベントがなくなるまで処理し続け, なければ描画処理を行う.
    while (SDL_PollEvent(&event)) {
      switch (event.type) {
        case SDL_QUIT:
          quit = true;
          break;
        case SDL_WINDOWEVENT:
          quit = event.window.event == SDL_WINDOWEVENT_CLOSE;
          break;
        default: break;
      }
    }
    if (quit) break;
    render();
    SDL_Delay(10);
  }

  SDL_Quit();
  return 0;
}

*1:裏技として SDL_RenderFillRect(s) で画像を描画することができる場合があります. たとえばレンダリングドライバーに OpenGL を用いている場合に, SDL を経由せずに直接 OpenGL テクスチャーをバインドしてから SDL_RenderFillRect(s) を呼び出せば, テクスチャーレンダリングを行ってくれます (内部的には glRecti (OpenGL の場合) や glDrawArrays (OpenGL ES の場合) を呼び出すだけなので). さらに変換行列を設定しておけば座標変換もしてくれます. このような気持ち悪いことをするならば, SDL_RenderFillRect(NULL) と SDL_RenderClear() は違う効果を生むことになります.

canvas でトランジションをしたら

Javascript で 2D ゲームを作りたいと思ったらいろんなことができるようになってきたけど, 画面トランジションは躊躇するところ. canvas 要素の ImageData を使えばピクセル操作ができるので, 原理的には可能だけど画面全体でやったら重そう.

と思ってたけど実際試してみたことないなあと思ってやってみました. 今時間ないから詳細書きませんが, Safari で試したところによると, 毎フレーム 640x480 の ImageData を 2 つピクセル単位で合成して別の ImageData に書き, それを canvas に入れる, という操作はやっぱり重いみたいで, 手元の Macbook で 20 fps ちょいまで落ちました.

ちなみにやってみたのはフェードするカーテントランジション. ユニバーサルトランジションならもう一枚ルール画像の ImageData を読みながらになるので, さらに重くなりそう. 逆にフェードカーテンがやりたいだけなら, 実際にはフレームごとに書き換える必要があるのは画面の一部だけで, 部分更新でやればそこそこの速度を保てました (フェードの幅を 64 pixel にしたら 50 fps くらいは保てた). 低スペック環境でもそこそこ動いてほしいという場合はこっちも厳しいかもしれないけど.

canvasピクセル配列は一般の配列より (大抵の環境で) 少しだけ速いらしいです. 整数型固定とか, いろいろ制約が多いので. あと, こういうことやるときには効率が重要ですが, Javascript の場合現状は手動の最適化がかなり効くらしいです. スコープチェーンをできるだけたどらないようにするとか, その手の.

とはいっても本当にバリバリ動くゲームを作るなら今後は WebGL ですよね.

FFmpeg+SDL で音声再生 (2)

前回の続きです.

ファイルを開く

音声ストリームを持つコンテナーファイルを開くには次のようにします.

// ファイルを開く
AVFormatContext* fctx;
if (av_open_input_file(&fctx, "test.mp3", NULL, 0, NULL)) ...;
if (av_find_stream(fctx) < 0) ...;

// 音声ストリームを探す
int nb_streams = fctx->nb_streams;
int audio_stream_index = -1;
for (int i = 0; i < nb_streams; ++i) {
  if (fctx->streams[i]->codec->codec_type == CODEC_TYPE_AUDIO) {
    audio_stream_index = i;
    break;
  }
}
if (audio_strea_index < 0) ...;

// 対応するコーデックを開く
AVCodecContext* cctx = fctx->streams[audio_stream_index]->codec;
AVCodec* codec = avcodec_find_decoder(cctx->codec_id);
if (!codec || avcodec_open(cctx, codec) < 0) ...;

if 文の後に ... となっているところはエラー処理です. av_open_input_file() で音声を含むメディアファイルを開きます. 後ろの NULL, 0, NULL はファイルの開き方 (フォーマットの指定, オプションバッファー, などなど) のデフォルト指定です. av_find_stream() でストリームがあるかどうかを調べます.

その後, 音声ストリームを探します. ストリーム情報は fctx->streams[] に並んでいます. 動画ストリームも探す場合は CODEC_TYPE_VIDEO となるストリームを一緒に探します.

対応するコーデック (デコーダー) は avcodec_find_decoder() で見つけて avcodec_open() で開きます. まあここら辺は関数名の通り.

パケットキュー

ストリームを開いたら, パケットを読み出してコールバック関数に送り, デコードして SDL のオーディオバッファーに書き込みます. このとき, パケットの読み込みはスレッドを分けるのが一般的です. ここでもその方法で行きます.

パケットはキューで管理します. パケット読み込みスレッドはパケットを読んでキューに次々プッシュしていきます. キューにある程度溜まったらポップされるまで待機します. コールバック関数はキューからパケットをポップし, デコードして, バッファーに書き込みます. バッファーに書き終えるまでこれを続けます.

まずはパケットキューを定義します.

class PacketQueue {
 public:
  PacketQueue() : size_(0) {}

  void push(AVPacket& packet) {
    av_dup_packet(&packet);
    packets_.push_back(packet);
    size_ += packet.size;
  }

  bool pop(AVPacket& packet) {
    if (packets_.empty()) return false;
    packet = packets_.front();
    packets_.pop_front();
    size_ -= packet.size;
    return true;
  }

  int dataSize() const { return size_; }

 private:
  std::deque<AVPacket> packets_;
  int size_;
};

(スレッド同期に関するコードは省略しています. 本来は必須)

push() で渡される packet は後述する av_read_frame() で取得したパケットです. packet には圧縮された音声データへのポインター (packet.data) が含まれますが, av_read_frame() から取得した段階では内部バッファーを指していることがあります. この場合次の av_read_frame() 呼び出しでその領域のデータは上書きされてしまいます. よってデータを保持するためには別の領域を確保してデータをコピーする必要があります. これを行ってくれるのが av_dup_packet() です. この関数は packet が指すデータが内部バッファーかどうかを判別して, 必要な場合にだけコピーを行います.

size_ および dataSize() はキューに溜まっているパケットの総データ量を表します.

次にパケットを読み込むスレッドです.

void queueingThread() {
  static int const MAX_BUFFER_SIZE = ...;
  AVPacket packet;
  PacketQueue* queue = ...;  // コールバック関数と共有する
  AVFormatContext* fctx = ...;  // 先ほど取得したもの
  int stream_index = ...;  // 先ほど取得したもの
  bool* quit = ...;  // 終了フラグ

  while (!*quit) {
    if (queue->dataSize() > MAX_BUFFER_SIZE) {
      SDL_Delay(10);  // キューは満タン
    } else if (!av_read_frame(fctx, &packet)) {  // フレーム読み込みに成功
      if (packet.stream_index == stream_index) {  // 音声ストリーム
        queue->push(packet);
      } else {
        av_free_packet(&packet);  // それ以外のストリームは即破棄
      }
    } else if (url_feof(fctx->pb)) {  // EOF に到達
      break;
    } else if (!url_ferror(fctx->pb)) {  // 読み込みエラー
      break;
    } else {
      SDL_Delay(100);
    }
  }
}

いくつかの変数は他スレッドと共有しています (C++ 的にはメンバー変数と思えば OK). キューの内容量が MAX_BUFFER_SIZE を超えていたら待機します. この値は適当に決めればいいと思います (packet.size などをダンプして見て決めたり). このスレッドは av_read_frame() の呼び出しとキューへのプッシュがメインです. av_read_frame() は fctx の次のパケットを取得します. ここでは音声ストリーム以外は捨てていますが, その際に av_free_packet() を呼び出さないと場合によってはメモリーリークします.

後でこのスレッドにポーズやシークなどの処理も加えていきます.

そしていよいよコールバック関数です.

// フレームをデコードする関数
int decodeFrame(Uint8* buffer, int buffer_size) {
  AVPacket* packet = ...;  // 毎回共有. パケットを受け取る場所.
  uint8_t* packet_data = ...;  // 同上. 破棄のためにデータ先頭を取っておく場所.
  int& packet_size = ...;  // 同上. 破棄のためにデータサイズを取っておく場所.
  Uint16* buffer = ...;  // 同上. バッファー先頭へのポインター.
  int& buffer_size = ...;  // 同上. バッファー全体のサイズ.
  AVCodecContext* cctx = ...;  // 先ほど取得したもの.
  PacketQueue* queue = ...;  // queueingThread() と共有.

  for (;;) {
    while (packet->size > 0) {
      av_init_packet(packet);  // data と size 以外を初期化
      int getsize = buffer_size;
      int len1 = avcodec_decode_audio3(cctx, buffer, &getsize, packet);
      if (len1 < 0) break;  // エラー. フレームをスキップする.
      packet->data += len1;
      packet->size -= len1;
      if (getsize <= 0) continue;  // 展開するフレームデータがなかった.
      return getsize;
    }
    if (packet_data) {  // 古いパケットを破棄する
      packet->data = packet_data;
      packet->size = packet_size;
      av_free_packet(packet);
      packet_data = NULL;
    }
    if (!queue->pop(packet)) {
      return -1;
    }
    packet_data = packet->data;
    packet_size = packet->size;
  }
}

// コールバック
void audioCallback(void* userdata, Uint8* stream, int len) {
  int& current_index = ...;  // 同上. バッファー上の現在位置.
  int& loaded_size = ...;  // 同上. バッファーに読み込まれているデータサイズ.
  Uint16* buffer = ...;  // 同上. decodeFrame() と同じもの.
  int volume = ...;  // ボリューム. [0, 32768] の値.
  int pan = ...;  // 真ん中を 0 としたパン. [-16384, 16384] の値.

  while (len > 0) {
    if (current_index >= loaded_size) {
      int read_size = decodeFrame();
      if (read_size >= 0) {
        loaded_size = read_size;
      } else {
        loaded_size = 1024;
        std::memset(buffer, 0, 1024);  // 無音
      }
      current_index = 0;
    }
    int len1 = (loaded_size - current_index) * sizeof(*buffer);
    if (len1 > len) len1 = len;  // 実際に出力する量

    Uint16* s = reinterpret_cast<Uint16*>(stream);
    Uint16* b = buffer;
    stream += len1;
    len -= len1;
    current_index += len1 / sizeof(*buffer);

    // 書き込み. AUDIO_S16SYS でステレオと仮定する.
    len1 /= 2 * sizeof(*s) / sizeof(*stream);  // len1 をサンプル数に変換
    if (len1 > 0) do {
      *s++ = *b++ * volume / 32768 * (16384 - pan) / 32768;  // L
      *s++ = *b++ * volume / 32768 * (pan + 16384) / 32768;  // $
    } while (--len1);
  }
}

ちょっとごちゃごちゃしてますが. 変数群はほとんどスレッド外部から userdata 経由で与えられるものです. C++オブジェクト指向っぽくやるなら, userdata にオブジェクトへのポインターを入れといてメンバー関数を呼び出せば, すぐにクラス内部に入り込めるので, これらはすべてメンバー変数だと思ってもよいです.

decodeFrame() はキューからパケットを取り出してデコードし, バッファーにデータを入れてその長さを返します. avcodec_decode_audio3() がデコード処理本体です (3 はバージョン番号で, この関数のインターフェイスが何度も変わっていることを示しています). 上の実装では, パケット内に複数のフレームがある場合にも対応するように, パケット取得時の data と size を保存しておいて, デコードの度に packet->data や packet->size を更新しています. そして破棄するときには最初の data と size で破棄します (こうしないと当然破棄に失敗します).

audioCallback() は decodeFrame() によってパケットをデコードして buffer に読み込み, それを stream に書き込んでいきます. その際, buffer には len によって要求されるサイズぴったりは入らないので, 必要な分読み込んだり, 読み込みすぎた分は次回にまわしたりします. len, len1 や current_index, loaded_size の処理がこれにあたります. この際, len はバイト単位なのに対して current_index や loaded_size は 2 バイト単位であることに注意します.

書き込み処理ではボリュームとパンも反映しています. ここら辺はアプリケーションごとに好きにいじればよいところです. stream に書き込む値は, 絶対値が大きければボリュームも大きくなります. 上の実装では volume == 32768 が最大で, それ以下だと値に応じてデータを縮小します. pan は -16384 に近ければ左チャンネルを大きく保ち, 16384 に近ければ右チャンネルを大きく保ちます.

これで FFmpeg+SDL による音声再生の基本は完了です.

結局 2 回でも終わってないですが. 次回は再生位置の取得やポーズ/シーク, 複数音声のミキシングを手書きする話などの予定.

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 つ分だけが関わります (他をリンクしなくても動く).

SDL_ttf の Universal Binary 化

作ってるゲームで SDL_ttf を使うことにしたのですが, http://www.libsdl.org/projects/SDL_ttf/ に置いてある Mac OS X 用のバイナリーが x86_64 に対応していなかったので, これも自分でビルドしようと思ったら大変だったのでメモ. 環境は SnowLeopard + gcc4.0 or gcc4.2.

まず, ソースコードを落とします. svn から落とすと 2.0.10 で, 開発も落ち着いているようですが, とりあえず今回はサイトから直接落とせる 2.0.9 をビルドします. XCode のプロジェクトが圧縮されているので展開. SDL_ttf.xcodeproj を開きます. そのままだと i386/ppc 用の universal binary を生成するので, メニューのプロジェクト->プロジェクト設定を編集, からアーキテクチャーを 32/64bit universal とし, 有効なアーキテクチャーを i386 x86_64 に変更します.

とりあえず見た目これでよさそうですがビルドが通りませんでした. なんでかなあと思ったら, インストールされてる freetype が universal になってないようです. これが面倒でした. freetype の安定版ソースを http://ftp.twaren.net/Unix/NonGNU/freetype/ から落とします. 普通に configure / make すると使ってる環境用のバイナリーしか作れないので, 設定をいじる必要があります.

僕はまだ auto*** 周りに慣れてないので標準的な対処法がわかりませんでしたが, とりあえず configure 後に builds/unix/unix_cc.mk 内で CFLAGS と LDFLAGS の行にそれぞれ -arch i386 -arch x86_64 を付け足して, あとは普通に make, make install すれば 32/64bit universal な libfreetype がインストールできました. これを使えば SDL_ttf もビルドが通ります.

1. Hello, world

Hello, world と言ってもウィンドウを表示するだけのサンプルですが. ウィンドウの扱いを除けば, ほとんど SDL1.2 と変わりません. まずはコード*1.

#include <iostream>
#include <SDL.h>

int main(int, char**) { // 引数は省略できない.
  // SDL を初期化する.
  if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) == -1) {
    std::cerr << "SDL_Init failed:  " << SDL_GetError() << std::endl;
    return 0;
  }

  // ウィンドウを作成する.
  SDL_Window* window = SDL_CreateWindow("Hello, World!",
                                        SDL_WINDOWPOS_UNDEFINED,
                                        SDL_WINDOWPOS_UNDEFINED,
                                        640,
                                        480,
                                        SDL_WINDOW_SHOWN);
  // イベントループ
  SDL_Event event;
  for (bool quit = false; !quit;) {
    if (SDL_PollEvent(&event)) {
      switch (event.type) {
        case SDL_QUIT:
          quit = true;
          break;
        case SDL_WINDOWEVENT:
          quit = event.window.event == SDL_WINDOWEVENT_CLOSE;
          break;
        default: break;
      }
      continue;
    }
    // ここに各フレームでの処理 (描画など) を書くことになる.
    SDL_Delay(10);
  }

  SDL_Quit();
  return 0;
}

まず SDL を用いるプログラムでは main() 関数のシグネチャーは (int, char**) 固定です. これは SDL がプラットフォームごとのスタートアップ関数の差を吸収する機能に関わる制限で, SDL1.2 と同じ仕様です. たとえば Windows での GUI プログラムは WinMain() 関数がスタートアップとなりますが, SDL を使う場合にはリンクする SDLmain.lib の中に WinMain() 関数が実装されていて, ユーザーが定義した main() 関数を呼び出すようになっています. その際のシグネチャーが引数ありのものなので, ユーザーが定義する main() 関数も引数付きでないといけないわけです.

デバッグする際に一つ注意しなければならないこととして, SDL.h の中で次のようなマクロ定義がされています.

#define main SDL_main

つまり, 上のようにユーザーが定義した main() 関数は, 実際には SDL_main() という名前の関数になります.

SDL の機能は SDL_Init() 関数を呼び出してから使えるようになります. 引数には初期化したいサブシステムの種類を OR で指定します. これも SDL1.2 と同じですが, SDL1.3 ではサブシステムが増えています. 定義されている種類は SDL_INIT_TIMER, SDL_INIT_AUDIO, SDL_INIT_TIMER, SDL_INIT_JOYSTICK, SDL_INIT_HAPTIC, SDL_INIT_NONPARACHUTE, SDL_INIT_EVENTTHREAD で, これらをすべて有効にした SDL_INIT_EVERYTHING も定義されています. ちなみに現時点で SDL_INIT_EVENTTHREAD は未実装のようです.

ウィンドウの作成は SDL_CreateWindow() で行います. ウィンドウ周りは SDL1.3 で大幅に変更された部分です.

SDL_Window* SDL_CreateWindow(char const* title, int x, int y, int w, int h, Uint32 flags);
ウィンドウを作成します. title はキャプションを表し, x, y は SDL_WINDOWPOS_UNDEFINED または SDL_WINDOWPOS_CENTERED を指定します. w, h はウィンドウの解像度です. flags には SDL_WINDOW_FULLSCREEN, SDL_WINDOW_OPENGL, SDL_WINDOW_SHOWN, SDL_WINDOW_BORDERLESS, SDL_WINDOW_RESIZABLE, SDL_WINDOW_MAXIMIZED, SDL_WINDOW_MINIMIZED, SDL_WINDOW_INPUT_GRABBED を OR で指定します (どれも使用しない場合は 0 を渡す). 戻り値は名前の通り, 作成されたウィンドウ構造体へのポインターです *2. 0 の場合はエラーを表します.

SDL1.3 は複数ウィンドウ対応です. SDL1.2 と比べて, ウィンドウの作成や管理, イベントなどがかなり充実しています.

SDL_PollEvent() 関数を呼び出すと, その時点でイベントキューに要素があればそれを取り出しコピーして 1 を返し, 要素が溜まっていなければ 0 を返します. 上のコードでは, 毎フレームイベントキューをすべて消化しています.

イベントループは, 上のコードでは SDL1.2 とあまり変わっていませんが, ウィンドウの×ボタンが押された場合に SDL_QUIT イベントではなく SDL_WINDOWEVENT_CLOSE イベントが発生するようになっています. これは複数ウィンドウを使用する場合に, ×ボタンを押すことがプログラムの終了を表さないケースがあるからでしょう. SDL_WINDOWEVENT イベントを無視すると, ×ボタンから終了できなくなります.

SDL_Delay() は SDL1.2 と同じです. 指定した時間 (ミリ秒) だけ CPU を手放します. Windows でいう Sleep() と同じです (実際 Windows では Sleep() を呼び出します). このいわゆるスリープ処理は, 環境によっては 1 ミリ秒から有効となりますが, 一般的には 10 ミリ秒より短い時間だけスリープすることはできないと思った方がよいようです.

うん, この細かさじゃ書き続けられないな. 多分.

*1:特に深い理由はないけれど, C 言語は書き慣れていないので, サンプルは C++ 言語です. 細かい違いが面倒で……

*2:SDL 1.3 の r5524 までは SDL_WindowID というハンドルが使われていました. これに代わり新たに SDL_Window* を使うように変更されたので, この記事の内容もそれに合わせて修正しました.

0. SDL 1.3 のビルド

SDL 1.3 を使うには, まずは SDL 1.3 自体のソースコードを手に入れて, ビルドしなければいけません. subversion が使える人は

svn co http://svn.libsdl.org/trunk/SDL

リポジトリーをチェックアウトします. subversion を使わない人は http://www.libsdl.org/svn.php に最新版のスナップショットが置いてあるのでそれをダウンロード, 展開しておきます.

リポジトリーの URL を見ても分かる通り, 安定版ブランチではありません. SDL 1.3 にはメイン以外のブランチがまだないので.

ビルドは環境によって異なります.

Unix

Unix 環境ならば普通に

./configure
make
sudo make install

です. SDL を使うプログラムを GCCコンパイルする場合には, オプションに

gcc ... `sdl-config --cflags`

をつけ, リンクする際には

gcc ... `sdl-config --libs`

をつけます (SDL 1.2 と同じ). ヘッダーファイルは例えば /usr/local/include/SDL/***.h にコピーされます.

Windows

WindowsVC++ の場合, VirualC ディレクトリーに VisualC++ 用のプロジェクトファイルがあるので, それを開いてビルドします. この際, SDL 本体のプロジェクトと SDLmain のプロジェクトをそれぞれビルドします. ビルドには開発用の DirectX が必要です.

ビルドが完了したら VisualC/SDL/*** や VisualC/SDLmain/*** 内に SDL.dll, SDL.lib, SDLmain.lib ができるので, それを適当なディレクトリーに移して使います. SDL を利用するプログラムを書く際に必要となるヘッダーファイルは include ディレクトリーに入っているので, 適当にコピーして使うか, パスを通しておきます.

Mac OS X

Mac OS X では Unix の方法でもビルドできますが, Framework を作りたい場合には Xcode を使います. Xcode/SDL にプロジェクトファイルがあります. ただし r5514 現在, そのままではビルドに失敗します. ビルドするにはまず src/SDL_assert.c と src/SDL_assert.h を Library Source に追加して, src/video/cocoa/SDL_cocoavideo.m の 173 行目 (SDL_PromptAssertion_cocoa() 内)

const NSInteger clicked = [alert runModal];

の const NSInteger を適当に int か何かに置き換えます*1. とりあえずこれでビルドは通りました. すると Xcode/SDL/build/*** 内に SDL.framework ができるので, /Library/Frameworks にコピーするなりして使います.

とりあえずこれで SDL 1.3 が使えるようになりました.

*1:Objective-C はよく知らないのでかなり適当でその場しのぎです.