スクリプトでfoobar2000にて再生中の曲を喋る

foobar2000には、COM Automation Serverというプラグインがあって、そこから現在起動中のfoobar2000の情報の取得や、制御が出来るようだ。
制御の口は ProgID 'Foobar2000.Application.0.7' のCOMコンポーネントとして登録されていて、VB(S)ならCreateObject、JSならActiveXObjectでオブジェクトを生成して、そこからアクセスすることになる。
再生中かどうかは、Playback.IsPlaying (Boolean)プロパティ、一時停止中かどうかは、fb2k.Playback.IsPaused (Boolean)プロパティで確認できるので、今再生しているかどうかを条件分岐することが出来る。
また、曲名やアーティスト名を取得するには、書式に従って文字列を埋め込むPlayback.FormatTitleメソッドを利用する。例えば、%title%と書けばそこにタイトルが、%artist%と書けばアーティストが埋め込まれるという具合だ。
これらを組み合わせれば、こんな感じで今聴いている曲を喋るスクリプトを書ける。

function event::onChannelText(prefix, channel, text) {
  if (text == '@何聴いてる?') {
    var fb2k = new ActiveXObject('Foobar2000.Application.0.7');
    if (!fb2k.Playback.IsPlaying || fb2k.Playback.IsPaused) {
      // 再生していない
      send(channel, "今は再生していないよ。");
    } else {
      // 何か再生している
      send(channel, fb2k.Playback.FormatTitle("今 は%title%/%artist%を再生中だよ。"));
    }
  }
}

しかし、このプラグインには一つ問題があって、foobar2000が起動していないときにオブジェクトを生成すると、foobar2000が起動してしまう。その上、foobar2000が起動するまでの間、スクリプトエンジンが属するスレッドの動作をブロックする。foobar2000が常駐アプリと化しているのならいいけど、そうでない場合ではちょっと使い勝手が悪い。
それなら、foobar2000が起動しているかどうか確認して、起動しているときだけオブジェクトを生成すれば良いんじゃないか?
確かにそれは良いアイデアかも知れない。アプリケーションの起動をJScriptで検知するには幾つかの方法がある。
一つ目はtasklist.exeの出力を見ることだ。
tasklistコマンドは現在起動しているプロセスを列挙する。これをexecuteCommand関数で呼び出し、標準出力を構文解析することで、特定のプロセスが起動していることを確認できる。しかし、やっていることはスクレイピングなので、余り安定した方法とは言えない。tasklist相当のプロセス列挙プログラムを用意するなら、検討に値する方法ではある。
二つ目はWMIのWin32_Processクラスを利用する方法だ。
このクラスは現在起動しているプロセス情報を見ることが出来る。Win32_Process class - Windows applications | Microsoft Docsに特定のプロセス名を持つプロセスを列挙する例が載っている。一つ目と違い、明確な手順で取得できるので、安心して使えそうだが、クエリを発行するときに、若干秒スレッドをブロックすることがある。これを防ぐには、非同期発行とイベント接続を用いれば良いのだが、あいにくLimeChatスクリプトホストはその機能を提供していないので、却下せざるを得ない。
三つ目はfoobar2000の生成するミューテクスオブジェクトを検知することだ。
foobar2000は起動中「FOOBAR2000_("file://(foobar2000絶対パスUTF-8表現)"のCRC32を16進文字列化したもの)」という名前のミューテクスオブジェクトを生成して保持している。これをスクリプトで検知すれば、foobar2000の起動を確認したことになる。当然であるが、JScriptLimeChatスクリプトホストはその機能を提供していない。一つ目と同じく、プログラムを用意すれば検討に値する。
今回は、JScript上でのUTF-8文字列のCRC32計算を考えてみると言うこともかねて、三つ目の方法を採ることにした。
JScriptの文字列はUCS-2で文字符号化されているので、UTF-8文字列としてのCRC32を得るには、変換を行わなければいけない。UTF-8 - Wikipediaエンコード体系にその方法が掲載されている。

  • 128未満はそのまま移す
  • 2048未満は{110|上位5ビット}、{10|下位6ビット}に移す
  • 65536未満は{1110|上位4ビット}、{10|下位6ビット}、{10|下位6ビット}に移す

UCS-2の場合、上限は65535なので、4バイト以降の表現は必要ない。
この変換手順と、CRC-32-IEEE 802.3の生成多項式を用いたアルゴリズムと合わせると、次のようになった。

// int crc32(string text)
// UNICODE文字列のUTF-8表現に対応するCRC32の値を求める
// text : 入力文字列
function crc32(text) {
	var H = [0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3,0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91,0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7,0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5,0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B,0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59,0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F,0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D,0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433,0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01,0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457,0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65,0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB,0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9,0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F,0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD,0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683,0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1,0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7,0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5,0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B,0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79,0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F,0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D,0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713,0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21,0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777,0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45,0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB,0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9,0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF,0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D];
	var crc = -1;
	for(var i = 0; i < text.length; ++i) {
		var c = text.charCodeAt(i);
		if (c < 128) {
			crc = H[crc & 255 ^ c] ^ (crc >>> 8);
		} else if (c < 2048) {
			crc = H[crc & 255 ^ ((c >> 6) | 192)] ^ (crc >>> 8);
			crc = H[crc & 255 ^ ((c & 63) | 128)] ^ (crc >>> 8);
		} else {
			crc = H[crc & 255 ^ ((c >> 12) | 224)] ^ (crc >>> 8);
			crc = H[crc & 255 ^ (((c >> 6) & 63) | 128)] ^ (crc >>> 8);
			crc = H[crc & 255 ^ ((c & 63) | 128)] ^ (crc >>> 8);
		}
	}
	return crc ^ -1;
}

変換テーブルは予め用意しておいたけど、ランタイム生成でも良いかもしれない。
これで得た数値を次のように処理すれば、foobar2000ミューテクスオブジェクトの名前が手に入れられる。

// string HEX$(int num, int digits)
// 整数を符号無し16進数文字列に変換する
function HEX$(num, digits) {
	var result = '';
	do {
		result = (num & 15).toString(16).toUpperCase() + result;
		num >>>= 4;
	} while(num);
	return (STRING$(8, "0") + result).slice(-digits);
}

// string STRING$(int num, string chr)
// 文字を並べた文字列を取得する
function STRING$(num, chr) {
	var result = '';
	for(var i = 0; i < num; ++i) {
		result += chr;
	}
	return result;
}

// ----------------------------------------------------------------------------

var foobarPath = "c:\\bin\\foobar2000"; // foobar2000の絶対パス(パス区切りは必ず\・最後の区切りはつけない)
var foobarMutex = "FOOBAR2000_" + HEX$(crc32("file://" + foobarPath), 8);

あとは名前を引数に取り、ミューテクスオブジェクトが存在するかどうかの結果を返すプログラムを用意すれば、本方法の完成だ。
出来れば、何も用意せず完結させたいところなんだけど、そうなると二つめぐらいしか無さそう……