本章,我们将测试与额外音乐准则相连的音乐选择的可能性。几乎所有音乐都对创作者的选择具有音乐性上的影响,这里,我们将强化那个联系。这些准则可能包含诸如植物的电子脉冲、星星的位置、一个山的轮廓、或者雨雪的模式等等自然数据;它们可能包含类似其他音乐、文字、发车时间表等人类体系;或者掩饰的输入(masked input),比如在演出空间中,动作被映射到音高、振幅、音色等等之上的一个听众。
任何数据流,无论是从一个现存的文件读取,还是来自一个实时的输入源(比如鼠标或视频链接),可以被用于影响音乐的参数。SC提供了若干外部控制源,诸如MouseX和Y、Wacom手写板控制、MIDI输入、读取文档数据等等。
为什么要用其他数据源?因为模式和结构可以从额外音乐源传送至创作。一副星图,举个例子,用X、Y位置可以生成一组均匀分布的事件;一个山的轮廓将提供一个平滑的连续事件流。
除了外部标准提供的结构,还有一个为创作带来额外意义的哲学联系,例如,从声纹(voice print)中提取的音色,或一场演出中最喜欢的那首诗。
历史提供了很多关于这种类型主题连接的例证,特别是文字对创作的影响。例子不仅仅包括音乐化一个词汇,还包括对词汇精准的音乐符号化复制,就像巴赫的二重赋格,从四个音Bb, A, C, B(在德国是:B, A, C, H)开始。文字是一个普遍性选择,因此让我们从这开始吧。
最简单的将文字与音乐联系的方法是给每个字母一个MIDI值:a=0,b=1,z=23等等。这将提供两个八度的音程。字母表的字母在计算机代码内被描述为ascii值。即,计算机将字母“a”理解为一个整数。这对我们来说是一个方便的巧合。ascii数字已经存在于计算机里,我们仅需要调整它们到合适的范围即可。
文字转换
digit或ascii消息可以被用于将一个字符转换为数字。在digit下,”a” 和 “A” = 10, “z” 和 “Z” = 35。消息ascii返回实际的ascii值:A = 65, a = 97, Z = 90, z = 122。再次尝试运行下边代码以证实ascii的关联。
28.1. ascii值
a = "Test string"; a.at(2).ascii.postln; a.at(2).digit.postln; a.do({arg each; each.post; " ".post; each.ascii.postln;})
ascii值(大写字母的)的范围同样便利地与midi值匹配:65(F4的midi数)至90(F6的midi数)。但小写字母就有一点点超过了这个范围。因此你可以仅使用大写字母,或者你可以简单的加减缩放(scale)小写字母,或者全部。
这个直接关联(A = 60, Z = 90)带来的问题很明显:你会被字母的值给绊住。字母a和e都将始终是低音。又由于a和e在字母中出现的频率都很高,因此这些音产生的概率将会更高。
映射(mapping)
映射将允许我们对音符和字母间的关联进行更好地控制。为一个输入赋值,不管流程内每个项目的内在规定(intrinsic value)。这样,你便可以在控制创作本质(the nature of the composition)的同时,保留流程内模式的连接。在字母中,比如,我们知道元音几乎每隔一个字母就出现。有时两个成一行,偶尔又被四个辅音隔开。在这个认知下,我们可以使用一个映射,分配特殊的音高到元音上,并获得(举个例子)一种音调(tonality)的感觉:a = C4, e = E4, i = G4, o = C5 (a = 60, e = 64, i = 67, o = 72)等等。你也可以限制你的映射到使用中的字母,省略(或包括)类似spaces、punctation等等词。
SC使用IdentityDictionary创建一个映射,如下所示。变量pitchMap被分配到包含一对元素(初始字母和它的关联值)的数组IdentityDictianary。关联是通过语法”originalValue -> associatedValue” 或在当前情况下 “$b -> 4” 的方式实现的。意思是,让字母b等于整数4。
28.2. pitchMap
pitchMap = IdentityDictionary[ $H -> 6, $x -> 6, $b -> 6, $T -> 6, $W -> 6, $e -> 11, $o -> 11, $c -> 11, $, -> 11, $. -> 11, $n -> 3, $y -> 3, $m -> 4, $p -> 8, $l -> 9 ];
与字母关联的数字之后被用到MIDI音上。另一种选择,它们可以被用作MIDI音程。
在为若干项目使用IdentityDictionary后,我发现它很难处理数值改变(需要输入太多)。所以我选择使用一个二维数组。第二维的第一元素是组成一个字符串的字母列表,第二元素是关联的值。它们经由一个do函数分析,这个函数遍历每个第一元素,如果找到匹配的(使用includes),则将变量mappedValue设置到第二元素。
28.3. 映射数组
var mappedValue, intervMap; intervMap = [ ["ae", 2], ["io", 4], [" pst", 5], ["Hrn", -2], ["xmp", -1], ["lfg", -4], ["Th", -5], [".bdvu", 1] ]; intervMap.do({arg item; if(item.at(0).includes($o), {mappedValue = item.at(1)}) });
这个patch仅控制在绝对关联(即,实际的音而非音程)内被音化(pitched)的元素。
28.4. 额外音乐准则, 仅音
( var noteFunc, blipInst, midiInst, channel = 0, port = 0, prog = 0, intervMap, count = 0, ifNilInt = 0, midin = 0, inputString; //输入流. inputString = "Here is an example of mapping. The, them, there, these," "there, then, that, should have similar musical interpretations." "Exact repetition; thatthatthatthatthatthat will also" "be similar."; //intervMap包含字母和值的集合的数组。 //在下边的函数中,字母字符串与数字相连 intervMap = [ ["ae", 2], ["io", 4], [" pst", 5], ["Hrn", 7], ["xmp", 1], ["lfg", 3], ["Th", 6], [".bdvu", 11] ]; "// [Char, Interval, ifNilInt, midi interval, octave, midi]".postln; noteFunc = Pfunc({var parseInt, octave; //intervMap内的每个数组都在检查是否包含字母(inputString.wrapAt(count)) //如果包含,parseInt被设置为在item.at(1)的值 intervMap.do({arg item; if(item.at(0).includes(inputString.wrapAt(count)), {parseInt = item.at(1)}) }); //如果parseInt是notNil, midin就被设置为那样. //ifNilInt用来储存每个要被使用的parseInt //如果没有匹配被发现并且下次的parseInt是nil。 if(parseInt.notNil, {midin = parseInt; ifNilInt = parseInt}, {midin = ifNilInt} ); octave = 60; "//".post; [inputString.wrapAt(count), parseInt, ifNilInt, midin, octave/12, midin + octave].postln; count = count + 1; midin + octave }); Pbind( \midinote, noteFunc, \dur, 0.125, \amp, 0.8, \instrument, "SimpleTone" ).play; )
我承认这并不十分有趣。这有部分因为独自的音值并不对可辨认的风格作出解释。我们更习惯于不和谐的一个特定级别,而并非单独的音序。不谐和是音程分布的作用,而不是音的分布。为得到一个音程选择的特定平衡,intervMap应包含比例音程值而非绝对音。数字可能看上去完全一致,但在实际分析中,我们将使用midin = parseInt + midin%12(因此parseInt变为一个音程)而不是midin = parseInt。有趣的东西同样可以因将音映射到若干个八度,或映射八度选择到字母上而出现。
28.5. 额外音乐准则, 完全控制
( var noteFunc, blipInst, midiInst, channel = 0, port = 0, prog = 0, intervMap, count = 0, ifNilInt = 0, midin = 0, ifNilDur = 1, durMap, durFunc, ifNilSus = 1, susMap, susFunc, ifNilAmp = 0.5, curAmp = 0.5, ampMap, ampFunc, inputString; //输入流 inputString = "Here is an example of mapping. The, them, there, these," "there, then, that, should have similar musical interpretations." "Exact repetition; thatthatthatthatthatthat will also" "be similar."; //intervMap包含字母和值的集合的数组。 //在下边的函数中,字母字符串与数字相连 intervMap = [ ["ae", 6], ["io", 9], [" pst", 1], ["Hrn", -3], ["xmp", -1], ["lfg", -4], ["Th", -5], [".bdvu", 1] ]; durMap = [ ["aeiouHhrsnx", 0.125], ["mplf", 0.5], ["g.T,t", 0.25], ["dvc", 2], [" ", 0] ]; susMap = [ ["aei ", 1.0], ["ouHh", 2.0], ["rsnx", 0.5], ["mplf", 2.0], ["g.T,t", 4.0], ["dvc", 1.0] ]; susMap = [ ["aei ", 1.0], ["ouHh", 2.0], ["rsnx", 0.5], ["mplf", 2.0], ["g.T,t", 4.0], ["dvc", 1.0] ]; ampMap = [ ["aeHhrsnx ", 0.8], ["ioumplfg.T,tdvc", 1.25] ]; noteFunc = Pfunc({var parseInt, octave = 48; //intervMap内的每个数组都在检查是否包含字母(inputString.wrapAt(count)) //如果包含,parseInt被设置为在item.at(1)的值 intervMap.do({arg item; if(item.at(0).includes(inputString.wrapAt(count)), {parseInt = item.at(1)}) }); //如果parseInt是notNil, midin就被设置为那样. //ifNilInt用来储存每个要被使用的parseInt //如果没有匹配被发现并且下次的parseInt是nil。 if(parseInt.notNil, {midin = parseInt + midin%48; ifNilInt = parseInt}, {midin = ifNilInt + midin%48} ); [inputString.wrapAt(count)].post; ["pitch", parseInt, midin, octave/12, midin + octave].post; midin + octave }); durFunc = Pfunc({var parseDur, nextDur; durMap.do({arg item; if(item.at(0).includes(inputString.wrapAt(count)), {parseDur = item.at(1)}) }); if(parseDur.notNil, {nextDur = parseDur; ifNilDur = parseDur}, {nextDur = ifNilDur} ); ["dur", nextDur].post; nextDur }); susFunc = Pfunc({var parseSus, nextSus; susMap.do({arg item; if(item.at(0).includes(inputString.wrapAt(count)), {parseSus = item.at(1)}) }); if(parseSus.notNil, {nextSus = parseSus; ifNilSus = parseSus}, {nextSus = ifNilSus} ); ["sustain", nextSus.round(0.01)].post; nextSus }); ampFunc = Pfunc({var parseAmp; ampMap.do({arg item; if(item.at(0).includes(inputString.wrapAt(count)), {parseAmp = item.at(1)}) }); if(parseAmp.notNil, {curAmp = curAmp*parseAmp; ifNilAmp = parseAmp}, {curAmp = curAmp*ifNilAmp} ); count = count + 1; if(0.5.coin, {curAmp = rrand(0.2, 0.9)}); ["amp", curAmp.round(0.01)].postln; curAmp.wrap(0.4, 0.9) }); Pbind( \midinote, noteFunc, \dur, durFunc, \legato, susFunc, \amp, ampFunc, \instrument, "SimpleTone" ).play; )
与文件协同工作
将你想用的文本或数据值输入实际代码文件内往往是不实际的。一旦你设计了一个可接受的文本映射,你便可以将映射、创作和文本想做一个驱动音乐的模块组件。在这种情况下,一个将文本作为数据流读入正在运作程序的方法便是必须的。随着这个功能的到位,映射创作便可与储存欲读数据的文件分离。SC具有标准的文件管理工具可被用于这一目的。
与我们协同工作的文件类型是文本文档(看下边的数据类型),因此我们应当创建文本文件。但MS Word文档,甚至一个rtf文档,都含有非文本格式化信息,这是一种干扰。我尝试用SimpleText, BBEdit, SC 编辑器 和 Word 创建“文本”文件,Word和BBEdit是唯一两个不产生不需要东西的编辑器。
路径名同样需要一点点的解释。当SC(以及绝大多数程序)打开一个文件时,它会首先在SC所在目录内寻找这个文件。因此,如果你使用的文件与SC位于同一文件夹内,那么路径可以仅是文件名。如果文件位于别处,一个文件夹层级必须包含进去。层级的写法类似超链接,各个文件夹间用斜杠分隔开。如果数据文件位于SC文件夹内的某文件夹内,那么路径名可以由那个文件夹开始。如果文件位于SC文件夹之外,那么你必须给出整个文件的路径。因此,一个位于SC文件夹内的文件即简单的“我MyFile”,如果位于子文件夹,它可能是”/Data Files/MyFile”,如果在其他区域,那么可能是”/Users/Students/Documents/Computer Music/Data Files/MyFile”。
要打开或读取文件,首先申明一个承载文件的变量。然后使用File()打开文件并表明你要用的模式(read, write, append, 等等)。在本例中,我们使用读取模式,或者“r”。
一旦打开了文件,便可使用getChar一次性检索全部字符,但我想建议读取整个文档并将它存储到一个数组,因为在之前的例子中,文本被存储在一个数组内。这里是代码,假设文本文档被命名为“Text File”。这部分可以被插入上例input = “Here is . . . 等等的部分。
28.6 读取一个文件
( var input, filePointer; //申明变量 filePointer = File("Text File", "r"); input = filePointer.readAllString; filePointer.close; input.postln; )
更长,但也更对用户友好的方法,使用openDiaglog。我通常只用一个文件工作,因此这是我并不需要的额外一步。但在很多情况下,它去掉了路径名的猜测。它同样可被用于指出任何给定文件的确切路径名。有一个小故障:要么一切都在openDialog里,要么使用一个全局变量并分别运行代码的两个部分。
28.7. 读取一个文件
// 列印全部路径名备用 File.openDialog("", { arg pathName; pathName.postln}); // 用dialog打开 ( var input, filePointer; //申明变量 File.openDialog("", {arg pathname; filePointer = File(pathname, "r"); input = filePointer.readAllString; input.postln; // 所有东西都应在这个函数内 }, {"File not found".postln}); ) // 或打开文件并存入全局 ( var filePointer; //申明变量 File.openDialog("", {arg pathname; filePointer = File(pathname, "r"); ~input = filePointer.readAllString; }, {"File not found".postln}); ) // 然后 ~input.postln; // 或者将~input含入patch