高 弘
內容提要:編程技術對作曲家實現創作的局部自動化具有重要意義。當代,人們對于作曲家的綜合素質也提出了時代性的新要求,即應該積極掌握計算機輔助創作領域的各項技能,成為計算機與作曲兩個交叉領域的綜合型人才。利用編程技術,作曲家把個人需求通過計算機算法來實現,再把算法植入在程序里,從而達到“人機合一”的理想效果。本文著重在節奏這一音樂參數上探究,如何在避免機械性的同時,提煉個性化的創作需求,最終通過算法的控制來形成程序。以此論證,相較于單純依靠人力來進行音樂創作的傳統方法,算法作曲在未來有著很大的優勢和潛能。
近幾年來,國內外對于計算機技術介入作曲領域的研究呈現出不斷加速、擴大的趨勢。這主要得益于現今人們獲取數據的方式已經變得更為方便,計算機CPU 硬件等性能較以前也得到了大幅度提高。基于這一現象,人們可以試驗各種各樣的算法來訓練計算機不斷優化模型。無疑,音樂這一人類引以為豪且極富創造性的領域,也遭到了計算機這一“不速之客”的介入。其實,這一趨勢并不是在最近幾年才開始出現的。因為計算機介入作曲呈現出的方式不同,其差異性主要取決于人類對它的定位,或者說,在何種范圍賦予計算機一定的“自主權”。計算機是作曲家的助手、提高其創作效率與分析的工具,如何去利用好這一工具,才能在既貼近作曲家的想法,又不干擾作曲家藝術構思的前提下,提高創作效率,甚至達到能反哺創作思維的效果?
本文試圖在節奏這一領域,探究如何把作曲家對音樂的控制思路,演化為對應的算法,并把這些算法納入到程序的設計里,使作曲家的想法可以迅速地“變現”,反饋給作曲家,進一步把程序作為模塊根據功能的不同而分類、存儲,以便創作時可以隨需求的變化加以調整和復用,從而極大地擴展作曲家的創造能力。
我們首先需要思考幾個問題。第一,程序設計的結構是什么? 我們最終需要以什么數據形式來產出? 如果要得到需要產出的這種數據形式,需要進行什么操作? 第二,有哪些參數是需要我們在過程中鍵入的? 有哪些是可以預設,以備程序隨時調取使用的? 第三,假設前兩步能夠完成,我們要如何進一步優化代碼,使其中一些模塊可以被其他作品再次使用? 或是如何改善容錯性,增強其在適用范圍以外的韌度,更加靈活地貼近隨時變更的創作需求?
在編程的領域中,這種理念就被提煉為是“高內聚、低耦合”。意思就是,首先要有模塊化的思維,使諸多模塊盡量聚焦于一個單一功能來實現,也就是提高內聚性,作為一個模塊,我們要盡量使得一個模塊內的各個子功能、子模塊或子元素之間緊密聯系起來,以增強代碼彼此之間的關聯性,而且這也是模塊復用的基礎。而低耦合是指在高內聚的基礎上,盡量削弱不同功能模塊之間的重疊甚至彼此依賴的程度。即在不同功能的模塊之間,其中一個模塊的使用并不強制依賴另一個模塊的介入與調用。簡單來說,就是使模塊與模塊間盡可能獨立地存在,讓每個模塊單獨完成某個特定的子功能。而模塊與模塊之間的接口部分,盡量純粹和不依賴其他模塊的介入。這樣就可以允許作曲家根據不同的需求來獨立調用不同功能的模塊,或是加以組合、拼接以達到更加豐富的創作功能。所以,在接下來講述代碼實現的章節中,筆者將根據結構的種類來劃分為不同的函數模塊,并針對各類模塊分別來進行代碼實現。
自由平行樂段,意指由四個樂句構成的一個結構樂段。其內部分別由第一樂句與第三樂句相互對應,而第二樂句與第四樂句相互對應,見譜例1。
譜例1

以上譜例是一段由大提琴所演奏的低音聲部,我們先集中解決結構與節奏部分,所以需要暫時剔除所有的音高信息。如譜例所示,這是一個長度為16小節的規整的平行樂段,共4句,每句4小節。其中,第1句與第3句成對應關系,第2句與第4句成對應關系。
那么接下來,首先需要考慮的是,我們需要計算機產出什么形式的數據? 為了得到這種數據,我們又需要如何給予一個程序參數?
面對第一個問題,我們首先需要的是一組列表,它能夠儲存計算機產生的節奏數據。為方便計算機能“讀懂”我們的節奏信息,我們可以用4來代表一個全音符,2代表一個二分音符,以此類推,見表1。

表1
當然,也可以根據自身需要新建其他節奏型,上表只是為達到我們的章節目標而展示出的部分。
接著,我們把譜例1中的節奏信息“翻譯”為能夠在程序中存儲的列表,如下:
'(4 4 4 4)
'(2 1 1 4 4 2.5 .5 .5 .5)
'(4 4 4 4)
'(2.5 .5 .5 .5 2 1 .5 .5 2 2 4)
第一個目標是借助程序內提前設定的算法,最后產出類似于這樣的一個列表,但是并不需要如此“精確”,否則只是實現了一個打譜功能而已。真正需要達到的目標,是在實現同樣樂段結構的基礎上,有一定程度的“自由”,以保證創作的個性化。如果每次的調用都只能產生同一結果,那也就失去了設計此算法的意義。
那如何保證,在擁有一定程度的個性化時,又能展示出預期的結構呢? 首先,就要對樂段結構有深刻的理解,然后使這些理解在與為之設計的代碼過程中間有所體現。從譜例1中可以很明顯地看出,這是一個典型的平行樂段。第1句與第3句完全對應,第2句與第4句雖然也是對應關系,但是并不完全相同,它們之間的對應,更多的是體現在過程中,其中有節奏變化元素的彼此對應,但這并不影響他們之間的對應關系,從曲式角度來說,它們不是彼此獨立存在的。
為此,需要把我們頭腦中的曲式概念在編程環境里加以模擬,從而“告訴”計算機,這4句里實際上只有2句是需要調用“生產”的模塊,即需要產生一個新的樂句,而后2句因為和前2句是對應關系,我們只需要重復(當然是變化的重復)已得到的前2句的數據即可。那么我們需要一個專門用來控制曲式結構的列表,在上方這個譜例中,它會顯示為如下:
'(1 2 1 2)
有了控制曲式的列表之后,我們需要關心的是每個樂句的內容。本質上來講,也是曲式,只不過是更低一層次的結構。顯然,第1句雖然只是一個初步的展示,但我們發現它的節奏過于單一,甚至幾乎都是全音符。這是因為這個聲部是低音聲部,難免在節奏的豐富程度上無法與旋律聲部相比擬。也就是說,同樣是第1句,它是否是低音聲部、旋律聲部,或是中間填充的和聲聲部,對于節奏元素的產生有著決定性的意義。而這在我們的實際創作過程中也是很常見的情況。但對于計算機來說,我們需要“告知”它,需要按哪一種聲部類型來產生樂句中的節奏。所以,我們還需要另外構建一個品種列表,如低音、主旋律、副旋律、填充聲部等,用來對應不同的節奏生成策略。
至此,有了兩組數據,一個是曲式列表'(1 2 1 2),另一個是聲部列表'(bass),接下來要深入到結構內部。這時會發現,第2句雖然屬于新產生的一個獨立樂句,但也和第1句有著緊密的內在聯系。譬如,第2、第3小節,也都是由全音符組成,而第1小節可以看成是一個全音符的兩次自我分裂。一個全音符,理論上可以不斷地進行自我分裂,如譜例2所示。
譜例2

不難發現,每次分裂都是把最后一個音符作為被分裂對象,而前面的值都予以保留(第一個原始的全音符也可看為自己的最后一個音符),三次分裂后的狀態會出現在第2樂句和第4樂句中。所以,自我分裂是眾多實現新樂句方法的一種,為此我們可以構建代碼(見圖1)。

圖1
這個名為division的函數模塊解決的就是這個問題。關于分裂次數方面為避免每次都需人工給予參數,筆者在內部把它交給了隨機數字處理,但不會超過分裂三次的情況,我們可以看到在給定參數值4(意味著是全音符)的情況下,多次運算后在控制臺顯示的結果情況,與我們所預期的完全符合。
在第2句的第4小節,我們發現了一個并不能通過分裂模型得到的結果(見譜例3)。
譜例3

它同樣也出現在了第4樂句的第1小節,正如前面筆者所做的,我們必須從具體的事物出發,然后再提煉出具有模型化的抽象。那譜例3的這個動機本質上是什么邏輯?
從這個小節第三拍的后半拍開始的三個連續八分音符,本質上是一種連接,它起到了一個連接句的功能,也就是說,如果在抽象層面上來理解,它也可以是這種形式(見譜例4)。
譜例4

或者:

上面幾個譜例所展示的,從根本上來說都是具有向后面平滑過渡的性質。也就是說,筆者需要構建一個可供連接句選擇的節奏元素列表,在需要時調取即可。至于調取的究竟是哪個元素,其實并不重要。那么,之所以在最后一小節出現帶有連接性質的節奏元素,是為了更好地進入到下一樂句,我們可以把第2樂句看成是一個既保留第1句節奏元素的同時,擁有自我分裂模型與連接模型的樂句,只要同時實現這三個功能,即便一些具體的時值每次運算會有不同,也不會影響到第2句在曲式結構中的功能。下面請看在以上思路通過代碼實現之后,多次運算的第2句的數據情況(見圖2):(代表第2句的模塊里只擁有一個參數入口,就是被運算的節奏列表,此處為第1句的節奏列表)。

圖2
以上經過運算的結果正是我們想要的:1.在第2樂句中,第1樂句的節奏元素予以了部分保留;2.擁有自我裂變;3.順利完成最后一個小節具有連接性質的樂句。
在一個平行樂段中,第1樂句與第2樂句相對較為核心,能夠表達樂段的思想,而第3樂句與第4樂句則視為前2句的一個變奏,也就是說,接下來的核心就是如何實現“變化”的功能。相對較為“簡單”地通過調整順序以達到變化的效果,此效果只需要一個簡單的函數,幾乎市面上所有的編程語言都封裝了這樣一個可以實現隨機排列的函數,所以就不在本文講了。筆者認為,隨機排列雖然可以起到變化的效果,但是難以實現相較之原對象情緒上的增減起伏,作為平行樂段的后半部分,只擁有隨機排列這個功能肯定是不夠的。一個現實可行的思路是,在隨機排列的基礎上,結合原有的自我分裂模型或自我合并模型(對應樂曲情緒的相對起伏)實現變化。此種思路的根本原理建立在“節奏在音樂中起到調節情緒的基本功能”這一觀點上。聽密集的節奏,會讓人感覺活潑;聽稀疏的節奏,音樂則會顯得舒緩。而唯一需要注意的是,第4樂句最后一個節奏時值要相對夠長,以達到結束整個樂段的目的。
在理清了思路之后,筆者開始實現第3樂句與第4樂句的代碼。
譜例中,第3樂句與第1樂句完全一致,我們暫且可以只用隨機排列函數(因為全部一致,所以隨機排列在此處與完全重復并無區別)。第4樂句要同時實現3個功能:1.隨機排列;2.在原有基礎上繼續自我裂變;3.尾音需要較長的時值已獲得段落穩定感。
按照此要求來設計代碼后的多次運算結果如圖3所示:

圖3
實現第4樂句的代碼模塊(fourthphrase)也只有一個參數入口,由于我們是在第2樂句的基礎上去實現功能的,這里的參數就是第2樂句的節奏列表。經過多次運算后我們發現,已經實現了對第4樂句的預期功能。
下面筆者需要把之前所做的功能整合在一起,同時要時刻關注起初提出的問題,即我們最后需要產出的數據形式是什么? 我們因此需要暴露出哪些參數接口,以方便我們操作?
毫無疑問,我們需要得到的數據形式為一段完整的平行樂段(當然只涉及節奏,音高將會在下一個章節中涉及),里面分別清晰地展示4個樂句,每一樂句都擁有自己的節奏列表。第二個問題是我們需要制作出何種程序形式便于我們操作? 也就是以什么樣的形式去操作這一組代碼模塊,筆者認為需要提供一個節奏列表,也就是第1句的節奏列表作為操作對象,或者,即提供第1樂句的節奏,然后代碼自動生成剩余3個樂句。圖4是這組代碼的運行結果,我們可以為這組代碼起個名字,叫parallel,中文即平行的意思。
在實際運用中,我們并不需要print功能(控制臺顯示為橘黃色的數據就是print功能輸出的),因為經由print功能輸出的數據,無法繼續傳遞或輸出到下一個函數。這里使用它,是為了較清晰地展示哪些數據是第1句,哪些數據是第2句,以此類推。

圖4
這也是除了平行樂段外,最常見的一種結構,較為通俗地稱呼為短短長。它主要表現為:首先出現兩個非常相似的短句,緊接著再來一個長度為兩個短句之和的長句。它通常可以獨立存在,也可以嵌入在某些結構體當中,如平行樂段。作為一種結構,它也有各種相應的變體,其變化主要體現在其長句的特征上:
1.長句的素材和短句的素材并無明顯相關;
2.長句的主體,尤其是長句的前半部分,和短句的素材高度相關;
3.長句內部,又嵌套了一個短短長的結構。
筆者要做的就是把這三個版本的短短長結構全部整合在一個函數里,然后交給計算機來隨機選擇,而創作者則只需指定一共占用多少時值即可,這也是該函數接收的唯一參數。同時,筆者在函數內部存儲16為參數的默認值,即當創作者不給予函數參數的時候,自動調取16為函數參數,然后執行函數。當給予函數參數時,函數以參數實際值執行函數。筆者把這個函數命名為ssl(意思即short-short-lang,中文意為“短短長”)。
效果見圖5。

圖5
由于此處沒有給予函數任何參數,函數假定了參數值為16調取執行了ssl函數(16這個數值意為整個短短長樂段時值為16拍,這個數值被筆者封裝在函數內部)。筆者連續執行了三次,可以從圖5中看出,第一次執行ssl函數的結果顯示是一個普通的短短長結構,其中第一個短句占時值4拍,第二個短句與第一個短句完全相同,長句占時值8拍。第二次執行ssl函數的結果顯示是一個短短長結構,其中的長句里又嵌套了一個短短長結構,即在長句所占的8拍的節奏空間內,又實現了一個2+2+4的短短長結構。第三次執行出的結果同第二次相同,在這三次執行的結果中并沒有顯示出第二種變體,即長句的主體,尤其是長句的前半部分,和短句的素材高度相關,那么可以再多執行幾次,看看是否會有這類結構出現(圖6)。

圖6
這次執行的第一次結果就符合預期:長句保留了短句的節奏素材,構成了長句的前半部分。
不難發現,這些數據雖然體現了一些結構的特征,但是從細節上來說,它們還是不夠豐富,或者說不具備足夠的人性化特點。固然,根據不同的創作需求,或許作曲家也可以接受這些節奏形態。但是筆者認為,應基于更加音樂性的角度來對這些節奏的數據進行二次處理,用來滿足差異化的創作需求,則更為合理。下面參見一組節奏數據的對比:
譜例5

在譜例5中,標注為原型的節奏列表為:
(.5 .5 .5 .5 1.5 .5 .5 .5 .5 .5 2)
從樂句結構上來說,這是一個長度為2小節的長句,停留音為2拍,即占樂句總長度的四分之一。無疑,這樣的一組節奏,有時候并不能滿足作曲家對于節奏豐富性的需求。所以,需要構建一個對這些節奏數據進行二次處理的函數,為的是在不破壞或顯著更改樂句結構的前提下,豐富節奏內容。為此筆者構建了一個名為humaniz的函數,來達到此目的。
筆者把這組數據,當成函數參數并以此調動humaniz函數,然后控制臺中顯示了這樣一組節奏數據:
(0.5 0.5 0.5 0.25 1.75 0.25 0.25 0.5 0.5 0.5 1.0 1.5)。
這也就是在譜例5中標注為“處理后”的節奏,與“原型”對比之后可以發現,原本在“原型”中平均化的節奏(主要表現為連續的八分音符進行)被打破,其手段是針對某一個八分音符進行分裂處理(分裂為兩個十六分音符)。而對于停留音的出現,并不是簡單地用二分音符來展示,取而代之的是遲滯一個八分音符而出現,并且這個八分音符與之前的時值相互合并,造成了一個延留音,避免造成了聽眾過于直接突兀的印象。事實上,筆者在humaniz函數內部封裝了三種對于停留音的處理辦法,遲滯出現只是其中一種,這樣就可以保證每次調用humaniz函數時對于停留音過于單一的處理情況。

圖7
目前為止,雖然已經實現了一些不同的結構,但是還是處于比較微觀的層次,無法在一個統一的平臺上綜合地使用。因為它們相互之間沒有聯系的,處于彼此孤立的狀態。就拿前文中已經實現的一些結構來舉例:假設,創作者需要一個片段,這個片段是由兩個時長為64拍的平行樂段、一個時長為16拍的短短長,再加上一個時長為32拍的半平行樂段。那么這時候,只能依次地按照如下的順序運算——parallel函數、parallel函數、ssl函數、halfparallel函數。雖然這樣的方式最后可以滿足想要的結構,但是仍不可避免會有以下三個缺點:
1.創作者需要進行頻繁的操作,此處至少需要操作四次,并且每次都要需要輸入每個函數相匹配的參數才能運行函數,這無疑加大了創作者的工作負擔,不利于此文講述的半自動化輔助功能的宗旨。
2.每個函數之間并沒有內在的聯系。從作曲創作的角度來講這是不能接受的,按照上例中的操作方式,每個函數運行的時候,并不依賴于任何其他函數產生的結果,而是獨立運行的,這就說明從音樂素材的角度來看,四個函數之間的結果沒有建立起任何關聯。
3.無法追溯已經存在過的結果。舉一個例子,當使用者第一次運行parallel函數,產生了一個平行樂段,而在之后的某個時刻,他需要重復這個平行樂段,那么即使他再次運行parallel函數,由于parallel函數內置的隨機性,也無法得到與之前完全相同的結果。
綜上,我們還需要設計一個可以統一調取不同功能的函數的平臺。它應當支持創作者盡可能以極簡的方式輸入“必要的信息”,從而產生指定的結構。這里面包含每個結構是否是獨立運行或是取材于哪個部分,以及只支持追溯。那么,究竟什么才是“必要的信息”? 對于不依賴于其他結構的獨立產生聲部的函數來說,總時長、結構名稱以及其中第一樂句的內容素材(可缺省)是必要的信息;而對于需要依賴其他結構內容從而才能產生聲部的函數來說,需要提供被依賴的聲部,或者說是被錨定的聲部,以及結構名稱作為必要的信息;最后,對于追溯功能來說,只需提供需要被追溯的位置,就能作為必要信息。
理清了上述思路之后,開始實現結構函數rhysglobal。
函數rhysglobal的假想內部結構為:
〈函數名〉〈時長列表〉〈函數列表〉
rhysglobal函數的運行邏輯如下:函數名設置為為rhysglobal,rhysglobal函數會首先讀取〈函數列表〉里的函數,根據它們的類型,決定是否讀取〈時長列表〉中的元素。如果該函數是獨立運行的,那么直接讀取〈時長列表〉中的元素來限定該函數產生節奏的總長度;如果該函數是依賴于其他結構內容的,則在該函數后需直接輸入被依賴內容的位置,這種情況下,不需要讀取〈時長列表〉中的元素。以下展示一個結構函數rhysglobal的實例(見圖8):

圖8
接下來詳細分析一下這個結果的產生過程:首先rhysglobal函數讀取其中〈函數列表〉的第一個元素——ssl,這是一個獨立運行的負責產生短短長結構的函數,因此,它需要讀取〈時長列表〉,而〈時長列表〉的第一個元素是4,即4小節的意思,那么rhysglobal產生了一個時長為4小節的短短長結構——((2 1 1)(2 1 1)(2 1 1 4))。繼續,rhysglobal函數開始讀取〈函數列表〉的第二個元素——(fssl 0),第二個元素自己本身是一個列表。這是由于fssl函數本身的特點決定的,不同于ssl,fssl雖然也產生短短長結構,但是它需要依賴于錨定聲部提供的素材,才能運行函數,而且不支持使用者手動輸入。從這一點上來說,fssl才像是一個自動化的函數。(為了方便,在我的使用習慣中,所有帶“f”子母開頭的函數,全部為自動化函數。f為function-automated的首字母。)所以,它需要提供給它錨定聲部的素材內容,這就是列表中第二個元素0的功能。0即代表定位出rhysglobal函數的第一個結果,也就是剛剛經由ssl函數產生的結果,以它作為素材,運行fssl。從結果上不難看出,兩者之間關聯程度比較高。
ssl-〉((2 1 1)(2 1 1)(2 1 1 4))
(fssl 0)-〉((2 1 1)(2 1 1)(1 0.5 2 2 0.5 0.25 0.25 0.5 1))
最后,rhysglobal函數讀取〈函數列表〉的最后一個元素——0,有別于〈函數列表〉中的前兩個元素都是函數,0并不是一個函數,只是一個數值。但這并沒有讓計算機報錯,原因就在于rhysglobal函數內置了定位函數,當識別出元素是數字的時候,內置的定位函數馬上開始遍歷(for-each函數)已經存在于rhysglobal函數中的結果,并按照數字把相應的結果取出來,再返還給rhysglobal函數。按照Scheme的計數方式,0即代表列表中的第一個元素,那么對于rhysglobal函數來說第一個元素就是函數ssl產生的((2 1 1)(2 1 1)(2 1 1 4)),所以,結果中又一次出現了((2 1 1)(2 1 1)(2 1 1 4))。
本文中實現的一些算法,主要是處于節奏這一音樂參數上面,其范圍涵蓋了自由平行樂段、綜合型句法這兩個基本結構元素的構建,以及設計結構函數來支持創作者對結構進行定制。并且,對于如何更加人性化處理一些節奏數據進行了一些探索。需要說明的是,在這條道路上需要作曲家不斷地付出與探索,就像一款軟件需要維護一樣,因為需求總是在不斷變化的,當有新的創作需求時,就需要完善、重構甚至重新設計對應的算法,來保障程序對創作的輔助作用。在這一過程中,作曲家也積累了越來越多的編程經驗,懂得如何利用編程技術來達到自己的創作目的,從而會設計出更為貼近自身創作特征的程序。最后,筆者概括利用算法對創作進行輔助所帶來的優勢有三點:1.加速作曲過程。雖然前期的程序設計工作較為耗時,但一旦順利完成,剩余工作將會輕松很多;2.會“反哺”并優化作曲家的創造思維。確立算法,并且植入程序后,所帶來的效果就可以直接反饋給作曲家,這會給作曲家帶來更多的靈感,也會給分析者帶來更清楚的思路。3.由于不同功能的是以一個個模塊的形式來儲存的,這樣有助于催生作曲家形成模塊化的創作思維模式。
綜上,本文的核心觀點,就是作曲家首先需要把自己對于作品的藝術構想,轉換為某種算法,或是算法的集合,然后把這些算法植入到程序的編寫中,以求達到計算機輔助作曲的目的。其實在世界各地已經有相當一部分具有編程知識的作曲家在某種程度上進行了實踐,但是運用范圍還不夠廣泛,依然不成體系,而且實驗性質的實踐居多。作曲家可以根據自己的創作體系特點,把實現過的函數按照不同的屬性予以歸類,也可以把一些常用的作曲技術,編寫進函數功能里,根據需要隨時調取,以輔助自己的創作。