-
Notifications
You must be signed in to change notification settings - Fork 1
/
improv.py
353 lines (299 loc) · 17.3 KB
/
improv.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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
from music21 import *
from itertools import product
import os
import glob
import random
import shlex
class rhythm:
''' Our class rhythm stores the probability matrix of going from a note of
duration i to a note of duration j. When initalized it builds this
this information using data observed from the pieces provided in the
corpus. The function new_note gives the ability to input some note
duration and using the probabilities obtained from the corpus, produces
a new note duration. '''
matrix = []
def __init__ (self, music_directory):
''' When initialized, a rythm object will create a matrix, represented
as a list of lists that will contain the probabilities of going
from one note duration to another. Element i,j of this matrix will
represent going from a note of duration of i to a note of duration
j. These lengths are increments of a sixteenth note and range from
1 sixteenth note to a whole note. There is also a representation
for an eight triplet and a sixteenth triplet. This matrix will be
of size 19x18, a row and column for every duration and an extra
column for all the totals. Triplets are represented as one note.
'''
self.matrix = [[0 for x in range(19)] for y in range(18)] # fill matrix
# with 0's
# for every .xml file in our directory
for infile in glob.glob(os.path.join(music_directory,'*.xml')):
noteList = [] # running count of all the note durations
# encountered in the pieces, in order
part = converter.parse(infile)[1] # 0th index is metadeta
for measure in range (1,len(part)):
tripcount = 0 # we keep track of which triplet of the three
# we are currently looking at.
for note in part[measure].notesAndRests:
if (note.duration.quarterLength)*3 == 1: # if eigth triplet
if tripcount < 2:
tripcount += 1
else:
noteList.append(17)
tripcount = 0
elif (note.duration.quarterLength)*6 == 1: # if 16 triplet
if tripcount < 2:
tripcount += 1
else:
noteList.append(18)
tripcount = 0
# If any notes are shorter than a sixteenth note, round up
# to a sixteenth. If any notes are longer than a whole note
# round up to a whole
elif (note.duration.quarterLength) * 4 < 1:
noteList.append(1)
elif (note.duration.quarterLength) > 16:
noteList.append(16)
else:
noteList.append(int(note.duration.quarterLength*4))
for note in range(0,len(noteList)-1): # last note does is not
# followed by anything
self.matrix[noteList[note]-1][noteList[note+1]-1] += 1
self.matrix[noteList[note]-1][18] += 1 # increment total
for row in self.matrix: # for each row, divide each element i,j by the
# row total to get the probability of going
# from i to j.
if row [18] != 0:
for duration in range(0,len(row)-1):
row[duration] = float(row[duration])/float(row[18])
def new_note(self, old_note):
''' When given a note duration, new_note goes to that note's row in the
probability matrix and traverses it until, adding probabilities
as it goes, until it reaches a uniformly chosen number between
0 and 1. It returns this note as the next note of the sequence '''
randomNum = random.random()
total = 0
row = self.matrix[old_note-1] # we subtract one because the caller sees
# the sixteenth note as a value 1
# duration, but it is in our matrix's
# 0 row.
for index in range(0,len(row)-1):
if row[index] + total < randomNum: # stop when we have reached
# random probability
total += row[index]
else:
return index+1
break
class pitches:
''' The pitch class holds the matrices for each chord class. Initializing
a pitch object with a corpus will fill in these chord class matrices
with the probabilities of going from some interval in a chord class to
another. Pitch objects then have the ability to produce a new note when
given some note and a chord by looking into the corresponding chord
class probabalistically finding the next interval, and then outputting
the corresponding note for the specific class. '''
matrices = {"7": [], "fd": [], "aug7": [], "hd": [], "maj": [], "min": []}
def __init__ (self, music_directory):
'''
Initializing an instance of a pitch class will fill in the matrices
of each chord class. The rows and columns of this matrices represent
intervals within the chord class and element A[i][j] represents the
probability of going from interval i to interval j. These
probabilities are calculated by seeing what has historically
happened in the pieces of our corpus, which are obtained from the
music_directory argument. Each matrix is size 14x13. The last
column is reserved for storing how many intevals of that row have
been inputted into the matrix in total '''
for chordClass in self.matrices: # Initialize all of the matrices to
# 14x13 matrices filled with 0's
self.matrices[chordClass] = [[0 for x in range(14)] for y in range(13)]
# for every xml file in the directory
for infile in glob.glob(os.path.join(music_directory,'*.xml')):
# noteList stores a temporary run of intervals in each chord class
# that is later used to update the matrix
noteList = {"7": [], "fd": [], "aug7": [], \
"hd": [], "maj": [], "min": []}
chordFile = open(str(infile)[:-3] + 'txt') # open corresponding
# chord list.
print "Reading " + str(infile) + "..."
part = converter.parse(infile)[1] # open a music stream
chordTonic, chordClass, remainingLength = \
self.__parse_chord(chordFile)
oldTonic = chordTonic
for measure in range (1,len(part)): # iterate over all measures
tmpTonic = chordTonic
if measure > 1: # do not scan first chord twice
chordTonic, chordClass, remainingLength = \
self.__parse_chord(chordFile)
if chordTonic != oldTonic: # flush list on new chord
self.__list_to_matrix(noteList) # update matrix
noteList = {"7": [], "fd": [], "aug7": [], "hd": [], \
"maj": [], "min": []}
oldTonic = tmpTonic
for note in part[measure].notesAndRests: # for notes in measure
if note.offset >= remainingLength: # if chord < 1 measure,
# switch chords halfway
# through
tmpTonic = chordTonic
chordTonic, ChordClass, remainingLength = \
self.__parse_chord(chordFile)
if chordTonic != oldTonic: # flush list on new chord
self.__list_to_matrixs(noteList)
noteList = {"7": [], "fd": [], "aug7": [], \
"hd": [], "maj": [], "min": []}
oldTonic = tmpTonic
interval = self.__find_interval(chordTonic,note)
noteList[chordClass].append(interval)
self.__list_to_matrix(noteList)
chordFile.close()
self.__normalize_matrices()
def __parse_chord(self,chordFile):
''' Reads the a line from the chord file associated with a song and
parses it to produce the chord tonic, the chord class, and the
chord duration '''
chord = shlex.split(chordFile.readline())
return (chord[0],chord[1],chord[2])
def __find_interval(self,root,note):
''' This function finds the interval between the root of a given
chord and a note. If the note is a rest it returns an interval
of 12 '''
if note.isRest:
return 12
elif note.pitchClass - (pitch.Pitch(root)).pitchClass < 0:
return note.pitchClass - (pitch.Pitch(root)).pitchClass + 12
else:
return note.pitchClass - (pitch.Pitch(root)).pitchClass
def __list_to_matrix(self,noteList):
''' This function takes in the list containing the interval sequence
observed for some chord class at some time in one of the corpus
pieces. It scans through this interval sequence and it increments
element i,j of a matrix if it observes the interval sequence
i,j in our list '''
for chord in noteList:
for index in range(len(noteList[chord])-1): # leave out last note
# because it does not
# jump to anything
self.matrices[chord][noteList[chord][index]] \
[noteList[chord][index+1]] += 1
# increment total number of times this interval was updated
self.matrices[chord][noteList[chord][index]][13] += 1
def __normalize_matrices(self):
''' When this function is called all of the matrices contain the total
number of times in which some interval i jumped to j. This function
takes these counts and divides by the number of times i jumps to
anything. These floats represent the probabilities of jumping from
one interval to another '''
for chords in self.matrices:
for row in self.matrices[chords]:
if row[13] != 0: # avoid divide by 0 errors by not normalizing
# matrices that do not have anything in them.
for interval in range(len(row)-1):
row[interval] = float(row[interval])/float(row[13])
def new_note(self,old_note,chord_note,chord_type):
''' New_note takes a note and a chord and using the probability
matrices, randomly generates the next note. It does this by
checking the interval between the old note and the chord. It then
isolates the row of the corresponding interval in the appropiate
chord class matrix and traverses a the row, adding probabilities
as it goes, until it reaches a uniformly chosen number between
0 and 1. '''
randomNum = random.random()
total = 0
old_note = note.Note(old_note)
start_interval = self.__find_interval(chord_note, old_note) # find interval
row = self.matrices[chord_type][start_interval]
tonic_pitch = pitch.Pitch(chord_note).pitchClass
for index in range(0,len(row)-1):
if row[index] + total < randomNum: # traverse list until sum is
# greater than random number
total += row[index]
else:
if index==12: # if interval is rest interval
return 12
else:
return (index+tonic_pitch)%12 # return interval of the chord
break
class improv:
''' The improv class creates a rhythm and pitch object and uses as well as
a music21 stream. It creates the pitch and rhythm objects in the
initialization method using the provided corpus. Once the main function
has created an instance of the improv class, it can call the gen method
with a chord file which will generate a solo in the solo stream. This
strem is an attribute of the class so it can later be accessed and
shown through a music reader installed on the user's computer. '''
solo = stream.Stream()
pitchMatrix = []
rhythmMatrix = []
def __init__(self, directory):
''' The initialization function will create instances of the pitch and
rhythm classes so that they can be later used to generate solos '''
mm = tempo.MetronomeMark(number=160) # 160 temp
inst = instrument.Viola() # Instrument is tenor sax
self.solo.append(inst)
self.solo.append(mm)
self.pitchMatrix = pitches(directory)
self.rhythmMatrix = rhythm(directory)
def gen(self,chords):
''' This function reads the user's chord file and iterates over all
chords for all measures and creates notes from the rhythm and
pitch matrix that will fill the time needed for each measure '''
chordfile = open(str(chords))
chord = shlex.split(chordfile.readline()) # parse chord line by line
chord_tonic = chord[0]
chord_type = chord[1]
chord_duration = float(chord[2])
for rand in range(200): # cycle through pitches and rhythms so we have
# random starting point
length = self.rhythmMatrix.new_note(0)
pitch = self.pitchMatrix.new_note(0,chord_tonic,chord_type)
while (len(chord) == 3): # while we have a chord to improvise on
length = self.rhythmMatrix.new_note(length)
pitch = self.pitchMatrix.new_note(pitch,chord_tonic,chord_type)
if pitch == 12: # if pitch is a rest
nextNote = note.Rest()
else: # create a music21 object with correct pitch
nextNote = note.Note(pitch)
if length == 17: # if rhythm is an eight triplet we must add them
# into our piece in groups of three
nextNote.duration.quarterLength = 1.0/3.0
self.solo.append(nextNote)
pitch = self.pitchMatrix.new_note(pitch,chord_tonic,chord_type)
nextNote = note.Note(pitch)
nextNote.duration.quarterLength = 1.0/3.0
self.solo.append(nextNote)
pitch = self.pitchMatrix.new_note(pitch,chord_tonic,chord_type)
nextNote = note.Note(pitch)
nextNote.duration.quarterLength = 1.0/3.0
elif length == 18: # if rhpitchthm is a sixteenth triplet add them
# into our piece in groups of three
nextNote.duration.quarterLength = 1.0/6.0
self.solo.append(nextNote)
pitch = self.pitchMatrix.new_note(pitch,chord_tonic,chord_type)
nextNote = note.Note(pitch)
nextNote.duration.quarterLength = 1.0/6.0
self.solo.append(nextNote)
pitch = self.pitchMatrix.new_note(pitch,chord_tonic,chord_type)
nextNote = note.Note(pitch)
nextNote.duration.quarterLength = 1.0/6.0
else:
nextNote.duration.quarterLength = float(length)/4.0
self.solo.append(nextNote)
# decrement how much time how chord has left to played
chord_duration = float(chord_duration) - \
float(nextNote.duration.quarterLength)/4.0
if chord_duration <= 0: # if our chord is finished,
chord = shlex.split(chordfile.readline())
if (len(chord) == 3):
chord_tonic = chord[0]
chord_type = chord[1]
chord_duration = chord[2]
if __name__ == "__main__":
random.seed() # start by seeding the random number generator
corpusDirectory = raw_input("Input corpus directory (default 'data/Charts'): ")
if corpusDirectory == "":
corpusDirectory = "../data/Charts"
userChords = raw_input("Input chord file (default 'data/user_chords'): ")
if userChords == "":
userChords = "../data/user_chords"
improvisation = improv(corpusDirectory)
improvisation.gen(userChords)
improvisation.solo.show()