基於樂理模型的算法做曲研究

===================================node

2019-3-2備註:一、去年在知識體系還沒有創建時就匆匆下筆,結果就是這篇「黑歷史」般的文章。git

如今從新審視這篇文章,頗有民科的風格。github

本文是純粹的基於規則的做曲,這讓我想起聊天機器人ELIZA的創始人曾經說過:算法

  「機器能夠表現得十分驚豔,能夠矇蔽最有經驗的觀察者。但若是你揭開程序的面紗,向人們解釋其內在的原理,那麼一切的魔法都將蕩然無存,那不過是一些工序罷了……」數據庫

二、項目是開源的。倉庫地址在後文有敘述express

 

下面記錄一些雜項內容:數組

===================================數據結構

2019-3-2:併發

1. 得知道這方面工做中別人是如何建模的,和大家這個有什麼區別,或者大家這個有什麼更好的,解決了之前沒解決的問題沒有,若是沒有的話,那寫起來就當一個技術博客來寫,也是對本身實現項目的很好總結。框架

  Re: 在開始本文寫做以前的確沒有詳細瞭解過別人的相關工做。在咱們研究過的已有方法中存在如下須要完善的地方:首先是沒有充分考慮到基礎樂理、編曲經驗(例如常見的和絃走向)對旋律生成的約束;其次是隻針對單個樂器生成旋律,沒有充分考慮各類樂器之間的配合。

  所以本文要解決的主要問題有兩點:一是如何將編曲的經驗做爲約束條件應用於算法做曲;二是試圖解決多樂器混合編配的相關問題。

  深感慚愧的是,我將一篇技術博客寫成了相似論文的寫做風格。其實在此以前我並無寫過任何論文,這的確是很是荒謬的。

 

2. 前面寫的激勵部分,後面好像也沒怎麼具體去實現。我我的看來,其餘的在技術上,感受都比較容易實現,可是創做動機這裏可能須要設計好的算法,才能使音樂不是機械的隨機不一樣。

  Re: 激勵器部分比較關鍵,寫文章前想到的方法是:輸入圖片,經過特徵算法提取特徵向量,並將所得向量映射爲激勵器的參數。若是算法設計得當,生成的音樂就有可能必定程度上與圖片相關。這些並無在本文中得出體現。若是繼續研究的話,也許能夠從這個角度入手。

 

3. 可能須要有個能夠評價的指標,來評價算法效果好壞。

  Re: 本文所涉及的部分算法考慮了一些量化方法,例如和絃轉換算法中,用Q殘差曲線來評估轉換旋律的效果,但本文並無給出具體的指標。在這裏簡單寫一種思路,例如經過求Q殘差的方差或標準差得出指標Dq,這樣當Dq越趨於0時,生成的旋律越機械(徹底按和絃走);而當Dq較大時,生成的旋律離和絃越遠。這樣當Dq介於必定範圍時,旋律聽起來比較均衡飽滿。能夠經過統計得出I的最佳取值範圍。

  而對於最終生成的音樂做品,由於本方法生成的旋律線比較連貫,基本沒有過遠偏離主調的音符,因此單純從圖形化旋律線角度不便於評估。並且因爲音樂的評估主觀性比較大,須要配合統計等,這裏暫時還沒想到更好的方法,這是繼續研究的重點。

 

4. 總體看起來有點兒大而全,能夠考慮找1,2個點深刻來寫。

  Re: 我的以爲形成臃腫的緣由之一是將技術細節也寫了進來,而重要理論細節卻沒有充分闡述,這是錯誤的。若是深刻去寫的話,可能會選擇以激勵層分析,或者編曲經驗規則建模這兩個角度爲主展開。

 

===================================

2019-3-2:項目代碼託管在Github上:

https://github.com/sci-dev-git/libautomusic

===================================

===================================

2019-4-2:commit:本次commit中實現了利用OpenCV計算圖像的Hu矩,將七個不變矩映射到激勵層參數,從而簡單實現了上面說的「圖像做曲」。

===================================

 

(英文摘要寫得比較蹩腳,見諒)

(全文開始)

Abstraction

  Musical composition is a sophisticated movement of thought that is unique for humans. More concretely, composition reflects the basic technology and theory of music, i.e. harmonics, polyphony, orchestration, structure and so on, which are utilized to express the spirit of originators. Currently algorithm composition has become a topic much worthy of discussion.

  In this paper we attempt to shrink the research scope and construct a model of musical theories, discussing a practical way to make pop music automatically. For the research on algorithm composition, it's the first step to abstract the traits of composition procedures, and the following is to take into account the implement for computer based on structured algorithms. This imparity of the procedure enforces the algorithm to follow principle of abstraction.

  There are mainly two chapters of this paper: primarily the theory foundation of automatic composition, secondarily details of computer implement. In conclusion, we have constructed a C++/YAML prototype program, including musical theorical model (MTM) and knowledge library system (KLS), then we have succeeded to design and evaluate the target algorithm.

 

Keywords: Algorithm Composition; Musical Theorical Model; Knowledge Library System.

 

摘要

  做曲是人類的一種複雜的思惟過程,具體來講做曲反應了創做者運用基本樂理、和聲學、復調、配器法、曲式結構的技術理論體系來表達音樂思想的方法,而如何利用計算機模擬做曲過程,反映做曲的理論體系,是一個值得探討的問題。

  本文試圖經過縮小研究範圍,從流行音樂的角度出發創建樂理模型,提出實現流行音樂算法做曲的可行實現方法,同時提出了一種研究算法做曲的思路:首先對做曲過程進行抽象化分析,圍繞過程抽象展開結構化程序的設計。本文主要從兩個層面論述:其一,做曲的理論支撐;其二,結構化算法程序的設計。經過C++/YAML原型設計,最終實現了樂理模型和知識庫系統的創建,完成了目標算法的設計、評估以及後續工做。

 

關鍵字:算法做曲;樂理模型;知識庫系統

 

前言

  當前算法做曲的研究主要集中於機器學習方向,其中基於離散時間馬爾可夫鏈的算法做曲是經典算法之一。經過創建各音符的狀態轉移機率矩陣,馬爾科夫鏈能夠對每一步的旋律走向進行決策。除馬爾可夫鏈外,外還存在基於遺傳算法、細胞自動控制理論等的其它算法。

  全部做曲算法都離不開基礎樂理、和聲學、復調、配器法、曲式結構等理論的約束,若是缺少有效約束,單純從機率等角度出發,則難以得出使人滿意的結果,所以關於算法做曲的研究,大多離不開對音樂約束的探究。本文的主要工做是創建基礎樂理的算法模型,從而爲算法做曲提供有效約束。該算法模型是一個由「激勵層」、「變換層」、「記譜層」構成的三層模型,它能自動化地組織音型,利用樂理建模實現音型變換,最終得出連貫的音樂做品。

 

1、抽象模型分析

  本算法是一個三層模型,第n層模型的輸出做爲第n+1層模型的輸入的一個子集。

  首先音樂創做離不開動機,所以第一層即是激勵層,它負責生成音樂動機,主要包括一個動機激勵器和知識庫系統。而接下來即是變換層,變換層主要包括四個獨立的變換模型,它根據生產的音樂動機對音型進行組織潤色。最後的記譜層則負責算法內部格式到外部格式的轉換,從而以可識別的格式輸出完整的音樂做品。

  下面描述算法的總體框架,並就每一個層面分別進行討論。

  圖中ΔA表示對變換過程的調度,ΣF表示對輸出音型的綜合,實線表示數據流路徑,虛線表示模塊間的依賴關係。

  利用黑盒模型,該框架能夠總結爲:從左邊輸入生成音樂動機所須要的必要參數,從右邊輸出完整的音樂做品。

 

一、激勵層(Actuator layout)

  做曲家開始音樂做品的創做,必要條件是具備創做動機。若是沒有動機,就不會有音樂做品的產生。從做曲家角度出發,創做動機的產生主要歸因於兩個層面:從空間角度講,受到主觀的思惟活動和客觀的環境兩方面影響;從時間角度講,做曲家的經驗閱歷等,也會對當前的做曲活動產生影響。

  本節研究的核心問題是解決計算機如何表示創做動機,如何產生創做動機,進而爲算法做曲提供總約束條件的問題。而最根本的問題是找出影響音樂風格的因素。因爲做曲家主觀思惟的參雜,該方面是本算法研究中的難點之一。從構成音樂的要素上看,旋律、結構、配器等,對影響情緒表達起到了決定性做用。只有綜合考慮和聲、旋律、配器等方面才能相對準確地控制做品的情感。

 

1.1 調式、和絃走向對做曲風格的約束

  調式由不一樣音高的音組成集合,這些音互相之間具備某種特定的音程關係,並在調式中擔任不一樣的角色。調式是決定音樂風格最重要的因素之一。在不一樣的歷史時期與不一樣的民族和地域,造成各類不一樣的調式,例如,中國民樂以五聲音階爲主的五聲調式或以五聲音階爲基礎的七聲調式。各類調式因其音階結構、調式音級間相互關係以及音律等方面的差別,而各具特點與表現力。調式和其餘表現手法配合在一塊兒,可賦予音樂以必定的表情素質與不一樣的風格。

 

  而和絃走向反映了旋律整體的發展趨勢,不一樣曲風的做品,和絃走向上會有所差異。

例如,Leading Bass走向:

  C—G/B—Am—C/G—F—C/E—Dm—G—C。其柱式和絃音型以下圖所示。

  Leading Bass起源於Canon。在多數表現浪漫主義色彩的做品中有所體現。

又如:

  Am—F—C—G。爲儘量確保變量惟一,這裏仍採用柱式和絃音型,以下圖所示。

   與Leading Bass相比,能夠發現整體風格的變化。

  產生和絃色彩的主要因素在於和絃組成音之間的音程關係,而具備不一樣色彩和絃的相互融合,使得其具備某種情感傾向。算法做曲中利用和絃走向的這種特性,能夠爲旋律提供反向約束條件,縮小旋律可用的音符範圍,爲旋律生成提供參考依據。

  然而,僅有旋律是遠遠不夠的,節奏型對情緒表達一樣起着相當重要的的做用。對於相同旋律,不一樣節奏能使其呈現出不一樣的表達效果。一樣以上節Am-F-C-G和絃爲例。下圖爲柱式和絃改編爲半分解和絃後的音型。與原始柱式和絃相比較,採用新的節奏型增大了音樂織體。柱式音型比較單一,而新的音型更加活潑。

 

1.2 結構與配器與做品風格的關係

  流行音樂的曲式結構由主歌(Verse),副歌(Chorus),過渡句(插句),橋段(Instrumental and Ending)(序唱,過門,間奏)等組成,不一樣形式下譜曲方法不一樣。結構的存在使流行音樂符合一套統一的「語法」,這一點爲算法做曲提供了有價值的參考。例如主歌與副歌存在的對照性關係。主歌與副歌構每每具備類似性;而副歌在旋律和情緒上都超越主歌。又例如前奏尾奏與主體的呼應關係,使得做品結構完整,善始善終。在算法做曲時,應當充分考慮這種語法約束,不能任憑旋律發展,而全然不顧形式的要求。

  下表總結了常見的流行音樂結構。

1 Blank, Prelude, Verse, Chorus, Ending
2 Blank, Prelude, Verse, Chorus, interlude, Verse, Chorus, Ending
3 Blank, Prelude, Verse, Trans, Chorus, Interlude, Verse, Trans, Chorus, Ending
4 Blank, Prelude, Verse, Trans, Chorus, Interlude, Verse, Trans, Chorus, Interlude, Verse, Trans, Chorus, Ending

   配器對曲風的影響也是顯著的。例如,流行音樂每每採用各種樂器逐步加入的方法,即前奏僅採用一種或幾種樂器,而隨着音樂發展加入樂器。這種手法使得音樂有曲直變化,更易於聽衆接受。其次,各類樂器在音樂中的核心功能是提供音色。因爲聽衆的經驗認知,部分音色自己就帶有某種情緒傾向。算法做曲也應當考慮到配器原理和配器手法,同時還應符合潛在的語法。

 

1.4 激勵層整體設計

  結合調式、和絃走向、結構與配器等,創做動機的生成歸結於以下輸入輸出模型:輸入反映做曲意圖的參數;輸出做品的基本參數(調式,調性,節拍等)、結構、和絃走向、配器。下文統稱該模型爲動機激勵器(Motivation Actuator)。該模型須要必定規模的數據庫做爲支撐,在這裏引入知識庫系統:動機激勵器承擔知識庫系統中推理機構的一個組成成分,而一個數據庫構成知識庫系統的數據來源。

  關於如何生成動機,本文所採起的方案是模糊匹配。將輸入的關於音樂調式、和絃、結構的參數與知識庫中已有音型的參數作模糊匹配,篩選出可用音型做爲接下來的變換層的直接素材。

  綜上所述,由動機激勵器和知識庫模型構成的子系統就是激勵層。

 

2. 變換層(Transformation layout)

  在徹底基於已知經驗的前提下,即僅考慮做曲家在已有經驗和音樂體系下的創做過程,不考慮對新理論體系的探索過程,旋律創做能夠歸納爲「變換」(Transformation),它主要存在於兩個層面:

  (1)、做曲家將已知音型變換爲新的音型,並最終成爲已知音型;

  (2)、做曲家在創做動機的驅動下,將已知音型變換爲新的音型,並最終成爲做品的一部分。

  做曲過程通過抽象後,能夠描述爲以變換爲基礎組織完整音樂做品的過程。上一節中,咱們主要討論了創做動機的相關問題。激勵層生成的是音樂的框架信息,僅僅爲做曲提供了約束條件。變換層討論的問題正是如何對這個框架進行展開。因爲激勵層爲接下來的做曲過程提供了約束條件,使得變換層處理的範圍大幅縮小,變換層只需依據激勵層的輸出對知識庫中的音型進行變換。

  因爲知識庫中的音型具備獨立的調性或節拍、和絃走向,所以變換層須要根據激勵層輸出的結果,對已有音型進行轉調或伸縮變換、和絃轉換等操做。此外,變換過程還必須充分考慮不一樣種類樂器的特性,作出必要的特定優化。例如主旋律和伴奏旋律具備顯著差別性,而伴奏旋律中又包括器樂獨奏和器樂合奏。即使對於同一種類型,樂器的適用音域,演奏方式等也存在差別。所以,應有針對地設計各種變換模型,利用調度器將變換的每一個分步驟分配到合適的模型進行處理。最終經過綜合器對全部輸出結果進行彙總。

   在本算法中,劃分了四個變換模型,即solo melody、solo instrumental、chord和percussion,負責根據輸入分別對旋律獨奏、器樂獨奏、和絃型伴奏和打擊樂的音符序列進行變換操做。

   最後須要由一個調度器負責結果的彙總。這裏採用調度-綜合的方式,避免了碎片化,提升封裝性,同時利於並行計算的實現。

   變換層即是由這四個模型與調度器、綜合器共同組成的子系統。

 

3. 記譜層(Notation layout)

  一方面,算法內部持有獨立的音符序列量化方法,另外一方面,算法內部的音符序列須要轉換到人類可讀的曲譜中,這就須要選擇適當的記譜格式。目前主流格式包括MusicXML,MIDI文件等,其中MusicXML普遍應用於各種記譜軟件中,而MIDI文件則更爲通用,幾乎全部的編曲或音頻工做站都支持MIDI文件格式,但MIDI文件是由頭信息和大量MIDI事件組成,更側重於對數字樂器的自動化控制,而非曲譜的表達。不論何種記譜格式,選擇的主要標準爲:是否具備準確表示音符序列的能力。

  根據譜表文件的格式,將算法內的量化標準,映射到目標譜表文件的量化標準,並按照目標格式存儲,是記譜層須要完成的工做。

 

2、做曲算法的程序設計

  激勵層-變換層-記譜層的三層模型爲算法設計提供了總體思路,接下來重點討論算法和程序的設計與實現,其中根據和聲學、配器法和曲式結構的相關理論創建樂理模型是本算法的核心,其次還包括知識庫系統的創建和記譜層的設計思路。

2.1 樂理模型的創建

  爲了便於進一步的建模,算法內部將音符表示爲一個四維向量Vn=(p, v, s, e),該向量的四個維度分別對應音符的音調、力度、起音時間和關斷時間。而算法在輸出階段須要將四維向量組成的序列映射到人類可讀的曲譜中。
  算法內部的量化過程須要遵循必定的標準,對於時間的量化,考慮到不一樣拍速(tempo)對音符時值的影響,只能以相對值的形式表示,具體方法是:採用固定的採樣頻率Fs對音符時值進行採樣。
    Fs = 1 / Tq
  Tq爲當前拍速下,每一個參考音符持續時間。參考音符時值決定量化精度,參考音符時值與量化精度成反比。
  將每一個採樣的時值除以Tq,便獲得時間刻度相對值。
  對於音調的量化,咱們採用MIDI標準,利用連續的整數爲每一個8度音階中的音符編碼,並肯定中央C(C4)編碼爲60。p值與音調呈正相關,p值差值的樂理含義爲以音數爲單位的音程差。力度採用128級分層,v = 0表示最弱,而v = 127表示最強。

 

2.1.1 調式建模

  調式規定了各音之間的音程關係。調式系統中最重要的是天然大調和天然小調,天然大調的規則是:除了中音和下屬音,下主音和主音這兩對音之間的音程是小二度以外,其餘相鄰兩音之間都是大二度 ,其音程總結來講即是「全全半全全全半」;而天然小調的音程關係則能夠總結爲「全半全全半全全「。

  下表總結了兩個八度內,天然大調與天然小調內每一個音與主音的相對關係。

  音階 C0 D0 E0 F0 G0 A0 B0 C1 D1 E1 F1 G1 A1 B1
天然大調 音數 0 2 4 5 7 9 11 12 14 16 17 19 21 23
天然小調 音數 0 2 3 5 7 8 10 12 14 15 17 19 20 22

  根據變換層的需求,這裏給出調式變換的實現思路。已知主音音高,則能夠換算出調內音的音高。而已知任意音和主音,卻不必定能換算出調內音。這是由於任意音不必定都在調內。通過實驗,選取離源音高最近的調內音便可知足要求。考慮到調內音的偏移變換,在程序末尾添加循環偏移,保證變換後調內音位於兩個八度以內。最終得出調內音後,根據偏移便可計算出絕對音高。算法程序以下。

 1 int pitch_get_in_scale(int pitch, int diff_tone, int key, int scale)
 2 {
 3   const int *in_scale_list = scale_component_pitch[scale];
 4   int in_scale_count = SCALE_COMPONENT_PITCH_NUM;
 5 
 6   int pitch_offset = util::floor_mod(pitch - key, 12);
 7   int index;
 8   for(index=0; index < in_scale_count; index++)
 9     {
10       if( pitch_offset == in_scale_list[index] )
11         break;
12     }
13   if( index == in_scale_count ) /* pitch in 12-tone is not in-scale, then utilize the nearest scale */
14     {
15       index--;
16       for(int i=0; i < in_scale_count; i++)
17         if( pitch_offset < in_scale_list[i] )
18           {
19             index = i;
20             break;
21           }
22     }
23 
24   int shifted_index = index + diff_tone;
25 
26   while( shifted_index > in_scale_count ) /* shift down if index is higher than current 2 oct */
27     {
28         index -= in_scale_count / 2;
29         shifted_index -= in_scale_count / 2;
30         pitch_offset -= 12;
31     }
32   while( shifted_index < 0 ) /* shift up when index is lower than current 2 oct */
33     {
34        index += in_scale_count / 2;
35        shifted_index += in_scale_count / 2;
36        pitch_offset += 12;
37     }
38 
39   return pitch + (in_scale_list[shifted_index] - pitch_offset);
40 }

 

2.1.2 和聲問題建模

  和聲學是一門研究和聲的產生、構成原則,和絃的鏈接與相互關係,和聲風格的造成、發展與演變的學科,而本算法僅涉及與和絃相關的內容。

  爲了便於研究,首先規定和絃的表示方法:和絃採用二維向量Vc=(r, s)表示,r爲和絃根音編碼,從0開始按照C調音階(C、#C、D、#D、E、F、#F、G、#G、A、#A、B)的順序編碼;s爲和絃記號,該編碼主要規則以下:

  0
m 1
m7 2
7 3
M7 4
aug 5
dim 6
dim7 7
sus2 8
sus4 9
7sus4 10
6sus4 11
6 12
m6 13
-5 14
+5 15
M7+5 16
m-5 17
7-5 18
7+5 19

所以,Vc=(0, 0)表示C和絃,Vc=(0, 1)表示Cm和絃。

  在接下來的算法中涉及取音階內相對音高的運算。爲了便於實現,本文采用基於floor除法的取餘運算,所以需另外規定floor_mod函數的實現。

1 template <typename T>
2   static inline T floor_mod(T op1, T op2)
3     {
4       return op1 - (op2 * static_cast<T>(std::floor(float(op1)/float(op2))));
5     }

  此時實現和絃轉換便很是容易,只須要轉換和絃根音,並保持和絃標記便可。

1 /**
2  * @brief Shift a chord by root with offset.
3  */
4 ChordPair chord_shift(const ChordPair &chord, int root_offset)
5 {
6   return ChordPair(util::floor_mod(chord.root + root_offset, 12), chord.sign);
7 }

 

  和絃是由幾個固定音程的音疊置組成的,爲了表示這種關係,咱們採用二維數組存儲組成音之間的相對偏移音數。值得注意的是,存在一部分和絃有4個組成音,而其它和絃只有3個組成音的狀況。因爲和絃根音與比根音高八度的音呈徹底協和音程關係,所以對於只有3個組成音的和絃而言,在第4個音位置添加八度音數。

 1 #define CHORD_COMPONENT_PITCH_NUM 4
 2 static const int chord_component_pitch[CHORD_SIGN_NUM][CHORD_COMPONENT_PITCH_NUM] =
 3   {
 4     {0, 4, 7, 12},      /**/
 5     {0, 3, 7, 12},      /* m */
 6     {0, 3, 7, 10},      /* m7 */
 7     {0, 4, 7, 10},      /* 7 */
 8     {0, 4, 7, 11},      /* M7 */
 9     {0, 4, 8, 12},      /* aug */
10     {0, 3, 6, 12},      /* dim */
11     {0, 3, 6, 9},       /* dim7 */
12     {0, 2, 7, 12},      /* sus2 */
13     {0, 5, 7, 12},      /* sus4 */
14     {0, 5, 7, 10},      /* 7sus4 */
15     {0, 5, 7, 9},       /* 6sus4 */
16     {0, 4, 7, 9},       /* 6 */
17     {0, 3, 7, 9},       /* m6 */
18     {0, 4, 6, 12},      /* -5 */
19     {0, 4, 8, 12},      /* +5 */
20     {0, 4, 8, 11},      /* M7+5 */
21     {0, 3, 6, 12},      /* m-5 */
22     {0, 4, 6, 10},      /* 7-5 */
23     {0, 4, 8, 10}       /* 7+5 */
24   };

  考慮變換層的需求,這裏提出對音序進行和絃變換的思路。爲了簡化問題,引入滑動窗口的變換方法,即假設音序中每拍對應一種和絃,因而對音序的和絃變換可拆分爲對每拍音符子序的變換,這使得每一步都只涉及一種和絃。

  滑動窗口時肯定當前窗口的起始位置和結束位置,每次僅轉換徹底被窗口包圍的音符,轉換完成後移動窗口至下一拍。滑動窗口的有效長度至關於一個1/4音符的長度,窗口的起止位置由以下通項決定(起止位置採用1/64量化),其中n>=1表示的窗口編號:

  Sn = 16 * (n - 1), En = 16 * n.

  對於每一個窗口的和絃變換。首先應界定新音序的音區,不然可能出現音調太高或太低的狀況,這裏以目標和絃根音與源和絃根音的音程之差做爲衡量依據,肯定是否有必要對音序進行升調或降調。根據實驗結果,判斷閾值肯定爲五度時效果最佳。當目標和絃高出源和絃五度以上時,新音序在源音高基礎上下降八度,反之,當目標和比源和絃低五度以上時,新音序應升高八度。

  本文提出一種基於和絃根音音程類似度的和絃變換方法。和絃變換依賴兩方面的輸入,其一爲原始和絃,其二爲目標和絃。全部變換都必須在調式體系下進行。爲了便於表達,首先定義音程差函數 f(p, r),p爲音序中音符的音調,r爲和絃根音的音調。爲了保證轉換先後音符與和絃根音音程差關係的類似性,再引入殘差Q,其中n爲音序中全部音符的個數,{Sn}爲原始音符組成的集合,而{Sr}{Dr}分別爲原始和絃與目標和絃的根音組成的集合。算法的目標是,求出全部知足約束條件的Xi,使得Q取最小值。

  Xi的約束條件以下:設集合S爲調內音階中從目標和絃的根音開始組成的子音階,則Xi∈S。該約束條件的目的是保證結果始終處在目標調式系統中,而不出現離調。

  爲了便於程序實現,在前序音區範圍的界定下,全部和絃變換都只需在一個八度音階下進行,具體方法爲:求出目標和絃根音的調內音Dri,再對原始音序中的音符逐一處理,求出每一個音符與原始和絃根音的音程差Sni,在高於Dri的調內音階中進行遍歷,找出使得Q最小的Xi。

  以下爲針對每一個音符進行變換的算法實現,其中變量dst_scale保存目標調式的索引,而變量dst_chord_tone保存了目標和絃根音的調內音。通過循環比較後,變量note_chord保存最終結果,即調內目標和絃根音的音程差。該結果與目標和絃根音相加,便獲得八度音階內的相對音高,最終根據界定的音區,可換算出新音符的絕對音高。

int min_delta = 100;
int min_delta_index = 0;
for(int j=dst_chord_tone + 1; j < SCALE_COMPONENT_PITCH_NUM; j++)
  {
    int Td = scale_component_pitch[dst_scale][j] - scale_component_pitch[dst_scale][dst_chord_tone];
    int delta = ABS(Td - Sd);
    if( ABS(delta) < min_delta )
     {
       min_delta = delta;
       min_delta_index = j;
     }
  }
note_chord = scale_component_pitch[dst_scale][min_delta_index] - scale_component_pitch[dst_scale][dst_chord_tone];

 

  如圖所示爲一個測試音序樣本,該樣本爲bB調,包含和絃走向#A, #A, F, F, Gm, Gm, F, F, #D, #D, Dm, Dm, Cm, Cm, F, F, #A, #A, F, F, Gm, Gm, F, F, #D, #D, Dm, Dm, Cm, Cm, F, F。採用該樣本測試算法的初步效果。

  設定目標調性爲#C調,目標和絃爲Fm, Fm, Fm, #C, #C, #C, Fm, Fm, Fm, #G, #G, #G, Fm, Fm, Fm, #C, #C, #C, #G, #G, #G, #C, #C, #C,目標節拍爲3/4拍。通過和絃變換後得出的音序如圖所示。因爲原始節拍與目標節拍不一樣,所以在變換過程當中對原始音序位置和時值進行了伸縮變換,原始音序長度大於和結果音序。

  該測試用例直觀地反映了和絃變換對音序的影響程度,與原始音序相比,變換後的音序在保持旋律的和諧性的基礎下,呈現出大相徑庭的效果。而爲了更加精確地對殘差Q進行評估,咱們將對上述音序變換過程當中每一個音符對應的原始音程差fs,目標音程差fd,以及殘差Q繪製成平滑曲線,以下圖所示。

 

2.1.3 配器問題建模

  配器是爲每一個聲部分配樂器的過程。在進行配器時,整個音樂做品的結構已經基本肯定了,這時只需爲每一個音軌選擇合適的音色。從流行音樂角度出發,流行音樂聲部注意包括:主旋律聲部、管絃伴奏聲部、打擊伴奏聲部,一個完整的做品至少應包含主旋律,而管絃伴奏和打擊聲部則能夠選擇性地保留;此外還包括一些裝飾音聲部,包括特殊效果器、合成器、特殊採樣等。因爲音色和音型的相關關係,做曲算法中的配器即是經過音色篩選音型的過程。本節從各類樂器之間的共性和特性出發,總結出適合程序實現的配器方法。

  首先分析音色之間的差別性:因爲不一樣樂器之間,適用音域、演奏方法、功能角色等不盡相同,每種樂器所對應的音型是不一樣的,這就要求激勵層充分考慮到樂器的差別,所以首先須要對General MIDI音色進行分類。事實上GM音色編碼的排列是按照音色規律進行的,但咱們但願給出對音型的分類,即音型堆分類,因而按照GM音色表將音型分爲了鋼琴、吉他、貝斯、風琴、鼓組、絃樂、管風、效果器、民樂和未定義十個堆。經過創建一個二維數組簡單地描述這種關係。

 1 #define GM_TIMBRE_BANK_NUM 10
 2 static const int gm_timbre_banks[GM_TIMBRE_BANK_NUM][27] =
 3   {
 4     {0, 1, 2, 3, 4, 5, 6, 7, -1},           /* Piano */
 5     {24, 25, 26, 27, 28, 29, 30, 31, -1},   /* Guitar */
 6     {32, 33, 34, 35, 36, 37, 38, 39, -1},   /* Bass */
 7     {16, 17, 18, 19, 20, 21, 22, 23, -1},   /* Organ */
 8     {128, -1},                              /* Drums */
 9     {40, 41, 42, 43, 44, 45, 48, 49, 50, 51, 52, 53, 54, 55, -1}, /* Strings */
10     {56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, -1}, /* Wind */
11     {80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, -1}, /* Effect */
12     {104, 105, 106, 107, 108, 109, 110, 111, -1}, /* National */
13     {8, 9, 10, 11, 12, 13, 14, 15, 46, 47, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, -1} /* Unsorted */
14   };

  從宏觀的的角度對音型進行分類,則能夠分爲伴奏類、獨奏類、加花類三種,這些類型都是互相獨立,不可替換的。

  再分析共同性:在同一類樂器間,某些音色是能夠互換的,正是這種互換性減小了分類討論的種數,同時也爲配器提供了更對的選擇。音型替換是在音型堆的層面上進行,但前提是類型必須相同,由於替換後的音型必須符和風格要求。以下代碼經過創建二維數組描述各音型堆之間的替換關係。

#define RELATED_TIMBRES_NUM 10
static const int related_timbre_banks[RELATED_TIMBRES_NUM][8] =
  {
    {FIGURE_BANK_GUITAR, FIGURE_BANK_PIANO, FIGURE_BANK_STRINGS, FIGURE_BANK_WIND, FIGURE_BANK_ORGAN, FIGURE_BANK_EFFECT, FIGURE_BANK_UNSORTED, -1},    /* Piano */
    {FIGURE_BANK_GUITAR, FIGURE_BANK_PIANO, FIGURE_BANK_STRINGS, FIGURE_BANK_WIND, FIGURE_BANK_ORGAN, FIGURE_BANK_EFFECT, FIGURE_BANK_UNSORTED, -1},    /* Guitar */
    {FIGURE_BANK_BASS, -1},             /* Bass */
    {FIGURE_BANK_STRINGS, FIGURE_BANK_WIND, FIGURE_BANK_ORGAN, FIGURE_BANK_EFFECT, FIGURE_BANK_PIANO, -1},      /* Strings */
    {FIGURE_BANK_DRUMS, -1},            /* Drums */
    {FIGURE_BANK_WIND, FIGURE_BANK_STRINGS, FIGURE_BANK_ORGAN, FIGURE_BANK_EFFECT, FIGURE_BANK_PIANO, -1},      /* Wind */
    {FIGURE_BANK_ORGAN, FIGURE_BANK_STRINGS, FIGURE_BANK_WIND, FIGURE_BANK_EFFECT, FIGURE_BANK_PIANO, -1},      /* Organ */
    {FIGURE_BANK_EFFECT, FIGURE_BANK_STRINGS, FIGURE_BANK_WIND, FIGURE_BANK_ORGAN, FIGURE_BANK_PIANO, -1},      /* Effect */
    {FIGURE_BANK_NATIONAL, FIGURE_BANK_PIANO, FIGURE_BANK_STRINGS, FIGURE_BANK_WIND, FIGURE_BANK_ORGAN, FIGURE_BANK_EFFECT, FIGURE_BANK_UNSORTED, -1},  /* National */
    {FIGURE_BANK_UNSORTED, FIGURE_BANK_PIANO, FIGURE_BANK_WIND, FIGURE_BANK_ORGAN, FIGURE_BANK_EFFECT, FIGURE_BANK_NATIONAL, -1}        /* Unsorted */
  };

  對音型的篩選,須要以當前聲部爲參考,優先選取知足當前類型的全部音序。假若得出的音型數量不足時,再考慮互換音色。以下程序給出了具體實現,循環體中,首先進行精確匹配,若找到匹配項則繼續下次循環,不然進行模糊匹配。模糊匹配的前提是音型的類型一致,其次是兩種音型具備可互換的音型堆。

 1 std::vector<const KnowledgeEntry *> related_entries;
 2 std::vector<int> related_tracks;
 3 
 4 for(std::size_t i=0; i < knowledge_entries.size(); i++)
 5   {
 6     std::vector<int> timbre_banks;
 7     std::vector<int> figure_banks;
 8     std::vector<int> figure_classes;
 9 
10     if( int err = get_timbres(knowledge_entries[i], timbre_banks, figure_banks, figure_classes) )
11       return err;
12 
13     bool not_found = true;
14     for(std::size_t j=0; j < timbre_banks.size(); j++)
15       {
16         if( figure_bank == figure_banks[j] && figure_class == figure_classes[j] )
17           {
18             not_found = false;
19             dst_entries.push_back(knowledge_entries[i]);
20             dst_tracks.push_back(j);
21             break;
22           }
23       }
24     if( not_found )
25       {
26         for(std::size_t j=0; j < timbre_banks.size(); j++)
27           {
28             if( figure_class == figure_classes[j] )
29               {
30                 if( is_timbre_bank_related(figure_bank, figure_banks[j]) )
31                   {
32                     related_entries.push_back(knowledge_entries[i]);
33                     related_tracks.push_back(j);
34                     break;
35                   }
36               }
37           }
38       }
39   }
40 
41 /*
42  * Utilize related figures when the quantity of matched figures is poor.
43  */
44 if( dst_entries.size() < 10 )
45   {
46     for(std::size_t i=0; i < related_entries.size(); i++)
47       dst_entries.push_back(related_entries[i]);
48     for(std::size_t i=0; i < related_tracks.size(); i++)
49       dst_tracks.push_back(related_tracks[i]);
50   }

  在音型篩選的基礎上,只需考慮聲部安排便可實現配器。在這以前咱們討論的做曲模型是面向單個音軌的,爲了便於多聲部做曲,有必要對做曲模型作出調整。首先爲N個音軌分配對應的邏輯變換層,注意邏輯實例與實例是兩個相對的概念,算法容許內存中只有一個變換層實例的狀況;其次在主數據路徑上添加配器篩選,調整後的做曲模型如圖所示:

 

  根據動機激勵器輸出,循環對每一個變換層進行配器。算法維護一個全局集合U,集合U記錄必須排除的樂器類型。每次配器前,算法都隨機選取樂器,並檢查當前樂器是否存在於U中,若存在則從新選取。得出樂器後,根據音色與音型的篩選規則對知識庫中的音型進行篩選,得出最終音型併發送到變換層處理。若選取的樂器爲主旋律樂器或鼓組,則將當前樂器添加到集合U中,不然無需添加到集合U。爲每一個音軌都分配了樂器後,算法結束。

  全局集合U的主要做用在於避免同一音樂做品中出現重複的主旋律或鼓組,同時不會約束其它樂器的重複出現。例如流行音樂中出現雙吉他或者多組絃樂是能夠接受的。

 

2.1.4 曲式結構的建模 

  流行音樂在發展過程當中,已經造成了基本穩定的曲式結構。算法做曲中對曲式結構的處理與對配器的處理徹底類似,其本質都爲根據曲式結構與音型的相關關係對音型進行篩選。曲式結構一樣能夠用模板來表達。而不一樣結構之間,一樣具備可互換替換性。整體來講,曲式結構篩選算法的實現思路與配器篩選算法基本一致,這裏不在贅述。

  以下程序實現曲式結構的篩選,其思路很是簡單:首先進行精確篩選,若沒法得出結果,則採起替換規則。若篩選後仍無結果,則從原始音型中隨機選取。

const FigureListEntry *pick_form(StructureForm::FormType form, const std::vector<const FigureListEntry *> &forms_vector)
{
  for(std::size_t i=0; i < forms_vector.size(); i++)
    if( int(form) == forms_vector[i]->segment )
      return forms_vector[i];

  const StructureForm::FormType *candidate_form = form_replacement_rules[form];

  for(unsigned int i=0; candidate_form[i] != StructureForm::FORM_INVALID; i++)
    for(std::size_t j=0; j < forms_vector.size(); j++)
      if( int(candidate_form[i]) == forms_vector[j]->segment )
        return forms_vector[j];

  return util::random_choice(forms_vector);
}

  爲了方便曲式結構的轉換,須要實現對不一樣長度音型之間的轉換。一種方法是對音符起止位置進行比例伸縮變換,但這種方法將致使原始音型拍速的改變,所以設計非連續的伸縮變換是解決問題的關鍵。另外,轉換分爲上行轉換和下行轉換兩種狀況:對於上行轉換,要求從短音型轉換到一個較長的音型,而下行轉換則偏偏相反。下面就兩種狀況分別作出討論。

  上行轉換時,因爲拍速的限制,不能對原始音型進行拉伸。爲了便於實現,採用重複原始音型結尾的方法。設原始音型長度爲Ls,目標音型長度爲Ld,ΔL0 = |Ls - Ld|,則又有以下兩種狀況:

  1. ΔL0 <= Ls,此時能夠直接從原始音型Ls - ΔL0位置開始,選取音符填補到空缺位置

  2. ΔL0 > Ls,因爲空缺位置長度超出了原始音符的長度,所以不能簡單地從Ls - ΔL0開始。此時能夠分塊填補,即:每次從Ls - min{Ls, ΔL}位置開始,每次填補後將ΔL減去已經填補的長度,並繼續下次填補,直到沒有空缺爲止。開始位置與ΔL的遞推式以下:

  下行轉換時,狀況則簡單不少。咱們一樣不能對所有音符進行壓縮,爲了簡化,直接將原始音型中,起音時間超出目標長度的音符刪除,並修改起音時間在容許範圍內,但關斷時間超出目標長度的音符,使其在目標音型長度的時間內關斷。

 

  向做曲算法整體框架中添加曲式結構篩選,如圖所示爲修改後的框架。

 

2.2 知識庫設計

  知識庫是知識庫系統中主要的數據來源,承擔數據的存取和調用的工做。咱們的知識庫採用YAML做爲底層格式。YAML是YAML Ain't Markup Language的遞歸縮寫,該語言旨在強化數據的中心地位,而不是語言自己,相較於XML等較爲複雜的標記語言,YAML提供了簡潔的格式,便於人類閱讀修改的同時,也能下降機器解析的成本。本文采用開源的yaml-cpp類庫實現YAML文件的解析工做。

  首先須要大致規劃知識庫。要求大體明確知識庫中存儲的數據內容,同時提供可擴展的特性,方便後續添加新的數據項。注意到整個知識庫可看做大型一維數組,而數組中的每一個元素構成了知識庫中的每一項數據。對於每項數據,咱們提供兩個入口knowledge_array、knowledge_constraint,knowledge_array是數據項內的小型數組,負責存儲知識實體;而knowledge_constraint是一個字典,負責存儲本數據項的相關參數,例如調式,拍號,速度等。

  以下爲數據文件中,一個knowledge_array的示例:

knowledge_array:
  -
    timbre_bank: 1
    figure_bank: 1
    class: 1
    figure_list: 
      - node:
        chord: [10,0,10,0,10,0,10,0,5,0,5,0,5,0,5,0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,10,0,...]
        segment: 1
        offset: 1
        begin: 0
        end: 8
        pitch: []
      - node:
        chord: [10,0,10,0,10,0,10,0,3,0,3,0,3,0,3,0,7,1,7,1,7,1,7,1,5,0,5,0,5,0,5,0,3,0,3,0,3,0,3,0,2,1,2,1,2,1,2,1,0,1,0,1,0,1,0,1,5,0,5,0,5,0,5,0]
        segment: 2
        offset: 1
        begin: 8
        end: 16
        pitch: [65,120,12,16,74,120,16,24,74,120,24,28,72,120,28,32,...]

  該實例顯示了knowledge_array中的每一個知識實體都具備的基本參數,timbre_bank反映了該實體的GM音色編號。figure_bank反映了該實體的音型堆編號,知識庫爲各類音型堆分配了連續的整數編號,見表1。

索引 符號 描述
0 FIGURE_BANK_PIANO 鋼琴
1 FIGURE_BANK_MELODY 旋律獨奏
2 FIGURE_BANK_GUITAR 吉他
3 FIGURE_BANK_BASS 貝斯
4 FIGURE_BANK_ORGAN 風琴
5 FIGURE_BANK_DRUMS 鼓組
6 FIGURE_BANK_STRINGS 絃樂
7 FIGURE_BANK_WIND 吹奏
8 FIGURE_BANK_FX 效果
9 FIGURE_BANK_NATIONAL 民樂
10 FIGURE_BANK_UNSORTED 未分類

表1

  class反映了該實體的音型特徵,具體編號見表2

索引 符號 描述
0 FIGURE_CLASS_CHORD 和絃
1 FIGURE_CLASS_SOLO 獨奏
2 FIGURE_CLASS_DEC 加花

表2

  figure_list中記錄了全部可能的音型。其中chord記錄了該音型對應的和絃序列。和絃序列由若干和絃向量組成。segment記錄了該音符序列可能出現的曲式結構。begin、end、offset記錄該音符序列在完整做品中出現的位置信息。pitch記錄了全部音符,音符以四維向量Vn=(p, v, s, e)的形式表示。

 

  而knowledge_constraint存儲的信息則比較簡單,以下爲一個實例:

knowledge_constraint:
  key: 10
  scale: 0
  tempo: 69
  time_beats: 4
  time_beat_type: 4
  character: [0,1,2,3,4,5,6,7]
  genre: [0,1,2]

  其中前五個字段主要記錄了數據項的一些基本參數,包括調式、調性、拍速、拍號。character和genre記錄了反映音型特性的數字索引。

  有了數據的具體格式,就能夠開始知識庫的設計工做。知識庫的首要任務就是將磁盤中的數據文件加載到內存中,並解析爲可用的數據結構,經過一個全局鏈表保存全部數據項。以下爲加載和解析數據文件函數的僞代碼:

 1 try
 2 {
 3   YAML::Node root = YAML::LoadFile(filename);
 4   YAML::Node knowledge_array = root["knowledge_array"];
 5   YAML::Node knowledge_constraint = root["knowledge_constraint"];
 6 
 7   if( knowledge_array.IsSequence() && knowledge_constraint.IsMap() )
 8     {
 9       (Create a data entry)
10       for(std::size_t i=0; i < knowledge_array.size(); i++)
11         {
12           (Read-in figures from knowledge_array)
13           (Push figures into the list of data entry)
14         }
15 
16       (Read-in parameetrs from knowledge_constraint)
17       (Modify data entry)
18       (Push data entry into global list)
19     }
20 }
21 catch( YAML::Exception &excp )
22 {
23   (error handling)
24 }

 

  其次還應提供操做數據的接口,包括數據的存取等。本算法所涉及的數據存取較爲簡單,主要爲經過character和genre匹配全部和絃序列、樂器音色或完整的音型,其次爲提供經過索引隨機訪問數據庫的接口。完整的知識庫接口以下:

 1 public:
 2   int loadModel(const char *filename);
 3   inline std::vector<const KnowledgeEntry *> & models()
 4     {
 5       return m_knowledgeEntries;
 6     }
 7     
 8   int getChord(std::vector<const KnowledgeEntry *> & dst, int character);
 9   int getTimbreBank(std::vector<const KnowledgeEntry *> & dst, int genre);
10   int getKnowledgeEntry(std::vector<const KnowledgeEntry *> & dst, int character);
11   int getKnowledgeEntry(std::vector<const KnowledgeEntry *> & dst, int character, int genre);

 

2.3 變換層模型設計

  變換層主要涉及四種基本模型。在抽象一節咱們指出,變換的對象涉及兩個音型:原始音型與目標音型,針對不一樣的音型類型有不一樣的變換方法。因爲篇幅有限,本節重點從獨奏音型變換和伴奏音型變換兩個層面論述。

2.3.1 solo音型變換

  solo音型的變換,根本目標是使變換後的旋律與原始旋律類似,而保持原始節奏。該算法的價值在於能得到大量新的音型,從而爲做曲過程增長變化。算法的輸入爲原始音序和目標音序,約束條件是原始音型的和絃走向,輸出爲變換後的音序。

  旋律變換須要解決的首要問題是原始音型音符數量與目標音型不一樣,原始節奏與目標節奏並不呈現一一對應關係,所以如何肯定目標音型中的每一個音便成爲了問題的關鍵。咱們採用取區間內平均值的方法,對於目標音型中每一個音符,其開始位置爲Ps,結束位置爲Pe,T爲原始音型中由Ps開始到Pe截止的全部音符的平均值,則易知目標音符Td與T有某種關聯。首先考慮直接取Td = T,發現得出的音型很是不協和,緣由是直接取平均值的方法沒有考慮到各音符直接音程的關係,出現大量不協和音程,解決辦法是從原始音型中選取音符,最大程度保留原始音程關係。

  參考前一節和絃走向變換中殘差Q的表示方法,咱們引入由原始音型中從Ps到Pe的全部音符組成的集合N,表示solo音型變換的殘差以下:

  但這種方法存在某種侷限性,由於使得殘差Q最小的的Td,不必定能與當前和絃相協和。而一旦出現這種不協和音,對最終效果的貢獻每每是負面的。解決的辦法是額外引入一個約束參數,使得Td與當前和絃達到必定程度上的協和。

  列中,g(x)爲音符x與當前和絃協和程度的評估函數,g(x)取值與協和程度正相關。Gthreshold爲協和程度的閾值,當超過這個閾值時,說明該音符協和程度達到了要求。至此,咱們已經創建了音高變換的基本模型。

  考慮評估函數g(x)的實現。對於出如今和絃組成音中的音,咱們能夠認爲該音與和絃徹底協和,規定此時g(x)=1。則對於徹底不協和與徹底協和之間的每一個音符,g(x)均可以取到(0,1]之間的某個數值。因爲原始音與和絃組成音的音程差與協和程度呈現負相關,所以用C++設計評估函數以下。

 1 float g_pitch_chord(int pitch, const ChordPair &chord)
 2 {
 3   int pitch_offset = util::floor_mod(pitch - chord.root, 12);
 4   int min_delta = 128;
 5   for(unsigned int i=0; i < CHORD_COMPONENT_PITCH_NUM; i++)
 6     {
 7       if( chord_component_pitch[chord.sign][i] == pitch_offset )
 8         return 1.0f;
 9       int delta = ABS(chord_component_pitch[chord.sign][i] - pitch_offset);
10       if( delta < min_delta)
11         min_delta = delta;
12     }
13   return 1.0f / min_delta;
14 }

   上述模型還須要考慮另外兩種特殊狀況,其一:原始音符與目標音符的起止位置嚴格對應,此時直接取原始音高做爲Td;其二:在原始音符中沒法匹配與當前和絃走向協和的音,此時只能從全部原始音符中選取適合的Td。變換過程關鍵代碼以下:

  1 for(int i=0; i < barlen; i++)
  2     {
  3       std::vector<PitchNote> reg_rhythm_figure_list;
  4       std::vector<PitchNote> reg_pitch_figure_list;
  5 
  6       /*
  7        * Data Window
  8        * Only does it process notes that is in the current bar.
  9        */
 10       int32_t bar_start = i * 2 * 2 * 2 * 2 * beats;
 11       int32_t bar_end = (i + 1) * 2 * 2 * 2 * 2 * beats;
 12       for(std::size_t j=0; j < src_figures.size(); j++)
 13         {
 14           if( bar_start <= dst[j].start && dst[j].start < bar_end )
 15             reg_pitch_figure_list.push_back(dst[j]);
 16         }
 17       for(std::size_t j=0; j < current_rhythm_figures.size(); j++)
 18         {
 19           if( bar_start <= current_rhythm_figures[j].start && current_rhythm_figures[j].start < bar_end )
 20             reg_rhythm_figure_list.push_back(current_rhythm_figures[j]);
 21         }
 22 
 23       for(std::size_t j=0; j < reg_rhythm_figure_list.size(); j++)
 24         {
 25           PitchNote &rhythm_figure = reg_rhythm_figure_list[j];
 26           int32_t start_figure = rhythm_figure.start;
 27           int32_t end_figure = rhythm_figure.end;
 28 
 29           std::vector<PitchNote> candidate_pitch_figure_list;
 30           for(std::size_t k=0; k < reg_pitch_figure_list.size(); k++)
 31             {
 32               const PitchNote &pitch_figure = reg_pitch_figure_list[k];
 33               if( pitch_figure.end > start_figure || end_figure < pitch_figure.start )
 34                 candidate_pitch_figure_list.push_back(pitch_figure);
 35             }
 36 
 37           if( candidate_pitch_figure_list.size() )
 38             {
 39               if( candidate_pitch_figure_list.size() == 1 )
 40                   rhythm_figure.pitch = candidate_pitch_figure_list[0].pitch;
 41               else
 42                 {
 43                   /*
 44                    * Statistics the quantity of in-chord notes and point out the average pitch.
 45                    * The average pitch will be considered as the center (main) pitch.
 46                    */
 47                   std::vector<int> in_chord_index;
 48                   int sum_pitch_no = 0;
 49                   for(std::size_t k=0; k < candidate_pitch_figure_list.size(); k++)
 50                     {
 51                       int pitch_no = candidate_pitch_figure_list[k].pitch;
 52                       if(theory::pitch_is_in_chord(pitch_no, chord_list[i]))
 53                         in_chord_index.push_back(k);
 54                       sum_pitch_no += pitch_no;
 55                     }
 56                   int avg_pitch_no = int(float(sum_pitch_no) / candidate_pitch_figure_list.size());
 57 
 58                   /*
 59                    * Set the pitch of current note as the nearest pitch related to the center pitch,
 60                    * which eliminates the possibility that rhythm becomes out of tone.
 61                    */
 62                   if( in_chord_index.size() == 1 )
 63                       rhythm_figure.pitch = candidate_pitch_figure_list[in_chord_index[0]].pitch;
 64                   else if( in_chord_index.size() > 1 )
 65                     {
 66                       int nearest_pitch_no = avg_pitch_no;
 67                       int nearest_pitch_dis = 129;
 68                       for(std::size_t m=0; m < in_chord_index.size(); m++)
 69                         {
 70                           int index = in_chord_index[m];
 71                           if( ABS(candidate_pitch_figure_list[index].pitch - avg_pitch_no) < nearest_pitch_dis )
 72                             {
 73                               nearest_pitch_dis = ABS(candidate_pitch_figure_list[index].pitch - avg_pitch_no);
 74                               nearest_pitch_no = candidate_pitch_figure_list[index].pitch;
 75                             }
 76                         }
 77                         rhythm_figure.pitch = nearest_pitch_no;
 78                     }
 79                   else
 80                     {
 81                       int nearest_pitch_no = avg_pitch_no;
 82                       int nearest_pitch_dis = 129;
 83                       for(std::size_t index=0; index < candidate_pitch_figure_list.size(); index++)
 84                         {
 85                           const PitchNote &figure_pitch = candidate_pitch_figure_list[index];
 86                           if( ABS(figure_pitch.pitch - avg_pitch_no) < nearest_pitch_dis )
 87                             {
 88                               nearest_pitch_dis = ABS(figure_pitch.pitch - avg_pitch_no);
 89                               nearest_pitch_no = figure_pitch.pitch;
 90                             }
 91                         }
 92                         rhythm_figure.pitch = nearest_pitch_no;
 93                     }
 94                 }
 95             }
 96           if( (candidate_pitch_figure_list.size() == 0 || j == reg_rhythm_figure_list.size() - 1) && reg_pitch_figure_list.size() > 0 )
 97             rhythm_figure.pitch = reg_pitch_figure_list[reg_pitch_figure_list.size()-1].pitch;
 98         }
 99 
100       for(std::size_t j=0; j < reg_rhythm_figure_list.size(); j++)
101         dst.push_back(reg_rhythm_figure_list[j]);
102     }

 

  爲了評估solo變換模型,咱們仍然選擇以前的測試的樣本做爲原始音型,而選擇另外一樣本做爲目標音型,目標音型如圖所示。

  通過solo變換後的音型以下圖所示。

  以音符序號爲橫座標,Td和T爲縱座標,將變換中的T和結果Td繪製成平滑曲線,如圖所示。

 

2.4 記譜層設計 

  記譜層須要解決表達整個音樂做品的數據結構的問題。流行音樂是由若干個曲式結構(Form)構成的,而完整的音樂做品,能夠表達爲由N個forms組成的鏈表,如圖顯示了鏈表結構。每一個form都具備可動態附加的數據項。鏈表結構使得算法能夠流水線式地向結構中追加數據,隨着數據在算法中的逐層流動,鏈表所保存的信息將逐步增長。當流動到記譜層時,鏈表所擁有的信息已經足夠組織完整的音樂做品。

  爲了提升通用性,記譜層採用MIDI文件做爲最終格式,本節所論述內容也圍繞MIDI文件的生成,分析了MIDI文件格式的具體內容與算法內部格式到MIDI規範格式的轉換。

  MIDI文件是二進制文件,一個標準的MIDI文件包含了文件頭(Headers)和由事件塊數據組成的實體(Body)兩大部分,其中文件頭以MThd開始(小端序機器32bit表示爲0x4d546864),包含了數據實體的長度、格式、音軌數量與參考時鐘分辨率4個參數。參考時鐘是同步全部樂器的統一標準,MIDI中的時間以時鐘節拍(Tick)爲單位,而參考時鐘分辨率與拍速共同決定了每一個時鐘節拍絕對時間的長度。

  一個MIDI文件能夠包含多個樂器軌道,每一個樂器軌道的全部事件包圍在成對出現的track開始事件與結束事件中。一個典型的MIDI文件以下圖所示:

  MIDI事件是數據主體的組成元素,每一個MIDI事件都由事件的時間標記(Delta-Time)和操做碼(Opcode)開始,而不一樣的操做碼所對應的參數列表不一樣。本文重點討論音符起音(Note On)和關斷事件(Note Off)。

  MIDI中時間採用增量系統,而事件則是模態(Model)的。所謂增量系統,即當前事件的時間是以相對於上一個事件的時間增量表示的;而模態系統是指若當前事件與前序的事件操做碼相同,則省略當前操做碼。其次,MIDI還使用了可變長整數表示方法,這種方法稱爲「動態字節」,具體說來是根據整數自身的二進制長度,動態調整所佔的字節數。每一個動態字節由1bit標識符和7bit二進制數值組成。標識符的做用爲肯定當前字節中7bit是否足夠表示目標整數,而不發生溢出個。7bit二進制能夠表示0~127的整數,若是數據溢出,則把本字節的標誌位置1,記錄下元素數據高7位,剩下位由下一字節處理,依次循環寫入,如圖所示爲對動態字節的處理,圖1(左)顯示了對原始整數的拆分,圖2(右)顯示了按序寫入每一個動態字節的過程。

  MIDI的量化體系爲以tick爲核心的相對時間體系。而算法中採用的是1/64音符量化。首先規定每一個1/64音符的相對時值爲T0,則參考時鐘的分辨率爲:

  Division = T0 * 16

  將算法內部音序中全部時間值乘係數T0,即可獲得對應於MIDI體系的tick值。

 

3、後記

  本文對樂理模型建模和基於該模型的做曲算法進行了初步討論,有待完善的地方還有不少:其一:本文所涉及的算法並不屬於機器學習,所以缺少自我學習的能力;其二,本文僅考慮對理論或編曲經驗的簡單建模,利用的數學工具較爲單一,建模精度較低。最後,缺少一種有效的評估機制,對整個算法的指標進行量化分析。全部這些都有待完善。

 

  因爲我的能力限制,多有疏漏之處,還望批評指正。

相關文章
相關標籤/搜索