見出し画像

モノフォニックシンセへの(やや泥濘んだ)道

SuperColliderにMIDIキーボードを入力して、モノフォニックシンセを作りたい。MIDIデバイスを接続するにはMIDIInを使う。「MIDIIn.connectAll」を実行して、PCに接続したデバイスが表示されれば使用できる筈である(Example 0)。

// Example 0

MIDIIn.connectAll;

まずはMIDIFuncを使ってキーボードから送られて来るMIDIメッセージを確認する(Example 1)。今、筆者はKORG microKEY-25を接続しているので、鍵盤からノートオン/オフのメッセージが得られる他、ジョイスティックからコントロールチェンジとピッチベンドの値が得られた。ただし今回はジョイスティックを使わない。メッセージの構成はコメントの通りで、ノートオン/オフの場合、1番目がベロシティ、2番目がノートナンバーである。

// Example 1

(
~noteOn.free;
~noteOff.free;
~susCc.free;
~bend.free;

~noteOn = MIDIFunc.noteOn({|...args|	// vel, note, ch, src
	("on:"++args).postln;
});

~noteOff = MIDIFunc.noteOff({|...args|	// vel, note, ch, src
	("off:"++args).postln;
});

~susCc = MIDIFunc.cc({|...args|			// val, cc, ch, src
	("cc:"++args).postln;
});

~bend = MIDIFunc.bend({|...args|		// val, cc, ch, src
	("bend:"++args).postln;
});
)

モノフォニック楽器の前に、ポリフォニック楽器を作ってみる。これはノートオン/オフのメッセージをそのまま、シンセの起動と除去に対応させればよいから、恐らく簡単である。シンセは正弦波を重ねてオルガン風の音色とし、オーディオバスを使ってリバーブ付きのミキサーに投げる(Example 2)。

// Example 2

(
SynthDef(\organ, {|pch, gte|
	var env, frq, sig;
	env = EnvGen.ar(Env.asr(0.01, 1, 0.01), gte, doneAction: 2);
	frq = pch.midicps;
	sig = SinOsc.ar(2 ** (0..5) * frq, 0, env / 50);
	Out.ar(10, Mix(sig));
}).add;

Ndef(\mix, {
	var in;
	in = InFeedback.ar(10);
	in = Limiter.ar(in, 0.7);
	FreeVerb2.ar(in, in, 0.5, 0.8);
}).play;
)

SynthDef(\organ)のエンベロープ(env)は、ゲート(引数gte)の値が「0より大きい」場合は持続する。gteが0になると、0.01秒のリリースタイムを経て「doneAction: 2」により自身のインスタンスを除去する(freeSelf)。つまり各々のシンセのインスタンスは「鍵盤を押している時間+0.01秒」の間存在する。通常はこれで良い筈である。Example 2を実行した後、Example 3を実行し、適当に鍵盤をまさぐる。

// Example 3

s.plotTree;		// See node tree

(
~noteOn.free;
~noteOff.free;

~noteOn = MIDIFunc.noteOn({|...args|	// vel, note, ch, src
	var pch = args[1];
	s.sendMsg(\s_new, \organ, pch, 0, 0, \pch, pch, \gte, 1);
});

~noteOff = MIDIFunc.noteOff({|...args|	// vel, note, ch, src
	var pch = args[1];
	s.sendMsg(\n_set, args[1], \gte, 0);
});
)

Example 3はポリフォニック楽器の一例である。普通に弾いてみた感じは問題がないように思える。ところが、キーボードを叩き壊す勢いで滅茶苦茶にひっ搔き鳴らしたり、高速にグリッサンドするだけでも、時折「FAILURE IN SERVER /s_new duplicate node ID」とエラー文が発出され、シンセのインスタンス(ノード)が残ってしまうという不具合を見ることができる。状況がどうであれ、プログラミングとして想定外の事象は潰しておきたい。

OSCメッセージ「\s_new」はIDを指定してシンセを起動するが、ここではノートナンバーをそのままシンセのIDにしている。何らかの要因でノードが消えずに残ってしまった場合、同じIDのシンセを立てようとすると「FAILURE IN SERVER /s_new duplicate node ID」となる。重複IDが何番なのか言えよと常々思うが(思うにsclangの手抜かりである)、どのノードが残っているかについては「s.plotTree」等で確認できる。

一抹の不安はあるものの、一旦問題を放置してモノフォニックに進む。モノフォニックとは同時に一音しか鳴らないという意味である。通常はただ一つのシンセを立ち上げて、そのパラメータを変化させれば済む話だろう。とはいえ、急激なパラメータの変化は、波形の断裂によるクリックノイズの原因ともなる。またパラメータの補間を行えば音色が変質する。この場合、使える音色がある程度限定されることが予測できる。

そのため今回は、機構はポリフォニックのまま、聴覚上はモノフォニックである演出を検討する。これは次のシンセが立ち上がる時に、直前のシンセを除去すればよい。こうすることで各音の音色は変質を免れると思われる。

一旦シンセから離れて、打鍵時の振る舞いを考える。Example 4は打鍵の履歴に着目している。新しく打鍵されたノートナンバーは配列(~ary)の末尾に追加され、離鍵したノートナンバーは削除する。単純な機構だが、キーボードの現在の状態がこれで把握できる。

// Example 4

(
~ary = [];		// Pressed keys and history

~noteOn.free;
~noteOff.free;

~noteOn = MIDIFunc.noteOn({|...args|	// vel, note, ch, src
	var pch = args[1];
	~ary = ~ary.add(pch);
	~ary.postln;
});

~noteOff = MIDIFunc.noteOff({|...args|	// vel, note, ch, src
	var pch = args[1];
	~ary.remove(pch);
	~ary.postln;
});
)

Example 4の動作を眺めると、これにそのままシンセの起動/除去の機構を加えれば簡単にモノフォニック楽器が作れる気がする。ここからは少し頭の体操になる。例えばC→Dの順でキーを押下し(続け)た場合は、Dのシンセを起動すると同時にCのシンセを除去する機構が必要となる。

C→Dの順でキーを押下し続け、その後に最新履歴のDを離した場合は、一つ手前のCが再び鳴るべきだ。そうではなくCを離した場合は、何も新たには起こらず、Dが鳴り続けるべきである。要する履歴が新しいキーを優先するという規則だが、言葉だけで考えてもよくわからないので、とにかくプログラムを書いて動かしてみる(Example 5)。

// Example 5

CmdPeriod.run;		// Clear all synths
Ndef(\mix).play;

(
~ary = [];

~noteOn.free;
~noteOff.free;

~noteOn = MIDIFunc.noteOn({|...args|
	var pch = args[1];

	if ( ~ary.size > 0 )		// If any keys are already pressed,
	{ s.sendMsg(\n_set, ~ary.last, \gte, 0) };		// remove the synth.

	~ary = ~ary.add(pch);
	s.sendMsg(\s_new, \organ, pch, 0, 0, \pch, pch, \gte, 1);

	~ary.postln;
});

~noteOff = MIDIFunc.noteOff({|...args|
	var pch = args[1];

	if ( ~ary.last == pch )		// If the last pressed key is released,
	{
		s.sendMsg(\n_set, pch, \gte, 0);		// remove the synth.

		if ( ~ary.size > 1 )	// Then, if there is a penultimate key,
		{
			var nxt = ~ary[~ary.size - 2];		// activate that synth.
			s.sendMsg(\s_new, \organ, nxt, 0, 0, \pch, nxt, \gte, 1);
		};
	};

	~ary.remove(pch);
	~ary.postln;
});
)

べろべろと適当に鍵盤を弾いてみると、序盤はモノフォニック楽器として動作が正常な気がするものの、どんどん残留ノードが増えていく。ノードが残ると、シンセの発音は終わっているのに鍵盤が押さえっぱなしであるのと同じ状態となり、次の発音ができない。どうしたものか。

Example 6でシンセの起動/除去の各実行時間を確認する。

// Example 6

CmdPeriod.run;
Ndef(\mix).play;

(
~ary = [];
~time = Date.getDate.rawSeconds;	// Start time

~noteOn.free;
~noteOff.free;

~noteOn = MIDIFunc.noteOn({|...args|
	var pch = args[1];

	if ( ~ary.size > 0 )
	{
		[Date.getDate.rawSeconds - ~time, \prev_off, ~ary].postln;
		s.sendMsg(\n_set, ~ary.last, \gte, 0);
	};

	~ary = ~ary.add(pch);

	[Date.getDate.rawSeconds - ~time, \note_on, ~ary].postln;
	s.sendMsg(\s_new, \organ, pch, 0, 0, \pch, pch, \gte, 1);
});

~noteOff = MIDIFunc.noteOff({|...args|
	var pch = args[1];

	if ( ~ary.last == pch )
	{
		[Date.getDate.rawSeconds - ~time, \note_off, ~ary].postln;
		s.sendMsg(\n_set, pch, \gte, 0);

		if ( ~ary.size > 1 )
		{
			var nxt = ~ary[~ary.size - 2];
			[Date.getDate.rawSeconds - ~time, \prev_on, ~ary].postln;
			s.sendMsg(\s_new, \organ, nxt, 0, 0, \pch, nxt, \gte, 1);
		};
	};

	~ary.remove(pch);
});
)

Example 6の実行後、適当な2音和音(C-Gなど)のスタッカートを弾いてみる。するとExample 7のような結果が得られる。どうもCとGをほぼ同時に叩いたため、シンセの起動/除去の間隔が短すぎるとノードの残留が生じるようだ。

// Example 7

[ 1.2232077121735, note_on, [ 55 ] ]
[ 1.2271785736084, prev_off, [ 55 ] ]
[ 1.2272419929504, note_on, [ 55, 48 ] ]
[ 1.4796454906464, note_off, [ 55, 48 ] ]
[ 1.4797365665436, prev_on, [ 55, 48 ] ]
FAILURE IN SERVER /s_new duplicate node ID	// 55 organ
[ 1.4921753406525, note_off, [ 55 ] ]

ノードの残留について、同じエンベロープを持つ別のシンセで検証する(Example 8)。変数tによって起動と削除の間隔を探ると、0.001秒などと間隔が短過ぎる場合にノードが残留する。また「t = 0」の場合は間違いなく残留する。

// Example 8

CmdPeriod.run;
s.plotTree;

(
SynthDef(\hoge, {|gte|
	var env, sig;
	env = EnvGen.ar(Env.asr(0.01, 1, 0.01), gte, doneAction: 2);
	sig = SinOsc.ar(1000, 0, env/4);
	Out.ar(0, sig!2);
}).add;
)

(
t = 0.1;		// This works
// t = 0.01;	// This also works
// t = 0.001;	// 'freeSelf' failed
// t = 0;		// Always failing

fork {
	s.sendMsg(\s_new, \hoge, 100, 0, 0, \gte, 1);
	t.wait;
	s.sendMsg(\n_set, 100, \gte, 0);
};
)

1/1000秒オーダーで鍵盤を弾いたつもりはないのだが、問題は現実に起きている。これを解決するために丸一日を要した。とはいえ混沌を極めた経緯は割愛する。ともかく、問題を解いたと思われるのがExample 9である。「s.bind」と「Score.play」の二つのトリックがあるが、特に「s.bind」は完全に行き詰ったと思われた時に出鱈目に書き付けて、偶然に辿り着いたものだ。何故これで上手く行くのかは各自考えてみて欲しい。

// Example 9

CmdPeriod.run;
Ndef(\mix).play;

(
~ary = [];

~noteOn.free;
~noteOff.free;

~noteOn = MIDIFunc.noteOn({|...args|
	var pch = args[1];

	if ( ~ary.size > 0 )
	{
		s.bind {
			Score([
				[0, [\n_set, ~ary.last, \gte, 0]],
				[0.01, [\n_free, ~ary.last]]
			]).play;
		};
	};

	~ary = ~ary.add(pch);

	s.bind {
		s.sendMsg(\s_new, \organ, pch, 0, 0, \pch, pch, \gte, 1);
	};
});

~noteOff = MIDIFunc.noteOff({|...args|
	var pch = args[1];

	if ( ~ary.last == pch )
	{
		s.bind {
			Score([
				[0, [\n_set, pch, \gte, 0]],
				[0.01, [\n_free, pch]]
			]).play;
			};

		if ( ~ary.size > 1 )
		{
			var nxt = ~ary[~ary.size - 2];
			s.bind {
				s.sendMsg(\s_new, \organ, nxt, 0, 0, \pch, nxt, \gte, 1);
			};
		};
	};

	~ary.remove(pch);
});
)

SuperColliderでは大量のOSCメッセージが混み合った時に、メッセージが遅延して動作のタイミングが崩れることがあり、これを正すのが「s.bind」である。予めサーバーのレイテンシーをかませることで、体感的にも明らかな遅延を生じるが、OSCメッセージの実行順序は保たれるという。ビート系のライブコーディングでも必須のメソッドである。

「Score.play」の方は全くの奇策である。打鍵のタイミングによってEnvGenがfreeSelfに失敗する件を諦め、エンベロープの終了とノードの解放をマニュアルで行うことにした。何やら見た目は奇怪だが(時折エラー文も飛ぶが)、ともかく演奏を妨げる残留ノードは防げるようである。他に良い書き方があれば是非、ご教示を賜りたい。

この記事が気に入ったらサポートをしてみませんか?