Processing => ChucK
ChucKにはネットワーク系の機能が実装されているため、ChucK単体で使うだけでなく他のアプリケーションと連携させることもできる。今回はOSCという通信プロトコルを使って、ChucKとProcessingを連携させる。
OSC(OpenSound Control)とは、音響データをネットワークで転送するための通信プロトコル。同種の通信プロトコルであるMIDIには、速度が遅い、帯域が狭い、データ表現の制約が強いなどの欠点があったため、ポストMIDIとしてOSCが考案された。OSCをサポートしているソフトウェアやハードウェアは多く、OSCを使うことでそれらを連携させることができる。
ChucKは標準でOSCをサポートしている。Processingは標準ではOSCをサポートしていないけど、oscP5というライブラリをインストールしてインポートすればOSCを使える。
OSCに関しては既にいくつか日本語の解説文があったおかげで、ほとんど英語を読まずに済んだ。OSC自体の説明は、表現のためのオープンソースソフトウェアというWikiにある記事OpenSound Controlが簡潔で分かりやすい。OSCを使ったChucKとProcessingの連携については、多摩美術大学で開講されたメディアアートの講義SSAW08の講義ノートに「OpenSoundControl 3 - ChucK入門、ChucKとProcessingの連携」というそのままの内容がある。素晴らしい!
習作として、ライフゲームを可聴化してみた。Processing側は、ライフゲームの実行と画面描画に加えて、一世代ごとに空間の状態をChucKへ送信する。空間の状態とは、生きているセルの数と分布から算出した値。生きているセルの分布は、中央からどの向きにどれだけ偏って生きているセルが存在するかを表す。ChucK側は、受信した値を基に音響合成(二つのサイン波をリング変調)して音を鳴らす。空間の状態と音響合成との対応は以下。
- 生きているセルの数 => 音量
- 生きているセルの横方向の分布 => 片方のサイン波の周波数
- 生きているセルの縦方向の分布 => もう一方のサイン波の周波数
分布と周波数の関係ついて下の図の例で大雑把に説明すると、分布としては横方向にはマイナスの向きに偏っていて、縦方向にはプラスの向きに偏っているので、ChucK側では低い周波数のサイン波と高い周波数のサイン波がリング変調される。
Processingのプログラム。
import oscP5.*; import netP5.*; OscP5 osc; NetAddress addr; int port = 12000; int fps = 5; int wWidth = 42; int wHeight = 42; int cellSize = 10; boolean[][][] world = new boolean[wWidth][wHeight][2]; int flip; boolean isRunning; boolean drawMode; int population; // 生きているセルの数 float meadianX; // 生きているセルの横方向の分布 float meadianY; // 生きているセルの縦方向の分布 void setup() { size((wWidth-2)*cellSize, (wHeight-2)*cellSize); background(0); noStroke(); smooth(); frameRate(fps); osc = new OscP5(this, port); addr = new NetAddress("127.0.0.1", port); isRunning = false; flip = 0; initRandom(0.5); } void initRandom(float density) { for (int i = 0; i < wWidth; i++) { for (int j = 0; j < wHeight; j++) { if (i == 0 || j == 0 || i == wWidth-1 || j == wHeight-1) { world[i][j][flip] = false; } else { world[i][j][flip] = random(1) < density; } } } } void draw() { background(0); ellipseMode(CORNER); population = 0; meadianX = meadianY = 0.0; for (int i = 1; i < wWidth-1; i++) { for (int j = 1; j < wHeight-1; j++) { if (isRunning ? updateAt(i, j) : world[i][j][flip]) { fill(255, 255, 0); ellipse((i-1)*cellSize, (j-1)*cellSize, cellSize, cellSize); } else if (!isRunning) { fill(100, 100, 100); ellipse((i-0.6)*cellSize, (j-0.6)*cellSize, cellSize/4, cellSize/4); } } } if (isRunning) { OscMessage msg = new OscMessage("/life"); msg.add( (float)population/((wWidth-2)*(wHeight-2)) ); msg.add( meadianX ); msg.add( meadianY ); osc.send(msg, addr); flip ^= 1; } } boolean updateAt(int i, int j) { int n = 0; if (world[i-1][j-1][flip]) n++; if (world[i ][j-1][flip]) n++; if (world[i+1][j-1][flip]) n++; if (world[i-1][j ][flip]) n++; if (world[i+1][j ][flip]) n++; if (world[i-1][j+1][flip]) n++; if (world[i ][j+1][flip]) n++; if (world[i+1][j+1][flip]) n++; if (n == 3 || (n == 2 && world[i][j][flip])) { population++; meadianX += (i - wWidth/2)/5.0; meadianY += (wHeight/2 - j)/5.0; return world[i][j][flip^1] = true; } else { return world[i][j][flip^1] = false; } } void keyReleased() { if (key == 's') isRunning ^= true; else if (key == 'r') initRandom(0.5); else if (key == 'c') initRandom(0); } void mousePressed() { if (isRunning) return; int i = mouseX/cellSize + 1; int j = mouseY/cellSize + 1; drawMode = world[i][j][flip] = !world[i][j][flip]; redraw(); } void mouseDragged() { if (isRunning) return; int i = mouseX/cellSize + 1; int j = mouseY/cellSize + 1; world[i][j][flip] = drawMode; redraw(); }
ChucKのプログラム。
SinOsc c => ADSR e => JCRev r => dac; SinOsc m => e; 3 => e.op; e.set(10::ms, 90::ms, 0, ms); .05 => r.mix; OscRecv recv; 12000 => recv.port; recv.listen(); recv.event("/life, f f f") @=> OscEvent oe; float gain, mfreq, cfreq; while(true) { oe => now; while (oe.nextMsg() != 0) { oe.getFloat() => gain; oe.getFloat() => cfreq; oe.getFloat() => mfreq; } Math.min(5 * gain, 1) => e.gain; cfreq + 440 => c.freq; mfreq + 660 => m.freq; 1 => e.keyOn; }
録画したもの。音ズレ…。12月6日追記:多少まともな映像に差し替え。