SC:总线、节点和组:把东西连起来

免责声明

这是非常困难的一章。请关掉电视先。

总线(bus)和节点(node)是SC3的新概念,并且有很多内容值得一说。我将会把重心从了解足够的代码知识转向到创作的层面。我的目标是让你对总线、节点和组(group)足够的熟悉,可以让你们用它们做一些简单的patch,或者读懂示范代码和帮助文档。

在进入讨论前,一个提醒:下边的代码对记忆合成器定义的结构很有帮助,它同样可以用于学习、复习总线分配。


18.1. 浏览合成器定义

( 
SynthDescLib.global.read; 
SynthDescLib.global.browse; 
)

合成器定义

创建合成器定义就像建立一个小合成器:制造声音的虚拟金属盒子。就像真的硬件中的架线,一旦传送到服务器,便不能被更改。看下边的例子。它将一直用LFNoise作为频率控制,引数也将永远是10, 15, 400, 和800。这就像一个我们制作的在封闭的盒子里没有控制与输入的合成器一样。我们可以重做代码,然后它就会是一个不同的合成器。

18.2. 第一个Patch (play, SinOsc, LFNoise0, .ar)

{SinOsc.ar(LFNoise0.ar([10, 15], 400, 800), 0, 0.3)}.play

像下例这样,通过用引数替代固定值,并使用set改变那些值,我们可以获得更大的灵活性。现在,在我们的虚拟盒子上,至少有了可以改变参数的旋钮了。

18.3. 第一个合成器定义

//SynthDef (命名它) 和引数 
( 
SynthDef("RandSine", 
{ 
arg rate = 9, scale = 300, offset = 600, pan = 0, out; 
out = Pan2.ar(SinOsc.ar(LFNoise0.ar(rate, scale, offset), mul: 0.3), pan); 
DetectSilence.ar(out, doneAction:2); 
Out.ar(0, out) 
}).load(s) 
) 

// 单独运行下列诸行 
a = Synth("RandSine", [\pan, -1, \rate, 8]); 
b = Synth("RandSine", [\pan, 1, \rate, 13]); 
b.set(\offset, 2000); 
a.set(\rate, 20); 
b.set(\rate, 6); 
a.set(\scale, 550);

但内部的架线,RandSine的流程图仍将是一样的。它将永远用LFNoise作为一个控制。

要改变控制源为其他东西,LFNoise1,音序器,或S&H,我们应该用那些控制重写patch:新建一个盒子,让我们姑且这么说。这是SC2的局限之一。模块化的引入将更高效,即从SinOsc中建立LFNoise0, LFNoise1, 以及 S&H 为单独的patch,然后将我们相用的任意一个插入SinOsc。SC3的结构是遵循这一考量而设计的。这允许你一次运行若干个patch,甚至是运行同样的patch若干次,并且它允许你使用总线将这些patch连接在一起。

所有的patch都具备输入与输出。LFNoise0有一个输出,SinOsc将这个输出作为自己的输入。我对一个输出使用过传送、控制、源以及调制,对一个输入使用过接受、滤波和载体。但不管你如何使用它,总有从一个装置得到的输出以及对另一个来说的输入。目前为止,我们是使用嵌套的方式将它们联系在一起。现在,我们将学习如何用总线来做这个事。

在混音器(mixer)中,总线是很普通的,它们被用于将信号按特定的线路传递。它们常不以任何方式改变信号,它们仅仅是路由(route)它。它们像城市穿梭巴士。一个工人登上一辆巴士,而后两三个工人加入这条线路最后到达各自的目的地。它们可以全部在一个地方下车,或分别在不同的地方下车。很多条线路的巴士可以带人们到不同的工厂。对于信号,我们可以做同样的事情。一个UGen的输出将是人群,目的地可能是一个混响部件。

有学生曾问我,在将信号路由到一条总线后,它去哪了。答案是哪也没去。它们并非真的去了哪个特定的地方。它们仅是patch点或连线,像城市穿梭巴士一样,认识到它们是由你定义的这点是很重要的:谁上车以及他们去哪。

音频及控制总线

在SC中,有两类总线:音频类和控制类。在早前的例子中,scope窗口显示了12条正弦波。它们在12条总线上演奏。我们能听到最上边的两条,因为它们被路由到了电脑的输出上。而其他的,则连接到了我们无法听到的输出上。

使用Out.arIn.ar做音频总线交互连接。控制总线使用Out.krIn.kr。共有128条音频总线和4096条控制总线。如果你习惯于经典工作室,你可以想象一个有128个插口的插线板(或集线器)。插线板的一端标注“In.ar”,另一端标注“Out.ar”,而另外一块插线板则有4096个插口。它们连接的是什么?目前还没有,除了:在绝大多数的系统内,音频母线0和1通常是被路由到你硬件(电脑音频输出)的左右通道。可以用{Out.ar(0, SinOsc.ar)}.play{Out.ar(1, SinOsc.ar)}.play来证实这一点。同样地,2和3默认是连接到硬件的输入。运行{In.ar([2, 3])}.play证实这一点(使用耳机以避免反馈)。如果尝试{In.ar(0)}.play,你将听到你的扬声器。同样,{Out.ar(2)}.play将向你的内置麦克传递信号。如果你做{任何事}.play,将自动路由到输出0和1.因此,{In.ar([2, 3]}.play{Out.ar([0, 1], In.ar([2, 3])}.play是相同的。

控制总线没有默认连接。

为说明控制和音频总线,运行下边的代码。记住一个数组将扩张到与其元素数量同样多的通道,这点可以在scope窗口内被证明。我们仅能听到前两个通道(除非你有一个多通道接口)。我们听不到任何控制总线。注意,控制scope是蓝色的,音频scope是黄色的。

18.4. 音频和控制总线

( 
{ 
   [ 
      SinOsc.ar, 
      PinkNoise.ar, 
      LFNoise1.ar, 
      LFNoise0.ar, 
      LFTri.ar, 
      WhiteNoise.ar 
   ]*0.4 
}.scope 
)
// 控制总线 
( 
{ 
   [ 
      SinOsc.kr(100), 
      Dust.kr(50), 
      Impulse.kr(78), 
      LFNoise0.kr(100), 
      LFNoise1.kr(100), 
      WhiteNoise.kr 
   ]*0.4 
}.scope(zoom: 10) 
)

以下是将第一个patch分解为两个部件的展示。SinOscLFNoise0Out对象表明信号路由到的总线,第一个引数是总线编号。你可以输入一个数字得到单声道信号,或一个数组做立体声。但即使你并未输入一个数组,正确的总线编号也将被自动分配。如果patch是一个立体声信号(比如,使用多通道扩展),Out总线编号是0,那么实际上它将使用0和1。如果信号是四通道的,总线输出是4,那么它将使用4,5,6,7。确保它们值间没有冲突就是你的责任了。

LFNoise0模块被送至控制总线9(记住总线分音频和控制,.kr方法将使用控制总线)。我把有muladd的patch写在LFNoise0之外,因此它将在scope上显示。

在第三例中,当我隔离LFNoise后,你在两个通道内同时听到它,但仅是一系列的爆裂声。当SinOsc移动到总线5,你将听不到它,但可以在scope上看到它。在最后一例中,你仍然听不到声音,因为控制总线9并未连接一个音频输出。为什么是总线5和9呢?为说明总线是任意的,你可以使用你想要的任意总线,只要你将重点连接到相同的总线编号上。

18.5. 分配总线

//整个patch 
{SinOsc.ar(LFNoise0.kr([10, 15]) * 400 + 800, 0, 0.3)}.scope
//仅仅是SinOsc 
{SinOsc.ar(800, 0, 0.3)}.scope
//仅仅是LFNoise0 
{LFNoise0.ar([10, 15])}.scope
//仅仅是Sine附到音频输出0 
{Out.ar(0, SinOsc.ar(800, 0, 0.3))}.scope;
//仅仅是Sine附到音频输出5 
{Out.ar(5, SinOsc.ar(800, 0, 0.3))}.scope(16);
//仅仅是LFNoise def 
{Out.kr(9, LFNoise0.kr([10, 15], 1, 0))}.scope(16, zoom: 10)

现在,两个合成器同时在跑。一个在5号口产生音频,另一个在9号控制口产生一个控制率信号,等待我们告诉它动身去往何处。现在我们需要它去SinOsc工厂。要让它到哪里,我们使用In.kr(不是In.ar,这将让它连接到音频总线上)。下例中,In.kr的第一引数为总线编号(9),第二引数为通道数(2)。为什么是9?因为那时LFNoise连接的地方。比方说,它搭乘的巴士。为什么是两个通道?因为LFNoise是立体声信号(频率引数是数组),所以它使用9和10。In.kr从控制总线9和10读取,这是LFNoise0连接并用于作为一个控制的地方。SinOsc随后播放到音频总线0和1,因为现在它也是一个立体声信号了。

如果你还没有这么做,按下cmd+. 停止一切进程。

在这个例子中,我颠倒了数序,因此你可以先开启LFNoise0。观察服务器上CPU占用的增加,一个由大约5个UGen组成的合成器被创建,尽管我们还无法听到它。而后运行SinOsc听两个patch同时运行。然后反过来,先运行SinOsc,而后再开启LFNoise。

18.6. 用总线将patch串起来

{Out.kr(20, LFNoise0.kr([8, 11], 500, 1000))}.scope
{Out.ar(0, SinOsc.ar(In.kr(20, 2), 0, 0.3))}.scope

下一步是为总线编号设置一个引数,然后我们便可实时改变它了。为什么要在合成器运行时改变总线编号?为了将它连到一个不同的控制上,我已把它添加如下,分配到不同的总线上。你同样可以使用一个引数分配控制总线。但在本例中还不必要这么做。你可以在圆括号内一次定义全部合成器。当它们运行时,注意服务器窗口显示合成器:4个左右的UGen。我们仅听到其中之一:没有控制的正弦波。另外两个则在控制总线上,我们听不到。运行每个a.setPatchableSineIn输入分配一个新的总线编号,因此,连接任意一个合成器控制即连接到它的总线并改变频率的控制。

18.7. 动态总线控制的patch

(
//开启全部合成器 
SynthDef("LFN0Control", 
   {Out.kr(20, LFNoise0.kr([8, 11], 500, 1000))}).play(s); 

SynthDef("SineControl", 
   {Out.kr(22, SinOsc.kr([3, 3.124], mul: 500, add: 1000))}).play(s); 

SynthDef("MouseControl", 
   {Out.kr(24, MouseX.kr([100, 200], 1000))}).play(s); 

a = SynthDef("PatchableSine", {arg busInNum = 0; 
   Out.ar(0, SinOsc.ar(In.kr(busInNum, 2), 0, 0.3))}).play(s); 
) 

a.set(\busInNum, 20); //设置到LFNoise0 
a.set(\busInNum, 22); //设置到SineControl 
a.set(\busInNum, 24); //设置到MouseControl

为什么控制总线要编号20,22,24?试着使用20,21,22看看会发生什么。

让三个控制一次性同时跑是低效的,因此这样也没问题:

18.8. 用总线、动态控制源连接合成器

a = Synth("PatchableSine", [\busInNum, 20]); b = Synth("LFN0Control"); 

b.free; b = Synth("SineControl"); a.set(\busInNum, 22); 
b.free; b = Synth("MouseControl"); a.set(\busInNum, 24);

把若干控制连入一个单独的总线,或用一个总线控制若干合成器是可能的。在这个例子中,我使用Out.kr(0)以说明它与Out.ar(0)是分开的。注意,所有控制都连在总线0上,因此它们都控制着接收者。在第二例中,所有接收者都连在一些任意总线上,因此它们都被那个合成器控制着(听起来可能像一个合成器,所以先听左耳塞然后再听听右耳塞。)。

18.9. 在一条总线上的若干控制

( 
SynthDef("SendControl1", { 
   Out.kr(0, SinOsc.ar(0.3, mul: 1000, add: 1100))}).send(s); 
SynthDef("SendControl2", {Out.kr(0, LFNoise0.ar(12, 200, 500))}).send(s); 
SynthDef("Receive", {Out.ar(0, SinOsc.ar(In.kr(0)))}).send(s); 
) 

Synth("Receive"); 
Synth("SendControl1"); 
Synth("SendControl2"); 

//或者 

Synth("Receive"); 
Synth("SendControl2"); 
Synth("SendControl1"); 

//或者 

( 
SynthDef("SendControl", {Out.kr(1275, LFNoise0.ar(12, 200, 500))}).send(s); 
SynthDef("Receive1", { 
   Out.ar(0, RLPF.ar(PinkNoise.ar, In.kr(1275), 0.05))}).send(s); 
SynthDef("Receive2", {Out.ar(1, SinOsc.ar(In.kr(1275)))}).send(s); 
) 

Synth("Receive1"); 
Synth("Receive2"); 
Synth("SendControl"); 

//用command+. 停止一切。 然后试着反过来运行它们。 

// 打开浏览器检查总线分配 

( 
// 合成器定义浏览器 
SynthDescLib.global.read; 
SynthDescLib.global.browse; 
)

节点

它能运作于任何顺序因为它是一个控制总线,在音频总线方面,顺序也没关系。当你创建一个合成器,SC将之存储于一个节点或存储单元。每创建一个新的合成器便产生一个新的节点(当你开启一个新的合成器,你会在post窗口内看到它们)。节点是彼此相连的。计算机了解顺序,并可以在列表头或尾或其他任何地方增加一个新的节点。请注意节点编号与节点顺序是没有联系的。编号仅仅是一个地址,并不影响或参考节点的顺序。在目前我们所完成的例子中,这个连接流程仍处于幕后。但现在我们正在将合成器们连起来,因此我们不得不采取控制。我们并非在探讨合成器以什么顺序传递入服务器,我们讨论的是它们被创建以及在服务器生成声音的顺序。

创建如下两个合成器定义然后运行下列例子。注意如果你先运行Saw,然后滤波器,将没有声音产生。

18.10. 节点顺序, 头, 尾

( 
//定义的顺序无关紧要 
SynthDef("Saw", { arg filterBus = 16; 
   Out.ar(filterBus, LFSaw.ar([60, 90])) 
}).send(s); 

SynthDef("Filter", {arg filterBus = 16; 
   Out.ar(0, RLPF.ar(In.ar(filterBus, 2), LFNoise0.kr(12, 300, 350), 0.2)) 
}).send(s); 
) 

//行不通 
Synth("Saw"); //源 
Synth("Filter"); //滤波器 

//可行 
Synth("Filter"); //滤波器 
Synth("Saw"); //源 

//或者 
Synth("Filter"); Synth("Saw");

在上例中,我们将一个音源连接到一个滤波器。我为所有信号使用了音频总线(Out.ar)因为它们均需是音频率。音源从16号口送出。我不能使用0,1,2,3因为它们连接在我的硬件输入、出上。那为什么不用5呢?因为我可能在最后将之用于支持8进8出(0~15)的机器上,所以我移动到了16(尽管我同样可以使用60)。请参阅下边的动态总线分配。

上边两例唯一的不同点就在于执行的顺序。在第一例中,Saw(音源)先开启,然后是Filter(滤波器)。第二例则反之。

当你创建一个新的合成器,它会被赋予一个节点id并被放到列表的头部。所有原先存在于列表的合成器都被向下移动。因此如果你先执行Saw,它将被赋予一个节点并放置到头部。而后你执行Filter,它同样被赋予一个id,并占据头部而将Saw挤到尾部。但信号是从头流向尾的,因此Saw必须在头部,而Filter应该是尾。

我将头想作是流程图的顶端,因为我们习惯于通过向下流动的信号来图解一个patch。不幸的是,这是令人迷惑的,因为它与执行的顺序是相反的,看下例。

18.11. 执行顺序,节点顺序
这个执行顺序

Synth("Receive"); 
Synth("SendControl1"); 
Synth("SendControl2");

导致了这些节点被列印到post窗口
Synth("Receive" : 1000)
Synth("SendControl1" : 1001)
Synth("SendControl2" : 1002)

但他们的顺序,从头到尾,从上到下是:
Group(0)
   Group(1)
      Synth 1002 //头
      Synth 1001
      Synth 1000 //尾

因此,执行顺序与节点列表是相反的,从上到下(从头到尾)。

这是我努力记住的:执行的顺序是先输入,后输出;先接收,后发送;先终点,后离开。这里我的意思是,执行将要从它的输入总线接收其他合成器信号的合成器,然后再执行发送一个将被使用的信号的合成器。(这么说也许会更清楚一些:在把工人们放到巴士之前,你得先建好你的工厂。如果在一个重点存在前,你就将它们放进巴士,你便无法告诉它们在哪下车。荒唐的记忆法,我得承认,但这与所有奶牛都在晚餐前吃草差不多。)

如果你想将它们想做头和尾:输入是尾(底部),输出是头(顶部),因为信号是从上往下流淌的。

执行顺序:输入然后输出。节点顺序:输入在尾,输出在头。

当使用Synth时,节点默认是被放在组(group)的头部。但你同样可以精确的分配一个节点到头或尾。下例中,Saw被放到头部。第二例将Filter放到尾部而不是头部,放到头部的话将使Saw移动到尾部。使用tail和head意味着执行顺序无关紧要。

在使用head和tail时,第一引数是放置合成器的组,头尾就是它的。我们目前还未创建任何组,因此他自己将自己放在服务器上,它默认的第一个节点id是0,默认组是1。

18.12. 节点顺序, 头, 尾

Synth.head(s, "Saw"); 
Synth.tail(s, "Filter");

相同的东西

Synth.tail(s, "Filter"); 
Synth.head(s, "Saw");

我可以想象你的脑子已经满了。出去透透气吧。我等你回来。

ok,更混淆的来了。

动态总线分布

在这前那个例子中,我使用了16号总线以为一个8通道的接口留出空间,8进,8出。如果你不知道你已经有多少个输出、入口怎么办呢?自然地,有一个管理总线的自动化方法。Bus.controlBus.audio可被用于返回下一个可用的控制与音频总线的目录。Bus.index返回那个目录。下边第一个例子将在post窗口返回:Bus(internal, audio, 4, 1),意味着一个在目录第四位位于internal服务器的音频总线被返回。为什么是4?因为0~3已被电脑的输入和输出所占用,因此4是下一个可用的总线。方法free释放那个总线,然后被重新分配。接下来我重新分配一个两通道总线到c,接着是一个两通道总线到b。它们应当是4,5和6,7,但你的里程表可能不同:它们实际上将是任何你系统内空闲的口。然后我向这些分配的端口分别发送一个SawSinOsc,使用12通道的scope,以使你可以看到它们全部。注意它们都是立体声总线,但Saw是一个单声道patch。尽管如此,第二个c总线仍是可用的。

请确保在每个例子运行完毕之后释放每条总线。

18.13. 总线分配及重分配

b = Bus.audio; 

b.index; 

b.free; 

c = Bus.audio(s, 2); 
b = Bus.audio(s, 2); 

{Out.ar(c.index, Saw.ar)}.play; 
{Out.ar(b.index, SinOsc.ar([500, 1000])).scope(8) 

b.free; c.free;

如果你bypass(设旁路)Bus并仅输入一个目录编号,它将不会被注册到一个总线分配。这被称为硬接线(hardwiring)。为说明这点,我将首先用一个硬接线的哑巴(dummy)合成器接管一些总线。而后使用Bus获得下一个可用的总线,列印它的目录,然后使用它的目录创建一个新的合成器。注意将它与发送到第一个SinOsc使用的总线上。

18.14. 总线分配和再分配

{Out.ar(4, SinOsc.ar([100, 200, 300, 400]))}.scope(8); 

b = Bus.audio(s, 2); 

b.index; // 不管上边的代码是什么,应该仍为4 

{Out.ar(b.index, Saw.ar([800, 1000]))}.scope(8); // 添加到正弦波上 

b.free;

紧接着在运行下边这堆代码的同时观察post窗口内的反馈。

18.15. 总线分配

a = Bus.audio(s, 2) // 获取接下来可用的两个通道 
b = Bus.audio(s, 1) // 获取接下来的一个通道 
c = Bus.audio(s, 2) // 再获取两个 
c.index // 列印c 
a.index // 列印a 
a.free // 释放a 
b.free // 释放b 
d = Bus.audio(s, 1) // a and b are now free, so these 
e = Bus.audio(s, 2) // should take over those indexes 
a = Bus.audio(s, 2) // reallocate a and b, will probably 
b = Bus.audio(s, 1) // 9 through 11 
[a, b, c, d, e].postln; // 列印全部 
s.scope(14); // 开启一个scope 

// 现在开启一些合成器. 我将把它们全部混至总线0,1。 
// 因此我们将首先开启它。记住 
// 先入, 后出, 先收, 后送. 

{Out.ar(0, Mix.ar(In.ar(2, 12))*0.1)}.play 
{Out.ar(a.index, SinOsc.ar)}.play 
{Out.ar(b.index, SinOsc.ar(1000))}.play 
{Out.ar(c.index, Saw.ar([400, 800]))}.play 
{Out.ar(d.index, Pulse.ar(200))}.play 
{Out.ar(e.index, [Saw.ar(500), FSinOsc.ar(900)])}.play 
// 你在没有将一条总线分配给一个变量的情况下仍能获得一条总线 
// 这样仅仅将使你无法在之后释放它. 
{Out.ar(Bus.audio.index, Saw.ar(2000))}.play 
// 你可以将两个信号写入一条总线 
{Out.ar(a.index, Saw.ar(2000))}.play 

[a, b, c, d, e].do({arg each; each.free}) // 释放全部

我不得不承认,对于总线我并非经验丰富。我的直觉是,动态分配可能是不可或缺的或另一个不必要的复杂的层面。你自己做决定。

使用总线提高效率

总线可以使你的patch更高效。你可以为若干合成器使用一个单独的控制,或将若干合成器发送至一个单独的全局fx。若一个fx被建入一个合成器,复制这个合成器同样会复制那个fx。将这个patch看作一个触发器和一个混响。运行它,并查看CPU占用以及服务器窗口内UGen的数量。运行一个合成器的拷贝不太遭,但尝试使用Synth(“inefficient”)运行六个它们,再看CPU占用以及每个合成器UGen的增涨。(注意因为触发器是随机的,因此你可能需要让它运行一会才能听到声音。)

有一个运行同一行若干次的小技巧:把Synth(“inefficient”)打到代码窗口的最后一行,不要按回车。这时,当你按下enter的时候,光标仍然会停留在这一行。每按下一次enter,一个新的合成器将被同时创建。

18.16. 低效的patch

( 
SynthDef("inefficient", 
{ 
var out, delay; 
out = 
   SinOsc.ar(LFNoise0.kr(15, 400, 800), mul: 0.2) 
   * 
   EnvGen.kr( 
      Env.perc(0, 1), 
      gate: Dust.kr(1) 
   ); 

delay = CombC.ar(out, 0.5, [0.35, 0.5]); 
out = Pan2.ar(out, Rand(-1.0, 1.0)); 
Out.ar(0, (out + delay)) 
}).play; 
) 

// Type this into a new window with no return and keep pressing enter
Synth("inefficient")

如果你运行六个版本,你会得到六个混响(CombC)和六个触发器(Dust.kr)。在我的机器上,这时UGen总数是96而CPU占用为16%。当我们拆分这个patch后,触发器,音源(SinOsc)以及混响便是完全独立的单元,然后我们可以用一个触发器和一个混响得到几乎一样的效果。

18.17. 使用总线更高效的模块化方法

( 
//一次传递定义全部合成器 
SynthDef("singleTrigger", { 
   Out.kr( 
      //输出母线是1560~1565 
      LFNoise0.kr(5, mul: 4.0, add: 1563).div(1), 
      Dust.kr(6) 
   ) 
}).send(s); 

SynthDef("source", 
{ arg trigIn, rate; 
var out, delay; 
out = 
   SinOsc.ar(LFNoise0.kr(rate, 400, 800), mul: 0.1) 
   * 
   EnvGen.kr( 
      Env.perc(0, 1), 
      gate: In.kr(trigIn) 
   ); 

out = Pan2.ar(out, Rand(-1.0, 1.0)); 
Out.ar(16, out) 
}).send(s); 

SynthDef("singleReverb", 
{ 
var signal; 
signal = In.ar(16, 2); 
   Out.ar(0, (signal + CombC.ar(signal, 0.5, [0.35, 0.5]))) 
}).send(s); 
) 

// 开启触发器 
Synth("singleTrigger", [\rate, 1/4]) 

// 开启混响 
Synth("singleReverb") 

// 开启音源 "监视" 触发器总线4-9 
// 先开启4和9以确保其工作正常 (用一个 
// 快和慢的rate确保你可以保持追踪) 
Synth("source", [\trigIn, 1560, \rate, 4]) 
Synth("source", [\trigIn, 1565, \rate, 25]) 
Synth("source", [\trigIn, 1561, \rate, 10]) 
Synth("source", [\trigIn, 1562, \rate, 8]) 
Synth("source", [\trigIn, 1563, \rate, 17]) 
Synth("source", [\trigIn, 1564, \rate, 7])

在这个patch中,我首先定义了一个在六条总线(1560~1565)活动的触发器。为什么是第1560呢?因为它们可以是任何东西。LFNoise生成1559~1566间的值,用div(1)转为整数,并被用于“散播”出总线编号。想象在总线1560~1566间,每个口都有一个LED指示灯,当LFNoise0在它们中间不规律跳跃时,每次亮起一个。它们将担当触发器的角色。当LFNoise0“撞到”总线任一,它便向那条总线发送一个触发器。

音源合成器从一条总线获得它的触发器,由引数trigIn设置。当我们运行每个音源的拷贝,我们都分配其“监视”总线中的一条。当LFNoise0触发器连接到,比如说,总线1563,随后监听那条总线的音源将被点燃。

我的机器显示79UGen以及6%;低效版本CPU占用的一半。

何时将一个patch打散为组件?何时不这么做?这里,混响是完全一样的。如果你需要每个延迟时间不同,比如说,如果你挑剔的需要它们不同,则它们应是patch的一部分,于是每个合成器都将有一个唯一的混响。但在绝大多数情况下,我相信在所有低效复制中的CombC都是完全一样的,具有同样的延迟时间,因此,分离这个组件并仅使用一个是值得尝试的行为。

希望到目前为止,你的patch正在跑数十或上百个节点。管理并单独控制各个节点将很困难。这里便是组(group)的切入点,因为控制可以被送至一个整体的组/在链表中,它们同样具有一个顺序,组中的全部节点继承组的顺序。全部的音源可以被“领先地”放在一个含有滤波器的组中。你无需担心每个独立音源及滤波器的位置。如果滤波器组在音源组之后的话,组内的全部滤波器均将位于音源之后。

组操作

一个组可以包含大约十个你想要齐奏的一个合成器的副本。一个传送至组的控制振幅、频率、衰减等等的方法将一次性改变它的全部子集(独立的合成器定义)。

18.18. 组,组操作

( 
//创建一个合成器 
SynthDef("ping", 
{arg fund = 100, harm = 1, rate = 0.2, amp = 0.1; 
a = Pan2.ar(SinOsc.ar(fund*harm, mul: amp) * 
EnvGen.kr(Env.perc(0, 0.2), gate: Dust.kr(rate)), Rand(-1.0, 1.0)); 
Out.ar(0, a) 
}).load(s); 
) 

// 使用一个全局变量(~)定义一个组 
~synthGroup = Group.head(s); 

// 运行这些八次左右, 向组内添加一个新的ping 
Synth("ping", [\fund, rrand(100, 1000), \rate, 1], ~synthGroup); 
Synth("ping", [\fund, rrand(100, 1000), \rate, 1], ~synthGroup); 
Synth("ping", [\fund, rrand(100, 1000), \rate, 1], ~synthGroup); 
Synth("ping", [\fund, rrand(100, 1000), \rate, 1], ~synthGroup); 
Synth("ping", [\fund, rrand(100, 1000), \rate, 1], ~synthGroup); 
Synth("ping", [\fund, rrand(100, 1000), \rate, 1], ~synthGroup); 
//等等。 

// 改变组的全部rate 
~synthGroup.set(\rate, 3/5); 
~synthGroup.set(\rate, 8); 

// 改变振幅 
~synthGroup.set(\amp, 0.2); 
~synthGroup.set(\amp, 0.01); 

//Command+. 将停止合成器和组, 
//所以用这个以保留组。 
~synthGroup.freeAll;

组内所有具rateamp引数的元素将识别出改变。如果我们添加一个没有上述引数的合成器,命令将被忽略。

我为合成器组使用了一个全局变量(用波浪号定义)。

Group.head的引数是目标。使用 .head意味着这个组位于其父集(服务器)的头部。它可以被添加到另一个存在的组,但由于它是我们唯一拥有的一个,因此它被添加到正在运行的服务器(默认组)。我们使用Synth为组添加ping。之前我们曾使用Synth创建新的节点。它们在我们所不知的情况下被添加入默认组。现在,我们将它们明确地添加给了一个组。

最后一个命令,freeAll,我们使用它而不是cmd+.,因为我们希望即使在停止了全部节点后,组仍处于活跃状态。

下例用Array.fill创建节点,并把每个元素加入组。这允许我们用引数i在合成器被创建后操作每个和声。我可以使用12.do,但用合成器填充一个数组允许对数组每个元素的控制。如果想只改变一个的话,我可以使用~all.at(n).set(args)

假设组仍处于活跃。如果不是这样的话,重新载入它。

18.19. 自动化节点创制

~all = Array.fill(12, 
   {arg i; Synth("ping", [\harm, i+1, \amp, (1/(i+1))*0.4],~synthGroup)}); 

~synthGroup.set(\rate, 0.8); 
~synthGroup.set(\rate, 5); 

Array.fill(12, {arg i; i/2+1}) 

// 改变一个节点的振幅 
~all.at(6).set(\amp, 1); 
~all.at(6).set(\amp, 0.1); 

// 用一个公式改变全部和声 
// 我用数组Array.fill(12, {arg i; i/2+1})检查公式 

~all.do({arg node, count; node.set(\harm, count/2+1)}); //1, 1.5, 2, 等等. 
~all.do({arg node, count; node.set(\harm, count*2+1)}); //1, 3, 5, 7, 等等. 
~all.do({arg node, count; node.set(\harm, count*1.25+1)}); 
~all.do({arg node, count; node.set(\harm, count*1.138+1)}); 

// 改变基准频率 
~synthGroup.set(\fund, 150); 
~synthGroup.set(\fund, 250); 
~synthGroup.set(\fund, 130); 

// 停止节点,但不停止组 
~synthGroup.freeAll; 

// 创建一个添加新合成器的任务 
r = Task({{Synth("ping", 
   [\fund, rrand(100, 2000), \rate, 2], ~synthGroup); 1.wait}.loop}).play 

// 当它很多时减速attack 
~synthGroup.set(\rate, 0.2); 

// 降低它们的音量. 注意新的合成器将沿用老的音量。 
~synthGroup.set(\amp, 0.01); 

// 停止除任务外的一切 
~synthGroup.free; 

// 停止任务 
r.stop;

现在我们要为默认组(服务器)的尾部添加另一个组,用来放所有效果器。注意如果我要串联效果器,应是音源 -> fx1 -> fx2, -> fx3,而后我需要它们及它们的组按顺序排列。但这里它们是平行的:音源被路由到所有的回声,所有的回声被混入总线0,1。

为了将ping路由到回声,我将它的输出总线改到了16。我一次发送了所有定义,为ping创建一个音源组,为回声创建一个效果组。创建的顺序没有关系,因为它们的节点是由组决定的:synthGroup理所当然的是头,fxGroup则是尾。我可以按任意顺序开启或停止它们。

实际上,效果组将有三个“回声”。其中两个是使用Comb的回声,但两者我都没有混入干信号。因此回声1和回声2仅是湿信号,没有音源。我使用一个重路由音源到总线0和1的干的合成器,因此我可以控制单独它,向最后的混音添加它,或从最后的混音中移除它。这同样可以用Out.ar([0, 1, 16, 17], a)做到,为效果路由音源到16和17,为干信号路由到0和1。

18.20. 音源组, 效果组

( 
SynthDef("ping", 
{arg fund = 400, harm = 1, rate = 0.2, amp = 0.1; 
a = Pan2.ar(SinOsc.ar(fund*harm, mul: amp) * 
EnvGen.kr(Env.perc(0, 0.2), gate: Dust.kr(rate)), Rand(-1.0, 1.0)); 
Out.ar(16, a) 
}).load(s); 

SynthDef("dry", 
{var signal; 
signal = In.ar(16, 2);  
   Out.ar(0, signal); 
}).load(s); 

SynthDef("echo1", 
{ 
var signal, echo; 
signal = In.ar(16, 2); 
echo = CombC.ar(signal, 0.5, [0.35, 0.5]); 
   Out.ar(0, echo); 
}).load(s); 

SynthDef("echo2", 
{ 
var signal, echo; 
signal = In.ar(16, 2); 
echo = Mix.arFill(3, { CombL.ar(signal, 1.0, LFNoise1.kr(Rand(0.1, 0.3), 0.4, 0.5), 
15) }); 
   Out.ar(0, echo*0.2) 
}).load(s); 
) 

~synthGroup = Group.head(s); 
~fxGroup = Group.tail(s); 

// 12.do不允许我控制每一个元素, 但没关系 
( 
12.do({arg i; 
   Synth("ping", [\harm, i+1, \amp, (1/(i+1))*0.4], 
   ~synthGroup)}); 
) 

// "ping"在总线16活动, 所以我们听不到它 

// 开启回声1 (湿的), 回声2 (仍是湿的), 然后是干信号 
a = Synth("echo1", target: ~fxGroup); 
b = Synth("echo2", target: ~fxGroup); 
c = Synth("dry", target: ~fxGroup); 

b.free; // 以不同顺序移除它们 
a.free; 
c.free; 

// 最初的ping仍在运行, 因此停止它. 
~synthGroup.freeAll; 

// 这么搞也行 
a = Synth("echo1", target: ~fxGroup); 
b = Synth("echo2", target: ~fxGroup); 
12.do({arg i; Synth("ping", [\harm, i+1, \amp, (1/(i+1))*0.4],~synthGroup)}); 
c = Synth("dry", target: ~fxGroup); 

~synthGroup.freeAll; // 停止音源, 但回声仍在运作 

// 再次开启音源 
12.do({arg i; Synth("ping", [\harm, i+1, \amp, (1/(i+1))*0.4],~synthGroup)}); 

~synthGroup.set(\rate, 0.8); 
~synthGroup.set(\rate, 5); 

~synthGroup.free; 
~fxGroup.free;

练习:铃和回声

18.21. 铃和回声

(
SynthDef("bells",
{arg freq = 100;
var out, delay;
out = SinOsc.ar(freq, mul: 0.1)
*
EnvGen.kr(Env.perc(0, 0.01), gate: Dust.kr(1/7));

out = Pan2.ar(Klank.ar(`[Array.fill(10, {Rand(100, 5000)}),
   Array.fill(10, {Rand(0.01, 0.1)}),
   Array.fill(10, {Rand(1.0, 6.0)})], out), Rand(-1.0, 1.0));

Out.ar(0, out*0.4); //将干信号送到主输出
Out.ar(16, out*1.0); //将更响的干信号送到效果器总线

}).load(s);

SynthDef("delay1", // 第一个回声
{var dry, delay;
dry = In.ar(16, 2);
delay = AllpassN.ar(dry, 2.5,
   [LFNoise1.kr(2, 1.5, 1.6), LFNoise1.kr(2, 1.5, 1.6)],
   3, mul: 0.8);
Out.ar(0, delay);
}).load(s);

SynthDef("delay2", // 第二个回声
{var delay, dry;
dry = In.ar(16, 2);
delay = CombC.ar(dry, 0.5, [Rand(0.2, 0.5), Rand(0.2, 0.5)], 3);
Out.ar(0, delay);
}).load(s);

SynthDef("delay3", // 第三个回声
{
var signal, delay;
signal = In.ar(16, 2);
delay = Mix.arFill(3, { CombL.ar(signal, 1.0, LFNoise1.kr(Rand([0.1, 0.1], 0.3),
0.4, 0.5), 15) });
   Out.ar(0, delay*0.2)
}).load(s);
)

//定义组
~fxGroup = Group.tail;
~bellGroup = Group.head;

// 开启回声之一和4个铃
f = Synth("delay3", target: ~fxGroup);
4.do({Synth("bells", [\freq, rrand(30, 1000)], target: ~bellGroup)})

// 停止现存的回声并换到另一个
f.free; f = Synth("delay1", target: ~fxGroup);
f.free; f = Synth("delay2", target: ~fxGroup);
f.free; f = Synth("delay3", target: ~fxGroup);
Synth("delay1", target: ~fxGroup); // 不移除delay3增加delay1
Be Sociable, Share!

Published by

ww1way

http://about.me/ww1way

Leave a Reply

Your email address will not be published. Required fields are marked *