ホーム < ゲームつくろー! < Ogg Vorbis入門編

その2 Oggファイルを読み込んで音を鳴らしてみよう


 前章でライブラリの生成とテスト環境の構築を行いプログラムをできる体制を整えました。本章ではさっそくライブラリを使って何らかの音を鳴らす所まで進めたいと思います。ライブラリの使い方の基本です。



@ Ogg Vorbisは「Play」を提供しません

 当初私はOgg Vorbisは再生部分をサポートしてくれているのかな〜と踏んでいたのですが、Vorbisfileライブラリのリファレンスを見てもそれらしい関数は見当たりません。よく考えてみたら、再生環境がわからない状態でPlayできるはずがありません。では、Vorbisfileは何を提供してくれるのか?読み込みという面から見ると、Vorbisfileは「ファイルへのアクセス」「デコード(無圧縮のリニアPCMに変換)」を行うための関数を提供してくれます。ですから、実際に音を鳴らすにはデコードされたPCMを再生する部分を構築する必要があります。PCMはWAVEの一部みたいなものですからWindows APIでも再生できますし、DirectSoundでももちろん可能です。

 とりあえず、ファイルを読み込んでPCM音声を取り出す部分を以下で見ていくことにしましょう。



A ファイル読み込みと終了処理

 Vorbisfileライブラリを使ってoggファイルを読み込むのは極めて簡単で、ov_fopen関数を使うだけです。またファイルの消去(終了処理)はov_close関数で行います:

ファイルオープンとクローズ
#pragma comment ( lib, "ogg_static.lib" )
#pragma comment ( lib, "vorbis_static.lib" )
#pragma comment ( lib, "vorbisfile_static.lib" )

#include "stdafx.h"
#include "vorbis/vorbisfile.h"

int _tmain(int argc, _TCHAR* argv[])
{
   OggVorbis_File ovf;
   int error = ov_fopen( "Test.ogg", &ovf );

   if ( error != 0 ) {
      switch( error ) {
         case OV_EREAD:       break;
         case OV_ENOTVORBIS:  break;
         case OV_EVERSION:    break;
         case OV_EBADHEADER:  break;
         case OV_EFAULT:      break;
         default:             break;
      }
      return 0; // エラー
   }

   ov_clear( &ovf );

   return 0;
}


 ov_fopen関数は第1引数に.oggファイル、第2引数にそのファイルの持つ情報を格納するOggVorbis_File構造体を渡します。関数が成功した場合0が返ります。失敗した場合はエラーフラグが返ります。フラグの詳細はマニュアルのov_fopenをご覧下さい。

 読み込んだoggファイルはov_clear関数でクリアします。気を付けたいのが、ov_fopen関数で失敗した時、OggVorbis_File構造体は不定の値のままになってしまいます。それをov_clear関数に渡すとアプリケーションがストップしてしまいます。よって、必ずov_fopen関数の戻り値をチェックして下さい。



B Oggファイルのデコード

 ファイルを開いたら、ファイル内の音データを読み込みます。これはov_read関数を用います。この関数の定義を見てみましょう:

ov_read関数
long ov_read(
   OggVorbis_File* vf,
   char*           buffer,
   int             length,
   int             bigendianp,
   int             word,
   int             sgned,
   int*            bitstream
);

vfにはオープンしたOggファイルを指定します。
bufferには音データ(PCMデータ)を格納するバッファを指定します。マニュアルによると典型的には4096バイトのバッファを指定するようです。
lengthにはbufferのサイズを指定します。
bigendianpには音データの格納形式としてリトルエンディアン(0)かビッグエンディアン(1)を指定します。Ogg VorbisのライブラリはWindowsだけではなくてUnixやMacなどでも使えるようになっているのでこういうフラグがあります。Windowsはリトルエンディアンなので「o」が指定されます。
wordにはいわゆる「WORD」のサイズをフラグで指定します。8bitだと1、16bitだと2です。WindowsのWORDは16bitなのでここには「2」が入ります。
sgnedにはPCM音声の符号の有無をフラグ指定します。符号無しの場合は0、有りは1です。普通PCMは符号付きなので「1」です。
bitstreamにはストリーム再生中の位置が返ります。今は特に気にしないで下さい。

 関数が成功すると、戻り値には実際に読み込まれたデータのサイズが返ります。Ogg Vorbisは可変長ビットレートなので、この値が4096になるとは限りません。実際に512などが返ってくる事もざらです。これはストリーム再生などで特に気を付ける必要があります。

 ov_read関数の典型的な設定は次の通りです:

ov_read関数の設定例
char buffer[ 4096 ];
int bitstream = 0;
int readSize = 0;

readSize = ov_read(
   vf,
  buffer,
   4096,
   0,           // リトルエンディアン
   2,           // WORDは16bit
   1,           // 符号有り
   &bitstream
);


 ov_read関数は呼び出す度にOggファイルから少しずつデータを取り出し、それを内部でデコードしてPCMに変換して出力してくれます。ov_read関数を呼び出す度に先へ進んでくれますので、何も考えずにストリーム再生が行えます。逆に言えば、ファイルを全部読み込むということはどうも出来ないようです。

 さて、実際の所これだけOggファイルからPCMの音がデコードできてしまいました。凄い簡単ですね。後はこれを再生するだけ・・・再生するだけなんですが、うっかりしていました。テスト環境をコンソールで作ってしまったのでDirectSoundが扱い辛い(初期化に必要なウィンドウハンドルを取得しにくい…本当は頑張れば取れます(^-^;)。そこで、今回はWindows APIで簡易再生してみます。



C Windows APIによるPCM音声の簡易再生

 Windows APIで最も簡単にPCMを再生する方法はPlaySound関数を使う事です。この関数はwinmm.lib内にありまして、次のように定義されています:

PlaySound API
BOOL PlaySound(
   LPCSTR    pszSound
   HMODULE   hmod,
   DWORD     fdwSound
);

pszSoundには再生可能なPCMサウンドバッファへのポインタ、もしくはファイル名を指定します。今回はサウンドバッファとなります。
hmodにはモジュールハンドルを渡すのですが、今回はNULLで構いません。
fdwSoundには再生に必要なフラグを指定します。詳しくはマニュアルを参照して頂きたいのですが、今回はメモリ上にあるPCMサウンドを再生するためのフラグを指定します。

 何らかのバッファに再生可能なPCMの情報があるとして、それをPlaySound関数で再生するには次のように実装します:

PlaySound APIのオンメモリPCMの再生
PlaySound( PCMBuffer, NULL, SND_ASYNC | SND_MEMORY );

これは実行されるとメモリ上にあるPCM情報(SND_MEMORY)を非同期で(SND_ASUNC)ループ無し再生します。

 さて、PCMBufferにはPCMの音声データそのものではなくて「PCM形式のWAVEファイル」の情報が必要になります。つまりAで取得したPCMデータに、それがどういうPCMなのか(ステレオ?ビットレートは?)WAVEフォーマットに従ったヘッダー情報を付け加えなければなりません。これが微妙に面倒なわけですが、以下で説明していきます。

 まず、チャンネル数やサンプリングレートなどの必須情報をOggファイルから手に入れます。これはov_info関数を用います:

ov_info関数
vorbis_info* ov_info(
   OggVorbis_File*  vf,
   int              link
);

vfには読み込んだOggファイル情報を渡します。
linkにはビットストリームのリンク番号を指定します。今の所何の事だか良くわかりません(^-^;。デフォルトで-1を指定すると無視されるようです。

 ov_info関数が成功すると戻り値にvorbis_info構造体へのポインタが返ります。このポインタの先に音声フォーマット情報が格納されています。もしエラーが起こった場合はNULLが返ります。vorbis_info構造体は次のように定義されています:

vorbis_info構造体
typedef struct vorbis_info{

   int version;
   int channels;
   long rate;
   long bitrate_upper;
   long bitrate_nominal;
   long bitrate_lower;
   long bitrate_window;
   void *codec_setup;

} vorbis_info;

versionはVorbisのエンコーダバージョンです。再生にはあまり関係がありません。
channelsはビットストリームのチャンネル数です。モノラルなら1、ステレオなら2などです。ホームシアターの5.1chのような構成の場合は6などが入るのかもしれませんが、通常はステレオ再生の2が多いと思います。
rateはサンプリングレートです。44100のようなサンプリングレート数が入ります。サンプリングレートは1秒辺りのデータ数ですから、数が大きいほどクリアな音質になります。再生には重要な値です。
bitrate_uperbitorate_normalbitrate_lowerは可変長ビットレート(VBR : Variable Bit Rate)の情報です。可変長ビットレートについてはここでは詳しく述べません。再生に関してはこの情報は必要ありません。
bitrate_windowは現在使用されていません。
codec_setupはコーデックに必要な情報へのポインタが返ります。これも再生には必要ありません。

 結局、再生に必要なのはchannelsrateだけです。


 こうして得たチャンネル数とサンプリングレートをPCMのヘッダー情報に盛り込みます。PCM(WAVE)のヘッダーについてはこちらのサイトが大変に詳しいので本章でも参考に致しました(http://www.kk.iij4u.or.jp/~kondo/index.shtml)。ありがたい事です。

 WAVEファイルのヘッダー情報は大きく「RIFFチャンク」「フォーマットチャンク」「データチャンク」という3つの大きな塊に分かれているのですが、これらをずらっと並べると次のようになります:

サイズ 項目
4 RIFFヘッダー 'R' 'I' 'F' 'F'
4 RIFFヘッダーとこの4バイトを含まないファイルサイズ(全体のファイルサイズ-8) 145936
4 WAVEヘッダー 'W' 'A' 'V' 'E'
4 fmtチャンク(チャンクの種類を表す4文字) 'f' 'm' 't' ' '
4 fmtチャンクのサイズ。今回のPCMの場合は16バイト固定。 16
2 フォーマットID。PCMは1固定。 1
2 チャンネル数。ステレオなら2。 2
4 サンプリングレート 44100
4 データ速度。1秒当たりに送るデータサイズ。
[サンプリングレート]×[ビットレートサイズ]×[チャンネル数]
44100 * 2(byte) * 2(channel) = 176400
2 ブロックサイズ。1サンプルのサイズ。
[ビットレートサイズ]×[チャンネル数]
2(byte) * 2(channel) = 4
2 サンプル当たりビット数 16
4 dataチャンク(チャンクの種類を表す4文字) 'd' 'a' 't' 'a'
4 PCMデータのサイズ 145900
n PCMデータ。Oggから取得したのはここ以下に格納される。 ****


 沢山の項目がありますが、黄色い部分が与える必要のある値で、緑色の部分が与えた値から計算できる値です。面倒なのでPCM専用の構造体を定義して、黄色い部分を与えるとメモリ上にWAVEファイルを作ってくれる関数を作ってしまいます。

 PCMを格納するWAVEヘッダーの構造体は次の通りです:

PCM用WAVEフォーマット構造体
struct PCM_WAVE_FORMAT {
   char riff[ 4 ];
   unsigned int riffSize;
   char wave[ 4 ];
   char fmt[ 4 ];
   unsigned int fmtSize;
   unsigned short formatID;
   unsigned short channel;
   unsigned int samplingRate;
   unsigned int dataSizePerSec;
   unsigned short blockSize;
   unsigned short bitPerSample;
   char data[ 4 ];
   unsigned int PCMSize;

   // コンストラクタ
   PCM_WAVE_FORMAT( unsigned short inChannel, unsigned int inSamplingRate, unsigned int inPCMDataSize ) {
      // 固定値
      memcpy( riff, "RIFF", 4 );
      memcpy( wave, "WAVE", 4 );
      memcpy( fmt , "fmt ", 4 );
     memcpy( data, "data", 4 );
      fmtSize      = 16;
      bitPerSample = 16; // 16bit固定にします
      formatID     =  1; // PCM

      // 値
      channel        = inChannel;
      samplingRate   = inSamplingRate;
      blockSize      = bitPerSample / 8 * channel;
      dataSizePerSec = samplingRate * blockSize;

      // サイズ計算
      PCMSize = inPCMDataSize;
      riffSize = sizeof( PCM_WAVE_FORMAT ) - 8 + PCMSize;
   }
};

コンストラクタの引数に必要な値を入れると構造体の値をすべて埋めてくれます。これがヘッダーになります。この構造体のすぐ後にPCMデータが来る事になりますが、それは次のPCMを格納したWAVEファイルイメージを作る関数で作成します:

PCMを格納したWAVEファイルイメージ作成関数
char* createPCMWAVEFileImage(
   unsigned short channel,
   unsigned int   samplingRate,
   char*          PCMData,
   unsigned int   PCMDataSize,
   unsigned int&  PCMImageSize
)
{
   PCMImageSize = sizeof( PCM_WAVE_FORMAT ) + PCMDataSize;
   char* PCMImage            = new char[ PCMImageSize ];

   PCM_WAVE_FORMAT format( channel, samplingRate, PCMDataSize );
   memcpy( PCMImage, &format, sizeof( PCM_WAVE_FORMAT ) );
   memcpy( PCMImage + sizeof( PCM_WAVE_FORMAT ), PCMData, PCMDataSize );

   return PCMImage;
}

 引数にはチャンネル数、サンプリングレート、PCMのデータそしてそのサイズを与えます。後は内部で勝手にWAVEファイルのイメージを作って返してくれます。作成したファイルイメージのサイズはPCMImageSizeに返ります。


 この関数を使い、この章のお話を踏まえた段階で、実際にOgg Vorbisファイルから音声を抽出して10MB分のPCM音声を再生するサンプルは次のようになりました:

Ogg Vorbisファイルを読み込んで再生するサンプルプログラム
#pragma comment ( lib, "ogg_static.lib" )
#pragma comment ( lib, "vorbis_static.lib" )
#pragma comment ( lib, "vorbisfile_static.lib" )
#pragma comment ( lib, "winmm.lib" )

#include "stdafx.h"
#include <windows.h>
#include "vorbis/vorbisfile.h"


// リニアPCM用WAVEフォーマット
struct PCM_WAVE_FORMAT {
   char riff[ 4 ];
   unsigned int riffSize;
   char wave[ 4 ];
   char fmt[ 4 ];
   unsigned int fmtSize;
   unsigned short formatID;
   unsigned short channel;
   unsigned int samplingRate;
   unsigned int dataSizePerSec;
   unsigned short blockSize;
   unsigned short bitPerSample;
   char data[ 4 ];
   unsigned int PCMSize;

   // コンストラクタ
   PCM_WAVE_FORMAT( unsigned short inChannel, unsigned int inSamplingRate, unsigned int inPCMDataSize ) {
      // 固定値
      memcpy( riff, "RIFF", 4 );
      memcpy( wave, "WAVE", 4 );
      memcpy( fmt, "fmt ", 4 );
      fmtSize = 16;
      bitPerSample = 16; // 16bit固定にします
      formatID = 1; // PCM

      // 値
      channel = inChannel; // ステレオ
      samplingRate = inSamplingRate;
      dataSizePerSec = samplingRate * bitPerSample / 8 * channel;
      blockSize = bitPerSample / 8 * channel;
      memcpy( data, "data", 4 );

      // サイズ計算
      PCMSize = inPCMDataSize;
      riffSize = sizeof( PCM_WAVE_FORMAT ) - 8 + PCMSize;
   }
};

//! PCMを格納したWAVEファイルイメージを作成する関数
char* createPCMWAVEFileImage(
   unsigned short channel,
   unsigned int samplingRate,
   char* PCMData,
   unsigned int PCMDataSize
) {
   PCM_WAVE_FORMAT format( channel, samplingRate, PCMDataSize );
   unsigned int PCMImageSize = sizeof( PCM_WAVE_FORMAT ) + PCMDataSize;
   char* PCMImage = new char[ PCMImageSize ];
   memcpy( PCMImage, &format, sizeof( PCM_WAVE_FORMAT ) );
   memcpy( PCMImage + sizeof( PCM_WAVE_FORMAT ), PCMData, PCMDataSize );

   return PCMImage;
}

const int PCMSize = 1024 * 10000; // 10MB


//! メイン
int _tmain(int argc, _TCHAR* argv[])
{
   OggVorbis_File ovf;

   if ( ov_fopen( "Test.ogg", &ovf ) != 0 )
      return 0;

   // Oggファイルの音声フォーマット情報
   vorbis_info* oggInfo = ov_info( &ovf, -1 );

   // リニアPCM格納
   char* buffer = new char[ PCMSize ];
   memset( buffer, 0, PCMSize );
   char* tmpBuffer = new char[ PCMSize ];
   int bitstream = 0;
   int readSize = 0;
   int comSize = 0;
   while( 1 ) {
      readSize = ov_read( &ovf, (char*)tmpBuffer, 4096, 0, 2, 1, &bitstream );
      if ( comSize + readSize >= PCMSize || readSize == EOF )
         break;
      memcpy( buffer + comSize, tmpBuffer, readSize );
      comSize += readSize;
   }

   char *PCMImage = createPCMWAVEFileImage( oggInfo->channels, oggInfo->rate, buffer, comSize );

   BOOL res = PlaySound( (LPCWSTR)PCMImage, NULL, SND_ASYNC | SND_MEMORY );

   Sleep( 30000 ); // 30秒間待つ

   ov_clear( &ovf );

   delete[] PCMImage;
   delete[] buffer;
   delete[] tmpBuffer;

   return 0;
}


 メイン関数はとても短いです。特にOgg部分は極わずかですね。でもこれでちゃんと30秒間Oggファイルが再生ができます(Test.Oggというファイルを用意する必要があります)。このソースはコピペすると直ちに使えますので、実際に動かしてみて下さい。



 一先ず、Ogg Vorbisファイルを読み込んで音を鳴らす事はできるようになりました。最終的にPCMのバッファが取得できるのでいかようにも音を鳴らす事がでいます。しかし、今回の実装はあくまでもテストです。ゲームでまさかSleep( 30000 )なんて使えませんし、ヒープを10MB取るのも実際はありえません。次の章ではこの基本を踏まえて極わずかなメモリを用いてBGMを再生する「ストリーム再生」をDirectSoundを用いて実装してみます。