この記事はHoudiniアドベントカレンダー2025の9日目の記事です。
はじめに
普段3Dの形状を扱う自分にとって、3D空間の中でおきている「動き」とか「状態」をそのまま音にフィードバックしたいなという欲求がありました。頭の中でおぼろげにこうやったらできるのかなというイメージのままでいたのですが、最近XでKatsuhiro Chibaさんの下記の投稿を拝見し、「あ、これをやってみたい!」と火がつきました。
Advent Calendarのテーマとしても面白い内容かもということで、今回はHoudiniを物理演算ベースの音楽シーケンサーにするという試みを行ってみたいと思います。
具体的には、HoudiniのSOP Solverでボールが壁に跳ね返るシミュレーションを作り、その衝突のタイミングや強さを音に変換してみます。イメージとしては、Teenage Engineeringの「OP-1」というシンセサイザーに搭載されている 「Tombola(トンボラ)」 というシーケンサーの3D版です。多角形の枠の中でボールが跳ね回って、まるで福引のガラガラ抽選機のような、予測不能なリズムとメロディを奏でる仕組みを3次元で再現したら楽しいかもと思って始めました。
本来Houdiniはリアルタイムパフォーマンス用のツールではないのですが、SOPで細かいロジックが作れる強みはどのツールにも負けないと思っているので、そこを最大限活かせたら良いだろうなと思いました。
ただ、Houdini単体でリッチな音を出すのは難しいので、音源部分はAbleton Liveに任せることとします。音に関わる通信は、通常はMIDIを使うのだと思いますが、macOSのHoudiniだとリアルタイムのMIDI受信・送信がうまくいかなかったので(MIDIファイルは使えた)、今回はOSC(Open Sound Control)というプロトコルを使ってHoudiniからAbleton Liveへ衝突情報を送るワークフローを構築していきます。
記事の最後にプロジェクトファイルも用意していますので、この記事を見ていただいて興味を持っていただいた方は自分の環境でも試してみてください。
推奨環境
- Houdini 21 Indie(Apprenticeでもたぶんできる)
- Ableton Live 12 Suite (OSCが扱えれば別DAWでも可)
- Scaler 3(適当に打ったキーを音楽理論に沿って整えてくれるVSTプラグイン)
- 音源(DAW付属のもので大丈夫。自分はChromaphone 3というモーダルシンセを使った)
ワークフロー
- [Houdini] ボールのバウンスシミュレーションをSOP Solverで作る
- [Houdini] シミュレーション結果を可視化する
- [Houdini] バウンス時の情報をOSCで送信する
- [Ableton Live] Max for Liveを使いOSCをMIDIに変換する
- [Ableton Live] Scaler 3を使いMIDIノートを整える
- [Ableton Live] MIDIを音源を使って音を鳴らす
Houdiniではボールのバウンスシミュレーションをまず作り、そのシミュレーション結果を可視化しつつ、ボールがくるくるまわるガラガラ抽選機のような容れ物の壁に当たった時、その時のメロディ情報や強さをOSCでAbleton Liveに送ります。
Ableton Live側では、Max for Live (M4L) を使ってOSCデータをMIDIに変換します。ポイントは、その後にScaler 3というプラグインを通すことです。これによって、物理演算から生まれるランダムなノート情報を「音楽理論的に正しいコード進行」に整え、不協和音を防ぐことができます。
最終的な音源には、物理的な衝突のイメージに合わせて、物理モデリングシンセ(モーダルシンセ)の一つであるChromaphone 3を使用しました。でもピアノのような音源を使っても不協和音にならないので心地よく聴こえます。
最終的に完成したのが、次の動画のようなものです。
プロジェクトファイル
Houdiniパート
今回シミュレーションのコントロールに使いたいパラメータは、CONTROLLERという名前のNullノードに次のようにまとめています。
| パラメータ名 | 型 | 値 | 範囲 | 説明 |
|---|---|---|---|---|
| max_pt | int | 20 | 1-50 | ボールの総数 |
| rt_pt_num | float | 0.2 | 0.0-1.0 | シミュレーションに使うボール数割合 |
| base_note | int | 60 | 0-100 | ベースのノート番号 |
| note_range | int | 30 | 0-100 | ランダムに選ばれるノートの範囲 |
| friction | float | 1.0 | 0.0-1.0 | 空気抵抗率(1.0がなし) |
| bounce | float | 0.99 | 0.0-1.0 | バウンス時の速度減衰率 |
| min_speed_thresh | float | 5.0 | 0.0-10.0 | 低速判定のしきい値 |
| min_speed_bounce_impact | float | 30.0 | 0.0-100.0 | 低速になったときに与えるインパクトの強さ |
| volume | int | 90 | 0-127 | 音量 |
| rot_speed | float | 0.5 | 0.0-10.0 | ボールの容れ物の回転速度 |
| rad | float | 1.0 | 0.0-5.0 | ボールの半径 |
大まかな流れとして、まずはSOPソルバーを使ってボールのバウンスシミュレーションを作り(Step 1)、そのシミュレーションを可視化し(Step 2)、最後にPythonを使ってOSCでバウンス(反射)時の情報を送信します(Step 3)。
Step 1. [Houdini] バウンスシミュレーションを作る
Step 1-1. ボールがバウンスする容れ物を作る
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| Circle | -* | Orientation | YZ Plane | YZ平面に多角形を作る |
| Uniform Scale | 20 | ボールを容れる容れ物の厚み | ||
| Divisions | 7 | 七角形にする | ||
| PolyExtrude | - | Distance | 20 | 容れ物の厚み |
| Output Side | ☑ | 側面をグループに入れる | ||
| PolyFill | - | Fill Mode | Single Polygon | 側面を作る |
| Primitive Wrangle | faceid | VEXpression | コード#1 |
面に番号をつける |
| Match Size | - | - | - | 形状の中心を原点位置に移動する |
| Null | CONTAINER | - | - | - |
*「-」はデフォルトの値の意味
まず厚みをもった七角形の容れ物を作ります。 多角形にしている理由は、ボールが当たった側面に応じて音色を変化させたいからです。七角形にすることで、最大7種類のコード(和音)のバリエーションが出せるようにします。
そのための初期設定として、各面に番号をふっておきます。
コード#1
i@faceid = @primnum;
Step 1-2. ボールの位置を示す点群と容れ物の回転アニメーションを作る。
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| IsoOffset | - | Offset | -ch("../CONTROLLER/rad") |
ボリュームをオフセットする |
| Uniform Sampling Divs | 50 | ボリュームの解像度を設定する | ||
| Scatter | - | Force Total Count | ch("../CONTROLLER/max_pt") |
ボールの最大数を設定する |
| Point Wrangle | init | VEXpression | コード#2 |
ボールの初期設定を行う |
| Point Relax | - | Max Iterations | 100 | ポイントリラックスの繰り返し回数 |
| Point Radius Scale | ch("../CONTROLLER/rad") |
ポイント間の最小距離を設定 | ||
| Transform | transform1 | Rotate X | $FF * ch("../CONTROLLER/rot_speed") |
容れ物の回転アニメーションを作る |
| Null | ROTATING_CONTAINER | - | - | - |
容れ物ができたら、その中に初期のボールをScatterノードで配置します。 点同士が近すぎると、シミュレーション開始時に衝突判定が暴発して吹き飛んでしまうため、Point Relaxノードを使って位置を調整しておきます。
各ポイントにシミュレーション用の初期設定もしておきましょう。
コード#2
v@v = (rand(@P) - set(0.5, 0.5, 0.5)) * 5; // ボールの初期速度を設定
f@melody = 0.0; // メロディの値を初期化
i@toneindex = -1; // コード種類のインデックスを初期化
i@prevtoneindex = -1; // 前フレームのコード種類のインデックスを初期化
またTransformノードで容れ物を回転させます。
Step 1-3. Solverを使ってバウンスシミュレーションを作る
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| Solver | - | - | - |
サブネットワーク#1 参照 |
| Delete | - | Group | using | 適用グループを指定 |
| Operation | Delete Non-Selected | 指定のポイントだけ抽出 | ||
| Entity | Points | ポイントタイプを指定 | ||
| Null | SIM_POINTS | - | - | - |
Solverノード内でバウンスのシミュレーションを行います。 シミュレーション後、使われなかったポイントはDeleteノードで取り除いておきます。
サブネットワーク#1
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| Point Wrangle | using | VEXpression | コード#3 |
シミュレーションに用いるポイントを指定する |
| ptn | ch("../../../../CONTROLLER/rt_pt_num") |
シミュレーションに用いるポイント数の割合 | ||
| Normal | - | Add Normals to | Primitives | 面の法線アトリビュートを作る |
| Point Wrangle | bounce | Group | using | シミュレーションに使うポイントのフィルタリング |
| VEXpression | コード#4 |
バウンスのシミュレーションを行う | ||
| bounce | ch("../../../../CONTROLLER/bounce") |
反射係数 | ||
| bouncemult | ch("../../../../CONTROLLER/min_speed_bounce_impact") |
最小速度以下の時に追加する反射時の速度 | ||
| rad | ch("../../../../sphere1/scale") |
ボール半径 | ||
| friction | ch("../../../../CONTROLLER/friction") |
空気抵抗係数 | ||
| min_speed | ch("../../../../CONTROLLER/min_speed_thresh") |
最初速度閾値 | ||
| Null | SIM_POINTS | - | - | - |
ソルバーネットワークの中に入り、まず1つ目のPoint Wrangleノードでシミュレーションに使うポイントの数をパラメータで設定し、usingグループに入れます。これでリアルタイムにシミュレーションするボールの数を調整できるようになります。
コード#3
float ptn = chf("ptn"); // パラメータから閾値となる数値を取得
if(@ptnum <= ptn * npoints(0)){ // 全ポイント数に対して割合をかけ、その範囲内か判定
setpointgroup(0, "using", @ptnum, 1); // 範囲内なら "using" グループに追加
}else{
setpointgroup(0, "using", @ptnum, 0); // 範囲外なら "using" グループから外す
}
次にバウンスシミュレーションのメインとなるアルゴリズムをもう一つのPoint Wrangleノードに書きます。 今回はシンプルなバウンス処理をVEXで実装しています。処理の流れは以下の通りです。
-
重力の適用: まず重力加速度分だけ速度を落とします。(
v = v - g * t) -
壁(容れ物)情報の取得:
xyzdist()を使い、一番近い壁の距離・位置・法線を取得します。 -
他ボール情報の取得:
nearpoints()を使い、近くにある別のボールとの距離などを取得します。 - 衝突(めり込み)判定: 距離と方向から「現在めり込んでいるか」または「これから衝突しそうか」を判定します。
-
位置の補正: 衝突していた場合、球の中心をサーフェスの外側に押し出してめり込みを解消します。(
P = mpos - n * r) -
反射と減衰: 壁や相手の法線を使って速度ベクトルを反射させ、反発係数で勢いを減衰させます。(
v = v * bounce) - 低速時の微調整: 速度が遅くなりすぎた時は、法線方向にランダムなインパルスを足して少し弾かせ、動きが止まらないようにします。
-
アトリビュート更新: 接触フラグ(
contact)や、音階選択用のインデックスなどを更新します。 -
グラデーション減衰: 接触していないフレームでは、発光表現用のグラデーション値(
grad)を徐々に減らします。 -
摩擦と位置更新: 空気抵抗(摩擦)で速度を少し落とし、最終的な速度から今のフレームの位置を決定します。(
P = P + v * t) -
後処理: 可視化やOSC送信のために、速度の大きさを0〜1に正規化して
speedに、法線を@Nとして保存します。
コード#4
float rad = chf("rad"); // 衝突半径
float gravity = 9.8; // 重力加速度
float bounce = chf("bounce"); // 反発係数
float friction = chf("friction"); // 摩擦係数
v@v.y -= gravity * @TimeInc; // 重力でY方向の速度を減少させる
// 入力1の形状(ボールの容れ物)との衝突判定のための情報取得
int prim;
vector uv;
float d = xyzdist(1, @P, prim, uv);
vector norm = primuv(1, "N", prim, uv);
vector mpos = primuv(1, "P", prim, uv);
vector dir = normalize(@P - mpos);
// 他のボールとの衝突判定情報を取得
int npts[] = nearpoints(0, "using", @P, rad * 2, 2);
if(len(npts) == 2){ // 自分以外の近傍ポイントが見つかった場合
int npt = npts[1]; // もう一方のポイント番号を取得
vector npos = point(0, "P", npt);
d = (distance(@P, npos) - rad); // 2点間距離から半径分を引いた値を距離として使用
mpos = npos + normalize(@P - npos) * rad; // 半径分だけ離れた位置を接触位置として再計算
norm = normalize(npos - @P); // 法線を2点の方向から再計算
dir = normalize(@P - mpos); // 方向ベクトルを更新
}
// 衝突条件
if(d < rad - 0.1 || (dot(dir, norm) > 0.0 && d > 0.1)){
v@P = mpos - norm * rad; // 半径分だけ外側の位置に補正
v@v = reflect(v@v, norm); // 法線で速度ベクトルを反射
v@v *= bounce; // 反発係数をかける
// 最低速度より遅い場合は速度を上げる
if(length(v@v) < chf("min_speed")){
v@v += normalize(-norm) * fit01(rand(@P), 2.0, chf("bouncemult"));
}
i@contact = 1; // 接触フラグON
f@grad = 1.0; // グラデーション用の値をリセット
f@melody = rand(@P); // 位置ベースでメロディ値を更新
i@contactprim = prim; // 接触した面の番号を記録
v@contactuv = uv; // 接触した面上のUVを記録
if(inprimgroup(1, "extrudeSide", prim) == 1){ // 容れ物の側面かどうかチェック
int faceid = prim(1, "faceid", prim);
i@prevtoneindex = i@toneindex; // 以前のコード種類のインデックスを保存
i@toneindex = faceid; // 現在のコード種類のインデックスを面の番号で更新
}
}else{
i@contact = 0; // 接触フラグOFF
f@grad -= 0.05; // グラデーション値を減衰
f@grad = max(f@grad, 0.0); // 0未満にならないよう制限
i@prevtoneindex = i@toneindex; // 以前のコード種類のインデックスを更新
}
v@v *= friction; // 速度に空気の摩擦を適用
v@P += v@v * @TimeInc; // 速度から位置を更新
f@speed = fit(length(v@v), 0, 20.0, 0.0, 1.0); // 速度情報を正規化する
@N = norm; // 法線をポイント法線として設定
これにより、ボールが壁や他のボールに接触したとき、反射するようになります。今回は衝突時の情報をOSCで送りたいので、メロディの値、衝突した面の番号、速度などをポイントアトリビュートとして格納しておくのがポイントです。
Step 2. [Houdini] シミュレーション結果を可視化する
Step 2-1. ボールを可視化する
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| Sphere | - | Uniform Scale | ch("../CONTROLLER/rad") |
ボールの半径を設定 |
| Frequency | 3 | ボールの解像度を設定 | ||
| Pack | - | - | - | 描画軽量化のためパックする |
| Point Wrangle | color | VEXpression | コード#5 |
容れ物との衝突情報に応じて色をつける |
| Copy to Points | - | - | - | ボールをポイントにコピーする |
| Material | - | Material | /mat/principledshader1 |
ボールにマテリアルを設定する |
次に、結果をビジュアルとして表現します。 ボールが側面に衝突してからの時間(gradアトリビュート)に応じて、ボールが発光するように色をつけます。これによって、ボールが衝突した瞬間ぱっと光り、徐々に暗くなるような表現になります。
コード#5
vector col1 = set(0,0,0); // 黒色(開始色)
vector col2 = hsvtorgb(i@toneindex / 7.0, 1.0, 1.0); // toneindex から色相を計算してRGBに変換
v@Cd = lerp(col1, col2, f@grad); // grad 値に応じて黒→色へ補間
f@speed = length(v@v); // 速度ベクトルの長さを speed として保存
Step 2-2. 容れ物を可視化する
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| Object Merge | CONTAINER1 | Object 1 | ../CONTAINER |
容れ物の形状を取得する |
| Unique Points | - | - | - | 面を分離する |
| Delete | - | Group | extrudeSide | 側面グループを指定する |
| Operation | Delete Non-Selected | 指定したグループ以外を削除する | ||
| Transform | transform2 | Rotate Y | ch("../transform1/ry") |
容れ物形状を回転する |
| Object Merge | SIM_POINTS1 | Object 1 | ../SIM_POINTS |
シミュレーション結果を取得する |
| Sort | - | Point Sort | By Attribute | アトリビュートに応じてポイントを並び替える |
| Attribute | grad | グラデーションアトリビュートに応じて並び替える | ||
| Primitive Wrangle | alpha_col | VEXpression | コード#6 |
衝突情報に応じて面に色をつける |
| Attribute Promote | - | Original Name | Cd | 色のアトリビュートをプロモートする |
| Original Class | Primitive | プリミティブのアトリビュートをポイントにプロモートする | ||
| Convert Line | - | - | - | 面を曲線に変換する |
| Transform | transform3 | Rotate Y | ch("../transform1/ry") |
曲線を回転する |
| Merge | - | - | - | 回転するコンタクト面と曲線をまとめる |
| Material | - | Material | /mat/principledshader2 |
透明度のあるマテリアルを設定する |
次に容れ物のアウトラインと、衝突した面だけを可視化します。 こちらもボール同様、gradアトリビュートを使って、ボールの当たった面がぽわんと光るような表現にします。
コード#6
int pt = findattribval(1, "point", "toneindex", i@faceid, 0); // 入力1から、toneindex が faceid と一致するポイントを検索
vector col1 = set(0,0,0);
vector col2 = hsvtorgb(i@faceid / 7.0, 1.0, 1.0); // faceid を色相としてRGBに変換
float grad = 0; // 初期のグラデーション値
// 対応するポイントが見つかった場合グラデーションの値を取得
if(pt >= 0){
grad = point(1, "grad", pt);
}
v@Cd = lerp(col1, col2, grad); // grad に応じて色を補間
Step 2-3. 衝突エフェクトを作る
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| Circle | - | Divisions | 12 | 円の解像度を設定 |
| Arc Type | Open Arc | 円を曲線にする | ||
| Pack | - | - | - | 描画軽量化のためパックする |
| Object Merge | SIM_POINTS2 | Object 1 | ../SIM_POINTS |
シミュレーションの結果を取得する |
| Object Merge | ROTATING_CONTAINER2 | Object 1 | ../ROTATING_CONTAINER |
回転する容れ物を取得する |
| Point Wrangle | contact_circle | VEXpression | コード#7 |
容れ物に衝突した位置と法線を設定 |
| maxscale | ch("../CONTROLLER/rad")*2 |
衝突エフェクトの最大サイズの設定 | ||
| Color | - | - | - | 色をデフォルトの白に設定 |
| Copy to Points | - | - | - | 円の曲線を衝突の位置にコピー |
| Material | - | Material | /mat/principledshader1 |
マテリアル設定する |
衝突の瞬間に広がる波紋のようなエフェクトを作ります。 gradアトリビュートに応じて円のサイズを広げつつ、法線方向に配置します。
コード#7
v@P = v@contactpos; // 接触位置からポイントを配置
f@pscale = lerp(0.0, fit01(pow(f@speed,2), 1.0, chf("maxscale")), pow(f@grad, 2)); // speed と grad に基づいて pscale を計算
vector cpos = primuv(1, "P", i@contactprim, v@contactuv); // 接触していた面上の位置を取得
vector cnorm = primuv(1, "N", i@contactprim, v@contactuv); // 接触していた面の法線を取得
@P = cpos - cnorm * 0.05; // 法線方向に少し押し出して配置
@N = cnorm; // 法線をポイントの法線として設定
// pscale が極小のときポイントを削除
if(f@pscale < 0.0001){
removepoint(0, @ptnum);
}
Step 3. [Houdini] バウンス情報をOSCで送信する
Step 3-1. Pythonを使いOSCでバウンス情報を送信する
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| Object Merge | SIM_POINTS3 | Object 1 | ../SIM_POINTS |
シミュレーションの結果を取得 |
| Delete | - | Group | @contact==1 |
衝突したポイントを指定 |
| Operation | Delete Non-Selected | 指定したポイントだけ残す | ||
| Entity | Points | ポイントに設定 | ||
| Python | osc | Python Code | コード#8 |
OSCで衝突したポイントの情報を送信 |
このステップが今回のキモで、バウンスシミュレーションの衝突情報を、Pythonを使ってOSCで送信します。 外部の便利なライブラリは使わず、Houdini標準のPythonモジュールだけで実装しています。
[送信したいデータ]
- コード変更のトリガー: ボールが容れ物の新しい面に当たった瞬間、その面に応じたコード(和音)に変更する信号を送ります(黒鍵盤のMIDIノートとして送信)。
- ノート情報: ボールが持っている「メロディ値」と、衝突の「強さ(速度)」を送ります(白鍵盤のMIDIノートとして送信)。
このデータを受け取ったAbleton Live側で、Scaler 3が「指定されたコード」の中で「一番近いキー」を選んで鳴らしてくれる、という仕組みです。
コード#8
node = hou.pwd()
geo = node.geometry()
import socket
import struct
import math
import random
# --- 設定 ---
# OSC送信用のIP・ポート・アドレスなど基本設定
IP = "127.0.0.1"
PORT = 9000
ADDRESS = "/h_signal"
# --- 送信 ---
# Houdiniジオメトリのポイント取得、使用するスケール(音階)・トーンの定義
pts = geo.points()
scale_intervals = [0, 2, 4, 5, 7, 9, 11]
tones = [49, 51, 54, 56, 58, 61, 63]
# --- OSCメッセージの構築 (バイナリパック) ---
# OSCの仕様に合わせ、アドレス文字列と型タグを4バイト境界に揃えるパディングを準備
addr_bytes = ADDRESS.encode('utf-8')
addr_pad = b'\x00' * (4 - (len(addr_bytes) % 4))
# 型タグ(float 2つ)も同様にOSC形式にパディングして準備
type_tag = b',ff'
type_pad = b'\x00' * (4 - (len(type_tag) % 4))
# --- 各ポイントの情報を読み取り、OSCとして送信 ---
# 各ポイントごとに、接触判定やトーン変化を条件にメッセージを送信
for pt in pts:
num = pt.number()
melody = pt.attribValue("melody")
contact = pt.attribValue("contact")
prev_tone_index = pt.attribValue("prevtoneindex")
tone_index = pt.attribValue("toneindex")
speed = math.floor(pt.attribValue("speed") * node.parm("volume").eval())
# 接触した瞬間にトーンメッセージを送る(トリガー)
if contact == 1:
if prev_tone_index != tone_index and tone_index < len(tones):
tone = tones[tone_index]
c_val_bytes = struct.pack('>ff', tone, 1)
c_message = addr_bytes + addr_pad + type_tag + type_pad + c_val_bytes
# UDP送信(失敗しても落ちないように例外処理)
c_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
c_sock.sendto(c_message, (IP, PORT))
except:
pass
finally:
c_sock.close()
# メロディ値から音階を計算し、MIDIノート番号を決定
root_note = node.parm("base_note").eval()
index = root_note + math.floor(melody * node.parm("note_range").eval())
octave = index // 12
note_idx = index % len(scale_intervals)
midi_value = (octave * 12) + scale_intervals[note_idx]
# 音階と速度をOSCメッセージにパックして送信
val_bytes = struct.pack('>ff', midi_value, speed)
message = addr_bytes + addr_pad + type_tag + type_pad + val_bytes
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(message, (IP, PORT))
except:
pass
finally:
sock.close()
Step 3-2. すべての可視化メッシュとPythonノードを1つにまとめる
| ノード | ラベル | パラメータ名 | 値 | 説明 |
|---|---|---|---|---|
| Merge | - | - | - | すべての可視化した形状とPythonノードをまとめる |
| Null | FINAL | - | - | - |
最後に、可視化したモデルとOSC送信をするPythonノードをMergeノードでまとめます。 フレーム数を多めに設定して再生し、シミュレーションが動き、Pythonノードがエラーを吐いていなければHoudini側の準備は完了です。
Ableton Liveパート
Step 4. [Ableton Live + M4L] OSCデータをMIDIデータに変換する
次は受け取り側です。Ableton Live Suiteを使って新しくプロジェクトを作りまずはMax for Live(Ableton Live Suiteに付属)を使って、シンプルな受信デバイスを作ります。テンプレートとして Max for Live > Max MIDI Effect をトラック1に追加して編集します。
パッチはスクリーンショットのように組みます。 Houdiniと同じ9000番ポートから受け取ったOSCメッセージを、MIDIのノート情報に変換して出力します。
この時点で適当な音源(E-Pianoなど)をトラックに挿してHoudiniを再生してみましょう。音が鳴れば通信成功です。
Step 5. [Ableton Live + Scaler 3] MIDIデータのノートを整える
現状、通信が成功しても、このままではボールがぶつかるたびにランダムなキーが鳴るため、ただの不協和音(ノイズ)になってしまっているはずです。
ここで今回の影の主役であるVSTプラグインScaler 3を使います。Scaler 3は音楽理論に則ったコード進行をある程度自動で作れたりする、音楽素養のないような自分にとってはとってもありがたいツールです。このツールを使うことで、物理シミュレーションが生み出すカオスなタイミングはそのまま活かしつつ、音程だけは「音楽的に正しいルール」に強制的に従わせることができるようになります。
もちろんHoudini側で完璧な音楽理論を構築することも可能ですが、今回は専用のプラグインに任せることで、「適当にシミュレーションを回しても、なぜか心地よい音楽になる」という魔法のような状態を簡単に作ります(シミュレーションは愚直に1から書くくせに)。
まずは先に追加したMIDI楽器を削除して、Max for Liveのデバイスの右に Plug-ins > Scaler Music > Scaler 3 をドラッグして追加します。
まずはScaler 3をリアルタイムなツールとして使うための設定を行います。設定画面から、Sync to DawをOffにしておきます。
次に、多角形の辺の数(今回は7つ)分のコードを登録します。これらが、ボールが壁に当たった時に切り替わるコードとなります。
その上でロックボタンをクリックして、特定の黒鍵盤(Houdiniから送られてくるtones)でコードが切り替わるように設定します。
そしてKey LocksオプションをオンにしてChord Notesに設定します。
この設定により、どんなキーが入力されても、「現在鳴っているコードの構成音」に強制的に補正されます。これにより、ボールがどれだけランダムに跳ね回っても、絶対に音が濁らないシステムが完成します。
Step 6. [Ableton Live + 音源] MIDIの音を鳴らす
最後のステップとして、Scaler 3で整えられたMIDI信号を、別のトラックに送って好きなシンセで鳴らします。
トラック2を作成し、MIDI Fromの設定でトラック1のScaler 3からの出力を受け取るようにします。
音源には、ガラス玉のような音を出したかったので、モーダルシンセのChromaphone 3のGlassyというプリセットを使ってみました。ここは各々好きな音源を使ってください。
これで完成です。Houdiniのタイムラインを再生すると、ボールの衝突に合わせてアンビエントな音楽が生成されるはずです。
プロジェクトファイルの保管場所
gravity-sequencer-houdini-abletonlive
おわりに
とりあえず作ってみたかったものはできましたが、やっているうちに色々な欲が出てきました。
例えばChromaphone 3の物理パラメータもHoudiniの値でコントロールしてみたいとか、2Dのシーケンサーで個人的に好きなOrcaの3D版をHoudiniでできないかなとか、HoudiniにDAWからの情報をフィードバックしてシミュレーションに影響を与えてみるとか...(Synplant 2という遺伝的アルゴリズムを使ったシンセサイザーと組み合わせても面白そう)。
あと、そもそもHoudiniじゃなくて良くない!?とは自分でも思います。元々がリアルタイムに適したソフトウェアではないので、UnityやTouchDesignerでやったほうがビジュアルはより派手なものが作れる気はします。
でも、SOPで組んだロジックがそのまま音になるという実験はぜひやってみたかったというのがあり、形にはなってよかったです。Houdiniはやっぱり楽しい...
Orcaのシーケンサーはずっとみていられる。
変なシンセ























