SuperColliderでシンセサイズ

はじめに

SC3を使ってるよ、はじめようと思うよ、などと特殊な方々にとっては、シンセサイズの基礎なんざ極めすぎて困っているんだ!というところでしょうが、SC3用にまとめておきます。max/msp用の解説は多いのですが、パッチ見てもよくわかりませんでした。

加算合成 additive synthesis

/*
加算合成とは、その名の通り波形信号を足し合わせて新たな波形信号を作り出す合成方法です。最も基本的な合成方法のひとつで、理論上はサイン波を加算合成することであらゆる波形を生成可能です。sc3上で実現するためには、"+"。要は適当に信号を足せばもう加算合成の出来上がりです。
では、ちょっと足してみましょう。
*/

{ SinOsc.ar(440, 0, 0.8) }.scope;
{ SinOsc.ar(440,0,0.4) + SinOsc.ar(880,0,0.4) }.scope
{ SinOsc.ar(440,0,0.3) + SinOsc.ar(880,0,0.3) + SinOsc.ar(220, 0, 0.3) }.scope

/*
1行目は440Hzのサイン波です。2行目は440Hzに880Hzの倍音を加えています。ちょっとオルガンぽくなりましたね。3行目ではさらにリッチになりました。スペアナのスケールをlogにして確認してみてください。周波数の山が2つ、3つとできています。上記のように基本的に加算合成では、基本周波数の倍音を加えていきます。

※信号を"+"するときには加算結果のmul(振幅)が1を超えないようにした方が耳のためになります。
*/

{ SinOsc.ar( 440, 0, 0.4 ) + SinOsc.ar( 441, 0, 0.4 ) }.scope
{ SinOsc.ar( 440, 0, 0.4 ) + WhiteNoise.ar( 0.05) }.scope

/*
倍音以外の加算ですと別々に音が鳴っているように聴こえたり、もはやエフェクタだな、くらいになってしまいます。

また、sc3ではちまちま"+"をつかって合成する必要はありません。Mixクラスが用意されています。
*/

{ Mix.ar( [ SinOsc.ar(220), SinOsc.ar(440), SinOsc.ar(880) ])*0.4 }.scope
{ Mix.ar( SinOsc.ar( [220, 440, 880] ) * 0.4) }.scope

/*
上記二つのサンプルは同じ処理です。嫌がらせのようにショートカット記述が多いので自分の書き方を決めておいた方が良いかもしれません。
Mixは複数の配列(チャンネル)を一つに落とし込みます。array.fillで要素を埋めた配列を用意してやると効果的に使えます。
*/

(
	{
	Mix.ar(
		Array.fill(1000, { SinOsc.ar(800.rand) }  )	//1000個のSinOsc要素を持つ配列を作成し、一つにまとめる
	)*1/1000
	}.scope
)

/*
加算合成は最近ではほとんど使われないようです。面倒、マシンパワー喰い過ぎなどなどの理由からですね。加えていく波形に対して、それぞれエンベロープなどを設定したりするため、膨大な作業量となってしまいがちです。加算合成について詳しく知りたい方はフーリエ合成、フーリエ変換でググってみて下さい。嫌になりますw
*/

減算合成 subtractive synthesis

/*
アナログシンセの基本合成法です。加算合成とは逆で、倍音を沢山含んだ信号の倍音をフィルタで削って、アンプで音量などの調整を行います。ググればいっぱい情報がでてきます。自然の音は、減算合成と同じような方法で生成されています。例えば、弦楽器などでは、まずランダムな励振(excitation)を生成して、自然な共振周波数を持った楽器のボディを通して音をフィルタリング、整形します。人間の声も舌とか口の形で音を加工しているので、減算合成みたいなものです。
SC3でも普通のシンセの音作りと同じでオシレータにフィルタかけて、エンベロープで調整します。
*/

/*
まずはオシレータ。
ヘルプのOscillatorsの項目を見てみて下さい。アナログシンセなどで基本的に使われる波形信号は、
SinOsc :サイン波
Saw :ノコギリ波
Pulse :パルス波
LFTri :三角波
ですね。他にも色々とありますが、あれもこれもと手をだしていると収集が付かなくなります。スペアナを確認すると、ノコギリ波、パルス波など多くの周波数成分が含まれています。こいつらをフィルタで削ってやるんですよ。
*/

//SinOsc.ar(freq, phase, mul, add)
{ SinOsc.ar(440, 0, 0.8) }.scope

//Saw.ar(freq, mul, add)
{ Saw.ar(440, 0.8) }.scope

//Pulse.ar(freq, width, mul, add)
{ Pulse.ar(440, 0.8) }.scope

//LFTri.ar(freq, iphase, mul, add)
{ LFTri.ar(440, 0, 0.8) }.scope

/*
次にフィルタ。ヘルプのFiltersの項目を見ていただけるとお分かりかと思いますが、鬼のように沢山あります。
基本は、
RLPF :ローパスフィルタ
RHPF :ハイパスフィルタ
BPF :バンドパスフィルタ
の三つです。これ以外は、このフィルタないすか?とか思ったときに探せば良いと思います。大抵見つかります。LPF、HPFじゃないの?→LPF、HPFではレゾナンスをいじれないため大人しく”R”付きを使いましょう。
※ローパスとかハイパスとか分かりにくい、という話がCottle氏のPDFにもありますね。まー、ハイカットフィルタ、ローカットフィルタとかの方が減算合成として使う分には直感的なんでしょうね。私は”パス”を強調して混乱しないようにしてます。
まずはフィルタを体感してみましょう。マウスのX軸でカットオフ周波数を変化させられます。視覚化するとわかりやすいですね。
*/

RLPF.scopeResponse
RHPF.scopeResponse
BPF.scopeResponse

/*
では、引数を説明します。
RLPF.ar( in, freq, q, mul, add )
RHPF.ar( in, freq, q, mul, add )
BPF.ar( in, freq, q, mul, add )
引数は同じですね。
in : 入力信号。フィルタをかけたい波形、信号を入力します。
freq : カットオフ周波数。この周波数を境目にカットするかパスするか、が決定されます。ただし、カットといっても完全に消すわけではなく、フィルタにもよりますが、ゆるやかに減少させることになります。
q : レゾナンス(または、reciprocal of q : bandwidth/cutoff frequency)。上手く説明できませんが、一般的なアナログシンセでは、カットオフ周波数を強調するのに使われます。みょーん、ってやつですね。バンドパスでは、パスする周波数帯の山の緩やかさを調整できます。(詳しくは、cottle氏のPDFを見て下さい)

実際に、以下のサンプルを実行してfreqscopeで見てみましょう。マウスのY軸でレゾナンスを調整できます、上にもっていくと周波数の山がとんがることが確認できるかと思います。
*/

//cottle氏のPDFより
(
{
	var signal, filter, cutoff, resonance;
	signal = PinkNoise.ar(mul: 0.7);
	cutoff = MouseX.kr( 40, 10000, 1 );
	resonance = MouseY.kr( 0.01, 2.0 );
	
	BPF.ar( signal, cutoff, resonance)	//RLPF,RHPFでもためしてみてください
}.scope(1)
)

/*
最後に、エンベロープについて。エンベロープとは、時間軸で波形を変化させることです。アナログシンセではADSR方式のエンベロープが用いられることがほとんどです。ADSRとは、アタック、ディケイ、サスティン、リリースの頭文字をとったもので、これら4つのパラメータを用います。adsrでググればwikipediaが引っかかるのチラ見しておくと良いかと思います。

SC3でのエンベロープの基本的な作り方は、Envクラスのメソッドで形を選択し、インスタンス(クラスの実体化のこと)を生成します。生成されたインスタンスをenvelope generatorのEnvGenによって、再生可能な形のエンベロープとして生成します。(説明が違う気もします)

ま、とりあえず、エンベロープを見てみましょう。
*/

//基本形によるエンベロープ
Env.linen.test.plot
Env.triangle.test.plot
Env.sine.test.plot
Env.perc.test.plot

//サスティンを持ったエンベロープ
Env.adsr.test.plot
Env.dadsr.test.plot
Env.asr.test.plot
Env.cutoff.test.plot

/*
SC3ではこんな感じで色々そろっています。(引数なしだとデフォのパラメータ値が割り当てられます)そんな既存のものなんかつかわねー、自分で作るんだ!という方は、.new( levels, times, curves, releaseNode, loopNode )でいかようにも作れます。一からちまちま書いてらんねーよ、な方は、SCEnvelopeView,SCEnvelopeEditを利用すると手っ取り早く書けそうです。使ったことありませんので、help見て下さい。すみません。

では、エンベロープの形の作り方はおkとして、EnvGenについて。Envクラスで作ったエンベロープを制御するのがEnvGenと考えて下さい。

EnvGen.ar( envelope, gate, levelScale, levelBias, timeScale, doneAction )
envelope : Envで作成したインスタンス
gate : この値が0より大きい場合にエンベロープをオン(使用可能)にします。固定長のエンベロープを使った場合には、単にトリガーとして動作します。要は、gateを1にしたタイミングでエンベロープがオンになり、音がでるようになります。また、サスティンを持ったエンベロープの場合は、gateが0になるまでエンベロープが終了しません。0になったタイミングからエンベロープのリリースが始まります。重要なのは、サスティンを持ったadsrなどでgateを0にして解放してやらない限りいつまでも音が鳴り続いてしまう、ということです。必ずどこかのタイミングでgateを0にして解放しましょう。
levelScale : ここらへん使う人っているのかな?割愛
levelBias : 同上
timeScale : 同上
doneAction : エンベロープが終了した時点、もしくはgateが0になった時点での挙動を指定します。これは使い方が色々あるのですが、とりあえずは、エンベロープが終了したらシンセを解放する、2を与えておけば問題ないでしょう。他には、何もしない0、同じグループの前のノードを全て解放する7、全てのシンセを解放する14とかあって、このシンセが終わったら他のシンセも解放したい、という場合に使えそうですが、グループ、ノードを極めてから、調べても遅くないです。
EnvGenで作ったエンベロープをどのようにして使うか、ですが信号に*するだけでおkです。
*/

{ EnvGen.ar(Env.perc, 1.0, doneAction: 2) * SinOsc.ar }.play
//doneActionの挙動が知りたい方は、まず0にして数回実行し、サーバパネルのSynthsが増えていることを確認しましょう。その後、数値を変更して実行してみてサーバパネルのSynthsの値を見てみるとちょっと分かった気になれます。

/*
エンベロープについては延々と書けそうですが、ここまで。

では、どのような感じで減算合成を書いていくか、簡単なサンプルを。
*/

(
	var signal, envelope, filter, out;
	{
		//基本の波形を決めsignalとします
		signal = LFTri.ar;
		//signal =Mix.ar( LFTri.ar( [220, 440, 880] ) * 0.5); //加算合成するとリッチに
		
		//使用するフィルタを決めます。ここで入力に上のsignalを用います
		filter = RLPF.ar( signal ,440, 0.8);
		
		//エンベロープをつくります
		envelope = EnvGen.ar( Env.linen, 1, doneAction: 2);  //他のエンベロープも色々試してみて下さい

		//最後に、フィルタで加工した波形にエンベロープをかけて完成
		out = envelope * filter; //filterをsignalに変えてみて、フィルタを通さない場合も確認
	}.scope
) 

FMシンセ:周波数変調合成 frequency modulation synthesis, AMシンセ:振幅変調合成 amplitude modulation synthesis

/*
FMシンセの特徴は、少ないオシレータ数で沢山の倍音を含んだ音を合成できるため、マシンの負荷が少なくてすむ、といわれています。FMシンセの原理は簡単で、モジュレータを使用して、キャリアの周波数を変調させる、といったものになります。なんのこっちゃ?
オペレータ: シンセが保有する、加工していない基本波形のこと。アナログシンセでのオシレータのこと。一般的には正弦波を用いる
キャリア: 加工される側の波形のこと。オペレータを使用する
モジュレータ: 加工する側の波形のこと。オペレータを使用する
(この用語はyamaha語です、がもはや一般化していますので把握しておきましょう)
要は、ある波形を使用して、別の波形の周波数をいじくる、ということです。
これをSC3で表現すると、以下となります。
*/

{ SinOsc.ar( 440 + SinOsc.ar (4400, 0, 500, 300), 0, 0.8 ) }.scope

/*
モジュレータの周波数を高くすることで、合成された波形を別の音色としてとらえることができます。スペアナで見てみると、基本周波数の横っちょに複数の山ができていることが確認できます。こんなに簡単に沢山の周波数成分を持たせることができるのです。モジュレータにあたるサイン波の周波数、mul、addを一桁づつ増やしてみるなど、色々変えて試してみて下さい。モジュレータの振幅を1.0にして周波数を低くすると、ビブラートになりますね。基本は以上です。簡単ですね。

実際のFMシンセの基本形は、次の式で示します。
y = a * sin( Fc * 2pi + b * sin( Fm * 2pi ) );
a: キャリア振幅
b: 変調指数
Fc: キャリア周波数
Fm: モジュレータ周波数

実は単純な式でできているんですね。では、これをSC3で示すと次のようになります。
*/

(
	var car, mod;
	var freq = 440, carAmp = 1.0, modAmp = 500.0, modAdd = 1000;
	{
		mod = SinOsc.ar( freq * 100, 0, modAmp, modAdd );
		
		//mod = SinOsc.ar( freq *10 + mod , 0, modAmp, modAdd ); //モジュレータを周波数変調させてみる
		//mod = SinOsc.ar( freq *10 + mod , 0, modAmp, modAdd ); //もう一回
		//mod = SinOsc.ar( freq *10 + mod , 0, modAmp, modAdd ); //まだまだ
		//mod = SinOsc.ar( freq *10 + mod , 0, modAmp, modAdd ); //さらに
		
		car = SinOsc.ar( freq + mod, 0, carAmp );
		car
	}.scope
)

/*
大抵の解説では分かりやすくするため、キャリアをモジュレータで一回周波数変調する例がほとんどですが、モジュレータのamp(mul)にオペレータを接続してみる、キャリアを別のキャリアに接続などなど、好き放題になんでもくっつけられます。意図した音はだせませんけれども。そうそう、AM合成はFM合成の周波数を振幅に置き換えたものです。(これで伝わりますよね?)RM合成もあります。

tutorialでは以下のようにFM合成つかってますね。
*/

//Introduction tutorial 14. Frequency-Modulationより
(
SynthDef("fm1", { arg bus = 0, freq = 440, carPartial = 1, modPartial = 1, index = 3, mul = 0.05;

	// index values usually are between 0 and 24
	// carPartial :: modPartial => car/mod ratio
	
	var mod;
	var car;
	
	mod = SinOsc.ar(
		freq * modPartial, 
		0, 
		freq * index * LFNoise1.kr(5.reciprocal).abs
	);
		
	car = SinOsc.ar(
		(freq * carPartial) + mod, 
		0, 
		mul
	);
	
	Out.ar(
		bus,
		car
	)
}).load(s);
)

(
Synth("fm1", [\bus, 0, \freq, 440, \carPartial, 1, \modPartial, 2.4]);
Synth("fm1", [\bus, 1, \freq, 442, \carPartial, 1, \modPartial, 2.401]);
)

位相変調合成 phase modulation synthesis

/*
もうその名の通り位相をいじります。これはPMOscクラスがあるので、こいつ使って下さい。気が向いたら追記します。
*/

以上

/*
以上、つらつらと書きましたが、sc3ではもう好きなようにオシレータでオシレータのあらゆる部分をいじくり倒せます。ごく簡単に。もちろん、収拾がつかなくなること間違い無しです!こんな感じにね。
*/

(
	SynthDef("addSynth",
	{
		arg amp,  freq=440, gate=1, pan=0, outBus=0, fxBus=10, fx=0.0, vol=1.0;
		var env, outSend, fxSend, scale, key;
		var car, mod, filter;
		var carAmp = 0.6, modAmp = 300.0, modAdd = 400;
		
		scale = Scale.major;
		key = DegreeToKey.kr( scale.as(LocalBuf), Rand(0, 12), scale.stepsPerOctave, 1,36 ).midicps;
		
		env = Env.adsr(0.01, 0.05, 0.25, 0.06 );	//シーケンスパートにてgate=0で解放しないと鳴りっぱなし。
		mod = SinOsc.ar( freq * 100, modAmp, modAdd ) + SinOsc.ar( freq * 120, modAmp, modAdd );
		mod = LFTri.ar( freq *100 + mod , 0, modAmp, modAdd ); //モジュレータを周波数変調させてみる
		car = SinOsc.ar( key + mod, 0, carAmp + Pulse.ar(4000, 0.15, 0, 0.05) );
		filter = RLPF.ar( car, 800, 1.2 );
		
		fxSend = filter * fx;
		outSend = filter * (1.0 - fx);
		Out.ar(fxBus,
			Pan2.ar(EnvGen.ar(env, gate, doneAction: 2) * fxSend, pan)
		);
		Out.ar(outBus,
			Pan2.ar(EnvGen.ar(env, gate, doneAction: 2) * outSend, pan)
		);
	}).store;	
)
(
	var t, n, addsynth1, addsynth1Loop;
	~effectGroup = Group.tail(s);
	~synthGroup = Group.tail(s);
	
	t = TempoClock(120/60);		//120bps
	
	addsynth1 = Task({
		var pattern;
		pattern = Pseq([1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0], inf).asStream;
		loop({
			addsynth1Loop = Synth.tail(~synthGroup, "addSynth",
				[\amp, pattern.next]
			);
		(t.beatDur).wait;
		addsynth1Loop.set(\gate, 0);	//synthのenvがadsrの時はここらで解放してやります
		});
	});
	t.sched(0, { addsynth1.start });
)

最後に

例のごとく、htmlファイルをsupercollider.jpにアップしておきます。ここにアップしました。お暇な方、奇特な方がいらっしゃれば追記、修正でブラッシュアップしていただければと思います。