DirectX8のワンスキンアニメーションな話
意外と簡単です
Write: 02/09/29 UpDate:--/--/--

次回作用にXファイルのアニメーション再生ルーチンをワンスキンに対応させたので公開しておきます。
ワンスキンって何ぞやということに関しては、こことかこことかこことかで調べましょう。
以前の記事『DirectX8のアニメーションな話』をベースにしているので、まだ見ていない場合はそちらを先にご覧下さい。
時代はシェーダーによる実装に移りつつありますが、ここではソフトウェア処理で行います。
代わりに前のプログラムからの変更点はわずかです。
ついでにTransformマトリクスの扱いの改善と行列キーの対応、メモリからの読み込み対応、
ハンガリー記法をやめた(笑)
等、細かい修正を行ってます。

まずはワンスキンのXファイルを眺めて、親子付けのXファイルとの違いを拾っていきましょう。
いくつかテンプレートが追加されているので、MSDNと照らし合わせながら見ていきます。

まずXSkinMeshHeader、ここでボーンの全般的な設定が記述されます。
nMaxSkinWeightsPerVertex メッシュ内の頂点に影響するトランスフォームの最大数
nMaxSkinWeightsPerFace 任意の面の 3 つの頂点に影響する一意のトランスフォームの最大数
nBones メッシュの頂点に影響するボーンの数

次にVertexDuplicationIndices、これはメッシュ内のどの頂点が互いに重複しているかという情報を保持しています。
nIndices インデックス数
nOriginalVertices 重複が発生する前のメッシュ内の頂点の数
indices 頂点の重複状態を表すインデックス値

この2つは内部的に使用されるだけのようなので軽く流しちゃいましょう。

スキニングで中心になるデータはSkinWeightsで、こいつはMeshに属してます。
一つのMeshの中に複数のボーンが存在可能です。

SkinWeightsの中には
transformNodeName ボーン名
nWeights ボーンの影響を受ける頂点の数
vertexIndices ボーンの影響を受ける頂点のインデックス
weights ボーンの影響を受ける各頂点の重み
matrixOffset メッシュの頂点をボーン空間に変換するトランスフォーム行列

でもってこいつらはD3DXLoadSkinMeshFromXofで読み取られます。
親子付けXファイル読み込みで使っていたD3DXLoadMeshFromXofに比べて
ppBoneNames、ppBoneTransformsが増えてます。

HRESULT D3DXLoadSkinMeshFromXof(
LPDIRECTXFILEDATA pXofObjMesh,
DWORD Options,
LPDIRECT3DDEVICE8 pD3DDevice,
LPD3DXBUFFER* ppAdjacency,
LPD3DXBUFFER* ppMaterials,
PDWORD pMatOut,
LPD3DXBUFFER* ppBoneNames,
LPD3DXBUFFER* ppBoneTransforms,
LPD3DXSKINMESH* ppMesh
);

ppBoneNames->GetBufferPointer()でtransformNodeNameが読み取れる。
ppBoneTransforms->GetBufferPointer()でmatrixOffsetが読み取れる。

ボーン自体はFrameで表現されます。
実は、ボーンのアニメーション自体は親子付けとまったく同じ処理が行われます。
ボーンは実体のない仮想的な存在なので、持っているデータは
初期姿勢をあらわすFrameTransformMatrixとアニメーションを指定するAnimationSetだけです。

以上Xファイル観察結果でした。
後はアニメーションしているボーンに対してポリゴンを肉付けすればOK。
一つのMesh内には複数のボーンが存在しても構わないんですが、 頂点に対する複数のボーンのウエイト値を足し合わせると1になります。
各ボーンの姿勢行列を線形補完しているだけって匂いがプンプンしますね。
理屈は実にしょーもないんで、あとはDirectX環境でどう実装するかだけです。
D3DXLoadMeshFromXofで取得できるLPD3DXMESH はDrawSubsetで簡単に表示できるんですが、
D3DXLoadSkinMeshFromXofで取得できるLPD3DXSKINMESH はそのままでは表示できません。
そこで登場するのがGenerateSkinnedMesh関数とUpdateSkinnedMesh関数です。

HRESULT GenerateSkinnedMesh(
DWORD Options,
FLOAT minWeight,
CONST LPDWORD pAdjacencyIn,
LPDWORD pAdjacencyOut,
LPD3DXMESH* ppMesh
);

HRESULT UpdateSkinnedMesh(
CONST D3DXMATRIX* pBoneTransforms,
LPD3DXMESH pMesh
);

GenerateSkinnedMesh関数を使ってLPD3DXSKINMESHからLPD3DXMESHを取得します。
さらにボーン行列へのポインタpBoneTransforms を食わせるとボーン変形後のLPD3DXMESHを返してくれます。
pBoneTransformsは matrixOffset * ボーンの姿勢行列 で求められます、んー楽勝ですね。
姿勢行列はCD3DFrameクラスの中に存在するのでボーン名transformNodeNameを手がかりに
検索すれば簡単に手に入ります。

さて、私の3D生活はLightWaveに支えられているのですが、
LightWave7.0 + DirectX Export Ver.1.3.2 でワンスキンのバイナリ形式Xファイルを作って、DirectX8.1環境で読み込ませると

D3DXOF:xparse::yylex_binary: Unexpected EOF
D3DXOF:358: syntax error near "("

D3DXOF:XStreamRead::GetNextObject: Error while parsing stream
D3DXOF:GetNextObject: Parse error.

と言って怒られてしまい、動いてくれません。
とりあえずはテキスト形式でXファイルを作ることで対処しています。
どうでもいい事ですがこのエラーメッセージから察するに、Xファイルパーサーはyaccで書いてあるみたいですね。
LightWaveで作ったアニメーションをこのプログラムで再生したときの精度についてですが、
キーフレーム間の補間方法の違いによる誤差がどうしても発生するため、
DirectX ExportオプションのFrameStepsをある程度小さくすることをお勧めします 。
かといって小さくしすぎるとデータが巨大になるので、適当な見切りも必要です。
行列キーを使うとデータサイズを小さくできますが(移動、回転、拡大縮小の各行列がまとめられているので当然)
キーフレーム間の補間が不可能なので再生速度を落とした時、動きがカクついてしまいます。
この辺も含めてアニメーションデータの特性に応じた良いセッティングを選ぶ必要があります。

今回のサンプルソース
.NETで書いてあるので、VC6以下の人は自分でプロジェクトを作り直してください。
あと外部依存ファイルに対するパスは自分で直してください。
毎度のことですが、エラーチェックは最低限であり、
このサンプルの使用によるいかなる不幸も当方では責任を負いかねます。

C++でDLLな話
なんで簡単にできないですか
Write: 02/07/02 UpDate:--/--/--
無知とは罪である。
かつてWindowsプログラム始めたころ、
CreatePenでペンを作成してDeleteObjectを呼ばないプログラムを平気な顔をして書いていたものです。
フレーム毎にそんな事をしていたものだから、あっという間にリソースを食いつぶしてしまい、
挙句の果てにはウインドウ右上のボタン類の表示が数字に化けてびっくりした事があります。
さて、今回のお話はC++でDLLを書くというネタ。
以前CでDLLを書いた事があったので、余裕をかましていたらひどい目にあいました。
そんなわけで参考文献、これとか、これとか、これを読みながら覚書を書いておきます。
ただし、まだ知らない事があるかもしれないので信用しきってはいけません。
無知とは恐ろしい事なのです。

1. メモリ管理
メモリの確保、解放に関して注意すべきことがあります。
簡単な事で、「DLL内で確保したメモリはDLL内で解放しなければならない」ということです。
newやmallocのルーチンがアプリケーションとDLLで異なっている可能性があるからです。
コンパイラが違えば当然ですし、リリースビルドとデバッグビルドでもリンクされる
ランタイムが異なるので危険です。

2. 命名規則
C++の関数名はコンパイラによって勝手に書き換えられます。
例えばhogehogeクラスのmoemoe関数はVCを使うと
?moemoe@hogehoge@@UAE_NXZといった感じになります。
name manglingというらしいんですが、この作業によってC++のオーバーロードの実装なんかが可能になっています。
で、この名前の変換はコンパイラ毎に異なっています。
モジュール定義ファイル(.def)を使う事で名前の変換を抑制できますが、 .defが各コンパイラ間で使いまわせるのか謎です。
さらに関数が増えるとそれだけ関数名の指定をしなければいけないので大変です。
とはいえ、自作のプログラムの場合DLLもそれを使うプログラムもVCだからと割り切ってしまうなら、
__declspec(dllexport)を使うことで.defがいらなくなります。

// DLL.h
class __declspec(dllexport) Test{ // DLLをビルドするときはdllexport
//class __declspec(dllimport) Test{ // DLLを使用するときはdllimport
public:
Func();
};

// DLL.cpp
#include "DLL.h"

Test::Func()
{
}

// DLLTest.cpp
#include "DLL.h"
#pragma comment( lib, "DLL.lib" )

void main()
{
Test a;
a.Func();
}

こんな感じでしょうか。
DLL1.hでdllexportとdllimportを使い分けなければならないのがちょっとめんどくさいですが、
#ifdef DLLEXPORT
#define DLLEXPorIMP __declspec(dllexport)
#else
#define DLLEXPorIMP __declspec(dllimport)
#endif
ってな風にマクロでごまかすのも手かもしれません。
あとはDLL側のビルドオプションで/D "DLLEXPORT"を指定すればOK。
ちなみに手を抜いてこの切り替えをしないでヘッダを__declspec(dllexport)のままにして DLLTest.cppをコンパイルすると
exeとlibができて再度関数がエクスポートされるという不思議な事になります。まぁ、それでも動くみたいですが。
クラス宣言に__declspec(dllexport)みたいな妙なものが付くのが嫌な場合は真面目に.defを書く事になります。詳しくはこちら
簡単に説明すると
一度目のコンパイル時にmapファイルを作って、そこに書かれているname mangling後の関数名を
defに書き写し、そのdefをプロジェクトに組み込んで再度コンパイルといった流れです。

3. DLLのバージョンアップ
DLL化してうれしい事の一つに、アプリケーションを一切変更することなく、新しいDLLに差し替えるだけで性能を上げることができるという事があります。
それに伴ってクラスの定義を変更して
class Test{
public:
int x[30000];
Func();
};
とかヘッダの構成を変えてしまうと危険です。
なぜなら、古いヘッダと新しいヘッダではsizeof(Test)が異なり、配列Xを使った瞬間、
古いヘッダを前提としたアプリケーションではメモリアクセス違反を引き起こしてしまいます。
MFCのDLLがMFC42.DLLみたいにバージョンごとにファイル名を変えているのはこのためです。
もっとも、これでは古いアプリケーションでは古いDLLを使う事になるので、バージョンアップの恩恵を受ける事ができません。

4. DLLのバージョンアップその2
__declspec(dllexport)を使うと.defファイルやname manglingのことを気にしなくていい代わりに、
エクスポート序数をこちらの思い通りに制御できません。
序数というのは.defで
EXPORTS
Func @1
と書く時の@に続く数のことですが、関数名とこの数を常に同じになるようにしておけば、
DLLのバージョンを上げてもアプリケーション側の変更は必要ありません。
つまり__declspec(dllexport)を利用するとDLLの変更の度にアプリケーションを再コンパイルする必要が出てくるわけです。

5. インターフェースクラス
さすがに、クラス定義を変更できないのはつらいので、実装クラスの上にインターフェースクラスをかぶせて誤魔化します。

// IDLL.h
class Test; // 実装クラスの名前を導入
class __declspec(dllexport) ITest{ // DLLをビルドするときはdllexport
//class __declspec(dllimport) ITest{ // DLLを使用するときはdllimport
Test *pTest;
public:
ITest();
~ITest();
Func();
};

// IDLL.cpp
#include "IDLL.h"

ITest::ITest()
{
pTest = new Test();
}

ITest::ITest()
{
delete pTest;
}

Test::Func()
{
pTest->Func();
}

ITestのクラス定義でclass Test;として実装クラスの名前を導入するというテクニックは知らなかったですね。
知らなくてもTest *pTest;をvoid *p;として(Test*)p->Func();といったvoidポインタのキャストでなんとかなるんですが。
とにかく、このようなラッパーを用意する事で実装は隠蔽されるので、安心してDLLのバージョンアップが出来ます。
しかし、Testクラスの関数が増えるたびにITestクラスの関数を増やしていかなければならないのは大変なので、
インターフェースクラスを純粋仮想関数にしてこれを実装クラスに継承させます。

// IDLL.h
class __declspec(dllexport) ITest{ // DLLをビルドするときはdllexport
//class __declspec(dllimport) ITest{ // DLLを使用するときはdllimport
public:
virtual Func() = 0;
};

// DLL.h
class Test : public ITest{
public:
Func();
};

こうすることでIDLL.cppが不要になって手間がかかりません。
ついでに仮想関数にアクセスするためのテーブル(vtbl)の構造がWindows上のコンパイラなら同じらしいので、
コンパイラ間の差異を吸収することが出来ます。
と良い事づくめに思えますが、インターフェースクラスが純粋仮想関数になってしまったのでクラスのインスタンスを作る事が出来ません。
よってDLL.cppに

ITest __declspec(dllexport) *CreateInstance() {
return new Test();
}

というインスタンス生成用の関数を用意します。
さらにDLLで確保したメモリはDLLで解放しなければならないので

Test::Delete()
{
delete this;
}

と、自爆用の関数を追加します。
アプリケーション側は

#include "IDLL.h"
#pragma comment( lib, "DLL.lib" )
ITest *CreateInstance();
void main()
{
ITest* pTest = CreateInstance();

pTest->Func();
pTest->Delete();
}

こんな感じで使います。
ちなみに.libを生成する事でCreateInstanceをそのまま使う事ができますが、代わりにDLLを任意の場所に置くことができません。
私の知る限りでは.libを使ったDLLの暗黙的リンクを行うとDLLをsystemディレクトリや、実行ファイルのある場所におかなければなりません。
せっかくならどこにDLLを置いても使えるようにしたいので、.libが不要なLoadLibraryによるDLLの明示的リンクを行いましょう。
その場合、CreateInstanceの名前がname manglingされると鬱陶しいので.defもあわせて作る必要があります。
あと注意しなくてはならないのは、IDLL.hを必ずDLLをビルドしたときのものとアプリからDLL使うときのものとで一致させなければならない事です。
仮想関数を追加するのはもちろん、宣言する順番を入れ替えるのも不可です。
IDLL.hはvtblの構造そのものなので、このフォーマットが崩れたら最後、まともに動作しません。
DLLを作ってる最中に色々試行錯誤していると、ついうっかりIDLL.hを揃えるのを忘れてしまう事があるので気をつけましょう。

そんなこんなで、それなりな感じのDLLのフレームワークはこちら
いやはや、DLL内のクラスを使うのがこんなに大変だとは思いませんでした。

02/07/09 追記 DLLのバージョンアップその2を追加。

システムチェックな話
隠してあると知りたくなるのが自然の理
Write: 02/05/09 UpDate:02/07/09

Windowsでプログラムを書くにあたって頭の痛いことの一つに環境依存性があります。
このマシンでは動くのにあっちのマシンでは動かないといったことは日常茶飯事です。
OS、CPU、メモリ、ドライブ、グラフィックカード、etc...原因は山のようにあって、いいかげん嫌になってきます。
結局、この手の問題に対処するには長年の経験と勘によるしかないような気がします。
とにもかくにも、まずはマシンの環境を洗いざらい調べないことには始まりません。
これが意外と曲者で、環境をチェックするプログラム自身が 環境に依存することがあるんですね。
そのあたりのノウハウを含めて、役に立つような立たないような環境チェッカーを作ってみました。
今回のソースはこれ
基本的なテクニックとして、OSのバージョンに依存する関数は、
直接使わないでGetProcAddressで関数ポインタとして取得することが挙げられます。
そうしないと、その関数がシステムのDLLに含まれていないOSでは
プログラムが不正終了してしまいます。
それでは一つずつ解説しましょう。

【OS】
MSDNに判定法が出ています。そのまま流用。
【メモリ】
GlobalMemoryStatusを呼ぶだけです。ただし4G(場合によっては2G)を超えるメモリが
搭載されていると正しい値を返しません。
その時はGlobalMemoryStatusExを使えば良いのですが、この関数はWin2000以降用です。
【ブートモード】
GetSystemMetricsを呼ぶだけです。
【ユーザー名】
GetUserNameを呼ぶだけです。
【ドライブ】
ドライブの存在チェックはGetLogicalDrives、ドライブ種別はGetDriveType
ボリューム名等ドライブ情報はGetVolumeInformationを呼びます。
一番知りたい情報の容量については、Win95ではGetDiskFreeSpaceExが使えない、
NTはGetDiskFreeSpaceExA(ASCII版)が失敗するらしい、
98ではGetDiskFreeSpaceExW(Unicode版)が失敗する、とややこしいことになっています。
適宜OS毎に使えるものを選択しなければなりません。
GetDiskFreeSpaceは容量2Gまでという制限があります。
また、リムーバブルドライブ(FD,MO等)についてはメディアが入っていないと エラーダイアログが出て非常にうっとうしいので
SetErrorModeで ダイアログを抑制します。
【ネットワーク】
WinSockを使います。
で、大した機能は使わないのでSocketをver1.0で作ろうとすると うまくいかないのでver1.1で作ります。
gethostname、gethostbynameでホスト名とIPアドレスが取得できます。
【実行ファイルパス】
GetModuleFileNameを呼ぶだけです。
【CPU】
cpuidというアセンブラ命令を使います。
この命令は486の後期辺りのCPUから実装されているもので、CPUの情報を山盛りで教えてくれます。
問題は山盛り過ぎて大変ということなんですが、頑張ってプログラムにしてみました。
今回のメインです、情報源はここ
CPU周りに関してはWCPUIDクラスのチェックをしてます。
通常 EAX=0000_0000h〜0000_0003hとして調べるstandard level程度しか調べないソフトが多く、
気が利いていても8000_0000h〜8000_0008hとして調べるextended level止まりなんですが、
さらに8086_0000h〜8086_0007hとして調べるTransmeta level(Crusoe用)まで使っています。
クロックを計測するにはrdtscという命令を利用します。
一度rdtscを呼んでからしばらく時間がたった頃を見計らってrdtscを再度呼び出してその差分からクロックを計算します。
2回のrdtsc呼び出しの間にどれだけ時間を置くかで計測精度が変わります。
ソースは人様のを流用。
【グラフィック】
デバイスドライバー名の取得にはEnumDisplayDevicesを使います。
MSDNではWin2000以降となってますが実は98でも使えたりします、NTは??。
現在の解像度、色数はMSDNではEnumDisplaySettings( NULL, ENUM_CURRENT_SETTINGS, ...)で 取得できるといってるんですが、
失敗することがあるので、代わりにGetDeviceCapsを呼びます。
周波数を知るにはEnumDisplaySettingsを使うしかないのですが、95系では取得できません。

と、こんな感じでいくらでも調べることができるので、きりがありません。
とりあえず上に挙げたものがメジャーなものでしょう。
あとは必要に応じて追加してください。

02/05/11 追記 EnumDisplayDevicesをNT(SP6まで上げても)で使おうとするとランタイムエラーが出るみたいです。
VC6付属の古いMSDNだと「この文章はβ版」との断りつきでNTで使えそうなことが書いてあるんですが、
新しい奴だと2000以降対応になっています。NTでは使わない方が無難ですね。
02/07/09 追記 OSVERSIONINFOEXの構造体メンバwProductTypeはVC6のヘッダには入ってない模様。
VC7を使うのが吉です。

まねデモな話
無駄なことが美しいのです
Write: 02/04/22 UpDate:--/--/--

まずはこちらのデモをご覧ください。
実はこの手のテキスト動画は古いメガデモにはいくつかあるのですが、 初めて見ると結構インパクトがあります。
HDDを整理していて出てきたこのデモに触発されて今回はテキスト動画を
普通の動画で作るという無駄極まりない事にチャレンジです。
元ネタのAVIファイルからフレームごとの画像データさえ手に入れば
後はモノクロ画像化して8ドット単位に区切ってその輝度にしたがって フォントデータを当てはめていけばOK。
その画像を再びAVIとして書き出せば完成です。
問題はフレーム画像をどうやって取り出すか、という点ですが、
最初MCI(Multimedia Control Interface)の高レベル関数を使ってやってたんですが
一見非常に簡単で良さそうな感じでした

MCIWndCreateでウインドウをWS_VISIBLEで作ってMCIWndOpen、MCIWndPlayしてやるだけで
いきなり動画が再生されます。
MCIWndCreateの返値がHWNDを返すので、これに対してGetDCしてやれば画面をキャプチャできます。
一コマずつキャプチャしたいならMCIWndSeekでシークするだけですね。
しかし難点が2つ。
ひとつは再生しているウインドウに、ほかのウインドウが重なると、キャプチャ画像に
そのウインドウが入ってしまう点。
もうひとつはMCIWndSeekの動作が非同期臭い?点です。
これは使えないってことで他のAPIを探すとMCI系でもう少し低レベルなAPI群があったので
そちらを使うことにしました。
頭にAVIという頭文字がついてる一連の関数はその名の通り、AVIファイルを操作するための
ものなのですが、ファイルを操作するだけで映像は出してくれません。
今回の場合は単なるコンバータなのでそれはそれで構いません。
ついでにMCI系のコマンドはCodecを勝手に処理してくれるので、
こちらが圧縮フォーマットを気にする必要がありません。
で、これまた使い方は簡単でAVIFileOpenしてAVIStreamGetFrameするだけで
フレーム画像へのポインタが取得できます。
画像を加工後、AVIに書き出すときは同様にAVIFileOpenしてAVIFileCreateStream、
AVIStreamSetFormat、AVIStreamWriteするだけです。
最後にAVIStreamRelease、AVIFileReleaseをちゃんと呼んであげないと
正常なAVIファイルにならないので注意。
今回のソースはこちら
ソースの動画が24bitカラーであることを仮定しているので注意。
それ以外の色モードで使うときは自分でなんとかしてやって下さい。
使い方は AVI2Font [入力ファイル] [出力ファイル] です。
font.bmpがフォントデータになっているので、このファイルは必須です。
これも24bitカラーに決め打ちしてます。
出力は無圧縮AVIなのでHDDの容量を食います。
さらには音声はコンバートしないので、適当なツールで結合する必要があります。
手抜きで申し訳ないです。
サンプル動画はこちら
サンプルの内容に対する突っ込みは無しです。
テキスト動画に向いている画像ってものがありまして、
『動きが大きい』『アップが多い』というのが良いと思われます。
手持ちの画像を漁ったらこれしかなかったと言う訳です。
余談ですがカラーからモノクロに落とすときはRx0.30+Gx0.59+Bx0.11を
計算してあげます。
人間の目にはRGBの明るさがそれぞれ違って感じるらしいですね。

続OggVorbisな話
僕以外の誰か書いて欲しかった
Write: 02/04/01 UpDate:02/07/03

そんなわけでOggVorbis用のサウンドドライバを書いてます。
さすがにライブラリが正式リリース前のものなのでドキュメントの手抜きが散見されますね。
vorbis_synthesis系の解説が無いような…、デコードの中心部分なのに。
3月中旬から怒涛の勢いで本家のライブラリがバージョンアップを繰り返しているので、
ドキュメントの方もじきに整理されてくるのかもしれません。
まぁ、libの差し替えだけで問題ないと思われるのでOggVorbisドライバーのコードを公開しておきます。
しょぼいですが、サンプルソースってことで、ご愛嬌。
なお、この記事からプロジェクトがVisualStudio.NET用になっているので注意。
VC6以下のコンパイラではプロジェクトを自分で作り直してください。
インクルードパスに.\includeを含め、ランタイムライブラリをマルチスレッドに、
デバッグリリース時にはライブラリLIBCMTDを無視するようにすればOKです。
ドライバ本体だけで使いたい方はこちらにDLL化したものがあります。
マルチスレッドなプログラムなので、他のプログラムに組み込んでも特に再生処理を意識する必要はありません。
おまけで多重再生OK。
実装されているのは

bool Load( char *pFileName ); // 初期化
bool Play(); // 再生
bool Pause(); // 休止
bool Release(); // 解放
bool SetLoop( int Count ); // ループ回数設定
bool CheckDevice(); // 再生デバイスの存在チェック
bool SetVolume( float Volume ); // ボリューム設定
float GetVolume(); // ボリューム取得
bool Fade( float StartVolume, float EndVolume, int FadeTime ); // フェード

といったところ。
ボリューム調節はできるんですが、waveOutSetVolumeを使うとどうやら
Windowsすべてのボリュームを変更してしまうようです。
んー、まいった。
荒業としてはデコードしたPCMデータをひとつずつ手作業でスケーリングしてやればいいんですが、
そんな大量の割り算したら速度的に悲しいことになるかも。
16段階でいいならビットシフトという手もあるんですけどね。
waveOut系で実装するのはあまり宜しくないのかも知れません。


02/04/02追記 秒間44.1k回も割り算をしたら大変かと思ったら、そうでもなかったです。
少なくともCeleron300クラスでは誤差の範囲内でした。
ダミーで val(PCMデータ) = val / 3.14を入れてみたところ
まぁ当たり前の話ですが、

; 248 : val = val / 3.14;
; 249 : *ptr = val;

0013b db 44 24 10 fild DWORD PTR _val$55403[esp+52]
0013f dc 0d 00 00 00
00 fmul QWORD PTR __real@3fd461d59ae78a99
00145 e8 00 00 00 00 call __ftol2
0014a 66 89 06 mov WORD PTR [esi], ax
0014d d9 47 fc fld DWORD PTR [edi-4]

と、FPUに投げちゃってるんですよね。
それも乗算命令に置き換えられてます。
ついでにFPUとCPUが並列に処理されていれば遅くならないわけです。
下手するとキャストの call __ftol2 の方が時間を食ってたりして…
なんのことはない、変数Volumeをスケール値として乗算すれば

; 248 : val = val * Volume;

0014b db 44 24 14 fild DWORD PTR _val$55404[esp+56]
0014f dc 8b 70 27 00
00 fmul QWORD PTR [ebx+10096]

になるというだけのことでした。
というわけでソースを修正したついでにフェード関数を追加しました。

02/05/18 追記 フェード周りがいまいちだったので修正しました。
02/07/03 追記 バグ修正版に差し替え。

 


戻る