Python編曲實踐(八):我,喬魯諾·喬巴那,能用兩百行代碼寫出JOJO黃金之風裏我自己的出場曲!

前言

前些天筆者寫的文章 Python編曲實踐(七):整整一百行Python代碼寫出黑人擡棺梗曲《Astronomia》的旋律 受到了大家的許多支持和好評,本篇文章挑戰更復雜、更有挑戰性,同時也很有梗的一首音樂,那就是《JOJO的奇妙冒險第五部:黃金之風》的主要OST之一:Giorno’s Theme。這首音樂時常在主人公喬魯諾·喬巴那召喚替身使者“黃金體驗”(如下圖)的時候響起,強烈的律動感使人過耳不忘。
在這裏插入圖片描述

B站有很多這首音樂的翻唱作品,其中下面這個比較有特色(爲了更容易實現,本篇文章暫時忽略了中間的複雜部分,而保留了最有標誌性的內容):

那不勒斯街頭用薩克斯吹JoJo黃金之風的BGM-GIORNO'S THEME

與黑人擡棺曲《Astronimia》的相比,這篇音樂的結構更復雜,主要在以下幾點:

  • 使用了兩個音軌來模擬鋼琴演奏時的左手低音區部分右手高音區部分
  • 出現了和絃,即在同一時間點多個音符同時奏響的織體結構
  • 使用了升降號,包括升號♯與降號♭,通過這兩個標記來使得音符在原本的音高上升高或降低一個半音

後兩種變化使得之前使用的MidiExtended類變得無駄無駄無駄了,爲了適應這些變化,筆者對其進行了更新和升級。大家可以在文章頭部下載當前最新版本的 MidiExtended_v2,並務必在運行代碼前完成以下幾項簡單工作:

  • 使用pip指令安裝好 mido 庫和 PyGame 庫,MidiExtended類中的相關功能依賴於這兩個庫;
  • 將midi_extended.zip中的內容解壓,並將整個文件夾拷貝到工程根目錄下;
  • 確保歌曲的保存路徑是存在的

做好準備工作之後,我們就開始歐拉歐拉歐拉吧!如果這一過程中遇到任何問題請及時評論或私信反映給我!

二百行代碼

同上一篇一樣,如果你是個急性子,已經迫不及待地想颳起黃金之風,那麼就可以在確保上述三項準備工作已經就緒的前提下直接在你的電腦上運行這二百行代碼(或者可以在Github中參考這一文件
):

from midi_extended.MidiFileExtended import MidiFileExtended

class GoldenWind(object):
    def __init__(self):
        self.bpm = 127
        self.time_signature = '4/4'
        self.key = 'Am'
        self.file_path = '../data/midi/write/golden_wind.mid'
        self.mid = MidiFileExtended(self.file_path, type=1, mode='w')

    def write(self):
        self.mid.add_new_track('Piano1', self.time_signature, self.bpm, self.key, {'0': 0})
        self.mid.add_new_track('Piano2', self.time_signature, self.bpm, self.key, {'0': 0})

        for i in [1, 2, 1, 3]:
            self.intro_outro(i, False)
        for i in [1, 2, 1, 4]:
            self.intro_outro(i, True)
        for i in [1, 2, 1, 3,
                  1, 2, 1, 3,
                  1, 2, 1, 3,
                  1, 2, 1, 3]:
            self.piano2_pattern(i)
        for i in [1, 2, 3, 4]:
            self.piano1_parapraph(i)
        for i in [1, 2, 1, 3]:
            self.intro_outro(i, False)
        for i in [1, 2, 1, 4]:
            self.intro_outro(i, True)

    def intro_outro(self, pattern, both):
        tracks = [self.mid.get_extended_track('Piano1'), self.mid.get_extended_track('Piano2')]
        for i in range(2):
            track = tracks[i]
            if not both and i == 1:
                track.wait(1)
                continue
            if pattern == 4:
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(6, 1/16, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.wait(7/16)
            else:
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/16, base_num=-1-i)
                track.add_note(6, 1/16, base_num=-1-i)
                track.wait(1/16)
                track.add_note(7, 1/16, base_num=-1-i)
                track.wait(1/16)
                if pattern == 1:
                    track.add_note(2, 1/16, base_num=-i)
                    track.wait(1/16)
                    track.add_note(7, 1/16, base_num=-1-i)
                    track.wait(1/16)
                    track.add_note(4, 1/16, base_num=-1-i, alt=1)
                elif pattern == 2:
                    track.add_note(4, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(3, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(2, 1/16, base_num=-i)
                elif pattern == 3:
                    track.add_note(4, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(3, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(7, 1/16, base_num=-1-i)
                track.add_note(6, 1/8, base_num=-1-i)

    def piano2_pattern(self, pattern):
        track = self.mid.get_extended_track('Piano2')
        if pattern == 1:
            track.add_note([7, 4, 7], 1/4+1/8, alt=[0, 1, 0], base_num=[-1, -1, -2])
            track.add_note([4, 2, 5], 1/8+1/4, alt=[0, 0, 1], base_num=[-1, -1, -2])
            track.wait(1/4)
        elif pattern == 2:
            track.add_note([7, 7], 1/2, base_num=[-1, -2])
            track.add_note([4, 4], 1/2, base_num=[-1, -2], alt=[1, 1])
        elif pattern == 3:
            track.add_note([1, 1], 1/2, base_num=[0, -1], alt=[1, 1])
            track.add_note([4, 4], 1/2, base_num=[-1, -2], alt=[1, 1])
        elif pattern == 4:
            track.add_note([4, 7], 1/4, base_num=[-1, -2], alt=[1, 0])
            track.wait(3/4)

    def piano1_parapraph(self, paragraph):
        track = self.mid.get_extended_track('Piano1')
        if paragraph == 1:
            track.add_note(4, 1/4+1/8, base_num=1, alt=1)
            track.add_note(4, 1/8+1/4, base_num=1)
            track.wait(1/8)
            track.add_note(2, 1/16, base_num=1)
            track.add_note(3, 1/16, base_num=1)

            track.add_note(4, 1/8+1/16, base_num=1)
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note(1, 1/8+1/16, base_num=1, alt=1)
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

            track.add_note(4, 1/4+1/8, base_num=1, alt=1)
            track.add_note(7, 1/8+1/4, base_num=1)
            track.add_note(7, 1/8)
            track.add_note(1, 1/8, base_num=1, alt=1)

            track.add_note(2, 1/8+1/16, base_num=1)
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note(1, 1/8+1/16, base_num=1, alt=1)
            track.add_note(6, 1/16+1/8, base_num=1)
            track.add_note(5, 1/8, base_num=1)

        if paragraph == 2:
            track.add_note([4, 2, 7], 1/4+1/8, alt=[1, 0, 0], base_num=[1, 1, 0])
            track.add_note([4, 2, 7], 1/8+1/4, base_num=[1, 1, 0])
            track.wait(1/8)
            track.add_note(2, 1/16, base_num=1)
            track.add_note(3, 1/16, base_num=1)

            track.add_note([4, 1, 5], 1/8+1/16, alt=[0, 1, 0], base_num=[1, 1, 0])
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note([1, 6], 1/8+1/16, alt=[1, 1], base_num=[1, 0])
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

            track.add_note([4, 7], 1/4+1/8, alt=[1, 0], base_num=[1, 0])
            track.add_note([7, 4], 1/8+1/4, base_num=[1, 1])
            track.add_note(7, 1/8, base_num=1)
            track.add_note(1, 1/8, base_num=2, alt=1)

            track.add_note([2, 7], 1/8+1/16, base_num=[1, 0])
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(5, 1/8)
            track.add_note(4, 1/8+1/16, alt=1)
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

        if paragraph == 3:
            track.add_note([4, 2, 7], 1/4+1/8, alt=[1, 0, 0], base_num=[1, 1, 0])
            track.add_note([4, 2, 7], 1/8+1/4, base_num=[1, 1, 0])
            track.wait(1/8)
            track.add_note(2, 1/16, base_num=1)
            track.add_note(3, 1/16, base_num=1)

            track.add_note([4, 7, 5], 1/8+1/16, base_num=[1, 0, 0])
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note([1, 5], 1/8+1/16, alt=[1, 0], base_num=[1, 0])
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

            track.add_note([4, 2, 7], 1/4+1/8, alt=[1, 0, 0], base_num=[1, 1, 0])
            track.add_note([7, 4, 1], 1/8+1/4, base_num=[1, 1, 1], alt=[0, 0, 1])
            track.add_note(7, 1/8, base_num=1)
            track.add_note(1, 1/8, base_num=2, alt=1)

            track.add_note(2, 1/8+1/16, base_num=1)
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note(1, 1/8+1/16, base_num=1, alt=1)
            track.add_note(6, 1/16+1/8, base_num=1)
            track.add_note(5, 1/8, base_num=1)

        if paragraph == 4:
            track.add_note([4, 2, 7], 1/4+1/8, alt=[1, 0, 0], base_num=[1, 1, 0])
            track.add_note([4, 2, 7], 1/8+1/4, base_num=[1, 1, 0])
            track.wait(1 / 8)
            track.add_note(2, 1/16, base_num=1)
            track.add_note(3, 1/16, base_num=1)

            track.add_note([4, 1, 5], 1/8+1/16, alt=[0, 1, 0], base_num=[1, 1, 0])
            track.add_note(3, 1/16+1/8, base_num=1)
            track.add_note(2, 1/8, base_num=1)
            track.add_note([1, 6], 1/8+1/16, alt=[1, 1], base_num=[1, 0])
            track.add_note(2, 1/16+1/8, base_num=1)
            track.add_note(3, 1/8, base_num=1)

            track.add_note([4, 2], 1/4+1/8, base_num=1, alt=[1, 0])
            track.add_note([7, 4], 1/8+1/4, base_num=1)
            track.add_note(7, 1/8)
            track.add_note(1, 1/8, base_num=1, alt=1)

            track.add_note(2, 1/8+1/16, base_num=1)
            track.add_note(5, 1/16+1/8, base_num=1)
            track.add_note(4, 1/8, base_num=1, alt=1)
            track.add_note(4, 1/8+1/16, base_num=1)
            track.add_note(2, 1/16+1/8, base_num=2)
            track.add_note(6, 1/8, base_num=1, alt=1)


if __name__ == '__main__':
    golden_wind = GoldenWind()
    golden_wind.write()
    golden_wind.mid.save_midi()
    golden_wind.mid.play_it()

大家可能已經發現,這兩百行代碼的複雜程度較上一篇《Astronomia》而言有較大的提升,不過不用擔心,下面的內容我將帶大家梳理好音樂結構與代碼安排的對應關係,並讓大家熟悉一下MidiExtended這個類的基礎使用方法。

實現過程

尋找曲譜資源

看過JOJO的朋友們都知道,其中的音樂不論是插曲還是片頭片尾曲,質量都一點不含糊,而這一首《Giorno’s Theme》則因爲其複雜的結構和大量的即興演繹空間而受到大量音樂UP主的喜愛,因而變得超級火爆。經過大量搜索,最終筆者找到了既能體現原版音樂旋律精髓,又不會因太過冗長而難以實現的一個最佳版本,大家可以去試聽一下,其中包含的兩頁曲譜如下圖(倒數第三小節出現了明顯的問題,筆者在本文中進行了修正,同時對結尾部分使用了前奏部分的結構):
在這裏插入圖片描述
可以發現這一曲譜包含兩個聲部,上面的五線譜有一個高音譜號,下面的五線譜有一個低音譜號,分別對應於鋼琴的高音區與低音區:
在這裏插入圖片描述
通過這一對照表,我們就可以對五線譜中表示的旋律進行辨識,並開始正式編程:

初始化

在加入音符前需要對GoldenWind類進行初始化,分別對音樂的速度(BPM)、節拍、調性、MIDI文件保存地址和使用的MidiFileExtended對象進行初始化(根據五線譜的調號來看,這一段音樂的調性可能是C大調或者是A小調,筆者通過音符的出現範圍來看初步判斷它是A小調,如果有錯誤請及時指正):

    def __init__(self):
        self.bpm = 127
        self.time_signature = '4/4'
        self.key = 'Am'
        self.file_path = './golden_wind.mid' 
        self.mid = MidiFileExtended(self.file_path, type=1, mode='w')

下面是用來調用負責不同部分的函數來實現整篇音樂的write函數,其中聲明瞭兩個Track來負責不同的鋼琴部分。由於整篇音樂第一部分與最後一部分相同(筆者修改過,與原樂譜不同),故可以通過調用intro_outro函數來統一實現;而中間部分的高音區部分使用piano1_paragraph函數來實現,低音區部分使用piano2_pattern函數實現:

    def write(self):
        self.mid.add_new_track('Piano1', self.time_signature, self.bpm, self.key, {'0': 0})
        self.mid.add_new_track('Piano2', self.time_signature, self.bpm, self.key, {'0': 0})

        for i in [1, 2, 1, 3]:
            self.intro_outro(i, False)
        for i in [1, 2, 1, 4]:
            self.intro_outro(i, True)
        for i in [1, 2, 1, 3,
                  1, 2, 1, 3,
                  1, 2, 1, 3,
                  1, 2, 1, 3]:
            self.piano2_pattern(i)
        for i in [1, 2, 3, 4]:
            self.piano1_parapraph(i)
        for i in [1, 2, 1, 3]:
            self.intro_outro(i, False)
        for i in [1, 2, 1, 4]:
            self.intro_outro(i, True)

前奏/尾奏:intro_outro函數

前奏/尾奏包括八小節的長度,對應樂譜前八小節的內容,通過intro_outro函數來實現。
改進後的add_note函數中第一個參數表示一個八度內的音高,第二個參數表示以全音符爲單位長度下音符的時值,base_num表示以中央C的音區爲基準的降低/升高八度數,alt用於表示升/降音高

    def intro_outro(self, pattern, both):
        tracks = [self.mid.get_extended_track('Piano1'), self.mid.get_extended_track('Piano2')]
        for i in range(2):
            track = tracks[i]
            if not both and i == 1:
                track.wait(1)
                continue
            if pattern == 4:
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(6, 1/16, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.wait(7/16)
            else:
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/8, base_num=-1-i)
                track.add_note(7, 1/16, base_num=-1-i)
                track.add_note(6, 1/16, base_num=-1-i)
                track.wait(1/16)
                track.add_note(7, 1/16, base_num=-1-i)
                track.wait(1/16)
                if pattern == 1:
                    track.add_note(2, 1/16, base_num=-i)
                    track.wait(1/16)
                    track.add_note(7, 1/16, base_num=-1-i)
                    track.wait(1/16)
                    track.add_note(4, 1/16, base_num=-1-i, alt=1)
                elif pattern == 2:
                    track.add_note(4, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(3, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(2, 1/16, base_num=-i)
                elif pattern == 3:
                    track.add_note(4, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(3, 1/16, base_num=-i)
                    track.wait(1 / 16)
                    track.add_note(7, 1/16, base_num=-1-i)
                track.add_note(6, 1/8, base_num=-1-i)

這一函數的關鍵設置如下:

  • 整個八小節是四種不同小節樣式的組合。故使用參數中的pattern用於區分不同的小節樣式,並根據pattern參數的值來進行不同小節樣式的音符輸入,以避免冗餘代碼;
  • 高音區與低音區的音高差了整整一個八度。這樣就可以採用數組的形式來存儲兩個音軌,並通過循環變量i來輔助對低音區音軌降低一個八度;
  • 爲了使得音樂結構複雜度的漸進性體現的更好,我對這八小節的內容進行了改編,使得前四小節僅有高音部分而刪去了低音部分,函數內通過both參數來判斷是否使用兩個聲部

中間部分的低音區樣式:piano2_pattern函數

通過觀察可以發現,在中間十六小節的音樂部分中,低音區僅僅有四種不同的樣式,故可以通過以下函數來實現,其中pattern參數用於區分不同的樣式:

    def piano2_pattern(self, pattern):
        track = self.mid.get_extended_track('Piano2')
        if pattern == 1:
            track.add_note([7, 4, 7], 1/4+1/8, alt=[0, 1, 0], base_num=[-1, -1, -2])
            track.add_note([4, 2, 5], 1/8+1/4, alt=[0, 0, 1], base_num=[-1, -1, -2])
            track.wait(1/4)
        elif pattern == 2:
            track.add_note([7, 7], 1/2, base_num=[-1, -2])
            track.add_note([4, 4], 1/2, base_num=[-1, -2], alt=[1, 1])
        elif pattern == 3:
            track.add_note([1, 1], 1/2, base_num=[0, -1], alt=[1, 1])
            track.add_note([4, 4], 1/2, base_num=[-1, -2], alt=[1, 1])
        elif pattern == 4:
            track.add_note([4, 7], 1/4, base_num=[-1, -2], alt=[1, 0])
            track.wait(3/4)

這一段代碼包含了四個和絃,這一功能可以通過將add_note函數的第一個參數設置爲數組來實現,若每個組成音的長度、所在八度區域、升降號不同的話,也可以將後面的參數設置成數組形式,並保證數組中參數的順序相互對應。

中間部分的高音區段落:piano1_paragraph函數

中間十六小節的高音部分在整篇音樂中是最爲複雜的,也是最難以實現的一部分,博主是一個一個音符來。聽過之後可以發現,這一段音樂是四小節爲單位進行劃分的,即每四小節音樂的大體行進結構是相同的,故我們使用paragraph這個參數來區分不同的段落(由於最後四小節的內容明顯出現了不協調,我對其進行了微調,故與原曲譜有一定的出入)。
函數整體由於體量太大,在此不單獨給出,如果想參考的話請移步二百行代碼板塊,或者參考Github上的文件

主函數

同上一篇文章一樣,主函數用於調用write函數來編寫音樂,並對音樂進行保存和播放。若運行成功則會立即播放音樂,並在之前選擇的保存目錄中找到該MIDI文件。

if __name__ == '__main__':
    golden_wind = GoldenWind()
    golden_wind.write()
    golden_wind.mid.save_midi()
    golden_wind.mid.play_it()

生成的 golden_wind.mid文件MidiEditor 中打開界面如下:
在這裏插入圖片描述

結語

如果您對本文使用的 MidiFileExtended 類感興趣,或者想進一步瞭解該類的話,請在下方評論或私信聯繫我,也歡迎大家對的文章提出質疑和改進方法。如您在實現本文章中提到的任何內容時遇到任何困難,請及時在下方評論或私信聯繫我!
最後,歡迎大家查看本專題下其他博文內容,十分感謝您的耐心閱讀!

Python編曲實踐(一):通過Mido和PyGame來編寫和播放單軌MIDI文件
Python編曲實踐(二):和絃的實現和進行
Python編曲實踐(三):如何模擬“彎音輪”實現滑音和顫音效果
Python編曲實踐(四):向MIDI文件中添加鼓組音軌
Python編曲實踐(五):通過編寫爬蟲來爬取海量MIDI文件,預備構建數據集(附有百度雲下載鏈接)
Python編曲實踐(六):將MIDI文件轉化成矩陣,繼承PyTorch的Dataset類來構建數據集(附數據集網盤下載鏈接)
Python編曲實踐(七):整整一百行Python代碼寫出黑人擡棺梗曲《Astronomia》的旋律

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章