Random Ripple Generator
概要
本稿では、TDを使った「無限さざなみジェネレーター」を提案し、実装する。オブジェクトプールと信号による駆動によって、従来の手作業のフレームアニメーションの非効率を解消し、効率的で自動化され、再利用できるさざなみの視覚効果の生成器を実現した。今後の拡張により、インタラクティブ性や芸術的な表現力をさらに高めていく。
Tag
音声の視覚化 / Touchesigner / AudioReactive / Ableton / MaxMsp / 平面MGアニメーション / 自動生成 / Python / さざなみ
はじめに
少し前、新曲『drown at 2am』のミュージックビデオ(PV、図1)を制作した。TouchDesigner(以下TD)で音声に連動するさざなみの拡散を実現し、ピアノの鍵盤が鳴るたびに、画面へ外に広がる円形の波を生成した。こうした視覚効果にはミュージックビデオでの先例があり、残像と組み合わせることで幻想的でしなやかな空気をつくり、より豊かな視覚体験を与えられる。

『drown at 2am』PV
PVを見る
図2は、波紋の効果だけを取り出して示したものだ。

従来の実装はAdobe After Effectsでの手作業のフレームアニメーションに頼ることが多く、速く密集したピアノのパッセージでは非常に非効率だ。一方、TDは高い拡張性と制御性で知られ1、面白い音声視覚化の効果を多く実現できるが、3Dの効果の制作に寄るようで2、平面アニメーションの例はまだ多くない。そこでTD 2023.11880を開発環境として3、自動的なさざなみジェネレータープラグインを設計し、実装してみた。
プラグインの全体構成
ジェネレーターは、トリガー信号→オブジェクトプール→さざなみオブジェクトという三層に分けられる。基本の流れは、外部信号のトリガーイベントを捉え、再利用できるさざなみ円環のオブジェクトをつくり、プールで寿命を管理し、パラメーターによって動的な拡大と消失を実現することだ。

全体構成を示す図
第1層:トリガー信号
- 音量を使ってトリガーする(TD内で実装)
- OSCプロトコルで、異なるプラットフォーム間にMIDI信号を送る
方法1:音量を使ってトリガーする(TD内で実装)
ピアノはおもしろい楽器で、音の立ち上がり、つまりトランジェントが強い6。弦楽器というより打楽器に近く、打鍵のたびに音量のピークが生じ、そこから音が小さくなっていく。
まずピアノのトラックから音声を取り出し、TDに読み込む。AnalyzeCHOPで一定区間の最大値、つまり短い時間内の音量の最大値を求める。そのうえで信号を滑らかにし、音量のピークを捉えやすくする。

打鍵のたびに、音量の信号がおおむね0.1を超えることが確認できる。
そこで、この信号から0.1を引く(図では感度を少し調整しているため、0.1025を引いている)。残った値が0より大きければ、その瞬間に打鍵があったと判断できる。
LogicCHOPで値が0を超えているかを判定すれば、使いやすいトリガー信号になる。

信号処理の流れ
この方法には、さらに工夫を加えられる。
-
音量の最大値だけでなく、傾きによってトリガーを判定することもできる。傾きが正なら音量は増加中であり、打鍵があったことを示す。
-
もちろん、ほかの方法も使える。たとえばTDに付属するAudioAnalysis 7なら、入力した音声をもっと細かく分解できる。
-
音声信号の周波数から音高を調べ、音が高いほど円を大きくするなど、自分で決めた操作につなげることもできる。
詳しい説明は、ここでは省く。

傾きを使ったトリガーの判定。
方法2:OSCで異なるプラットフォーム間にMIDI信号を送る
最も効率がよく、確実な信号源は、やはりMIDIだ。ただし、この方法には音声信号の知識や、場合によっては編曲の知識も関わってくる。ミュージシャンでなくても、信号を送る仕組みに詳しくなくても、MIDIファイルさえ用意できれば大丈夫だ。用意できない場合は、一つ目の方法をおすすめする。
OSCを使うと、プラグインを介してDAWとTouchDesignerを接続できる。たとえば、Abletonに組み込まれたビジュアルプログラミング言語Maxでは、すぐに使えるM4Lプラグインを利用できる。Connector KitsのOSC SendやOSC MIDI Send 9などは、そのまま信号を送ることができる。今回はMIDI信号を送るので、OSC MIDI Sendを選び、MIDI楽器のトラックに配置する。

Max for LiveのOSC MIDI Sendプラグインを使い、MIDI信号を送信する。
Portの初期値は2348だが、使われていないポートなら、別の番号でもかまわない。Windowsでは、コマンドプロンプトcmdでnetstat -anoを実行すると、使用中のポートを確認できる(通常2348は空いているが、念のため確認しよう)。
次に、TDでOsc In CHOPを使い、ポート番号を先ほどの設定と同じにする。受け取る信号を簡単に絞り込むと、準備は完了だ(ここでは音量だけを選んだが、音高を使って、さらにおもしろい操作もできる)。これでTDは、Max、つまりAbletonから確実なMIDI信号を受信できる!

TDでMIDI信号を受け取り、処理する。
第二層:さざなみオブジェクトの個体
トリガーができたので、つくりたい効果を分解してみよう。生成される小さなさざなみの振る舞いは、次のとおりだ。
- 形状は円環。
- 半径0から、一定の大きさまで広がる。
- 広がるにつれて、徐々に消える。
こう整理すれば、それぞれをどう実現するか、順に考えられる。
2.1 円環の形状
TDでは、CircleSOPを使って高さ0の3Dの円環をつくることができる。それを3Dレンダリングの環境に置き、カメラを配置して環境光を設定する方法だ。ただ、2Dアニメーションをつくるには、少し大がかりすぎる。
実際には、TDには2D用のCircleTOPが用意されている。fill(塗りの透明度)やborder(縁)などのパラメーターを調整するだけで、中が空いた円環をつくれる。

CircleTOPで中が空いた円環を作成する。
2.2 半径0から広げる
円を拡大するには半径を変える必要がある。そのため、時間とともに変化する信号を使う。
ここで使いやすいのが、TDのTimerCHOPだ。Timerをトリガーするたびに、設定した長さや速度に従ってカウントダウンする。

TimerCHOP

円環が非線形に広がるようにする。
円のパラメーターにこの値を参照させれば、非線形に広がる円ができる。

2.3 円環を消す
円環が消える過程にも、同じようにTimerCHOPを使える。ただ、現れた瞬間から消え始めるのでは困る。ある程度現れてから、徐々に消えてほしい。そこで、消失を制御するTimerに遅延を加えればよい。

消え始めるまでの遅延を設定する。
値にもう一つ処理を加え、1から時間の進行度を引く。この結果を、透明度の比率として使う。

TimerCHOPで処理した値を、透明度の比率に割り当てる。
円環を生成するときに、二つのtimerを同時にトリガーすれば、この章で目指した、円環が広がって消える効果が得られる。
2.4 カプセル化
これまでつくったものを少し調整し、すべて選択してCollapseで一つにまとめる。これで、最小限使えるさざなみオブジェクトができる。




さざなみ円環をオブジェクトにまとめる
base_circleと名づけ、Customize Componentで少し編集し、今後の拡張を容易にする。Radius、Bordersize、Centers、Centery、Opacity、Appeartimespan、DisappearStartPoint、AppearTimespanを公開する(円環の変数に対応させる手順は省略したが、先の参照とほぼ同じだ)。これで上の層からパラメーターを変え、独自のさざなみ円環を作りやすくなる。
パラメーターをドラッグして、さざなみ円環の各属性を操作してみてもよい。これでカプセル化は完了だ。
第3層:オブジェクトプールの仕組み
ここが、この仕組みの中で最も複雑な部分になる。Pythonのプログラミング知識も必要になるかもしれないが、使うコードはすべて掲載する。必要に応じてPythonのドキュメント11も参照してほしい。
3.1 オブジェクトプールが必要な理由
残念ながら、消失を担当するTimerが終了しても、さざなみ円環はキャンバスに残っている。透明度0で見えないだけで、メモリーとCPU資源を消費し、性能上の壁になる。トリガーのたびに新しい円環を生成すれば、資源はすぐ尽きる。こうした仕組みでは拡張して使えない。
解決のためにオブジェクトプールの考え方12を導入する。一定数のさざなみ円環を先につくり、使ったら返して繰り返し再利用する。
短い紙幅で詳しく説明はできないが、簡単な例を挙げよう。会社が50台のカメラを管理し、500人の社員が1日ずつ借りて返すとする。これなら全員が同時に使えるよう500台買わずとも、使いたいときにカメラがある状態を最大限確保できる。
この場面に置き換えると、プールはカメラの管理庫、台数はプールの容量、社員数は鍵盤を押す総回数、借りる行為はさざなみ円環を取り出して拡大から消失までのアニメーションを再生すること、利用時間は円環の出現から消失までの時間だ。小さなプールで管理と再利用を行い、限られた円環で無限の波紋を実現できる。
3.2 データ表と待機表の初期化
TDではTableでデータを保存する。ここではTableCHOPで、すべてのさざなみ円環オブジェクトのデータと、待機中のさざなみ円環をそれぞれ記録する(TDではパスや名前がノードの絶対アドレスなので、名前だけでよい)。二つのTableCHOPをInitTable、IdleTableと名づける。

TableCHOPでさざなみ円環オブジェクトのデータを記録する。
データを一つずつ手入力する必要はない。CHOP Executeに次のPythonコードを入力すれば、表を自動でつくれる。
def onOffToOn(channel, sampleIndex, val, prev):
# replicator の template 表をゲット
initTable = op('initTable')
# 古い表を削除
initTable.clear()
poolSize = 49
# デフォルトプールサイズを49とする
for i in range(poolSize):
initTable.appendRow([f"circle{i}"])
return
手前にButtonCOMPを置き、クリックでコードを実行する。これで二つの表は、49個のさざなみ円環オブジェクト名で自動的に埋まる。

表を使って、円環オブジェクトの名前を設定する。
3.3 さざなみ円環オブジェクトの初期化
先ほどの49行のデータでさざなみ円環を初期化できる。
TDのReplicatorCOMPは、選んだ表のデータを基に、その行数に対応する選択オブジェクトを生成する。ここではinit表とbase_circleをデータ源に選ぶ。

ReplicatorCOMPを設定する。
Ignore First Rowをオフにすることも忘れずに。operator prefix(接頭辞)には、先ほど決めたcircleを指定する。するとbase_circleが49個複製され、circle0~48という名前がつく。表にある名前と、一つずつ対応している。

49個のさざなみ円環を初期化する。
3.4 円環を重ねて出力する
用意した49個の円環を、出力する画面にすべて重ねる。そのためにCompositeTOPを使い、重ね方はAddを選ぶ。

CompositeTOPで、すべての円環を重ねて画面に出力する。
49個のcircleをすべて選択し、CompositeTOPにつなげればよい。

すべてのcircleをCompositeTOPにつなぐ。
コードを使えば、もっと手軽に自動化できる。先のデータ初期化と同じ方法で、すべてを走査してこのCompositeTOへ接続し、ボタンで実行できる(同じくCHOP Execute)。
def onOffToOn(channel, sampleIndex, val, prev):
poolSize = 49
# すべての円環をOutputへ繋ぐ
for i in range(poolSize):
op(f"circle{i}").outputConnectors[0].connect(op('comp').inputConnectors[i])
return
これですべてのさざなみ円環が自動的に重ねられ、出力される。この仕組みはシステムの負荷を減らし、高頻度のトリガーでも安定を保つ。残るは最後の一歩だ。
最後の手順:すべての部品をつないで統合する
4.1 プールから円環を取り出す
準備はすべてできたが、まだ効果は見えない。さざなみの呼び出しも、拡大・消失も、まだ起動されていないからだ。トリガー信号と、その後ろのプールをつなぐものが必要になる。
まず、これまでつくった部分のうち、トリガー信号以外をすべてまとめ、CirclePoolと名づける。

まとめた円環オブジェクトプール
次にCHOP Executeへ以下のコードを入力する。信号がオフからオンに変わったとき、円環の取り出し、位置の設定、タイマーの開始が自動で行われるようにする。
import random
import math
def onOffToOn(channel, sampleIndex, val, prev):
# 円環をランダムに生成する
pool = op('CirclePool')
x = round(random.uniform(-1, 1), 2) * 400
y = round(random.uniform(-1, 1), 2) * 200
r = round(random.uniform(0.4, 1), 2) * 180
# プールからから使用されていない円環を取得し、各プロパティにランダムな値を設定する
idle = pool.op('idle')
id = idle[0, 0].val
circle = pool.op(id)
circle.par.Centerx = x
circle.par.Centery = y
circle.par.Radius = r
# 円環のカウンターをスタートし,プールから出す
circle.op('disappear_timer').par.start.pulse()
circle.op('expand_timer').par.start.pulse()
idle.deleteRow(0)
return
これで円環は自動で現れ、適切なタイミングで消えるようになった。ただ、しばらくすると新しい円環が現れなくなる。プールから取り出した円環を、まだ戻していないためだ。
4.2 円環をプールに戻す
ここまで来れば、もう気づいた読者もいるだろう。円環ごとに、消失用タイマーが終了したら自動でidle表に戻すようにすれば、リソースを解放できる。
先のbase_circleに次の拡張をするだけで実現できる。まず、timerが動作中ならisBusy = 1とする処理を加える。

timerが終了したかを検出する制御信号を追加する。
CHOP Executeを追加し、信号が1から0に変わったとき、次のコードを動かす。
def onOnToOff(channel, sampleIndex, val, prev):
myId = me.par.owner.parent().name
pool = me.par.owner.parent(2)
pool.op('idle').appendRow([myId])
return
この段階でプール内の円環をすべて消去し、二つの初期化ボタンを押せば、求めた効果が得られる。鍵盤が鳴るたびに円環が現れ、密集したピアノのパッセージでも安定して動作し、重なりのあるさざなみ波紋をリアルタイムに描画できる。

最終効果。
今後の拡張
これまでつくったものをすべてまとめれば、いつでも使えるプラグインになる。今後このようなMG効果が必要になったら、プラグインを接続し、公開したパラメーターを調整するだけで、すぐ手軽にさざなみを実現できる。

プラグインとしてまとめる
TDは細かく制御でき、拡張性も高い。本稿の方法にも、さらに工夫を加えられる。今後の研究や応用のきっかけとして、いくつかの方向を挙げておこう。
効果の切り替え:円環のほか、多角形、粒子、フラクタル図形、3Dオブジェクトなどにも広げ、さまざまな音楽のスタイルに合わせる。
データの最適化:より効率的なデータ構造や並列計算を取り入れ、大規模なオブジェクトプールを使う場合の描画性能を高める。
複数のパラメーターのマッピング:音高、和声の密度、周波数分布などを、色、透明度、拡大速度などへ対応させ、音と視覚の結びつきを複雑にする。
芸術的なスタイルの多様化:グラデーション、歪み、粒子化など、パラメーターで制御する設計を増やし、拡張できる視覚表現のテンプレート集をつくる。
こちらに、まとめた.toeファイルと試聴用の音声を用意した。必要な方は自由に使ってほしい。ファイルは表示4.0国際ライセンスに従い、商用を含むどんなプラットフォームでも使用、共有、翻案、変更できる。基本的には自作だと主張する以外、好きに使ってかまわない。役立ったなら、入手の際に私のSNSをフォローしていただけるとうれしい!
この文章を通して、TDで音声を視覚化するおもしろさが伝わればうれしい。
著者について
千歲世野(YONOCO) YONOCO
VOCALOID / Hyperpop / Leftfield Bassを制作する、音声視覚化にも強い関心をもつオリジナルエレクトロニック・ミュージックプロヂューサー。
注
https://derivative.ca/feature/extensibility-and-customization
https://stevezafeiriou.com/touchdesigner-tutorial-for-beginners/
https://docs.derivative.ca/
https://forum.derivative.ca/t/midi-clock-settings-syncronization/6861/6
https://docs.derivative.ca/index.php?title=Beat_Dialog
https://www.audiolabs-erlangen.de/resources/MIR/FMP/C1/C1S3_Timbre.html
https://derivative.ca/UserGuide/Palette%3AaudioAnalysis
https://opensoundcontrol.stanford.edu/spec-1_0.html
https://www.ableton.com/en/packs/connection-kit/
https://iopscience.iop.org/article/10.1088/0143-0807/25/1/014
https://docs.python.org/3/
https://en.wikipedia.org/wiki/Object_pool_pattern