Skip to content
Snippets Groups Projects

Outvoke

これはライブラリであると同時にフレームワークです。そして同時に内部DSLです。

If you need English Edition of this README, see README.md

注意

作者であるcleemy desu wayoは、Linuxでしか動作確認してません。

最初のリリースバージョンは0.1になる予定です。おそらく2025年前半です。

2024年の夏から秋にかけて、互換性が失われるような破壊的な変更が加えられる可能性があります。samples/ 以下に置いたサンプルコードはなるべく従来通りの動作になるようにするつもりです。

これらの変更が加えられる前の最後のバージョンは、version 0.0.99.20240619です。

システム要件

  • Ruby 3.0 or later

概要

Outvokeを使用すれば、流れていくデータストリームをフックするようなコードが容易に書けます。

セットアップ

カレントディレクトリに outvoke.rb を置き、実行権限を与えてください。

以下のように -e オプションを使用して起動し、5秒おきに出力があるなら、とりあえず起動はできています。

$ ./outvoke.rb -e 'hook "every-sec", /[012][0-9]:[0-9][0-9]:[0-9][05]/'
# starting Outvoke 0.1 (version 0.0.99.20240815) ---- 2024-08-16 12:46:06 +0900
# ----
# given ruby code:
# hook "every-sec", /[012][0-9]:[0-9][0-9]:[0-9][05]/
# ----

[2024-08-16 12:46:06 +0900] [outvoke-system] vrchat-001: a new log file was found.
[2024-08-16 12:46:07 +0900] [outvoke-system] vrchat-001: first time log check has done.
[2024-08-16 12:46:10 +0900] 2024-08-16 12:46:10 +0900
[2024-08-16 12:46:15 +0900] 2024-08-16 12:46:15 +0900
[2024-08-16 12:46:20 +0900] 2024-08-16 12:46:20 +0900
[2024-08-16 12:46:25 +0900] 2024-08-16 12:46:25 +0900
[2024-08-16 12:46:30 +0900] 2024-08-16 12:46:30 +0900

終了は Ctrl + C で行います。

VRChatがインストールされていなかったり、ログファイルの場所が違っていれば、エラーの行があるかもしれません。

VRChatがインストールされていて、かつVRChatが起動されてから長い時間が経過しているような場合、Outvoke を起動してから first time log check has done. の行が出るまでに長い時間がかかる場合があります。VRChatを再起動すれば、この時間は短縮できます。

設定ファイル

カレントディレクトリに outvoke.conf.rb というファイルがあると、Outvokeはまずそのファイルをインクルードします。

例えば outvoke.conf.rb に以下のような行があれば、VRChatのログファイルを探すディレクトリを指定することができます。

$outvoke.sources['vrchat-001'].log_dir = "#{Dir.home}/.steam/debian-installation/steamapps/compatdata/438100/pfx/drive_c/users/steamuser/AppData/LocalLow/VRChat/VRChat"

基本的な使い方

引数なしでOutvoke本体を起動すると、カレントディレクトリの main.rb を探してインクルードします。

ファイル名を指定することもできますし、-e オプションによってワンライナーを書くこともできますが、話を分かりやすくするためにこのREADMEでは main.rb に書いていくものとします。

この main.rb の先頭で 「using OutvokeDSL」 としておくと、内部DSLのモードになります。設定ファイルを書くような気軽さで記述できます。

main.rb はRubyのスクリプトとして解釈されますので、複雑なプログラムを書くこともできます。

以下は main.rb の例です。

using OutvokeDSL

hook 'every-sec', /05:00:00/ do
  spawn 'play', '-q', '-t', 'wav', '-v', '0.6', 'ring.wav', 'repeat', '50'
end

午前5時になるとカレントディレクトリの ring.wavplay コマンドによって再生します。

(以下執筆中)

VRChatでの利用

いくつかの解説やサンプルについては、以下でみつかるかもしれません。

以下のサンプルもご覧ください。

samples/2024/vrchat_multiple_ds2.rb を動作させている動画: https://x.com/metanagi/status/1802512101111689368

(以下執筆中)

ワンライナーのサンプル集

ワンライナーの場合も、基本的にRubyの機能はすべて使えます。

実行する前に、以下の準備が必要です。

  1. カレントディレクトリに outvoke.rb を用意

  2. カレントディレクトリに maoudamashii-se-system47.wav を用意

( https://maou.audio/se_system47/ から wavファイルをダウンロード)

  1. play コマンドによって音が鳴るかどうか確認
$ play maoudamashii-se-system47.wav
  1. 以下のような内容の outvoke.conf.rb をカレントディレクトリに用意
def ring(vol = "0.05")
  spawn "play", "-v", vol.to_s, "-q", "maoudamashii-se-system47.wav", :err=>"/dev/null"
end
  1. 以下のワンライナーで1秒に1回音が鳴るかどうか確認
$ ./outvoke.rb -e 'hook "every-sec", /./ do ring ; end'

version 0.0.99.20240818 およびそれ以降では、以下のような書き方も可能:

$ ./outvoke.rb -e 'hook "every-sec" do ring ; end'

あるいは以下のように書くことも可能:

$ ./outvoke.rb -e 'hook("every-sec"){ring}'

これらのワンライナーにおける注意点

ここに掲載したワンライナーでは、_1 (numbered parameter) を多用しています。

$ ./outvoke.rb -e 'hookvr(/pickup object/i){puts _1.body}'

上記は、以下と同じです。

$ ./outvoke.rb -e 'hookvr(/pickup object/i){|e| puts e.body}'

2024年8月現在、hook "vrchat-001"hookvr と書くのと同じですが、あえて hookvr は使用していません。

少し複雑な目覚まし時計ワンライナー

さて、ここからワンライナーの例を多数紹介していきます。

AM 5:00 から AM 05:10 までの間、10秒に3回音を鳴らす:

$ ./outvoke.rb -e 'hook("every-sec", / 05:0[0-9]:[0-5][036]/){ring}'

AM 05:00 から AM 05:55 までの間、5分に1回動画を再生:

$ ./outvoke.rb -e 'hook("every-sec", / 05:[0-5][05]:00/) {spawn "mpv", "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "--really-quiet", :err=>"/dev/null"}'

22:48になったら「蛍の光」のループ再生を開始、23:05に終了:

$ ./outvoke.rb -e 'hook("every-sec", / 22:48:00/) {spawn "mpv", "https://www.youtube.com/watch?v=OgYWssWn7uQ", "--really-quiet", "--loop", :err=>"/dev/null"} ; hook("every-sec", / 23:05:00/) {spawn "pkill", "mpv", :err=>"/dev/null"}'

この例では、pkill mpv によりmpvはすべて終了されることに注意してください。

PIDを記憶しておいて、ピンポイントで終了することも可能です:

$ ./outvoke.rb -e 'mpv_pid = nil; hook("every-sec", / 22:48:00/) {mpv_pid = spawn "mpv", "https://www.youtube.com/watch?v=OgYWssWn7uQ", "--really-quiet", "--loop", :err=>"/dev/null"} ; hook("every-sec", / 23:05:00/) {spawn "kill", mpv_pid.to_s, :err=>"/dev/null" if mpv_pid}'

AM 05:00 ちょうどに音を鳴らす、ただし月曜から金曜の間だけ:

$ ./outvoke.rb -e 'hook("every-sec", / 05:00:00/){ring if (1..5).include?(Time.now.wday)}'

Time.now の代わりに、_1.status.now も使用可能:

$ ./outvoke.rb -e 'hook("every-sec", / 05:00:00/){ring if (1..5).include?(_1.status.now.wday)}'

この例ではどちらでもかまいませんが、Time.now は実行するたびに現在時刻を取得しようとすることに注意する必要があります。

シンプルなVRChat関連ワンライナー

誰かがjoinしてきたら音を鳴らす:

$ ./outvoke.rb -e 'hook("vrchat-001", /onplayerjoincomplete/i){ring}'

/onplayerjoined/i は推奨できません。

何かオブジェクトをpickupした時に音を鳴らす:

$ ./outvoke.rb -e 'hook("vrchat-001", /pickup object/i){ring}'

動画関連のログをとりあえず全部表示:

$ ./outvoke.rb -e 'hook "vrchat-001", /(resolv|video)/i'

"resolv" と書いておけば、"resolve" と "resolving" の両方にマッチします。

TopazChat関連のログをとりあえず全部表示:

$ ./outvoke.rb -e 'hook "vrchat-001", /(rtsp|topaz)/i'

少し複雑なVRChat関連ワンライナー

誰かがjoinしてきたら音を鳴らす、ただし自分がjoinしてから90秒間は音を鳴らさない:

$ ./outvoke.rb -e 'hook("vrchat-001", /onplayerjoincomplete/i) {ring if _1.status.elapsed > 90}'

_1.status.elapsed で、自分がjoinしてからの経過時間が取得できます。

これにより、人が大量にいるインスタンスに自分がjoinした直後に、不必要に音が鳴るのを防ぐことができます。

音が鳴らない時も文字列の出力は欲しいという場合:

$ ./outvoke.rb -e 'hook("vrchat-001", /onplayerjoincomplete/i) {ring if _1.status.elapsed > 90 ; _1.body}'

出力したいのが _1.body なら、実際は true でOK:

$ ./outvoke.rb -e 'hook("vrchat-001", /onplayerjoincomplete/i) {ring if _1.status.elapsed > 90 ; true}'

true ではなく 1 などにするのは推奨できません。Outvokeの仕様変更によって将来的に違う動作になる可能性があります。

ワールド内で動画再生が開始されるたびに、mpvでその動画を再生:

$ ./outvoke.rb -e 'hook("vrchat-001", /video playback.*resolve url..(https[-_.a-zA-Z0-9&=?%:\/]*)/i) {spawn "mpv", _1.m[1], "--really-quiet", :err=>"/dev/null"; _1.m[1]}'

ワールド内の動画プレイヤーでエラーが起こると、リトライのせいで複数回mpvが起動する場合があります。

Kernel.#spawn に渡す引数を動的に変更する場合、セキュリティ上のリスクに注意を払う必要があります。OSのコマンドライン全体を単一の文字列として渡すのは避けてください。

$ ruby -e 's = "aaa;date" ; spawn "echo", s'
aaa;date
$ ruby -e 's = "aaa;date" ; spawn "echo #{s}"'
aaa
2024年  8月 24日 土曜日 18:29:43 JST
$ ruby -e 's = "aaa\";date;#" ; spawn "echo \"#{s}\""'
aaa
2024年  8月 24日 土曜日 18:29:48 JST

ワールド内で動画再生が開始されるたびに、URLとタイトルを出力:

$ ./outvoke.rb -e 'hookcc("vrchat-001", /video playback.*resolve url..(https[-_.a-zA-Z0-9&=?%:\/]*)/i) {title = IO.popen(["./yt-dlp_linux", "-q", "--get-title", _1.m[1], :err=>"/dev/null"]).each_line.first; loputs "#{_1.m[1]} -- #{title}"} ; hooklo'

hookcc やマルチスレッドについては、samples/2024/vrchat_loputs.rb および samples/2024/vrchat_loputs_hookcc.rb をご覧ください。

yt-dlp_linux というのは、yt-dlp のLinux版バイナリです。https://github.com/yt-dlp/yt-dlp/releases/ から最新版を入手可能です。

yt-dlp によるタイトル取得にどれくらいの時間がかかっているかを知りたい時は、以下のようにブロックの冒頭でも loputs を実行するといいかもしれません:

$ ./outvoke.rb -e 'hookcc("vrchat-001", /video playback.*resolve url..(https[-_.a-zA-Z0-9&=?%:\/]*)/i) {loputs "#{_1.m[1]} -- hookcc start" ; title = IO.popen(["./yt-dlp_linux", "-q", "--get-title", _1.m[1], :err=>"/dev/null"]).each_line.first; loputs "#{_1.m[1]} -- #{title}"} ; hooklo'

IO.popen に渡す引数を動的に変更するのもセキュリティリスクがあります。OSのコマンドライン全体を単一の文字列として渡すのは避けてください。また、セキュリティ上の理由により、Kernel.#` や %x を使うのは慎重になったほうがいいでしょう。

(まだまだサンプルを追加予定)

teeコマンドについて

Outvokeと直接関係はありませんが、ターミナルに文字列を出力しつつそれを記録に残したいという場合、teeコマンドと組み合わせると便利です。

$ ./outvoke.rb vrchat_join_log2.rb | tee join_history_$(date "+%Y-%m-%d_%H-%M-%S").txt

もちろんワンライナーのモードの時でも使えます。

$ ./outvoke.rb -e 'hookcc("vrchat-001", /video playback.*resolve url..(https[-_.a-zA-Z0-9&=?%:\/]*)/i) {title = IO.popen(["./yt-dlp_linux", "-q", "--get-title", _1.m[1], :err=>"/dev/null"]).each_line.first; loputs "#{_1.m[1]} -- #{title}"} ; hooklo' | tee video_history_$(date "+%Y-%m-%d_%H-%M-%S").txt