0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TidalCyclesで、いまどのサイクルから音が出ているか確認する方法

0
Last updated at Posted at 2026-03-16

はじめに

TidalCyclesはライブコーディングによって音やリズムを生成できるオープンソースのソフトウェアで、これによって複雑なリズムパターンをコードベースで実験・楽曲制作・ライブパフォーマンスが可能です。

ライブパフォーマンスにおける課題

自分はライブでSuperColliderによってシンセを作ってソフトウェアとしての楽器を制作したり、SuperColliderで作成したシンセをTidalCyclesから呼び出してライブパフォーマンスをすることが多いです。

TidalCyclesはコードベースのため記録しやすさがあったり、細かいパラメータをいじりやすいためとても重宝しています。

一方で、最近では1~2時間の長時間にわたるライブパフォーマンスをすることがあり、いまどのサイクル(orbit)からどの音を出しているのかわからなくなることがあります。
(Tidalは特別な設定をしない限りすべてのorbitから出る音は2chになるため)

現状これを一番良い方法で解決できる方法が見つかったのでシェアさせていただきます。

Tidal Orbit Monitor

いきなり作ったものですが、画像のように各Tidalのチャンネルのボリュームを見られるようにしています。
これによっていま鳴っているチャンネルがわかるので、音と比べればどのorbitからどの音が鳴っているか感覚的にわかると思います。

特別なソフトを使う必要がなく、SuperColliderのエディタから起動できるかつ軽量なので扱いやすいです

環境構築手順

  1. startup.scdの末尾に以下を追記する。(startup.scdはSuperColliderのエディタから「File -> Open startup file」で開けます)
  2. SuperColliderを再起動して、SuperDirtを起動させる
  3. SuperDirtの起動後に~orbitMonitor.();を「Cmd + Enter」で実行すればTidal Orbit Monitorが立ち上がる
startup.scd
// =======================
// 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に以下を追加

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に以下を追記

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"
+:}
+
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?