人工智能即用计算机听得懂的语言向其描述人类现象:数字、可能性和规则。任何时候,紧缩计算机在音乐框架内的选择,从某种意义上来说,是在教它们某些关于音乐的东西,并让它们做出“聪明的”或见多识广的选择。对随机游走来说也是一样的:如果你紧缩MIDI音的选择,例如说,你教会了patch(通过将MIDI值转为cps)音阶、比率、音程和平均律。如果你将随机选择限制在C大调音阶,那么cpu便对一个音节中全音程与半音程的关系变得“聪明”。如果我们倾向于C比其他音有更多被选择的机会,那么cpu就有一点点懂得调性(tonality)了。这种倾向映射经常以比率或可能性的形式出现。在简单的倾向性随机选择里仅有一级可能性:在音阶内每个可能选择的可能性。这被称为零阶可能性马尔可夫过程(Markov Process with a zeroth order probability)。
零可能性系统将不会给我们逻辑连续的感觉,因为音乐根本上是依靠音与音之间的关系,而不是单独的音本身或音的整体分布。我们通过当前音和下一个音的、最后三个音的、一分钟前听到的音的关系来感知旋律和音乐进行。为了描述一段旋律,你不得不去描述音与音之间的联系,例如音程。
要得到音之间的关系,我们需要使用一个高阶马尔可夫链:至少一阶或二阶。这个技术在Charles Dodge写的Computer Music(page 283)和Moore写的Elements of Computer Music (page 429)中都有描述。我建议你阅读那几章,但我也会在这进行解释。
描述两个音之间联系的方式是将一个下一个可能音图表(chart)给到当前音。用C键的G音举例。如果你想依据可能性描述调性,你会说C跟随G(结果是V-I关系)的机会大于F跟随G(逆向的V-IV)的。如果说当前音是F,那么下一个音是E(IV的分辨)的机会便大于是C(逆向)的机会。马尔可夫链并不打算只做调性音乐。在非调性音乐中,你可能同样通过避免G和C的联系来描述关系。因此如果当前的音是G并避免调性关系,那么下一个音是C的机会就很小,但下一个音是升D或降A的机会就更大。
你可以用马尔可夫链描述任何类型的音乐。你甚至可以基于现存作品的分析模仿它作曲家的风格。例如,你可以分析所有Stephen Foster写的曲调,检查G音(或与之同调的)以及伴随每个G音的音符。然后你可以生成一个所有跟随G的可能性的图表。计数他音乐中每个跟随音的每次发生并将那个数字输入图表。这是一个精准的Stephen Foster对待G音(或音阶的五分之一步)的可能性图表。
如果我们基于我们的分析断定了一个音的可能性,下一步便是为所有可能的当前音计算同样的可能性并将它们合并入图表。这被称为转换表。要创建曲子”Frere Jacque”的如上分析,你需要首先在第一列创建所有当前可能音的图表,在每列之上用所有下一个音标注一行。第一个音是C,跟着是D。我们通过在D列下C行写1表述这种可能性组合。
接下来,我们汇总D跟随C的次数并在那列输入那个数字(2)。接下来检测C,计数C, E, F, G跟随C的次数,然后输入那些值。然后顺序为音D, E等等照搬这个程序。
每一行的汇总都被列出。每行的可能性计算:每列实际发生数 除以 全部可能结果,因此C列的值将是1/4, 2/4, 1/4。
这是一个一阶(first order)转换表。因为我们仅使用前一个和下一个音符(一个连接),因此将失去旋律进行的令人信服的感觉。要模拟一段旋律,我们真的需要着眼于两或三个音符的模式。这将我们带至二阶(secod order)马尔可夫链。二阶增加了一级音序。即,给最后两个音,下一个音是C, D等等的可能性是什么。这是扩大到涵盖整个”Frere Jacque”的同样的图表,以及一个二阶可能性。有36种组合,但并非它们所有都会发生(比如C-A),无需将它们涵盖入图表,因此我从那些组合中移除了它们。
这有几条指导方针:注意我是在底部汇总了所有组合。这是一个快速检查总链接数是否正确的方式。总数应当是这个片段音符数减二(因为前两个没有三个项目(或二阶)的连接)。另外一件你需要关注的事情是死链接。死链接是在图表中不可能的一行的连接。以C-C组合为例。如果你在C-C行输入一个F列的可能性,那么组合C, C, F会产生。但却没有可能的C-F行,这种情况下程序将返回一个nil值并崩溃(或陷入一个循环)。我没有一个快速或聪明的方式检查你是否有坏的倾向。你只能靠自己细心校对。
百分比图表。
这种体系最大的问题在于内存需求。如果,尼汝说,你要做一个韦伯恩钢琴作品的图表,假设一个四个八度的范围,每个八度12个音,二阶可能性会为每个单独的音要求一个110592引用(reference)的矩阵。如果你将这个模型扩展到涵盖节奏和乐器选择,动态与结合,你会马上处于无数中。因此需要一个更高效的描述矩阵的方式。这也是在下边Froster例子中我做了一些易混淆的动作的原因,但空间节约了捷径(but space saving short cuts)。上边”Frere Jacque”的图表在文件Simple Markov.中进行论证。接下来是一些代码的解释。
29.1. 转换表
//使用的音的集合 legalPitches = [60, 62, 64, 65, 67, 69]; //一个二维数组, 代表每对之前可能的组合 transTable = [ [0, 0], //C, C [0, 1], //C, D [0, 2], //C, E [0, 4], //C, G [1, 2], //D, E [2, 0], //E, C [2, 3], //E, F [3, 2], //F, E [3, 4], //F, G [4, 0], //G, C [4, 2], //G, E [4, 3], //G, F [4, 4], //G, G [4, 5], //G, A [5, 4] //A, G ];
使用真实的midi值是低效的,因为如此多的midi值跳入了一个调性计划(tonal scheme)。因此legalPitches被用于描述我所有要用的音,并且实际代码寻找并围绕数组位置工作,不是midi值。(即,包含midi值的数组位置)
变量transTable描述我转换表的第一列。每个可能的之前音被存储于二维数组中。
用来对比与储存当前两个音的值是currentPair。这是一个包含两个元素的单独数组,组合中的第一、二个音我将用于链中。程序之初,它们被设置为0,0或C, C。
下一步,我需要将currentPair与数组transTable匹配。这由do循环搞定。在这个函数内,每个二位(two position)数组将被与变量currentPair(同样是一个二维数组)进行比较。当一个匹配被发现,那个匹配(或者在数组中被发现的位置)的索引便被储存到nextIndex。换句话说,我发现了currenPair的索引位置。这是必要的,因为我将表削减为仅包括我需要用到的组合。
29.2. 剖析转换表
transTable.do({arg index, i; if(index == currentPair, {nextIndex = i; true;}, {false})});
接下来我为每个之前的组合描述了索引。如果,例如,当前组合为D, E。它们在transTable中的值应为[1, 2],之上的代码应在数组索引4(记得从0数起)发现这个匹配。这意味着,我应在下边的图表中使用可能性数组的第四位。在这个图表中,我有100%的机会在currentPair用C跟随D, E。我修改(已注明)了Dodge原始图表的可能性。
29.3. 可能性图表
nPitchProb = [ //C D E F G A [0.00, 0.33, 0.00, 0.00, 0.66, 0.00], //C, C [0.00, 0.00, 1.00, 0.00, 0.00, 0.00], //C, D [0.00, 0.00, 0.00, 1.00, 0.00, 0.00], //C, E [0.66, 0.00, 0.00, 0.00, 0.00, 0.33], //C, G [1.00, 0.00, 0.00, 0.00, 0.00, 0.00], //D, E [0.50, 0.00, 0.25, 0.00, 0.25, 0.00], //E, C [0.00, 0.00, 0.00, 0.00, 1.00, 0.00], //E, F [1.00, 0.00, 0.00, 0.00, 0.00, 0.00], //F, E [0.00, 0.00, 0.50, 0.00, 0.50, 0.00], //F, G [1.00, 0.00, 0.00, 0.00, 0.00, 0.00], //G, C [0.00, 0.00, 0.00, 1.00, 0.00, 0.00], //G, E [0.00, 0.00, 1.00, 0.00, 0.00, 0.00], //G, F [0.00, 0.00, 0.00, 0.00, 0.00, 1.00], //G, G [0.00, 0.00, 0.00, 0.00, 1.00, 0.00], //G, A [0.00, 0.00, 0.00, 1.00, 0.00, 0.00] //A, G ];
选择实际上由windex完成。函数windex(加权索引)使用可能性数组作为其第一引数。数组nPitchProb是个二维数组,我想要用那些数组中的一个作为我的可能性数组,比如说索引4。我识别数组中的数组的方式是nPitchProb.at(4)。因为这是一个多维数组,我不得不引用其两个,例如nPitchProb.at(4).at(5)。
使用变量就变为:(nPitchProb.at(prevPitch).at(nextIndex)。windex数组总合为1,我并不记得为什么我输入值的总合为16,但这可以用normalizeSum搞定。windex返回值是一个数组位置,这我将存储于nextIndex,并且告诉我将哪个数组应用于nextPitchProbility的变量也是nextIndex。
变量nextPitch是一个数组位置,它可以之后与legalPitches协同返回正确音的midi值:legalPitches.at(nextPitch)。它同样被用于一些必需的记录。我需要重新排列currentPair来反映我新的选择。数组currentPair第二位置的值需要被移至第一位置,nextPitch值需要被储存于数组currentPair的第二位置。(换句话说,currentPair是D, E,或数组位置1,2,我选取了C,或对照表,一个0。因此,[1, 2]需要在下次经过函数时变为[2, 0]。)
currentPair.put(0, currentPair.at(1));
currentPair.put(1, nextPitch);
下例展示了一个更复杂的转换表和马尔可夫过程。使用了Dodge写的Computer Music第287页Stephen Foster曲调的详细内容,我不久前将它写成,并且我想还有更高效的制表方法(我搞了一些易混淆的捷径),但无论如何,它起作用了。
持续时间你可以使用第一行:0.125。这可能是一个更精确的实现,因为我们未讨论节奏。但为了给它给多的旋律的感受,我加入了一些随机的节奏单元。我们没有给这个系统任何关于语法结构或韵律的信息,并且你可以听到那些缺失的成分。尽管如此,它还是非常Foster。
29.4. Foster马尔可夫
( var wchoose, legalPitches, previousIndex, prevPitch, currentPitch, nextIndex, nextPitch, nPitchProb, pchoose, blipInst, envelope, pClass, count, resopluck; prevPitch = 3; currentPitch = 1; count = 1; pClass = #["A3", "B3", "C4", "D4", "E4", "F4", "F#4", "G4", "A4", "B4", "C5", "D5"]; //pchoose是选取下一个值的机制. pchoose = { legalPitches = [57, 59, 60, 62, 64, 65, 66, 67, 69, 71, 72, 74]; //prevPitch和nextPitch都不是音, 而是数组位置. previousIndex = [ [2], //之前是0或A3 [2], //1或B3 [0, 1, 2, 3, 4, 5, 7, 9, 10], //2: C4 [1, 2, 3, 4, 7, 10], //3: D4 [2, 3, 4, 5, 7, 8], //4: E4 [4, 5, 7, 8], //5: F4 [7], //6: F#4 [2, 4, 5, 6, 7, 8, 10], //7: G4 [2, 4, 5, 6, 7, 8, 10], //8: A4 [8, 10], //9: B5 [7, 8, 9, 10, 11], //10: C5 [7, 9] //11: D5 ]; previousIndex.at(prevPitch).do({arg index, i; if(index == currentPitch, {nextIndex = i; true;}, {false})}); nPitchProb = [ // [00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11] array position // A, B, C, D, E, F, F#, G, A, B, C, D [ //A3数组 [00, 00, 16, 00, 00, 00, 00, 00, 00, 00, 00, 00] // 一个数组: C4 ], [ //B3数组 [00, 00, 05, 06, 00, 00, 00, 05, 00, 00, 00, 00] // 仅是C4 ], [ //C4数组 [00, 00, 16, 00, 00, 00, 00, 00, 00, 00, 00, 00], // A3 [00, 00, 16, 00, 00, 00, 00, 00, 00, 00, 00, 00], // B3 // [00, 02, 02, 09, 02, 10, 00, 00, 00, 00, 00, 00], 初始的C4 [00, 06, 02, 09, 02, 06, 00, 00, 00, 00, 00, 00], // C4 [00, 00, 03, 04, 08, 00, 00, 01, 00, 00, 00, 00], // D4 [00, 00, 00, 07, 03, 02, 00, 04, 00, 00, 00, 00], // E4 [00, 00, 00, 00, 11, 00, 00, 00, 05, 00, 00, 00], // F4 [00, 00, 00, 00, 04, 00, 00, 12, 00, 00, 00, 00], // G4 [00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 16, 00], // A4 [00, 00, 00, 00, 00, 00, 00, 02, 11, 03, 00, 00] // C5 ], // A, B, C, D, E, F, F#, G, A, B, C, D [ //D4数组 [00, 00, 16, 00, 00, 00, 00, 00, 00, 00, 00, 00], // B4 // [01, 00, 01, 04, 05, 00, 00, 01, 00, 01, 03, 00], 初始的C4 [05, 00, 01, 04, 01, 00, 00, 01, 00, 01, 03, 00], // C4 // [00, 01, 12, 01, 02, 00, 00, 00, 00, 00, 00, 00], 初始的D4 [00, 06, 07, 01, 02, 00, 00, 00, 00, 00, 00, 00], // D4 [00, 00, 01, 03, 06, 04, 00, 01, 01, 00, 00, 00], // E4 [00, 00, 00, 00, 00, 00, 05, 08, 03, 00, 00, 00], // G4 [00, 00, 00, 00, 00, 00, 00, 00, 00, 16, 00, 00] // C5 ], [ //E4数组 [00, 00, 00, 12, 03, 01, 00, 00, 00, 00, 00, 00], // C4 // [00, 02, 07, 03, 02, 00, 00, 01, 00, 01, 00, 00], 初始的D4 [00, 05, 04, 03, 02, 00, 00, 01, 00, 01, 00, 00], // D4 [00, 00, 03, 04, 06, 02, 00, 01, 00, 00, 00, 00], // E4 [00, 00, 00, 00, 04, 03, 00, 06, 03, 00, 00, 00], // F4 [00, 00, 00, 00, 02, 00, 00, 10, 03, 00, 01, 00], // G4 [00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00, 00] // A4, ], // A, B, C, D, E, F, F#, G, A, B, C, D [ //F4数组 [00, 00, 00, 08, 00, 08, 00, 00, 00, 00, 00, 00], // E4 [00, 00, 00, 00, 00, 08, 00, 08, 00, 00, 00, 00], // F4 [00, 00, 02, 00, 00, 00, 00, 10, 00, 00, 04, 00], // G4 [00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00, 00] // A4, ], [ //F#4数组 [00, 00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00] // G4, ], [ //G4数组 [00, 00, 00, 11, 05, 00, 00, 00, 00, 00, 00, 00], // C4 [00, 00, 05, 04, 03, 01, 00, 02, 01, 00, 00, 00], // E4 [00, 00, 00, 00, 16, 00, 00, 00, 00, 00, 00, 00], // F4 [00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00, 00], // F#4 [00, 00, 00, 00, 04, 01, 04, 04, 03, 00, 00, 00], // G4 [00, 00, 01, 00, 01, 00, 05, 07, 01, 00, 01, 00], // A4 [00, 00, 00, 00, 00, 00, 00, 06, 05, 03, 02, 00] // C5 ], // A, B, C, D, E, F, F#, G, A, B, C, D [ //A4数组 [00, 00, 16, 00, 00, 00, 00, 00, 00, 00, 00, 00], // C4 [00, 00, 00, 11, 05, 00, 00, 00, 00, 00, 00, 00], // E4 [00, 00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00], // F4 [00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00, 00], // F#4 [00, 00, 01, 00, 09, 01, 00, 02, 01, 00, 02, 00], // G4 [00, 00, 00, 00, 02, 00, 00, 12, 00, 00, 02, 00], // A4 [00, 00, 00, 00, 00, 00, 00, 09, 02, 05, 00, 00] // C5 ], [ //B5数组 [00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00, 00], // A4 [00, 00, 00, 00, 00, 00, 00, 00, 06, 00, 00, 10] // C5 ], // A, B, C, D, E, F, F#, G, A, B, C, D [ //C5数组 [00, 00, 00, 00, 14, 00, 00, 02, 00, 00, 00, 00], // G4 [00, 00, 00, 00, 00, 01, 00, 05, 06, 00, 04, 00], // A4 [00, 00, 00, 00, 00, 00, 00, 00, 12, 00, 04, 00], // B4 [00, 00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00], // C5 [00, 00, 00, 00, 00, 00, 00, 05, 00, 11, 00, 00] //D5 ], [ //D5数组 [00, 00, 00, 00, 00, 00, 00, 16, 00, 00, 00, 00], // G4 [00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 16, 00] // B4 ] ]; nextPitch = (nPitchProb.at(prevPitch).at(nextIndex).normalizeSum).windex; //当前的被设置为之前的, 下一个是下一轮的当前. //实际的音是从nextPitch返回的合法音 [pClass.at(nextPitch), legalPitches.at(nextPitch)].post; // if((count%10) == 0, {"".postln};); count = count + 1; prevPitch = currentPitch; currentPitch = nextPitch; legalPitches.at(nextPitch) }; Pbind( \dur, 0.125, \dur, Prand([ Pseq(#[1]), Pseq(#[0.5, 0.5]), Pseq(#[0.5, 0.5]), Pseq(#[0.25, 0.25, 0.25, 0.25]), Pseq(#[0.5, 0.25, 0.25]), Pseq(#[0.25, 0.25, 0.5]), Pseq(#[0.25, 0.5, 0.25]) ], inf), \midinote, Pfunc(pchoose), \db, -10, // \instrument, "SimpleTone", \pan, 0.5 ).play )
数据文件,数据类型
像之前章节说的,把转换表存入一个文件可能是很有用的,这样一来,程序就可与不同创作的特定表独立开来。数据文件之后可以被用作基础马尔可夫patch的模块组件。
文本文件,包含字符。但计算机是通过数字(ascii数字)来认知它们。你使用的文本编辑程序将数字转化为字符。你可以使用SimpleText创建一个包含字符代表一个转换表的文件,但那些数字不仅是数字,相当于字符。对cpu来说,“102”不是整数102,而是代表102的三个字符(它们的ascii数字为49,48和50)。下边的图表(见原书P321)展示了ascii数字以及它们对应的字符。低于32的是非打印字符,比如回车,tab,beep和段落标记。空格(32)的ascii数也被包括在了这里,因为它如此普遍。这个图表停止于127(8位数字最大值),但仍有大于127的ascii字符。类似的字符通常是可区别的组合和拉丁字母。
如果你想测试的话,用BBEdit,SimpleText或MS Word(仅保存文本)创建一个文本文档并运行这几行代码,它们检索文本文档内每个8位整数然后作为整数列印,之后是ascii对应的。
29.5. ascii测试
var fp; fp = File("Testascii.rtf", "r"); //打开一个文本文档 fp.length.do({a = fp.getInt8; [a, a.ascii].postln}); //以整数读取文件
适合做转换表(整数或浮点)的数据可以被文本编辑器打开,但可能显示乱码,而不是转换数据。因此问题是,该如何创建一个数据文件?真并不像一个文本文件那么简单。这必须由一个读写不仅仅是字符还包括数据流的程序完成。SC可以创建这样的文件。(但还请继续读下去,因为还有更简单的方法。)
上边的转换表使用整数,但可能性表示用浮点数。区分两者很重要。下边是读写包含整数和浮点值文件的代码。对于整数,有三个消息:putInt8, putInt16, 和 putInt32 分别用于8位(一个字节),16比特(两个字节),和32字节(四个字节)。每个大小都有个限制容量。一个8位整数最长到128,16位可以长到32768,32位有多于20亿的容量。对于浮点数,有两条信息:putFloat 和 putDouble。“Double”更大因此更精确,但它们占据两倍空间,其实Float对我们正在做的东西也足够了。对于字符来说,消息putChar和putString可被使用。
理解这些数据类型很重要,因为你需要读取和你所写的同样的类型。如果你以16位整数写入但却以8位整数读取它们,数字将会不同。接下来是读写浮点值和整数的代码段。
29.6. 数据文件
var fp, data; fp = File("TestInt", "w"); //打开一个文件 data = [65, 66, 67, 68, 69, 70, 71]; data.do({arg eachInt; fp.putInt16(eachInt)}); //将每个整数放入文件 fp.close; var fp, data; fp = File("TestInt", "r"); //打开一个文件 data = fp.readAllInt16; //以整数数组读取一切 data.postln; fp.close; var fp, data; fp = File("TestFloat", "w"); //打开一个文件 data = [6.5, 6.6, 6.7, 6.8, 6.9, 7.0, 7.1]; data.do({arg eachFloat; fp.putFloat(eachFloat)}); fp.close; var fp, data; fp = File("TestFloat", "r"); //打开一个文件 data = fp.readAllFloat; //以数组读取一切 data.postln; fp.close;
我选择写整数65~71,因为它们与ASCII字符A~G一致。位证实这点,用SimpleText, MS Word, 或 SC打开TestInt文件(应该仅含整数,没有字符),证实这些程序已将它们转换为文本字符。
解释字符串
包含整数和浮点数的文件仍将呈现一个问题。管理数据比较困难,尤其它们被集成到二维数组,比如马尔可夫链。的确没有简单便捷的方法来检查文件的数据正确有否。你无法以一个文本文件来读它。你不得不以8,16,32位或浮点值的形式读取它。如果你碰到任何不对的数据类型,或者如果一个值在错误的位置,结构和数组将被关闭。(别误会:这不可能完成,它将仅是一个麻烦(hassle)和错误请求(invites error)。)
如果使用一个文本编辑器或SC创建文件将更简单一些,但以数据文件的方式读取它们。或者以字符串方式读取它们但将它们分析为正确的数据。
对于C的用户来说,你们知道管理和分析数据意味着大量的编程。一如往常,在SC中这很简单。interpret消息将一个字符串翻译为SC懂得的代码。你可以保存全部函数、数据结构、列表、错误信息,以及可以用SC和任何文本编辑器编辑的文件中的宏指令,但在SC patch中被用作代码。
欲测试这点,首先打开一个新窗口(仅文本,非富文本)并输入代表二维数组的这几行,包含不同的数据类型(注意我并未以一个分号结尾):
[ [1, 2, 3], [2.34, 5.12], ["C4", "D4", "E4"], Array.fill(3, {rrand(1.0, 6.0)}) ]
我人为地使用若干不同数据类型填充数组,包括字符串,以此阐明在SC中这多么简单,而在其他更古老的语言中有多复杂。如果我是使用C编译器来管理文本,我必须保持对下列的持续捕捉:每种数据类型、每个数组中的项目数、每行的总大小和长度,等等。总之很麻烦。
运行下述代码,注意,当读取文件的初始,第一个postln显示那个数组确实是一个字符串,但在后来的代码中,它却被当作代码对待。
29.7. 解释字符串
var fp, array; fp = File("arrayfile", "r"); array = fp.readAllString; //将文档读入一个字符串 array.postln; //列印它,以证明它是一个字符串 array = array.interpret; //再次解释并储存它 array.at(0).postln; //正是它是代码而非字符串 array.at(1).sum.postln; array.at(2).at(0).postln; array.at(3).postln;
这个方法的好处是,我可以用BBEdit或其他任何文本编辑器打开arrayfile,并将数据当作文本来修改。更有前途的:我可以从数据库例如FileMaker Pro或Excel将数据当作一个文本文件导入。任何可以被存为文本的数据源都可以被读入SC程序。现在,便可以为马尔可夫patch使用含不同可能性数据的模块文件了。
[延伸讨论:遗传机率(genetic probabilities)]
很有意思阿。这个表有很多0,用邻接链表存能省很多空间吧。你这里用的是什么语言?
@fork, SuperCollider