Tutorial 〜或いは迷宮への誘い〜 (2004/07〜)


序文

いきなりですが、今日から初心者向けのAvisynth プラグインの作成のための手助けを始めたいと思います。
本当は、私などでは力不足でおこがましい気持ちもありますが、プラグイン作者のすそ野を広げる一助にでもなれば、
と思ったことがきっかけです。
あまり文章がうまくないので、たどたどしいことになろうとは思いますが、そこは徐々に加筆修正して
いこうと思っています。
ソースコードの実例をあげて、分かり易くしようとは思っていますが、さすがにC++の文法までは面倒みきれません。
書籍を読むなり、インターネット上にいくつか解説サイトがありますので探してみてください。
猫でもわかるプログラミング

ただ、いつまで続けられるのかはお約束できないのがちょっと問題なのですが...
少なくとも第一陣のこれっきりということは無いようにはしたいな、と思っています。


INDEX

  1. Avisynthフィルタの動作 [new 2005/05/05]
  2. 開発環境を用意する(1) [C++ 編] [new 2005/07/16]
  3. 開発環境を用意する(2) [ASM 編]
  4. 開発環境を用意する(3) [Avisynth 編]
  5. 開発環境を用意する(4) [DirectX 編]

  6. 色空間形式(color format)を理解しよう
  7. 色空間(color space)についてもっと理解しよう
  8. 色空間変換(color conversion)を理解しよう
  9. クリップ(Clip)構造を理解しよう [画像編]
  10. クリップ(Clip)構造を理解しよう [サウンド編]
  11. クリップ(Clip)の各要素

  12. スケルトン(skeleton) - Sample -
  13. ビルド(nmake)のしかた
  14. デバッグ(debug)のしかた

  15. pixel_typeとThrowError()について
  16. データアクセスの基礎(1) [YUY2編]
  17. データアクセスの基礎(2) [YV12編]
  18. 引数いろいろ(1) --- AVSValue
  19. 引数いろいろ(2) FilterRangeEx part(1) --- env->Invoke(), env->FunctionExists()
  20. 引数いろいろ(3) FilterRangeEx part(2) --- Tryブロックと例外ハンドラ
  21. 引数いろいろ(4) FilterRangeEx part(3) --- Eval,ScriptClip呼び出し、user_data
  22. 引数いろいろ(5) FilterRangeEx part(4) --- おまけ ユーザ関数バージョン

  23. ドロップフレーム(NULL frame, Drop frame)について
  24. DLLのデータ領域の使用についての注意点
  25. Clipのデータアライメントの注意点

  26. クリップの属性変更(1) - VideoInfoとenv->NewVideoFrame()


  27. [asm編]アセンブラ入門 [new 2005/05/10]
  28. [asm編]MMX, SSE, SSE2, 3D Now!のサポートCPUの判定 [new 2005/05/10]
  29. [asm編]インラインアセンブラを使ってみる(1) C++データへのアクセス [new 2005/05/10]
  30. [asm編]インラインアセンブラを使ってみる(2) レジスタの使用と保護 [new 2005/05/10]
  31. [asm編]インラインアセンブラを使ってみる(3) ジャンプラベル [new 2005/05/10]
  32. [asm編]関数呼び出し規約とスタック [new 2005/05/10]
  33. [asm編]関数と復帰値 [new 2005/05/10]
Special Thanks

Avisynthフィルタの動作     2005. 5. 5 (01版)

Avisynthでは、入力クリップが与えられ、その処理後、クリップを出力するという形式のフィルタを
外部プラグインという形で作成することができるようになっています。
このプラグインは、各々、一繋がりにチェーンされます。つまり、あるプラグインでの入力データは
その直前のフィルタの出力結果だということです。

まず、APLがオープンすると、スクリプト上のLoadPlugin行で、dllがロード/チェックされ、
AvisynthPluginInit2()がコールバックされます。
ここで、AddFunction()によりプラグイン管理テーブルに関数が登録されることになります。

次に、スクリプト上で関数の記述があれば、上記テーブルに登録されている関数エントリがinvokeされます。
そこで実際にフィルタクラスのインスタンスが生成され、コンストラクタが実行されることになります。
これがスクリプトの最後まで処理されることで、フィルタのチェーンが完成することになります。

チェーンが完成後は、APLからのフレームデータ要求(read)に応じて、チェーンの最後尾のフィルタのGetFrame()
がコールバックされ、その中からその手前のフィルタのGetFrame()が呼び出されるということを繰り返し、
そのGetFrame()要求は先頭のフィルタまで到達し、最後には入力ソースデータに行き着くことになります。
(なお、GetFrame()はフレームb指定でき、各フィルタは複数のフレームデータを要求することもできます。)
逆に画像データは入力データから要求されたフィルタ処理を順次通過して、最後にはAPLに通知されることになります。

APLがクローズすると、各フィルタクラスは破棄され、その時点でデストラクタが呼び出されます。

abstract

目次へジャンプ。

開発環境を用意する(1) [C++ 編]     2005. 7.16(04版)
4月のSDK関連の変更にあわせて内容を一新。旧版は右を参照→ 2004. 7. 3 (01版)
2005. 5. 5 (02版) 2005. 5.19(03版)

まず、プラグインを作ってみようと思っても、その環境がなければ作ることはできません。
「じゃあ高価なコンパイラーとかを買ってこなくちゃいけないの?」とか思われたかも知れませんが、
ご安心を。最近何故かMicrosoftが気前よく無料で公開しています。
これを使うことで十分作成することが出来ます。
ただ、市販のVisual C++.NETと全く同じというわけには流石にいかず、Visualな開発環境が付属していません。
(まあ他にも、いろいろ制限はありますが。)
操作は、コマンドプロンプトでキーボードからコマンドをタイプして行うことになります。

以下の2つをダウンロード、インストールしてください。

1. Visual C++ Toolkit 2003
Microsoft Visual C++ Toolkit 2003

2. Platform SDK (以下のいづれか一つ)
Windows Server 2003 SP1 Platform SDK Web Install
Windows Server 2003 SP1 Platform SDK Full Download
Windows Server 2003 SP1 Platform SDK ISO Install
XPSP2 PSDK Full Download with Local Install

※ 一見XPSP2 PSDKが良さそうに見えますが、これには nmake.exe が入っていません。
  ただ以下の.NET framework SDKをインストールするのならその中にnmake.exeは入っています。
  またリソースをコンパイルするのなら、コンパイル済リソース(.res)をCOFFオブジェクトに変換する
  ためのcvtres.exeが必要になってきますが、なんとPSDK SP1の場合、以前のように\win64から
  コピーしてきても動作しません。
  依存するdllが新しくなっており、msvcr71.dllではなくmsvcr80.dllが必要なようです。
  つまり残念ながらリソースのコンパイルはできなくなってしまいました。
  (リソースコンパイラ rc.exe 自体は存在するのに何故cvtres.exeは無いのだろう?)
  どうしてもリソースもリンクしたい人は、以下のものもダウンロード/インストールしてください。

  3. .NET Framework
  通常のWindows Updateなどでインストールできるランタイム版で良いのですが、
  SDKの方も欲しい人のためにリンクをあげておきます。
  (SDKはそのバージョンのランタイムのインストールも必要とします)
.NET Frameworkダウンロード情報
   .NET Framework 1.1 再頒布パッケージ
   Microsoft .NET Framework 1.1 Service Pack 1
   Microsoft .NET Framework 1.1 SDK
   Microsoft .NET Framework Version 1.1 日本語 Language Pack
.NET Framework 2.0 Beta 2 ダウンロード
   .NET Framework 2.0 再頒布可能パッケージ Beta 2 (32ビット版)
   .NET Framework 2.0 日本語 Language Pack Beta 2 (32ビット版)
   .NET Framework 2.0 SDK Beta 2 日本語版
   (なおbeta版はいろいろ互換性の問題を抱えているようで、
    .NETアプリが動作できなくなることもあるようです。)

以下Windows2000またはWindowsXP(32bit)環境に、PSDKをインストールする手順を
Windows Server 2003 SP1 Platform SDK Full Download版で説明します。
(クロスプラットフォーム開発環境で、IA64やXP 64-bit用の実行ファイルも作成できるようです。)
なお、
インストール先は F:\Program Files\ フォルダに、
ソースファイルは F:\Src\ フォルダに
共通の独自インクルードファイル(avisynth.h等を置く)は F:\Src\include\ フォルダに
置いた場合で解説しますが、適宜各々の環境に合わせて読み替えてください。

  1. すべてダウンロードする。
  2. PSDK-FULL.exeを実行してextract.exeとPSDK-FULL.batをcab群と同じフォルダに解凍する。
  3. PSDK-FULL.bat %temp% とかフォルダ先をパラメタ指定してcab群を解凍する。
  4. Setup.Exeを実行する。(Web Install版では PSDK-x86.exe を実行)
  5. 途中コンポーネント選択画面が現れるので、必要なもののみを選択する。(web版ではCore SDKのみとか)
    PSDK

    けれども、このままではPSDKとVC++ Toolkit2003が関連してないので両者を関連づけることにします。
    (Toolkit2003以外のコンパイラでも同様に関連づけできます)

  6. configration optionsが原因なのかどうか分かりませんが、
    ユーザー環境変数に設定された場合は、そのINCLUDE,LIB,PATHを削除する。
  7. VC++ Toolkit2003のインストール先にあるvcvars32.batを(必要であれば)編集します。
    私は、以下のようにしました。
    ----- file : F:\Program Files\Microsoft Visual C++ Toolkit 2003\vcvars32.bat -----
    @echo off
    
    Set PATH=F:\Program Files\Microsoft Visual C++ Toolkit 2003\bin;%PATH%
    Set INCLUDE=F:\Program Files\Microsoft Visual C++ Toolkit 2003\include;%INCLUDE%
    Set LIB=F:\Program Files\Microsoft Visual C++ Toolkit 2003\lib;%LIB%
    goto skip
    
    echo Setting environment for using Microsoft Visual C++ 2003 Toolkit.
    echo (If you have another version of Visual Studio or Visual C++ installed and wish
    echo to use its tools from the command line, run vcvars32.bat for that version.)
    echo.
    echo Thank you for choosing the Visual C++ Toolkit 2003!  Get started quickly by
    echo building the code samples included in the "Samples" directory.  Each sample
    echo includes a short whitepaper discussing the Visual C++ features, and a batch
    echo file for building the code.
    echo.
    echo Type "cl /?" for brief documentation on compiler options.
    echo.
    echo Visit http://msdn.microsoft.com/visualc/using/documentation/default.aspx for
    echo complete compiler documentation.
    
    :skip
    f:
    cd \src
    echo ようこそ Microsoft Visual C++ の世界へ
    ----------------------------------------------------------------------------------
    

  8. PSDKをインストールした先の、SetEnv.Cmd を編集する。
    長いので省略して変更部分のみを示します。
    ----- file : F:\Program Files\Microsoft Platform SDK\SetEnv.Cmd [line:40]---------
    :Chk_OS
    REM Set OS/platform-specific variables
    set  path=%SystemRoot%\Microsoft.NET\Framework\v1.1.4322;%path%
    call "%VCToolkitInstallDir%\vcvars32.bat"
    Set Include=%MSSdk%\Include\mfc;%Include%
    goto Set_OS
    
    :Finish
    set INCLUDE=f:\src\include;%INCLUDE%
    Goto end
    
    ----------------------------------------------------------------------------------
    (.NET Framework v1.1の人は、set path行で、\v1.1.4322;を、
                     v2.0βの人は、\v2.0.50215を入れてください。
      また入れてない人はこの行を削除してください。)
     なお、パスを通すのが不安な人は cvtres.exeを%VCToolkitInstallDir%\bin\ にコピーしてください)
    (もし、VC++ Toolkit 2003の方の環境を優先したければ、44行目の:FinishとGoto endの間に
      call文を移動してください。)
    

  9. スタートメニューにある、PSDKの Open Build Environment Windowから以下のコマンドプロンプトを (必要に応じて)クィックランチャーやデスクトップにドラッグコピーするしておく。
    Windows 2000 Build Environment - Set Windows 2000 Build Environment (Retail)
    または
    Windows XP 32-bit Build Environment - Set Windows XP 32-bit Build Environment (Retail)
  10. 上記を起動して、プロパティから以下を変更する。
    オプション:簡易編集モードにチェック
    レイアウト:画面バッファのサイズ 高さ(H)を適当に増やす。(1000〜2000とか)

設定し終えたら、コマンドプロンプトを起動して環境変数がきちんと設定されているか確認しましょう。

F:\Src>set path
とタイプし、Enterキーを押せば設定内容が表示されます。以下同様に、
F:\Src>set include
F:\Src>set lib
Command Prompt
以上で基本の環境の構築は終わりです。

[TIPS] Windows2000のコマンドプロンプトでフォルダ/ファイル名の入力補完を有効にする
Windows XPではデフォルトで有効となっているコマンドプロンプトのオートコンプリート機能ですが、
Windows2000ではレジストリで設定する必要があります。
regeditを起動して、「HKEY_CURRENT_USER\Software\Microsoft\Command Processor」の下の
「CompletionChar」の値を「0」から「9」に変更しします。
これで、<TAB>キーでフォルダ名、ファイル名の入力補完が有効となります。
なお、WindowsXPでファイル名の入力補完は同じくレジストリ設定ですが、TweakUI等を使ってください。

[TIPS] iostreamのstd::coutを正常に動作させる
プログラムでコンソール出力しようとして、
cout << "Hello world\n";
とか書いても何も出力されない。
これは、C++標準ライブラリ(デフォルトライブラリ)が、PSDKのlibcp.libをリンクするために起きる現象です。
このlibにはいくつか処理が空の関数があるようです。
これは、上記PSDKのlibcp.libを変名しておくか、VCTKの方のlibの環境を優先させるようにすることで回避できます。
(私はPSDKを優先して、libcp.libを変名しています。)
目次へジャンプ。
開発環境を用意する(2) [ASM 編]     2004. 7. 3 (01版)

C++の範囲でプログラミングするには、開発環境を用意する(1)の作業だけで十分なわけなんですが、 世の中で公開されているプラグインソースをビルドしようとすると、アセンブラが必要になってくる場合があります。
そこで、アセンブラソース(拡張子.asmというのが多い)が含まれていてもビルドできるように準備しておこう、 というのがこの章の主旨です。
アセンブラは不要という人は本章は読み飛ばして頂いて結構です。
では、MASM 6.15を入手しましょう。

Visual C++ 6.0 Processor Pack Download
ここから、vcpp5.exeをダウンロードします。
winzipからこのファイルを開き、中にあるml.errとml.exeの2つを解凍し、G:\Program Files\Microsoft Visual C++ Toolkit 2003\bin フォルダの中に置きます。
これで準備完了です。

本来はVisual C++ 6.0を持っている人のためのものですので、これを使用するのはちょっとしたグレーゾーンです。
以前ならWin98 DDK(この中にMASM 6.12が含まれている)とMASM 6.1x to 6.14 patchでMASM 6.14が入手できたんですが、 Win98に限らず、すべてのDDKが無料ではダウンロードできなくなってしまったようです。patchはまだ入手できるんですが。
目次へジャンプ。
開発環境を用意する(3) [Avisynth 編]      2004. 7. 3 (01版)

開発環境を用意する(1) 開発環境を用意する(2)で C++ソースとasmソースをビルドする環境は整ったわけですが、Avisynthのプラグインを作成するには インターフェースを記述しているヘッダーファイル(avisynth.h)が必要です。
このファイルは、Avisynthのソースファイルに含まれていますので、それを入手します。
Avisynth2のsourceforgeサイトから、 2.54のソース [AviSynth_254_src.zip] を入手してください。
解凍して、この中から avisynth.h を抜き出します。
このファイルは作成しようとしているソースと同じフォルダに置いてもよいのですが、 今回は専用のフォルダを用意することにしましょう。

仮にf:\src\includeに置くとします。
開発環境を用意する(1)で設定したINCLUDE環境変数に追加しときましょう。
または、
set INCLUDE=f:\src\include\.;%INCLUDE%
を実行しても良いでしょう。

以上で必要なものは大体揃ったことになりましたね。
おっと、大事なものがまだありません。ソースコードを作成するものが必要です。
テキストエディタと呼ばれる分野のソフトですね。
世の中にはいろいろ存在するので、自分に合ったものを探しておきましょう。
Windows標準のメモ帳(notepad.exe)を使う場合は、文字コードセットはANSIにしておきましょう。

目次へジャンプ。
色空間形式(color format)を理解しよう     2004. 7. 3 (01版)

まず、プログラミングに入る前に、いったい画像データがどのような形をしているのか知っておかないと 何もできません。そこで最初に画像データ形式を勉強しましょう。
さて、世の中にはさまざまな色空間が存在しますが、Avisynthで取り扱う色空間は、現在のところ RGB32 / RGB24 / YUY2 / YV12 の4つがあります。
現在はYUY2が一番良く使われており、Avisynthに限ればYV12も良く使われます。 以前はRGBが中心だったんですが、ビデオカードでYUVオーバーレイが当たり前になってきたのに歩調を合わせて、 YUV-RGB色空間変換の手間を省いて、より高速化が目指された結果こういうことになってきました。

では、順に説明していきましょう。
Group
ところで、画像データは平面、2次元ですよね?これはX-Y軸という2つの基準軸があるということです。
これをどうやってデータ化しているんでしょうか?
ご存知のようにデータを格納する場所は、連続した番地が付けられています。
言い換えると1次元で管理されているということです。
すると、2次元を1次元に変換する何らかの手段が必要になってきますね。
素直に考えると、左側の点(pixelとかdotとか呼びます)から順に右側の点へと、また 上から下へと順に並べれば、この1次元への変換はできますね。
なぜこんなことを言い出したかと言いますと、実はこの2次元→1次元の手段が、RGBとYUVでは異なっている わけなんです。
ではどのようになっているかを下図に示します。
YUY2/YV12 RGB32/24
RGBがどうして下から上に画像データが格納されているのかというと、静止画のフォーマットでBMP(DIB)というのが Microsoftで作られた時、それが下から上の順だったわけです。画像を取り扱うのに、物理的画面と切り離した 仮想画面(左下が原点の第一象限)を作り出したので、DIBも下から上にしたのかもしれません。(JPEGもそうですね)
ただ、次の行データへの間隔(pitchという言葉が使われます)をマイナス値にすると結果的には上から下にも格納できます。
動画のフォーマットを作るときも、それを引き継いだみたいですね。(RGBのFourCCも"DIB "が良く使われます)
ではYUVはどうして上から下なんでしょう?
知りません。(きっぱり!)
多分表示機器メーカが物理的な構造に合わせて効率よくしたのかもしれないですね。
昔はビデオチップのハードウェアアクセラレーションでBitBlitが上から下、左から右しかサポートされてないものもありました。

さて、画面のピクセルの並びがわかったところで、 いよいよ具体的に、各ピクセルの色の表し方を見ていきましょう。
まず一番わかり易く、馴染みのあるのがRGB表色系のフォーマットであるRGB24/RGB32です。
これは、光の3原色(加法混色)である、R(red),G(green),B(blue)の各要素成分を数値化したフォーマットです。

RGB24

画像データの各点(pixel)につき、24bitの大きさのデータを持つ形式です。
そうです、RGBの後ろの32とか24とかはそういう意味を持っていたんですね。
Red,Green,Blueの各色素の値(色値)を各々256の段階で表せます。
256 x 256 x 256 = 16777216
つまり良く言われる1677万色(true colorとかreal colorとかfull colorとかと呼ばれています。)を表現できます。

また、RGBの各値を1 pixel単位にまとめて格納します。こういう形式をPacked Color Formatと分類します。
RGB24
1ピクセルあたり3バイトなので、横幅によっては合計が奇数になったりします。でも画像データはDWORD境界
であるため、境界に合うように行末にはpadding dataが詰められます。(※1)
※1 ただし、Avisynth内では、データ境界(Date Boundary)はDWORD(4bytes)とは限りません。
  8/16/32バイトの場合もありますし、それ以上の場合もあります。
  この余白は、後の章で説明するpitchで管理することになります。

RGB32 / RGBA

RGB24と同じく、RGB各色値を8bitで持ちます。合計24bitですね。
では残り8bitには何が詰まっているのでしょう?
キャプチャーデータには実は何も詰まっていません。ただの隙間(padding data)です。
では、何故無駄とも思えるこんなことをしているのでしょうか?
これはCPUにとって取り扱いやすい単位であるDWORD境界(32bit boundary)にしているわけです。
ただ、これだけではいかにももったいないので、別の情報をそこに入れましょうという、謂わば拡張フォーマットが RGBAなんですね。Avisynthでは一部のプラグインでRGBAの処理を行うことができます。
RGBAとは、色情報に加えて、透明度(alpha channel)を追加したものです。
alpha値も、0から255までの256段階の値で表します。
RGB32

YUY2

いよいよYUV表色系の説明ですが、これはより少ない情報量で効率よく色を表現しようとした、
人工的なものであり、直感的なものではないというものです。
ただし詳しい説明はここでは致しません。別の章に譲ることにします。
YUY2は、別名(YUYV,YUNV and V422)というYUV422系の16bitフォーマット中の一つです。
Yは輝度(明るさ)を、U,Vは青、赤成分と輝度成分との差分を表します。(U=B-Y, V=R-Y)
さらにUVには、128のオフセットを足して、-128から+127の範囲の値を取るようになっています。
(つまりデータ値128は、色差0を意味します。UVとも色差0の時は輝度のみ、つまりモノクロ画像となります。)

各ピクセル毎に、輝度(Y)は持ちますが、色差情報 U(Cb),V(Cr) は隣り合う2ピクセルで共有します。
従って、データも2ピクセルで32bit単位であり、つまり2の倍数(偶数)(multiple of 2, even)幅(※2)しか許されません。
YUY2
※2 ただし、Avisynth 2.5系では、横幅は4の倍数(multiple of 4)でなければなりません。

YV12

このフォーマットは、今までのものとはちょっと異なります。
1ピクセルのデータをパックしないで、同じ種類(Yのみとか、Vのみとか)ごとの平面(plane)に分離した形式なのです。
YUY2と同様に、YUVの情報を持ちますが、色差 U,V については、2x2ピクセル単位で共有します。
つまり、Y が M x N の平面なら、U,Vは、各々(M/2) x (N/2) の平面(1/4のデータ量)になります。
という事は、横幅と高さは2の倍数(※3)でなければならないということにもなります。
この形式は、実はMPEG圧縮形式(YUV4:2:0)のデータの持ち方と同じであり、MPEG Raw形式と呼んでもよいものです。
また、I420という、U,V平面の順番が入れ替わった形式もありますが、Avisynthでは各平面へはポインタでアクセス
するようになっているので、どちらもYV12形式として取り扱って(意識しないでも)良いようになっています。
※3 Avisynth 2.5系では、横幅は4の倍数(multiple of 4)でなければなりません。
YV12 Progressive

I420 Progressive

さてここで、インターレースと言う言葉を聞いたことがある人も多いと思います。 このインターレースの説明は省くとして、上記YV12のファイルフォーマットではインターレースな画像を
格納するのは不都合があります。でもインターレースを切り捨てるのも実用上問題があります。
そこでAvisynthでは、YV12にもインターレースという概念を取り込んでいます。
(本当はYV12には、もともとインターレースという場合も規定されていたのかも知れませんが、
私はそういう文章を見たことがありません。)
つまり、1ラインおきに同じタイミングの画なのだから、色差U,Vも1ラインおきに共通にすれば良いじゃないか、
という考え方です。
YV12 Interlaced

この図を見れば、YV12でインターレースの場合は、ライン数は4の倍数でないといけないことが分かると思います。
目次へジャンプ。
クリップ(Clip)構造を理解しよう(画像編)    2004. 7. 3 (01版)

Avisynthでは、この画像データにサーフェースを付けて一纏まりの動画データとして管理しています。
大まかに、Clipは, VideoInfoとVideoFrameを要素に持ち、これとScriptEnvironmentという操作系で成り立っています
下図に、動画の要素を示します。
Clip - Video
目次へジャンプ。
クリップ(Clip)構造を理解しよう(サウンド編)    2004. 7. 3 (01版)

サウンドは、単純にサンプリングしたサンプルデータが1直線に並んだ形式となっています。
16bit stereo 48KHzの場合は、1秒あたり、(1sample=2bytes) x 2 channels x 48000(audio_samples_per_second)
ただし、サウンドデータは上記フレーム単位ではなく不定長のブロックとして渡されてきます。
下図に、サウンドの要素を示します。
Clip - Sound
目次へジャンプ。
クリップ(Clip)の各要素    2004. 7. 3 (01版)

VideoInfo int width, height
unsigned int fps_numerator, fps_denominator
int num_frames
int pixel_type
int audio_samples_per_second
int sample_type
__int64 num_audio_samples
int nchannels
int image_type
bool HasVideo()
bool HasAudio()
bool IsRGB()
bool IsRGB24()
bool IsRGB32()
bool IsYUV()
bool IsYUY2()
bool IsYV12()
bool IsColorSpace(int c_space)
bool Is(int property)
bool IsPlanar()
bool IsFieldBased()
bool IsParityKnown()
bool IsBFF()
bool IsTFF()
bool IsVPlaneFirst()
int BytesFromPixels(int pixels)
int RowSize()
int BMPSize()
__int64 AudioSamplesFromFrames(__int64 frames)
int FramesFromAudioSamples(__int64 samples)
__int64 AudioSamplesFromBytes(__int64 bytes)
__int64 BytesFromAudioSamples(__int64 samples)
int AudioChannels()
int SampleType()
int SamplesPerSecond()
int BytesPerAudioSample()
void SetFieldBased(bool isfieldbased)
void Set(int property)
void Clear(int property)
int BitsPerPixel()
int BytesPerChannelSample()
void SetFPS(unsigned numerator, unsigned denominator)
bool IsSameColorspace(const VideoInfo& vi)
...未稿
目次へジャンプ。
スケルトン(1)     2004. 7. 7 (01版)

まずは、何もしないプラグインを作成しましょう。
今後の土台となるものですね。
なお、ファイルの位置は以下のようにしています。
   F:\Src
       |
       +--- include
       |       |
       |       +--- avisynth.h
       |
       +--- sample
               |
               +--- sample.cpp
               |
               +--- makefile
---------------------------- f:\src\sample\sample.cpp : start ---------------------------
#include <windows.h>
#include "avisynth.h"

class Sample : public GenericVideoFilter {
public:
	Sample(PClip _child, IScriptEnvironment* env);
	~Sample();
	PVideoFrame __stdcall GetFrame(int n, IScriptEnvironment* env);
};

Sample::Sample(PClip _child, IScriptEnvironment* env) : GenericVideoFilter(_child) {
}

Sample::~Sample() {
}

PVideoFrame __stdcall Sample::GetFrame(int n, IScriptEnvironment* env) {
	PVideoFrame src = child->GetFrame(n, env);
	env->MakeWritable(&src);
	const int   pitch   = src->GetPitch();
	const int   rowsize = src->GetRowSize();
	const int   width   = src->GetRowSize()>>1; //case of YUY2
	const int   height  = src->GetHeight();
	const int   modulo  = pitch - src->GetRowSize();
	const BYTE* srcp    = src->GetReadPtr();
	BYTE*       dstp    = src->GetWritePtr();
	
	
	return src;
}

AVSValue __cdecl Create_Sample(AVSValue args, void* user_data, IScriptEnvironment* env) {
    return new Sample(args[0].AsClip(), env);  
}

extern "C" __declspec(dllexport) const char* __stdcall AvisynthPluginInit2(IScriptEnvironment* env) {
    env->AddFunction("Sample", "c", Create_Sample, 0);
    return "`Sample' Sample plugin";
}
---------------------------- f:\src\sample\sample.cpp : end ----------------------------
少し内容を説明しましょう。
#include "avisynth.h"
プラグインの動作に必要な変数や関数、クラスを定義しています。
プラグインは、Avisynth.dllをロードして関数エントリを調べる必要はありません。
Avisynth.dllからコールバックされる形になります。
class Sample : public GenericVideoFilter {
public:
	Sample(PClip _child, IScriptEnvironment* env);
	~Sample();
	PVideoFrame __stdcall GetFrame(int n, IScriptEnvironment* env);
};
Avisynthがプラグイン作成のために提供しているGenericVideoFilterクラスを
継承して新しい派生クラス Sample を定義します。
これは標準で1つのClipを持っています。
もともとGenericVideoFilterクラスはIClipクラスの派生クラスで、
プラグイン作成の便宜をはかるためのものです。
このクラスを使うことで、不要なメンバ関数を省略することができます。

ただし、IClipクラスから直接派生させた方が良い場合もあります。
例えば、私のプラグインの一つであるBefaですが、入力が音声のみでも動作するように
IClipから派生させています。入力引数にClip(VideoFrame)が無い場合、GenericVideoFilterクラスを
ベースにしているとVideoFrameが無い為にエラーになってしまうからです。
まあ、あまりこのような必要も無いとは思いますが。

ここでは、publicなメンバ関数として以下の3つをプロトタイプ宣言しています。
   ・Sample は、コンストラクタ(初期化処理関数)であり
     クラスのインスタンスが生成された時に行う初期処理を記述します。
     引数は、この場合は クリップのポインタ _child と 環境 env の2つですが、
     後述するプラグインの登録時の指定に依存します。
   ・~Sampleはデストラクタ(終了処理関数)であり
     インスタンスが破棄される時に行う終了処理を記述します。(省略可能)
   ・GetFrameには、実際に行う画像フィルタ処理を記述します。
最後が;(セミコロン)で終わっているのは、ここでは実際の処理は記述せずに
メンバ関数のプロトタイプ宣言であるということを示しています。
もちろん、ここで実際の処理内容を記述することもできますが、長くなると
クラス構造の見通しが悪くなるので、このようにしています。

ここには記述してはいませんが、他にもオーバーロードできるメンバ関数として以下のものがあります。
    void __stdcall GetAudio(void* buf, __int64 start, __int64 count, IScriptEnvironment* env)
    bool __stdcall GetParity(int n)
    void __stdcall SetCacheHints(int cachehints,int frame_range)

また、このクラスにはprotectedなメンバ変数として、
    PClip child;
    VideoInfo vi;
があります。
(protectedとは、アクセスの指定でありpublicとprivateの中間であり、
  クラス外からの参照は不可ですが、派生したクラスからの参照は可能なものです。)
Sample::Sample(PClip _child, IScriptEnvironment* env) : GenericVideoFilter(_child) {
}
前述したSampleクラスのコンストラクタです。
スコープ演算子::で所属するクラスを指定しています。
なお、引数は、後述する入口関数Create_Sampleでクラスを生成するとき指定したパラメタ
が渡されてきます。必要が無ければenvを省略して Sample(PClip _child) としてもかまいません。
コンストラクタは、クラスの初期処理を記述します。動的な領域の獲得や変数の初期化等です。

ここでviの値を変更することで、フレームデータの出力形式を変更することができます。
色空間、フレームレート、幅、高さ等を変更する場合に処理を記述します。

PClip は、
IClipオブジェクトの"smart pointer"であり、参照カウントを自動的にカウントアップダウンして
管理してくれます。最後のPClipを破棄する時点(参照カウントが0)でIClipオブジェクトを自動的に
削除してくれるので、管理を気にする必要がありません。
PClipは通常のポインタと同じく4バイトで、生成時には、値は0に初期化されます。
また値を他に受け渡すことも容易です。
でも再帰的にPClipを参照したり、アロケートしたメモリにPClipを置いてて、メモリを開放することを
忘れたりすることには気をつけましょう。

 : GenericVideoFilter(_child) は、
初期化子でありGenericVideoFilterクラスのコンストラクタを呼び出しています。
初期化子はパラメタリストとコード本体の間にカンマで区切って列挙します。
メンバ変数への代入 foo = bar は、foo(bar)というように記述します。
初期化子は、コード本体より先に実行され、高速に処理されます。
なお、GenericVideoFilter(_child)では、
           child(_child) { vi = child->GetVideoInfo(); }
が実行されます。
Sample::~Sample() {
}
Sampleクラスのデストラクタです。
動的に獲得した領域の開放等の処理を記述します。
PVideoFrame __stdcall Sample::GetFrame(int n, IScriptEnvironment* env) {
	PVideoFrame src = child->GetFrame(n, env);
	env->MakeWritable(&src);
	
	return src;
}
フィルタのメイン部分です。
引数として、カレントフレームを示す n とスクリプト環境 env を伴ってコールバックされてきます。
復帰値としては PVideoFrame値を返します。
(これもPClipと同様、ref countを自動管理してくれます。)
クラスのインスタンスは、一つのAVSをオープンする毎に作成され、それぞれ区別されます。
異なるインスタンスでは、envも別であるかも知れません。

PVideoFrame src = child->GetFrame(n, env);

    PClip childは、GenericVideoFilterクラスのコンストラクタで設定されていましたね。
    protectedなメンバ変数なので継承クラスSampleからでも参照できます。

    フレームNo. n のVideoFrameバッファを参照します。
    もし必要なら別のフレームNo.のデータも参照できます。
    他のフレームNo.を指定した場合、childでチェーンされている他フィルタも通ってきますが、
    内蔵キャッシュが、キャッシュデータを渡してくる場合もあります。

env->MakeWritable(&src);

    srcで指定されたVideoFrameバッファへの変更(書き込み)を可能にします。
    この時点でそのまま再利用可能で無い場合には新たなフレームバッファが生成されます。
    つまりこの前後でデータポインタの挿すアドレスが変化する場合があるということに
    気をつける必要があります。

    デフォルトでは読み込みは許可ですが、書き込みは不可になっています。
    in-place filter(再利用できる場合は元々のフレームバッファをそのまま書き換えて使用する)
    の場合に使用します。
    なお、新たにVideoFrameを構築することもできます。
    (画像の色空間や幅、高さを変える場合には必要になってきますが、詳細は後ほどします。)
        PVideoFrame dst = env->NewVideoFrame(vi);
        //--- この場合はデフォルトで書き込み可能になります。
        return dst;

    VideoFrameは、バッファ内に自動的に作成され、PVideoFrameのref countが無くなった
    時点で再利用されるので、意識してメモリをアロケートしたり開放したりする必要はありません。
    なおNewVideoFrameで作成されたVideoFrameは初期化されてないので、そのままだとそのメモリ
    に入っている値となってしまいます。
    (ゴミが表示されるという(私が出した)バグの原因になったりします。)

const int   pitch   = src->GetPitch();

    ラインピッチを求めています。
    ここで注意すべき点として、各フレームでこのpitchは同じとは限らないということです。
    つまり、最初に一度値を求めて保存したものを次回から使おうとするのはダメだということです。
    (私が、以前 _2DCleanYUY2 で、これをやってしまい、メモリアクセスバイオレーションを
     引き起こしたことがありました。)

AVSValue __cdecl Create_Sample(AVSValue args, void* user_data, IScriptEnvironment* env) {
    return new Sample(args[0].AsClip(), env);  
}
フィルタの入口関数になります。関数名は任意に付けられます。
引数として、パラメタ群argsと、オプショナルデータuser_data、環境envをとります。
new演算子で、Sampleクラスのインスタンスを生成しています。
復帰値は、そのインスタンスのコンストラクタによりAVSValue(IClip *)になります。

AVSValueは、Variant変数で以下の複数の型のどれかになります。
    Bool型、Int型、Float型、String型、Clip型、それら型の配列、無(未定義)
型チェックには、
    IsBool(),IsInt(),IsFloat(),IsString(),IsClip(),IsArray(),Defined()
型定義には、
    AsBool(),AsInt(),AsFloat(),AsString(),AsClip(), ArraySize()または[]による配列

extern "C" __declspec(dllexport) const char* __stdcall AvisynthPluginInit2(IScriptEnvironment* env) {
    env->AddFunction("Sample", "c", Create_Sample, 0);
    return "`Sample' Sample plugin";
}
フィルタをAvisynth本体に登録します。
このDLLで、この関数がexportされることになります。
(DEFFILE等で明示的にexportする必要はありません。)

関数名は、AvisynthPluginInit2でなければなりません。※

複数のAVSスクリプトがオープンされ、異なるenvで2度以上この関数が呼び出されるかも知れません。
(つまりグローバル変数にenvは保存してはいけないということです。)
この関数はAddFunctionを呼びだして、フィルタリストにこのフィルタを登録することだけに
しておきましょう。

復帰値として、プラグインの説明の文字列を返します。
特に明示しない場合、0(NULL strings)を返してもかまいません。
(実際、どういう場合にこの文字列が使われるのか判らないです。)

※ Avisynth 1.xや2.xでは、AvisynthPluginInitという名前でしたが、
   Avisynth 2.5以降でインターフェースが変更されたため、関数名を変更して
   1.x/2.xのavisynth.dllから2.5xのプラグインが呼ばれたり、その逆が起こらないようにしています。

env->AddFunction("Sample", "c", Create_Sample, 0);

    第一パラメタは、登録するプラグイン名を指定します。
    第二パラメタは、パラメタタイプ文字列を指定します。
    第三パラメタは、フィルタの入口関数を指定します。
    第四パラメタは、必要であれば入口関数に渡されるユーザデータCookieを指定します。

    パラメタタイプ文字列は、AVSスクリプトで指定されるパラメタの型チェックをこれで行います。
    "c"        : Clip
    "b"        : Bool
    "i"        : Integer
    "f"        : Float
    "s"        : String
    "*"        : 配列用 zero or more
    "+"        : 配列用 one or more
    "."        : 任意(引数1つに対して)
    "[string]" : 名前付け引数

      【例】 "c[level]i[msg]s"  --- 3つの引数をとる
                                          Sample(clip, level=int, msg="string")

目次へジャンプ。
ビルド(make)のしかた     2004. 7. 7 (01版)

さて、プラグインのソースを作成しました。でもこのソースからどうやってdllを作成すればよいのでしょう?
C++コンパイラを使って、コンパイルすればいいのです。
コマンドプロンプトを起動して、
cd sample
cl sample.cpp /LD
コンパイル
なお、コマンドのオプション一覧は、cl /?(または cl /help)などのように指定すると表示されます。

でも、単一のソースファイルならこれでもいいですが、ソースファイルが2つ以上
になってくるととたんに大変になってきます。
そこで一連の手続きを記述して、ビルドを簡単に実行できるようにしましょう。
nmake.exeとmakefileを使います。
ソースファイルと同じフォルダに、makefileを作成しましょう。

---------------------------- f:\src\sample\makefile : start ---------------------------
NAME    = sample
INDIR   = .
OUTDIR  = ./release
TARGET  = $(NAME).dll
INCLS   = \
          ../include/avisynth.h
OBJS    = \
          "$(OUTDIR)/$(NAME).obj"
RESFILE = 
DEFFILE = 

#-----------------------
CC   = cl
LINK = link
RC   = rc
ASM  = ml

LIBS = kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib \
       advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib

COPT   = /c /W3 /G6 /GX /O2 /Oi /FAcs /Fa"$(OUTDIR)/" /arch:SSE \
         /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /Fo"$(OUTDIR)/"
LOPT   = /DLL /nologo /OPT:NOWIN98 /SUBSYSTEM:WINDOWS /machine:I386 \
         /implib:$*.lib /out:$@
ASMOPT = /c /coff /Fl"$(OUTDIR)/" /Sa /Fo"$(OUTDIR)/"
RCOPT  = /r /fo$(RESFILE)

#-----------------------
ALL:"$(OUTDIR)/$(TARGET)"

"$(OUTDIR)/$(TARGET)": "$(OUTDIR)" $(OBJS) $(RESFILE) $(DEFFILE)
	$(LINK) $(LOPT) @<<
        $(OBJS)
        $(LIBS)
        $(RESFILE)
<<

$(OBJS): $(INCLS)
###$(RESFILE): resource.h

"$(OUTDIR)":
	if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)"

#-----------------------
clean:
#	-@erase "$(OUTDIR)\$(TARGET)"
	-@erase "$(OUTDIR)\*.lib"
	-@erase "$(OUTDIR)\*.exp"
	-@erase "$(OUTDIR)\*.obj"
	-@erase "$(OUTDIR)\*.cod" 2>nul
	-@erase "$(OUTDIR)\*.lst" 2>nul
	-@erase "$(OUTDIR)\*.res" 2>nul

#-----------------------
.SUFFIX:
.SUFFIX: .cpp .obj .rc .res .asm .c

{$(INDIR)}.c{$(OUTDIR)}.obj::
	$(CC) $(COPT) $<

{$(INDIR)}.cpp{$(OUTDIR)}.obj::
	$(CC) $(COPT) $<

{$(INDIR)}.rc{$(OUTDIR)}.res::
	$(RC) $(RCOPT) $<

{$(INDIR)}.asm{$(OUTDIR)}.obj::
	$(ASM) $(ASMOPT) $<

---------------------------- f:\src\sample\makefile : end  ---------------------------
makefileを作成したら、コマンドプロンプトを起動して、nmake.exeを実行します。
>cd sample
>nmake
または
>nmake all
これで、sample.dllが.\releaseフォルダに作成されましたね。
2回目からのnmakeの実行は、更新されたファイルからだけを再生成するようになります。
NMAKE
なお、
>nmake clean
と入力すれば、中間ファイルが削除されて、再びはじめからMakeできるようになります。

このフィルタを呼び出すAvisynthスクリプトは以下のようになります。
ただし、何もしないフィルタなので画像データに変化はありません。

LoadPlugin("f:\src\sample\release\sample.dll")
AVISource("foo.avi")
Sample()
return last

では、記述内容を少し説明します。

NAME    = sample                   プロジェクト名を記述します。.dllの拡張子を取った名前です。
INDIR   = .                        ソースファイル群のフォルダを指定ます。
OUTDIR  = ./release                出力するフォルダを指定します。
                                   \ は / と記述してもいいです。
                                      .  はカレントフォルダ、
                                      .. は親フォルダ、
                                   を意味し、相対パスで指定したい場合に使います。
                                   なお、半角空白が含まれるフォルダ名でも""で括らずに
                                   そのまま記述してください。
                                       【例】OUTDIR = G:\Program Files\bar baz

TARGET  = $(NAME).dll              作成するDLLの名前を指定します。
                                       $(NAME)とは、マクロ NAMEの内容を参照するという意味です。
                                       この場合、NAMEには"sample"が入っているので、
                                       TARGET=sample.dllと同意です。
INCLS   = \
          ../include/avisynth.h    ヘッダ(インクルード)ファイルを列挙します。
                                       末尾の'\' は次行に続くことを指定します。
                                       半角空白を含むファイル名は""で括ってください。
                                       【例】独自のヘッダファイル foo.h bar.h baz.h の
                                             3つを記述する場合は、
                                             INCLS = ../include/avisynth.h foo.h bar.h baz.h
                                             と記述します。
                                       ここで列挙したファイルは、OBJSで指定したファイルの
                                       依存ファイルになります。
OBJS    = \
          "$(OUTDIR)/$(NAME).obj"  オブジェクトファイルを列挙します。
                                       オブジェクトとは、ソースファイルから作成される
                                       中間ファイルのことです。
                                       MS C++の場合、C++ソースからもASMソースからも、
                                       .objが作成されます。
                                       【例】foo.obj, bar.obj, baz.objと3つ記述する場合は、
                                            OBJS = "$(OUTDIR)/foo.obj" "$(OUTDIR)/bar.obj" \
                                                   "$(OUTDIR)/baz.obj"
                                       と記述します。
RESFILE =                           リソースファイル(.res)を指定します。
                                    .resは.rcソースファイルから生成されます。
                                       【例】RESFILE = "$(OUTDIR)/sample.res"

#-----------------------
COPT   = /c /W3 /G6 /GX /O2 /Oi /FAcs /Fa"$(OUTDIR)/" /arch:SSE \
         /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /Fo"$(OUTDIR)/"

                                    これは、PIII CPU向けのコンパイルオプションです。
                                    P4/Athlon向けにしたい場合は、
                                    /G6 を /G7 に、/arch:SSE を /arch:SSE2 に変更してください。
                                    なお、msvcrt.lib が提供されていないため、 /MD オプションは
                                    付けないでください。

#-----------------------生成規則定義部

                       (左辺):(右辺) (左辺)は(右辺)に依存していることを示します。

ALL:"$(OUTDIR)/$(TARGET)"           ターゲット(最終生成物)とその依存関係を指定します。

"$(OUTDIR)/$(TARGET)": "$(OUTDIR)" $(OBJS) $(RESFILE) $(DEFFILE)
$(OBJS): $(INCLS)

                                    ファイルの依存関係です。
                                    ここでは、全てのオブジェクトは、全てのインクルードファイル
                                    に依存しているようにしています。(無駄が多いですが)
                                        ファイルの依存関係は、
                                            $(INDIR)/foo.cpp: $(INDIR)/bar.h $(INDIR)/baz.h
                                            $(INDIR)/baz.h:   $(INDIR)/qux.h
                                         などのように正確に列挙してもよいです。

#-----------------------サフィックスルール(推測規則)
.SUFFIX:                              暗黙のルールをクリア
.SUFFIX: .cpp .obj .rc .res .asm .c   ルールを適用する拡張子を列挙します。

{$(INDIR)}.cpp{$(OUTDIR)}.obj::       .cpp.obj: は、cpp → obj の暗黙の生成規則を指定します。
	$(CC) $(COPT) $<                  .cpp.obj::は、コマンドプロンプトからプロセスを
                                      毎回起動しないようにします。

#-----------------------

nmakeで使えるマクロは、
$(name)     nameの内容を展開します。
$@          生成コマンドの左辺に展開されます。
                foo.obj:bar.cpp ならば、 $@は、foo.objになります。
$*          生成コマンドの左辺の拡張子を除いたものに展開されます。
                foo.obj:bar.cpp ならば、 $*は、fooになります。
$?          生成コマンドの右辺の更新されたものの一つに展開されます。
                foo.obj:bar.cpp baz.cpp,qux.cpp, quux.cpp で、bar.cppとbaz.cppを更新した場合は、
                $?は bar.cpp か baz.cpp かになります。
$<          推測ルールで使用され、依存しているファイル(右辺)の一つに展開されます。

なお、$(foo/*/bar&baz)などは使えないようです。

#-----------------------

目次へジャンプ。
デバッグ(debug)のしかた     2004. 7.14 (02版)
2004. 7.11 (01版)

上記のビルドでは、その結果がずらずらと表示されるので、大量のエラーメッセージ
などが表示された場合などは見るのが大変になります。
そこで、結果をファイルにリダイレクトすることにしましょう。
nmake >result.txt
または
nmake 1>result.txt
これで、標準出力(stdout)がファイル result.txt にリダイレクトされます。
定義済みのファイルNo.には、0(標準入力 stdin)、1(標準出力 stdout)、2(標準エラー出力 stderr)があります。
ファイルに上書きじゃなく、追加書き込みしたい場合は、
nmake >>result.txt
です。
なお同時に標準エラー出力(stderr)もリダイレクトしたい場合は、
nmake 1>result.txt 2>result2.txt
nmake 1>result.txt 2>&1
です。(後者は同じファイル result.txt に両方の出力をリダイレクトしています。)
このファイルをタグジャンプ機能を備えたテキストエディタで開き、
エラー表示行にカーソルを移動後、タグジャンプすることで、
ソースファイルを開き、自動的に該当行にジャンプすることができます。

では、実際に見てみましょう。

例えば、sample.cppで、
dstp = src->GetWritePtr();
と書くべきところを、
dstp = src->GetWritePt();
と書いてしまいました。

nmakeしたところ


テキストエディタで result.txt を開いたところ


result.txtのエラー表示行(sample.cpp(27) : )で、タグジャンプしたところ

次に、コンパイルエラーは無くなったけれど、思ったような結果にならなかった場合など
逐次実行時点の変数の値などを確認したい場合が出てきます。
これには、デバッグメーセージ出力が使えます。
この方法は非常に原始的ですが、有効な手段です。
なお、デバッグメッセージへの出力は、DebugViewで見ることができます。

#include <stdio.h>

PVideoFrame __stdcall Sample::GetFrame(int n, IScriptEnvironment* env) {
    //*** in-place filtering ***
    PVideoFrame src = child->GetFrame(n, env);
    const BYTE* srcp0    = src->GetReadPtr();
    BYTE*       dstp0    = src->GetWritePtr();
    env->MakeWritable(&src);
    const int   pitch   = src->GetPitch();
    const int   rowsize = src->GetRowSize();
    const int   height  = src->GetHeight();
    const int   modulo  = pitch - src->GetRowSize();
    const BYTE* srcp    = src->GetReadPtr();
    BYTE*      dstp     = src->GetWritePtr();
    

{
    char msg[256];
    sprintf(msg, "\nframe=%d  --- rowsize=%d height=%d dstp=%08x\n", n, rowsize, height, dstp);
    OutputDebugString(msg);
}

    return src;
}

DebugViewを起動後、AVSスクリプトを実行し、少しフレームをコマ送りしたところ

目次へジャンプ。

参考文献 及び 使用ソフト一覧

本章の作成にあたり、以下の文章を参考にさせていただきました。
また、以下のソフト及び素材を使わせて頂いています。