OggVorbisデータを汎用ストリーム経由で再生する

OggVorbisライブラリは汎用ストリームに対応しています。
C++でいうiostream、istream、ostreamに相当します。
今回はistreamを用いてOggVorbisデータを再生してみます。
この汎用ストリームを用いることで、ファイル以外にメモリ上やネットワークからOggVorbisデータを読み込み、再生することが可能になります。

OggVirbisデータをストリームでオープンするにはov_open_callbacks()関数を使います。
これは以下のフォーマットになっています。

int ov_open_callbacks(
    void *datasource,
    OggVorbis_File *vf,
    char *initial,
    long ibytes,
    ov_callbacks callbacks);

第1引数のdatasourceにはアプリケーションで使うデータへのポインタを指定します。
これは後述するコールバック関数に渡されるポインタです。
したがって、ストリームのインスタンスなどを指定すればよいです。

第2引数のvfにはOggVorbis_File型の変数へのポインタをしていします。

第3引数のinitialはとりあえずNULLで構いません。
第4引数のibytesには0を指定します。

第5引数のcallbacksには次の構造体データへのポインタを指定します。

typedef struct {
    size_t (*read_func)  (void *ptr, size_t size, size_t nmemb, void *datasource);
    int    (*seek_func)  (void *datasource, ogg_int64_t offset, int whence);
    int    (*close_func) (void *datasource);
    long   (*tell_func)  (void *datasource);
} ov_callbacks;

この関数ポインタからコールバック関数を呼び出します。
ov_open_callbacks()関数の呼び出し以降は通常通りにOggVorbisデータを扱うことが出来るようになります。
重要なのは初期化の部分です。

以上を踏まえ、istream経由でOggVorbisファイルを再生するプログラムを以下に記します。

#include <windows.h>
#include <stdexcept>
#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>
#include <dsound.h>
#include <fstream>

#define RELEASE(obj) if ( obj ) { obj->Release(); obj = NULL; }

#pragma comment(lib, "libogg_static.lib")
#pragma comment(lib, "libvorbis_static.lib")
#pragma comment(lib, "libvorbisfile_static.lib")
#pragma comment(lib, "dsound.lib")
#pragma comment(lib, "dxguid.lib")

// 再生用ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch ( uMsg ) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

// OggVorbisコールバック関数
size_t Callback_Read(
    void* ptr,
    size_t size,
    size_t nmemb,
    void* datasource
) {
    std::istream &in = *static_cast<std::istream *>(datasource);
    return in.read(static_cast<char*>(ptr), size * nmemb).gcount();
}

int Callback_Seek(
    void *datasource,
    ogg_int64_t offset,
    int whence
) {
    std::istream &in = *static_cast<std::istream *>(datasource);

    std::ios::seekdir dir;

    switch ( whence ) {
    case SEEK_SET:
        dir = std::ios::beg;
        break;

    case SEEK_CUR:
        dir = std::ios::cur;
        break;

    case SEEK_END:
        dir = std::ios::end;
        break;
    }

    in.seekg(offset, dir);

    return 0;
}

int Callback_Close(void *datasource) {
    return 0;
}

long Callback_Tell(void *datasource) {
    std::istream &in = *static_cast<std::istream *>(datasource);
    return in.tellg();
}


// メイン関数
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    OggVorbis_File vf;
    bool isOpened = false;

    IDirectSound8* pDS = NULL;
    IDirectSoundBuffer *pPrimaryBuffer = NULL;
    IDirectSoundBuffer8* pSecondaryBuffer = NULL;

    try {
        //---------------------------------------------------------
        //  ストリームの準備
        //---------------------------------------------------------
        // ファイルを開く
        std::ifstream ifs("test.ogg", std::ios_base::binary);

        if ( !ifs )
            throw std::runtime_error("ファイルオープンに失敗しました。");

        // OggVorbisファイルを汎用ストリームで開く
        ov_callbacks callbacks = {
            Callback_Read,
            Callback_Seek,
            Callback_Close,
            Callback_Tell
        };
        if ( ov_open_callbacks(&ifs, &vf, NULL, 0, callbacks) )
            throw std::runtime_error("OggVorbisのストリームオープンに失敗しました。");


        //---------------------------------------------------------
        //  Oggファイルの読み込み
        //---------------------------------------------------------

        //// OggVorbisファイルを開く
        //if ( ov_fopen(fileName, &vf) )
        //    throw std::runtime_error("ファイルオープンに失敗しました。");

        isOpened = true;

        // OggVorbisファイルの情報を取得する
        vorbis_info *vi = ov_info(&vf, -1);

        if ( vi == NULL )
            throw std::runtime_error("OggVorbisファイルの情報取得に失敗しました。");

        // WAVEFORMATEX構造体へのデータの書き込み
        WAVEFORMATEX wf;
        wf.wFormatTag = WAVE_FORMAT_PCM;
        wf.nChannels = vi->channels;
        wf.nSamplesPerSec = vi->rate;
        wf.nAvgBytesPerSec = vi->rate * vi->channels * 2;
        wf.nBlockAlign = vi->channels * 2;
        wf.wBitsPerSample = 16;
        wf.cbSize = sizeof(WAVEFORMATEX);

        // デコード後のデータサイズの取得
        DWORD decodeSize = static_cast<dword>(ov_pcm_total(&vf, -1)) * vi->channels * 2;


        //---------------------------------------------------------
        //  再生用ウィンドウの作成
        //---------------------------------------------------------
        WNDCLASS wc;
        wc.style = CS_CLASSDC;
        wc.lpfnWndProc = WndProc;
        wc.cbClsExtra = 0;
        wc.cbWndExtra = 0;
        wc.hInstance = hInstance;
        wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
        wc.hCursor = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = static_cast<hbrush>(GetStockObject(WHITE_BRUSH));
        wc.lpszMenuName = NULL;
        wc.lpszClassName = TEXT("OggPlayTest");

        if ( !RegisterClass(&wc) )
            throw std::runtime_error("ウィンドウクラスの登録に失敗しました。");

        HWND hWnd = CreateWindow(
            wc.lpszClassName,
            TEXT("Ogg再生テスト"),
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            NULL,
            NULL,
            hInstance,
            NULL
            );

        if ( hWnd == NULL )
            throw std::runtime_error("ウィンドウの作成に失敗しました。");

        // ウィンドウの表示
        ShowWindow(hWnd, nCmdShow);
        UpdateWindow(hWnd);


        //---------------------------------------------------------
        //  DirectSoundの初期化
        //---------------------------------------------------------

        // DirectSoundの初期化
        if ( FAILED(DirectSoundCreate8(NULL, &pDS, NULL)) )
            throw std::runtime_error("DirectSoundの初期化に失敗しました。");

        // 協調レベルの設定
        if ( FAILED(pDS->SetCooperativeLevel(hWnd, DSSCL_PRIORITY)) )
            throw std::runtime_error("協調レベルの設定に失敗しました。");

        // プライマリバッファの作成
        DSBUFFERDESC desc;
        desc.dwSize = sizeof(DSBUFFERDESC);
        desc.dwFlags = DSBCAPS_GLOBALFOCUS;
        desc.dwBufferBytes = decodeSize;
        desc.dwReserved = 0;
        desc.lpwfxFormat = &wf;
        desc.guid3DAlgorithm = GUID_NULL;

        if ( FAILED(pDS->CreateSoundBuffer(&desc, &pPrimaryBuffer, NULL)) )
            throw std::runtime_error("プライマリバッファの作成に失敗しました。");

        // セカンダリバッファの作成
        if ( FAILED(pPrimaryBuffer->QueryInterface(
            IID_IDirectSoundBuffer8,
            reinterpret_cast<lpvoid *>(&pSecondaryBuffer))) )
            throw std::runtime_error("セカンダリバッファの作成に失敗しました。");


        // プライマリバッファの開放
        RELEASE(pPrimaryBuffer);


        //---------------------------------------------------------
        //  OggVorbisファイルのデータをバッファにデコード
        //---------------------------------------------------------

        // バッファロック
        LPVOID pWrite;
        DWORD lockSize;
        if ( FAILED(pSecondaryBuffer->Lock(
            0,
            0,
            &pWrite,
            &lockSize,
            NULL,
            NULL,
            DSBLOCK_ENTIREBUFFER)) )
            throw std::runtime_error("バッファのロックに失敗しました。");

        DWORD totalReadSize = 0;
        INT bitstream;
        bool result = true;

        while ( totalReadSize < decodeSize ) {
            // データのデコード
            LONG readSize = ov_read(
                &vf,
                static_cast<char*>(pWrite)+totalReadSize,
                decodeSize - totalReadSize,
                0,
                2,
                1,
                &bitstream
                );

            if ( readSize == 0 ) {      // ファイルの終端まで読み込んだ
                result = true;
                break;
            }
            else if ( readSize < 0 ) {  // エラー発生
                result = false;
                break;
            }

            totalReadSize += readSize;
        }

        // アンロック
        pSecondaryBuffer->Unlock(pWrite, lockSize, NULL, 0);

        // ファイル読み込みの成否チェック
        if ( !result )
            throw std::runtime_error("OggVorbisファイルの読み込みに失敗しました。");


        // OggVorbisファイルを閉じる
        ov_clear(&vf);


        // 再生
        pSecondaryBuffer->Play(0, 0, 0);

        // ウィンドウメッセージループ
        MSG msg;
        while ( GetMessage(&msg, NULL, 0, 0) > 0 ) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        // 停止
        pSecondaryBuffer->Stop();


        // バッファ開放
        RELEASE(pDS);


    }
    catch ( std::exception& e ) {
        if ( isOpened )
            ov_clear(&vf);

        RELEASE(pDS);

        MessageBoxA(NULL, e.what(), NULL, MB_OK | MB_ICONERROR);
        return -1;
    }

    return 0;
}

今回のポイントは次の部分です。

        //---------------------------------------------------------
        //  ストリームの準備
        //---------------------------------------------------------
        // ファイルを開く
        std::ifstream ifs("test.ogg", std::ios_base::binary);

        if ( !ifs )
            throw std::runtime_error("ファイルオープンに失敗しました。");

        // OggVorbisファイルを汎用ストリームで開く
        ov_callbacks callbacks = {
            Callback_Read,
            Callback_Seek,
            Callback_Close,
            Callback_Tell
        };
        if ( ov_open_callbacks(&ifs, &vf, NULL, 0, callbacks) )
            throw std::runtime_error("OggVorbisのストリームオープンに失敗しました。");

ifstreamでOggVorbisファイルを開き、それをov_open_callbacksの第1引数に指定しています。
ov_callbacks構造体には自前で用意した関数を指定しています。
これらの関数は以下のように実装しました。

// OggVorbisコールバック関数
size_t Callback_Read(
    void* ptr,
    size_t size,
    size_t nmemb,
    void* datasource
) {
    std::istream &in = *static_cast<std::istream *>(datasource);
    return in.read(static_cast<char*>(ptr), size * nmemb).gcount();
}

int Callback_Seek(
    void *datasource,
    ogg_int64_t offset,
    int whence
) {
    std::istream &in = *static_cast<std::istream *>(datasource);

    std::ios::seekdir dir;

    switch ( whence ) {
    case SEEK_SET:
        dir = std::ios::beg;
        break;

    case SEEK_CUR:
        dir = std::ios::cur;
        break;

    case SEEK_END:
        dir = std::ios::end;
        break;
    }

    in.seekg(offset, dir);

    return 0;
}

int Callback_Close(void *datasource) {
    return 0;
}

long Callback_Tell(void *datasource) {
    std::istream &in = *static_cast<std::istream *>(datasource);
    return in.tellg();
}

Callback_Read()はデータ読み込み、Callback_Seek()はシーク、Callback_Close()はストリームクローズ、Callback_Tell()はストリーム位置を返す関数となります。
上記プログラムでは、istreamを用いてこれらの動作を実現させています。

このように、汎用ストリームでの再生はすんなりと出来ます。
因みに、ov_open()関数は以下のように実装されています。

int ov_open(FILE *f,OggVorbis_File *vf,const char *initial,long ibytes){
  ov_callbacks callbacks = {
    (size_t (*)(void *, size_t, size_t, void *))  fread,
    (int (*)(void *, ogg_int64_t, int))              _fseek64_wrap,
    (int (*)(void *))                             fclose,
    (long (*)(void *))                            ftell
  };

  return ov_open_callbacks((void *)f, vf, initial, ibytes, callbacks);
}

上記より、C言語の標準ライブラリのファイル操作関数を直で指定していることが分かります。

このように、OggVorbisライブラリは最初から汎用ストリームを意識して設計されているので融通が利きますね。

■参考サイト
その5 メモリにあるOggファイルを再生する
https://svn.xiph.org/trunk/Tremor/vorbisfile.c