控制消息“if”
人工智能和计算机辅助创作始于逻辑控制。即,告诉机器在特定的情况下做什么。如果你饿了,do打开冰箱。如果里边没有面包,那么do去商店。如果你钱够多,do买四块烤肉。回家并再次打开冰箱。如果里边有果冻或果酱,do选其一做个三明治。不要用腌豆腐。有很多诸如while, for, 和 forBy 等迭代和控制的方法,但是do和if是最普遍的。
if消息或函数有三个引数:一个被评估的表达式,一个true函数,以及一个false函数。它评估表达式是真是假并返回结果。
if(表达式, {真函数}, {假函数})
真、假常由运算符比如大于、小于来判断(注意一个等号和两个等号间的区别。“=”意思是将某个值存入变量,”==”意思是“相等吗?”)。运行下例每一行。第一行评估并返回真(因为1的确等于1)因此它运行第一个函数。第二例是假,因此假函数被运行。
21.1. if 范例
if(1 == 1, {"true statement";},{"false statement";})
if(1 == 4, {"true statement";},{"false statement";})
// 注释版:
if(
1 == 1, //被评估的表达式; "1等于1" 对还是错?
{"true statement";}, //如果是对,运行此行
{"false statement";} //如果是错,运行此行
)
下边是其他的布尔运算符
< 小于
> 大于
<= 小于或等于
>= 大于或等于
!= 不等于
== 等于
消息or 合并两个声明,如果两者之一是对的则返回真:or(a > 20, b > 100)。消息and 合并两个声明,只有当两者都对时返回真:and(a > 20, b > 100)。单词true是真,false是假。
21.2. if 范例
if((1 == 1).and(5 < 7), {"全部都对"},{"只有一个对";})
if((1 == 20).and(5 < 7), {"全部都对";},{"一个错或全错";})
if((1 == 4).or(true), {"对永远是对";},{"1不等于4";})
if(false.or(true), {"对永远是对";},{"对用或者(or)赢了";})
if(false.and(true), {"对永远是对";},{"但错用和(and)赢了";})
if(or(10 > 0, 10 < 0), {34},{78})
if((1 == 1).and((10 > 0).or((5 < 0).or(100 < 200))), {78},{88})
这些孤立的数例在没有上下文关系对照时显得莫名其妙(比如,为什么我会用到表达式 10>0?为什么你仅仅是列印“真”或“假”?)if函数常被用于与一些迭代过程比如do结合使用。下边是一个真实的音乐范例。下边的代码由MIDI值60(C4)开始。然后它选取一个新的MIDI音程,将它加到m并返回那个新值。观察结果。
21.3. do 50 MIDI音程
(
m = 60;
50.do(
{
m = m + [6, 7, 4, 2, 11, 8, -2, -6, -1, -3].choose;
m.postln;
}
)
)
我倾向于更多高低音程起伏的选择。因此最后MIDI值超过了一个对于绝大多数乐器来说合理的范围。即使我已小心的平衡了这个选择,但在一列内一个正值将被选择20次是很明显的。下边的if表述检查每次循环后的值,如果超过84则减去两个八度,如果低于36则增加两个八度。
21.4. do 50 MIDI音程
(
m = 60;
50.do(
{
var next;
next = [6, 17, 14, 2, 11, 8, -12, -16, -1, -3].choose;
"next interval is : ".post; next.postln;
m = m + next;
"before being fixed: ".post; m.post;
if(m > 72, {m = m - 24});
if(m < 48, {m = m + 24});
"after being fixed: ".post; m.postln;
}
)
)
当写类似这样的小片段时,在SC里边闲逛一下,看看是否已经有某个函数存在并能做你想做的事情是值得的。wrap,它会将一个值包裹起来,但在本例中,我们做的是缓冲(buffering)而不是包裹(wrapping)。因此在这个状况下,我们必须得要加入额外的代码。
下例展示了在一个音类数组中的do循环与if协同测试查找C,D或E。电脑不可能按实际的音去理解这些(看下边弦乐的讨论),它们看到的只是文字。即使如此,它仍能比较它们是否相等。
21.5. 音类 do
(
["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"].do(
{arg item, count;
if((item == "C").or(item == "E").or(item == "G"), //布尔测试
{item.post; " is part of a C chord.".postln;}, //对函数
{item.post; " is not part of a C chord".postln;} //错函数
)
}
)
)
你可以说我们将会了计算机认识C和弦。这便是人工智能开始的地方。
if表达式也可用于在荧幕上定义一个区域。当鼠标进入指定的区域生成一个正值,或一个触发器。在本例中,* 相当于和(and)。这里没有对错,只有值1(开启)和0(关闭)。
21.6. 鼠标区域触发器
(
{
var aenv, fenv, mgate, mx, my;
mx = MouseX.kr(0, 1);
my = MouseY.kr(0, 1);
mgate = if((mx>0.3) * (mx<0.5) * (my>0.3) * (my<0.7), 1, 0);
aenv = EnvGen.kr(Env.asr(0.1, 0.5, 2), mgate);
fenv = EnvGen.kr(Env.asr(1, 1, 2), mgate, 1000, 100);
RLPF.ar(Saw.ar(100)*aenv, fenv, 0.1)
}.play
)
一个使用if很棒的小技巧控制何时postln而不是post,这并不列印新的一行:
21.7. 新行
(
100.do(
{
arg count;
100.rand.post;
if(count%10 == 9, //每个第9次
{" new line: ".postln;}, //列印一个回车
{" : ".post;} //仅是" * ",没有回车
);
}
)
)
while
while函数在第一个引数(一个评估函数)返回错误前重复第二引数(一个函数)。它对新手来说是一个危险的工具。如果你写一个永远不会返回假的条件,SC将永远运行下去(这是多数情况下造成程序锁死的原因)。下边第一例展示了一个常见的错误。代码在第二个函数在变量a中存储变量10前会持续重复。但10.rand只能选取0到9间的数字,因此它永远选不到10,因此在你强行停止前会无限运行下去。也正因此,也可以利用它给一个而外的条件使程序停止。
最后一行展示了while的典型用法。这行代码英告诉你它从100中选到数字13所用的时间长短。
21.8. while
// 典型的错误,因为a永远不会等于10
a = 0; while({a != 10}, {a = 10.rand}) // 死循环
// 在特定次数后停止显得更安全一些?
// 注意and的写法
c = 0; a = 0;
while({and(a != 10, c < 100)}, {a = 10.rand; c = c + 1;})
//while的典型用法。告诉你从100中挑到13需要多长时间。
(
a = 0; c = 0;
while({(a != 13).and(c < 10000)}, {a = 100.rand; c = c + 1;});
c
)
for, forBy
for允许你指定循环的开始与结束点,forBy包含一个步进值引数。在之前的章节,我们用奇数谐波建立了一个patch。计数器用于计算每个谐波,但由于它从0开始,我们不得不加1(counter+1).我们同样不得不仅为奇数谐波做一点数学计算。但有了forBy,这个计算可以被整合。
21.9. for, forBy
34.for(84, {|i| i.midicps.postln});
34.forBy(84, 5, {|i| [i, i.midicps].postln});
1.forBy(24, 2, {|i| [i, i*500].postln}); // 奇数谐波
( // 加法方波
{
o = 0;
1.forBy(24, 2, {|i| o = o + SinOsc.ar(i*500, mul: 1/i)});
o*0.4
}.scope
)
MIDIIn
MIDIIn从一个MIDI输入读取数据。有很多可用的方法。首先是触发一个你之前设计的乐器:
21.10. MIDI输入触发SynthDef
(
// 先定义一个乐器
SynthDef("simpleInst",
{
arg midi = 60, vel = 60, inst;
inst = {
SinOsc.ar(
freq: (midi.midicps)*(LFNoise0.kr(12) + 2).round(1),
mul: vel/256)
}.dup(5) *
EnvGen.kr(Env.perc(0, 3), doneAction: 2);
Out.ar(0, inst)
}
).play
)
MIDIIn.connect;
(
// 然后将它链到MIDI输入
MIDIIn.noteOn = {
arg src, chan, midiNum, midiVelo;
Synth("simpleInst", [\midi, midiNum, \vel, midiVelo]);
};
)
要控制其他SynthDef乐器,只需用乐器名替换"simpleInst"并确保挂上该乐器对应的引数即可。
实时篡改(Real-Time Interpolation)
MIDI输入的另一用法是在经过某些修改之后,将输入反映到你的MIDI设备(或其他设备)。乍一看,这看起来像一个很好的恶作剧,但同样可以用于篡改(interplotion)。篡改利用现存组件比如一个即兴器的结构或直觉的成就,将之映射到一部分或全部新材料的音乐元素上。比如说,一个钢琴家可能在即兴时用爵士共有的全音阶展示技巧,但如果被要求包含不同的音阶或构思、12音体系、合成音阶,或复杂的轮唱技术时就傻逼了。通过读取他使用他所熟知的材料进行的实时即兴,你可以将这流畅的直觉思维映射到更复杂的实验上。
在篡改数据时,你常不得不使用一个if表达式添加一个逻辑过滤器,允许一些材料不作改变就通过,同时改变其他的。下例用一种新奇的方式使用了过滤器。第一是一个简单的延迟,但它仅在力度高于60的情况下被添加,使用一个if表达式作为过滤器。力度60以下不作改变然后通过。
第二取MIDI输入并在C4轴反向后将之反射到MIDI设备。然而,if表达式只搜寻白键([0, 2, 4, 5, 7, 9, 11]%12)并仅将反向添加到那些MIDI值上。黑键不被反向。因此布鲁斯音阶C4, D4, Eb4, E4, F4, F#4, G4, A4, Bb4, C5 将变为 C4, Bb3, Eb4, Ab3, G3, F#4, F3, Eb3, Bb4, C3。不消说,这真的可以摆脱一个演奏者,因此为了效率,他们应该脱离监听正确MIDI信息的耳机,在听众仅听到篡改版的时候,激发精确反馈的直觉即兴。用多层、古典、即兴、不寻常音阶、调谐、改良的节奏等等尝试这个。可能性是无限的。
21.11. 通过if() 过滤器进行MIDI输入篡改
// 为力度高于60的做简单延迟
(
MIDIIn.noteOn = {arg src, chan, num, vel;
var delay = 0.5;
thisThread.clock.sched(delay, {m.noteOn(1, num, vel)});
};
MIDIIn.noteOff = {arg src, chan, num, vel;
var delay = 0.5;
thisThread.clock.sched(delay, {m.noteOff(1, num, vel)});
};
)
// 篡改: 仅反向白键
(
var white;
white = [0, 2, 4, 5, 7, 9, 11];
MIDIIn.noteOn = {arg src, chan, num, vel;
if(white.includes(num%12),
{m.noteOn(1, (60 - num) + 60, vel);},
{m.noteOn(1, num, vel)});
};
MIDIIn.noteOff = {arg src, chan, num, vel;
if(white.includes(num%12),
{m.noteOff(1, (60 - num) + 60, vel);},
{m.noteOff(1, num, vel)});
};
)
分析
MIDI输入最后的用处是做分析。下例读取MIDI值,计算距上个值的音程,然后将那个值存入一个数组,因此可以持续记录一个音乐家或即兴演奏家使用每个音程的频度。有两个版本。第一个以移动到任何音的动作来计数音程,不管是升是降,即使倒置(上边这段话的原文:The first counts an interval as motion to any pitch, whether up or down, as the same interval since they are inversions。翻译至此,忽然一阵头晕,再加上Cottle说话爱绕的写作风格,因此不确定能忠实反映原文意思。先放着,想到好的翻译再修正。):C4升到G4(五度)被记录得与C4降到G3(降4度)一样。第二版本,在例子中被注释了,以相等计数音程升降:C4升到G4(五度)与C4降到F3(降5度)一样。
21.12. 用if()过滤器做MIDI输入篡改
(
// 运行此以开始读取输入
var previousPitch = 62, thisInterval;
~intervals = Array.fill(12, {0});
MIDIIn.noteOn = {arg src, chan, num;
// 计数倒置为一样 Counts inversions as the same
thisInterval = (12 - (previousPitch - num))%12;
// 计数音程升降为一样 Counts intervals up or down as the same
// thisInterval = abs(previousPitch - num);
// 反注释以监视值
// [previousPitch, num, thisInterval].postln;
~intervals.put(thisInterval, ~intervals.at(thisInterval) + 1);
previousPitch = num;
// 反注释以实时查看
// ~intervals.postln;
};
)
// 一切就绪后,运行此行
~intervals;

篡改似应译为插值.
或许此处”篡改”较易理解.
@kakyo, 感谢你的指正,由于我并非计算机专业出身,因此在某些单词的翻译上肯定会存在问题,因此在对于我没有把握的词汇翻译后,一般我都会标注英文原文。是的,译为“篡改”完全出于我个人认为较易理解的初衷。