之前的部分处理合成。从这里开始,我们将通过使用之前章节的合成器定义,或外部MIDI乐器,用SC构建组织事件系统。
运算符,优先级
下例结合了数字及+, /, -, 和 * 等运算符。分别评估每行吧。
19.1. 运算符 (+, /, -, *)
1 + 4
5/4
8*9-5
9-5*8
9-(5*8)
最后三个表达式看起来很相似,但结果却不同。第三个是8*9,然后-5=67,第四个是9-5然后*8=32。区别是优先级。SC中的优先级很简单(但与你学校里学的不同):包围优先,从左至右。第三个表达式中由于8*9先出现,于是它在-5前被计算。第四个表达式中,9-5先被计算,然后它的结果被乘以8。最后一例表述了括号如何改变优先级。首先算5*8,因为它位于括号内,然后9再减它的结果。
你能在运行下边每行前预计结果吗?
19.2. 更多运算符
1 + 2 / 4 * 6
2 / 4 + 2 * 6
(2 * 6) - 5
2 * (6 - 5)
以下是另外一些二进制运算符。运行每行然后看SC返回什么:> 大于,< 小于,== 等于。% 模。 19.3. 二进制运算符 (>, <, ==, %)
10 > 5
5 < 1
12 == (6*2)
106%30
> 和 < 符号返回:真(true)和假(false)。SC知道10大于5(因此为真)以及5不比1小(假)。我们将把这一逻辑用于后边的章节。 模(%)是非常有用的运算符,它返回第一个数字处以第二个数字之后最小的余数。比如说,43%10 将返回3,12946%10 是6。一个普遍的音乐应用是模12,它将数字减至12一下,或一个八度。 你能预见这些表达式的结果吗? 19.4. 预知
(8+27)%6
((22 + 61) * 10 )%5
全部例子中使用的都是整数。小数被称为浮点(floating-point)值。在SC中,表述整数可以直接写对应的数字 (7, 142, 3452)。浮点值必须在小数点两边都有数字,即使是小于1的值:5.142, 1.23, 456.928, 0.0001, 0.5 (不是 .0001 或 .5)
消息,引数,接收器
你应该能自如地运用消息(message)、引数(argument)和接收器(receiver)。记住数字也可以是对象。消息常有一个有意义的名字,比如sum或abs,并伴随着装入被逗号分隔开的引数的括号。接下来的便是电脑音乐常用的典型的消息。在这前的章节,消息使用语法Object.message。这被称为接收器表示法。对象就是接收器。一个与之相同的语法在下例中引入,我为它使用了函数表示法:message(argument)。对象在引数列表中被置于第一位。5.rrand(10) 可以被表达为 rrand(5, 10)。同样的,min(10, 100) 可以被表达为 10.min(100)。
19.5. 音乐相关的消息
cos(34) //返回余弦 abs(-12) //返回绝对值 sqrt(3) //平方根 midicps(56) //给一个MIDI数字,在一个等音程内返回每秒周数 cpsmidi(345) //给每秒周数, 返回MIDI midiratio(7) //给一个MIDI音程, 返回比率 ratiomidi(1.25) //给一个比率, 返回MIDI数字 rand(30) //返回一个0~29间的随机数 rand2(20) //返回一个-30~30间的随机数 rrand(20, 100) //返回一个20~100间的随机数 // 接收器表示法范例 30.cos //等于cos(30) 0.7.coin //等于coin(0.7) 20.rand //等于rand(20) 7.midiratio // 二进制函数有两个引数 min(6, 5) //返回两个值的最小值 max(10, 100) //返回最大值 round(23.162, 0.1) //rounds first argument to second argument // 引数可以是表达式 min(5*6, 35) max(34 - 10, 4) //返回两个值的最大值
练习,音乐计算器
SC甚至在你不做音乐的时候也很有用。我常只将它用作一个音乐计算器:计算平均律Ab的频率,平均律5度和自然音程5度间有几分,或者说音程,两个频率间的分数(cent)。下边是几个例子。
19.6. 音乐计算器
// 大音阶频率 ([0, 2, 4, 5, 7, 9, 11, 12] + 60).midicps.round(0.01) // 大音阶音程率 [0, 2, 4, 5, 7, 9, 11, 12].midiratio.round(0.001) // 佛里吉亚音阶频率 ([0, 1, 3, 5, 7, 8, 10, 12] + 60).midicps.round(0.01) // 佛里吉亚音阶音程率 [0, 1, 3, 5, 7, 8, 10, 12].midiratio.round(0.001) // 平均和自然米索利地安音阶比较 [0, 2, 3, 4, 5, 7, 9, 10, 12].midiratio.round(0.001) [1/1, 9/8, 6/5, 5/4, 4/3, 3/2, 8/5, 7/4, 2/1].round(0.001) // 以平均律分表述的自然比率(米索利地安)Just ratios (mixolydian) in equal tempered cents // (以及它们相较平均律的偏差) [1/1, 9/8, 6/5, 5/4, 4/3, 3/2, 8/5, 7/4, 2/1].ratiomidi.round(0.01) // 倒向12-音配置(Retrograde of a 12-tone set) [0, 11, 10, 1, 9, 8, 2, 3, 7, 4, 6, 5].reverse // 反向12-音配置 Inversion of a 12-tone set 12 - [0, 11, 10, 1, 9, 8, 2, 3, 7, 4, 6, 5] // And of course, retrograde inversion (see where I'm heading?) (12 - [0, 11, 10, 1, 9, 8, 2, 3, 7, 4, 6, 5]).reverse // 12-音配置随机变调 ([0, 11, 10, 1, 9, 8, 2, 3, 7, 4, 6, 5] + 12.rand)%12 // 12-音设置随机置换 (479,001,600之外) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].permute(479001600.rand) + 60
函数,引数,范围
一个函数就是装入一对花括号内的一系列表达式。全部函数常(但不总是)被分配到一个变量。代码顺序被执行,并返回最后一行的结果。当函数被调用或在程序的任何地方运行,就好象所有的代码都被替代函数插入。一个函数被使用value消息评估。
一个没有变量或引数的简单函数:
19.7. 函数
( var myFunc; myFunc = {100 * 20}; myFunc.value.postln; )
第一行申明了被用于函数的变量。第二行是函数分配(让myFunc等同于{100 * 20},或者说将表达式100*20存入myFunc)。每个你放入myFunc的地方也等于说你放入了2000。
引数就像变量,但它们可以在函数被调用时传递给它。在左花括号之后申明引数。接下申明变量,来如果要使用它们的话。
下边是带引数的函数。
19.8. 有引数的函数
( var func; func = { arg a, b; var r; r = (b * 20)%a; r.postln; }; func.value(15, 5); )
将一个包含一个元素集合的数组传递给一个函数常常是很有用的,比如说,当函数中你想使用的元素数量倾向于改变的时候。考虑下例,求三个数的总合。如果你想求和一次,十次,接下来两次或六次,怎么办?使用引数,你可能不得不每次改变引数的数字,或写一个新的函数。一个解决方式是传递一个数组作为引数。要这么做,你必须首先申明一个数组变量,在它之前写三个句号,就像最后那个例子中的一样。
19.9. 用数组引数的函数
( var func; func = { arg a = 1, b = 2, c = 4; [a, b, c].sum; }; func.value(15, 5, 100); )
( var func; func = { arg ... a; a.postln; a.sum.postln; [a.sum, 1.0.rand.sum].sum }; func.value(15, 5, 100); func.value(15, 5, 100, 3, 78, 18, 367); func.value(1, 2); )
结合所有语法
( var func; func = { arg a = 0, b = 0 ... c; [a, b, c].postln; c.sum.postln; [c.sum, 3.0.rand.postln].sum.postln; (a/b*c).postln; }; func.value(15, 5, 100, 45); func.value(15, 5, 100, 3, 99, 754, 78, 18, 367); func.value(1, 2, 3, 4, 5); )
范围(scope)描述一个变量或引数的有效范围。一个变量仅能申明它的函数内部使用。在那个函数内的函数可以利用外部变量。一个全局变量,用波浪号申明,在任何地方都可用。
19.10. 有引数和变量的函数
var func, outside = 60; ~myGlobal = 22; func = { arg first = 5, second = 9; var inside = 10; inside = (first * 11)%second; [first, second, inside, outside, ~myGlobal].postln; // 全部都行得通 (outside/inside).postln; //行得通 }; //inside.postln; // 反注释这一行,行不通 func.value(15, 6); // 引数被传递入函数
你能在运行下边代码之前预测到myFunc调用的值吗?第一例没有引数传递入函数并将使用默认(10和2)。接下来的两个使用默认。
19.11. 函数调用
(//行1 var myFunc; myFunc = { arg a = 10, b = 2; b = (b * 100)%a; b.postln; }; myFunc.value; //行7 myFunc.value(15); //行8 myFunc.value(11, 30); //行9 )
你也可以为你自己的函数使用关键字。
19.12. 关键字
( var myFunc; myFunc = { arg firstValue = 10, secondValue = 2; firstValue = (firstValue * 100)%secondValue; firstValue.postln; }; myFunc.value; myFunc.value(firstValue: 15); myFunc.value(firstValue: 30, secondValue: 11); myFunc.value(secondValue: 30, firstValue: 11); myFunc.value(secondValue: 23); )
在之前的例子中,最后一行列印最后结果。但在绝大多数函数中,值会回到函数被调用的地方。函数的最后一行被返回。这是一个有一点点音乐感觉的例子。
19.13. 返回
( var octaveAndScale; octaveAndScale = { arg oct = 4, scale = 0; var scales, choice; oct = (oct + 1)*12; //将"4" (C4)转换到MIDI八度(60) scales = [ [0, 2, 4, 5, 7, 9, 11], //大调 [0, 2, 3, 5, 6, 8, 9, 11], //八音 [0, 2, 4, 6, 8, 10] //全音 ]; scale = scales.at(scale); //more on the "at" message below choice = scale.choose; //选一个音高 choice = choice + oct; //增加八度 choice //返回最后的结果 }; octaveAndScale.value; //choose from major scale, C4 octave octaveAndScale.value(3); //choose from C3 octave, major scale octaveAndScale.value(7, 2); //choose from C7 octave, whole tone scale octaveAndScale.value(scale: 1); //choose from C4 octave, octatonic scale )
什么时候你该使用一个函数?我们在目前几乎所有例子中都使用了它们。像max,choose,midicps这样的信息,是被SC作者放到一起的函数(以接收器表示法)。在何时谐你自己的函数?便利或清楚,比方说当你一次又一次使用你自己开发的一段代码时。相对在每个地方重复这些代码,写和使用一个你自己的函数将会更清晰高效。另一情况是当没有现存的消息或函数但你却又正好需要他们的时候,因此你只能自己去写。
练习,闪烁
在这个patch里,函数返回下一个将被用于闪烁(flashing)乐器里的频率。每个新的音都将基于之前的音来计算,因为它使用纯律,就像我们在之前章节里做的那样。我将列印实际的音,以及在平均律中最近的等价物。
它从之前的一个patch开始,只不过用引数和SynthDef替代了一些东西:fundamental, decay, 和 filter。这个乐器被task和loop完美演奏。在进入循环前,我定义了freqFunc,它基于上一个音和新的纯音程率选取一个新的音。这被称为自由纯律(在实体乐器上很难实现,在电脑上却很容易)。我添加了一个wrap以使频率保持在一个特定范围内。nextEvent不仅控制着下一个事件将是什么,而且那个值还被传递入Flash乐器作为衰减,以确保第一个声音在第二个声音响起后迅速衰减。在这个patch内,我不敢确保滤波器会以它应该是的工作方式工作。
19.14. 函数练习, free, just tempered flashing
( //先运行这个 SynthDef("Flash", { arg fund = 400, decay = 4, filter = 1; var out, harm; out = Mix.ar( Array.fill(7, { arg counter; var partial; partial = counter + 1; SinOsc.ar(fund*partial) * EnvGen.kr(Env.linen(0, 0, decay + 2), levelScale: 1/(partial*filter) ) * max(0, LFNoise1.kr(rrand(5.0, 12.0))) }) )*0.3; //整体音量 out = Pan2.ar(out, Rand(-1.0, 1.0)); DetectSilence.ar(out, doneAction:2); Out.ar(0, out) } ).play(s); ) ( //然后这个 r = Task({ var freqFunc, pitch = 440, nextEvent; freqFunc = {arg previousPitch; var nextPitch, nextInterval; nextInterval = [3/2, 2/3, 4/3, 3/4, 5/4, 4/5, 6/5, 5/6].choose; nextPitch = (previousPitch*nextInterval).wrap(100, 1000); nextPitch.round(0.01).post; " != ".post; nextPitch.cpsmidi.round(1).midicps.round(0.01).postln; nextPitch }; { nextEvent = [0.5, 0.25, 5, 4, 1].choose; pitch = freqFunc.value(pitch); Synth("Flash", [\fund, pitch, \decay, nextEvent, \filter, rrand(1.0, 4.0)]); //选择一个下一事件前的等待时间 nextEvent.wait; }.loop; }).play )
练习:函数范例
19.15. 音函数
var freqFunc, pitches, pitch = 440, count, midiNote, nextEvent; pitches = [60, 61, 62, 63, 64]; //申明一个音的数组 freqFunc = { midiNote = pitches.choose; //从数组选取一个音 midiNote.midicps; // 返回那个音的cps }; var freqFunc, pitches, pitch = 440, count, midiNote, nextEvent; pitches = [60, 62, 64, 67, 69, 72, 74, 76]; //申明一个音的数组 count = 0; //初始化计数 freqFunc = { midiNote = pitches.wrapAt(count); // 包裹的计数目录 if(count%30 == 29, //每九次 {pitches = pitches.scramble} //reset "pitches" to a scrambled //verion of itself ); count = count + 1; //递增 midiNote.midicps; //返回cps }; // 我的最爱: var freqFunc, pitches, pitch = 440, count, midiNote, nextEvent; pitches = [60, 62, 64, 67, 69, 72, 74, 76].scramble; freqFunc = { midiNote = pitches.wrapAt(count); // 包裹计数目录 if(count%10 == 9, //every tenth time {pitches.put(5.rand, (rrand(60, 76)))}//put a new pitch between //65 and 75 into the array pitches //at a random index ); count = count + 1; //递增 midiNote.midicps; //返回cps };