あなたの言葉が音楽になる　ー 感情分析ライブラリと音楽学習生成ライブラリを用いた文章による音楽生成 ー

以前からAIをアートの分野に応用する試みが色々とされていて、音楽の分野とりわけ作曲に活用する事例を多く目にするようになった。
広く公開されているライブラリも多数あり、自分も以前からGoogleが作成した音楽学習生成ライブラリmagentaをいじっていて機能の拡張ができないか検討していたところ、下記の記事で詩から音楽を生成するプログラムが紹介されていたので、これをヒントに似たようなシステムを作成してみようと思いGoogle Colabで実装してみた。

* フロストソング：AIを使用して詩から音楽を生成する
https://ichi.pro/furosutosongu-ai-o-shiyoshite-shi-kara-ongaku-o-seiseisuru-109320446959370

試してみたい方は以下からGoogle Colabを立ち上げてください。
* Emotions Melody
https://github.com/noriakihanya/EmotionsMelody.git

# 1.Magentaを簡単に紹介
GoogleのAI関連プロジェクトであるGoogleAIによって運営されている音楽とアートのための機械学習の研究 プロジェクト 
https://magenta.tensorflow.org

学習データとして主にMIDI （Musical Instruments Digital Interface ）ファイルを利用し、magentaが生成する音楽もMIDIとして出力される。

メロディを生成するのは、主に以下のモデルがあるが、
* melody_rnn
* improv_rnn
* polyphony_rnn
* pianoroll_rnn
* performance_rnn

この中から和音が生成できて豊かな音楽表現ができるpolyphony_rnnを使ってみることにした。

### polyphony_rnnの主な特徴
・和音の学習生成ができる
・最初にデータを与えることで続きのメロディを生成してくれる

# 2.方法
参考の”フロストソング”では歌詞の単語と音符が紐付いたモデルを使用して基本となるメロディを作成しているが、そのようなデータを用意するのは困難であった。

そこで歌詞とその歌詞に使われているコードで代用してみることにした。
具体的には、
・取得した歌詞を感情分析ライブラリTransformersで分析して結果をコードと一緒にまとめて、
・音楽にしたい歌詞を感情分析ライブラリTransformersで分析し、リストの中から数値が一番近い歌詞のコード（進行）を返して、
・音楽生成ライブラリにコード進行から音楽を作らせる方法を取った。

# 3.歌詞とコード進行情報の収集
歌詞とコードはU-FRETさんのサイトから以下の記事を参考にスクレイピングで取得しているが、こちらのプログラムはだいぶ長くなってしまうので割愛。
* 【U-FRET】
https://www.ufret.jp/

* 曲のコードをword2vecでベクトル化し、t-SNEで可視化してみた
https://qiita.com/kinopee0120/items/7bbcda4afa7e501d6272

# 4.入力した歌詞を分析し、コードを出力
Transformersの分析結果はポジティブ・ネガティブを0.5〜1.0で返すため、ネガティブの場合マイナスとなるようにして、リストから最も値が近い歌詞のコードを出力するようにした。
```
def text2chord(key,Target_TXT):
    result = nlp(Target_TXT)
    if result[0]['label'] == "ネガティブ":
        num = result[0]['score'] * -1
    else:
        num = result[0]['score']
    
    # 指定値に最も近い値のインデックスを取得
    distance = np.abs(data['score']-num)
    indices = np.where(distance == np.min(distance))[0]
    text_list = data['chord'][np.random.choice(indices)]

    no = maj_data.index.get_loc(key)
    dic = maj_data['chord_dic'][no]    
    
    transed = []
    transed = [dic.get(moji, moji) for moji in text_list]
    chord_pr = ''.join(transed).lstrip(' ')
    chord_pr = chord_pr.replace("bC","B")
    chord_pr = chord_pr.replace("bD","Db")
    chord_pr = chord_pr.replace("bE","Eb")
    chord_pr = chord_pr.replace("bF","E")
    chord_pr = chord_pr.replace("bG","Gb")
    chord_pr = chord_pr.replace("bA","Ab")
    chord_pr = chord_pr.replace("bB","Bb")
    chord_pr = chord_pr.replace("E#","F")
    chord_pr = chord_pr.replace("B#","C")
    chord_pr = chord_pr.replace("C##","D")
    chord_pr = chord_pr.replace("D##","E")
    chord_pr = chord_pr.replace("F##","G")
    chord_pr = chord_pr.replace("G##","A")
    chord_pr = chord_pr.replace("A##","B")
    chord_pr = chord_pr.replace("Cbb","Bb")
    chord_pr = chord_pr.replace("Dbb","C")
    chord_pr = chord_pr.replace("Ebb","D")
    chord_pr = chord_pr.replace("Fbb","Eb")
    chord_pr = chord_pr.replace("Gbb","F")
    chord_pr = chord_pr.replace("Abb","G")
    chord_pr = chord_pr.replace("Bbb","A")
    chord_pr = chord_pr.replace("Cb#","C")
    chord_pr = chord_pr.replace("Db#","D")
    chord_pr = chord_pr.replace("Eb#","E")
    chord_pr = chord_pr.replace("Fb#","F")
    chord_pr = chord_pr.replace("Gb#","G")
    chord_pr = chord_pr.replace("Ab#","A")
    chord_pr = chord_pr.replace("Bb#","B")
    
    if "la" not in Target_TXT:
        if 0 <= chord_pr.count(" ") <= 2:
            chord_pr = chord_pr + " " + chord_pr
```
任意のキーを指定できるようにしているのだが、処理の都合上”bC”や”Cbb”などが出力されてしまうので、replace関数で変換するようにした（力技となってしまったが、とりあえず出力されることを優先）。

```
#ボブ・ディラン　風に吹かれて
text2chord("D","la3")
text2chord("D","人は何度見上げれば空が見えるのか")
text2chord("D","人にはいくつ耳があれば人々の悲しみが聞こえるのか")
text2chord("D","どれ位の人が死んだら")
text2chord("D","あまりにも多くの人が亡くなったことに気づくのか")
text2chord("D","友よ答えは風に吹かれて")
text2chord("D","風に吹かれている")
text2chord("D","la3")
```
以下のように出力される。
```
la3 : A B E

人は何度見上げれば空が見えるのか : Emaj7 Em7 Emaj7 Em7

人にはいくつ耳があれば人々の悲しみが聞こえるのか : G F#7 Bm Am D

どれ位の人が死んだら : Dmaj7 A/C# Bm Dm

あまりにも多くの人が亡くなったことに気づくのか : G A D G A D

友よ答えは風に吹かれて : G#/C A#m G# G#/C A#m G#

風に吹かれている : Bm F#/Bb Bm F#/Bb

la3 : G F# Bm
```
”la+数字”と入力すると、数字分のコード進行がランダムで出力されるようになっている（ときどき失敗する）。

Transformersの使い方は以下を参考にした。
* Python で日本語文章の感情分析を簡単に試す (google colab で試す)
https://qiita.com/hnishi/items/0d32a778e375a99aff13

# 5.コードから入力MIDIを生成
MagentaにはMIDIファイルで渡す必要があるため、出力されたコードからMIDIを作成する。
MIDIを作成するはpretty_midiというライブラリを使用した。

MIDIの作成には以下のサイトを参考にさせてもらった。
* pretty_midiでコード進行のMIDIファイル生成
https://qiita.com/a2kiti/items/c0fea6e415bf7d743e16
```
def chord2midi_p(lyric_chord):
    root        = {'C':0,
                   'C#':1,
                   'Db':1,
                   'D':2,
                   'D#':3,
                   'Eb':3,
                   'E':4,
                   'E#':5,
                   'F':5,
                   'F#':6,
                   'Gb':6,
                   'G':7,
                   'G#':8,
                   'Ab':8,
                   'A':-3,
                   'A#':-2,
                   'Bb':-2,
                   'B':-1,}

    chord_type = {'':np.array([0, 4, 7]),
                  'm':np.array([0, 3, 7]),
                  '7':np.array([0, 4, 12, 22]),
                  'm7':np.array([0, 3, 12, 22]),
                  'mM7':np.array([0, 3, 12, 23]),
                  'maj7':np.array([0, 4, 12, 23]),
                  'dim':np.array([0, 3, 6, 9, 12]),
                  'aug':np.array([0, 4, 8, 12]),
                  'add9':np.array([0, 4, 12, 26]),
                  'sus4':np.array([0, 5, 7, 12]),
                  '7sus4':np.array([0, 5, 12, 22]),
                  'm6':np.array([0, 3, 12, 21]),
                  '6':np.array([0, 4, 12, 21]),
                  'm7-5':np.array([0, 3, 6, 12, 22]),
                  'm6':np.array([0, 3, 12, 21]),
                  '9':np.array([0, 4, 7, 22, 25]),}
    
    on_chord = {'/Cb':47,
                '/C':48,
                '/C#':49,
                '/Db':49,
                '/D':50,
                '/D#':51,
                '/Eb':51,
                '/E':52,
                '/Fb':52,
                '/E#':53,
                '/F':53,
                '/F#':54,
                '/Gb':54,
                '/G':55,
                '/G#':56,
                '/Ab':56,
                '/A':57,
                '/A#':58,
                '/Bb':58,
                '/B':59,}


    def split_chord(chord):
        j=chord    
        c=j
        o=j
        if '/' in o:
            target = '/'
            idx = o.find(target)
            o = o[idx:]
        else:
            idx = ''
            o=''
        
        if len(c)>1:
            if idx != '':
                c=c[0:2]
                if c[1]=='#' or c[1]=='b':
                    c=c[0:2]
                    j=j[2:idx]
                else:
                    c=c[0:1]
                    j=j[1:idx]
            else:
                c=c[0:2]
                if c[1]=='#' or c[1]=='b':
                    c=c[0:2]
                    j=j[2:]
                else:
                    c=c[0:1]
                    j=j[1:]
        else:
            j=''
        return c, j, o

    pm_p = pretty_midi.PrettyMIDI(resolution=960, initial_tempo=120)
    instrument = pretty_midi.Instrument(0)
    
    imput_chords = open(os.path.expanduser(lyric_chord),'r').read()
    chords = np.array(re.split(" +", imput_chords.rstrip()))
    chords_len = len(chords)
    
    i = 0
    d_time_list = [0.125, 0.125, 0.125, 0.25, 0.25, 0.25, 0.5, 1.0, 1.0, 2.0] #コードを鳴らす間隔
    time = 0
    
    for chord in chords:
        i += 1
        croot, ctype, otype = split_chord(chord)
        #print(croot, ctype, otype)
        
        d_time = float(random.choice(d_time_list)) #コードを鳴らす間隔
        e_time = 2.0 - d_time
        
        if otype != "":
            notes = 60 + root[croot]
        else:
            notes = 48 + root[croot]
        
        if otype != "":
            if ctype in chord_type:
                notes += chord_type[ctype]
                notes = np.insert(notes, [0], on_chord[otype])
            else:
                notes += np.array([0, 12])
                notes = np.insert(notes, [0], on_chord[otype])
        else:
            if ctype in chord_type:
                notes += chord_type[ctype]
            else:
                notes += np.array([0, 12])
        
        if i == chords_len:
            d_time = 2.0
            for note_number in notes:
                note = pretty_midi.Note(velocity=100, pitch=note_number, start=time, end=time+d_time) 
                instrument.notes.append(note)
        else:
            for note_number in notes:
                note = pretty_midi.Note(velocity=80, pitch=note_number, start=time, end=time+d_time) 
                instrument.notes.append(note)
            for e_number in range(int(e_time/d_time+1)):
                note = pretty_midi.Note(velocity=0, pitch=0, start=time, end=time+d_time) 
                instrument.notes.append(note)
                time = time + d_time

    pm_p.instruments.append(instrument)
```
同じコードでも毎回生成される音楽が変化するよう、入力する音の長さがランダムになるようにリスト化したが、下記のように入力することである程度任意の確率で出現させられるようにした。
```
#0.125:十六分音符, 0.25:八分音符, 0.5:四分音符, 1.0:二分音符, 2.0:全音符
d_time_list = [0.125, 0.125, 0.125, 0.25, 0.25, 0.25, 0.5, 1.0, 1.0, 2.0] 
```

# 6.Magentaで音楽生成
```
temperature=[1.0, 1.1, 1.2, 1.3]
i=0

for midi_path, chords_file in zip(midi_paths,file_paths):
    i += 1
    in_chords = open(chords_file,'r').read()
    chords_num = (in_chords.count(' ')+1) * 2 * 16
    temp = float(random.choice(temperature))
    
    !polyphony_rnn_generate \
    -bundle_file=$mag_file \
    -output_dir=$output_dir \
    -primer_midi=$midi_path \
    -num_outputs=1 \
    -num_steps=$chords_num \
    -temperature=$temp \
    -condition_on_primer=false \
    -inject_primer_during_generation=true
```
Jupyterはコード内でshellを簡単に実行できるため非常に便利である。
入力MIDIの長さに合わせて小節数を計算し、ついでにtemperatureもランダムにすることで再度音楽を生成しても同じにならないようにした。
ちなみにnum_stepsは16で１小節となり、コードが３つの場合は48となるが、入力分3つ（48）と出力分3つ（48）を合計（96）して渡す必要がある。

# 7.生成したMIDIファイルの結合
```
new_stream = m21.stream.Stream()

for i, melody_path in enumerate(melody_paths):
    if i == 0:
        new_stream = m21.converter.parse(melody_path)
    else:
        melody = m21.stream.Stream()
        melody = m21.converter.parse(melody_path)
        new_stream[0].append(melody[0])

melody_file = melody_dir + "EmotionsMelody.mid"
new_stream.write("midi",melody_file)
```
文章ごとに作成されたMIDIをmusic21というライブラリを用いて結合する。
pythonでMIDIファイルを扱えるライブラリはいくつかあり機能の紹介がなされているが、MIDIファイル同士を結合する方法がなかなか見つけられなかった。pythonでMIDIファイルの操作を検討されている人に参考になるかもしれない。

# 8.最後に
生成された音楽がこちら
https://soundcloud.com/user-288749361/emotionsmelody?utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing

課題として、

* 今回使用しているモデルはGoogleがクラシック曲を中心に学習させているため、生成される音楽が似通ってしまう

* せっかく感情分析をしているが、必ずしも感情に沿ったメロディが生成されているとは言い難い

より豊かな表現をさせるには、多様なジャンルの学習データの収集と、感情に沿ったメロディが生成できるようデータの分類と学習などの工夫が必要そう。