类似钟鸣和Klank,Karplus/Strong “扯”也是由噪音开始的物理模型。它模仿一根被拉扯的弦,由手指或匹克的拨动触发,复制了在两端摇摆的事件。
在较早的章节,我们通过复制一个千分之一秒的采样并将它不断粘贴入一个新的文件来从噪音中创造一个周期波。Karplus/Strong使用一个延迟UGen达到了同样的效果。一个噪音的爆发被释放入一间回音室,重复和衰减的回音组成了那个爆发的周期重复,如果这个重复持续的时间够短的话,便可以将之视为一个音。
Karplus-Strong弦乐
我们首先从一个短促的噪音爆发开始(类似钟鸣)。
17.1. 噪音爆发
( { var burstEnv, att = 0, dec = 0.001; //声明变量 burstEnv = EnvGen.kr(Env.perc(att, dec), gate: Impulse.kr(1)); //包络 PinkNoise.ar(burstEnv); //噪音, 由burstEnv控制振幅 }.play )
延迟
下一步是将这个噪音爆发传送至回音室;CombL,它有如下引数:in, maxdelaytime, delayTime, decayTime, mul, add。输入将是我们刚才创建的噪音。延迟时间和最大延迟时间与本例相同。它们代表了信号被延迟(回音)的时间总量,以秒计。衰减时间是回音延续的时间。试试改变延迟时间和衰减时间看看。gate频率被设为delayDecay持续时间的倒数,因此一个新的impulse将在之前的噪音爆发消失后触发另一个噪音。
17.2. 具有延迟的噪音爆发
( { var burstEnv, att = 0, dec = 0.001; var out, delayTime = 0.5, delayDecay = 10; burstEnv = EnvGen.kr(Env.perc(att, dec), gate: Impulse.kr(1/delayDecay)); out = PinkNoise.ar(burstEnv); out = out + CombL.ar( out, delayTime, delayTime, delayDecay); //回音室 out }.play )
为什么使用噪音作为一个激励源?注意每个拉扯声都有些许差别。这是因为每个新的噪音爆发都会与前一个有着轻微的差别(不是回音,而是最初的爆发)。结果是微妙的,除了声音的自然变奏。在下例中,我为同样的噪音爆发触发器加入了一个RandSeed。当取消注释后,这粒种子将会重置数字生成器,于是你每次都将得到同样的噪音爆发。注意所有的拉扯都以同样的方式发声。
将延迟时间改为0.1(每秒十次,或10Hz),0.01(每秒100次,或100Hz),0.001(1000Hz)等等。延迟时间是我们听到的音的倒数(一秒的1/100th,100Hz)。对A440来说,该填入什么延迟时间呢?
注意在之前章节中,关于持续时间和频率我们也碰到过相同的问题。因为我们正在创建一个音高事件,因此我们会以频率来考虑问题。但CombL关于延迟的引数是一个持续时间。为达到一个特定的音高,比如说,200Hz,我们不得不为那个音的每次循环输入持续时间:一秒的1/200th。
SC可以借助reciprocal方法做这样的计算。440.reciprocal将返回一个频率为440的波的每个周期的持续时间。3.5.reciprocal将返回一个持续3.5秒的事件频率。如果我们还使用midicps将MIDI数字转换为频率,我们将提升语言的层次,同时向我们的想法更进一步。
17.3. midi到cps到延迟时间
// 这将返回一个波每次的循环周期 // 这个波的频率的MIDI值是69,或者A 440 69.midicps.reciprocal;
440.reciprocal; // 一样的
// 将这部分插入弦乐 ( { var burstEnv, att = 0, dec = 0.001; var burst, delayTime, delayDecay = 0.5; var midiPitch = 69; // A 440 delayTime = midiPitch.midicps.reciprocal; // RandSeed.kr(Impulse.kr(1/delayDecay), 111); burstEnv = EnvGen.kr(Env.perc(att, dec), gate: Impulse.kr(1/delayDecay)); burst = PinkNoise.ar(burstEnv); CombL.ar(burst, delayTime, delayTime, delayDecay, add: burst); }.play )
插入postln查看值。
为什么不试试用WhiteNoise作为噪音源呢?试试其他的噪音和波形。再试试混合一堆复杂的正弦振荡器集合。
用延迟制造复杂
早些时候,我们看到,用多通道扩展以及Mix和dup可以多么快速地充实一个简单的patch。在这样的情况下,它们是在同样的时间点被复制。延迟具备同样的效果。它们复制一个patch,但是循序地。注意以下两个例子间的微小差异。第一个例子创建了一个单独的进程,并实时复制它。第二个例子则创建了五个独立的进程。
17.4. 用延迟制造复杂
( { t = Impulse.kr(5); o = SinOsc.ar(TRand.kr(2000, 4000, t), mul: EnvGen.kr(Env.perc(0.001, 0.1), t))*0.1; Mix.ar(Pan2.ar( CombL.ar(o, 2.0, Array.fill(5, {rrand(0.2, 1.9)}) ), Array.fill(5, {1.0.rand2}) )); }.play )
与之比较
( { t = Impulse.kr(Array.fill(5, {rrand(4.0, 7.0)})); Mix.ar(Pan2.ar( SinOsc.ar(TRand.kr(2000, 4000, t), mul: EnvGen.kr(Env.perc(0.001, 0.1), t))*0.1, Array.fill(5, {1.0.rand2}) )); }.play )
合成器定义
在post窗口内,你可能已经注意到一些类似下边这样的反馈:
Synth("-429504755" : 1008)
Synth("-1768726205" : 1009)
Synth("-2052671376" : 1010)
Synth("-713843897" : 1011)
Synth("2119841241" : 1012)
引号内的数字是合成器定义的名字,冒号后的是节点数。它们在每个patch运行是自动生成。目前为止,我们使用了一个向后兼容(对于SC2用户)的捷径,允许SC在每次运行选定代码时创建一个专制的(arbitrary)合成器定义。即使你运行相同的代码两次,它仍会创建一个新的合成器,当然,这是低效的。如果能重播一个已存在的定义将会更好。试着运行以下几行代码,用你在post窗口看到的数字替代下例中的数字(你可以使用复制-粘贴,或者移去冒号和节点数,然后运行post窗口内的陈述)。
17.5. 播放一个synthDef
//首先 {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play //你将会在post窗口看到类似下边这样的东西 //Synth("1967540257" : 1012) //然后, 用你在post窗口内看到的数字替代1967540257 Synth("1967540257");
之前我们一直使用cmd+. 来停止每个patch,这会停止全部进程。如果一个合成器被分配到一个变量,你可以用free方法精确地停止它。单独运行下边两行;同样的,用你在post窗口内看到的数字替代1967540257。
17.6. 停止一个synthDef
a = Synth("1967540257"); a.free;
我们可以通过将原始patch分配给一个变量来忽略掉寻找其名字的过程。我们同样可以运行多个例子,然后单独或全部停止它们。
17.7. 运行一个synthDef
//随意运行一个或几个或全部 a = {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play b = {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play c = {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play d = {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play //停止一些 a.free; b.free; //或全部停止 c.free; d.free
SynthDef对象允许你为一个patch命名并添加引数。一个引数类似一个变量并被用于向函数传递值。你可以在第一次创建合成器或者在它运行后设置引数。下边第一个例子是之前我们惯用的写法。另一个例子用合成器定义来写,并使用了名字和引数。
17.8. SynthDef
(//初始patch { var rate = 12, att = 0, decay = 5.0, offset = 400; var env, out, pan; pan = LFNoise1.kr(1/3); env = EnvGen.kr(Env.perc(att, decay)); out = Pan2.ar( Blip.ar(LFNoise0.ar(rate, min(100, offset), offset), (env)*12 + 1, 0.3), pan)*env; out }.play )
//SynthDef (命名它)和引数 ( SynthDef("SH", { arg rate = 12, att = 0, decay = 5.0, offset = 400; var env, out, pan; pan = LFNoise1.kr(1/3); env = EnvGen.kr(Env.perc(att, decay), doneAction: 2); out = Pan2.ar( Blip.ar(LFNoise0.ar(rate, min(100, offset), offset), (env)*12 + 1, 0.3), pan)*env; Out.ar(0, out) }).play )
观察post窗口,你将看到第一例的那串长数字现在已被名字“SH”所替代。
使用SynthDef后,一些之前自动完成的东西在这里需要明确一下。首先是使用Out.ar分配一个输出母线(bus,以后说明)。另外一点是当一个patch完成演奏后停止它。我们使用包络生成事件,但你可能已注意到CPU和UGen们仍是一样的。那是因为即使包络终止了我们听到的声音,但patch却仍在运行。当包络终止后,有几可能性。默认是什么也不做。把它改为doneAction: 2将告诉服务器释放已完成UGen的进程占用。(要看其重要性,试着在运行下例前注释掉这一行。观察服务器CPU占用的增加以及最后的崩溃。)
现在,代码已经被载入服务器,我们可以使用set控制值运行若干个乐器的拷贝。引数被置入一个数组并以斜杠开始,值:[\rate, 10, \offset, 200]。逐次运行一下诸行。(在SC中,这很方便,因为你运行完一行代码后,游标将跳动到下一行,因此,你仅需一直按enter即可。)
17.9. SH的多节点(node)
//具备不同引数的三个"SH"独立节点 //逐一运行以下三行然后停止 a = Synth("SH", [\rate, 10, \offset, 200]); b = Synth("SH", [\offset, 400, \att, 3.0, \decay, 0]); c = Synth("SH", [\rate, 30, \offset, 2000]); //让它们自己消失或用以下诸行停止它们. a.free; b.free; c.free;
//改变一个已存在节点的参数. 逐一运行一下诸行. a = Synth("SH", [\rate, 23, \offset, 30, \decay, 20]); a.set(\offset, 1000) a.set(\offset, 300) a.set(\offset, 800) a.free;
//具备引数的两个节点 a = Synth("SH", [\rate, 7, \offset, 200, \decay, 20]); b = Synth("SH", [\rate, 23, \offset, 1200, \decay, 20]); a.set(\offset, 40) b.set(\offset, 1000) a.set(\offset, 800) b.set(\offset, 600) a.set(\offset, 1200) b.set(\offset, 50) a.free; b.free
你同样可以使用引数数字跟着值,或字符串跟着值的方式设置引数,就像下边这样。
17.10. 传递引数的语法
//每行代表的都是同样的东西 a = Synth("SH", [\rate, 10, \offset, 200]); a = Synth("SH", [0, 10, 3, 200]); a = Synth("SH", ["rate", 10, "offset", 200]);
最后,你可以指出一个在控制与改变间的时滞,以此获得一个平滑的过渡。
17.11. 控制改变间的过渡时间
//SynthDef, 引数, 过渡 ( SynthDef("SH", { arg rate = 12, att = 0, decay = 5.0, offset = 400; var env, out, pan; pan = LFNoise1.kr(1/3); env = EnvGen.kr(Env.perc(att, decay), doneAction: 2); out = Pan2.ar( Blip.ar(LFNoise0.ar(rate, min(100, offset), offset), (env)*12 + 1, 0.3), pan)*env; Out.ar(0, out) }, [0.5, 0.1, 0, 4] //为以上每个引数过渡 ).play ) a = Synth("SH", [\rate, 6, \decay, 20, \offset, 200]); a.set(\rate, 18); a.set(\offset, 1000);
我们可能可以改变初始patch的参数并再次运行它。但那不够灵活。现在使用SynthDef与引数,允许我们用不同的控制值来自动创建乐器的副本。
现在,在我们的服务器中,已经载入了一个合成器定义,我们可以建立在作曲进程的基础上发送一系列的“play”命令。为了重复这些命令,我们将使用一个循环。
方法loop循环一个函数。Task允许你暂停循环(不要尝试一个没有暂停的循环,或者至少在你这么做之前保存你的工作。)。每次循环,一个新的SH都被用一个随机的值作为引数(或控制)所创建。
17.12. SH的多节点
( r = Task({ { Synth("SH", [ \rate, exprand(3.0, 22.0), \decay, rrand(0.5, 15.0), \att, [0, rrand(0, 3.0)].choose, \offset, rrand(100, 2000)]); rrand(1.0, 5.0).wait; //每两次重复间的等待时间 }.loop; //重复这个函数 }).play ) r.stop;
试着改变本例内的任意值。同样试试插入postln监视那些值(比方说,rrand(1.0, 5.0).wait.postln)。
最后,一旦你将你的乐器调教到值得保存的地步,你可以将它写入你的硬盘(SC文件夹内的synthdefs文件夹)。下次运行SC,你将能够通过名字运行这个合成器(将之送入正确的服务器是重要的,无论哪个正在运行。如果你点击正在运行的服务器上边的default按钮,它将去到那个服务器。或者你可以使用目标引数将之送给一个或另一个。我在下例中包含了boot表达式以说明上边说的第二种方法)。
17.13. SH的多节点
(//储存文件并在服务器"s"内载入 SynthDef("SH", { arg rate = 12, att = 0, decay = 5.0, offset = 400; var env, out, pan; pan = LFNoise1.kr(1/3); env = EnvGen.kr(Env.perc(att, decay), doneAction: 2); out = Pan2.ar( Blip.ar(LFNoise0.ar(rate, min(100, offset), offset), (env)*12 + 1, 0.3), pan)*env; Out.ar(0, out) }).load(s) ) //现在退出SC, 在synthdefs文件夹内查找"SH.scsyndef" //运行SC并运行以下两行 s = Server.internal; s.boot; a = Synth("SH", [\rate, 10, \offset, 200], target: s);
当你编译了一个牛逼的乐器库,类似KSpluck,你有可能忘记它们有什么控制母线,输出母线以及引数。你可以用下列代码查找所有信息。
17.14. SynthDef浏览器
( SynthDescLib.global.read; SynthDescLib.global.browse; )
下边是一个SynthDef里的KSpluck,然后是一个循环例程(routine)。
17.15. KSpluck SynthDef (EnvGen, Env, perc, PinkNoise, CombL, choose)
( //首先载入合成器并存盘 SynthDef("KSpluck", { arg midiPitch = 69, delayDecay = 1.0; var burstEnv, att = 0, dec = 0.001; var signalOut, delayTime; delayTime = [midiPitch, midiPitch + 12].midicps.reciprocal; burstEnv = EnvGen.kr(Env.perc(att, dec)); signalOut = PinkNoise.ar(burstEnv); signalOut = CombL.ar(signalOut, delayTime, delayTime, delayDecay, add: signalOut); DetectSilence.ar(signalOut, doneAction:2); Out.ar(0, signalOut) } ).play; ) ( //然后运行这个回放任务 r = Task({ {Synth("KSpluck", [ \midiPitch, rrand(30, 90), //选择一个音高 \delayDecay, rrand(0.1, 1.0) //选择持续时间 ]); //在下一个事件前选择一个等待时间 [0.125, 0.125, 0.25].choose.wait; }.loop; }).play(SystemClock) ) //停止之 r.stop;
右通道在八度上翻倍(MIDI,+12)。试着加入一个引数,以允许你翻倍其他的音程(五分之一,四分之一,三分之一,等等)。改变等待时间的选择。为什么在本例中我要使用0.125和0.25?增加postln以检查变量。加入一个链接MIDI音高与衰减时间的陈述,使得高音符短衰减,低音符长衰减。
仔细聆听每个音的特征及每一个attack。这明摆着是同一件乐器,然而每个音符仍然有轻微不同的音色。特点改变了是由于每次造成pluck的噪音暴发都是新的,它会有不同的波形。传统合成器预置缺乏这样的复杂性,这是自然乐器固有的。
练习:Karplus-Strong Patch
在K-S patch中,midiPitch的引数由一个“合法”音调选择的数组设置。然后它被添加到一个八度选择的数组中。变量art(结合)取代了delayDecay,因为它被用于缩短或加长每个音头长度。注意突发包络为our提供一个立体声(数组)信号。即使你听到一个音,左、右声道应当稍有不同。delayTime同样用一个立体声数组设置:右声道高八度(MIDI数字)。整个patch通过用LFNoise1控制滤波曲线的RLPE传递。数组wait为偏差选择使用了一个快速的方法:载入骰子。因为有五个0.125和一个1的实例,因此0.125被选中的机率有86%。
尝试改变midiPitch数组到多样的音阶上:全音阶,自然音阶,半音阶,八音音阶,四分之一音阶等等。尝试为patch的其他方面增加立体声数组,比如,LFNoise率或滤波截止。
17.16. 练习: K-S 拉扯 (EnvGen, PinkNoise, LFNoise1, Out, DetectSilence)
//载入这个定义 ( SynthDef.new("KSpluck3", { //Ugen函数开始 arg midiPitch, art; var burstEnv, att = 0, dec = 0.01, legalPitches; //申明变量 var out, delayTime; delayTime = [midiPitch, midiPitch + 12].midicps.reciprocal; burstEnv = EnvGen.kr(Env.perc(att, dec)); out = PinkNoise.ar([burstEnv, burstEnv]); //Noise burst out = CombL.ar(out, delayTime, delayTime, art, add: out); //Echo chamber out = RLPF.ar(out, LFNoise1.kr(2, 2000, 2100), 0.1); //Filter DetectSilence.ar(out, doneAction:2); Out.ar(0, out*0.8) } ).play; ) //然后运行这个例程 ( r = Task({ {Synth("KSpluck3", [ \midiPitch, [0, 2, 4, 6, 8, 10].choose + [24, 36, 48, 60].choose, \art, [0.125, 0.25, 0.5, 1.0, 2.0].choose ]); //选择一个间隔时间 [0.125, 0.125, 0.125, 0.125, 0.125, 1].choose.wait; }.loop; }).play(SystemClock) )