この記事は Node.js Advent Calendar 2015 3日目の記事です。
ちょうどひと月前、10月24日から11月3日にかけて 奈良・町家の芸術祭はならぁと というイベントがあり、そこで ADSR展 というインスタレーション作品群の制作に携わりました。
そのシステムを Node.js を使って構築したので、その構成について書きます。
作品概要
どこか懐かしく美しい町の空間に、光と音による体験型作品を点在させる。体験者は夕刻に街並みを歩いていると、ふと風景の中に、微小に、時にダイナミックに変化する音や灯火の存在に気づくことになる。目前の風景(空間)が楽譜化され、音と光のパターンが立ち現れる。この音は、スマートフォンを用いて、光の変化に完全同期して生成される。
技術よりの視点から情緒なく説明すると、町のいくつかの場所に設置された電球と音の鳴るガジェット(モーター式オルガンや電磁リレーなど)、さらにそれらを補完する形でスマホのブラウザから出力される音を連携させて、町自体が持つ魅力を拡張するという作品でした。
開発計画
予想
- 仕様は最後まで決まらないような気がする
- 直前まで本番構成で動かせないないかもしれない
- 本番稼動のあとで調整が多々ありそう
- 失敗しました とは言えない
方針
- "表現" と "技術" とにレイヤーを分割して前者は柔軟に後者は入念に準備する
- あいまいな部分が残ると思われるので部分的にアップデートしやすいようにする
- 性能よりも開発のしやすさを優先する
基本の構成
サーバーで音楽的なデータを生成して、そのデータをブラウザや RaspberryPI にプッシュ送信するシンプルな構成です。データを受けたブラウザは Web Audio API を使って音を出力し、データを受けた RaspberryPI は 照明機器制御の信号を出力します。
- Server: 音楽的なデータを生成する
- MobilePhone: サーバーからのデータを受けて音を生成する
- RaspberryPI: サーバーからのデータを受けてDMXを制御する
- DMXController: 照明機器を制御する装置
サーバーが生成するデータはブラウザへ送信するための JSON と RaspberryPI 向けに送信するための OSC(OpenSound Control) へ変換できるようにしています。OSC というのは Max/MSP や openFrameworks などメディアアート系のプログラム環境で良く使われているメッセージングプロトコルで、今回は JSON から OSC のメッセージ形式に変換するライブラリを使い、一方通行的にデータを送信します。OSC では一般的に UDP が使われます。
const OscMessage = require("osc-msg");
class ServerData {
constructor(address, params) {
this.address = address;
this.params = params;
}
// RaspberryPI に送るときの形式
toOSC() {
return OscMessage.toBuffer({
address: this.address,
args: [
{ type: "integer", value: this.params[0]|0 },
{ type: "integer", value: this.params[1]|0 },
{ type: "integer", value: this.params[2]|0 },
]
});
}
// ブラウザに送るときの形式
toJSON() {
return {
address: this.address,
params: this.params,
};
}
}
データを送信する
サーバーと RaspberryPI との接続はグローバルIPアドレスを持たない安価なプリペイド式SIMを使ったので、サーバーから RaspberryPI へのデータは UDP で送信することができません。そこで、逆に RaspberryPI からサーバーにソケット接続して UDP -> TCP -> UDP とブリッジする仕組みを導入します。
WebServer: 音楽的なデータを生成する
UDP-TCP: OSC を UDP で受けて TCP で送信する (接続先)
TCP-UDP: OSC を TCP で受けて UDP で送信する (接続元)
DMXMessageGenerator: サーバーからのデータを受けてDMXを制御する
この機能は最初のプログラムに直接組み込むのではなくて、新たにプログラムを用意して別プロセスとして実行します。こうすることで例えばサーバー側のアルゴリズムを更新してプロセスの再起動が必要な場合でも、受信側への影響を考慮する必要がなくなりますし、開発中で TCP のブリッジが必要ないときはショートカットできるので開発や運用の面で柔軟性が増します。
const net = require("net");
const dgram = require("dgram");
let sockets = [];
// UDP で受信したら..
let receiver = dgram.createSocket("udp4");
receiver.on("message", (buffer) => {
sockets.forEach((socket) => {
// そのまま TCP で転送する
socket.write(buffer);
});
});
receiver.bind(SEND_PORT);
// クライアントからの接続を管理する
let server = net.createServer();
server.on("connection", (socket) => {
sockets.push(socket);
socket.on("close", () => {
let index = sockets.indexOf(socket);
if (index !== -1) {
sockets.splice(index, 1);
}
});
});
server.listen(SERVER_PORT);
const net = require("net");
const dgram = require("dgram");
// サーバーに接続する
let client = net.connect({ host: SERVER_ADDR, port: SERVER_PORT });
// サーバーからデータが来たら..
client.on("data", (buffer) => {
let sender = dgram.createSocket("udp4");
// そのまま UDP で転送する
sender.send(buffer, 0, buffer.length, RECV_PORT, "127.0.0.1", () => {
sender.close();
});
});
稼働時間を制御する
作品は町なかに設置するため一日中ずっと明滅し音を鳴らし続けるわけにはいかず、稼働時間に制限があります。稼働時間は何時から何時までと固定されていれば良いのですが、実際には「今日は少し早めに動かし始めたい」「お客さんが多いので少し遅くまで動かしたい」「テスト期間中はフルタイムで稼働させたい」などの要求に合わせてフレキシブルに設定できる必要があるため、時間に応じてメッセージをフィルタリングするプログラムを用意します。
Application: 音楽的なデータを生成する
MessageGatekeeper: 設定された時間だけメッセージを通過させる
上の図では RaspberryPI 側とブラウザ側の2箇所にフィルタリング機能がありますが、実際は1つのプログラムで設定ファイルで指定された時間に限りX番のポートで受信したデータはY番のポートへ転送するという機能を提供します。こうすることでアプリケーション自体は作品の稼働時間を気にする必要がなくなりました。
const dgram = require("dgram");
let receiver = dgram.createSocket("udp4");
// データを受信したとき
receiver.on("message", (buffer) => {
// 設定時間であれば..
if (isPassable(Date.now())) {
let sender = dgram.createSocket("udp4");
// そのまま転送する
sender.send(buffer, 0, buffer.length, SEND_PORT, "127.0.0.1", () => {
sender.close();
});
}
});
receiver.bind(RECV_PORT);
テストをする
基本的には単体テストをしっかりします。ただし、時間で値が変化するようなものはテストしにくく、特に表現に関する部分だと動かして確認したいことが多いです。そういう場合は単純に受けたデータをプリントデバッグするプログラムを用意したり、せっかく OSC を使っているので Max/MSP を使って UI 込みでパラメーターを操作してメッセージを送信して、処理の結果を可視化できる仕組みを簡単に作るのが有効です。
const dgram = require("dgram");
const OscMessage = require("osc-msg");
let receiver = dgram.createSocket("udp4");
// OSC をプリントデバッグする
receiver.on("message", (buffer) => {
let message = OscMessage.fromBuffer(buffer);
let values = message.args.map(x => x.value).join(", ");
console.log(values);
});
receiver.bind(RECV_PORT);
最後に
上記のように、小さい機能ごとに別プログラムを用意する方法はテストやアップデートがしやすく、システムの全体像が見えない状態での開発にとても有効でした。
今回のインスタレーションでは、上記の他にも「複数のメッセージをまとめて送信して受信側で展開する機能」や「DMXの制御信号を良い感じに調整する機能」などを同じような方針で拡張しました。作品の設置場所が4ヶ所 (システム構成的には5ヶ所) あったり、作品を利用したライブのイベントがあったりして実際にはもう少し複雑でしたが、機能の分割がよく作用して制作の追い込みでごちゃごちゃしていく中でも的確に対処することができたので良かったと思います。(ところで、こういう創作系のやつって準備時間がたくさんあっても最終的に全然間に合わなくなるみたいなの何でなんですか?)