Index Game Pixel Link Blog Info
Index Code Graphic Report Tofu Contest English
Ogg Vorbis Sample Source Code

序章

音楽を扱うoggというフォーマットがあります。mp3がかかえるライセンス問題が無く、ファイルサイズも小さいのが魅力です。しかし、これをC#で鳴らそうと思うと、非常に面倒です。なぜなら、解説しているところがほとんど無いからです。ここではとにかく音を鳴らすことを主眼において解説します。

oggとvorbisを一つのdllにまとめて、C#で読み込む方法は.NET Claimwork 3.0というサイトで紹介されています。DLLを作る際にはかなり参考にさせていただきました。しかし、このやり方にはゲームで音を鳴らす観点から見ると少々疑問があります。独自DLLを作るのなら、いっそファイルの読み込みもDLL内でやって、waveをC#のゲームに返したら良いのでは、と思い立ちました。

ゲームそのものがoggの存在を知る必要は無いのです。音を鳴らすにもどっちみちwaveに変換しないといけないので、面倒な処理は全てogg側にやって貰おうと考えます。

流れを簡単に説明すると、こうなります。

1. C#でMarshal.AllocHGlobalを使って必要なメモリを確保。
2. DLLには[In][Out] ref IntPtrでそれを送る。
3. DLLはvoid** ptrとしてそれを受け取る。
4. DLLで*ptrをGlobalReAllocする。
5. DLLで*ptrをGlobalLockする。
6. DLLでファイルを読み込んで、メモリに流し込む。
7. DLLで*ptrをGlobalUnlockする。
8. C#でIntPtrをMarshal.Copyでbyte[]に流し込む。

第1章:C++のDLLでoggを読み込む

まずはDLLを作るところから始めます。ダウンロードしたサンプルのOggVorbisフォルダが完成品です。完成したDLLはOggVorbis.dllです。このDLLはOggVorbisPlay/bin/Debugフォルダ内にあります。

Microsoft Visual Studioを起動して、Win 32 Projectを新たに作ってください。サンプルではOggVorbisという名前にしておきました。この際、Application SettingsでDLLを選択してください。ここまで来たら、半分終わったも同然です。

次は3つのファイルを用意します。OggVorbis.h、OggVorbis.cpp、OggVorbis.defの3つです。

OggVorbis.hの中身
#ifndef OGG_VORBIS_H
#define OGG_VORBIS_H

extern "C" {
  extern int LoadOgg(const char *, char *);
}

#endif
実にさっぱりしたファイルです。C#のアプリはこの関数を読んで、oggのデータを入手します。const char *がファイル名、char *がDLLで読み込まれたwaveのデータです。

OggVorbis.defの中身
LIBRARY "OggVorbis"
EXPORTS
LoadOgg
これも簡単なファイルです。これでLoadOggという関数を他のアプリからも呼び出せるようにしています。

OggVorbis.cppの中身
#include "../include/oggvorbis.h"

#include
#include
#include
#include

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

int LoadOgg(const char* file, void** ptr)
{
const int iWaveHeadSize = 36; // WAV Header - Head
const int iWaveDataSize = 8; // WAV Header - Data

// Constant Data
const int quiet = 0;
const int bits = 16;
const int endian = 0;
const int raw = 0;
const int sign = 1;

// Variable Data
long data_size = 0;
long ret = 0;
long size = 0;
int current_section = 0;

char *data;
OggVorbis_File vorbisFile; // Vorbis File

if (ov_fopen((char *)file, &vorbisFile) != 0)
{
  return 0;
}
/////////////////////////////////////////////////////////////////
// Size Of File Post Decode - Channels * Rate * Total Time * Bits
data_size = (long)ceil(vorbisFile.vi->channels * vorbisFile.vi->rate * ov_time_total(&vorbisFile, -1) * bits/8);
/////////////////////////////////////////////////////////////////
// Allocate Space For Data
*ptr = GlobalReAlloc(*ptr, sizeof(char)*(data_size + iWaveHeadSize + iWaveDataSize), GMEM_MOVEABLE);
data = (char*)GlobalLock(*ptr);

if (data == NULL)
{
  ov_clear(&vorbisFile);
  GlobalUnlock(*ptr);
  return 0;
}
/////////////////////////////////////////////////////////////////
// Decode
// Fill in data+44->datamax
// data 0->44 filled in at very last...Interesting
while ((ret = ov_read(&vorbisFile, data + size + iWaveHeadSize + iWaveDataSize, data_size - size/*4096*/, endian, bits/8, sign, ¤t_section)) != 0)
{
  // Error Checking
  if(ret < 0 && !quiet) {
    continue; // IMPORTANT!!
  }
  // If you don't like continue, make sure to use "else { size += ret; }" instead.
  size += ret;
}
/////////////////////////////////////////////////////////////////
// Initialize Wave Headers - Can't Do It Before b/c "size" needed for "riffSize"
int riffSize = size + iWaveHeadSize;
int ckSize = sizeof(WAVEFORMAT) + sizeof(int);
WAVEFORMAT w;

w.nAvgBytesPerSec = vorbisFile.vi->rate * vorbisFile.vi->channels * bits/8;
w.nBlockAlign = vorbisFile.vi->channels * bits/8;
w.nChannels = vorbisFile.vi->channels;
w.nSamplesPerSec = vorbisFile.vi->rate;
w.wFormatTag = WAVE_FORMAT_PCM;
/////////////////////////////////////////////////////////////////
// Copy Header
int s = 0;
memcpy(data, "RIFF", sizeof(char[4])); s += sizeof(char[4]);
memcpy(data + s, &riffSize, sizeof(riffSize)); s += sizeof(riffSize);
memcpy(data + s, "WAVE", sizeof(char[4])); s += sizeof(char[4]);
memcpy(data + s, "fmt ", sizeof(char[4])); s += sizeof(char[4]);
memcpy(data + s, &ckSize, sizeof(ckSize)); s += sizeof(ckSize);
memcpy(data + s, &w, sizeof(w)); s += sizeof(w);
memcpy(data + s, &bits, sizeof(bits)); s += sizeof(bits);
memcpy(data + s, "data", sizeof(char[4])); s += sizeof(char[4]);
memcpy(data + s, &size, sizeof(size)); s += sizeof(size);

/////////////////////////////////////////////////////////////////
// Cleanup
ov_clear(&vorbisFile);
GlobalUnlock(*ptr);

return data_size + iWaveHeadSize + iWaveDataSize;
}
ややこしいように見えて、実際はそれほどでも無いファイルです。

まずは変数を用意します。本当はもっと種類があるのですが、ゲームで音を鳴らすのならこれで問題なく動きます。多種多様なoggやwaveフォーマットに対応させる場合、oggを一度読み込んで、そこからデータを拾ってきてください。

次はov_fopenを呼びます。ファイルが開いたら成功、駄目ならそこで終了です。

ファイルが開いたら、waveのサイズを求めます。そして、そのファイル用にメモリをGlobalReAllocで確保します。

次にov_readを呼びます。簡単に言うと、oggを読み込んで、buffer配列の44番目から順にwave化したデータを入れているだけです。

最後にbuffer配列の0から43番目にデータを入れます。これがwaveのヘッダ情報になります。なお、ヘッダを最後に入れるには理由があって、上のデータが無いとヘッダの一部が計算できないのです。

これで読み込みと変換が完了しました。

さて、これでコンパイルしてもエラーが出てしまいます。oggとvorbis関連の情報が抜けているからです。サンプル内部を見てもらうのが一番簡単だと思いますが、liboggとlibvorbisをxiph.orgからダウンロードして、そこにある*.hと*.cppファイルをプロジェクトに追加します。なお、ヘッダ関連以外でエラーが出る*.cppファイルは削除しても問題ありません。

ヘッダ関連のエラーですが、追加しただけでは<ogg/ogg.h>が見つからないと文句を言ってきます。サンプルでは"../include/ogg/ogg.h"という風に変更してあります。他のogg/vorbisヘッダファイルも同様に変更してあります。

後はコンパイルするだけ。これで完成です。

第2章:C#でoggを鳴らす

Microsoft Visual Studioを起動して、Windows Forms Applicationを新たに作ってください。サンプルではOggVorbisPlayという名前にしておきました。特に基本設定を弄る必要はありません。Project->OggVorbisPlay Properties>Buildを開け、Platform Targetをx86に指定してください。x64だとエラーが出て、動作しません。/bin/DebugフォルダにOggVorbis.dllとsound.oggをコピーしておいてください。次にForm1.csを開いてください。

Form1.csの中身
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using System.IO;
using System.Runtime.InteropServices;
using System.Media;

namespace OggVorbisPlay
{
  public partial class Form1 : Form
  {
    [DllImport("OggVorbis.dll")]
    static extern int LoadOgg(String file, [In][Out] ref IntPtr wave);

    public Form1()
    {
      InitializeComponent();

    }

    private void Form1_Load(object sender, EventArgs e)
    {
      IntPtr wave = Marshal.AllocHGlobal(1); // Allocate Something So That C# Has Easier Access

      int size = LoadOgg("sound.ogg", ref wave);

      byte[] buffer = new byte[size];

      Marshal.Copy(wave, buffer, 0, buffer.Length);

      Marshal.FreeHGlobal(wave);

      SoundPlayer snd = new SoundPlayer();
      snd.Stream = new MemoryStream(buffer);
      snd.Load();
      snd.Play();
    }
  }
}
面倒な処理を全て排除し、oggファイルを鳴らすことにのみ特化させました。まずはSystem.IO、System.Runtime.InteropServices、System.Mediaを追加します。IOはMemoryStream、InteropServicesはMarshal、そしてMediaはSoundPlayerのために必要です。

DLLを読み込むことから始めます。[DllImport("OggVorbis.dll")] static extern int LoadOgg(String file, [In][Out] ref IntPtr wave)と書くだけです。

肝心の再生を行うにはまずMarshal.AllocHGlobal(1)でIntPtrを初期化します。本当はメモリ確保が全てDLL内で行えたら楽なのですが、いまだ成功しません。次はDLLの関数であるLoadOggを呼びます。waveのファイルサイズを帰すので、それをintに保存してください。

帰ってくるwaveの大きさが分かったら、次はそのwaveをコピーするbyte[]を用意します。

最後にMemoryStreamを使って、そのbyte[]をSoundPlayerに渡して、音を鳴らします。これで完成です。

終章

これで全て終了しました。
もっと上手く出来る方法が無いか、継続して調べてみます。



意見や拍手があればよろしく!

簡易インデックス

C#でogg再生
DirectXでogg再生
-
DirectXでCatmull-Rom