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年前半です。バージョン0.1はコンセプトを示すということが優先なので、速度面は期待しないでください。

2024年11月現在、このREADMEには設計思想に関することなどが書かれていません。こういうことについては、バージョン0.1の公開と同時に解説を加える予定です。note.com にも日本語の記事を書くかもしれません。

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

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

システム要件

  • Ruby 3.0 or later

概要

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

セットアップ

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

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

$ ./outvoke.rb -e 'hook "every-sec", /..:..:.[05]/'
# starting Outvoke 0.1 (version 0.0.99.20240928) ---- 2024-11-03 17:16:37 +0900
# ----
# given ruby code:
# hook "every-sec", /..:..:.[05]/
# ----

[2024-11-03 17:16:37 +0900] [outvoke-system] vrchat-001: a new log file was found.
[2024-11-03 17:16:38 +0900] [outvoke-system] vrchat-001: first time log check has done.
[2024-11-03 17:16:40 +0900] 2024-11-03 17:16:40 +0900
[2024-11-03 17:16:45 +0900] 2024-11-03 17:16:45 +0900
[2024-11-03 17:16:50 +0900] 2024-11-03 17:16:50 +0900
[2024-11-03 17:16:55 +0900] 2024-11-03 17:16:55 +0900
[2024-11-03 17:17:00 +0900] 2024-11-03 17:17:00 +0900
[2024-11-03 17:17:05 +0900] 2024-11-03 17:17:05 +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 を探してインクルードします。

以下のようにファイル名を指定して実行すれば、カレントディレクトリの test.rb を探してインクルードします。

$ ./outvoke.rb test.rb

-e オプションによってワンライナーを書くこともできます。

話を分かりやすくするためにこのセクションでは main.rb に書いていくものとします。

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

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

以下は main.rb の例です。

using OutvokeDSL

hook 'every-sec', /22:08:00/

'every-sec' の箇所はデータソースの名前の指定です。/22:08:00/ の箇所は、条件の指定です。

Outvokeを実行したあと放置し、22時8分になると以下のような行が出力されるはずです。

[2024-11-03 22:08:00 +0900] 2024-11-03 22:08:00 +0900

ご自身の環境に合わせて、/22:08:00/ の部分を変えてみてください。

3行目の hook のある行については、以下のように書くのと同じです。

hook 'every-sec', /22:08:00/ do |e|
  e.body
end

別の例を示します。

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 コマンドによって再生します。

複数のデータソースを扱う例を以下に示します。

using OutvokeDSL

hook 'every-sec', /..:..:0[02468]/ do |e|
  "every-sec: #{e.body}"
end

hook 'stdin' do |e|
  "stdin: #{e.body}"
end

上記の main.rb を用意した上で、以下のように実行します。

$ { sleep 5 ; echo aaa ; sleep 5 ; echo bbb ; sleep 5 ; echo ccc ; } | ./outvoke.rb

すると、以下のような出力になるはずです。

# starting Outvoke 0.1 (version 0.0.99.20240928) ---- 2024-11-03 16:06:22 +0900
# ----
# loading ./main.rb ...
# ----

[2024-11-03 16:06:22 +0900] [outvoke-system] vrchat-001: a new log file was found.
[2024-11-03 16:06:22 +0900] [outvoke-system] vrchat-001: first time log check has done.
[2024-11-03 16:06:24 +0900] every-sec: 2024-11-03 16:06:24 +0900
[2024-11-03 16:06:26 +0900] every-sec: 2024-11-03 16:06:26 +0900
[2024-11-03 16:06:27 +0900] stdin: aaa
[2024-11-03 16:06:28 +0900] every-sec: 2024-11-03 16:06:28 +0900
[2024-11-03 16:06:30 +0900] every-sec: 2024-11-03 16:06:30 +0900
[2024-11-03 16:06:32 +0900] stdin: bbb
[2024-11-03 16:06:32 +0900] every-sec: 2024-11-03 16:06:32 +0900
[2024-11-03 16:06:34 +0900] every-sec: 2024-11-03 16:06:34 +0900
[2024-11-03 16:06:36 +0900] every-sec: 2024-11-03 16:06:36 +0900
[2024-11-03 16:06:37 +0900] stdin: ccc
[2024-11-03 16:06:38 +0900] every-sec: 2024-11-03 16:06:38 +0900
[2024-11-03 16:06:40 +0900] every-sec: 2024-11-03 16:06:40 +0900
[2024-11-03 16:06:42 +0900] every-sec: 2024-11-03 16:06:42 +0900
[2024-11-03 16:06:44 +0900] every-sec: 2024-11-03 16:06:44 +0900
[2024-11-03 16:06:46 +0900] every-sec: 2024-11-03 16:06:46 +0900
[2024-11-03 16:06:48 +0900] every-sec: 2024-11-03 16:06:48 +0900

この例では、入力ストリームが複数ある grep のようなものと考えると、イメージしやすいと思います。

ちなみに、-q オプションによって grep のように機能させることは可能です。

$ seq 10 20 | grep 2
12
20
$ seq 10 20 | ./outvoke.rb -q -e 'hook "stdin", /2/'
12
20

現状のOutvokeは、EOFがやってきても自動的に終了はしません。

データソース stdin を使用する場合は、version 0.0.99.20240928 およびそれ以降の outvoke.rb をご利用ください。

VRChatでの利用

Outvokeでは、every-secstdin 以外にも、データソース vrchat-001 がビルトインで用意されています。

データソース vrchat-001 では、VRChatクライアントが出力するログファイルを監視し、入力ストリームであるかのように取り扱います。このデータソースは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年11月現在、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"}'

「蛍の光」ループ再生、終了時刻を指定するのではなく17分後に終了するバージョン:

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

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

Kernel.#spawn はPIDを返します。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}'

「蛍の光」ループ再生、死活監視バージョン:

$ ./outvoke.rb -e 'is_music_on = false; hook("every-sec", / 22:48:00/) {is_music_on = true} ; hook("every-sec") {next if (not is_music_on) || IO.popen( "ps aux | grep [O]gYWssWn7uQ").to_a.length != 0 ; spawn "mpv", "https://www.youtube.com/watch?v=O" + "gYWssWn7uQ", "--really-quiet", "--loop", :err=>"/dev/null" ; sleep 0.5 } ; hook("every-sec", / 23:05:00/) {is_music_on = false; spawn "pkill", "mpv", :err=>"/dev/null"}'

上記の例では、22:48になったらフラグを立てるだけで、あとは1秒おきに死活監視してmpvを起動します。間違ってmpvを終了してしまっても、すぐにまた再生が開始されます。

ワンライナーの欠点は、ソースコード全体が ps aux の出力に含まれてしまうことです。ps aux によるプロセスの死活監視には泥臭い工夫が必要になるかもしれません。これぐらい複雑な場合、ワンライナーではなくファイルを用意したほうがいいでしょう。

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 は実行するたびに現在時刻を取得しようとするため、日付が次の日になる可能性があることに注意する必要があります。

Rubyの一般的な話題として、include? と曜日は相性が良いといえます。例えば月・水・金のいずれかに該当するかどうかは以下のように書けます。

[1,3,5].include?(Time.now.wday)

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

ここからは、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_log3.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

mpvのオプションについて

Outvokeからmpvを起動する上で、知っておくと便利なmpvのオプションは例えば以下です。

  • --fullscreen (全画面)
  • --geometry= (ウィンドウ位置、左上なら --geometry=0:0)
  • --loop (ループ再生)
  • --no-audio (映像のみ)
  • --no-video (音のみ)
  • --no-keepaspect (縦横比を維持しない)
  • --no-osc (GUI無効、このOSCは「On Screen Controller」であって「OpenSound Control」ではない)
  • --really-quiet (余計な出力を抑制)
  • --volume= (音量指定、100が100%)

mpvである必要はなく、VLC media player やコマンドとしての firefoxchromiumxdg-open なども便利です。