はじめに
TidalCyclesはライブコーディングによって音やリズムを生成できるオープンソースのソフトウェアで、これによって複雑なリズムパターンをコードベースで実験・楽曲制作・ライブパフォーマンスが可能です。
ライブパフォーマンスにおける課題
自分はライブでSuperColliderによってシンセを作ってソフトウェアとしての楽器を制作したり、SuperColliderで作成したシンセをTidalCyclesから呼び出してライブパフォーマンスをすることが多いです。
TidalCyclesはコードベースのため記録しやすさがあったり、細かいパラメータをいじりやすいためとても重宝しています。
一方で、最近では1~2時間の長時間にわたるライブパフォーマンスをすることがあり、いまどのサイクル(orbit)からどの音を出しているのかわからなくなることがあります。
(Tidalは特別な設定をしない限りすべてのorbitから出る音は2chになるため)
現状これを一番良い方法で解決できる方法が見つかったのでシェアさせていただきます。
Tidal Orbit Monitor
いきなり作ったものですが、画像のように各Tidalのチャンネルのボリュームを見られるようにしています。
これによっていま鳴っているチャンネルがわかるので、音と比べればどのorbitからどの音が鳴っているか感覚的にわかると思います。
特別なソフトを使う必要がなく、SuperColliderのエディタから起動できるかつ軽量なので扱いやすいです
環境構築手順
- startup.scdの末尾に以下を追記する。(startup.scdはSuperColliderのエディタから「File -> Open startup file」で開けます)
- SuperColliderを再起動して、SuperDirtを起動させる
- SuperDirtの起動後に
~orbitMonitor.();を「Cmd + Enter」で実行すればTidal Orbit Monitorが立ち上がる
// =======================
// Orbit monitor synth
// =======================
SynthDef(\orbitMonitor, { |inbus, outbus|
var sig;
var amp;
sig = In.ar(inbus, 2);
amp = Amplitude.kr(Mix(sig)).lag(0.1) * 10;
Out.kr(outbus, amp);
}).add;
// =======================
// Orbit monitor setup
// =======================
~orbitMonitorSetup = {
~orbitMonitorBuses = Array.newClear(~dirt.orbits.size);
~orbitMonitorSynths = Array.newClear(~dirt.orbits.size);
~dirt.orbits.do { |orbit|
var bus;
var synth;
bus = Bus.control(s, 1);
synth = Synth.tail(
s,
\orbitMonitor,
[
\inbus, orbit.dryBus.index,
\outbus, bus.index
]
);
~orbitMonitorBuses[orbit.orbitIndex] = bus;
~orbitMonitorSynths[orbit.orbitIndex] = synth;
};
"Orbit monitor ready".postln;
};
// =======================
// Orbit GUI
// =======================
~orbitMonitorGUI = {
var n;
var rowHeight = 22;
var height;
// 既存Windowがあれば閉じる
if(~orbitWindow.notNil) {
~orbitWindow.close;
};
n = ~orbitMonitorBuses.size;
height = (n * rowHeight) + 20;
~orbitWindow = Window(
"Tidal Orbit Monitor",
Rect(200,200,260,height)
).front;
~orbitMeters = Array.newClear(n);
n.do { |i|
StaticText(~orbitWindow, Rect(10, 10 + (i*rowHeight), 60, 18))
.string_("orbit " ++ i);
~orbitMeters[i] =
LevelIndicator(~orbitWindow, Rect(70, 10 + (i*rowHeight), 160, 18));
};
Routine({
loop {
n.do { |i|
var v = ~orbitMonitorBuses[i].getSynchronous;
{
~orbitMeters[i].value = v.clip(0,1);
}.defer;
};
0.05.wait;
}
}).play(AppClock);
"Orbit monitor GUI started".postln;
};
~orbitMonitor = {
~orbitMonitorSetup.();
~orbitMonitorGUI.();
};
追記
最近(2026年6月時点)の自分のライブでは、以下のようなシステムにアップデートしています。
- orbitではなくd1などを表示するようにする
- サイクルにタグ名をつけられるようにする
TidalCyclesのパラメータに
d1
$ s "bd"
# amp 0.2
# ntag "sin1_y04"
のようにntagパラメータを渡せば、モニターに以下のように表示されます。
startup.scdに以下を追加
// =======================
// Orbit monitor synth
// =======================
SynthDef(\orbitMonitor, { |inbus, outbus, orbitIndex=0|
var sig;
var amp;
sig = In.ar(inbus, 2);
amp = Amplitude.kr(Mix(sig)).lag(0.1) * 10;
Out.kr(outbus, amp);
// 20Hzで振幅をlanguage側にプッシュ
SendReply.kr(Impulse.kr(20), '/orbitAmp', [orbitIndex, amp]);
}).add;
// =======================
// Orbit monitor setup
// =======================
~orbitMonitorSetup = {
~orbitMonitorBuses = Array.newClear(~dirt.orbits.size);
~orbitMonitorSynths = Array.newClear(~dirt.orbits.size);
~dirt.orbits.do { |orbit|
var bus;
var synth;
bus = Bus.control(s, 1);
synth = Synth.tail(
s,
\orbitMonitor,
[
\inbus, orbit.dryBus.index,
\outbus, bus.index,
\orbitIndex, orbit.orbitIndex
]
);
~orbitMonitorBuses[orbit.orbitIndex] = bus;
~orbitMonitorSynths[orbit.orbitIndex] = synth;
};
"Orbit monitor ready".postln;
};
// =======================
// ntag受信
// =======================
~orbitTags = Array.fill(16, { "" });
OSCdef(\ntagReceiver, { |msg|
var orbit;
var tag;
// 想定フォーマット: /ntag orbit tag
orbit = msg[1].asInteger;
tag = msg[2].asString;
if(orbit.notNil && tag.notNil) {
~orbitTags[orbit] = tag;
};
}, '/ntag');
// =======================
// Orbit GUI
// =======================
~orbitMonitorGUI = {
var n;
var rowHeight = 22;
var height;
if(~orbitWindow.notNil) {
~orbitWindow.close;
};
n = ~orbitMonitorBuses.size;
height = (n * rowHeight) + 20;
~orbitWindow = Window(
"Tidal Orbit Monitor",
Rect(200,200,320,height) // 少し広げる
).front;
~orbitMeters = Array.newClear(n);
~orbitTagTexts = Array.newClear(n);
n.do { |i|
// d番号
StaticText(~orbitWindow, Rect(10, 10 + (i*rowHeight), 30, 18))
.string_("d" ++ (i+1));
// タグ表示 ← NEW
~orbitTagTexts[i] =
StaticText(~orbitWindow, Rect(45, 10 + (i*rowHeight), 80, 18))
.string_("");
// メーター
~orbitMeters[i] =
LevelIndicator(~orbitWindow, Rect(130, 10 + (i*rowHeight), 170, 18));
};
// 振幅をlanguage変数に記録するだけ(deferしない)
~orbitAmps = Array.fill(16, 0.0);
OSCdef(\orbitAmpReceiver, { |msg|
var orbit = msg[3];
var amp = msg[4];
if(orbit.notNil && amp.notNil) {
~orbitAmps[orbit.asInteger] = amp.asFloat;
};
}, '/orbitAmp');
// GUI起動時に~orbitTagsの現在値を即時反映
if(~orbitTags.notNil) {
~orbitTags.do { |tag, i|
if(~orbitTagTexts[i].notNil) {
~orbitTagTexts[i].string = tag;
};
};
};
// SkipJack1個でメーターとタグをまとめて10Hz更新
// deferが1回/100msなのでAppClockが詰まらない
// SkipJackはCmdPeriodで止まらない
if(~orbitSkipJack.notNil) { ~orbitSkipJack.stop };
~orbitSkipJack = SkipJack({
{
try {
n.do { |i|
if(~orbitMeters.notNil && ~orbitMeters[i].notNil) {
~orbitMeters[i].value = (~orbitAmps[i] ? 0).clip(0, 1);
};
if(~orbitTagTexts.notNil && ~orbitTagTexts[i].notNil) {
~orbitTagTexts[i].string = (~orbitTags[i] ? "");
};
};
};
}.defer;
}, 0.1, name: "OrbitMonitor");
"Orbit monitor GUI started".postln;
};
~orbitMonitor = {
~orbitMonitorSetup.();
~orbitMonitorGUI.();
};
// CmdPeriod後: ウィンドウを閉じてクリーンにし、1秒後に自動再オープン
// ウィジェット更新を一切行わないことでAppClockのフリーズを回避
CmdPeriod.add({
"CmdPeriod: closing orbit monitor window...".postln;
// ウィンドウを同期的に閉じる(deferなし=Qtウィジェット更新を回避)
if(~orbitWindow.notNil) {
~orbitWindow.close;
~orbitWindow = nil;
};
~orbitMeters = nil;
~orbitTagTexts = nil;
// 1秒後にシンセ再作成+GUI再オープン
SystemClock.sched(1.0, {
"CmdPeriod: reopening orbit monitor...".postln;
try {
if(~dirt.notNil && ~orbitMonitorBuses.notNil) {
// orbitMonitorシンセを再作成
~dirt.orbits.do { |orbit|
Synth.tail(s, \orbitMonitor, [
\inbus, orbit.dryBus.index,
\outbus, ~orbitMonitorBuses[orbit.orbitIndex].index,
\orbitIndex, orbit.orbitIndex
]);
};
// GUIを再オープン(AppClockが正常な状態になってから)
{ ~orbitMonitorGUI.(); }.defer;
"CmdPeriod: orbit monitor restored".postln;
};
} { |e| ("CmdPeriod restore error: " ++ e.messageString).postln; };
nil;
});
});
"Orbit monitor CmdPeriod handler registered".postln;
// タグデータを更新するだけ(GUI更新はSkipJackが担当)
~updateTag = { |orbit, tag|
if(~orbitTags.notNil) { ~orbitTags[orbit] = tag; };
};
// .fix でCmdPeriod後のOSCdef.freeAllでも消えないようにする
OSCFunc({ |msg|
var tag = nil;
var orbit = nil;
msg.drop(3).pairsDo { |key, val|
if(key == \ntag) { tag = val; };
if(key == \orbit) { orbit = val; };
};
if(tag.notNil && orbit.notNil) {
tag = tag.asString;
if(tag == "off") { tag = ""; };
~updateTag.(orbit, tag);
};
}, '/dirt/play').fix;
OSCFunc({ |msg|
var orbit = msg[1];
var tag = msg[2].asString;
if(tag == "off") { tag = ""; };
~updateTag.(orbit, tag);
}, '/ntag').fix;
~orbitTags = Array.fill(16, "");
OSCFunc({ |msg|
var tag = msg[1].asString;
if(tag == "off") { tag = ""; };
16.do { |orbit| ~updateTag.(orbit, tag); };
}, '/ntagAll').fix;
// tidal sync n_set
SynthDef(\tick, { |cycle=0, freq=440, amp=0.1, nodeId=1000|
SendReply.kr(
Impulse.kr(0),
"/tick",
[cycle, freq, amp, nodeId]
);
FreeSelf.kr(Impulse.kr(0));
}).add;
OSCdef(\tick, { |msg|
var cycle, freq, amp, nodeId;
#cycle, freq, amp, nodeId = msg[3..];
s.sendMsg("/n_set", nodeId,
"freq", freq,
"amp", amp
);
}, "/tick");
BootTidal.hsに以下を追記
+ import Sound.Osc.Fd as O hiding (Time)
-- 中略 --
forId i t = transition tidal False (Sound.Tidal.Transition.mortalOverlay t) i
- d1 = p 1 . (|< orbit 0)
- d2 = p 2 . (|< orbit 1)
- d3 = p 3 . (|< orbit 2)
- d4 = p 4 . (|< orbit 3)
- d5 = p 5 . (|< orbit 4)
- d6 = p 6 . (|< orbit 5)
- d7 = p 7 . (|< orbit 6)
- d8 = p 8 . (|< orbit 7)
- d9 = p 9 . (|< orbit 8)
- d10 = p 10 . (|< orbit 9)
- d11 = p 11 . (|< orbit 10)
- d12 = p 12 . (|< orbit 11)
- d13 = p 13
- d14 = p 14
- d15 = p 15
- d16 = p 16
+ d1 = p 1 . (|< orbit 0) . (|< pS "ntag" "unknown")
+ d2 = p 2 . (|< orbit 1) . (|< pS "ntag" "unknown")
+ d3 = p 3 . (|< orbit 2) . (|< pS "ntag" "unknown")
+ d4 = p 4 . (|< orbit 3) . (|< pS "ntag" "unknown")
+ d5 = p 5 . (|< orbit 4) . (|< pS "ntag" "unknown")
+ d6 = p 6 . (|< orbit 5) . (|< pS "ntag" "unknown")
+ d7 = p 7 . (|< orbit 6) . (|< pS "ntag" "unknown")
+ d8 = p 8 . (|< orbit 7) . (|< pS "ntag" "unknown")
+ d9 = p 9 . (|< orbit 8) . (|< pS "ntag" "unknown")
+ d10 = p 10 . (|< orbit 9) . (|< pS "ntag" "unknown")
+ d11 = p 11 . (|< orbit 10) . (|< pS "ntag" "unknown")
+ d12 = p 12 . (|< orbit 11) . (|< pS "ntag" "unknown")
+ d13 = p 13 . (|< orbit 12) . (|< pS "ntag" "unknown")
+ d14 = p 14 . (|< orbit 13) . (|< pS "ntag" "unknown")
+ d15 = p 15 . (|< orbit 14) . (|< pS "ntag" "unknown")
+ d16 = p 16 . (|< orbit 15) . (|< pS "ntag" "unknown")
:}
+ :{
+ let sendTag o t = do
+ r <- O.openUdp "127.0.0.1" 57120
+ O.sendMessage r $ O.Message "/ntag" [O.int32 o, O.string t]
+ :}
+
+ :{
+ let sendTagAll t = do
+ r <- O.openUdp "127.0.0.1" 57120
+ O.sendMessage r $ O.Message "/ntagAll" [O.string t]
+ :}
+
+ :{
+ let toOrbit i =
+ case i of
+ 1 -> 0
+ 2 -> 1
+ 3 -> 2
+ 4 -> 3
+ 5 -> 4
+ 6 -> 5
+ 7 -> 6
+ 8 -> 7
+ 9 -> 8
+ 10 -> 9
+ 11 -> 10
+ 12 -> 11
+ 13 -> 12
+ 14 -> 13
+ 15 -> 14
+ 16 -> 15
+ _ -> 0
+ silence' i = do
+ streamSilence tidal i
+ sendTag (toOrbit i) "off"
+ :}
+
+ :{
+ let hush' = do
+ streamHush tidal
+ sendTagAll "off"
+ :}
+
+:{
+ntag = pS "ntag"
+:}
+