はじめに
学部3年の時に作った自動作曲システムについて、記録用も兼ねて書いていきたいと思います。
ざっくりとした内容としては、「1.コード進行を入力 -> 2.それをもとにメロディーを自動で生成する」というものです。
今回はmusic21というライブラリを用いて制作しました。なお、music21のインストール方法は以下のリンク先を辿っていってください。
http://web.mit.edu/music21/doc/usersGuide/usersGuide_01_installing.html
また、ソースコードは以下のリンク先にあります。
https://github.com/Kota-0226/jishu_project
今回は、自分の大好きなアーティストである「Official髭男dism」の曲のフレーズを、コード進行別に集めてそれをデータセットとし、自分が入力したコード進行にあった髭男っぽいメロディーを、マルコフ連鎖を用いて作るというシステムを作りました。
環境
macOS(バージョン12.5)
music21
python 3.9.12
musescore 3.6.2
musescoreは自動でmxl形式で生成されたメロディーを表示するのに用います。
詳細
1.製作過程
「コード進行を入力するだけでメロディーを自動で作曲してくれるマシーン作りたいなあ」と思ったのがきっかけで取り組み始めました。
そこで、以下のマルコフ連鎖を用いて自動作曲している人がいたので、「これだ!」と思いとりあえず真似してみました。
https://inglow.jp/techblog/python-scoremake/
ただ、これだとメロディーを生成するだけで、コード進行との紐付けができない。どうしたものかということで、色々工夫をして作ってみました。具体的には以下のような流れで作っています。
1.データセットの収集
マルコフ連鎖を用いる際に使うメロディー群のデータ収集です。髭男の曲のフレーズのコード進行が、Chord1からChord9の中のどれに最も似ているかを判断し、その該当ディレクトリ(musicxml_simple1 ~ 9)にmxl形式でフレーズの耳コピ楽譜を保存します。また、コード進行の類似を判断する際には、classify_chordprogression.pyというファイルを実行していました。このファイルの詳細は後ほど述べます。
この文だと分かりづらいと思うので、例を挙げます。
まず、ある曲のフレーズのコード進行をclassify_chordprogression.pyに入れてみます。その結果、それがChord 1に最も類似しているとなったら、そのフレーズの耳コピ楽譜をmusicxml_simple1に保存する、という感じです。こうして、似たコード進行を持つフレーズ同士をまとめて保存して、データセットを生成しました。
2.コード進行を入力したら実際にメロディーを返すpyファイルの作成
そのままです。先ほど作ったデータセットをもとにメロディーを自動で生成し、mxl形式で返してくれるので、それをmusescoreを用いて表示します。実際には8つのコードからなるコード進行を文字で入力し、4小節分のフレーズが返ってきます。
(Ex: "C G Am Em F C F G"と入力すると、そのコード進行をもとに4小節のフレーズを返す)
2. システムの具体的な原理
コード進行を入力してから、自動で生成したメロディーを生成するまでの基本的な流れは以下のようになっています。
上の写真を言葉で説明しようと思います。
- auto_generating_melody.pyを実行して、立ち上がった入力画面に、メロディーを自動生成してほしいコード進行を入力する(8つのコードを入力する)
- そのコード進行が、あらかじめ用意してある典型的なコード進行例Chord1 ~ Chord 9の中でどれに類似しているかを判断する
- 最も類似していると判断されたコード進行と似たコード進行を持つフレーズが集まったデータセット(musicxml_simple1 ~ musicxml_simple9)から、自動でメロディーを作成
- 3でできたメロディーに、入力されたコード進行を伴奏として楽譜に追加し、出力する(4小節分)
それぞれのファイルが実際に何をしているのかを、コードと共に解説していこうと思います。
auto_generating_melody.py
これが実行するファイル。まず、コード進行を入力してもらうためのUIを立ち上げる。その後、calc_innerproduct.pyを呼び、そのコード進行がChord 1 ~ Chord 9 のどれと最も類似しているかを判断する。そして、その結果をもとにmake_melody.pyをよび、そこで作成されたメロディー と、入力されたコード進行を伴奏として楽譜に追加して出力する。
import os
import glob
import sys
import subprocess
import PySimpleGUI as sg
import make_melody as mm
import make_chords as mc
import return_chordmatrix as rc
import calc_innerproduct as ci
sg.theme('DarkTeal2') #コード進行の入力を受け付けるUIの立ち上げ
layout = [ [sg.Text('これは、入力されたコード進行から自動でOfficial髭男dismみたいなメロディーを生成してくれるものです。')],
[sg.Text('コードは半角スペースで区切って8つ入力してください。一つのコードにつき2拍、トータルで4/4拍子で4小節のメロディーが生成されます。')],
[sg.Text('以下のコードが使えます:maj min dim aug M7 m7 7 dim7 hdim7 mM7 M6 m6 9 M9 m9 sus2 sus4')],
[sg.Text('ただし、以下の注意点に気を付けてください。')],
[sg.Text('1.フラットを入力したいときは"-"と入力してください。 ex)E♭ -> E-')],
[sg.Text('2.C majorを入力したい時は単に"C", C minorを入力したい時は"Cm"のように入力してください。')],
[sg.Text('入力例:C C-sus2 E#m Em Am Am Dm Dm')],
[sg.Text('それではコード進行を入力してください。')],
[sg.Input(key='chord_progression'),sg.Button('実行', key='start')],
[sg.Output(size=(100,30))]
]
window = sg.Window('コード進行から自動でメロディーを生成するツール', layout)
while True:
event, values = window.read()
if event == sg.WIN_CLOSED: #ウィンドウのXボタンを押したときの処理
break
if event == 'start':
chord_progression = list(values['chord_progression'].split())
print("chord_progression={}".format(chord_progression))
l = len(chord_progression)
chord_array = rc.chordProgression_matrix(chord_progression)
result = ci.calc_innerproduct(chord_array) + 1 #何番目のコード進行に類似しているか print("This entered chord progression is most similar to Chord{}".format(result))
DS = os.sep #これは"/"を意味する bs = os.path.dirname(__file__) + DS #このファイルのありか xmlpath = bs + 'musicxml_simple' + str(result) + DS
stream1 = mm.make_melody(xmlpath)
stream2 = m21.stream.Part()
for i in range(l): #ここから下は楽譜に伴奏を追加していくための部分
key = mc.chord_transpose(chord_progression[i])
if key > 5:
key = -1 * (12 - key)
n = mc.pickup_chordtype(chord_progression[i]) #コードの種類が数字で返ってくる、詳しくはmake_chords.py参照
chord = mc.return_chord_type(n)
stream2.append(chord.transpose(key)) #二拍分の伴奏追加
stream2.append(chord.transpose(key))
score = m21.stream.Score()
score.insert(0, stream1)
score.insert(0, stream2)
score.show('musicxml')
window.close()
make_melody.py
いくつかの曲のフレーズから新しいフレーズを作るファイル。具体的には、あるディレクトリの中の musicxmlたちをテキストデータに変換して、マルコフ連鎖に投げて新しいテキストを生成し、それをまたmusicxmlに変換し直して出力する。
import music21 as m21
import markovify as mrkv
import os
import glob
import make_chords as mc
#参考:https://inglow.jp/techblog/python-scoremake/
def make_melody(a): #aにはとってくるサンプルデータが入ってくるディレクトリを表す!
#読み込ませるテキスト準備用
note_txts = []
#フォルダ内のxmlファイルを取得
xmls = glob.glob(a + "*.mxl")
for x in xmls:
piece = m21.converter.parse(x)
ntxt = []
for n in piece.flat.notesAndRests:
#n.name:音名の取得 n.duration.quaterLength:音の長さの取得(1拍,0.5拍など...)
#ここは楽譜が単音だけ出ないとエラーを吐く
ntxt.append(str(n.name) + '_' + str(n.duration.quarterLength))
#1曲が終わったら追加する
note_txts.append(' '.join(ntxt))
#最後に、改行区切りでテキストデータを準備
txts = '\n'.join(note_txts)
times = 0
while(1):
times += 1 #何回whileを回しているか
count = 0 #音価の長さを測るためのもの
text_model = mrkv.NewlineText(txts,well_formed=False) #必要があれば、state_sizeを1-3で設定する。デフォは2.あと、well_formed=Falseとしないと読み込めないやつは排除されない
sentence = text_model.make_sentence(tries=100)
#メロディをmusicXMLに変換する
meas1 = m21.stream.Part() #楽譜オブジェクトの生成
meas1.append(m21.meter.TimeSignature('4/4')) #拍子を4/4で固定
print("sentence={}".format(sentence))
melo = sentence.split() #半角スペース区切りで配列にする
for m in melo: #[E_0.5,E_0.5,D_0.5,C#_0.5,...]のデータを順に処理
ptch,dist = m.split('_') #アンダーバーで区切る
if(ptch == 'rest'): #rest=休符,この場合は休符の長さだけ追加
n = m21.note.Rest(quarterLength = float(dist))
else: #音と音符の長さを追加
n = m21.note.Note(ptch,quarterLength = float(dist))
count += float(dist)
#楽譜に追加
meas1.append(n)
print("times={}".format(times))
if count == 16:
break
return meas1
make_chords.py
コードを、そのコードの構成音のところだけを1にした12次元のベクトルで返す。12次元とは、下のドから1オクターブ上のドまでの12個の音(0番目がド、1番目がド#、2番目がレ...)に対応している。例えば、Cというコードは、構成音がド、ミ、ソなので、100010010000というようになり、C7と いうコードは構成音がド、ミ、ソ、シ♭なので、100010010010というようになる。
この章で一番最初に紹介したシステム構成図の中にはこのファイルは載っていませんが、次に紹介するreturn_chordmatrix.pyで入力された8つのコード進行(例えばCなど)をnumpy配列にして返すために、コードをnumpy配列で表現したものを定義しているというファイルです。auto_generating_melody.pyの中でも、伴奏の楽譜を追加するための処理を行なっている部分で呼ばれます。
import music21 as m21
import numpy as np
"""
a0 = major
a1 = minor
a2 = dim
a3 = aug
a4 = maj7
a5 = min7
a6 = seventh
a7 = dim7
a8 = hdim7
a9 = minmaj7
a10 = maj6
a11 = min6
a12 = ninth
a13 = maj9
a14 = min9
a15 = sus2
a16 = sus4
"""
major = m21.chord.Chord(['c3','e3','g3'])
minor = m21.chord.Chord(['c3','e-3','g3'])
dim = m21.chord.Chord(['c3','e-3','g-3'])
aug = m21.chord.Chord(['c3','e3','g#3'])
maj7 = m21.chord.Chord(['c3','e3','g3','b3'])
min7 = m21.chord.Chord(['c3','e-3','g3','b-3'])
seventh = m21.chord.Chord(['c3','e3','g3','b-3'])
dim7 = m21.chord.Chord(['c3','e-3','g-3','a3'])
hdim7 = m21.chord.Chord(['c3','e-3','g-3','a#3'])
minmaj7 = m21.chord.Chord(['c3','e-3','g3','b3'])
maj6 = m21.chord.Chord(['c3','e3','g3','a3'])
min6 = m21.chord.Chord(['c3','e-3','g3','a3'])
ninth = m21.chord.Chord(['c3','d3','e3','g3','b-3'])
maj9 = m21.chord.Chord(['c3','d4','e-3','g3','b3'])
min9 = m21.chord.Chord(['c3','d4','e-3','g3','b-3'])
sus2 = m21.chord.Chord(['c3','d3','g3'])
sus4 = m21.chord.Chord(['c3','f3','g3'])
a = np.zeros((17,12),dtype = int)
a[0] = np.array([1,0,0,0,1,0,0,1,0,0,0,0])
a[1] = np.array([1,0,0,1,0,0,0,1,0,0,0,0])
a[2] = np.array([1,0,0,1,0,0,1,0,0,0,0,0])
a[3] = np.array([1,0,0,0,1,0,0,0,1,0,0,0])
a[4] = np.array([1,0,0,0,1,0,0,1,0,0,0,1])
a[5] = np.array([1,0,0,1,0,0,0,1,0,0,1,0])
a[6] = np.array([1,0,0,0,1,0,0,1,0,0,1,0])
a[7] = np.array([1,0,0,1,0,0,1,0,0,1,0,0])
a[8] = np.array([1,0,0,1,0,0,1,0,0,0,1,0])
a[9] = np.array([1,0,0,1,0,0,0,1,0,0,0,1])
a[10] = np.array([1,0,0,0,1,0,0,1,0,1,0,0])
a[11] = np.array([1,0,0,1,0,0,0,1,0,1,0,0])
a[12]= np.array([1,0,1,0,1,0,0,1,0,0,1,0])
a[13] = np.array([1,0,1,0,1,0,0,1,0,0,0,1])
a[14] = np.array([1,0,1,1,0,0,0,1,0,0,1,0])
a[15] = np.array([1,0,1,0,0,0,0,1,0,0,0,0])
a[16] = np.array([1,0,0,0,0,1,0,1,0,0,0,0])
chord_key = ["C","C#","C-","D","D#","D-","E","E#","E-","F","F#","F-","G","G#","G-","A","A#","A-","B","B#","B-",]
def chord_transpose(strings):
#stringsはコードを想定。e.g. C#m,E-sus4
#コードの根音を取り出すための関数 e.g. C#mだったらC#,EmだったらEみたいな
if ("C#" in strings) or ("D-" in strings) :
return 1
elif ("D#" in strings) or ("E-" in strings):
return 3
elif ("F# "in strings) or ("G-" in strings) :
return 6
elif ("G#" in strings) or ( "A-" in strings):
return 8
elif ("A#" in strings) or ("B-" in strings):
return 10
elif "C" in strings:
return 0
elif "D" in strings:
return 2
elif "E" in strings:
return 4
elif "F" in strings:
return 5
elif "G" in strings:
return 7
elif "A" in strings :
return 9
else:
return 11
def pickup_chordtype(strings):
#コードの種類を引っ張ってくる関数。e.g.Amなら"m",EM7なら"M7"にあたるところを抜き出してくる
if len(strings) == 1:
return 0
elif (strings[1] == "#") or (strings[1] == "-"):
if len(strings) == 2:
return 0
else:
return chordtype(strings[2:])
else:
return chordtype(strings[1:])
def chordtype(s):
#コードの種類に応じて数字を返す
if s == "m":
return 1
elif s == "dim":
return 2
elif s == "aug":
return 3
elif s == "M7":
return 4
elif s == "m7":
return 5
elif s == "7":
return 6
elif s == "dim7":
return 7
elif s == "hdim7":
return 8
elif s == "mM7":
return 9
elif s == "6":
return 10
elif s == "m6":
return 11
elif s == "9":
return 12
elif s == "M9":
return 13
elif s == "m9":
return 14
elif s == "sus2":
return 15
elif s == "sus4":
return 16
else: #とりあえずどこにも当てはまらないやつが入力されたら、基本のmajorスケ\\
ールのコードとして返す。
print("This chord ({}) isn't expected to be entered,so we regard {} as \
major chord.".format(s,s))
return 0
def return_chord_type(n):
#数字に応じてコードの種類を返す
if n==1:
return minor
elif n==2:
return dim
elif n==3:
return aug
elif n==4:
return maj7
elif n==5:
return min7
elif n==6:
return seventh
elif n==7:
return dim7
elif n==8:
return hdim7
elif n==9:
return minmaj7
elif n==10:
return maj6
elif n==11:
return min6
elif n==12:
return ninth
elif n==13:
return maj9
elif n==14:
return min9
elif n==15:
return sus2
elif n==16:
return sus4
else:
return major
return_chordmatrix.py
make_chords.pyを呼び出すことによって、入力された8つのコードを、8(コードの数)×12(コード の構成音だけ1、他は0)のnumpy配列で返す。
import music21 as m21
import numpy as np
import make_chords as mc
def chordmatrix(string):
transpose = mc.chord_transpose(string) #コード進行のベース音が入る
chord_type = mc.pickup_chordtype(string) #コード進行の種類が入る
return np.roll(mc.a[chord_type],transpose) #1*12のnp配列が入る
def chordProgression_matrix(chord_progression): #chord_progressionにはコード進行の文字列が[1,8]のリストとして入ってくることを想定している
l = len(chord_progression)
tmp = np.zeros((l,12))
for i in range(l):
tmp[i] = chordmatrix(chord_progression[i])
return tmp
basic_chordmatri.py
Chord 1 ~ Chord 9 までを、make_chords.pyを呼び出すことによって、8(コードの数)×12(コードの構成音だけ1、他は0)のnumpy配列で返す。
return_chordmatrix.pyが入力された8つのコードを8x12の配列で返すのに対し、こっちはあらかじめ用意してあるChord 1 ~ Chord 9を8x12の配列で返すためのファイル。
import numpy as np
import music21 as m21
import return_chordmatrix as rc
"""
ここでは基本的なコード進行9つのmatrixを作る
Chord1 = C C F F G G G G
Chord2 = F F G7 G7 Em Em Am Am
Chord3 = Am Am F F G G C C
Chord4 = Am Am Dm Dm G G Am Am
Chord5 = C C Am Am F F G7 G7
Chord6 = F F G G Am Am Am Am
Chord7 = C C Am Am Dm Dm G7 G7
Chord8 = Am Am Em Em F F G7 G7
Chord9 = C G Am Em F C F G
"""
C = rc.chordmatrix("C")
F = rc.chordmatrix("F")
G = rc.chordmatrix("G")
G7 = rc.chordmatrix("G7")
Em = rc.chordmatrix("Em")
Am = rc.chordmatrix("Am")
Dm = rc.chordmatrix("Dm")
Chord_1 = rc.chordProgression_matrix(["C","C","F","F","G","G","G","G"])
Chord_2 = rc.chordProgression_matrix(["F","F","G7","G7","Em","Em","Am","Am"])
Chord_3 = rc.chordProgression_matrix(["Am","Am","F","F","G","G","C","C"])
Chord_4 = rc.chordProgression_matrix(["Am","Am","Dm","Dm","G","G","Am","Am"])
Chord_5 = rc.chordProgression_matrix(["C","C","Am","Am","F","F","G7","G7"])
Chord_6 = rc.chordProgression_matrix(["F","F","G","G","Am","Am","Am","Am"])
Chord_7 = rc.chordProgression_matrix(["C","C","Am","Am","Dm","Dm","G7","G7"])
Chord_8 = rc.chordProgression_matrix(["Am","Am","Em","Em","F","F","G7","G7"])
Chord_9 = rc.chordProgression_matrix(["C","G","Am","Em","F","C","F","G"])
Chord = np.array([Chord_1,Chord_2,Chord_3,Chord_4,Chord_5,Chord_6,Chord_7,Chord\
_8,Chord_9])
calc_innerproduct.py
入力されたコード進行と、Chord 1 ~ Chord 9 の類似度を比較して、最もscoreのいいもの(類似 度が高いもの)を出力するためのもの。 具体的には、return_chordmatrix.pyで返された、入力されたコード進行を8 ×12のnumpy配列に したものの各行成分と、basic_chordmatrix.pyで返された、Chord 1 ~ Chord 9までに対応する 8 ×12のnumpy配列の各行成分との内積の和を計算する。そしてChord 1 ~ Chord 9のなかで最も スコアの良かったもの、すなわち最も構成音が被っていたコード進行を出力する。
import numpy as np
import basic_chordmatrix as bc
def calc_innerproduct(chord_matrix):
score = 0
index = 0
for i in range(9):
sum = 0
#print("calc_innerproduct")
for j in range(8):
tmp = np.dot(bc.Chord[i][j],chord_matrix[j])
#bc.Chord[i]は9(基本コード進行の数) * 8(コードの数) * 12(各コードの音に対応する,1,0が入っている)からなる。
#chord_matrixは(8,12)になる
sum += tmp
#print("tmp = {}".format(tmp))
if sum > score:
score = sum
index = i
#print("inner_product with Chord{} = {}".format(i+1,sum))
print("score={}".format(score))
return index
classify_chordprogression.py
入力された8つのコードから成るコード進行をreturn_chordmatrix.pyで8×12のnumpy配列にし、 それをcalc_innerproduct.pyに渡して、Chord 1 ~ Chord 9のなかで、入力されたコード進行に最も類似しているものを返す。 この自動作曲システム自体では使われないものの、先ほどの章でも述べたとおり、データセットを作成する際に、そのフレーズのコード進行がChord 1 ~ Chord 9のどれに最も類似しているかを判断するために用いました。
import music21 as m21
import numpy as np
import return_chordmatrix as rc
import calc_innerproduct as ci
print("Just enter 8 chords and it will automatically generate a melody in 4/4 t\
ime for 4 measures.")
print("Enter eight chord progressions that form the base of the melody, separat\
ed by one-byte spaces. Each chord is equivalent to two beats.")
print("In addition, the following chords can be used.")
print("maj,min,dim,aug,M7,m7,7,dim7,hdim7,mM7,M6,m6,9,M9,m9,sus2,sus4")
print("when you enter the chord progression,please following the following rule\
s.")
print("1.Enter '-' for flat (♭)")
print("2.If you want to enter 'C major',please enter 'C'")
print("3.In other case,please enter 'RootNote + Chordname' e.g. 'C major 7' -> 'CM7'")
print("Example: C C-sus2 E#m Em Am Am Dm Dm")
print(" ")
print("Please input chords progression.")
chord_progression = input().split()
print(chord_progression)
chord_array = rc.chordProgression_matrix(chord_progression)
result = ci.calc_innerproduct(chord_array) + 1 #何番目のコード進行に類似しているか
print("This entered chord progression is most similar to Chord{}".format(result))
できたもの
とりあえず最終成果物を載せようと思います。
1.まずauto_generating_melody.pyを実行する
こちらはこの成果物を動画の形に収めたものです。編集とかもしておらず、本当に実行結果を録画しただけですが...
https://youtu.be/EQ7OQAJIy20
最後に
もう少しクラスとかを使ってスッキリかけたんじゃないか...とも思っています。それは今後の課題ですね。
それと、ほぼほぼ初めてのQiita投稿なので、読みづらさもあるかと思いますが、何かあれば遠慮なくご指摘いただければと思います。
最後まで読んでいただき、ありがとうございました!!