はじめに
この記事は TouchDesigner Advent Calendar 2025 🎅 11日目の記事です
2025年10月からTouchDesignerを使ったVJとして活動を開始しはじめためろちだです。
TouchDesigner どころか、VJの経験もまだまだ浅いです。
個人的に、TouchDesigner上に汎用的な VJ アプリケーションを構築する上で
疎結合な単一責務なコンポーネントをたくさん実装し、それの組み合わせで
VJアプリケーションを実現したいと考えています。
その上で、今回は汎用的なビートジェネレーターを実装してみました。
できたもの
DAW の ドラムマシンのような UI で四つ打ち以外のビートを気軽に生成できます
また、TAP ボタンを押すこと あるいは、フェーダーで BPM を調節することもできます
Reset ボタンを押すと拍の頭出しができます
📋 全体構成
まず、BeatGenerater は大きく2つの COMP から構成されています
BeatGenerater/
├── BPMController/
└── StepController/
Part 1: BPMコントローラー部分
BPMを設定する方法が2つ用意しています
- スライダーで直接設定 - 数値を見ながら正確に設定
- タップテンポ - 曲に合わせてタップしてBPMを検出
1. スライダーによるBPM設定
これは シンプルに Slider COMP と CHOP を繋いで BPM の Range を指定しただけのものです

2. タップテンポ機能
タップテンポは「曲を聴きながらボタンをタップすると、その間隔からBPMを自動計算する」機能です。
2秒以上間隔が空くとリセットされ、タップ間隔の平均からBPMを計算しています。
CHOP Execute DAT の Python スクリプトで実装しました。
tap_times = [] # タップ時刻を保持するリスト
def onOffToOn(channel, sampleIndex, val, prev):
current_time = time.time()
# 2秒以上経過したらリセット
if len(tap_times) > 0 and (current_time - tap_times[-1]) > 2.0:
tap_times.clear()
op('bpm_data').par.value0 = 120.0 # デフォルトBPMに戻す
op('bpm_data').par.value1 = 0 # タップ数リセット
# タップ時刻を記録
tap_times.append(current_time)
# 最大8タップまで保持(古いものは削除)
if len(tap_times) > 8:
tap_times.pop(0)
# 2回以上タップしたらBPM計算
if len(tap_times) >= 2:
# 各タップ間隔を計算
intervals = [tap_times[i] - tap_times[i-1]
for i in range(1, len(tap_times))]
# 平均間隔を算出
avg_interval = sum(intervals) / len(intervals)
# BPMに変換(60秒 ÷ 平均間隔)
bpm = 60.0 / avg_interval if avg_interval > 0 else 120.0
op('bpm_data').par.value0 = round(bpm, 1)
op('bpm_data').par.value1 = len(tap_times)
3. 二種類の BPM の設定方法から後勝ちで BPM を決める
スライダーとタップテンポ、どちらの値を使うかを自動判定するようにしています
基本的に最後に操作されたほうの BPM が選ばれます
last_values = {}
last_changed = None
last_time = 0
def onCook(scriptOp):
global last_values, last_changed, last_time
# 全入力をチェック
for i, inp in enumerate(scriptOp.inputs):
current = inp[0].eval()
key = f"input_{i}"
# 値が変わった入力を検出
if key not in last_values or last_values[key] != current:
last_values[key] = current
last_changed = i
last_time = time.time()
# 最後に変更された入力の値を出力
if last_changed is not None:
scriptOp.clear()
scriptOp.appendChan('value')[0] = scriptOp.inputs[last_changed][0].eval()
scriptOp.appendChan('source')[0] = last_changed # どの入力が選ばれたか
Part 2: ビート生成部分
Timer CHOPでビートを刻む
BPMから実際のビート信号を生成する心臓部です。
BPM Controller から 指定された BPM を受け取り、実際にそれに従い Beatを生成している場所です
今回は1小節を16分割(16分音符)で刻みたいので、1拍あたりの長さをさらに4分割しています。
例えば BPM 128 の場合、計算式は以下のようになります:
60秒 / 128 BPM / 4 = 約0.117秒(16分音符1つ分の長さ)
生成された Beat を Count CHOP で数え、 count の数に応じてシーケンサーを進めます
シーケンサーを進めたとき該当の場所の ボタンが ON になっていれば Beat として output します
ボタンの入力と16分のビートから実際に欲しいビートを生成する
in1 が Timer CHOP で刻んだビートです, in2 が ボタン16個の On/Off の状態です
これらを Script CHOP で演算し、ボタンが押されていたら Beat を生成するようにしています
def onCook(scriptOp):
scriptOp.clear()
# 入力1: step値 (0-15)
step_input = scriptOp.inputs[0]
# 入力2: Value0-Value15のフラグ (0 or 1)
flags_input = scriptOp.inputs[1]
# stepの現在値を取得
step = int(step_input[0][0]) # 最初のチャンネルの最初のサンプル
# 出力チャンネルを作成
output_chan = scriptOp.appendChan('output')
step_chan = scriptOp.appendChan('step')
# 結果を計算
result = 0
# step が 0-15 の範囲内かチェック
if 0 <= step <= 15:
# 対応するフラグをチェック (Value{step} のフラグ)
flag = flags_input[step][0] # step番目のチャンネルの値
# フラグが1の場合のみ1を出力
if flag == 1:
result = 1
# stepが整数のときのみビートを出力(遷移中は出力しない)
if step_input[0][0] == int(step_input[0][0]):
output_chan[0] = result
else:
output_chan[0] = 0
step_chan[0] = step
return
Beat ジェネレーターの用途
この生成された Beat を使って、四つ打ちじゃないドラムパターンのジャンルに対応した、ステップシーケンサーが簡単に作れます
通常、VJでは音声入力からビートを検出することが多いですが、AudioAnalyzer の場合、LPF/HPF で特定の音域から Trigger としきい値で Beat の有無を取得することになると思います。しかし、ジャンルによってキックの音域やSustain、Atack などが異なります。
また、イベントの制約によっては、Line がもらえないこともあると思います。
そういった場面で役立つのじゃないかなと思い実装してみました。
汎用的な tox としておくことで、いつでも気軽に利用もできます。
実際の利用例
Beat をトリガーとして、ステップシーケンサーを使ってもよいですが
オーディオリアクティブではなく、汎用的なプリレンダのVJ用ループ素材を作るときにも便利です

おわりに
今回は、いくつか自分が作って使っている小さめのコンポーネントのうちの1つである ビートジェネレーター を紹介しました
他にも、標準の Tools とは少し違った AudioAnalyzer や、Midi のコントローラーなど色々なコンポーネントを UI 付きで実装している段階です
最終的に、この小さな コンポーネントをイベントごとに組み替えて臨機応変に対応できるようしたいなと構想しています





