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