Extension

PeloReaper Extensionの情報やダウンロードはこちらからどうぞ。
PeloReaper Extension for REAPER is here.
ReaperBanner01_50p

C++ REAPER Extension (6) : CやReaScript(lua,EEL)にAPIを公開

こんにちは、ぺろりです。
今回はExtensionからC/C++とReaScript(lua,EEL)に対してAPIを公開する方法を紹介します。
Pythonでスクリプトを書く方法は試してないので機会があれば・・・。

ここまで読み進めて来たという方であればコード見ればすぐ分かりそうですので、そこからいきましょう。
/// REAPER経由での公開関数
/// @param[in] num 任意の数値
/// @return てきとーに計算した結果
static int UnkoFunc(int num)
{
	return 2 * num; // てきとーに値を倍にして返すだけ
}
/// 単なる便利用typedef
typedef int (*FUNC_UnkoFunc)(int);

/// Luaから呼び出せるようにするための関数
/// - Script系の言語からはCの関数を直接ではなくて、こういった可変引数的な呼ばれ方をする模様
/// @param[in] arglist luaとかからくる引数リスト
/// @param[in] numParams arglistの要素数
/// @return 結果の値(ここだとUnkoFunc()の戻り値)
static void* __vararg_UnkoFunc( void** arglist, int numParams )
{
	// Cで公開していた関数を呼ぶ
	int res = UnkoFunc( (intptr_t)arglist[0] );
	// 戻り値をvoid*にして返してやる
	return (void*)(intptr_t)(res);
}

REAPER_PLUGIN_DLL_EXPORT int 
REAPER_PLUGIN_ENTRYPOINT(
	REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t *rec)
{
	// (中略)
	// 初期化時の処理で、以下の登録処理を行う

	// 公開する処理を書く関数を登録
	plugin_register( "API_UnkoFunc", (void*)UnkoFunc );
	// 関数の引数やHelp文字列などの情報を登録
	plugin_register( "APIdef_UnkoFunc",
			"int\0int\0num\0[Pelori:API]Exported Unko API" );
	// luaなどのスクリプト側から呼ばれる関数を登録
	plugin_register( "APIvararg_UnkoFunc", __vararg_UnkoFunc );


	// Cで呼び出せるかのテスト
	// 関数のアドレスをREAPERから取得
	FUNC_UnkoFunc unkofunc_ptr = (FUNC_UnkoFunc)(rec->GetFunc("UnkoFunc"));
	// 呼び出してみる
	int unkoResult = unkofunc_ptr( 3 ); // 3*2=6が返ってくればOK

// (後略)
}

plugin_register( "API_UnkoFunc", (void*)UnkoFunc ) は、"UnkoFunc"という名前でExtensionAPIを登録しています。"API_"以下の部分が公開APIの名前となりますが、登録する関数(第2引数のやつ)の名前は合わせなくても問題ないようです(__UnkoFuncという関数を登録しても問題なかった)。

plugin_register( "APIdef_UnkoFunc", ... ) ではAPIの戻り値、Helpテキストといった情報を登録します。これも登録が必須です。文字列のフォーマットは
"戻り値\0引数の型リスト\0引数の名前リスト\0Helpテキスト"
といった感じでしょうか。複数引数があるなら、例えば "int\0int,double\0arg1,arg2\0Hoge Function" という感じです。

plugin_register( "APIvararg_UnkoFunc", ... ) はスクリプトから呼ばれる関数登録となります。スクリプトからは "API_○○" で登録した関数が直接呼ばれるわけではありません。"APIvararg_○○" で登録した関数が呼ばれることになります。
スクリプトから呼び出されると、引数(arglist)がどれだけ積まれたか(numParams)という情報がやってきますので、必要なだけ情報を引用して結果を返してやればいいようです。今回のサンプルだと UnkoFunc を呼んで結果を返してやればOK。

あとはExtension終了時に plugin_register( "-API_UnkoFunc", (void*)UnkoFunc ); などで登録したものを解除して実装完了です。(このへんはまぁ、もう分かりますよね)

実際のところ、1つのAPI登録で3つの登録用文字列が必要なことから、沢山APIを公開しようとすると結構煩雑になっていくと思います。SWSのReaScript.cppあたりを見てみると、マクロで文字列を一括定義したりといった部分などもろもろ参考になるのではないかと思います。



これらを実装したExtensionをREAPERにインストールしてからREAPERを起動して、reaper_plugin_functions.hを書き出す(Action"[developer] Write C++ API functions header"で[Write functions exported by 3rd party extensions]にチェックを入れる)と、自分で登録した関数(ここだとUnkoFunc)がヘッダに出力されて使えるようになります。
// (↓ reaper_plugin_functions.hに書き出された部分)
#if defined(REAPERAPI_WANT_UnkoFunc) || !defined(REAPERAPI_MINIMAL)
REAPERAPI_DEF //==============================================
// UnkoFunc
// [Pelori:API]Exported Unko API

  int (*UnkoFunc)(int num);
#endif

・・・っと書いたものの、自分の環境だとこの3rdPartyの関数も出力したreaper_plugin_functions.hを使おうとしてみたところ、他のExtensionが公開している関数で使われている構造体などの型が見つからず、コンパイルエラーになってしまいました(このへんもうちょっと頑張って取り組むのは後にしようかと思います)。
なので、自分は前述のサンプルコードにあるようにGetFunc()で関数ポインタを取得して使う方法を今のところ使っています。

ひとまずC++側では呼べるようになったので、次はluaで呼び出してみます(ActionListでReaScriptのNewボタンでluaファイルを作成してスクリプトを書きます)。
val = reaper.UnkoFunc(3)
reaper.ShowConsoleMsg(val)
QS_20180128-190450
ハイ、簡単ですね。

ついでにEELでも試してみます。今度は.eelファイルを作成して書きます。
EELではExtension APIの呼び出しに extension_api() を使います。
num = extension_api("UnkoFunc", 3);
sprintf(#str, "Result:%d", num);
ShowConsoleMsg(#str);
QS_20180128-192654

ちなみにExtensionからの公開関数はREAPERのメニューから、[Help > ReaScript documentation]でHTML出力されるので、自分の関数がこのHTMLにも表示されるようになります。
QS_20180128-195149


今回はREAPERのExtension API公開方法を解説しました。
これでスクリプトからもあなたの関数を呼び出すことができて、また一段とREAPER機能実装の自由度が上がるのではないかと思います。

しかしこのDAW、ホントやりたい放題出来てオモチャとして面白すぎる・・・

ではまた次の記事でお会いしましょう。

C++ REAPER Extension (5) : ポーリング処理を行うには?

こんばんは、ぺろりです。
このブログ、最初っからテクニカルな話題ばかりで普通のREAPERユーザーを完全に置いてきぼりにしてる感が半端ないですが、もう少しだけ続きます。

今回はポーリング処理についてですが、その前にhookcommand2コールバックに来るActionIDの話を少し。

REAPERはActionを実行する際、Extensionに対してどのActionがリクエストされてますよ、ということをhookcommand2コールバックで通知してきます。
これによって何かActionが実行されるタイミングだということが「ある程度」検知できます。
何故、「ある程度」なのかというと、実際にActionを実行するExtensionが「実行しましたよ」ということをhookcommand2コールバックで返す(戻り値をtrueで返す)と、そのActionIDの通知はそれ以上他のExtensionへ通知されることはないからです。
恐らくExtensionのDLL登録の順番に依存しているのかもしれません(未検証)が、そういった環境依存で自分のExtensionに通知が来たり来なかったりと動作が変わってしまうのも困ります。

REAPERの組み込みアクションであれば、最後にREAPER本体が処理しそうですが、これにも問題があるようです。
Action実行リクエストの通知はREAPER上のすべての挙動を検知できる訳ではないようで、例えば、Markerは作成(Mキー押下)時にはActionが通知されてきましたが、MarkerをAlt+クリックで削除する際にはAction通知は来ないようでした。
Markerが作成されるタイミングは分かっても、削除されたタイミングが分からないので、自分でMarker情報をもとに何か処理を書く時にだいぶ扱いづらそうなのが予想出来るかと思います。
例えばMarkerの情報をキャッシュしておいて自分のウィンドウに表示等したいが、削除されたタイミングが検知できないとそのキャッシュを更新するタイミングがない、とかでしょうか。
せめてポーリングできるような仕組みがあればなあ・・・

ということで前置きが長くなりましたが、ポーリングで処理を書く方法があるのでそれを紹介します。

■ポーリング処理の実装方法
SWSの実装を参考にすると、IReaperControlSurface::Run() というコールバックメソッドを利用した方法が使えそうです。
ダミーのControlSurfaceクラスを作って利用する感じです。
// DummyのControlSurface
class DummyControlSurface : public IReaperControlSurface
{
  public:
	DummyControlSurface() { }
	virtual ~DummyControlSurface() { }
    
	virtual const char *GetTypeString()   { return ""; } // SWSを参考に
	virtual const char *GetDescString()   { return ""; } // SWSを参考に
	virtual const char *GetConfigString() { return ""; } // SWSを参考に

	// 30FPSかそれくらいで呼ばれるらしい(頻度は未検証)
	virtual void Run()
	{
		// ここでの処理は出来るだけ軽くしておいた方がいいかも。
		// IReaperControlSurfaceには他にも特定のタイミングで
		// 呼ばれるメソッドがあるので、出来るだけそちらで処理して
		// どうしてもダメならここで適当な間隔でポーリングする
	}
};

このクラスのインスタンスを生成し、"csurf_inst"で登録(plugin_register)してやります。
(不要になったらこれまでのコールバック等と同様に "-csurf_inst" で登録解除してあげましょう)
// Run()タイミング検知用ControlSurfaceを生成・登録
DummyControlSurface* csurfInst = new DummyControlSurface();
plugin_register( "csurf_inst", csurfInst );
これで DummyControlSurface::Run() が定期的に呼ばれるようになります。

今回はポーリング処理の書き方について説明しましたがいかがでしたでしょうか。
おそらくこれまでの5回分の記事の解説で、REAPERのExtensionを実装する最低限の準備が揃ったというか、これで大抵のことは無理矢理なんとか実装&問題回避等出来るかもしれません(ホントか?)。

ではまた次の記事でお会いしましょう。

C++ REAPER Extension (4) : Preferencesにページ追加

こんばんはぺろりです。
今日はREAPERのPreferencesに自分のページを追加する方法を説明します。

といってもやるべきことはシンプルで、以下の二つです。
・ページ毎にウィンドウ作成コールバックを実装
・prefs_page_register_t を "prefpage" という名前で登録(plugin_register)
 (この構造体のメモリはどこかに保持しておく)

■Preferencesページ情報の登録
まずはコードを見てみましょう。
prefs_page_register_t prefPageMain; // メインページ用のデータ(どこかに保持しておく)
prefs_page_register_t prefPageChild; // 子供ページ用のデータ(どこかに保持しておく)

// 大項目
memset( &prefPageMain, 0, sizeof(prefPageMain) );
prefPageMain.idstr        = "PREF_PELORI_MAIN"; // ID名(任意)
prefPageMain.displayname  = "Pelori"; // Preferenceに表示される名前
prefPageMain.create       = OnCreatePrefWndMain; // Window生成コールバック
prefPageMain.childrenFlag = true; // 子供がいる
// 登録
plugin_register( "prefpage", &prefPageMain );

// 小項目(子供ページ)
memset( &prefPageChild, 0, sizeof(prefPageChild) );
prefPageChild.idstr        = "PREF_PELORI_CHILD1";
prefPageChild.par_idstr    = prefPageMain.idstr; // 親のIDを指定
prefPageChild.displayname  = "Sub1";
prefPageChild.create       = OnCreatePrefWndChild;
prefPageChild.childrenFlag = false; // 子供はいない
// 登録
plugin_register( "prefpage", &prefPageChild );

ここではメインの大項目とその1階層下に表示できる小項目(子供ページ)を追加しています。
子供ページが不要であれば大項目のページ登録だけでも問題ありません(この場合は prefPageMain.childrenFlag = false にする)。

登録できると以下のような感じにPreferencesウィンドウに表示されることになります。
QS_20180124-022801

次にウィンドウ生成時のコールバック実装ですが、引数に親ウィンドウとなるHWNDが渡されてきますので、このウィンドウに子ウィンドウとしてセットしてやればOKです。
また、呼ばれるタイミングはページの項目を選択した時です(上の例だと"Pelori"や"Sub1"選択時)。
こちらで作ったウィンドウが勝手に破棄されたりはしないようなので、あらかじめウィンドウは作成しておいて、このコールバックで渡されてきたHWNDに毎回子ウィンドウとしてくっつけてやればよいようです。
HWND OnCreatePrefWndMain( HWND hWndParent )
{
	// あらかじめ作っておいたウィンドウのハンドル(ウィンドウ作成まわりは割愛)
	HWND hWndPage;
	// SetParent()の前にWS_CHILDを設定する必要がある
	SetWindowLong( hWndPage, GWL_STYLE,
		 (GetWindowLong(hWndPage, GWL_STYLE) | WS_CHILD) );
	SetParent( hWndPage, hWndParent );
	return hWndPage;
}
小項目(子ページ)側もやることは同様です。

■Preferences情報の登録解除
DLLエントリポイントの終了処理を書くあたりで、prefs_page_register_t のデータの登録解除や自分で作成したウィンドウの破棄等を行います。
plugin_register( "-prefpage", &prefPageChild );
plugin_register( "-prefpage", &prefPageMain );

これでPreferencesウィンドウへのページ追加に関しては完了です。

Preferencesまわりで書ける処理はこの程度のようで、例えば「Preferencesウィンドウが閉じられた」とかいったタイミングを検知する方法は特に提供されていない(?)ようです。
なので、Preferences設定の保存や復帰も自分でいい感じのタイミングに手動で実装する必要があるのではないかと思います。

というところで今回はここまで。
また次回の記事でお会いしましょう。
このブログについて
ぺろりがREAPERで遊びたいというだけのブログかもしれない

必ずこちらをお読みください

twitter: @pelori

管理人用
  • ライブドアブログ