心拍に合わせて画像を脈動させるOBSプラグインを作る！【プラグイン編】

この記事は、[Kyoto University Advent Calendar 2021](https://adventar.org/calendars/6580)の25日目の記事です。まだ25日だからセーフです[^1]。こんなに忙しくなるとは思ってなかったんだ……。後日もっと丁寧なのに差し替えます……。

# 概要
画像を一定周期で拡大縮小させるOBSプラグインを作成しました。

# 目的
[去年作ったアプリ](https://qiita.com/Eater_WhiteEye/items/e3d036634ae4043a1cf7)を友人に見せたらもっといろいろできるはずと言われたのでOBSのフィルタという形で実装すれば楽しいのではと思い作ることにしました。
とりあえず簡単な気がしたので心拍に合わせてサイズが変わるフィルタの実装を目指します。通信部が間に合わなかったので今回は固定周波数で実装しました。

# 方法
## OBSの開発環境を用意する。
[OBSのサイト](https://obsproject.com/wiki/Install-Instructions)の[Build from source](https://obsproject.com/wiki/Install-Instructions#windows-build-directions)を見ながら環境を整えていきます。ハマりどころがいくつかありますがこのブログがわかりやすかったです。
### プラグインのプロジェクトを作る
[プラグインに関する公式ドキュメント](https://obsproject.com/docs/plugins.html)を参考にCmakeListを書いてプロジェクトを作ります。僕はそんな余裕がなかったのでサンプルプロジェクトを上書きしました。
### コードを書く
[プラグインに関する公式ドキュメント](https://obsproject.com/docs/plugins.html)のみを参考に書くのはかなり厳しいので類似するプラグインを探しました。
すると[OBS Scale To Sound](https://github.com/Qufyy/obs-scale-to-sound)という音で画像サイズを変化させるプラグインが見つかったのでこれを見て書く事にしました。詳細な中身については割愛して(後で追加しますごめんなさい)、参考プログラムからの大きな変更点のみをまとめます。

このプラグインは元画像のサイズを音量に比例したサイズになるように変更しており、それは`obs_source_info`構造体の`video_render`に渡す関数の中で行われています。フィルタエフェクトを触る時は直前に`obs_enter_graphics`と`obs_source_process_filter_begin`を呼び出し、終了時に`obs_source_process_filter_end`と`obs_leave_graphics'を呼びます。実はフィルタから出力される画像のサイズは`obs_source_process_filter_end`に渡す引数で決まります。なので引数にいい感じにスケーリングしたサイズ情報を渡せば画像を拡大縮小させることができます。ただそのまま拡大縮小させると、画像の左上を基準にスケールされてしまうのでスケールすると同時に画像を斜め方向に移動させて中心をそろえる必要があります。拡大縮小と移動を同時に行うコードはすでに参考プログラムが書いてくれているのでこれを一定周波数の正弦波によってスケーリングされるように書き換えれば目的は達成です。

僕が書いた`video_render`に渡す関数は以下の通りです。

```C
static void hrv_filter_render(void *data, gs_effect_t *effect)
{
	int16_t pchRequest[BUFSIZE];
	DWORD cbRead;

	struct hrv_filter_data *filter = data;
	int32_t w = filter->src_w;
	uint32_t h = filter->src_h;

	DWORD now = GetTickCount();
	float duration = (now - filter->last_time) / 1000.0f;
	float s = filter->hear_rate * duration / 60;
	float _sin = sinf(2 * M_PI * s);
	if (s > 1) {
		filter->last_time =
			now - (DWORD)(duration - 60 / filter->hear_rate);
	}

	filter->current_scale = fmax(_sin * filter->scale + 1, 0);

	obs_enter_graphics();
	if (!obs_source_process_filter_begin(filter->context, GS_RGBA,
					     OBS_ALLOW_DIRECT_RENDERING))
		return;

	gs_effect_t *move_effect = filter->effect;
	gs_eparam_t *move_val =
		gs_effect_get_param_by_name(move_effect, "inputPos");
	gs_eparam_t *show = gs_effect_get_param_by_name(move_effect, "show");

	gs_effect_set_float(show, 1.0f);
	/*if (audio_w <= 0 || audio_h <= 0) {
		gs_effect_set_float(show, 0.0f);
		audio_w = 1;
		audio_h = 1;
	}*/

	//Change the position everytime so it looks like it's scaling from the center
	struct vec4 move_vec;
	vec4_set(&move_vec, (w - w * filter->current_scale) / 2, (h - h * filter->current_scale) / 2,
		 0.0f, 0.0f);

	gs_effect_set_vec4(move_val, &move_vec);

	obs_source_process_filter_end(filter->context, move_effect,
				      w * filter->current_scale,
				      h * filter->current_scale);
	obs_leave_graphics();

	blog(LOG_DEBUG, "w%ld, h%ld, scale%f", w * filter->current_scale,
	     h * filter->current_scale,
	     filter->current_scale);

	UNUSED_PARAMETER(effect);
}
```

# 結果
こんな感じ。周期は60Hz固定です。Scaleを変えると振幅が変わります。
![Desktop 2021.12.25 - 23.48.01.03_8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390575/887c7544-71ab-984d-7a7b-38d887a04dfb.gif)

# 今後の展望
今回は心拍センサとの連携が全く持って間に合わずとりあえずフィルタだけ書いた形となりました。早急に心拍センサと連携したい所存です。去年作ったアプリが心拍数をブロードキャストしてプラグインが受信する形にしたいなと考えています。

[^1]: 投稿ボタンを押す瞬間までは25日でしたが、タグが指定されていないというエラーで差し戻されて26日になりました。