前回、テキストで音楽を表現する方法 を調べた。今回は実際に char-rnn を利用して音楽を生成してみた。

まずは結果から

バッハのインベンションの No.1 - No.15 の全 15 曲を学習データにして、 char-rnn を利用して学習させた。結果がこちら。

無調音楽に近いような感じ。

実装

以下のサイトの記事を真似しました。

参考にした記事では、karpathy さんの char-rnn をそのままつかっていたけれども、 理解を深めるために Tensorflow で char-rnn を実装した。 といっても、Udacity で学んだことを応用しただけ。

その他、

  • テキスト形式としては、**kern 記譜法を採用。
  • 音楽を扱うライブラリは music21 を利用。

今回の Jupyter Notebook はコチラ。いつものようにコードを抜粋しながら説明。

KernScore からデータ取得

KernScores には、大量のクラシック音楽の **kern 記譜法でかかれた楽譜が置いてある。 まずは、バッハのインベンションの楽譜を取得した。

from urllib.request import urlopen
for i in range(1,15+1):
    filename = "inven{0:02d}.krn".format(i)
    file = urlopen("https://kern.humdrum.org/cgi-bin/ksdata?l=osu/classical/bach/inventions&file=%s&f=kern"%filename)
    with open("kernscore/bach/"+filename,'wb') as output:
        output.write(file.read())

データを char-rnn に食わせるために整形

データを見てみると余分なものがあることに気づく。これはいらない。

  • metadata
  • コメント
  • 小節記号
!!!COM: Bach, Johann Sebastian
!!!OPR: Inventio
!!!OTL: Inventio 1
!!!XEN: Two-part Inventions [for keyboard]
!!!ONM: No. 1
!!!ONB: In C major.
!!!SCT: BWV 772
!!!YEC: Copyright 1994, David Huron
!!!YEM: Rights to all derivative electronic formats reserved.
!! Performance duration: 1:29; performance tempo: 59.3 quarter-notes per minute.
!! (Andras Schiff, piano, "Two-Part Inventions BWV 772a-786; Three-part
!! Inventions BWV 787-801" Decca 411 974-2)
**kern  **kern
*staff2 *staff1
*clefF4 *clefG2
*k[]    *k[]
*C: *C:
*M4/4   *M4/4
*MM59.3 *MM59.3
=1- =1-
2r  16r
.   16c
.   16d
.   16e
.   16f
.   16d
.   16e
.   16c
16r 8g
16C .
16D 8cc
16E .
16F 8bM
16D .
16E 8cc
16C .

...

.   16b
=22 =22
1CC; 1C;    1e; 1g; 1cc;
==|!    ==|!
*-  *-
!!!CDT: 1685/3//-1750/7/28
!!!OCY: Deutschland
!!!AGN: polyphony
!!!AST: baroque
!!!AMT: simple quadruple
!!!SCA: Bach-Werke-Verzeichnis
!!!YOR: Bach Gesellschaft
!!!AIN: cemb; clavi
!!!EEV: 1.0
!!!RDT: 1986 November
!!!YER: 1995 May 22
!!!YEN: Canada
!!!EFL: 1/15
!!!EED: David Huron

これらを除去して、インベンション 15 曲を一つのファイルに出力する。

import glob
REP="@\n"
def trim_metadata(output_path, glob_path):
    comp_txt = open(output_path,"w")
    ll = glob.glob(glob_path)
    for song in ll:
        lines = open(song,"r").readlines()
        out = []
        found_first = False
        for l in lines:
            if l.startswith("="):
                ## new measure, replace the measure with the @ sign, not part of humdrum
                out.append(REP)
                found_first = True
                continue
            if not found_first:
                ## keep going until we find the end of the header and metadata
                continue
            if l.startswith("!"):
                ## ignore comments
                continue
            out.append(l)
        comp_txt.writelines(out)
    comp_txt.close()



output_path = "composer/bach.txt"
glob_path = "kernscore/bach/*.krn"
trim_metadata(output_path, glob_path)

char-rnn で音楽生成

ここからのコードは、Udacity で与えられたコードをほぼそのまま使った。 ハイパーパラメータは、以下のとおりに設定した。これで、学習時間が 30 分くらい。 曲の完成度からみると、これではすくないような気もする。

batch_size = 10
num_steps = 10 
lstm_size = 512
num_layers = 2
learning_rate = 0.001
keep_prob = 0.5

出力データのフォーマットを整える

出力データが 結構ぐちゃぐちゃに吐き出される。

  • 右手左手の 2 行だけでよいのに、3 行目がある。
  • 右手パートしかない
  • 左手パートしかない
  • 終端マークが途中で現れる。

これらを解消するために、**kern 記法に沿ったフォーマットで吐き出されるようにした。

ついでに、メタデータと小節番号の bar も追加。

r = []

r.append("**kern\t**kern\n")
r.append("*staff2\t*staff1\n")
r.append("*clefF4\t*clefG2\n")
r.append("*k[]\t*k[]\n")
r.append("*C:\t*C:\n")
r.append("*M4/4\t*M4/4\n")
r.append("*MM80\t*MM80\n")

bar = 1
for line in samp.splitlines():
    sp = line.split('\t')
    if sp[0] == '@':
        r.append("={bar}\t={bar}\n".format(bar=bar))
        bar += 1
    else:
        ln = len(sp)
        if ln == 1 and sp[0] != "":
            r.append(sp[0])
            r.append('\t')
            r.append('.')
            r.append('\n')
        elif ln == 1 and sp[0] == "":
            r.append(".")
            r.append('\t')
            r.append('.')
            r.append('\n')
        elif sp[0] == "*-" or sp[1] == "*-":
            continue
        else:
            r.append(sp[0])
            r.append('\t')
            r.append(sp[1])
            r.append('\n')

r.append("==|!\t==|!\n")
r.append("*-\t*-\n")

open("results/bach2ai.krn","w").writelines(r)

これで、楽譜が完成!

midi 吐き出し

あとは、これを midi に吐き出して再生するだけだ。

from music21 import *
m1 = converter.parse("results/bach2ai.krn")
m1.write('midi', fp='midi/bach2ai.mid')

Jupyter Notebook 上でも再生できる。

m1.show("midi")

おわりに

今回は、あまり満足のいく作品ができなかった。改善点としては、

  • char ベース見ていくのではなくて、音符単位(8a, 7b など)で音を見る
  • 左手、右手を分けて音を見ていく
  • 学習をもっと実施する、ハイパーパラメータの調整

などが考えられる。とはいえ、音楽をディープラーニングで生成できた。 ディープラーニングを学ぶ動機のひとつが音楽生成だったので、達成できて嬉しい。

音楽に関係する取り組みは、char-rnn ベース以外にもあるみたいだ。 今後は、これらを理解していくことにしよう。