Outvoke
This is a library, and at the same time a framework, and at the same time an internal DSL.
If you need Japanese Edition of this README, see README.ja.md
CAUTION
The author, cleemy desu wayo, has only tested it on Linux.
The first release version will be 0.1. Probably in the first half of 2025. Version 0.1 has the priority of demonstrating the concept, so do not expect the execution speed.
As of November 2024, this README does not include anything about the design philosophy. Explanations on these matters will be added when version 0.1 is released. And I may write an article in Japanese on note.com as well.
During the summer and winter of 2024, potentially disruptive changes will be made that will result in incompatibility. I will try to make the sample code I put under samples/ work as well as before.
The last version before these changes were made is version 0.0.99.20240619.
Requirements
- Ruby 3.0 or later
What's This?
Outvoke makes it easy to write code that hooks into the data stream.
Setup
Put outvoke.rb
in the current directory and give it execute permission.
Start it using the -e
option as follows, and if you see output every 5 seconds, it is running for now.
$ ./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
To exit, press Ctrl + C.
There may be error lines if VRChat is not installed or the log files are in a different location.
If you have VRChat installed and it has been a long time since VRChat was started, it may take a long time after Outvoke is started before you see the first time log check has been done.
This time can be shortened by restarting VRChat.
Config File
If there is a file outvoke.conf.rb
in the current directory, Outvoke will first include that file.
For example, if you have a line like the following in outvoke.conf.rb
, you can specify the directory for VRChat log files to look for.
$outvoke.sources['vrchat-001'].log_dir = "#{Dir.home}/.steam/debian-installation/steamapps/compatdata/438100/pfx/drive_c/users/steamuser/AppData/LocalLow/VRChat/VRChat"
Basic Usage
When you run Outvoke with no arguments, it looks for main.rb
in the current directory and includes it.
If you run Outvoke by specifying a file name as shown below, it will look for test.rb
in the current directory and include it.
$ ./outvoke.rb test.rb
You can write a one-liner using the -e
option.
For the sake of clarity, this section assumes that you are writing in main.rb
.
If you write using OutvokeDSL
at the beginning of main.rb
, you will be in the mode of internal DSL. In the internal DSL mode, you can write it as casually as writing a configuration file.
Since main.rb
is interpreted as a Ruby script, you can write complex programs.
The following is an example of main.rb
.
using OutvokeDSL
hook 'every-sec', /22:08:00/
The 'every-sec'
part specifies the name of the data source. The /22:08:00/
part specifies the condition.
Leave it alone after executing Outvoke, and at 10:08 PM, a line that looks like the following should be output.
[2024-11-03 22:08:00 +0900] 2024-11-03 22:08:00 +0900
Try changing the /22:08:00/
part to suit your environment.
For the third line with hook
, it is equivalent to writing:
hook 'every-sec', /22:08:00/ do |e|
e.body
end
Here is another example.
using OutvokeDSL
hook 'every-sec', /05:00:00/ do
spawn 'play', '-q', '-t', 'wav', '-v', '0.6', 'ring.wav', 'repeat', '50'
end
At 5:00 AM, ring.wav
in the current directory is played by the play
command.
Here is an example of treating multiple data sources:
using OutvokeDSL
hook 'every-sec', /..:..:0[02468]/ do |e|
"every-sec: #{e.body}"
end
hook 'stdin' do |e|
"stdin: #{e.body}"
end
After preparing the above main.rb
, run it as follows:
$ { sleep 5 ; echo aaa ; sleep 5 ; echo bbb ; sleep 5 ; echo ccc ; } | ./outvoke.rb
And you should get output like this:
# 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
In this example, it helps to think of it like grep
with multiple input streams.
If that helps, it is possible to make Outvoke work like grep
by using the -q
option.
$ seq 10 20 | grep 2
12
20
$ seq 10 20 | ./outvoke.rb -q -e 'hook "stdin", /2/'
12
20
Currently Outvoke does not automatically terminate when EOF arrives.
If you use the data source stdin
, please use version 0.0.99.20240928 or later of outvoke.rb
.
With VRChat
In addition to every-sec
and stdin
, Outvoke has a built-in data source vrchat-001
.
The data source vrchat-001
monitors the log file output by the VRChat client and treats it as like an input stream. This data source only watches the log files output by the VRChat client, so just using it normally does not constitute cheating.
Some explanations and samples may be found at the following:
- [cdwact-2023-12] https://note.com/cleemy/n/nf9ea83c0c5e3
- [cdwact-2024-01] https://note.com/cleemy/n/n4ceff128e355
- [cdwact-2024-04] https://note.com/cleemy/n/nf7cce0493fc0
Also see the samples below.
- samples/vrchat_join_log2.rb (entry and exit history)
- samples/vrchat_join_log3.rb (entry and exit history, with user ID)
- samples/2024/vrchat_waittest1.rb (pickup and change the interval)
- samples/2024/vrchat_waittest2.rb (pickup and change the interval)
- samples/2024/vrchat_get_log_file_name.rb (usage of e.status)
- samples/2024/vrchat_multiple_ds1.rb (handling multiple data sources)
- samples/2024/vrchat_multiple_ds2.rb (writing an own data source)
- samples/2024/vrchat_loputs.rb (basic usage of lo and loputs)
a video of samples/2024/vrchat_multiple_ds2.rb in action: https://x.com/metanagi/status/1802512101111689368
(The rest is currently being written)
One-liner examples
Even with one-liners, basically all of Ruby's features are available.
Before you run them, you'll need the following:
-
Put outvoke.rb to the current directory
-
Set up
maoudamashii-se-system47.wav
to the current directory
(download a wav file from https://maou.audio/se_system47/ )
- Check to see if sound is played by the play command
$ play maoudamashii-se-system47.wav
- Put
outvoke.conf.rb
to the current directory with the following contents
def ring(vol = "0.05")
spawn "play", "-v", vol.to_s, "-q", "maoudamashii-se-system47.wav", :err=>"/dev/null"
end
- Check to see if the sound is played once per second with the following one-liner
$ ./outvoke.rb -e 'hook "every-sec", /./ do ring ; end'
In version 0.0.99.20240818 or later, the following is also possible:
$ ./outvoke.rb -e 'hook "every-sec" do ring ; end'
You can also write as follows:
$ ./outvoke.rb -e 'hook("every-sec"){ring}'
Notes on the one-liners presented here
These one-liners make heavy use of _1
(numbered parameter).
$ ./outvoke.rb -e 'hookvr(/pickup object/i){puts _1.body}'
The above is the same as:
$ ./outvoke.rb -e 'hookvr(/pickup object/i){|e| puts e.body}'
As of November 2024, hook "vrchat-001"
is the same as hookvr
, but these one-liners dare not use hookvr
.
Slightly complicated alarm clock one-lilners
Now, here are a number of one-liner examples.
Sound three times every 10 seconds between 5:00 AM and 05:10 AM:
$ ./outvoke.rb -e 'hook("every-sec", / 05:0[0-9]:[0-5][036]/){ring}'
Play the video once every 5 minutes between 05:00 AM and 05:55 AM:
$ ./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"}'
At 22:48, start looping "Hotaru no Hikari" and end at 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"}'
looping of "Hotaru no Hikari", version that ends after 17 minutes instead of specifying an end time:
$ ./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"}'
Note that in these examples, pkill mpv
will terminate all mpv.
Kernel.#spawn
returns PID. PID can be memorized so it's possible to terminate the mpv with pinpoint accuracy:
$ ./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}'
looping of "Hotaru no Hikari", life-and-death monitoring version:
$ ./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"}'
When 22:48 is reached, just set a flag and then it will start life-and-death monitoring every other second and execute mpv. If you exit mpv by mistake, it will immediately start playing again.
The downside of the one-liner is that the entire source code will be included in the output of ps aux
. Muddy ingenuity may be required to life-and-death monitoring of the process with ps aux
. For a case as complex as this, it is better to prepare a file instead of a one-liner.
Sound at exactly 05:00 AM, but only between Monday and Friday:
$ ./outvoke.rb -e 'hook("every-sec", / 05:00:00/){ring if (1..5).include?(Time.now.wday)}'
Instead of Time.now
, you can also use _1.status.now
:
$ ./outvoke.rb -e 'hook("every-sec", / 05:00:00/){ring if (1..5).include?(_1.status.now.wday)}'
In these examples, either is fine, but you should be aware that Time.now
attempts to get the current time every time it is evaluated, so the date may end up being the next day.
As a general Ruby topic, include?
can handle days of the week well. For example, whether it falls on Monday, Wednesday, or Friday can be written as follows:
[1,3,5].include?(Time.now.wday)
simple VRChat-related one-liners
From here, I will present a number of samples related to VRChat.
Play a sound when someone joins:
$ ./outvoke.rb -e 'hook("vrchat-001", /onplayerjoincomplete/i){ring}'
/onplayerjoined/i
is not recommended.
Play a sound when you pick up an object:
$ ./outvoke.rb -e 'hook("vrchat-001", /pickup object/i){ring}'
Display all video-related logs at all:
$ ./outvoke.rb -e 'hook "vrchat-001", /(resolv|video)/i'
If you write "resolv", it will match both "resolve" and "resolving".
Display all TopazChat-related logs at all:
$ ./outvoke.rb -e 'hook "vrchat-001", /(rtsp|topaz)/i'
Slightly complicated VRChat-related one-liners
Play a sound when someone joins, but no ringing for 90 seconds after you join:
$ ./outvoke.rb -e 'hook("vrchat-001", /onplayerjoincomplete/i) {ring if _1.status.elapsed > 90}'
You can get the elapsed time since you joined by _1.status.elapsed
.
This will prevent unnecessary noise immediately after you join an instance with a large number of people.
When you want to output string even without sound:
$ ./outvoke.rb -e 'hook("vrchat-001", /onplayerjoincomplete/i) {ring if _1.status.elapsed > 90 ; _1.body}'
If you want to output _1.body
, you can actually use true
:
$ ./outvoke.rb -e 'hook("vrchat-001", /onplayerjoincomplete/i) {ring if _1.status.elapsed > 90 ; true}'
Using 1
instead of true
, for example, is not recommended. It may result in different behavior in the future due to changes in the Outvoke specification.
Whenever a video starts playing in the world, play that video in 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]}'
If an error occurs in a video player in the world, mpv may start multiple times due to retries.
If you dynamically change the arguments passed to Kernel.#spawn
, you should be aware of the security risks. Avoid passing the entire OS command line as a single string.
$ ruby -e 's = "aaa;date" ; spawn "echo", s'
aaa;date
$ ruby -e 's = "aaa;date" ; spawn "echo #{s}"'
aaa
Sat Aug 24 18:30:35 JST 2024
$ ruby -e 's = "aaa\";date;#" ; spawn "echo \"#{s}\""'
aaa
Sat Aug 24 18:30:38 JST 2024
Every time a video starts playing in the world, output the URL and title:
$ ./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'
For information on hookcc
and multi-threading, see samples/2024/vrchat_loputs.rb and samples/2024/vrchat_loputs_hookcc.rb.
yt-dlp_linux
is a binary of yt-dlp for Linux. The latest version is available from https://github.com/yt-dlp/yt-dlp/releases/.
If you want to know how long it takes to get the title by yt-dlp, you may also do loputs
at the beginning of the block as follows:
$ ./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'
Dynamically changing the arguments passed to IO.popen
has also a security risk. Avoid passing the entire OS command line as a single string. And you should also be cautious about using Kernel.#` or %x
for security reasons.
(More and more samples to be added)
About tee command
Although not directly related to Outvoke, it is useful in combination with the tee command if you want to output strings to the terminal while keeping a record of it.
$ ./outvoke.rb vrchat_join_log3.rb | tee join_history_$(date "+%Y-%m-%d_%H-%M-%S").txt
Of course, it can also be used in one-liner mode.
$ ./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
About options of mpv
In launching mpv from Outvoke, some useful mpv options to know are, for example:
-
--fullscreen
(full screen) -
--geometry=
(position of window,--geometry=0:0
for upper left) -
--loop
(looping) -
--no-audio
(only video) -
--no-video
(only audio) -
--no-keepaspect
(not maintain aspect ratio) -
--no-osc
(GUI disabled, this OSC is "On Screen Controller", not "OpenSound Control") -
--really-quiet
(suppress unnecessary output) -
--volume=
(volume setting, 100 is 100%)
It does not have to be mpv. VLC media player or firefox
, chromium
, or xdg-open
as commands are also useful.