- 이전 글
[작곡 인공지능 개발기] 2. pyknon 예제 테스트
2. pykon 예제 테스트에서 pyknon을 사용해보았으므로, 이제 마르코프 체인을 활용한 작곡 모델 파일을 공부해보려고 한다.
1. 마르코프 체인의 개념
마르코프 성질: 특정 상태의 확률이 오직 과거의 상태에만 의존할 때, 마르코프 성질을 갖는다.
마르코프 체인: 마르코프 성질을 가진 이산 확률과정
예시:
2. python으로 마르코프 체인이 구현된 부분 살펴보기
https://github.com/kairess/MarkovMusic
GitHub - kairess/MarkovMusic: A markov chain based VERY simplistic procedural music generator.
A markov chain based VERY simplistic procedural music generator. - GitHub - kairess/MarkovMusic: A markov chain based VERY simplistic procedural music generator.
github.com
본 포스팅에서는 jcbozonier 이 작성한 코드를 유튜버 빵형의 개발도상국님이 수정한 MarkovMusic 코드를 사용하였다.
MarkovMusic 코드는 어떤 음악을 입력으로 받아 이를 바탕으로 pitch, duration에 대한 인접행렬을 생성한다.
그 이후, 초기 값으로 음 하나를 주면 생성된 행렬을 활용하여 작곡을 한 뒤 midi파일의 형태로 출력하는 코드이다.
pitch, duration에 대한 인접행렬이 생성되는 과정은 다음과 같다.
1. 입력된 음악의 pitch 의 종류,duration의 종류에 따라 그 종류의 수에 대응하는 정사각행렬을 만든다.
2. 입력된 음악에서 음이 전환될때마다 해당하는 행렬의 항에 가중치 1을 더해준다.
아래의 사진을 예시로 하면, C->F로의 전환이 총 6번 일어난 것이다.
사진의 행렬을 예시로 하면, C음 다음으로 F음이 올 확률은 6/(1+6+3+1), A음 다음으로 A음이 올 확률은 4/(2+4+1)가 된다.
사진의 행렬을 예시로 하면, 2분음표 다음으로 8분음표가 올 확률은 1/(1+4+1), 16분 음표 다음으로 16분 음표가 올 확률은 4/(2+4)가 된다.
행렬생성에 대한 모듈은 MarkovBuilder.py, pitch와 duration행렬의 생성은 MarkovMusic.py, 전체적인 활용은 main.py에서 다루고 있다.
아래 코드의 주석을 보면서 이해해보자.
<MarkovBuilder.py>
import random
class MarkovBuilder:
def __init__(self, value_list):
#value_list: 노트 또는 쉼표의 종류들
self._values_added = 0
self._reverse_value_lookup = value_list
self._value_lookup = {}
for i in range(0, len(value_list)):
self._value_lookup[value_list[i]] = i
#Initialize our adjacency matrix with the initial
#probabilities for note transitions.
#노트 전환의 초기 확률로 인접 행렬을 초기화한다.
#value_list= [c4, e, g]이면
#_reverse_value_lookup = [c4, e, g]
#_value_lookup = { c4 : 0, e : 1, g : 2 }
self._matrix=[[0 for x in range(0,len(value_list))] for i in range(0,len(value_list))]
#value list의 길이를 가지는 정사각행렬로 초기화
def add(self, from_value, to_value):
"""Add a path from a note to another note. Re-adding a path between notes will increase the associated weight."""
value = self._value_lookup
self._matrix[value[from_value]][value[to_value]] += 1
#2차원 배열 _matrix의 from_value행 to_value열의 값에 1추가
self._values_added = self._values_added + 1
#_values_added에 1 추가
def next_value(self, from_value):
value = self._value_lookup[from_value]
#from_value의 인덱스를 value에 저장
value_counts = self._matrix[value]
#_matrix에서 value에 해당하는 가중치 배열을 value_counts에 할당
value_index = self.randomly_choose(value_counts)
#randomly_choose 메소드에 value_counts를 넣어 반환된 index를 value_index에 할당
if(value_index < 0):
raise RuntimeError("Non-existent value selected.")
else:
return self._reverse_value_lookup[value_index]
#다음 음표 또는 쉼표의 값을 리턴
def randomly_choose(self, choice_counts):
"""Given an array of counts, returns the index that was randomly chosen"""
counted_sum = 0
count_sum = sum(choice_counts)
#선택된 행의 가중치합을 구해 count_sum에 할당
if count_sum == 0:
return random.randint(0, len(choice_counts)-1)
else:
selected_count = random.randrange(1, count_sum + 1)
#1에서 가중치합사이의 값하나를 뽑아 selected_count에 할당
for index in range(0, len(choice_counts)):
counted_sum += choice_counts[index]
#0으로 초기화 됬던 counted_sum에 choice_counts[index]를 더한다.
if(counted_sum >= selected_count):
return index
#for문으로 계속 더하다가 counted_sum >= selected_count일때의 index를 return한다.
raise RuntimeError("Impossible value selection made. BAD!")
<MarkovMusic.py>
import pysynth
import numpy as np
from .MarkovBuilder import MarkovBuilder
class MusicMatrix:
def __init__(self, song=None):
self._previous_note = None
if song is not None:
notes = np.array(song, dtype=str)[:, 0]
durations = np.array(song, dtype=str)[:, 1]
for i, d in enumerate(durations):
durations[i] = self.float2str(durations[i])
self._markov = MarkovBuilder(np.unique(notes).tolist())
self._timings = MarkovBuilder(np.unique(durations).tolist())
#노트의 종류의 수를 파악하여 해당하는 수의 크기로 리스트 초기화하여 _markov멤버변수에 할당
#박자의 종류의 수를 파악하여 해당하는 수의 크기로 리스트 초기화하여 _timings멤버변수에 할당
for note in song:
self.add(note)
#초기화한 행렬(_markov,_timings)에 song의 note를 하나씩 추가
else:
self._markov = MarkovBuilder(["a", "a#", "b", "c", "c#", "d", "d#", "e", "f", "f#", "g", "g#"])
self._timings = MarkovBuilder([1, 2, 4, 8, 16])
# print(self._markov._value_lookup)
# print(self._timings._value_lookup)
def float2str(self, d):
if float(d) >= 1:
return '%d' % int(float(d))
else:
return '%.2f' % float(d)
def add(self, to_note):
"""Add a path from a note to another note. Re-adding a path between notes will increase the associated weight."""
#노트에서 다른 노트로 경로를 추가합니다. 노트 사이에 경로를 다시 추가하면 관련 가중치가 증가합니다.
to_note = list(to_note)
to_note[1] = self.float2str(to_note[1])
if(self._previous_note is None):
self._previous_note = to_note
return
from_note = self._previous_note
#이전노트 초기화
self._markov.add(from_note[0], to_note[0])
self._timings.add(from_note[1], to_note[1])
#MarkovBuilder클래스의 메소드인 add로,
# 2차원 배열 _matrix(음표행렬: _markov,박자행렬: _timings)의 from_value행 to_value열의 값에 1추가
self._previous_note = to_note
def next_note(self, from_note):
from_note = list(from_note)
from_note[1] = self.float2str(from_note[1])
return [self._markov.next_value(from_note[0]), float(self._timings.next_value(from_note[1]))]
if __name__ == "__main__":
#MaekovMusic 모듈 직접 실행시 실행되는 코드(테스트용)
# Playing it comes next :)
#test = [['c',4], ['e',4], ['g',4], ['c5',1]]
#pysynth.make_wav(test, fn = "test.wav")
print("MarkovMusic module start")
musicLearner = MusicMatrix()
# Input the melody of Row, Row, Row Your Boat
# The MusicMatrix will automatically use this to
# model our own song after it.
musicLearner.add(["c", 4])
musicLearner.add(["c", 4])
musicLearner.add(["c", 4])
musicLearner.add(["d", 8])
musicLearner.add(["e", 4])
musicLearner.add(["e", 4])
musicLearner.add(["d", 8])
musicLearner.add(["e", 4])
musicLearner.add(["f", 8])
musicLearner.add(["g", 2])
musicLearner.add(["c", 8])
musicLearner.add(["c", 8])
musicLearner.add(["c", 8])
musicLearner.add(["g", 8])
musicLearner.add(["g", 8])
musicLearner.add(["g", 8])
musicLearner.add(["e", 8])
musicLearner.add(["e", 8])
musicLearner.add(["e", 8])
musicLearner.add(["c", 8])
musicLearner.add(["c", 8])
musicLearner.add(["c", 8])
musicLearner.add(["g", 4])
musicLearner.add(["f", 8])
musicLearner.add(["e", 4])
musicLearner.add(["d", 8])
musicLearner.add(["c", 2])
random_score = []
current_note = ["c", 4]
for i in range(0,100):
print(current_note[0] + ", " + str(current_note[1]))
current_note = musicLearner.next_note(current_note)
random_score.append(current_note)
pysynth.make_wav(random_score, fn = "first_score.wav")
<main.py>
import pysynth as ps
from pyknon.genmidi import Midi
from pyknon.music import NoteSeq, Note, Rest
from src.MarkovMusic import MusicMatrix
from pprint import pprint
# In[4]:
def make_midi(midi_path, notes, bpm=120):
note_names = 'c c# d d# e f f# g g# a a# b'.split()
#c~b까지의 음이 담긴 배열을 note_names에 할당
result = NoteSeq()
for n in notes:
duration = 1. / n[1]
if n[0].lower() == 'r':
result.append(Rest(dur=duration))
else:
pitch = n[0][:-1]
octave = int(n[0][-1]) + 1
pitch_number = note_names.index(pitch.lower())
result.append(Note(pitch_number, octave=octave, dur=duration))
#받은 notes의 쉼표와 음표를 구분하여 NoteSeq배열에 추가
midi = Midi(number_tracks=1, tempo=bpm)
midi.seq_notes(result, track=0)
midi.write(midi_path)
#midi_path경로에 미디 파일을 생성
# # Row Row Row Your Boat
# In[26]:
song = [['c4', 4], ['c4', 4], ['c4', 4], ['d4', 8], ['e4', 4], ['e4', 4], ['d4', 8], ['e4', 4], ['f4', 8], ['g4', 2], ['c4', 8], ['c4', 8], ['c4', 8], ['g4', 8], ['g4', 8], ['g4', 8], ['e4', 8], ['e4', 8], ['e4', 8], ['c4', 8], ['c4', 8], ['c4', 8], ['g4', 4], ['f4', 8], ['e4', 4], ['d4', 8], ['c4', 2]]
#리리리자로 끝나는 말은~노래이다.
#ps.make_wav(song, fn='examples/test.wav')
# In[28]:
matrix = MusicMatrix(song)
#song에 대응하는 마르코프 행렬 생성
start_note = ['c4', 4]
#첫째 노트 지정
random_song = []
for i in range(0, 100):
start_note = matrix.next_note(start_note)
random_song.append(start_note)
#MusicMatrix클래스의 객체의 next_node메소드로 다음 노트를 계속해서 지정
# ps.make_wav(random_song, fn='examples/random.wav')
make_midi(midi_path='midi/random_rowboat.mid', notes=random_song)
#미디파일 생성
3. 발견된 문제
MarkovMusic 코드에서 요구하는 의존성을 만족시키기 위해
pyknon 모듈과 pysynth 모듈을 pip install 커맨드를 이용하여 설치하려고 했다.
하지만 pyknon 은 내 파이썬 버전인 3.8.8에서는 대응하는 버전이 PyPi에 배포되어 있지 않다는 로그가 떴다.
다행히도 깃허브 링크의 issues에 같은 문제를 겪은 사람이 있었고, 제작자가 깃허브링크를 통해 직접 다운하라는 답변을 달아놓았었다.
4. 결과물
입력에 사용할 Suspenseful Third Day곡의 음을 배열로 작성하여 make_midi()함수를 통해 midi파일로 만들어 보았다. 8마디만 사용하였는데 47개의 음을 일일이 음정과 옥타브,박자 까지 쓰느라 눈이 아팠다. 분명 더 효율적인 방법이 있지 않을까.
<main.py>에 추가한 코드이다.
song_suspense=[['c4', 8],['e4', 8],['c5', 8],['c5', 8],['e5', 8],['c6', 8],['b5', 8],['b4',8],['g5', 8],['b4',8],['e5', 8],['b4',8],['a3', 8],['c4', 8],['a4', 8],['a4', 8],['c5', 8],['a5', 8],['g5', 8],['d5', 8],['g4', 8],['b4', 8],['c5', 8],['d5', 8],['e5', 8],['c5', 8],['a4', 8],['f4', 8],['a4', 8],['c5', 8],['e5', 4],['a4', 8],['f4', 8],['a4', 8],['d5', 8],['e5',8],['b4',8],['a4',8],['e4',8],['a4',8],['b4',8],['a4',8],['e4',8],['a4',8],['g#4',8],['e4',8],['d4',8]]
make_midi(midi_path='midi/song_suspense.mid', notes=song_suspense)
matrix = MusicMatrix(song_suspense)
pprint(matrix._markov._matrix)
pprint(matrix._timings._matrix)
start_note = ['c4', 8]
random_song = []
for i in range(0, 500):
start_note = matrix.next_note(start_note)
random_song.append(start_note)
# ps.make_wav(random_song, fn='examples/random_mix.wav')
make_midi(midi_path='midi/random_song_suspense.mid', notes=random_song)
https://www.youtube.com/watch?v=lbIqreyYaSU (원곡 유튜브 링크)
파일을 재생하기위해 미디파일을 간지나게 연주해주는 Concert Creator프로그램을 사용하였다.
https://www.concertcreator.ai/
<입력에 사용한 곡 연주영상>
<마르코프체인을 통해 작곡된 곡의 연주영상>
출력된 곡을 들어보니 썩 듣기 좋은 곡은 아니지만 원곡의 느낌이 느껴지기는 했다.
5. 이후에 할 일
좀 더 들어줄 만한 곡을 작곡하고 싶어서 자료를 조사하다 인공신경망을 활용해 화성진행까지 실제 작곡하는 방식과 유사하게 만드는 논문을 찾았다. 아직 화성에 대해서는 잘 몰라서 학교에서 화성진행 관련부분을 학습한 이후에 다시 도전해보려고 한다. 그때까지 신경망 공부나 해야겠다.
사진출처:
https://www.puzzledata.com/blog190423/
https://www.youtube.com/watch?v=qjFFPDLDLEo
- 다음 글