SC:运算符,优先级,引数,表达式和用户自定义函数

之前的部分处理合成。从这里开始,我们将通过使用之前章节的合成器定义,或外部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
};
Be Sociable, Share!

Published by

ww1way

http://about.me/ww1way

Leave a Reply

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