SC:加法合成,随机数字,CPU占用

为什么在经过差不多50年的电子音乐和乐器设计后,我们仍能听出某些乐器是被合成的?绝大多数合成器都缺乏复杂性和混沌(chao)。甚至连采样的乐器听起来都不真,因为对于任何乐器音符的精确复制都并非那个乐器的真实表现。将一片树叶的高分辨率数码照片复制n个看起来也不会像一棵树。一棵树上的所有叶子都不同,并且它们是实时改变的。对于钢琴来说也是这样。每个键都有它专属的和声指纹。甚至连续敲击同样的键基于距上一个音符产生后震动弦的位置也会产生不同的泛音。真乐器是复杂和混沌的。每个音符都很独特。尽管综合的上层谐波内容保持恒定,但小的细节改变了。SC是我碰到的第一个能够实时处理这些复杂工序的工具。我们将用最复杂和辅助计算集中法(以及在结果方面最有益的)来开始合成理论:加法合成(additive synthesis)。

谐波系(Harmonic Series)和波形

在之前的章节中,谐波系决定声音的特性。每个和声的存在和强度定义这个特性。但一根弦或一个振动体如何产生一系列相关频率呢?

密度和张力对等的条件下,短的弦比长的弦震动更快。如果你均分一个通常以100Hz振动的弦,它将会议两倍的速度振动,或者说以200Hz振动。如果你将之缩短为原先长度的三分之一,它将以300Hz的频率振动。依此类推。如此分开一根弦将产生和声/谐波(harmonic)。和声是西方音乐音级的基础。下图展示了对应每个上层谐波的音。最低的C大概65Hz,接下来是130, 195, 260(中央C),325,390, 455, 520, 585, 650, 715, 780, 845, 910, 975, 1040。

弦振动和上层谐波
弦振动和上层谐波

绝大多数弦(以及乐器体)同时以全部那些频率振动。弦在全部长度上振动的同时,同样在一半的长度上振动(看下边的图示,注意全部动作是同时发生的)。那个一半长度振动的频率是两倍快。它同样会以1/3,1/4…1/n的长度振动。因此一根以65Hz振动的弦,同样会制造130, 195等频率。

振动的弦
振动的弦

音色不仅是存在,同样也是这些谐波的振幅。每个谐波都是一条正弦波。一般地,较高的谐波有较低的振幅,但每个谐波的实际振幅将与每个音色不同。高次谐波(upper harmonics)的存在和振幅是任何声音的音色指纹。小提琴有较强的三次谐波(third harmonic),较弱的四次,没有五次,弱的六次。同时,长号有若的三次,强的四次,等等。下边是吉他,萨克斯,铃和电钢琴各演奏两个音符的声波图(它们是被合成的,但清晰可辨)。注意看高次谐波的区别。

吉他,萨克斯,铃和电钢琴高次谐波的区别
吉他,萨克斯,铃和电钢琴高次谐波的区别

下图是说“four score and seven years.”的人声声波图。肋状物即谐波。注意seven中两个e的区别,以及score和years中两个r的区别。year中的r一开始的三个谐波是强的,然后是大概四个小时谐波的范围,然后是四个相对强度(relative strength)的谐波。同样注意,每个谐波带是独立的,并随每个单词演化。

“four score and seven years”光谱分析
“four score and seven years”光谱分析

认识到音色(波形和高次谐波的存在)是实时变化的同样重要。绝大多数音乐家争取,但从未达致完美的音质。然而正是超级错误使得声音自然。合成的声音有完美的同源音色,但我们并不喜欢。举例说,锯齿波实际上是一系列用相对较低的振幅调至谐波系的正弦波。对其他复杂的波来说也是一样的:方波,三角波,甚至噪音。所有都可以用单独的正弦波来构造。这便是加法合成理论。

加法合成

绝大多数合成器都提供各式波形以搭建声音,但如果波形是预先确定的话,你便对个体的谐波失去了控制(滤波之外)。但它们仍然无法做加法合成,因为它们可能仅有四或五个正弦振荡器以及两三个包络生成器。但加法合成允许对每个波的参数进行独立控制,SC是合成VCO的虚拟仓库。仅仅几行,你便可创制数百拥有独立包络的分音(partials)。但让我们先从一打开始。

下边是一个频率为200Hz的锯齿波声波图。不仅因为其具备全部谐波,而且它们非常平坦连贯。这些高次谐波中的任意一个都可以被想作是频率乘以最低,或基础频率的正弦波。

频率为200Hz的锯齿波声波图
频率为200Hz的锯齿波声波图

欲重建这个锯齿波,我们需要独立的谐波正弦波,或者说乘以基础频率。下边是一个基础频率为400的粗加工版本。将12个正弦波叠加通常会失真,因此它们都被缩到0.1。

14.4. 叠加正弦波

(
{
(
    SinOsc.ar(400) + SinOsc.ar(800) + SinOsc.ar(1200) +
    SinOsc.ar(1600) + SinOsc.ar(2000) + SinOsc.ar(2400) +
    SinOsc.ar(2800) + SinOsc.ar(3200) + SinOsc.ar(3600) +
    SinOsc.ar(4000) + SinOsc.ar(4400) + SinOsc.ar(4800)
)*0.1
}.scope
)
// 谐波调整后
(
{
(
    SinOsc.ar(400, mul: 1) + SinOsc.ar(800, mul: 1/2) +
    SinOsc.ar(1200, mul: 1/3) + SinOsc.ar(1600, mul: 1/4) +
    SinOsc.ar(2000, mul: 1/5) + SinOsc.ar(2400, mul: 1/6) +
    SinOsc.ar(2800, mul: 1/7) + SinOsc.ar(3200, mul: 1/8) +
    SinOsc.ar(3600, mul: 1/9) + SinOsc.ar(4000, mul: 1/10) +
    SinOsc.ar(4400, mul: 1/11) + SinOsc.ar(4800, mul: 1/12)
)*0.1
}.scope
)

第二例多做了一个调整。每个正弦波的振幅应与分音数量同比例减小。第二分音应是第一个音量的1/2,第三个是1/3,第四个是1/4,以此类推。

另一例使用一个变量来计算高次谐波,并且使用了一个数组来讲正弦波分布到不同的总线(bus)上。你应该没有12个输出,但没问题,它只打算被看到而不是被听到。在运行它之前,将你的扬声器关小同时打开声音系统设置,选择线路输入作为输入(但不连接任何东西)。这将保证没有任何输入信号会影响到波。记得你可以调整scope窗口的大小以更好的查看每条波。

14.5. 变量做加法合成

(
{
f = 100;
[
   SinOsc.ar(f*1, mul: 1), SinOsc.ar(f*2, mul: 1/2),
   SinOsc.ar(f*3, mul: 1/3), SinOsc.ar(f*4, mul: 1/4),
   SinOsc.ar(f*5, mul: 1/5), SinOsc.ar(f*6, mul: 1/6),
   SinOsc.ar(f*7, mul: 1/7), SinOsc.ar(f*8, mul: 1/8),
   SinOsc.ar(f*9, mul: 1/9), SinOsc.ar(f*10, mul: 1/10),
   SinOsc.ar(f*11, mul: 1/11), SinOsc.ar(f*12, mul: 1/12)
]
}.scope(12)
)

在调整好窗口大小之后,按S键将模式调整为overlay(覆盖)。这个动作将覆盖所有正弦波到一个范围内。它们将看起来如下图:

六条正弦波叠加
六条正弦波叠加

这个patch图示了谐波是如何相互作用的。在图示约1/4的地方,你可以看到所有的正弦波是如何同步被推向波峰的。这也创造了锯齿波锋利的边缘。随着更高的谐波向下移动,它们开始删除掉更低的波的能量。这个过程的集合体是随着更多谐波移入它们周期(cycle)负的相位时,一条能量逐渐下降的斜坡,直到模式的最后,你可以看到它们或多或少抖位于图示负的部分。这是锯齿波的底部。第二个图示是混合后同样的patch。我试图将它们排列起来以使周期匹配。

这是对此的另一种思考:我们知道所有高次分音都是基础频率的乘积,会有一些它们都处于0的点(100 Hz, 200 Hz, 300Hz,等等,每秒都会归0)。同时,还会有它们都位于波峰的同步:1或者-1。这是数学事实,因为它们是倍数。它们都位于1的点是锯齿波的波峰。都是0的点则是中点。-1当然就是锯齿波的波谷。我们可以用方波粗略阐述这个概念。

改变谐波将改变声音的特性。改变任意一个谐波的振幅都将微妙的改变声音特性。你可以尝试用MouseX.kr替代任意的mul引数(选择较大的),孤立那个谐波的振幅。注意听变化。下边是对等的patch,但每个振幅都被一个LFNoise1逐步控制。注意基础音高保持不变的同时,音质改变了。我们的大脑将所有正弦波加到一起,然后以一个音的形式听到它们。注意这不是滤波,而是加法。

14.6. 调制加法锯齿波

(
{
var speed = 14;
f = 300;
t = Impulse.kr(1/3);
Mix.ar([
   SinOsc.ar(f*1, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/1),
   SinOsc.ar(f*2, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/2),
   SinOsc.ar(f*3, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/3),
   SinOsc.ar(f*4, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/4),
   SinOsc.ar(f*5, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/5),
   SinOsc.ar(f*6, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/6),
   SinOsc.ar(f*7, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/7),
   SinOsc.ar(f*8, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/8),
   SinOsc.ar(f*9, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/9),
   SinOsc.ar(f*10, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/10),
   SinOsc.ar(f*11, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/11),
   SinOsc.ar(f*12, mul: LFNoise1.kr(rrand(speed, speed*2), 0.5, 0.5)/12)
])*0.5
}.scope(1)
)

试试改变speed的值,它控制着LFNoise的频率。然后用LFNoise1替换LFNoise0(使用find和replace)。最后,最后将f=100替换为f=someOtherControl(其他控制,比如正弦波,另一个LFNoise或者鼠标控制)。

接下来我们将要添加包络。我们可以把最后的*0.5用一个单独的包络替换以一次性控制所有振荡器的振幅。这也是绝大多数模块有限的合成器做的。但因为我们是通过代码来做这个事的,因此我们可以给每个振荡器分配一个包络,让每个谐波都不同,结果就是一个更自然的声音。

这些例子因为要清晰阐述加法合成的方法,显得有些不必要的冗余。即使如此,借助这几行代码,我们已然超越了绝大多数商业合成器的能力。但是,这个练习有一点点乏味,不过或许能让你一瞥早期电子创作者在连线式合成器领域内的先驱努力。

14.7. 具备独立包络的加法合成锯齿波

(
{
f = 100;
t = Impulse.kr(1/3);
Mix.ar([
   SinOsc.ar(f*1, mul: EnvGen.kr(Env.perc(0, 1.4), t)/1),
   SinOsc.ar(f*2, mul: EnvGen.kr(Env.perc(0, 1.1), t)/2),
   SinOsc.ar(f*3, mul: EnvGen.kr(Env.perc(0, 2), t)/3),
   SinOsc.ar(f*4, mul: EnvGen.kr(Env.perc(0, 1), t)/4),
   SinOsc.ar(f*5, mul: EnvGen.kr(Env.perc(0, 1.8), t)/5),
   SinOsc.ar(f*6, mul: EnvGen.kr(Env.perc(0, 2.9), t)/6),
   SinOsc.ar(f*7, mul: EnvGen.kr(Env.perc(0, 4), t)/7),
   SinOsc.ar(f*8, mul: EnvGen.kr(Env.perc(0, 0.3), t)/8),
   SinOsc.ar(f*9, mul: EnvGen.kr(Env.perc(0, 1), t)/9),
   SinOsc.ar(f*10, mul: EnvGen.kr(Env.perc(0, 3.6), t)/10),
   SinOsc.ar(f*11, mul: EnvGen.kr(Env.perc(0, 2.3), t)/11),
   SinOsc.ar(f*12, mul: EnvGen.kr(Env.perc(0, 1.1), t)/12)
])*0.5
}.scope(1)
)

捷径

在SC中,永远有做某件事更简单的方法。任何限制都只存在于写代码的人身上。比如说,我们可以用多通道扩展,将一行单独的代码扩展到一个数组中。下边的例子中,数组(一切在方括号里的东西)被用完全写出的振荡器填入。mix将它们合成到一个轨道。记住如果任何引数是一个数组,那个ugen即被扩展为一个ugen的数组,每个ugen继承它们对应位置的引数。

14.8. 数组扩张做加法合成

Mix.ar([SinOsc.ar(100), SinOsc.ar(200), SinOsc.ar(300)])

可以被写作

Mix.ar(SinOsc.ar([100, 200, 300]))

下例中,第一个midicps返回一个单一值。第二个返回一个值的数组。接下来的一行展示了写数组的捷径。最后一例用这个技术写成。对编译器来说,它们是一样的。优点是你可以打更少的字。同样地,这更简明、易读。

14.9. 数组扩展做加法合成

midicps(60);
midicps([60, 62, 64, 65, 57, 69, 71]);
(1..12) // 等同于 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
midicps((1..24)*60)
(1, 3..13) // 意味着 [1, 3, 5, 7, 9, 11, 13]
midicps((60, 63..72))
// 用快捷方式做加法合成
(
{
f = 100;
t = Impulse.kr(1/3);
Mix.ar(
   SinOsc.ar(
      f*(1..12),
      mul: EnvGen.kr(
         Env.perc(0, 1),
         t,
         levelScale: 1/(1..12),
         timeScale: [1.4, 1.1, 2, 1, 1.8, 2.9, 4, 0.3, 1, 3.6, 2.3, 1.1]
      )
   )
)*0.5
}.scope(1)
)

timeScale 数组需要完全写出来,因为它并非一个可以用快捷方式写出的逻辑序列。但如果它的值没必要指定的话,在1到3之间,可以使用rrand(1.0, 3.0).dup(12)来生成这个数组。

一个也许你需要逐个输入数组内容的情形是,一组没有常规几何模式的值,比如Cmaj 9 #11 和弦 (C, E, G, B, D, F#),或者自然音阶小调音阶(C, D, Eb, F, G, Ab, Bb, C).里全音和半音的关系。这一切都被展示在下边midi值的数组里。

14.10. 数组扩展做加法合成

(
{
t = Impulse.kr(1/3);
Mix.ar(
   SinOsc.ar(
      [60, 64, 67, 71, 74, 78].midicps,
      mul: EnvGen.kr(
         Env.perc(0, 1),
         t,
         levelScale: 1/(1..6),
         timeScale: rrand(1.0, 3.0).dup
      )
   )
)*[0.3, 0.3]
}.scope(1)
)
(
{
Mix.ar(
   Pan2.ar(
      SinOsc.ar(
         [60, 62, 63, 65, 67, 68, 71, 72].midicps,
         mul: LFNoise1.kr(rrand(0.1, 0.5).dup(8), 0.5, 0.5)
      ),
      1.0.rand2.dup(8)
   )
)*0.2
}.scope(1)
)

填充一个数组

这个patch的另一版本用 Array.fill 做所有的打字工作。Array.fill 基于一个函数生成一个数组。数组中的每个项目都是这个函数的结果。第一例简单地用随机数填充数组。第二例用正弦波填充数组。Array.fill 的第一引数为数组内项目的数量,第二引数为生成每个值的函数。可以把函数看作被给予任务的小型机器:可能是根据你的指令吐出数字。

14.11. Array.fill

Array.fill(16, {100.rand})
Array.fill(16, {SinOsc.ar(100.rand)})

当你给某人一项任务,比如切苹果,你常常想为随后的重复提供不同的指令。比如说,任务可以是切十个苹果。但你可能想要每个的切法不同:第一个整个留着,把第二个切成两半,第三个切成三瓣,第四个四瓣,等等。你能看到模式。而不是每次都告诉别人应该怎么切,你可以在指令中说明,清点每次循环,然后将每个苹果对应切成那些块数。为了做这个,系统需要知道自己位于第几次循环内。它需要一个计数器。我们可以用变量做我们自己的,就像下例中展示的一样。

14.12. 带计数器的Array.fill

( // 用计数器填充一个数组,接着增加计数
var counter = 0;
Array.fill(16, {counter = counter +1; counter})
)
( // 用计数器 * 100 填充一个数组
var counter = 0;
Array.fill(16, {counter = counter +1; counter*100})
)

Array 对象内建记录循环次数的计数器是如此普通的一个程序。那个数字作为一个引数被传入函数。引数和变量工作方式一样:你可以对其任意命名,甚至可以改变它的值,但通常你基本没有理由这么做。区别是,引数通常由一些外部程序提供并被传递给函数。在多于两个引数的情况下,它们便由其位置作为鉴别(就像目前为止我们看到的引数列中的引数一样)。你可以在下例中看到这个新语法:arg myNameForCount。运行下边诸行,然后检查SC post窗口里的结果。最后两例展示了另一个可替代语法:|myNameForCount|。第二例生成数组的数组。

14.13. 带引数的Array.fill

//用迭代填充一个数组
Array.fill(16, {arg myNameForCount; myNameForCount})
//用随机数填充一个数组
Array.fill(16, {arg myNameForCount; [myNameForCount, rrand(20, 100)]})
//用myNameForCount的乘积填充一个数组
Array.fill(16, { |myNameForCount| myNameForCount*3})
//用SinOsc对象填充一个数组, 每个的平率为counter*100
Array.fill(16, { |myNameForCount| SinOsc.ar(myNameForCount*100)})

要做一个谐波系(harmonic series),你会要向基础平率乘以1,2,3,4等等。计数器可以被用于计算每个SinOscfreq以及每个EnvGenlevelScale。计数器始于0而不是1,我不想要freq*0,因此我使用变量向其加1.下例生成16个高次分音,每个成比例地变软,衰减值在0.1到2.1间随机。fund*partial 保证它们都是和谐的。1/partial 是包络的 levelScale。第一个将是1/1,接下来1/2, 1/3, 1/4等等。Array.fill 返回一个SinOsc ugen的数组,Mix.ar 将它们全部混入一个通道内。

14.14. 加法锯齿波, 独立的衰减

(
{
var gate, fund;
gate = Impulse.kr(1/3);
fund = MouseX.kr(50, 1000);
Mix.ar(
   Array.fill(16,
   {arg counter;
   var partial;
   partial = counter + 1;
   SinOsc.ar(fund*partial) *
   EnvGen.kr(Env.adsr(0, 0, 1.0, TRand.kr(0.2, 2.0, gate)),
      gate, 1/partial)
   })
   )*0.2 //整体音量
}.scope(1)
)
// 使用捷径的同样的patch
(
{
var gate, fund;
gate = Impulse.kr(1);
fund = MouseX.kr(50, 1000);
Mix.ar(
   SinOsc.ar(fund*(1..16).postln) *
   EnvGen.kr(Env.perc(0, TRand.kr(0.2, 2.0, gate)),
      gate, 1/(1..16))
   )*0.2 //整体音量
}.scope(1)
)

我用这一节赞美SC复杂和丰富的处理能力。上边的patch并非令人惊讶,但你应该认识到给每个分音不同的衰减率会得到的更自然的声音。作为对比,这个patch仅给了全部分音一个包络。

14.15. 同样衰减的加法锯齿波

(
{
var gate, fund, env;
gate = MouseButton.kr(0, 1, 0);
fund = MouseX.kr(50, 1000);
env = Env.adsr(0, 0, 1.0, 2.0);
Mix.ar(
   Array.fill(16,
   {arg counter;
   var partial;
   partial = counter + 1;
   SinOsc.ar(fund*partial) *
   EnvGen.kr(env, gate, 1/partial)
   })
   )*0.2 //整体音量
}.scope(1)
)

做基于代码的合成(与基于图形的相反)来说,将一个简单的想法快速复制为复杂的声音很简单。对于加法合成和数组扩展来说尤其明显。以这个简单的patch为例,一个振荡器的振幅被另一个正弦波所控制。

14.16. 带控制的正弦波

{SinOsc.ar(400, mul: SinOsc.ar(1/3, mul: 0.5, add: 0.5))}.play

我们可以由这个简单的模块开始,通过以谐波的方式增加更多的正弦波来创建一个更复杂的声音。Mix.fill结合在Array.fill中使用的fill逻辑。它生成一个ugen数组并将它们混合。它同样具备一个可以被用于计算高次谐波的计数器引数。这个patch使用FSinOsc,它更高效。

14.17. 嘎嘎叫的正弦波

(
{
   var harmonics = 16, fund = 50;
   Mix.fill(harmonics,
        { arg count;
           Pan2.ar(
              FSinOsc.ar(
                 fund * (count + 1), // 计算每个谐波
                 mul: FSinOsc.kr(rrand(1/3, 1/6), mul: 0.5, add: 0.5 )),
              1.0.rand2)
         }
   ) / (2*harmonics)
}.play;
)

在本章下边练习的部分,你可以看到本patch的变化。

不谐和频谱

上例中的每个正弦波都是基础频率的乘积 (f*1, f*2, f*3, f*4等等)。这是和谐频谱。绝大多数调性乐器都具备和谐的频谱。非调性乐器比如锣、铃和镲都倾向于非和谐频谱,或者一系列并非建立于基某个准频率的频率叠加。为生成一个非和谐频谱,你需要为每个正弦波输入无关的值,比如135, 173, 239, 267, 306, 355, 473, 512, 572,和626。我是如何获得这些不相关的频率的?洗牌。

在上例中,每个高次谐波的振幅都基于它们的关系被计算出来:越高的谐波越软。但在不谐和频谱中,振幅没有模式。比如说,0.25, 0.11, 0.12, 0.04, 0.1, 0.15, 0.05, 0.01, 0.03, 0.02, 和 0.12。我使用了相似的“随机”法生成这个序列。

加法合成可用于生成纯净的波形,比如锯齿波、方波或者三角波,这些在商业合成器里也可以实现。是的,你可以对上层泛音进行更好的控制,但至少其他的波形是确实存在的。但不和谐频谱的波形是不存在的。商业合成器在完美的波形和完美的噪音间直来直去。没有中间地带。这便是SC独到的一个领域。

下边是一个混合了随机频率和振幅的patch。因为没有包络的关系,所以你不会有铃声的感觉,但它却具备一个非常金属的共鸣。

{Mix.ar( 
   SinOsc.ar( 
      [72, 135, 173, 239, 267, 306, 355, 473, 512, 572, 626], 
      0, //相位 
      [0.25, 0.11, 0.12, 0.04, 0.1, 0.15, 0.05, 0.01, 0.03, 0.02, 0.12] 
   ))
}.scope(1)

随机数,洞察力

我们称一组数字为“随机”因其没有明显的模式。于是随机便是有关承上启下和感知的问题。我们通过一组数字的前后关系以确定其是否随机。一个没有前后关系的数字不可能是真正的随机。

做一个例证:你,能,猜到,这组序列的,下一个,数字吗?

226, 966, 7733428, 843, 6398, 686237, 46, 8447, _____

看起来是随机的?其实不然。这是个非常清晰的系统。(提示:没有字符0和1)。当你掌握了这个伎俩,你便能猜到下一个甚至下三个或下四个数字。当你看到了模式,随机的品质也就一同消失了,即使序列和进程并没有改变。那么,改变的是什么呢?你看问题的角度。这是可供借鉴给音乐欣赏的其他领域(以及生活)的很好一课。

当我洗一副牌的时候,它们是“随机的”,因此我无法预知牌的序列可能会是怎样的。但如果我从一副新牌开始,并且知道其顺序,在洗牌时我使用一个精确的公式(比如说,将它们分成两半并相互调换顺序),或者当牌落下时确切的记下其顺序,然后我便能预知它们的顺序。在这样的情况下,其实牌并没有被随机。在一次真正的洗牌和一次精心安排的洗牌间,唯一的差别是我对进程的掌控。因此,随机真正意味的,是混合到一个常人无法预知结果的点。

在最近一个关于不和谐光谱的演讲中,我向下边的同学展示了一组由计算机挑选出的随机值。一个同学说他无法理解这是如何发生的。另一个同学则不理解第一位同学为什么不理解;计算机挑选了一个随机数,没了。但他的观察是精明的;尽管计算机有时看起来的确是在随机运作,但其实他们从不这样。让计算机随机运作是不可能的。但有时它们复杂运算的行为会使我们看起来就像随机一般。

为生成随机序列,作曲家使用了与我洗牌时类似的方法,但应用了一个更为巨大的数字集合。这些数字在计算机内部运用一个以算法的形式呈现的数学公式进行混合。计算机收到指令,并精确的执行它,并因为计算机没有自我意识,所以将它用于记录数字是很棒的,它“知道”序列,因为那是一个公式。然而,结果序列是如此的复杂,复杂到我们人类无法认识其模式的地步。这一系列数字被称为一个随机数生成器,并且到计算机或程序第一次运行时便被执行和储存。正因为每次都运用相同的公式,数字序列们都是相同的,甚至在不同的电脑上都应该是相同的。因此对于计算机来说,这完全不是随机。

在计算机出现之前,我们如何做随机?我们使用一本名为“一百万个随机符号”的书,它包含了一百万个随机符号。这并不是在开玩笑。看看Amazon对它的书评吧。

当你运行一个使用一些“随机”序列事件(比如骰子或者扑克牌)的程序时,计算机从这个数字序列中取出那些值。

另一个问题是,它始终从头开始,因此一开始你总是得到相同的值,就像使用一副没洗过的新牌一样,或者每天阅读“一百万个随机符号”相同的那一页。前几次,顺序貌似是随机的。但经过两到三次循环之后,我们将记住那个模式并且它将不再随机。随机的一部分,对我们来说,是每次不同的数字。

那么如何得到不同的数字呢?分开那副扑克。要分开一个随机数字生成器的扑克,你需要从序列的另一个点开始。这个点被称为一个随机的种子。你将一个数字作为一粒种子交给计算机,计算机就将从那个点开始在序列中使用数字。

还有另一个问题。如果你知道这个种子,那么数字序列的结果对你来说仍然不是真正的随机。你需要选择一个随机的种子。我的意思是,你需要随机地选择一颗随机的种子。我的意思是,随机种子必须是随机产生的(很绕?)。

解决这个问题的方法是使用CPU的内部时钟,这恰好是一串高速运转的数字序列。如果时钟上的每个数字都对应随机序列中的一个数字,那么在某种意义上来说,你就等于在随机序列中徜徉了(就像洗牌一样)。从时钟内截取一个数字用作种子就像在洗牌时你将手指放在牌上:你在那一刻获得了一个随机位置。这看起来些许复杂,但就是这么干的。

SC在后台自动的做一个随机播种。每次当你运行一条类似10.rand的代码,SC首先从它的内部时钟里读取数字作为一粒种子。而后移动入随机数生成器序列并从那个点开始其选择的序列。这对我们来说是真正的随机,因为我们无法预知时钟给的数字或者在那个点的数字的顺序。

那便是你如何获得伪随机选择的方式。但有时,你会想要重复一组随机的选择。在这种情况下,你需要一次次使用相同的种子来重制相同的事件序列。(比如,很多电脑扑克游戏允许你重新出牌。这便是它们如何实现这一点的。)一个给定的随机种子在你调试一个错误并想要每次重制相同错误的时候是很有用的。同样的,你可能会发现你喜欢并且需要确切重制的一个特定的随机事件变化。

首先我将展示一些随机选择,然后是使用种子的一些随机选择。SC中有若干随机函数。消息rand将返回0到指定数字间的随机数。55.rand 将返回0到55(不包括55)间的整数。55.0.rand 将返回0.0到55.0间的浮点数。多运行这些例子几次以观察随机数字是如何被挑选的。

14.19. rand

10.rand;
10.0.rand;

反复运行这些patch一定是巨枯燥的,因此如下是使用dup消息测试随机选择的方法,它用函数返回的结果填充一个数组。

14.20. 测试一个随机数组

{100.rand}.dup(20)

以下是一个典型的初学者错误。试着运行下例,看看结果多么不同。

14.21. 不使用函数的错误

(100.rand).dup(20)

代码挑选一个随机数,但它每次都是用那个相同的数字。第一例将随机数选择放入了函数内。一个函数意味着“运行着一行代码”,100.rand意味着选取一个随机数,而{100.rand}意味着每次选取一个随机数,100.rand意思是选取一个随机数并且每次使用它。

下边是在客户端(语言和编程方面)使用一颗种子的同样的例子。运行第一行若干次看数组是被如何填的。然后运行第二行若干次。因为你在为随机生成器做种,每次你都会在数组中得到同样的数字。尝试将种子改为5以外的数字。你将得到一个新的序列,但每次都是同样的序列。(运行四次后看起来便不非常“随机”了。)

14.22. 客户端随机种子

{100.rand}.dup(20);
thisThread.randSeed = 5; {100.rand}.dup(20);

这个东西并不影响伺服器,它演奏你设计好的patch。要在伺服器播种一个随机进程可以使用RandSeed。这个ugen可以被触发,因此你可以不断重设种子。第一引数为触发器,第二是种子。运行第一和第二例若干次。运行第三例一次。它将没5秒重置一次。

14.23. 伺服器随机种子

// 每次都不同
{SinOsc.ar(LFNoise0.kr(7, 12, 72).midicps, mul: 0.5)}.play
// 每次都相同
(
{
RandSeed.kr(1, 1956);
SinOsc.ar(LFNoise0.kr(7, 12, 72).midicps, mul: 0.5)
}.play
)
// 每5秒重置一次
(
{
RandSeed.kr(Impulse.kr(1/5), 1956);
SinOsc.ar(LFNoise0.kr(7, 12, 72).midicps, mul: 0.5)
}.play
)

如何使用一个时钟种子,并且知道它的值,以便以后纠错或者仅因为你喜欢某个种子的版本?

14.24. 列印时钟种子

thisThread.randSeed = Date.seed.postln; {100.rand}.dup(20);
( 
{ // 随机选取并重复使用一粒种子 
RandSeed.kr(Impulse.kr(1/5), Date.seed.postln); 
SinOsc.ar(LFNoise0.kr(7, 12, 72).midicps, mul: 0.5) 
}.play 
)

种子从时钟选取并被发布到post窗口。如果你想要重制一个版本,用post窗口内发布的值替换Date.seed.postln即可。

总结一下,有人可能会争辩说一个生成创作并非真的随机,因为可以通过使用相同的种子进行复制。它看起来像随机仅因为我们先前并未听过那个数字序列。但每个种子(数以亿计的),代表了一个特定的可重复版本。因此相对随机进程来说,它可以被理解为无数可能的变化,由不同的种子带来。于是你写的代码也成为了这无数变化(比如你每次演出中选择的)的DNA,并可以利用那粒种子进行鉴别和重复。

高次谐波结构是“音色”的指纹。它帮助我们分辨某个声音是什么(小提琴,笛子,人声),甚至是相同声音的微小变化(比如说你母亲感冒时的声音)。即使一部分频率集合是不和谐的,一个“随机”的集合,我们仍能区分它与任意其他的“随机”频率组合,就像下边的patch所展示的。

下边的代码生成一组类似铃的声音,每个都有自己的伪随机(pseudo-random)频谱。试着从内向外读这些代码。最里边的SinOsc的频率在50和4000间随机。它被乘以一个衰减在0.2到3.0间、音量在0到1间的包络。这串代码位于一个函数内,这个函数有个dup消息,它的引数是12dup消息创制一个具备不同频率和包络的SinOsc对象数组。因为触发器在函数外被创建,因此所有的Ugen都共享这个触发器。音头强度将是一致的,因此听起来就像是一个声音一样。那12条正弦波,或者说不谐和频率,被用Mix.ar混合、Pan2做声相。试着停止回放然后再运行一次第一例。重复四、五次。你能说出某些具有更长或更短衰减率的频率吗?

每一次执行,电脑都会从随机数生成器中挑选一组不同的随机数以创造每个铃音。你能添加一行代码,使用一个随机种子以连续两次产出同样的铃音集吗?

最后,在不停止之前铃音的情况下反复运行例子四、五次,这样的话,五、六个铃铛便在一起响了。注意尽管每个单独的铃音实际上都是一组可能独立和随机挑选的正弦波集合,我们的大脑还是会将它们结合为一个单一的可识别的声音。同样地,尽管每个集合都是随机的,我们都将它们辨识为一个独立单位,并且,我们能够跟进和识别它们的组。

我不禁想到这样一个问题:什么时候“随机”集合将不再随机?何时你会第二次听到它?

14.25. 随机频率 (Pan2, Mix, EnvGen, Env, fill)

( // 让它运行一会儿
{
var trigger, partials = 12;
trigger = Dust.kr(3/7);
Pan2.ar(
   Mix.ar(
      {
      SinOsc.ar(exprand(50.0, 4000)) *
      EnvGen.kr(
         Env.perc(0, rrand(0.2, 3.0)),
         trigger,
         1.0.rand
         )
      }.dup(partials)
   )/partials,
   1.0.rand2
)
}.play
)

最后,欲展示一个和谐频谱如何能够变为不和谐,以及调性到非调性的变形,下边的patch在上一例的基础上添加了一个ugen:一个当你从右向左移动便会对高次谐波进行去谐(detune)的鼠标控制。为了确保不谐和频谱,去谐总量在-1.4到1.4间随机选取。

14.26. 和谐到非和谐频谱

(
// 让它运行一会儿
// 或者将Dust.kr改为Impulse.kr
{
var trigger, partials = 12, fund;
trigger = Dust.kr(3/7);
fund = exprand(50, 700);
Pan2.ar(
   Mix.fill(partials,
      {arg count;
      SinOsc.ar(count + 1 * fund *
         MouseX.kr(1.0, 1 + 0.4.rand2)) * // detune
      EnvGen.kr(
         Env.perc(0, rrand(0.2, 3.0)),
         trigger,
         1.0.rand
         )
      }
   )/partials,
   1.0.rand2
)
}.play
)

CPU占用

我撒了谎。SC并非能够无限供应合成组件。我们受限于机器的处理能力。加法合成是昂贵的,因此此刻,伺服器窗口上的CPU信息便显得尤为重要。它显示CPU的平均值和峰值,以及Ugen的数量。反复不停地(不停止上一个声音的前提下)运行上边的例子,或者增加分音的数量,看看如果把CPU搞爆了会怎样。

伺服器窗口
伺服器窗口

在我的笔记本上,在一切变得奇慢无比之前,我可以运行最多3000个Ugen。这与只有两打模块的古旧设备有着巨大的差异。

回到铃音的patch并增加分音的数量。关注CPU占用的升高,同时不要忘记注意当你增加分音时,声音是如何改变的。200+ 不谐和分音的结果是什么?

随着随机分音数量的增加,声音会变得越来越失去焦点,然后变为噪音。这在下章会讲到。

说说下边的练习,分散的、会聚的、衰减的锣:我喜欢这些例子,因为它们听起来是如此的酷,并且它们与生活中的任何东西相比都如此不同,但同样因为它们经过了人工的构思。经过加法合成的一课,我们绞尽脑汁地使用加法技术的能量,并且在认识它们之前偶然发现了这些声音的理论。

练习:闪烁的正弦波,嘎嘎叫的正弦波,分散的、会聚的、衰减的锣

14.27. 闪烁的 (MouseButton, Mix, Array.fill, Pan2, EnvGen, Env LFNoise1)

(
{
var trigger, fund;
trigger = Dust.kr(3/7);
fund = rrand(100, 400);
Mix.ar(
   Array.fill(16,
   {arg counter;
   var partial;
   partial = counter + 1;
   Pan2.ar(
      SinOsc.ar(fund*partial) * 
      EnvGen.kr(Env.adsr(0, 0, 1.0, 5.0), 
         trigger, 1/partial
      ) * max(0, LFNoise1.kr(rrand(5.0, 12.0))), 1.0.rand2)
   })
   )*0.5 //整体音量
}.play
)
//混合若干上边那个声音
(
{
var trigger, fund, flashInst;
flashInst = Array.fill(5,
{
   trigger = Dust.kr(3/7);
   fund = rrand(100, 400);
   Pan2.ar(
      Mix.ar(
         Array.fill(16,
         {arg counter;
         var partial;
         partial = counter + 1;
            SinOsc.ar(fund*partial) *
            EnvGen.kr(Env.adsr(0, 0, 1.0, 5.0),
               trigger, 1/partial
            ) * max(0, LFNoise1.kr(rrand(5.0, 12.0)))
         })
         )*0.2,
   1.0.rand2)
});
Mix.ar(flashInst)*0.6
}.play
)
// 嘎嘎响的正弦波变种
(
{
   var harmonics = 16, fund = 50, speeds;
   speeds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]/5;
   Mix.fill(harmonics,
       { arg count;
           Pan2.ar(
              FSinOsc.ar(
                 fund * (count + 1),
                 mul: max(0, FSinOsc.kr(speeds.wrapAt(count)))),
              1.0.rand2)
        }
   ) / (2*harmonics)
}.play;
)
(
{
   var harmonics = 16, fund, speeds;
   speeds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]/20;
   fund = (MouseX.kr(0, 36).round(7) + 24).midicps;
   Mix.fill(harmonics,
      { arg count;
          Pan2.ar(
             FSinOsc.ar(
                fund * (count + 1),
                mul: max(0, FSinOsc.kr(speeds.choose))),
             1.0.rand2)
      }
   ) / (2*harmonics)
}.play;
)
// 用鼠标改变基础频率
(
{
   var harmonics = 16, fund;
   fund = (MouseX.kr(0, 36).round(7) + 24).midicps;
   Mix.fill(harmonics,
       { arg count;
           Pan2.ar(
              FSinOsc.ar(
                 fund * (count + 1),
                 mul: max(0, FSinOsc.kr(rrand(1, 1/3), mul: 20).softclip)),
              1.0.rand2)
       }
   ) / (2*harmonics)
}.play;
)
(
{
   var harmonics = 16;
   Mix.fill(harmonics,
         { arg count;
             Pan2.ar(
                FSinOsc.ar(
                   exprand(100, 2000),
                   mul: max(0, FSinOsc.kr(rrand(1/3, 1/6))*rrand(0.1, 0.9))),
                1.0.rand2)
         }
   ) / (2*harmonics)
}.play;
)

消散和汇聚的锣展示了如何通过复制一个想法来建立一个patch:典型的加法合成。它同样展示了如何用加法合成控制每一个谐波。用立体声耳机来听谐波的分支。

(
{
var dur = 6, base, aenv, fenv, out, trig;
base = Rand(40, 100);
trig = SinOsc.ar(1/10);
out = Mix.fill(15,{
   var thisDur;
   thisDur = dur * rrand(0.5, 1.0);
   aenv = EnvGen.kr(Env.perc(0, thisDur), trig);
   fenv = EnvGen.kr(Env.new([0, 0, 1, 0], [0.25*thisDur, 0.75*thisDur, 0]), trig);
   Pan2.ar(SinOsc.ar( Rand(base, base * 12) *
      LFNoise1.kr(10, mul: 0.02 * fenv, add: 1), // freq
      mul: aenv // amp
   ), ([1, -1].choose) * fenv)
}) * 0.05;
out
}.play(s);
{
var dur = 6, base, aenv, fenv, out, trig, detune;
base = Rand(40, 60);
detune = 0.1; // 增加这个值为第二个铃去谐
trig = SinOsc.ar(1/10, pi);
out = Mix.fill(15,
{ arg count;
   var thisDur;
   thisDur = dur * rrand(0.5, 1.0);
   aenv = EnvGen.kr(Env.perc(0, thisDur), trig);
   fenv = EnvGen.kr(Env.new([1, 1, 0, 1], [0.05*thisDur, 0.95*thisDur, 0]), trig);
   Pan2.ar(SinOsc.ar( base*(count+1+ detune.rand) *
      LFNoise1.kr(10, mul: 0.02 * fenv, add: 1), // freq
      mul: aenv // amp
   ), ([1, -1].choose) * fenv)
}) * 0.05;
out
}.play(s);
)
// 衰减的铃音
(
{
var aenv, fenv, out, trig, dur, base;
dur = rrand(1.0, 6.0);
base = exprand(100, 1000);
trig = Impulse.kr(1/6);
out = Mix.ar(
   Array.fill(15,{
      arg count;
      var thisDur;
      thisDur = dur * rrand(0.5, 1.0);
      aenv = EnvGen.kr(
          Env.new([0, 1, 0.4, 1, 0], [0, 0.5, 0.5, 0]), trig,
          timeScale: thisDur);
      fenv = EnvGen.kr(
          Env.new([0, 0, 0.5, 0.5, 0], [0.25, 0.5, 0.25, 0]),
              trig, timeScale: thisDur);
      Pan2.ar(SinOsc.ar( Rand(base, base * 12) *
          LFNoise1.kr(10, mul: 0.1 * fenv, add: 1), // freq
          mul: aenv // amp
      ), ([1, -1].choose) * fenv)
   })
) * EnvGen.kr(Env.linen(0, dur, 0), Impulse.kr(6), timeScale: dur,
      levelScale: 0.05, doneAction: 2);
out*0.3;
}.play;
)
Be Sociable, Share!

Published by

ww1way

http://about.me/ww1way

Leave a Reply

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