前言
人工智能編曲是一個十分複雜的話題,而這一話題的起點便是選擇一個良好的編曲媒介,使得開發者能夠將AI的音樂靈感記錄下來,並且能夠很方便地將其播放、編輯、分享。
MIDI文件是電腦編曲的一種通用格式,它容易通過音樂編輯軟件導入、導出,也有很多現成的庫函數來對其進行編輯加工。
首先,我找到了PythonWiki提供的音樂庫合集 - PythonInMusic,在這裏上百個庫之中,僅有寥寥幾個是支持Python3且仍有活力的,在其中Mido和PyGame.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像這樣:
-
單音軌的音樂聽起來還是比較單薄,這篇文章也是我進行智能編曲的嘗試和敲門磚,爭取之後能夠使用更簡便的方法做出更復雜更動聽的音樂,謝謝關注!
-
完整工程見 Github