-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmarkov_music.py
225 lines (190 loc) · 8.9 KB
/
markov_music.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import argparse
import random
from collections import Counter
from itertools import islice
from textwrap import wrap
from typing import List, Generator, Callable
import music21
from music21.duration import Duration
from music21.note import Note
from music21.pitch import Pitch
from markov import generate_random_stream
NoteGen = Generator[Note, None, None]
# the classes in music21 haven't implemented a hash function but
# generate_random_stream relies on one so I have added one to the classes that need it
Pitch.__hash__ = Note.__hash__ = Duration.__hash__ = lambda self: hash(
self.fullName)
def read_note_sequence(filename: str) -> List[Note]:
""" Reads the note sequence from a single line music xml file.
:param filename: the file to open
:return: the list of notes in the piece
"""
with open(filename) as f:
music_raw = f.read()
score = music21.converter.parseData(music_raw)
notes = []
for p in score.parts[0]:
try:
for i in p:
if isinstance(i, Note):
notes.append(i)
except TypeError: # ignore non-iterable items
pass
return notes
def convert_note_to_lilypond(note: Note) -> str:
""" Converts a note into a lilypond representation of said note
:param note: the note to convert
:return: the lilypond version of the note
"""
length_convert = {
'whole': 1,
'half': 2,
'quarter': 4,
'eighth': 8,
'16th': 16,
'32nd': 32,
}
pitch = note.pitch
duration = note.duration
octave = int(pitch.nameWithOctave[-1])
name = pitch.name.replace('-', 'es').replace('#', 'is').lower()
lily_octave = ''
if octave > 3:
lily_octave = (octave - 3) * "'"
elif octave < 3:
lily_octave = -1 * (octave - 3) * ','
try:
return name + lily_octave + str(length_convert[duration.type]) + '.' * duration.dots
except (ZeroDivisionError, KeyError):
return ''
def create_lilypond_file(notes: str) -> str:
""" Surround the provided text with a lilypond template
:param notes: the music to place in the template
:return: the full ready to compile piece of music
"""
return (fr"" "\n"
fr'\version "2.18.2"' "\n"
fr"" "\n"
fr"\paper {{" "\n"
fr' #(set-paper-size "letter")' "\n"
fr"}}" "\n"
fr"" "\n"
fr"\header {{" "\n"
fr' title = "Markov Music"' "\n"
fr' composer = "Danny Bramucci"' "\n"
fr" tagline = \markup {{" "\n"
fr" Engraved at" "\n"
fr' \simple #(strftime "%Y-%m-%d" (localtime (current-time)))' "\n"
fr" }}" "\n"
fr"}}" "\n"
fr"" "\n"
fr"\score{{" "\n"
fr"" "\n"
fr" \new Staff {{" "\n"
fr" \set Score.barNumberVisibility = #all-bar-numbers-visible" "\n"
fr' \new Voice = "rhythm"{{' "\n"
fr' \set midiInstrument = #"cello"' "\n"
fr" \tempo 4 = 120" "\n"
fr" \absolute{{" "\n"
fr" \time 4/4" "\n"
fr"\clef treble" "\n"
fr"{notes}" "\n"
fr" }}" "\n"
fr" }}" "\n"
fr" }}" "\n"
fr" \midi {{" "\n"
fr" \context {{" "\n"
fr" \Voice" "\n"
fr' \consists "Staff_performer"' "\n"
fr" }}" "\n"
fr" }}" "\n"
fr" \layout {{" "\n"
fr" #(layout-set-staff-size 25)" "\n"
fr" }}" "\n"
fr"}}" "\n"
fr" ")
def generate_rhythms(notes: List[Note], performed_pitches: List[Pitch]) -> NoteGen:
""" Generate notes based on a list of notes and the pitches that are to be used for these notes
:param notes: the notes to generate the relationships from
:param performed_pitches: the pitches to be used in the sequence of notes returned
:return: a generator of notes
"""
frequencies = {}
for pitch in {i.pitch for i in notes}:
frequencies[pitch] = Counter(i.duration for i in notes if i.pitch == pitch)
for pitch in performed_pitches:
count = frequencies[pitch]
possible_rhythms = list(count.keys())
weights = [count[beat] for beat in possible_rhythms]
yield Note(pitch, duration=random.choices(possible_rhythms, weights)[0])
def generate_random_music_by_notes(starting_notes: List[Note], length_of_history: int = 1,
mixup_period: int = None) -> NoteGen:
""" Generates notes based on the provided notes
:param starting_notes: the notes to inspire to new song
:param length_of_history: the length of history to base the decisions on
:param mixup_period: after this many notes, the generator will yield a completely random note
:return: a generator of notes
"""
yield from generate_random_stream(starting_notes, length_of_history, mixup_period)
def generate_random_music_independent_duration(starting_notes: List[Note], length_of_history: int = 1,
mixup_period: int = None) -> NoteGen:
""" Generates pitches and durations independently and then combines them into a series of notes
:param starting_notes: the notes to base the generator off of
:param length_of_history: the length of history to base decisions on
:param mixup_period: after this many notes, the generator will yield a completely random note
:return: a generator of notes
"""
pitches = generate_random_stream((n.pitch for n in starting_notes), length_of_history, mixup_period)
durations = generate_random_stream((n.duration for n in starting_notes), length_of_history, mixup_period)
yield from (Note(p, duration=d) for (p, d) in zip(pitches, durations))
def generate_random_music_dependent_duration(starting_notes: List[Note], length_of_history: int = 1,
mixup_period: int = None) -> NoteGen:
""" Generates pitches and then bases durations off of those notes
:param starting_notes: the notes to base the generator off of
:param length_of_history: the length of history to base decisions on
:param mixup_period: after this many notes, the generator will yield a completely random note
:return: a generator of notes
"""
pitches = generate_random_stream((n.pitch for n in starting_notes),
length_of_history, mixup_period)
yield from generate_rhythms(starting_notes, pitches)
def make_text(starting_notes: List[Note], strategy: Callable[[List[int], int, int], NoteGen],
length: int, length_of_history: int = 1, mixup_period: int = None) -> str:
""" Generates the lilypond file
:param starting_notes: The notes to base the new piece off of
:param strategy: a generator of notes that takes starting_notes, length_of_history, mixup_period as arguments
:param length: the length of the piece to create
:param length_of_history: the length of history to base decisions on
:param mixup_period: after this many notes, the music will randomly change
:return: the lilypond source for the new song
"""
lily_pond_notes = '\n'.join(
wrap(' '.join(convert_note_to_lilypond(n)
for n in islice(strategy(starting_notes, length_of_history, mixup_period), length)), width=80))
return create_lilypond_file(lily_pond_notes)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Generates music using Markov Chain inspired techniques")
parser.add_argument('file_in', help='This is the file you would like to use to learn from')
parser.add_argument('file_out', help='The name of the file to save to')
parser.add_argument('strategy', help='The strategy to generate the music')
parser.add_argument('length', help='The length of the piece to generate', type=int, default=250)
parser.add_argument('history_length', help='The length of history to base decisions on', type=int, default=2)
parser.add_argument('mixup_period',
help='The period that should pass before switching melodies randomly to prevent stallness',
type=int, default=None)
args = parser.parse_args()
file_in = args.file_in
file_out = args.file_out
strategy = args.strategy
length = args.length
history_length = args.history_length
mixup_period = args.mixup_period
if strategy == 'a' or strategy == 'note':
strategy = generate_random_music_by_notes
elif strategy == 'b' or strategy == 'dependent':
strategy = generate_random_music_dependent_duration
elif strategy == 'c' or strategy == 'independent':
strategy = generate_random_music_independent_duration
result = make_text(read_note_sequence(file_in), strategy, length, history_length, mixup_period)
with open(file_out, mode='w') as f:
f.write(result)