Python編曲實踐(一):通過Mido和PyGame來編寫和播放單軌MIDI文件

前言

人工智能編曲是一個十分複雜的話題,而這一話題的起點便是選擇一個良好的編曲媒介,使得開發者能夠將AI的音樂靈感記錄下來,並且能夠很方便地將其播放、編輯、分享。
MIDI文件是電腦編曲的一種通用格式,它容易通過音樂編輯軟件導入、導出,也有很多現成的庫函數來對其進行編輯加工。
首先,我找到了PythonWiki提供的音樂庫合集 - PythonInMusic,在這裏上百個庫之中,僅有寥寥幾個是支持Python3且仍有活力的,在其中MidoPyGame.midi庫是其中比較好用的兩個庫,本篇文章就採用這兩個庫來進行MIDI文件的編寫和播放

Mido編曲

關於用Mido庫來創建一個新的MIDI文件,官方文檔給出瞭如下示例代碼:

from mido import Message, MidiFile, MidiTrack

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)

track.append(Message('program_change', program=12, time=0))
track.append(Message('note_on', note=64, velocity=64, time=32))
track.append(Message('note_off', note=64, velocity=127, time=32))

mid.save('new_song.mid')

這段示例代碼雖然短,可是已經將編寫MIDI文件的基本思路完全表達出來了:

  • 首先創建一個MidiFile對象
  • 創建一個(或多個)MidiTrack對象,並將其append到MidiFile中
  • 向一個(或多個)MidiTrack對象內添加Message對象(包括program_change、note_on、note_off等)和MetaMessage對象(用以表示MIDI文件的節拍、速度、調式等屬性)
  • 保存MidiFile對象

下面我通過對Message和MetaMessage這兩個十分重要的概念的進一步說明,來加深大家理解

Message

Message對象的類型十分複雜,是根據MIDI文件的格式實現的,官方文檔有詳細列表,在此我們不一一列舉,而僅對我使用到的三種Message來進行分析:

1. control_change

program_change是用於更改不同channel的樂器音色的,格式爲:

Message('program_change', channel, program, time=0)
  • channel是指定的0~15的一個值,因爲MIDI文件給我們提供了默認的16個通道,通過這個值可以選擇更改樂器的通道編號;
  • program對於樂器編號,點可以查到不同樂器對應的編號

2.note_on

note_on消息,可以理解爲音符的開始,其格式爲

Message('note_on', note, velocity, time, channel)
  • 其中note是0~127的一個數字,代表音符的高低,通過實踐證明60代表的音高是C4,僅供參考;
  • velocity代表音強,也是0~127的一個數字,默認爲64,若要體現音符強度的變化可以修改它;
  • time是時間變量,是十分複雜的一個參數,在note_on信息這裏可以理解爲該音符寫在前一個音符結束多久之後,單位是微秒(ms);
  • channel同上一個函數一樣,代表通道的編號,即將這個音符寫到哪個通道之上,這可能起到更改樂器的效果

3.note_off

note_off消息,可以理解爲音符的結束,一般緊跟在note_on消息之後,其格式與上面的相同

Message('note_off', note, velocity, time, channel)
  • note參數與note_on消息保持一致,否則有可能不能成功寫入
  • velocity同note_on保持一致就好
  • time在此處表示的意義是音符的持續時間,也是以微秒(ms)爲單位
  • channel也是表示通道號,與note_on保持相同即可

MetaMessage

MetaMessage的種類也很多,可以參考官方文檔,我只使用了3種MetaMessage,列舉在下面:

	tempo = 75
    tempo = mido.bpm2tempo(bpm)
    meta_time = MetaMessage('time_signature', numerator=3, denominator=4)
    meta_tempo = MetaMessage('set_tempo', tempo = tempo, time=0)
    meta_tone = MetaMessage('key_signature', key='C')
  • 其中time_signature是對於節拍的表示,在此處即3/4,參數以分子和分母來命名,十分清晰
  • set_tempo是用於設置音樂的節奏快慢,由於這裏tempo的單位不是BPM(Beat Per Minute),故一般配合bpm2tempo來使用
  • key_signature是用於設置音樂的調式的,在此處我設置爲C大調,若是小調的話僅需要在後面添加小寫字母m,如Cm表示C小調

編程實現

1. play_note函數

由於Message對象需要的參數比較多而且單位轉換複雜繁瑣,故我自己編寫了一個play_note函數來更加方便編曲:

def play_note(note, length, track, base_num=0, delay=0, velocity=1.0, channel=0):
   meta_time = 60 * 60 * 10 / bpm
   major_notes = [0, 2, 2, 1, 2, 2, 2, 1]
   base_note = 60
   track.append(Message('note_on', note=base_note + base_num*12 + sum(major_notes[0:note]), velocity=round(64*velocity), time=round(delay*meta_time), channel=channel))
   track.append(Message('note_off', note=base_note + base_num*12 + sum(major_notes[0:note]), velocity=round(64*velocity), time=round(meta_time*length), channel=channel))
  • 由於我要編的歌曲是大調曲式,而大調的音階結構是“全全半全全全半”(這一規律可以通過鋼琴鍵盤的黑白鍵安排來得到,在此不贅述樂理知識),故我創建一個major_notes數組,用於根據根音計算出某一個音符的音高;
  • meta_time是根據bpm而計算出的每個節拍的時間長度,用於得到Message中的time參數
  • base_note是通過實驗得到的C4的音高,作爲根音來搭配major_notes得到每個音符的音高
  • base_num用於切換目前所在的音域,負值表示低幾度,正值表示高几度
  • velocity是一個0~2的浮點數,以64爲基準來進行比較

2. 編曲

下面開始正式編曲了,我選擇的是《大海啊,故鄉》這首歌,簡譜如下:
大海啊,故鄉
由於我們是純樂器演奏,而前奏與後面重複率極高,故略過前奏。之後我將此音樂以八小節爲單位分爲3個部分,其中後兩部分僅一個半音部分有區別。根據此特徵,我編寫了chorus和verse兩個函數,代碼如下:

def verse(track):
   play_note(1, 0.5, track)       # 小
   play_note(2, 0.5, track)       # 時
   play_note(1, 1.5, track)       # 候
   play_note(7, 0.25, track, -1)  # 媽
   play_note(6, 0.25, track, -1)  # 媽

   play_note(5, 0.5, track, -1, channel=1)  # 對
   play_note(3, 0.5, track, channel=1)      # 我
   play_note(3, 2, track, channel=1)        # 講

   play_note(3, 0.5, track)           # 大
   play_note(4, 0.5, track)
   play_note(3, 1.5, track)           # 海
   play_note(2, 0.25, track)          # 就
   play_note(1, 0.25, track)          # 是

   play_note(6, 0.5, track, -1, channel=1)  # 我
   play_note(2, 0.5, track, channel=1)      # 故
   play_note(2, 2, track, channel=1)        # 鄉

   play_note(7, 0.5, track, -1)  # 海
   play_note(1, 0.5, track)
   play_note(7, 1.5, track, -1)  # 邊
   play_note(6, 0.25, track, -1)
   play_note(5, 0.25, track, -1)

   play_note(5, 0.5, track, -1, channel=1)  # 出
   play_note(2, 0.5, track, channel=1)
   play_note(2, 2, track, channel=1)        # 生

   play_note(4, 1.5, track)       # 海
   play_note(3, 0.5, track)       # 裏
   play_note(1, 0.5, track)       # 成
   play_note(6, 0.5, track, -1)

   play_note(1, 3, track)         # 長


def chorus(track, num):
   play_note(5, 0.5, track)  # 大
   play_note(6, 0.5, track)
   play_note(5, 1.5, track)  # 海
   play_note(3, 0.5, track)  # 啊

   play_note(5, 0.5, track, channel=1)  # 大
   play_note(6, 0.5, track, channel=1)
   play_note(5, 2, track, channel=1)    # 海

   play_note(6, 0.5, track)  # 是(就)
   play_note(5, 0.5, track)  # 我(像)
   play_note(4, 0.5, track)  # 生(媽)
   if num == 1:
       play_note(1, 0.25, track, channel=1) # 活
       play_note(1, 0.25, track, channel=1) # 的
   if num == 2:
       play_note(1, 0.5, track, channel=1)  # (媽)
   play_note(6, 0.5, track, channel=1)      # 地(一)
   play_note(5, 0.5, track, channel=1)

   play_note(5, 3, track, channel=1)        # 方(樣)

   play_note(3, 0.5, track)  # 海(走)
   play_note(4, 0.5, track)  # 風(遍)
   play_note(3, 1.5, track)  # 吹(天)
   play_note(2, 0.25, track) # (涯)
   play_note(1, 0.25, track)

   play_note(6, 0.5, track, -1, channel=1)  # 海(海)
   play_note(2, 0.5, track, channel=1)      # 浪
   play_note(2, 2, track, channel=1)        # 湧(角)

   play_note(4, 0.5, track)              # 隨(總)
   play_note(5, 0.5, track)              # 我(在)
   play_note(4, 0.5, track)              # 漂(我)
   play_note(3, 0.5, track)              # 流(的)
   play_note(1, 0.5, track, channel=1)   # 四(身)
   play_note(6, 0.5, track, -1, channel=1)

   play_note(1, 3, track, channel=1)     # 方(旁)

3. play_midi函數

PyGame的midi模塊提供了一個很好的播放midi的功能,由於代碼非原創,故僅僅貼出這個函數:

def play_midi(file):
   freq = 44100
   bitsize = -16
   channels = 2
   buffer = 1024
   pygame.mixer.init(freq, bitsize, channels, buffer)
   pygame.mixer.music.set_volume(1)
   clock = pygame.time.Clock()
   try:
       pygame.mixer.music.load(file)
   except:
       import traceback
       print(traceback.format_exc())
   pygame.mixer.music.play()
   while pygame.mixer.music.get_busy():
       clock.tick(30)

總結

  • 至此編曲工作已經告一段落,順便向大家推薦一款免費MIDI播放與編輯軟件MidiEditor。雖然沒有Pro Tools和Cubase等專業編曲軟件的全面功能,但是對於MIDI文件編寫的基本需求而言足夠了,我們的作品在MidiEditor像這樣:
    MidiEditor

  • 單音軌的音樂聽起來還是比較單薄,這篇文章也是我進行智能編曲的嘗試和敲門磚,爭取之後能夠使用更簡便的方法做出更復雜更動聽的音樂,謝謝關注!

  • 完整工程見 Github

參考資料

  1. Mido官方文檔
  2. PyGame播放MIDI文件參考
  3. 簡譜來源
  4. MIDI Messages 深入解析
  5. MIDI音色代碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章