見出し画像

シンプルなシンセのシミュレーション(2)

前回の続きです。
こんな感じに、前回作ったシンセにLFOやエンベロープを、かけていきます。


ピッチにLFO


まずはLFOを使えるようにします。
lfoという箱を作って(varに登録して)、その中にサイン波を入れます。

//ex9
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, lfo;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);
	lfo = SinOsc.kr(6)*5;

	wg = Saw.ar(freq+lfo)!2;
	flt = RLPF.ar(wg, 2000) * amp * aEnv;

	Out.ar(0, flt);

}).add;
)

音源のピッチにlfoを足すことで、時間的変化を加えて(音源のピッチを揺らして)ビブラート効果を実現しています。
LFOの効果がわかりやすいように、一定の音程を長めの音符で鳴らしてあげます。

//ex10
(
Pbind(
	\instrument, \synth1,
	\midinote, 69,
	\dur, 4, 
	\sustain, 3
).play(TempoClock(140/60));
)

(command + bでサーバブートしたあとに、ex9とex10を実行します。音を止めるにはcommand + ピリオドです。)

ピッチに少しだけビブラートがかかっているのが聞こえると思います。

仕組み的には下記のような感じです。
いままで一定の音程で鳴っていた音に対して、

ピッチを一定周期で上げたり下げたりします。

SinOsc.kr(6)は、デフォルトでは-1から+1の間を往復する波形。それだとピッチの揺れを感じづらいので

SinOscで上げ下げ

5倍してあげることで、-5から+5の間を往復、

つまりfreqが中央ラの音(440Hz)の場合、440-5から440+5の間をゆらゆら揺れ動くことになります。

「足してるのになんでマイナスになるの?」と思った方、
SinOscの値は、
0→1→2→3→4→5→4→3→2→1→
0→ -1→ -2→ -3→ -4→ -5・・・(続く)
と変化していきます。(実際には小数点以下の数値の細かい変化です。)
この値を440に足していくことになるので、440というピッチは
440→441→442→443→444→445→444→443→442→441→
440→439→438→437→436→435・・・(続く)
って感じになるからです。
途中からマイナス値を足すことになっていくんですね。
440+(-1)
440+(-2)などのように。



この5の部分を大きくすればピッチの揺れ幅は大きくなり、6の部分を大きくすればビブラートの速さが増します(小さくすればゆっくりになります)。

//ex11
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, lfo;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);
	lfo = SinOsc.kr(15)*30;

	wg = Saw.ar(freq+lfo)!2;
	flt = RLPF.ar(wg, 2000) * amp * aEnv;

	Out.ar(0, flt);

}).add;
)

//ex10のPbindで鳴らしてください。



さて、このlfoの書き方ですが、以下のように書いても同じ意味です。

lfo = SinOsc.kr(6, 0, 5);

lfo = SinOsc.kr(6, mul:5);

lfo = SinOsc.kr(6).range(-5, 5);

どれも、SinOscの振幅(振れ幅)を「-5から5」に変更する書き方です。

最近僕がオススメと思うのは.rangeメソッドを使った書き方です。
SinOsc.kr(6)に対して「-5から5の範囲でね!」と指示してます。

//ex12
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, lfo;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);
	lfo = SinOsc.kr(6).range(-5, 5);

	wg = Saw.ar(freq+lfo)!2;
	flt = RLPF.ar(wg, 2000) * amp * aEnv;

	Out.ar(0, flt);

}).add;
)

//ex10のPbindで鳴らしてください。書き方は違うけどex9と同じ鳴り方です。

この書き方だと感覚的にわかりやすいですし、.range(-2, 8)みたいな感じで、ちょっと上振れたピッチのビブラートも簡単に書くことができます。



今回、LFOの波形にサイン波を使いましたが、
LFTriやSawやPulseなどなど、どんな波形でも試すことができます。



フィルターにLFO



LFOは、フィルターにもかけることができます。

//ex13
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, lfo;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);
	lfo = SinOsc.kr(6).range(-1000, 0);

	wg = Saw.ar(freq)!2;
	flt = RLPF.ar(wg, 2000 + lfo) * amp * aEnv;

	Out.ar(0, flt);
}).add;
)

//ex10のPbindで鳴らしてください。

rangeを-1000から0にしました。
これにより、もともと2000だったカットオフ周波数が1000から2000に変化することになります。

レゾナンスを深めにかけると、効果がわかりやすいです。

//ex14
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, lfo;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);
	lfo = SinOsc.kr(6).range(-1000, 0);

	wg = Saw.ar(freq)!2;
	flt = RLPF.ar(wg, 2000 + lfo, 0.3) * amp * aEnv;

	Out.ar(0, flt);
}).add;
)

//ex10のPbindで鳴らしてください。

(RLPFのレゾナンスは「1で最小、0でマックス」です。)



アンプにLFO



次に、アンプにLFOをかける例です。
SuperColliderでは、「1が最大音量」ですので、最終の出音が1以上にならないよう気をつけます。

※1を超えてしまうと、音が歪んだり、スピーカーや鼓膜を痛める原因になります。じゅうぶんに注意しましょう。
僕は突然の大音量で耳がキンキンになってしまったことが何度ももあります。

//ex15
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, lfo;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);
	lfo = SinOsc.kr(6).range(-0.1, 0.1);

	wg = Saw.ar(freq)!2;
	flt = RLPF.ar(wg, 2000) * (amp + lfo) * aEnv;

	Out.ar(0, flt);
}).add;
)

//ex10のPbindで鳴らしてください。

この例では、もともとのampは0.2なので、足していいのは0.8までです。つまり.rangeの最大値は大きくても0.8まで。
ただ、0.8でも結構な大音量になると思いますので、実験に数値入力する際には気をつけて。



複数のLFO



LFOを複数作ってあげれば、フィルターとアンプ同時にLFOをかけることも可能です。

//ex16
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, lfo1, lfo2;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);
	lfo1 = SinOsc.kr(6).range(-500, 0);
	lfo2 = SinOsc.kr(2).range(-0.1, 0.1);

	wg = Saw.ar(freq)!2;
	flt = RLPF.ar(wg, 2000 + lfo1, 0.3) * (amp + lfo2) * aEnv;

	Out.ar(0, flt);
}).add;
)

//ex10のPbindで鳴らしてください。




フィルターにエンベロープ




お待たせしました。フィルターにエンベロープを加えます。
まず、コード例をシンプルにするために、LFOのない状態に戻しました。
前回の終わりに使ったex8と同じです。

//ex8
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);

	wg = Saw.ar(freq)!2;
	flt = RLPF.ar(wg, 2000) * amp * aEnv;

	Out.ar(0, flt);
}).add;
)

アンプエンベロープを作ったときと同じ要領で、フィルターエンベロープ(fEnv)を作ります。
.rangeは、Envに対しても使えます。

//ex17
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, fEnv;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.15), gate, doneAction:2);
	fEnv = EnvGen.kr(Env.adsr(0.9, 1, 0.5, 0.15).range(0, 1000), gate);

	wg = Saw.ar(freq)!2;
	flt = RLPF.ar(wg, 1000 + fEnv, 0.3) * amp * aEnv;

	Out.ar(0, flt);
}).add;
)

//レゾナンスも設定しました。

これをex10で鳴らします。

//ex10
(
Pbind(
	\instrument, \synth1,
	\midinote, 69,
	\dur, 4,
	\sustain, 3
).play(TempoClock(140/60));
)


ピッチにもエンベロープ


同じ要領で、ピッチにもエンベロープをあてました。

//ex18
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, fEnv, pEnv;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.8), gate, doneAction:2);
	fEnv = EnvGen.kr(Env.adsr(0.9, 1, 0.5, 0.15).range(0, 1000), gate);
	pEnv = EnvGen.kr(Env.adsr(0.2, 0.3, 0, 015).range(0, 330), gate);

	wg = Saw.ar(freq + pEnv)!2;
	flt = RLPF.ar(wg, 1000 + fEnv, 0.3) * amp * aEnv;

	Out.ar(0, flt);
}).add;
)

//ex10のPbindで鳴らしてください。



複数のエンベロープを使う場合



ex17とex18では、aEnvとfEnvとpEnv、みっつのエンベロープを使っていますが、複数のエンベロープを同時に使う場合に気をつけないといけないことがあります。
doneAction:2の使い方です。

例えばex17ではfEnvのgateの右側に何も書いていません。何も書かなければdoneActionはデフォルト値(doneAction:0)で処理されます。
doneAction:0は、「エンベロープが最後まで達しても何もしない」です。

一方をdoneAction:0にしているのには訳があります。もしaEnvにもfEnvにもdoneAction:2を使った場合、どちらかリリースタイムが短いエンベロープの方が音の処理を止めてしまうからです。

例えば下記のような感じでフィルターのリリースタイムのほうが短い場合に弊害があります。

//ex19
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, fEnv;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.8), gate, doneAction:2);
	fEnv = EnvGen.kr(Env.adsr(0.9, 1, 0.5, 0.15).range(0, 1000), gate, doneAction:2);

	wg = Saw.ar(freq)!2;
	flt = RLPF.ar(wg, 1000 + fEnv, 0.3) * amp * aEnv;

	Out.ar(0, flt);
}).add;
)

実際にex10で鳴らしてみてください。
fEnvのリリースがゼロに達した時点で音の処理が止められてしまいます。(アンプエンベロープのリリースの途中なのに。)
ヘッドフォンで聴くと、プチっというノイズが発生しています。

そこで、doneAction:2を使うのはアンプエンベロープに対してのみ、にしておけばOK。
フィルターエンベロープが先に終了しても音の処理を止めないのでノイズになりませんし、最終的にアンプエンベロープ終了時に音の処理が止まるのでCPU負荷の問題もありません。

//ex20
(
SynthDef(\synth1, {
	arg gate=1, freq;
	var wg, amp=0.2, flt, aEnv, fEnv;
	aEnv = EnvGen.kr(Env.adsr(0.15, 1.0, 0.7, 0.8), gate, doneAction:2);
	fEnv = EnvGen.kr(Env.adsr(0.9, 1, 0.5, 0.15).range(0, 1000), gate);

	wg = Saw.ar(freq)!2;
	flt = RLPF.ar(wg, 1000 + fEnv, 0.3) * amp * aEnv;

	Out.ar(0, flt);
}).add;
)

//ex10のPbindで鳴らしてください。

じゃぁ、最初からaEnvもfEnvも両方ともdoneAction:0にしておけばいいんじゃない?って思いますよね。
それはそれで弊害があって、エンベロープが止まっても音の処理が止まらず、「音は止まってるのにCPUの処理が続いている」状態になってしまいます。(詳細は以前こちらの記事に書きましたので、読んでみてください。)


一見難しそうですが、使うエンベロープがフィルター用とアンプ用ならば、アンプ用のほうにdoneAction:2を設定しておけばOKです。   (音量がゼロになるのだから、音の処理を止めても問題ない、という考えです。)

でもSynthDef内にアンプ用Envが2個、なんてことになってる場合は、よく考えてdoneAction:2を設定する必要があります。(エンベロープの違う波形を二つミックスしているときなど。)
そういう場合はリリースの長い方に設定しておくべきです。


ちなみに、doneActionの種類はこちら。英語ですが、興味のある方は見てみてください。
通常使うのは0か2だと思います。
https://doc.sccode.org/Classes/Done.html



<目次へ>
https://note.com/sc3/n/nb08177c4c011


いいなと思ったら応援しよう!